From 19364ac8abba3b70449f8124e2ab1e23e3f43389 Mon Sep 17 00:00:00 2001 From: Christian Pederkoff <6548797+cpederkoff@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:33:24 -0700 Subject: [PATCH] fixes --- stltovoxel/polygon_repair.py | 35 +++++++++++++++------------- test/test_polygon_repair.py | 44 ++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/stltovoxel/polygon_repair.py b/stltovoxel/polygon_repair.py index 92c3784..a6fab8f 100644 --- a/stltovoxel/polygon_repair.py +++ b/stltovoxel/polygon_repair.py @@ -91,17 +91,20 @@ def find_polyline_endpoints(segs): def atan_sum(f1, f2): - # Angle sum and difference identity + # Atan sum identity # atan2(atan_sum(f1, f2)) == atan2(f1) + atan2(f2) x1, y1 = f1 x2, y2 = f2 return (x1*x2 - y1*y2, y1*x2 + x1*y2) -def atan_neg(f1): - # atan2(atan_neg(f1)) == -atan2(f1) - x, y = f1 - return x, -y +def atan_diff(f1, f2): + # Atan difference identity + # atan2(atan_diff(f1, f2)) == atan2(f1) - atan2(f2) + x1, y1 = f1 + x2, y2 = f2 + return (x1*x2 + y1*y2, y1*x2 - x1*y2) + def subtract(s1, s2): @@ -144,7 +147,7 @@ def distance(p1, p2): return math.sqrt((y2 - y1)**2 + (x2 - x1)**2) -def initial_direction(my_seg, other_segs): +def initial_direction(pt, segs): # Computing the winding number of a given point requires 2 angle computations per line segment (one per point). # This method makes 2 angle computations for every segment in other_segs to compute the winding number at my_seg[1]. # It also makes one angle computation from my_seg[0] to my_seg[1]. @@ -153,11 +156,15 @@ def initial_direction(my_seg, other_segs): # Also, generally angle computation would take the form of [atan2(start-pt)-atan2(end-pt)]+... , but multiple atan2 calls # can be avoided through use of atan2 identities. # Since the final angle would be converted back into a vector, no atan2 call is required. - pt = my_seg[1] - accum = subtract(my_seg[0], my_seg[1]) - for seg_start, seg_end in other_segs: - accum = atan_sum(accum, subtract(seg_start, pt)) - accum = atan_sum(accum, atan_neg(subtract(seg_end, pt))) + accum = (1, 0) + for seg_start, seg_end in segs: + # Low quality meshes may have multiple segments with the same start or end point, so we should not attempt + # to compute the angle from pt because the result is undefined. + if seg_start != pt: + accum = atan_sum(accum, subtract(seg_start, pt)) + if seg_end != pt: + # This will trigger for at least one segment because pt comes from this same list of segments. + accum = atan_diff(accum, subtract(seg_end, pt)) # Without this accum can get arbitrarily large and lead to floating point problems accum = normalize(accum) return np.array(accum) @@ -176,12 +183,8 @@ def winding_contour(pos, segs): def winding_number_search(start, ends, segs, polyline_endpoints, max_iterations): - # find the segment I am a part of - my_seg = next(filter(lambda seg: seg[1] == start, segs)) - # find all other segments - other_segs = list(filter(lambda seg: seg[1] != start, segs)) # Find the initial direction to start marching towards. - direction = initial_direction(my_seg, other_segs) + direction = initial_direction(start, segs) # Move slightly toward that direction to pick the contour that we will use below pos = start + (direction * 0.1) seg_outs = [(tuple(start), tuple(pos))] diff --git a/test/test_polygon_repair.py b/test/test_polygon_repair.py index f52f76f..5c149bc 100644 --- a/test/test_polygon_repair.py +++ b/test/test_polygon_repair.py @@ -35,31 +35,31 @@ def test_find_polylines_out_of_order(self): actual_polylines = polygon_repair.find_polylines(line_segments) self.assertEqual(actual_polylines, expected_polylines) - def test_get_direction(self): - other_segs = [((1, 0), (1, 1)), ((1, 1), (0, 1)), ] - my_seg = ((0, 1), (0, 0)) - actual = polygon_repair.initial_direction(my_seg, other_segs) + def test_initial_direction(self): + segs = [((0, 1), (0, 0)), ((1, 0), (1, 1)), ((1, 1), (0, 1)), ] + pt = (0, 0) + actual = polygon_repair.initial_direction(pt, segs) actual = tuple(actual) expected = (1.0, 0) self.tuples_almost_equal(actual, expected) - def test_get_direction2(self): - other_segs = [((1, 0), (0, 0))] - my_seg = ((0, 1), (1, 1)) - actual = polygon_repair.initial_direction(my_seg, other_segs) + def test_initial_direction2(self): + segs = [((0, 1), (1, 1)), ((1, 0), (0, 0))] + pt = (1, 1) + actual = polygon_repair.initial_direction(pt, segs) actual = tuple(actual) expected = polygon_repair.normalize((-1, -1)) self.tuples_almost_equal(actual, expected) - def test_get_direction3(self): - other_segs = [((1, 1), (1, 0))] - my_seg = ((1, 0), (0, 0)) - actual = polygon_repair.initial_direction(my_seg, other_segs) + def test_initial_direction3(self): + segs = [((1, 0), (0, 0)), ((1, 1), (1, 0))] + pt = (0, 0) + actual = polygon_repair.initial_direction(pt, segs) actual = tuple(actual) expected = polygon_repair.normalize((1, 1)) self.tuples_almost_equal(actual, expected) - def test_grad_90_norm(self): + def test_winding_contour(self): segs = [((0, 0), (1, 0)), ((1, 0), (1, 1))] pos = np.array((1, 1)) - np.array((0.1, 0.1)) # Treats starts as repellers and ends as attractors @@ -67,7 +67,7 @@ def test_grad_90_norm(self): expected = polygon_repair.normalize((-1, -1)) self.tuples_almost_equal(actual, expected) - def test_grad_90_norm2(self): + def test_winding_contour2(self): segs = [((0, 0), (1, 0)), ((1, 0), (1, 1)), ((1, 1), (0, 1))] pos = (0, 0.5) actual = polygon_repair.winding_contour(pos, segs) @@ -84,13 +84,27 @@ def test_grad_90_norm2(self): expected = polygon_repair.normalize((0, -1)) self.tuples_almost_equal(actual, expected) - def test_grad_90_norm3(self): + def test_winding_contour3(self): segs = [((0, 0), (1, 0)), ((1, 1), (0, 1))] pos = (0.00001, 0.00001) actual = polygon_repair.winding_contour(pos, segs) expected = polygon_repair.normalize((-1, -1)) self.tuples_almost_equal(actual, expected) + def test_repair_all_close_case(self): + segs = [((0, 0), (1, 0)), ((1, .5), (0, .5))] + repair = polygon_repair.PolygonRepair(segs, (10, 10)) + repair.repair_all() + expected = [[(0, 0), (1, 0), (1, 0.5), (0, 0.5), (0, 0)]] + self.assertEqual(repair.loops, expected) + + def test_repair_all_far_case(self): + segs = [((0, 0), (1, 0)), ((1, 1.5), (0, 1.5))] + repair = polygon_repair.PolygonRepair(segs, (10, 10)) + repair.repair_all() + expected = [[(0, 0), (1, 0), (0, 0)], [(1, 1.5), (0, 1.5), (1, 1.5)]] + self.assertEqual(repair.loops, expected) + if __name__ == '__main__': unittest.main()