add align parameter to import_svg

This commit is contained in:
snoyer 2025-02-21 20:28:06 +04:00
parent 40cf1437ed
commit 8e4aa3370d
3 changed files with 51 additions and 6 deletions

View file

@ -43,7 +43,7 @@ dependencies = [
"ezdxf >= 1.1.0, < 2", "ezdxf >= 1.1.0, < 2",
"ipython >= 8.0.0, < 9", "ipython >= 8.0.0, < 9",
"py-lib3mf >= 2.3.1", "py-lib3mf >= 2.3.1",
"ocpsvg >= 0.4, < 0.5", "ocpsvg >= 0.5, < 0.6",
"trianglesolver", "trianglesolver",
] ]

View file

@ -70,7 +70,8 @@ from OCP.XCAFDoc import (
from ocpsvg import ColorAndLabel, import_svg_document from ocpsvg import ColorAndLabel, import_svg_document
from svgpathtools import svg2paths 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 ( from build123d.topology import (
Compound, Compound,
Edge, Edge,
@ -337,6 +338,7 @@ def import_svg(
svg_file: str | Path | TextIO, svg_file: str | Path | TextIO,
*, *,
flip_y: bool = True, flip_y: bool = True,
align: Align | tuple[Align, Align] | None = Align.MIN,
ignore_visibility: bool = False, ignore_visibility: bool = False,
label_by: Literal["id", "class", "inkscape:label"] | str = "id", label_by: Literal["id", "class", "inkscape:label"] | str = "id",
is_inkscape_label: bool | None = None, # TODO remove for `1.0` release is_inkscape_label: bool | None = None, # TODO remove for `1.0` release
@ -346,6 +348,8 @@ def import_svg(
Args: Args:
svg_file (Union[str, Path, TextIO]): svg file svg_file (Union[str, Path, TextIO]): svg file
flip_y (bool, optional): flip objects to compensate for svg orientation. Defaults to True. 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. ignore_visibility (bool, optional): Defaults to False.
label_by (str, optional): XML attribute to use for imported shapes' `label` property. label_by (str, optional): XML attribute to use for imported shapes' `label` property.
Defaults to "id". Defaults to "id".
@ -368,12 +372,18 @@ def import_svg(
label_by = re.sub( label_by = re.sub(
r"^inkscape:(.+)", r"{http://www.inkscape.org/namespaces/inkscape}\1", label_by 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, svg_file,
flip_y=flip_y, flip_y=flip_y,
ignore_visibility=ignore_visibility, ignore_visibility=ignore_visibility,
metadata=ColorAndLabel.Label_by(label_by), 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): if isinstance(face_or_wire, TopoDS_Wire):
shape = Wire(face_or_wire) shape = Wire(face_or_wire)
elif isinstance(face_or_wire, TopoDS_Face): elif isinstance(face_or_wire, TopoDS_Face):
@ -381,6 +391,9 @@ def import_svg(
else: # should not happen else: # should not happen
raise ValueError(f"unexpected shape type: {type(face_or_wire).__name__}") 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: if shape.wrapped:
shape.color = Color(*color_and_label.color_for(shape.wrapped)) shape.color = Color(*color_and_label.color_for(shape.wrapped))
shape.label = color_and_label.label shape.label = color_and_label.label

View file

@ -16,10 +16,10 @@ from build123d.importers import (
import_step, import_step,
import_stl, import_stl,
) )
from build123d.geometry import Pos from build123d.geometry import Pos, Vector
from build123d.exporters import ExportSVG from build123d.exporters import ExportSVG
from build123d.exporters3d import export_brep, export_step 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): class ImportSVG(unittest.TestCase):
@ -116,6 +116,38 @@ class ImportSVG(unittest.TestCase):
self.assertEqual(str(svg[1].color), str(Color(1, 0, 0, 1))) self.assertEqual(str(svg[1].color), str(Color(1, 0, 0, 1)))
self.assertEqual(str(svg[2].color), str(Color(0, 0, 0, 1))) self.assertEqual(str(svg[2].color), str(Color(0, 0, 0, 1)))
def test_import_svg_origin(self):
svg_src = (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 1 6 8" width="6" height="8">'
'<circle r="1" cx="2" cy="3"/>'
"</svg>"
)
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 xmlns="http://www.w3.org/2000/svg" viewBox="1 1 6 8" width="6" height="8">'
'<rect x="1" y="1" width="6" height="8"/>'
"</svg>"
)
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): class ImportBREP(unittest.TestCase):
def test_bad_filename(self): def test_bad_filename(self):