Skip to content

Commit 163832f

Browse files
committed
Fix hatch pattern base points not translated in DXFPolygon.transform()
DXFPolygon.transform() applies the full affine transform to the boundary paths but hands the pattern only the linear part (scale + rotation) via self.pattern.scale(relative_factor, rotation_angle). PatternLine.base_point is a position, not a direction, so under any translation the pattern phase drifts relative to the boundary: a boundary vertex at (0, 0) maps to (37.5, 11.0) while the coincident pattern base_point stays at (0, 0). Capture each line's original base_point, run pattern.scale(...) unchanged, then overwrite each base point by mapping the original point once through the same OCSTransform the boundary vertices use. Mapping the original point with the full transform avoids double-applying the linear part that scale() already handled. This completes the translation half of the sibling issue #1392 (rotation was fixed in #1399, scaling in #1391). Fixes #1402.
1 parent b3eb37b commit 163832f

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

src/ezdxf/entities/polygon.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,25 @@ def transform(self, m: Matrix44) -> DXFPolygon:
408408

409409
# rotation relative to current state:
410410
rotation_angle = ocs.transform_deg_angle(0)
411+
412+
# Capture the original base points before scaling/rotating the
413+
# pattern. Pattern.scale() applies only the linear part (scale and
414+
# rotation) of the transformation to the base points, it never
415+
# translates them, so the pattern phase drifts relative to the
416+
# boundary under any translation (issue #1402).
417+
original_base_points = [line.base_point for line in self.pattern.lines]
411418
self.pattern.scale(relative_factor, rotation_angle)
412419

420+
# Translate the base points by mapping each original base point once
421+
# with the full transformation, exactly like a boundary path vertex
422+
# (see PolylinePath.transform). Mapping the original point with the
423+
# full affine transform avoids double-applying the linear part that
424+
# Pattern.scale() already handled.
425+
for line, base_point in zip(self.pattern.lines, original_base_points):
426+
line.base_point = ocs.transform_vertex(
427+
Vec3(base_point.x, base_point.y, elevation)
428+
).vec2
429+
413430
# The pattern_scale factor has to be applied to the base pattern to get the
414431
# final scaling. This is important for CAD applications, not for the rendering
415432
# of the pattern itself.

tests/test_02_dxf_graphics/test_229c_hatch_transformations.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)