diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 0c19b5a..301910d 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -300,12 +300,57 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) -> ) if 'column_count' in attr: - args['repetition'] = Grid( - a_vector=(attr['column_spacing'], 0), - b_vector=(0, attr['row_spacing']), - a_count=attr['column_count'], - b_count=attr['row_count'], + col_spacing = attr['column_spacing'] + row_spacing = attr['row_spacing'] + col_count = attr['column_count'] + row_count = attr['row_count'] + local_x = numpy.array((col_spacing, 0.0)) + local_y = numpy.array((0.0, row_spacing)) + inv_rot = rotation_matrix_2d(-rotation) + + candidates = ( + (inv_rot @ local_x, inv_rot @ local_y, col_count, row_count), + (inv_rot @ local_y, inv_rot @ local_x, row_count, col_count), ) + repetition = None + for a_vector, b_vector, a_count, b_count in candidates: + rotated_a = rotation_matrix_2d(rotation) @ a_vector + rotated_b = rotation_matrix_2d(rotation) @ b_vector + if (numpy.isclose(rotated_a[1], 0, atol=1e-8) + and numpy.isclose(rotated_b[0], 0, atol=1e-8) + and numpy.isclose(rotated_a[0], col_spacing, atol=1e-8) + and numpy.isclose(rotated_b[1], row_spacing, atol=1e-8) + and a_count == col_count + and b_count == row_count): + repetition = Grid( + a_vector=a_vector, + b_vector=b_vector, + a_count=a_count, + b_count=b_count, + ) + break + if (numpy.isclose(rotated_a[0], 0, atol=1e-8) + and numpy.isclose(rotated_b[1], 0, atol=1e-8) + and numpy.isclose(rotated_b[0], col_spacing, atol=1e-8) + and numpy.isclose(rotated_a[1], row_spacing, atol=1e-8) + and b_count == col_count + and a_count == row_count): + repetition = Grid( + a_vector=a_vector, + b_vector=b_vector, + a_count=a_count, + b_count=b_count, + ) + break + + if repetition is None: + repetition = Grid( + a_vector=inv_rot @ local_x, + b_vector=inv_rot @ local_y, + a_count=col_count, + b_count=row_count, + ) + args['repetition'] = repetition pat.ref(**args) else: logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).') diff --git a/masque/test/test_dxf.py b/masque/test/test_dxf.py index 0c0a1a3..5b038c6 100644 --- a/masque/test/test_dxf.py +++ b/masque/test/test_dxf.py @@ -112,6 +112,38 @@ def test_dxf_manhattan_precision(tmp_path: Path): assert isinstance(ref.repetition, Grid), "Grid should be preserved for 90-degree rotation" +def test_dxf_rotated_grid_roundtrip_preserves_basis_and_counts(tmp_path: Path): + lib = Library() + sub = Pattern() + sub.polygon("1", vertices=[[0, 0], [1, 0], [1, 1]]) + lib["sub"] = sub + + top = Pattern() + top.ref( + "sub", + offset=(0, 0), + rotation=numpy.pi / 2, + repetition=Grid(a_vector=(10, 0), a_count=3, b_vector=(0, 20), b_count=2), + ) + lib["top"] = top + + dxf_file = tmp_path / "rotated_grid.dxf" + dxf.writefile(lib, "top", dxf_file) + + read_lib, _ = dxf.readfile(dxf_file) + read_top = read_lib.get("Model") or read_lib.get("top") or list(read_lib.values())[0] + + target_name = next(k for k in read_top.refs if k.upper() == "SUB") + ref = read_top.refs[target_name][0] + assert isinstance(ref.repetition, Grid) + actual = ref.repetition.displacements + expected = Grid(a_vector=(10, 0), a_count=3, b_vector=(0, 20), b_count=2).displacements + assert_allclose( + actual[numpy.lexsort((actual[:, 1], actual[:, 0]))], + expected[numpy.lexsort((expected[:, 1], expected[:, 0]))], + ) + + def test_dxf_read_legacy_polyline() -> None: doc = ezdxf.new() msp = doc.modelspace()