From 8e4aa3370deaab114a3cccad72e56001297b3c95 Mon Sep 17 00:00:00 2001 From: snoyer Date: Fri, 21 Feb 2025 20:28:06 +0400 Subject: [PATCH] add `align` parameter to `import_svg` --- pyproject.toml | 2 +- src/build123d/importers.py | 19 ++++++++++++++++--- tests/test_importers.py | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2a96c458..b5aa3330 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "ezdxf >= 1.1.0, < 2", "ipython >= 8.0.0, < 9", "py-lib3mf >= 2.3.1", - "ocpsvg >= 0.4, < 0.5", + "ocpsvg >= 0.5, < 0.6", "trianglesolver", ] diff --git a/src/build123d/importers.py b/src/build123d/importers.py index 0fa4bb8f..73be5a7d 100644 --- a/src/build123d/importers.py +++ b/src/build123d/importers.py @@ -70,7 +70,8 @@ from ocpsvg import ColorAndLabel, import_svg_document from svgpathtools import svg2paths -from build123d.geometry import Color, Location +from build123d.build_enums import Align +from build123d.geometry import Color, Location, Vector, to_align_offset from build123d.topology import ( Compound, Edge, @@ -337,6 +338,7 @@ def import_svg( svg_file: str | Path | TextIO, *, flip_y: bool = True, + align: Align | tuple[Align, Align] | None = Align.MIN, ignore_visibility: bool = False, label_by: Literal["id", "class", "inkscape:label"] | str = "id", is_inkscape_label: bool | None = None, # TODO remove for `1.0` release @@ -346,6 +348,8 @@ def import_svg( Args: svg_file (Union[str, Path, TextIO]): svg file flip_y (bool, optional): flip objects to compensate for svg orientation. Defaults to True. + align (Align | tuple[Align, Align] | None, optional): alignment of the SVG's viewbox, + if None, the viewbox's origin will be at `(0,0,0)`. Defaults to Align.MIN. ignore_visibility (bool, optional): Defaults to False. label_by (str, optional): XML attribute to use for imported shapes' `label` property. Defaults to "id". @@ -368,12 +372,18 @@ def import_svg( label_by = re.sub( r"^inkscape:(.+)", r"{http://www.inkscape.org/namespaces/inkscape}\1", label_by ) - for face_or_wire, color_and_label in import_svg_document( + imported = import_svg_document( svg_file, flip_y=flip_y, ignore_visibility=ignore_visibility, metadata=ColorAndLabel.Label_by(label_by), - ): + ) + + doc_xy = Vector(imported.viewbox.x, imported.viewbox.y) + doc_wh = Vector(imported.viewbox.width, imported.viewbox.height) + offset = to_align_offset(doc_xy, doc_xy + doc_wh, align) + + for face_or_wire, color_and_label in imported: if isinstance(face_or_wire, TopoDS_Wire): shape = Wire(face_or_wire) elif isinstance(face_or_wire, TopoDS_Face): @@ -381,6 +391,9 @@ def import_svg( else: # should not happen raise ValueError(f"unexpected shape type: {type(face_or_wire).__name__}") + if offset.X != 0 or offset.Y != 0: # avoid copying if we don't need to + shape = shape.translate(offset) + if shape.wrapped: shape.color = Color(*color_and_label.color_for(shape.wrapped)) shape.label = color_and_label.label diff --git a/tests/test_importers.py b/tests/test_importers.py index 0fa7088d..f246ef31 100644 --- a/tests/test_importers.py +++ b/tests/test_importers.py @@ -16,10 +16,10 @@ import_step, import_stl, ) -from build123d.geometry import Pos +from build123d.geometry import Pos, Vector from build123d.exporters import ExportSVG from build123d.exporters3d import export_brep, export_step -from build123d.build_enums import GeomType +from build123d.build_enums import Align, GeomType class ImportSVG(unittest.TestCase): @@ -116,6 +116,38 @@ def test_import_svg_colors(self): self.assertEqual(str(svg[1].color), str(Color(1, 0, 0, 1))) self.assertEqual(str(svg[2].color), str(Color(0, 0, 0, 1))) + def test_import_svg_origin(self): + svg_src = ( + '' + '' + "" + ) + + svg = import_svg(StringIO(svg_src), align=None, flip_y=False) + self.assertAlmostEqual(svg[0].bounding_box().center(), Vector(2.0, +3.0)) + + svg = import_svg(StringIO(svg_src), align=None, flip_y=True) + self.assertAlmostEqual(svg[0].bounding_box().center(), Vector(2.0, -3.0)) + + def test_import_svg_align(self): + svg_src = ( + '' + '' + "" + ) + + svg = import_svg(StringIO(svg_src), align=Align.MIN, flip_y=False) + self.assertAlmostEqual(svg[0].bounding_box().min, Vector(0.0, 0.0)) + + svg = import_svg(StringIO(svg_src), align=Align.MIN, flip_y=True) + self.assertAlmostEqual(svg[0].bounding_box().min, Vector(0, 0)) + + svg = import_svg(StringIO(svg_src), align=Align.MAX, flip_y=False) + self.assertAlmostEqual(svg[0].bounding_box().max, Vector(0.0, 0.0)) + + svg = import_svg(StringIO(svg_src), align=Align.MAX, flip_y=True) + self.assertAlmostEqual(svg[0].bounding_box().max, Vector(0, 0)) + class ImportBREP(unittest.TestCase): def test_bad_filename(self):