diff --git a/ink/strokes/internal/BUILD.bazel b/ink/strokes/internal/BUILD.bazel index e2f9a3b1..d444cd99 100644 --- a/ink/strokes/internal/BUILD.bazel +++ b/ink/strokes/internal/BUILD.bazel @@ -806,6 +806,7 @@ cc_test( srcs = ["stroke_subtraction_test.cc"], deps = [ ":stroke_subtraction", + ":stroke_vertex", "//ink/geometry:affine_transform", "//ink/geometry:fuzz_domains", "//ink/geometry:mesh_format", diff --git a/ink/strokes/internal/brush_tip_extruder/BUILD.bazel b/ink/strokes/internal/brush_tip_extruder/BUILD.bazel index 477af7fa..df5c4445 100644 --- a/ink/strokes/internal/brush_tip_extruder/BUILD.bazel +++ b/ink/strokes/internal/brush_tip_extruder/BUILD.bazel @@ -80,6 +80,7 @@ cc_test( ":mutable_mesh_view", "//ink/geometry:mutable_mesh", "//ink/geometry:point", + "//ink/geometry:triangle", "//ink/geometry:type_matchers", "//ink/geometry:vec", "//ink/strokes/internal:stroke_vertex", diff --git a/ink/strokes/internal/brush_tip_extruder/derivative_calculator.cc b/ink/strokes/internal/brush_tip_extruder/derivative_calculator.cc index 5c2619df..1b4fe81c 100644 --- a/ink/strokes/internal/brush_tip_extruder/derivative_calculator.cc +++ b/ink/strokes/internal/brush_tip_extruder/derivative_calculator.cc @@ -358,18 +358,6 @@ void DerivativeCalculator::SaveSideMarginUpperBound(uint32_t index, namespace { -// Returns the segment starting at a triangle's `vertex_position`, and ending -// at `vertex_position +/- derivative` such that the segment is oriented away -// from the triangle's interior. -// -// `outset_sign` is expected to be the return value of -// `StrokeVertex::Label::DerivativeOutsetSign()`. -Segment MakeOutsetSegment(Point vertex_position, float outset_sign, - Vec derivative) { - return {.start = vertex_position, - .end = vertex_position + outset_sign * derivative}; -} - // Returns one of the segments that will be used to constrain vertex outsets. // // For a triangle, the returned segment will start at the given @@ -422,8 +410,22 @@ Point TrianglePosition(const Triangle& triangle, int vertex_index) { void DerivativeCalculator::AddMarginUpperBoundsForTriangle( const MutableMeshView& mesh, const std::array& triangle_indices) { - Triangle triangle = GetTriangleFromIndices(mesh, triangle_indices); + std::array outset_vectors; + for (int i = 0; i < 3; ++i) { + float outset_sign = + mesh.GetSideLabel(triangle_indices[i]).DerivativeOutsetSign(); + outset_vectors[i] = + outset_sign * mesh.GetSideDerivative(triangle_indices[i]); + } + std::array bounds = ComputeTriangleMarginUpperBounds( + GetTriangleFromIndices(mesh, triangle_indices), outset_vectors); + for (int i = 0; i < 3; ++i) { + SaveSideMarginUpperBound(triangle_indices[i], bounds[i]); + } +} +std::array DerivativeCalculator::ComputeTriangleMarginUpperBounds( + const Triangle& triangle, const std::array& outset_vectors) { // Degenerate triangles must be handled separately: if (triangle.SignedArea() == 0) { // Check if the triangle is degenerate, but no two vertices of the triangle @@ -431,15 +433,14 @@ void DerivativeCalculator::AddMarginUpperBoundsForTriangle( // collinear, so we need to set all of the side margins to 0. if (triangle.p0 != triangle.p1 && triangle.p0 != triangle.p2 && triangle.p1 != triangle.p2) { - for (int i = 0; i < 3; ++i) { - SaveSideMarginUpperBound(triangle_indices[i], 0); - } + return {0.0f, 0.0f, 0.0f}; } // Otherwise, since two of the vertices share the same position, we can skip // the entire triangle. Coincident vertices will be given the same // derivative values and be repositioned the same way in the shader, so the // degenerate triangle does not impact the margins of any of its vertices. - return; + return {StrokeVertex::kMaximumMargin, StrokeVertex::kMaximumMargin, + StrokeVertex::kMaximumMargin}; } // Each triangle splits its exterior into three regions according to the @@ -476,25 +477,24 @@ void DerivativeCalculator::AddMarginUpperBoundsForTriangle( MakeBoundingSegment(triangle.p1, triangle.GetEdge(2)), MakeBoundingSegment(triangle.p2, triangle.GetEdge(0))}; + std::array bounds = {StrokeVertex::kMaximumMargin, + StrokeVertex::kMaximumMargin, + StrokeVertex::kMaximumMargin}; for (int i = 0; i < 3; ++i) { - Point vertex_position = TrianglePosition(triangle, i); - float side_outset_sign = - mesh.GetSideLabel(triangle_indices[i]).DerivativeOutsetSign(); - if (side_outset_sign == 0) { - // If the outset sign is 0, this vertex should not be repositioned at + if (outset_vectors[i] == Vec{0, 0}) { + // If the outset vector is 0, this vertex should not be repositioned at // all, and the upper bound is 0. This happens for an interior vertex // label. - SaveSideMarginUpperBound(triangle_indices[i], 0); + bounds[i] = 0.0f; continue; } - Segment side_outset_segment = - MakeOutsetSegment(vertex_position, side_outset_sign, - mesh.GetSideDerivative(triangle_indices[i])); - float margin_upper_bound = std::min( - MarginUpperBound(side_outset_segment, bounding_segments[(i + 1) % 3]), - MarginUpperBound(side_outset_segment, bounding_segments[(i + 2) % 3])); - SaveSideMarginUpperBound(triangle_indices[i], margin_upper_bound); + Point pos = TrianglePosition(triangle, i); + Segment outset_segment = {.start = pos, .end = pos + outset_vectors[i]}; + bounds[i] = std::min( + MarginUpperBound(outset_segment, bounding_segments[(i + 1) % 3]), + MarginUpperBound(outset_segment, bounding_segments[(i + 2) % 3])); } + return bounds; } namespace { diff --git a/ink/strokes/internal/brush_tip_extruder/derivative_calculator.h b/ink/strokes/internal/brush_tip_extruder/derivative_calculator.h index 62719539..c444cc9b 100644 --- a/ink/strokes/internal/brush_tip_extruder/derivative_calculator.h +++ b/ink/strokes/internal/brush_tip_extruder/derivative_calculator.h @@ -20,6 +20,7 @@ #include #include "absl/types/span.h" +#include "ink/geometry/triangle.h" #include "ink/geometry/vec.h" #include "ink/strokes/internal/brush_tip_extruder/mutable_mesh_view.h" #include "ink/strokes/internal/stroke_vertex.h" @@ -82,6 +83,18 @@ class DerivativeCalculator { absl::Span right_indices_to_update, MutableMeshView& mesh); + // Computes the margin upper bounds imposed by `triangle` on its vertices + // given the vertex `outset_vectors`. + // + // The margins bound the extents to which vertices can safely be outset along + // their respective outset vectors without inverting the triangle. See + // `StrokeVertex::Label` for further background on margins. + // + // The margins computed by this function are approximate and clamped to the + // range [0, StrokeVertex::kMaximumMargin]. + static std::array ComputeTriangleMarginUpperBounds( + const Triangle& triangle, const std::array& outset_vectors); + private: // Prepares the tracked average derivatives and minimum margins for // calculating new values. The derivatives are zeroed out, and the margins are diff --git a/ink/strokes/internal/brush_tip_extruder/derivative_calculator_test.cc b/ink/strokes/internal/brush_tip_extruder/derivative_calculator_test.cc index 4481442f..3f01c474 100644 --- a/ink/strokes/internal/brush_tip_extruder/derivative_calculator_test.cc +++ b/ink/strokes/internal/brush_tip_extruder/derivative_calculator_test.cc @@ -14,6 +14,7 @@ #include "ink/strokes/internal/brush_tip_extruder/derivative_calculator.h" +#include #include #include @@ -22,6 +23,7 @@ #include "absl/types/span.h" #include "ink/geometry/mutable_mesh.h" #include "ink/geometry/point.h" +#include "ink/geometry/triangle.h" #include "ink/geometry/type_matchers.h" #include "ink/geometry/vec.h" #include "ink/strokes/internal/brush_tip_extruder/mutable_mesh_view.h" @@ -585,5 +587,58 @@ TEST(DerivativeCalculatorDeathTest, NonEmptyIndicesWithNoDataMeshView) { ""); } +TEST_F(DerivativeCalculatorTest, + ComputeTriangleMarginUpperBoundsUnconstrained) { + // 2 + // |\ + // | \ + // | \ + // | \ + // | \ + // 0-----1 - - o outset + // / + // o + // outset + + Triangle triangle = {.p0 = {0, 0}, .p1 = {10, 0}, .p2 = {0, 10}}; + std::array outset_vectors = {Vec{-1, -1}, Vec{1, 0}, Vec{0, 0}}; + + std::array bounds = + DerivativeCalculator::ComputeTriangleMarginUpperBounds(triangle, + outset_vectors); + + EXPECT_FLOAT_EQ(bounds[0], StrokeVertex::kMaximumMargin); + EXPECT_FLOAT_EQ(bounds[1], StrokeVertex::kMaximumMargin); + EXPECT_FLOAT_EQ(bounds[2], 0.0f); +} + +TEST_F(DerivativeCalculatorTest, ComputeTriangleMarginUpperBoundsConstrained) { + // Construct a triangle where vertex 0's outset ray crosses the bounding ray + // of vertex 1. + // . bounding ray of 1 + // . + // 0 - - - - - - - - - - - - - o outset + // / \ . + // / \ . + // / \ . + // / \ . + // / . \ + // / . \ + // / . \ + // /. \ + // 1-----------------2 + + Triangle triangle = {.p0 = {0, 10}, .p1 = {-10, 0}, .p2 = {10, 0}}; + std::array outset_vectors = {Vec{10, 0}, Vec{-10, 0}, Vec{10, 0}}; + + std::array bounds = + DerivativeCalculator::ComputeTriangleMarginUpperBounds(triangle, + outset_vectors); + + // Vertex 0's margin is clamped by the intersection with the bounding ray + // radiating from p1 through edge p0-p2 (at ratio 2/9). + EXPECT_FLOAT_EQ(bounds[0], 2.0f / 9.0f); +} + } // namespace } // namespace ink::brush_tip_extruder_internal diff --git a/ink/strokes/internal/stroke_subtraction.cc b/ink/strokes/internal/stroke_subtraction.cc index b667149b..084f5ce2 100644 --- a/ink/strokes/internal/stroke_subtraction.cc +++ b/ink/strokes/internal/stroke_subtraction.cc @@ -51,8 +51,8 @@ namespace ink::strokes_internal { namespace { -using AverageDerivative = - ::ink::brush_tip_extruder_internal::DerivativeCalculator::AverageDerivative; +using ::ink::brush_tip_extruder_internal::DerivativeCalculator; +using AverageDerivative = DerivativeCalculator::AverageDerivative; using ::ink::geometry_internal::ComputeSubtraction; using ::ink::geometry_internal::ComputeTriangulation; using ::ink::geometry_internal::Intersects; @@ -230,10 +230,22 @@ bool IsRight(BoundaryLabel label) { return label % 3 == 2; } bool IsFront(BoundaryLabel label) { return label / 3 == 1; } bool IsBack(BoundaryLabel label) { return label / 3 == 2; } +float SideOutsetSign(BoundaryLabel label) { + if (IsLeft(label)) return -1.0f; + if (IsRight(label)) return 1.0f; + return 0.0f; +} + +float ForwardOutsetSign(BoundaryLabel label) { + if (IsFront(label)) return -1.0f; + if (IsBack(label)) return 1.0f; + return 0.0f; +} + // Converts encoded float values to a BoundaryLabel enum value. BoundaryLabel DecodeBoundaryLabel(float side, float fwd) { - int side_idx = (side > 0.5f) ? 2 : (side < -0.5f ? 1 : 0); - int fwd_idx = (fwd > 0.5f) ? 2 : (fwd < -0.5f ? 1 : 0); + int side_idx = (side >= 0.5f) ? 2 : (side <= -0.5f ? 1 : 0); + int fwd_idx = (fwd >= 0.5f) ? 2 : (fwd <= -0.5f ? 1 : 0); return static_cast(3 * fwd_idx + side_idx); } @@ -465,6 +477,12 @@ class MeshBuilder { return {sd[0], sd[1]}; } + Vec GetForwardDerivative(uint32_t vertex_index) const { + auto fd = mutable_mesh_.FloatVertexAttribute( + vertex_index, attr_indices_.forward_derivative); + return {fd[0], fd[1]}; + } + void SetDerivatives(uint32_t vertex_index, Vec side, Vec forward) { // Override zero vectors with a small non-zero value to avoid undefined // zero-divided-by-zero in the rendering pipeline. @@ -477,6 +495,20 @@ class MeshBuilder { vertex_index, attr_indices_.forward_derivative, {forward.x, forward.y}); } + void SetMargins(uint32_t vertex_index, float side_margin, + float forward_margin) { + BoundaryLabel label = GetLabel(vertex_index); + auto [side, forward] = EncodeBoundaryLabel(label); + mutable_mesh_.SetFloatVertexAttribute( + vertex_index, attr_indices_.side_label, + {StrokeVertex::Label{side}.WithMargin(side_margin).encoded_value}); + mutable_mesh_.SetFloatVertexAttribute(vertex_index, + attr_indices_.forward_label, + {StrokeVertex::Label{forward} + .WithMargin(forward_margin) + .encoded_value}); + } + const MutableMesh& GetMesh() const { return mutable_mesh_; } const auto& GetEdgeTriangleAdjacencyMap() const { return edge_tri_adj_map_; } @@ -1006,6 +1038,59 @@ void ComputeAndSetDerivatives(MeshBuilder& mesh_builder) { forward_derivative[i].Value()); } } + +// Computes anti-aliasing margins for all vertices in the mesh. +void ComputeAndSetMargins(MeshBuilder& mesh_builder) { + // Recall that margins are used during rendering to constrain anti-aliasing + // vertex outsets and prevent self-overlap and triangle inversion. See the + // comments in `StrokeVertex::Label` and the implementation of + // `DerivativeCalculator::ComputeTriangleMarginUpperBounds` for details. + // + // In this function, we recompute the margins for all vertices by iterating + // through all triangles in the mesh. Each triangle imposes an upper bound + // margin on its vertices. We compute these per-triangle constraints, and + // assign each vertex the tightest (minimum) bound across its incident + // triangles, and then write the margins to the mesh. + // TODO(b/521448869): Recompute margins only for vertices in a neighborhood + // of the subtracted area. + const MutableMesh& mesh = mesh_builder.GetMesh(); + const uint32_t num_vertices = mesh.VertexCount(); + std::vector side_margins(num_vertices, StrokeVertex::kMaximumMargin); + std::vector forward_margins(num_vertices, + StrokeVertex::kMaximumMargin); + + for (uint32_t tri_idx = 0; tri_idx < mesh.TriangleCount(); ++tri_idx) { + std::array indices = mesh.TriangleIndices(tri_idx); + Triangle triangle = mesh.GetTriangle(tri_idx); + + std::array side_outsets, forward_outsets; + for (int i = 0; i < 3; ++i) { + BoundaryLabel label = mesh_builder.GetLabel(indices[i]); + side_outsets[i] = + SideOutsetSign(label) * mesh_builder.GetSideDerivative(indices[i]); + forward_outsets[i] = ForwardOutsetSign(label) * + mesh_builder.GetForwardDerivative(indices[i]); + } + + std::array side_bounds = + DerivativeCalculator::ComputeTriangleMarginUpperBounds(triangle, + side_outsets); + std::array forward_bounds = + DerivativeCalculator::ComputeTriangleMarginUpperBounds(triangle, + forward_outsets); + + for (int i = 0; i < 3; ++i) { + side_margins[indices[i]] = + std::min(side_margins[indices[i]], side_bounds[i]); + forward_margins[indices[i]] = + std::min(forward_margins[indices[i]], forward_bounds[i]); + } + } + + for (uint32_t i = 0; i < num_vertices; ++i) { + mesh_builder.SetMargins(i, side_margins[i], forward_margins[i]); + } +} // LINT.ThenChange( // //depot/google3/third_party/ink/strokes/internal/brush_tip_extruder/derivative_calculator.cc, // //depot/google3/third_party/ink/rendering/skia/common_internal/sksl_vertex_shader_helper_functions.h:calculate_antialiasing_and_position_outset, @@ -1100,6 +1185,7 @@ SubtractedMesh SubtractMeshes(absl::Span meshes, if (HasAntiAliasingAttributes(format) && anti_aliasing_enabled) { ComputeAndSetLabels(outlines, sub_mesh); ComputeAndSetDerivatives(sub_mesh); + ComputeAndSetMargins(sub_mesh); } return SubtractedMesh{ diff --git a/ink/strokes/internal/stroke_subtraction_test.cc b/ink/strokes/internal/stroke_subtraction_test.cc index 2c5baa5b..dd538d60 100644 --- a/ink/strokes/internal/stroke_subtraction_test.cc +++ b/ink/strokes/internal/stroke_subtraction_test.cc @@ -41,6 +41,7 @@ #include "ink/geometry/rect.h" #include "ink/geometry/triangle.h" #include "ink/geometry/vec.h" +#include "ink/strokes/internal/stroke_vertex.h" #include "ink/types/small_array.h" namespace ink::strokes_internal { @@ -114,8 +115,53 @@ void CheckVertexLabels(const Mesh& mesh, Point p, float expected_side, float expected_fwd) { std::optional idx = FindVertexIndex(mesh, p); ASSERT_TRUE(idx.has_value()); - EXPECT_FLOAT_EQ(mesh.FloatVertexAttribute(*idx, 2)[0], expected_side); - EXPECT_FLOAT_EQ(mesh.FloatVertexAttribute(*idx, 4)[0], expected_fwd); + EXPECT_EQ(StrokeVertex::Label{mesh.FloatVertexAttribute(*idx, 2)[0]} + .DecodeSideCategory(), + StrokeVertex::Label{expected_side}.DecodeSideCategory()); + EXPECT_EQ(StrokeVertex::Label{mesh.FloatVertexAttribute(*idx, 4)[0]} + .DecodeForwardCategory(), + StrokeVertex::Label{expected_fwd}.DecodeForwardCategory()); +} + +// Asserts that a vertex near `p` exists in `mesh` and has the expected side +// derivative (attribute 1). +void CheckVertexSideDerivative(const Mesh& mesh, Point p, Vec expected_side) { + std::optional idx = FindVertexIndex(mesh, p); + ASSERT_TRUE(idx.has_value()); + auto side = mesh.FloatVertexAttribute(*idx, 1); + EXPECT_NEAR(side[0], expected_side.x, 1e-3); + EXPECT_NEAR(side[1], expected_side.y, 1e-3); +} + +// Asserts that a vertex near `p` exists in `mesh` and has the expected forward +// derivative (attribute 3). +void CheckVertexForwardDerivative(const Mesh& mesh, Point p, Vec expected_fwd) { + std::optional idx = FindVertexIndex(mesh, p); + ASSERT_TRUE(idx.has_value()); + auto fwd = mesh.FloatVertexAttribute(*idx, 3); + EXPECT_NEAR(fwd[0], expected_fwd.x, 1e-3); + EXPECT_NEAR(fwd[1], expected_fwd.y, 1e-3); +} + +// Asserts that a vertex near `p` exists in `mesh` and has the expected decoded +// side margin (attribute 2). +void CheckVertexSideMargin(const Mesh& mesh, Point p, float expected_margin) { + std::optional idx = FindVertexIndex(mesh, p); + ASSERT_TRUE(idx.has_value()); + float side_label_val = mesh.FloatVertexAttribute(*idx, 2)[0]; + EXPECT_THAT(StrokeVertex::Label{side_label_val}.DecodeMargin(), + FloatNear(expected_margin, 1e-3f)); +} + +// Asserts that a vertex near `p` exists in `mesh` and has the expected decoded +// forward margin (attribute 4). +void CheckVertexForwardMargin(const Mesh& mesh, Point p, + float expected_margin) { + std::optional idx = FindVertexIndex(mesh, p); + ASSERT_TRUE(idx.has_value()); + float fwd_label_val = mesh.FloatVertexAttribute(*idx, 4)[0]; + EXPECT_THAT(StrokeVertex::Label{fwd_label_val}.DecodeMargin(), + FloatNear(expected_margin, 1e-3f)); } // Verifies that no interior edge (shared by multiple triangles) in `mesh` is @@ -977,36 +1023,19 @@ TEST(StrokeSubtractionTest, ComputeSideDerivatives) { const Mesh& result_mesh = result->RenderGroupMeshes(0)[0]; - auto check_labels = [&](Point point, float expected_side, - float expected_fwd) { - std::optional idx = FindVertexIndex(result_mesh, point); - ASSERT_TRUE(idx.has_value()); - ASSERT_FLOAT_EQ(result_mesh.FloatVertexAttribute(*idx, 2)[0], - expected_side); - ASSERT_FLOAT_EQ(result_mesh.FloatVertexAttribute(*idx, 4)[0], expected_fwd); - }; - - check_labels(A, kLeftLabel, kFrontLabel); - check_labels(D, kLeftLabel, kBackLabel); - check_labels(X3, kRightLabel, kBackLabel); - check_labels(X2, kRightLabel, kInteriorLabel); - check_labels(X1, kRightLabel, kFrontLabel); - - auto check_side_derivative = [&](Point point, Vec expected_side) { - std::optional idx = FindVertexIndex(result_mesh, point); - ASSERT_TRUE(idx.has_value()); - auto side = result_mesh.FloatVertexAttribute(*idx, 1); - EXPECT_NEAR(side[0], expected_side.x, 1e-3); - EXPECT_NEAR(side[1], expected_side.y, 1e-3); - }; + CheckVertexLabels(result_mesh, A, kLeftLabel, kFrontLabel); + CheckVertexLabels(result_mesh, D, kLeftLabel, kBackLabel); + CheckVertexLabels(result_mesh, X3, kRightLabel, kBackLabel); + CheckVertexLabels(result_mesh, X2, kRightLabel, kInteriorLabel); + CheckVertexLabels(result_mesh, X1, kRightLabel, kFrontLabel); // The remaining stroke has width = 5 so the recomputed side derivative across // all 5 vertices is exactly {5, 0}. - check_side_derivative(A, {5.0f, 0.0f}); - check_side_derivative(D, {5.0f, 0.0f}); - check_side_derivative(X1, {5.0f, 0.0f}); - check_side_derivative(X2, {5.0f, 0.0f}); - check_side_derivative(X3, {5.0f, 0.0f}); + CheckVertexSideDerivative(result_mesh, A, {5.0f, 0.0f}); + CheckVertexSideDerivative(result_mesh, D, {5.0f, 0.0f}); + CheckVertexSideDerivative(result_mesh, X1, {5.0f, 0.0f}); + CheckVertexSideDerivative(result_mesh, X2, {5.0f, 0.0f}); + CheckVertexSideDerivative(result_mesh, X3, {5.0f, 0.0f}); } TEST(StrokeSubtractionTest, ComputeForwardDerivatives) { @@ -1095,36 +1124,134 @@ TEST(StrokeSubtractionTest, ComputeForwardDerivatives) { const Mesh& result_mesh = result->RenderGroupMeshes(0)[0]; - auto check_labels = [&](Point point, float expected_side, - float expected_fwd) { - std::optional idx = FindVertexIndex(result_mesh, point); - ASSERT_TRUE(idx.has_value()); - ASSERT_FLOAT_EQ(result_mesh.FloatVertexAttribute(*idx, 2)[0], - expected_side); - ASSERT_FLOAT_EQ(result_mesh.FloatVertexAttribute(*idx, 4)[0], expected_fwd); - }; - - check_labels(A, kLeftLabel, kFrontLabel); - check_labels(B, kRightLabel, kFrontLabel); - check_labels(X1, kRightLabel, kBackLabel); - check_labels(X2, kInteriorLabel, kBackLabel); - check_labels(X3, kLeftLabel, kBackLabel); - - auto check_forward_derivative = [&](Point point, Vec expected_fwd) { - std::optional idx = FindVertexIndex(result_mesh, point); - ASSERT_TRUE(idx.has_value()); - auto fwd = result_mesh.FloatVertexAttribute(*idx, 3); - EXPECT_NEAR(fwd[0], expected_fwd.x, 1e-3); - EXPECT_NEAR(fwd[1], expected_fwd.y, 1e-3); - }; + CheckVertexLabels(result_mesh, A, kLeftLabel, kFrontLabel); + CheckVertexLabels(result_mesh, B, kRightLabel, kFrontLabel); + CheckVertexLabels(result_mesh, X1, kRightLabel, kBackLabel); + CheckVertexLabels(result_mesh, X2, kInteriorLabel, kBackLabel); + CheckVertexLabels(result_mesh, X3, kLeftLabel, kBackLabel); // The remaining stroke has length = 5 in y (from y=0 to y=5), so the // recomputed forward derivative across all 5 vertices is exactly {0, 5}. - check_forward_derivative(A, {0.0f, 5.0f}); - check_forward_derivative(B, {0.0f, 5.0f}); - check_forward_derivative(X1, {0.0f, 5.0f}); - check_forward_derivative(X2, {0.0f, 5.0f}); - check_forward_derivative(X3, {0.0f, 5.0f}); + CheckVertexForwardDerivative(result_mesh, A, {0.0f, 5.0f}); + CheckVertexForwardDerivative(result_mesh, B, {0.0f, 5.0f}); + CheckVertexForwardDerivative(result_mesh, X1, {0.0f, 5.0f}); + CheckVertexForwardDerivative(result_mesh, X2, {0.0f, 5.0f}); + CheckVertexForwardDerivative(result_mesh, X3, {0.0f, 5.0f}); +} + +TEST(StrokeSubtractionTest, ComputeMargins) { + // As with the anti-aliasing derivatives, the margins are not uniquely + // determined by the geometry of the subtraction -- they depend on the + // triangulation and boundary labels. We try to contrive a test case that + // minimizes the ambiguity: mesh_b bites a concave piece out of a triangle of + // mesh_a. + // + // J--------I + // / | + // D---------------C | + // | //| | + // | / / | | + // | / / | | + // | / / | | + // | / / | | stroke travel direction + // E F | mesh_b | ^ + // | \ \ | | | + // | \ \ | | +---> right + // | \ \ | | + // | \ \ | | + // | mesh_a \\| | + // A---------------B | + // \ | + // G--------H + + Point A{0, -2}, B{10, -2}, C{10, 2}, D{0, 2}, E{0, 0}; + Point F{2, 0}, G{14, -3}, H{16, -3}, I{16, 3}, J{14, 3}; + + absl::StatusOr format = MeshFormat::Create( + {{AttributeType::kFloat2Unpacked, AttributeId::kPosition}, + {AttributeType::kFloat2Unpacked, AttributeId::kSideDerivative}, + {AttributeType::kFloat1Unpacked, AttributeId::kSideLabel}, + {AttributeType::kFloat2Unpacked, AttributeId::kForwardDerivative}, + {AttributeType::kFloat1Unpacked, AttributeId::kForwardLabel}}, + IndexFormat::k32BitUnpacked16BitPacked); + ASSERT_THAT(format, IsOk()); + + // Set up mesh_a. + MutableMesh mesh_a(*format); + for (const Point& p : {A, B, C, D, E}) mesh_a.AppendVertex(p); + mesh_a.AppendTriangleIndices({4, 0, 1}); // EAB + mesh_a.AppendTriangleIndices({4, 1, 2}); // EBC + mesh_a.AppendTriangleIndices({4, 2, 3}); // ECD + mesh_a.SetFloatVertexAttribute(0, 2, {kLeftLabel}); + mesh_a.SetFloatVertexAttribute(0, 4, {kFrontLabel}); + mesh_a.SetFloatVertexAttribute(1, 2, {kRightLabel}); + mesh_a.SetFloatVertexAttribute(1, 4, {kFrontLabel}); + mesh_a.SetFloatVertexAttribute(2, 2, {kRightLabel}); + mesh_a.SetFloatVertexAttribute(2, 4, {kBackLabel}); + mesh_a.SetFloatVertexAttribute(3, 2, {kLeftLabel}); + mesh_a.SetFloatVertexAttribute(3, 4, {kBackLabel}); + mesh_a.SetFloatVertexAttribute(4, 2, {kLeftLabel}); + mesh_a.SetFloatVertexAttribute(4, 4, {kInteriorLabel}); + + for (uint32_t i = 0; i < 5; ++i) { + mesh_a.SetFloatVertexAttribute(i, 1, {20.0f, 0.0f}); + mesh_a.SetFloatVertexAttribute(i, 3, {0.0f, 4.0f}); + } + + std::vector mesh_a_outline = {0, 3, 2, 1}; + absl::StatusOr mesh_a_pm = + PartitionedMesh::FromMutableMesh(mesh_a, {{mesh_a_outline}}); + ASSERT_THAT(mesh_a_pm, IsOk()); + + MutableMesh mesh_b(MeshFormat{}); + for (const Point& p : {F, G, H, I, J}) mesh_b.AppendVertex(p); + mesh_b.AppendTriangleIndices({0, 1, 2}); // FGH + mesh_b.AppendTriangleIndices({0, 2, 3}); // FHI + mesh_b.AppendTriangleIndices({0, 3, 4}); // FIJ + + std::vector mesh_b_outline = {0, 4, 3, 2, 1}; + absl::StatusOr mesh_b_pm = + PartitionedMesh::FromMutableMesh(mesh_b, {{mesh_b_outline}}); + ASSERT_THAT(mesh_b_pm, IsOk()); + + // Subtract + absl::StatusOr result = + Subtract(*mesh_a_pm, AffineTransform::Identity(), *mesh_b_pm, + AffineTransform::Identity(), 0.1f, + /*anti_aliasing_enabled=*/true); + ASSERT_THAT(result, IsOk()); + + const Mesh& result_mesh = result->RenderGroupMeshes(0)[0]; + + // Note that the result of the subtraction is the mesh with triangles + // EBF, EFC, ECD, and EAB. + + // The vertex F must be labeled right. + CheckVertexLabels(result_mesh, F, kRightLabel, kInteriorLabel); + + // Given the triangulation and the labeling, the derivatives can, with a + // little effort, be computed. + CheckVertexForwardDerivative(result_mesh, B, {0.0f, 2.0f}); + CheckVertexForwardDerivative(result_mesh, C, {0.0f, 2.0f}); + CheckVertexForwardDerivative(result_mesh, F, {0.0f, 2.0f}); + + CheckVertexSideDerivative(result_mesh, B, {4.132f, 3.226f}); + CheckVertexSideDerivative(result_mesh, C, {4.132f, -3.226f}); + CheckVertexSideDerivative(result_mesh, F, {0.485f, 0.0f}); + + // The margins can also be computed, although this time requiring considerable + // effort and close attention to the details of + // `ComputeTriangleMarginUpperBounds`. + // As a check, since the derivatives of B and C point into the concavity, we + // expect them to be constrained. + CheckVertexSideMargin(result_mesh, B, 0.349f); + CheckVertexForwardMargin(result_mesh, B, StrokeVertex::kMaximumMargin); + CheckVertexSideMargin(result_mesh, C, 0.349f); + CheckVertexForwardMargin(result_mesh, C, StrokeVertex::kMaximumMargin); + + // On the other hand F, at the center of the concavity, is unconstrained. + CheckVertexSideMargin(result_mesh, F, StrokeVertex::kMaximumMargin); + CheckVertexForwardMargin(result_mesh, F, 0.0f); } TEST(StrokeSubtractionTest, AttributeInterpolation) {