Skip to content

Commit

Permalink
Update Polyline processing behavior with quality and consistency impr…
Browse files Browse the repository at this point in the history
…ovements

PiperOrigin-RevId: 705906189
  • Loading branch information
Ink Open Source authored and copybara-github committed Dec 13, 2024
1 parent 5269137 commit da0b7c2
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 37 deletions.
1 change: 1 addition & 0 deletions ink/geometry/internal/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ cc_library(
":algorithms",
":static_rtree",
"//ink/geometry:distance",
"//ink/geometry:envelope",
"//ink/geometry:point",
"//ink/geometry:rect",
"//ink/geometry:segment",
Expand Down
111 changes: 76 additions & 35 deletions ink/geometry/internal/polyline_processing.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#include "ink/geometry/internal/polyline_processing.h"

#include <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include <optional>
#include <utility>
Expand All @@ -23,6 +25,7 @@
#include "absl/log/absl_check.h"
#include "absl/types/span.h"
#include "ink/geometry/distance.h"
#include "ink/geometry/envelope.h"
#include "ink/geometry/internal/algorithms.h"
#include "ink/geometry/internal/static_rtree.h"
#include "ink/geometry/point.h"
Expand Down Expand Up @@ -51,14 +54,36 @@ float WalkDistance(PolylineData& polyline, int index, float fractional_index,
return total_distance;
}

float IntermediateWalkDistance(PolylineData& polyline, int start_index,
int end_index) {
float total_distance = 0.0f;
for (int i = start_index + 1; i < end_index; ++i) {
total_distance += polyline.segments[i].length;
}
return total_distance;
}

PolylineData CreateNewPolylineData(absl::Span<const Point> points) {
PolylineData polyline;

Point last_point = points[0];
int segment_count = 0;
polyline.segments.reserve(points.size() - 1);
for (int i = 0; i < static_cast<int>(points.size()) - 1; ++i) {
polyline.segments.push_back(
SegmentBundle{Segment{points[i], points[i + 1]}, i,
Distance(points[i], points[i + 1])});
for (int i = 1; i < static_cast<int>(points.size()); ++i) {
if (points[i] != last_point) {
float distance = Distance(last_point, points[i]);
polyline.total_walk_distance += distance;
polyline.segments.push_back(SegmentBundle{Segment{last_point, points[i]},
segment_count, distance});
++segment_count;
last_point = points[i];
}
}

Envelope envelope;
envelope.Add(points);
polyline.max_straight_line_distance =
std::hypot(envelope.AsRect()->Width(), envelope.AsRect()->Height());
return polyline;
}

Expand All @@ -83,17 +108,22 @@ void FindFirstAndLastIntersections(
Rect::FromTwoPoints(current_segment_bundle.segment.start,
current_segment_bundle.segment.end),
[&earliest_intersected_segment, &earliest_intersection_ratios,
&current_segment_bundle](SegmentBundle other_segment) {
&current_segment_bundle, &polyline](SegmentBundle other_segment) {
// Only check segments that are after the next segment.
if (other_segment.index > current_segment_bundle.index + 1) {
if (std::optional<std::pair<float, float>> intersection_ratios =
SegmentIntersectionRatio(current_segment_bundle.segment,
other_segment.segment);
intersection_ratios.has_value()) {
if (intersection_ratios->first <
earliest_intersection_ratios.first) {
earliest_intersected_segment = other_segment;
earliest_intersection_ratios = *intersection_ratios;
if (IntermediateWalkDistance(polyline,
current_segment_bundle.index,
other_segment.index) >
polyline.min_walk_distance / 2.0f) {
if (intersection_ratios->first <
earliest_intersection_ratios.first) {
earliest_intersected_segment = other_segment;
earliest_intersection_ratios = *intersection_ratios;
}
}
}
}
Expand Down Expand Up @@ -153,8 +183,12 @@ void FindFirstAndLastIntersections(
SegmentIntersectionRatio(current_segment_bundle.segment,
other_segment.segment);
intersection_ratios.has_value()) {
if (intersection_ratios->first > largest_intersection_ratio) {
largest_intersection_ratio = intersection_ratios->first;
if (IntermediateWalkDistance(polyline, other_segment.index,
current_segment_bundle.index) >
polyline.min_walk_distance / 2.0f) {
if (intersection_ratios->first > largest_intersection_ratio) {
largest_intersection_ratio = intersection_ratios->first;
}
}
}
}
Expand Down Expand Up @@ -283,6 +317,7 @@ void FindBestEndpointConnections(
std::optional<float> connection_projection =
current_segment.segment.Project(first_point);
ABSL_CHECK(connection_projection.has_value());

float clamped_projection =
std::clamp(*connection_projection, 0.0f, 1.0f);

Expand Down Expand Up @@ -363,31 +398,35 @@ void FindBestEndpointConnections(
}
}

std::vector<Point> CreateNewPolylineFromPolylineData(
PolylineData& polyline, absl::Span<const Point> points) {
if (!polyline.has_intersection) {
return std::vector<Point>(points.begin(), points.end());
}

int front_trim_index =
polyline.connect_first ? 0 : polyline.first_intersection.index_int + 1;
int back_trim_index = polyline.connect_last
? points.size()
: polyline.last_intersection.index_int + 1;

std::vector<Point> CreateNewPolylineFromPolylineData(PolylineData& polyline) {
std::vector<Point> new_polyline;
new_polyline.reserve(back_trim_index - front_trim_index + 2);

if (polyline.new_first_point != points[front_trim_index] &&
polyline.has_intersection) {
new_polyline.push_back(polyline.new_first_point);
}
for (int i = front_trim_index; i < back_trim_index; ++i) {
new_polyline.push_back(points[i]);
}
if (polyline.new_last_point != new_polyline.back() &&
polyline.has_intersection) {
new_polyline.push_back(polyline.new_last_point);
if (polyline.has_intersection) {
int front_trim_index =
polyline.connect_first ? 0 : polyline.first_intersection.index_int + 1;
int back_trim_index = polyline.connect_last
? polyline.segments.size()
: polyline.last_intersection.index_int + 1;
new_polyline.reserve(back_trim_index - front_trim_index + 3);
if (polyline.new_first_point !=
polyline.segments[front_trim_index].segment.start) {
new_polyline.push_back(polyline.new_first_point);
}
for (int i = front_trim_index; i < back_trim_index; ++i) {
new_polyline.push_back(polyline.segments[i].segment.start);
}
if (polyline.connect_last) {
new_polyline.push_back(polyline.segments.back().segment.end);
}
if (polyline.new_last_point != new_polyline.back()) {
new_polyline.push_back(polyline.new_last_point);
}
} else {
new_polyline.reserve(polyline.segments.size() + 1);
new_polyline.push_back(polyline.segments.front().segment.start);
for (size_t i = 0; i < polyline.segments.size(); ++i) {
new_polyline.push_back(polyline.segments[i].segment.end);
}
}
return new_polyline;
}
Expand All @@ -406,8 +445,10 @@ std::vector<Point> ProcessPolylineForMeshCreation(
ink::geometry_internal::StaticRTree<SegmentBundle> rtree(polyline.segments,
segment_bounds);
FindFirstAndLastIntersections(rtree, polyline);

FindBestEndpointConnections(rtree, polyline);
return CreateNewPolylineFromPolylineData(polyline, points);

return CreateNewPolylineFromPolylineData(polyline);
}

} // namespace ink::geometry_internal
4 changes: 3 additions & 1 deletion ink/geometry/internal/polyline_processing.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ struct PolylineData {
bool connect_first = false;
bool connect_last = false;
bool has_intersection = false;
float min_walk_distance;
float max_straight_line_distance;
float total_walk_distance = 0.0f;
float min_walk_distance = 0.0f;
float max_connection_distance;
float min_connection_ratio;
float min_trimming_ratio;
Expand Down
92 changes: 91 additions & 1 deletion ink/geometry/internal/polyline_processing_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ auto segment_bounds = [](SegmentBundle segment_data) {
segment_data.segment.end);
};

PolylineData CreatePolylineAndFindIntersections(std::vector<Point> points) {
PolylineData CreatePolylineAndFindIntersections(
std::vector<Point> points, float min_walk_distance = 0.0f) {
PolylineData output_polyline = CreateNewPolylineData(points);
output_polyline.min_walk_distance = min_walk_distance;
ink::geometry_internal::StaticRTree<SegmentBundle> rtree(
output_polyline.segments, segment_bounds);
FindFirstAndLastIntersections(rtree, output_polyline);
Expand Down Expand Up @@ -131,6 +133,61 @@ TEST(PolylineProcessingTest,
SegmentEq(Segment{{5, 0}, {2, 0}}));
}

TEST(PolylineProcessingTest,
CreateNewPolylineDataDiscardsDuplicatePointsAtPolylineStart) {
std::vector<Point> points = {
Point{5, 3}, Point{5, 3}, Point{5, 3}, Point{5, 3}, Point{5, 3},
Point{5, 8}, Point{5, 15}, Point{10, 20}, Point{15, 25}, Point{20, 30},
Point{30, 20}, Point{20, 10}, Point{11, 3}, Point{5, 3}};
PolylineData polyline = CreateNewPolylineData(points);

EXPECT_EQ(polyline.segments.size(), 9);
EXPECT_THAT(polyline.segments.front().segment,
SegmentEq(Segment{{5, 3}, {5, 8}}));
EXPECT_THAT(polyline.segments.back().segment,
SegmentEq(Segment{{11, 3}, {5, 3}}));
}

TEST(PolylineProcessingTest,
CreateNewPolylineDataDiscardsDuplicatePointsAtPolylineEnd) {
std::vector<Point> points = {
Point{1, 9}, Point{-1, 13}, Point{1, 17}, Point{6, 19}, Point{14, 15},
Point{18, 9}, Point{16, 3}, Point{10, 0}, Point{4, 3}, Point{2, 9},
Point{6, 15}, Point{14, 19}, Point{19, 17}, Point{21, 13}, Point{19, 9},
Point{19, 9}, Point{19, 9}, Point{19, 9}, Point{19, 9}, Point{19, 9}};
PolylineData polyline = CreateNewPolylineData(points);

EXPECT_EQ(polyline.segments.size(), 14);
EXPECT_THAT(polyline.segments.front().segment,
SegmentEq(Segment{{1, 9}, {-1, 13}}));
EXPECT_THAT(polyline.segments.back().segment,
SegmentEq(Segment{{21, 13}, {19, 9}}));
}

TEST(PolylineProcessingTest,
CreateNewPolylineDataDiscardsDuplicatePointsInPolylineMiddle) {
std::vector<Point> points = {
Point{1, 9}, Point{-1, 13}, Point{1, 17}, Point{1, 17}, Point{1, 17},
Point{1, 17}, Point{6, 19}, Point{14, 15}, Point{18, 9}, Point{16, 3},
Point{10, 0}, Point{10, 0}, Point{10, 0}, Point{4, 3}, Point{2, 9},
Point{6, 15}, Point{6, 15}, Point{14, 19}, Point{19, 17}, Point{21, 13},
Point{21, 13}, Point{21, 13}, Point{21, 13}, Point{21, 13}, Point{21, 13},
Point{21, 13}, Point{19, 9}};
PolylineData polyline = CreateNewPolylineData(points);

EXPECT_EQ(polyline.segments.size(), 14);
EXPECT_THAT(polyline.segments[2].segment,
SegmentEq(Segment{{1, 17}, {6, 19}}));
EXPECT_THAT(polyline.segments[7].segment,
SegmentEq(Segment{{10, 0}, {4, 3}}));
EXPECT_THAT(polyline.segments[10].segment,
SegmentEq(Segment{{6, 15}, {14, 19}}));
EXPECT_THAT(polyline.segments[12].segment,
SegmentEq(Segment{{19, 17}, {21, 13}}));
EXPECT_THAT(polyline.segments[13].segment,
SegmentEq(Segment{{21, 13}, {19, 9}}));
}

TEST(PolylineProcessingTest, IntersectionsForPerfectlyClosedLoop) {
std::vector<Point> points = {
Point{5, 3}, Point{5, 8}, Point{5, 15}, Point{10, 20}, Point{15, 25},
Expand Down Expand Up @@ -206,6 +263,22 @@ TEST(PolylineProcessingTest,
EXPECT_EQ(polyline.has_intersection, false);
}

TEST(PolylineProcessingTest,
IntersectionsDiscardsIntersectionsWithTooShortWalkDistance) {
std::vector<Point> points = {
Point{19, 22}, Point{14, 19}, Point{10.05f, 17},
Point{14, 15}, Point{18, 9}, Point{16, 3},
Point{10, 0}, Point{9.9f, -.1f}, Point{10.1f, -.1f},
Point{10, 0}, Point{4, 3}, Point{2, 9},
Point{6, 15}, Point{9.95f, 17}, Point{6, 19},
Point{2, 21}};

PolylineData polyline_with_walk_distance =
CreatePolylineAndFindIntersections(points, 2.0f);

EXPECT_EQ(polyline_with_walk_distance.has_intersection, false);
}

TEST(PolylineProcessingTest,
IntersectionsWithExplictlyOverlappingLineSegments) {
std::vector<Point> points = {
Expand Down Expand Up @@ -867,10 +940,27 @@ TEST(PolylineProcessingTest,
Point{10, 19}}));
}

TEST(PolylineProcessingTest,
ProcessPolylineWithOneValidIntersectionTrimsTwoInvalidIntersections) {
EXPECT_THAT(
ProcessPolylineForMeshCreation(
{Point{6, 23}, Point{6.2f, 23.1f}, Point{6.1f, 23.1f}, Point{8, 21},
Point{10, 19}, Point{14, 15}, Point{18, 9}, Point{16, 3},
Point{10, 0}, Point{4, 3}, Point{2, 9}, Point{6, 15}, Point{10, 19},
Point{11.9f, 21.2f}, Point{11.9f, 21.1f}, Point{14, 23}},
kMinWalkDistance, kMaxConnectionDistance, kMinConnectionRatio,
kMinTrimmingRatio),
testing::Pointwise(
PointsEq(), {Point{10, 19}, Point{14, 15}, Point{18, 9}, Point{16, 3},
Point{10, 0}, Point{4, 3}, Point{2, 9}, Point{6, 15},
Point{10, 19}}));
}

TEST(PolylineProcessingTest,
ProcessPolylineWithOneIntersectionConnectsBothPoints) {
EXPECT_THAT(
ProcessPolylineForMeshCreation(

{Point{1, 9}, Point{-1, 13}, Point{1, 17}, Point{6, 19},
Point{14, 15}, Point{18, 9}, Point{16, 3}, Point{10, 0}, Point{4, 3},
Point{2, 9}, Point{6, 15}, Point{14, 19}, Point{19, 17},
Expand Down

0 comments on commit da0b7c2

Please sign in to comment.