"""Run with python -m pytest blender/scripts/test_seam_gap.py (no Blender needed). The margin exists to absorb two-pass registration error, so the property that matters is that the band's width is the requested *physical* distance and that the edge is straight. Both are checkable in closed form on a triangle whose surface-to-UV map is known. """ import unittest from seam_gap import clip_polygon, polygon_area, triangle_gap_planes # Unit right triangle in the world XY plane, so world (x, y) and UV can be made identical. P0, P1, P2 = (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0) TRIANGLE = (P0, P1, P2) UV_IDENTITY = ((0.0, 0.0), (1.0, 0.0), (0.0, 1.0)) # The bottom edge, which lies along world y = 0 (and UV v = 0). SEAM_BOTTOM = ((P0, P1),) def _signed(plane, point): """How far ``point`` is on the kept side of ``plane`` (positive = kept).""" (cx, cy), k = plane return cx * point[0] + cy * point[1] - k class PlaneTests(unittest.TestCase): def test_no_seam_edges_gives_no_planes(self): self.assertEqual(triangle_gap_planes(TRIANGLE, UV_IDENTITY, [], 0.1), []) def test_world_margin_maps_to_the_same_uv_margin(self): """Identity mapping: a 0.1 mm cut in the world is v >= 0.1 in UV.""" planes = triangle_gap_planes(TRIANGLE, UV_IDENTITY, SEAM_BOTTOM, 0.1) self.assertEqual(len(planes), 1) # Assert the half-*space*, not the coefficients: the representation is not normalised, # so a scaled (c, k) describes the same cut. self.assertAlmostEqual(_signed(planes[0], (0.5, 0.1)), 0.0, places=9) self.assertLess(_signed(planes[0], (0.5, 0.05)), 0.0) self.assertGreater(_signed(planes[0], (0.5, 0.15)), 0.0) def test_uv_scale_is_accounted_for(self): """A 10x UV scale means the same physical cut is 10x further in UV.""" planes = triangle_gap_planes( TRIANGLE, ((0.0, 0.0), (10.0, 0.0), (0.0, 10.0)), SEAM_BOTTOM, 0.1 ) self.assertEqual(len(planes), 1) # The cut lands at v_uv = 1.0, i.e. the world distance 0.1 is honoured. self.assertAlmostEqual(_signed(planes[0], (5.0, 1.0)), 0.0, places=9) self.assertLess(_signed(planes[0], (5.0, 0.9)), 0.0) self.assertGreater(_signed(planes[0], (5.0, 1.1)), 0.0) def test_margin_scales_linearly_in_the_world(self): """Doubling the inset must double the world distance, whatever UV does.""" uv = ((0.0, 0.0), (10.0, 0.0), (0.0, 10.0)) near = triangle_gap_planes(TRIANGLE, uv, SEAM_BOTTOM, 0.05) far = triangle_gap_planes(TRIANGLE, uv, SEAM_BOTTOM, 0.10) # Boundary positions along the v axis: 0.5 world mm and 1.0 world mm in UV. self.assertAlmostEqual(_signed(near[0], (5.0, 0.5)), 0.0, places=9) self.assertAlmostEqual(_signed(far[0], (5.0, 1.0)), 0.0, places=9) def test_edge_orientation_does_not_change_the_cut(self): forward = triangle_gap_planes(TRIANGLE, UV_IDENTITY, ((P0, P1),), 0.1) reverse = triangle_gap_planes(TRIANGLE, UV_IDENTITY, ((P1, P0),), 0.1) for point in ((0.5, 0.05), (0.5, 0.1), (0.5, 0.2)): self.assertAlmostEqual( _signed(forward[0], point), _signed(reverse[0], point), places=12 ) def test_a_seam_edge_on_the_far_side_keeps_the_triangle(self): """The cut is oriented into the triangle, so the opposite edge cuts the other way.""" planes = triangle_gap_planes(TRIANGLE, UV_IDENTITY, ((P1, P2),), 0.1) self.assertEqual(len(planes), 1) polygon = clip_polygon(UV_IDENTITY, planes) # Clipping away from the hypotenuse keeps the corner at (0, 0). self.assertTrue(any(abs(x) < 1e-9 and abs(y) < 1e-9 for x, y in polygon)) def test_degenerate_uv_triangle_yields_no_planes(self): collapsed = ((0.0, 0.0), (0.0, 0.0), (0.0, 0.0)) self.assertEqual(triangle_gap_planes(TRIANGLE, collapsed, SEAM_BOTTOM, 0.1), []) def test_zero_margin_still_produces_the_edge_plane(self): planes = triangle_gap_planes(TRIANGLE, UV_IDENTITY, SEAM_BOTTOM, 0.0) self.assertEqual(len(planes), 1) self.assertAlmostEqual(planes[0][1], 0.0) class ClipTests(unittest.TestCase): def test_margin_strip_is_removed(self): """Removing v < 0.1 leaves the triangle (0,0.1), (0.9,0.1), (0,1): area 0.405.""" planes = triangle_gap_planes(TRIANGLE, UV_IDENTITY, SEAM_BOTTOM, 0.1) clipped = clip_polygon(UV_IDENTITY, planes) self.assertEqual(len(clipped), 3) self.assertAlmostEqual(polygon_area(clipped), 0.405, places=9) def test_clipped_edge_is_straight_at_the_requested_distance(self): planes = triangle_gap_planes(TRIANGLE, UV_IDENTITY, SEAM_BOTTOM, 0.1) clipped = clip_polygon(UV_IDENTITY, planes) # Every vertex introduced by the cut sits exactly on v = 0.1. on_cut = [point for point in clipped if abs(point[1] - 0.1) < 1e-9] self.assertEqual(len(on_cut), 2) # And nothing survives below it. self.assertTrue(all(point[1] >= 0.1 - 1e-9 for point in clipped)) def test_polygon_entirely_inside_is_returned_unchanged(self): planes = (((0.0, 1.0), -1.0),) # v >= -1, trivially satisfied clipped = clip_polygon(UV_IDENTITY, planes) self.assertAlmostEqual(polygon_area(clipped), 0.5) def test_polygon_entirely_outside_is_removed(self): planes = (((0.0, 1.0), 10.0),) # v >= 10 self.assertEqual(clip_polygon(UV_IDENTITY, planes), []) def test_two_planes_intersect_correctly(self): planes = (((0.0, 1.0), 0.1), ((1.0, 0.0), 0.1)) clipped = clip_polygon(UV_IDENTITY, planes) self.assertTrue(all(point[1] >= 0.1 - 1e-9 for point in clipped)) self.assertTrue(all(point[0] >= 0.1 - 1e-9 for point in clipped)) self.assertAlmostEqual(polygon_area(clipped), 0.32, places=9) def test_degenerate_input_is_returned_empty(self): self.assertEqual(clip_polygon([(0.0, 0.0), (1.0, 0.0)], []), []) if __name__ == "__main__": unittest.main()