diff --git a/ink/brush/BUILD.bazel b/ink/brush/BUILD.bazel index 0c4330cb..6b1cc0d0 100644 --- a/ink/brush/BUILD.bazel +++ b/ink/brush/BUILD.bazel @@ -62,6 +62,7 @@ cc_test( deps = [ ":brush", ":brush_behavior", + ":brush_coat", ":brush_family", ":brush_paint", ":brush_tip", @@ -69,6 +70,7 @@ cc_test( ":type_matchers", "//ink/color", "//ink/geometry:angle", + "@com_google_absl//absl/hash:hash_testing", "@com_google_absl//absl/log:absl_check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", @@ -106,6 +108,7 @@ cc_test( ":fuzz_domains", "//ink/geometry:mesh_format", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/hash:hash_testing", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", "@com_google_absl//absl/strings", @@ -158,6 +161,8 @@ cc_test( "//ink/geometry:angle", "//ink/geometry:point", "//ink/types:duration", + "@com_google_absl//absl/hash", + "@com_google_absl//absl/hash:hash_testing", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", "@com_google_absl//absl/status:statusor", diff --git a/ink/brush/brush.h b/ink/brush/brush.h index 21d98cc4..fe1c54a0 100644 --- a/ink/brush/brush.h +++ b/ink/brush/brush.h @@ -89,6 +89,14 @@ class Brush { sink.Append(brush.ToFormattedString()); } + bool operator==(const Brush& other) const = default; + + template + friend H AbslHashValue(H h, const Brush& brush) { + return H::combine(std::move(h), brush.family_, brush.color_, brush.size_, + brush.epsilon_); + } + private: Brush(const BrushFamily& family, const Color& color, float size, float epsilon); diff --git a/ink/brush/brush_behavior.h b/ink/brush/brush_behavior.h index 520c2eae..99cafe34 100644 --- a/ink/brush/brush_behavior.h +++ b/ink/brush/brush_behavior.h @@ -16,6 +16,7 @@ #define INK_STROKES_BRUSH_BRUSH_BEHAVIOR_H_ #include +#include #include #include #include @@ -378,6 +379,12 @@ struct BrushBehavior { friend bool operator==(const EnabledToolTypes&, const EnabledToolTypes&) = default; + + template + friend H AbslHashValue(H h, const EnabledToolTypes& ett) { + return H::combine(std::move(h), ett.unknown, ett.mouse, ett.touch, + ett.stylus); + } }; static constexpr EnabledToolTypes kAllToolTypes = { @@ -461,6 +468,12 @@ struct BrushBehavior { std::array source_value_range; friend bool operator==(const SourceNode&, const SourceNode&) = default; + + template + friend H AbslHashValue(H h, const SourceNode& sn) { + return H::combine(std::move(h), sn.source, + sn.source_out_of_range_behavior, sn.source_value_range); + } }; // Value node for producing a constant value. @@ -471,6 +484,11 @@ struct BrushBehavior { float value; friend bool operator==(const ConstantNode&, const ConstantNode&) = default; + + template + friend H AbslHashValue(H h, const ConstantNode& cn) { + return H::combine(std::move(h), cn.value); + } }; // Value node for producing a continuous random noise function with values @@ -492,6 +510,11 @@ struct BrushBehavior { float base_period; friend bool operator==(const NoiseNode&, const NoiseNode&) = default; + + template + friend H AbslHashValue(H h, const NoiseNode& nn) { + return H::combine(std::move(h), nn.seed, nn.vary_over, nn.base_period); + } }; ////////////////////////// @@ -509,6 +532,11 @@ struct BrushBehavior { friend bool operator==(const ToolTypeFilterNode&, const ToolTypeFilterNode&) = default; + + template + friend H AbslHashValue(H h, const ToolTypeFilterNode& ttfn) { + return H::combine(std::move(h), ttfn.enabled_tool_types); + } }; //////////////////////////// @@ -534,6 +562,11 @@ struct BrushBehavior { float damping_gap; friend bool operator==(const DampingNode&, const DampingNode&) = default; + + template + friend H AbslHashValue(H h, const DampingNode& dn) { + return H::combine(std::move(h), dn.damping_source, dn.damping_gap); + } }; // Value node for mapping a value through a response curve. @@ -546,6 +579,11 @@ struct BrushBehavior { EasingFunction response_curve; friend bool operator==(const ResponseNode&, const ResponseNode&) = default; + + template + friend H AbslHashValue(H h, const ResponseNode& rn) { + return H::combine(std::move(h), rn.response_curve); + } }; // Value node for integrating an input value over time or distance. @@ -571,6 +609,13 @@ struct BrushBehavior { std::array integral_value_range; friend bool operator==(const IntegralNode&, const IntegralNode&) = default; + + template + friend H AbslHashValue(H h, const IntegralNode& in) { + return H::combine(std::move(h), in.integrate_over, + in.integral_out_of_range_behavior, + in.integral_value_range); + } }; // Value node for combining two other values with a binary operation. @@ -583,6 +628,11 @@ struct BrushBehavior { BinaryOp operation; friend bool operator==(const BinaryOpNode&, const BinaryOpNode&) = default; + + template + friend H AbslHashValue(H h, const BinaryOpNode& bon) { + return H::combine(std::move(h), bon.operation); + } }; // Value node for interpolating to/from a range of two values. @@ -596,6 +646,11 @@ struct BrushBehavior { friend bool operator==(const InterpolationNode&, const InterpolationNode&) = default; + + template + friend H AbslHashValue(H h, const InterpolationNode& in) { + return H::combine(std::move(h), in.interpolation); + } }; ////////////////////// @@ -617,6 +672,11 @@ struct BrushBehavior { std::array target_modifier_range; friend bool operator==(const TargetNode&, const TargetNode&) = default; + + template + friend H AbslHashValue(H h, const TargetNode& tn) { + return H::combine(std::move(h), tn.target, tn.target_modifier_range); + } }; // Terminal node that consumes two input values (angle and magnitude), forming @@ -638,6 +698,12 @@ struct BrushBehavior { friend bool operator==(const PolarTargetNode&, const PolarTargetNode&) = default; + + template + friend H AbslHashValue(H h, const PolarTargetNode& ptn) { + return H::combine(std::move(h), ptn.target, ptn.angle_range, + ptn.magnitude_range); + } }; // A single node in a behavior's graph. Each node type is either a "value @@ -659,6 +725,11 @@ struct BrushBehavior { std::string developer_comment; friend bool operator==(const BrushBehavior&, const BrushBehavior&) = default; + + template + friend H AbslHashValue(H h, const BrushBehavior& behavior) { + return H::combine(std::move(h), behavior.nodes, behavior.developer_comment); + } }; namespace brush_internal { diff --git a/ink/brush/brush_coat.h b/ink/brush/brush_coat.h index c882a562..f59d28b6 100644 --- a/ink/brush/brush_coat.h +++ b/ink/brush/brush_coat.h @@ -39,6 +39,13 @@ namespace ink { struct BrushCoat { BrushTip tip; absl::InlinedVector paint_preferences = {BrushPaint{}}; + + bool operator==(const BrushCoat&) const = default; + + template + friend H AbslHashValue(H h, const BrushCoat& coat) { + return H::combine(std::move(h), coat.tip, coat.paint_preferences); + } }; namespace brush_internal { diff --git a/ink/brush/brush_coat_test.cc b/ink/brush/brush_coat_test.cc index aafd1082..41129f78 100644 --- a/ink/brush/brush_coat_test.cc +++ b/ink/brush/brush_coat_test.cc @@ -20,6 +20,7 @@ #include "gtest/gtest.h" #include "fuzztest/fuzztest.h" #include "absl/container/flat_hash_set.h" +#include "absl/hash/hash_testing.h" #include "absl/status/status.h" #include "absl/status/status_matchers.h" #include "absl/strings/str_cat.h" @@ -42,6 +43,43 @@ using ::testing::UnorderedElementsAre; constexpr absl::string_view kTestTextureId = "test-paint"; +TEST(BrushCoatTest, Equality) { + BrushCoat coat1, coat2; + EXPECT_EQ(coat1, coat2); + + coat1.tip.pinch = 0.5; + EXPECT_NE(coat1, coat2); + coat2.tip.pinch = 0.5; + EXPECT_EQ(coat1, coat2); + + coat1.paint_preferences.push_back(BrushPaint{}); + EXPECT_NE(coat1, coat2); + coat2.paint_preferences.push_back(BrushPaint{}); + EXPECT_EQ(coat1, coat2); + + coat1.paint_preferences[0] = + BrushPaint{.self_overlap = BrushPaint::SelfOverlap::kDiscard}; + EXPECT_NE(coat1, coat2); + coat2.paint_preferences[0] = + BrushPaint{.self_overlap = BrushPaint::SelfOverlap::kDiscard}; + EXPECT_EQ(coat1, coat2); +} + +TEST(BrushCoatTest, AbslHash) { + BrushPaint paint_a = {.self_overlap = BrushPaint::SelfOverlap::kDiscard}; + BrushPaint paint_b = {}; + BrushTip tip_a = {.pinch = 0.1f}; + BrushTip tip_b = {.pinch = 0.2f}; + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {BrushCoat{}, BrushCoat{.tip = tip_a}, BrushCoat{.tip = tip_b}, + BrushCoat{.paint_preferences = {paint_a}}, + BrushCoat{.paint_preferences = {paint_b}}, + BrushCoat{.paint_preferences = {paint_a, paint_b}}, + BrushCoat{.tip = tip_a, .paint_preferences = {paint_a}}, + BrushCoat{.tip = tip_b, .paint_preferences = {paint_b}}})); +} + TEST(BrushCoatTest, Stringify) { EXPECT_EQ(absl::StrCat(BrushCoat{.tip = BrushTip{}}), "BrushCoat{tip=BrushTip{scale=<1, 1>, corner_rounding=1}, " diff --git a/ink/brush/brush_family.h b/ink/brush/brush_family.h index 2888ca14..7221ba27 100644 --- a/ink/brush/brush_family.h +++ b/ink/brush/brush_family.h @@ -42,7 +42,13 @@ class BrushFamily { // the modeled inputs. This can be useful as a point of comparison for other // input models, or for callers who wish to do their own input modeling prior // to passing inputs into Ink. - struct PassthroughModel {}; + struct PassthroughModel { + bool operator==(const PassthroughModel&) const = default; + template + friend H AbslHashValue(H h, const PassthroughModel&) { + return h; + } + }; // Averages nearby inputs together within a sliding time window. To be valid, // the window size must be finite and strictly positive, and the upsampling @@ -57,6 +63,14 @@ class BrushFamily { // inserted between them. Set this to `Duration32::Infinite()` to disable // upsampling. Duration32 upsampling_period = Duration32::Seconds(1.0 / 180.0); + + bool operator==(const SlidingWindowModel&) const = default; + + template + friend H AbslHashValue(H h, const SlidingWindowModel& model) { + return H::combine(std::move(h), model.window_size, + model.upsampling_period); + } }; // Specifies a model for turning a sequence of raw hardware inputs (e.g. from @@ -86,6 +100,12 @@ class BrushFamily { std::string developer_comment; bool operator==(const Metadata&) const = default; + + template + friend H AbslHashValue(H h, const Metadata& metadata) { + return H::combine(std::move(h), metadata.client_brush_family_id, + metadata.developer_comment); + } }; // Returns the default `InputModel` that will be used by @@ -160,6 +180,15 @@ class BrushFamily { sink.Append(family.ToFormattedString()); } + bool operator==(const BrushFamily& other) const = default; + + template + friend H AbslHashValue(H h, const BrushFamily& family) { + return H::combine(std::move(h), family.coats_, family.input_model_, + family.metadata_, + family.opaque_decoded_proto_bytes_with_fallbacks_); + } + friend class BrushFamilyInternalAccessor; private: diff --git a/ink/brush/brush_family_test.cc b/ink/brush/brush_family_test.cc index 9e149e43..c861d207 100644 --- a/ink/brush/brush_family_test.cc +++ b/ink/brush/brush_family_test.cc @@ -22,6 +22,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "fuzztest/fuzztest.h" +#include "absl/hash/hash_testing.h" #include "absl/status/status.h" #include "absl/status/status_matchers.h" #include "absl/status/statusor.h" @@ -100,6 +101,73 @@ BrushCoat CreateTestCoat() { }; } +TEST(BrushFamilyTest, Equality) { + absl::StatusOr family1 = BrushFamily::Create({}); + ASSERT_THAT(family1, IsOk()); + absl::StatusOr family2 = BrushFamily::Create({}); + ASSERT_THAT(family2, IsOk()); + EXPECT_EQ(*family1, *family2); + + family1 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.5f}}}, + BrushFamily::SlidingWindowModel{.window_size = Duration32::Millis(1)}, + {.client_brush_family_id = "family1"}); + ASSERT_THAT(family1, IsOk()); + EXPECT_NE(*family1, *family2); + family2 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.5f}}}, + BrushFamily::SlidingWindowModel{.window_size = Duration32::Millis(1)}, + {.client_brush_family_id = "family1"}); + ASSERT_THAT(family2, IsOk()); + EXPECT_EQ(*family1, *family2); + + // Different coats should make families unequal. + family2 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.6f}}}, + BrushFamily::SlidingWindowModel{.window_size = Duration32::Millis(1)}, + {.client_brush_family_id = "family1"}); + ASSERT_THAT(family2, IsOk()); + EXPECT_NE(*family1, *family2); + + // Different input model should make families unequal. + family2 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.5f}}}, + BrushFamily::SlidingWindowModel{.window_size = Duration32::Millis(2)}, + {.client_brush_family_id = "family1"}); + ASSERT_THAT(family2, IsOk()); + EXPECT_NE(*family1, *family2); + + // Different metadata should make families unequal. + family2 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.5f}}}, + BrushFamily::SlidingWindowModel{.window_size = Duration32::Millis(1)}, + {.client_brush_family_id = "family2"}); + ASSERT_THAT(family2, IsOk()); + EXPECT_NE(*family1, *family2); +} + +TEST(BrushFamilyTest, AbslHash) { + absl::StatusOr family_default = BrushFamily::Create({}); + ASSERT_THAT(family_default, IsOk()); + absl::StatusOr family1 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.1f}}}, + BrushFamily::SlidingWindowModel{.window_size = Duration32::Millis(1)}, + {.client_brush_family_id = "family1"}); + ASSERT_THAT(family1, IsOk()); + absl::StatusOr family2 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.2f}}}, + BrushFamily::SlidingWindowModel{.window_size = Duration32::Millis(2)}, + {.client_brush_family_id = "family2"}); + ASSERT_THAT(family2, IsOk()); + absl::StatusOr family3 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.1f}}, BrushCoat{.tip = {.pinch = 0.2f}}}, + BrushFamily::PassthroughModel{}, {.developer_comment = "comment"}); + ASSERT_THAT(family3, IsOk()); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {*family_default, *family1, *family2, *family3})); +} + TEST(BrushFamilyTest, StringifyInputModel) { EXPECT_EQ( absl::StrCat(BrushFamily::InputModel{BrushFamily::PassthroughModel{}}), diff --git a/ink/brush/brush_test.cc b/ink/brush/brush_test.cc index a7cdc964..52fc59c3 100644 --- a/ink/brush/brush_test.cc +++ b/ink/brush/brush_test.cc @@ -21,6 +21,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/hash/hash_testing.h" #include "absl/log/absl_check.h" #include "absl/status/status.h" #include "absl/status/status_matchers.h" @@ -28,6 +29,7 @@ #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "ink/brush/brush_behavior.h" +#include "ink/brush/brush_coat.h" #include "ink/brush/brush_family.h" #include "ink/brush/brush_paint.h" #include "ink/brush/brush_tip.h" @@ -84,6 +86,65 @@ BrushFamily CreateTestFamily() { return *family; } +TEST(BrushTest, Equality) { + absl::StatusOr brush1 = + Brush::Create(CreateTestFamily(), Color::Black(), 1, 1); + ASSERT_THAT(brush1, IsOk()); + absl::StatusOr brush2 = + Brush::Create(CreateTestFamily(), Color::Black(), 1, 1); + ASSERT_THAT(brush2, IsOk()); + EXPECT_EQ(*brush1, *brush2); + + absl::StatusOr family2 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.5f}}}, BrushFamily::DefaultInputModel(), + {.client_brush_family_id = "f2"}); + ASSERT_THAT(family2, IsOk()); + brush2->SetFamily(*family2); + EXPECT_NE(*brush1, *brush2); + brush1->SetFamily(*family2); + EXPECT_EQ(*brush1, *brush2); + + brush2->SetColor(Color::Blue()); + EXPECT_NE(*brush1, *brush2); + brush1->SetColor(Color::Blue()); + EXPECT_EQ(*brush1, *brush2); + + EXPECT_THAT(brush2->SetSize(2), IsOk()); + EXPECT_NE(*brush1, *brush2); + EXPECT_THAT(brush1->SetSize(2), IsOk()); + EXPECT_EQ(*brush1, *brush2); + + EXPECT_THAT(brush2->SetEpsilon(0.5), IsOk()); + EXPECT_NE(*brush1, *brush2); + EXPECT_THAT(brush1->SetEpsilon(0.5), IsOk()); + EXPECT_EQ(*brush1, *brush2); +} + +TEST(BrushTest, AbslHash) { + absl::StatusOr family1 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.1f}}}, BrushFamily::DefaultInputModel(), + {.client_brush_family_id = "f1"}); + ASSERT_THAT(family1, IsOk()); + absl::StatusOr family2 = BrushFamily::Create( + {BrushCoat{.tip = {.pinch = 0.2f}}}, BrushFamily::DefaultInputModel(), + {.client_brush_family_id = "f2"}); + ASSERT_THAT(family2, IsOk()); + + absl::StatusOr brush_default = + Brush::Create(BrushFamily{}, Color::Black(), 1, 1); + ASSERT_THAT(brush_default, IsOk()); + absl::StatusOr brush1 = Brush::Create(*family1, Color::Red(), 2, 0.2); + ASSERT_THAT(brush1, IsOk()); + absl::StatusOr brush2 = + Brush::Create(*family2, Color::Green(), 3, 0.3); + ASSERT_THAT(brush2, IsOk()); + absl::StatusOr brush3 = Brush::Create(*family1, Color::Blue(), 4, 0.4); + ASSERT_THAT(brush3, IsOk()); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {*brush_default, *brush1, *brush2, *brush3})); +} + TEST(BrushTest, Stringify) { absl::StatusOr family = BrushFamily::Create(BrushTip{.scale = {3, 3}, .corner_rounding = 0}, diff --git a/ink/brush/brush_tip.h b/ink/brush/brush_tip.h index c3c86648..4b489647 100644 --- a/ink/brush/brush_tip.h +++ b/ink/brush/brush_tip.h @@ -114,6 +114,13 @@ struct BrushTip { std::vector behaviors; friend bool operator==(const BrushTip&, const BrushTip&) = default; + + template + friend H AbslHashValue(H h, const BrushTip& tip) { + return H::combine(std::move(h), tip.scale, tip.corner_rounding, tip.slant, + tip.pinch, tip.rotation, tip.particle_gap_distance_scale, + tip.particle_gap_duration, tip.behaviors); + } }; namespace brush_internal { diff --git a/ink/brush/easing_function.h b/ink/brush/easing_function.h index 9e5275cb..63efe14b 100644 --- a/ink/brush/easing_function.h +++ b/ink/brush/easing_function.h @@ -15,6 +15,7 @@ #ifndef INK_STROKES_BRUSH_EASING_FUNCTION_H_ #define INK_STROKES_BRUSH_EASING_FUNCTION_H_ +#include #include #include #include @@ -81,6 +82,11 @@ struct EasingFunction { float y2; friend bool operator==(const CubicBezier&, const CubicBezier&) = default; + + template + friend H AbslHashValue(H h, const CubicBezier& cb) { + return H::combine(std::move(h), cb.x1, cb.y1, cb.x2, cb.y2); + } }; // Parameters for a custom piecewise-linear easing function. @@ -107,6 +113,11 @@ struct EasingFunction { std::vector points; friend bool operator==(const Linear&, const Linear&) = default; + + template + friend H AbslHashValue(H h, const Linear& l) { + return H::combine(std::move(h), l.points); + } }; // Setting to determine the desired output value of the first and last @@ -151,6 +162,11 @@ struct EasingFunction { StepPosition step_position; friend bool operator==(const Steps&, const Steps&) = default; + + template + friend H AbslHashValue(H h, const Steps& s) { + return H::combine(std::move(h), s.step_count, s.step_position); + } }; // Union of possible easing function parameters. @@ -160,6 +176,11 @@ struct EasingFunction { friend bool operator==(const EasingFunction&, const EasingFunction&) = default; + + template + friend H AbslHashValue(H h, const EasingFunction& func) { + return H::combine(std::move(h), func.parameters); + } }; namespace brush_internal { diff --git a/ink/geometry/BUILD.bazel b/ink/geometry/BUILD.bazel index 37e4e30f..51afe3d5 100644 --- a/ink/geometry/BUILD.bazel +++ b/ink/geometry/BUILD.bazel @@ -571,6 +571,8 @@ cc_test( ":rect", ":type_matchers", "//ink/geometry/internal:mesh_packing", + "@com_google_absl//absl/hash", + "@com_google_absl//absl/hash:hash_testing", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", "@com_google_absl//absl/status:statusor", @@ -683,6 +685,7 @@ cc_test( ":triangle", ":type_matchers", "@com_google_absl//absl/container:inlined_vector", + "@com_google_absl//absl/hash:hash_testing", "@com_google_absl//absl/log:absl_check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", diff --git a/ink/geometry/mesh.h b/ink/geometry/mesh.h index 89790ae2..c6286ef2 100644 --- a/ink/geometry/mesh.h +++ b/ink/geometry/mesh.h @@ -122,6 +122,13 @@ class Mesh { Mesh(const Mesh&) = default; Mesh& operator=(const Mesh&) = default; + bool operator==(const Mesh& other) const = default; + + template + friend H AbslHashValue(H h, const Mesh& mesh) { + return H::combine(std::move(h), mesh.data_.get()); + } + // Returns the number of vertices in the mesh. uint32_t VertexCount() const { ABSL_DCHECK_EQ(data_->vertex_data.size() % VertexStride(), 0u); diff --git a/ink/geometry/mesh_test.cc b/ink/geometry/mesh_test.cc index 5b54ca82..a438661b 100644 --- a/ink/geometry/mesh_test.cc +++ b/ink/geometry/mesh_test.cc @@ -24,6 +24,8 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/hash/hash.h" +#include "absl/hash/hash_testing.h" #include "absl/status/status.h" #include "absl/status/status_matchers.h" #include "absl/status/statusor.h" @@ -1000,5 +1002,37 @@ TEST(MeshTest, CreateFromQuantizedDataErrorsWithAttributeOutOfBounds) { StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("range"))); } +TEST(MeshTest, EqualityAndHashing) { + absl::StatusOr m1 = Mesh::Create(MeshFormat(), + {// Position + {5, 10, 20}, + {50, -30, 12}}, + // Triangles + {0, 1, 2}); + ASSERT_THAT(m1, IsOk()); + + // Copy constructor should share data. + Mesh m2 = *m1; + EXPECT_EQ(*m1, m2); + EXPECT_EQ(absl::HashOf(*m1), absl::HashOf(m2)); + + // New instance with same content should NOT be equal because pointers differ + // (shallow equality). + absl::StatusOr m3 = Mesh::Create(MeshFormat(), + {// Position + {5, 10, 20}, + {50, -30, 12}}, + // Triangles + {0, 1, 2}); + ASSERT_THAT(m3, IsOk()); + EXPECT_NE(*m1, *m3); + + absl::StatusOr m4 = Mesh::Create(MeshFormat(), {{}, {}}, {}); + ASSERT_THAT(m4, IsOk()); + + EXPECT_TRUE( + absl::VerifyTypeImplementsAbslHashCorrectly({*m1, m2, *m3, *m4, Mesh()})); +} + } // namespace } // namespace ink diff --git a/ink/geometry/partitioned_mesh.h b/ink/geometry/partitioned_mesh.h index 9e867d8d..4660bf04 100644 --- a/ink/geometry/partitioned_mesh.h +++ b/ink/geometry/partitioned_mesh.h @@ -151,6 +151,16 @@ class PartitionedMesh { PartitionedMesh& operator=(const PartitionedMesh&) = default; PartitionedMesh& operator=(PartitionedMesh&&) = default; + // Compare by pointer equality, not the contents of the data. Notably for this + // class, the default deep equality would take into account the spatial index, + // which is not desirable as it is derived state. + bool operator==(const PartitionedMesh& other) const = default; + + template + friend H AbslHashValue(H h, const PartitionedMesh& mesh) { + return H::combine(std::move(h), mesh.data_.get()); + } + // Returns the number of render groups in this modeled shape. uint32_t RenderGroupCount() const; diff --git a/ink/geometry/partitioned_mesh_test.cc b/ink/geometry/partitioned_mesh_test.cc index 2aa18a9d..d0999b55 100644 --- a/ink/geometry/partitioned_mesh_test.cc +++ b/ink/geometry/partitioned_mesh_test.cc @@ -22,6 +22,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/container/inlined_vector.h" +#include "absl/hash/hash_testing.h" #include "absl/log/absl_check.h" #include "absl/status/status.h" #include "absl/status/status_matchers.h" @@ -57,6 +58,41 @@ using ::testing::IsEmpty; using ::testing::SizeIs; using ::testing::UnorderedElementsAre; +TEST(PartitionedMeshTest, Equality) { + PartitionedMesh shape1; + PartitionedMesh shape2; + EXPECT_EQ(shape1, shape2); + + absl::StatusOr shape1_or = + PartitionedMesh::FromMutableMesh(MakeStraightLineMutableMesh(10)); + ASSERT_THAT(shape1_or, IsOk()); + shape1 = *shape1_or; + EXPECT_NE(shape1, shape2); + shape2 = shape1; + EXPECT_EQ(shape1, shape2); + + absl::StatusOr shape3_status = + PartitionedMesh::FromMutableMesh(MakeStraightLineMutableMesh(10)); + ASSERT_THAT(shape3_status, IsOk()); + PartitionedMesh shape3 = *shape3_status; + EXPECT_NE(shape1, shape3); +} + +TEST(PartitionedMeshTest, AbslHash) { + absl::StatusOr shape1 = + PartitionedMesh::FromMutableMesh(MakeStraightLineMutableMesh(1)); + ASSERT_THAT(shape1, IsOk()); + absl::StatusOr shape2 = + PartitionedMesh::FromMutableMesh(MakeStraightLineMutableMesh(2)); + ASSERT_THAT(shape2, IsOk()); + absl::StatusOr shape3 = + PartitionedMesh::FromMutableMesh(MakeStraightLineMutableMesh(3)); + ASSERT_THAT(shape3, IsOk()); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {PartitionedMesh(), *shape1, *shape2, *shape3, *shape1})); +} + TEST(PartitionedMeshTest, DefaultCtor) { PartitionedMesh shape; diff --git a/ink/strokes/input/BUILD.bazel b/ink/strokes/input/BUILD.bazel index 4d0f8a1b..045682f4 100644 --- a/ink/strokes/input/BUILD.bazel +++ b/ink/strokes/input/BUILD.bazel @@ -81,6 +81,7 @@ cc_test( "//ink/geometry:angle", "//ink/types:duration", "//ink/types:physical_distance", + "@com_google_absl//absl/hash:hash_testing", "@com_google_absl//absl/strings", "@com_google_googletest//:gtest_main", ], @@ -97,6 +98,8 @@ cc_test( "//ink/geometry:angle", "//ink/types:duration", "//ink/types:physical_distance", + "@com_google_absl//absl/hash", + "@com_google_absl//absl/hash:hash_testing", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", "@com_google_absl//absl/status:statusor", diff --git a/ink/strokes/input/stroke_input.h b/ink/strokes/input/stroke_input.h index 91d10f84..0b414be7 100644 --- a/ink/strokes/input/stroke_input.h +++ b/ink/strokes/input/stroke_input.h @@ -85,6 +85,15 @@ struct StrokeInput { // is a separate condition from the orientation being indeterminant when // `tilt` is 0. Angle orientation = kNoOrientation; + + bool operator==(const StrokeInput&) const = default; + + template + friend H AbslHashValue(H h, const StrokeInput& input) { + return H::combine(std::move(h), input.tool_type, input.position, + input.elapsed_time, input.stroke_unit_length, + input.pressure, input.tilt, input.orientation); + } }; namespace stroke_input_internal { diff --git a/ink/strokes/input/stroke_input_batch.h b/ink/strokes/input/stroke_input_batch.h index cedd699d..05526404 100644 --- a/ink/strokes/input/stroke_input_batch.h +++ b/ink/strokes/input/stroke_input_batch.h @@ -217,6 +217,17 @@ class StrokeInputBatch { sink.Append(batch.ToFormattedString()); } + bool operator==(const StrokeInputBatch& rhs) const = default; + + template + friend H AbslHashValue(H h, const StrokeInputBatch& batch) { + h = H::combine(std::move(h), batch.size_, batch.tool_type_, + batch.stroke_unit_length_, batch.noise_seed_, + batch.has_pressure_, batch.has_tilt_, batch.has_orientation_, + batch.data_); + return h; + } + private: absl::Status PrepareForAppend(const StrokeInput& first_new_input); diff --git a/ink/strokes/input/stroke_input_batch_test.cc b/ink/strokes/input/stroke_input_batch_test.cc index 243d6f0c..55b0b614 100644 --- a/ink/strokes/input/stroke_input_batch_test.cc +++ b/ink/strokes/input/stroke_input_batch_test.cc @@ -21,6 +21,8 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "fuzztest/fuzztest.h" +#include "absl/hash/hash.h" +#include "absl/hash/hash_testing.h" #include "absl/status/status.h" #include "absl/status/status_matchers.h" #include "absl/status/statusor.h" @@ -1621,5 +1623,75 @@ TEST(StrokeInputBatchTest, AppendRangeWithIndexOutOfBounds) { ""); } +TEST(StrokeInputBatchTest, AbslHash) { + StrokeInputBatch empty_batch; + StrokeInputBatch one_input_batch; + ASSERT_THAT(one_input_batch.Append(MakeValidTestInput()), IsOk()); + absl::StatusOr multi_input_batch = + StrokeInputBatch::Create(MakeValidTestInputSequence()); + ASSERT_THAT(multi_input_batch, IsOk()); + absl::StatusOr different_tool_batch = + StrokeInputBatch::Create( + MakeValidTestInputSequence(StrokeInput::ToolType::kMouse)); + ASSERT_THAT(different_tool_batch, IsOk()); + std::vector no_pressure_inputs = MakeValidTestInputSequence(); + for (auto& input : no_pressure_inputs) { + input.pressure = StrokeInput::kNoPressure; + } + absl::StatusOr no_pressure_batch = + StrokeInputBatch::Create(no_pressure_inputs); + ASSERT_THAT(no_pressure_batch, IsOk()); + std::vector no_tilt_inputs = MakeValidTestInputSequence(); + for (auto& input : no_tilt_inputs) { + input.tilt = StrokeInput::kNoTilt; + } + absl::StatusOr no_tilt_batch = + StrokeInputBatch::Create(no_tilt_inputs); + ASSERT_THAT(no_tilt_batch, IsOk()); + std::vector no_orientation_inputs = MakeValidTestInputSequence(); + for (auto& input : no_orientation_inputs) { + input.orientation = StrokeInput::kNoOrientation; + } + absl::StatusOr no_orientation_batch = + StrokeInputBatch::Create(no_orientation_inputs); + ASSERT_THAT(no_orientation_batch, IsOk()); + std::vector different_stroke_unit_length_inputs = + MakeValidTestInputSequence(); + for (auto& input : different_stroke_unit_length_inputs) { + input.stroke_unit_length = PhysicalDistance::Centimeters(0.2); + } + absl::StatusOr different_stroke_unit_length_batch = + StrokeInputBatch::Create(different_stroke_unit_length_inputs); + ASSERT_THAT(different_stroke_unit_length_batch, IsOk()); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {empty_batch, one_input_batch, *multi_input_batch, *different_tool_batch, + *no_pressure_batch, *no_tilt_batch, *no_orientation_batch, + *different_stroke_unit_length_batch})); +} + +TEST(StrokeInputBatchTest, EqualityAndHashing) { + std::vector input_vector = MakeValidTestInputSequence(); + absl::StatusOr batch1 = + StrokeInputBatch::Create(input_vector); + ASSERT_THAT(batch1, IsOk()); + + // Copy constructor should share data. + StrokeInputBatch batch2 = *batch1; + EXPECT_EQ(*batch1, batch2); + EXPECT_EQ(absl::HashOf(*batch1), absl::HashOf(batch2)); + + // Deep copy should NOT share data. + StrokeInputBatch batch3 = batch1->MakeDeepCopy(); + EXPECT_NE(*batch1, batch3); + + // Modification should trigger copy-on-write and make them unequal. + StrokeInput input = MakeValidTestInput(); + input.elapsed_time = + input_vector.back().elapsed_time + Duration32::Seconds(1); + ASSERT_THAT(batch2.Append(input), IsOk()); + EXPECT_NE(*batch1, batch2); +} + } // namespace } // namespace ink diff --git a/ink/strokes/input/stroke_input_test.cc b/ink/strokes/input/stroke_input_test.cc index faae1cba..e5880b3f 100644 --- a/ink/strokes/input/stroke_input_test.cc +++ b/ink/strokes/input/stroke_input_test.cc @@ -15,6 +15,7 @@ #include "ink/strokes/input/stroke_input.h" #include "gtest/gtest.h" +#include "absl/hash/hash_testing.h" #include "absl/strings/str_cat.h" #include "ink/geometry/angle.h" #include "ink/types/duration.h" @@ -107,5 +108,44 @@ TEST(StrokeInputTest, NoOrientation) { EXPECT_FALSE(input.HasOrientation()); } +TEST(StrokeInputTest, EqualityAndHashing) { + StrokeInput input; + EXPECT_EQ(input, StrokeInput{}); + + StrokeInput input_with_tool_type = input; + input_with_tool_type.tool_type = StrokeInput::ToolType::kMouse; + EXPECT_NE(input, input_with_tool_type); + + StrokeInput input_with_position = input; + input_with_position.position = {1, 1}; + EXPECT_NE(input, input_with_position); + + StrokeInput input_with_time = input; + input_with_time.elapsed_time = Duration32::Seconds(1); + EXPECT_NE(input, input_with_time); + + StrokeInput input_with_stroke_unit_length = input; + input_with_stroke_unit_length.stroke_unit_length = + PhysicalDistance::Centimeters(1); + EXPECT_NE(input, input_with_stroke_unit_length); + + StrokeInput input_with_pressure = input; + input_with_pressure.pressure = 0.5; + EXPECT_NE(input, input_with_pressure); + + StrokeInput input_with_tilt = input; + input_with_tilt.tilt = Angle::Degrees(45); + EXPECT_NE(input, input_with_tilt); + + StrokeInput input_with_orientation = input; + input_with_orientation.orientation = Angle::Degrees(90); + EXPECT_NE(input, input_with_orientation); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {input, input_with_tool_type, input_with_position, input_with_time, + input_with_stroke_unit_length, input_with_pressure, input_with_tilt, + input_with_orientation})); +} + } // namespace } // namespace ink diff --git a/ink/types/internal/BUILD.bazel b/ink/types/internal/BUILD.bazel index 943ed5bc..953d30cb 100644 --- a/ink/types/internal/BUILD.bazel +++ b/ink/types/internal/BUILD.bazel @@ -33,6 +33,7 @@ cc_test( srcs = ["copy_on_write_test.cc"], deps = [ ":copy_on_write", + "@com_google_absl//absl/hash:hash_testing", "@com_google_googletest//:gtest_main", ], ) diff --git a/ink/types/internal/copy_on_write.h b/ink/types/internal/copy_on_write.h index 2e3f210c..71d251ac 100644 --- a/ink/types/internal/copy_on_write.h +++ b/ink/types/internal/copy_on_write.h @@ -106,6 +106,13 @@ class CopyOnWrite { return value_.get(); } + bool operator==(const CopyOnWrite& other) const = default; + + template + friend H AbslHashValue(H h, const CopyOnWrite& cow) { + return H::combine(std::move(h), cow.value_.get()); + } + private: absl_nullable std::shared_ptr value_; }; diff --git a/ink/types/internal/copy_on_write_test.cc b/ink/types/internal/copy_on_write_test.cc index 7060071e..fb73f437 100644 --- a/ink/types/internal/copy_on_write_test.cc +++ b/ink/types/internal/copy_on_write_test.cc @@ -19,6 +19,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/hash/hash_testing.h" namespace ink_internal { namespace { @@ -189,5 +190,32 @@ TEST(CopyOnWriteTest, ValueOnEmpty) { EXPECT_DEATH_IF_SUPPORTED(x.Value(), ""); } +TEST(CopyOnWriteTest, Equality) { + CopyOnWrite x1; + CopyOnWrite x2; + EXPECT_EQ(x1, x2); + + x1.Emplace(1); + EXPECT_NE(x1, x2); + x2 = x1; + EXPECT_EQ(x1, x2); + + CopyOnWrite x3; + x3.Emplace(1); + EXPECT_NE(x1, x3); +} + +TEST(CopyOnWriteTest, AbslHash) { + CopyOnWrite x1; + x1.Emplace(1); + CopyOnWrite x2; + x2.Emplace(2); + CopyOnWrite x3; + x3.Emplace(3); + + EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( + {CopyOnWrite(), x1, x2, x3, x1})); +} + } // namespace } // namespace ink_internal