Skip to content

Commit c111eef

Browse files
authored
Merge pull request #1407 from youdie006/fix/1402-hatch-pattern-base-point-translation
Fix #1402: translate hatch pattern base points in DXFPolygon.transform()
2 parents b3eb37b + 163832f commit c111eef

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)