@@ -239,3 +239,62 @@ def test_translation_does_not_change_pattern_line_angle():
239239
240240 hatch .transform (Matrix44 .translate (100 , 0 , 0 ))
241241 assert hatch .pattern .lines [0 ].angle == pytest .approx (pattern_line_angle )
242+
243+
244+ def test_translation_moves_pattern_base_point_like_a_boundary_vertex ():
245+ # Regression test for issue #1402: DXFPolygon.transform() translated the
246+ # boundary paths but left the hatch pattern line base points untouched, so
247+ # the pattern phase drifted relative to the boundary under any translation.
248+ hatch = Hatch ()
249+ path = hatch .paths .add_polyline_path ([(0 , 0 ), (10 , 0 ), (10 , 10 ), (0 , 10 )])
250+ hatch .set_pattern_fill ("ANSI31" , scale = 1.0 )
251+ # anchor the first pattern line base point at the boundary origin (0, 0)
252+ hatch .pattern .lines [0 ].base_point = Vec2 (0 , 0 )
253+
254+ offset = Vec2 (37.5 , 11.0 )
255+ hatch .transform (Matrix44 .translate (offset .x , offset .y , 0 ))
256+
257+ # the boundary vertex (0, 0) maps to the translation offset ...
258+ vx , vy , _ = path .vertices [0 ]
259+ assert Vec2 (vx , vy ).isclose (offset )
260+ # ... and the pattern base point must move by the same translation
261+ assert hatch .pattern .lines [0 ].base_point .isclose (offset )
262+
263+
264+ def test_translation_moves_every_pattern_base_point_by_the_same_offset ():
265+ # Regression test for issue #1402: every non-zero base point must move by
266+ # the same translation as the boundary, keeping the pattern phase locked.
267+ hatch = Hatch ()
268+ hatch .paths .add_polyline_path ([(0 , 0 ), (10 , 0 ), (10 , 10 ), (0 , 10 )])
269+ hatch .set_pattern_fill ("ANSI31" , scale = 1.0 )
270+ # give each pattern line a distinct non-zero base point
271+ for index , line in enumerate (hatch .pattern .lines ):
272+ line .base_point = Vec2 (index + 1 , 2 * (index + 1 ))
273+ original_base_points = [line .base_point for line in hatch .pattern .lines ]
274+
275+ offset = Vec2 (100 , 25 )
276+ hatch .transform (Matrix44 .translate (offset .x , offset .y , 0 ))
277+
278+ for line , base_point in zip (hatch .pattern .lines , original_base_points ):
279+ assert line .base_point .isclose (base_point + offset )
280+
281+
282+ def test_pattern_base_point_maps_like_boundary_vertex_under_full_transform ():
283+ # Regression test for issue #1402: a base point coincident with a boundary
284+ # vertex must land on the exact same transformed point under a combined
285+ # rotation + translation. This guards against double-applying the linear
286+ # part that Pattern.scale() already handles.
287+ vertices = [(2 , 3 ), (10 , 0 ), (10 , 10 ), (0 , 10 )]
288+ hatch = Hatch ()
289+ path = hatch .paths .add_polyline_path (vertices )
290+ hatch .set_pattern_fill ("ANSI31" , scale = 1.0 )
291+ hatch .pattern .lines [0 ].base_point = Vec2 (2 , 3 ) # coincident with vertices[0]
292+
293+ m = Matrix44 .chain (
294+ Matrix44 .z_rotate (math .radians (37 )),
295+ Matrix44 .translate (37.5 , 11.0 , 0 ),
296+ )
297+ hatch .transform (m )
298+
299+ vx , vy , _ = path .vertices [0 ]
300+ assert hatch .pattern .lines [0 ].base_point .isclose (Vec2 (vx , vy ))
0 commit comments