Skip to content

Commit 2da12fc

Browse files
Add STEP Import for Assemblies (#1779)
* Initial commit with colors and locations working for simple assemblies * Trying to fix mypy error * Jumping through hoops to make mypy happy * Lint fix * Refactored method interface based on suggestion * Use Self and @classmethod * Lint fix * Added a test specifically for testing metadata * Seeing if coverage and tests will pass * Added name loading from STEP file * Made name a public property of Assembly * Trying to increase test coverage a bit * Syncing up some experiments * Got color and layer search working, still need to get name search working through indirect lookup * Got tests working for layer and color info import * Got shape name loading to work * Trying to get approximate tuple comparison working * Black fix * Added a test for a bad filename, and added a custom color * Increase test coverage a bit and improve color name check * Removing code that should never be hit * Still trying to increase test coverage * Added a test for a plain assembly * Refactored a bit to support nested assemblies better in the future * Fixed location handling for components of the assembly * Fix the default color to not be black * Fixed bug with parent location not being applied when needed * Fixed importer for Assembly.export method * Fixed comment * Removed a stray import that was probably added by AI somewhere along the line. * Implement some of the suggestions * Added a test for nested subassemblies on import * Rework which covers everything except subshapes and layers * Added layer name support back in * Added a round-trip test and fixed issues that it revealed * mypy fixes * More mypy fixes * Missed a cast * More mypy fixes * Tried to remove the attribute iterator and could not, but moved some code out of loop * Fixes and simplifications based on codecov checks * More cleanup to try to get code coverage high enough without creating a contrived STEP file * Fix lack of application to the top-level assembly * Trying to increase test coverage high enough * Implemented changes based on PR from Adam * Fix mypy errors * Add isSubshape * Add __getitem__ * Mypy fix and a usability tweak. * Generic support for subshape metadata * Rework assy import * Start reworking tests * Streamline a bit * Add validation when adding subshapes * Fix pickling * Simplify assy import * Fix subshape name handling * Simplify * Additional test * Better test * Remove some changes * Fix some warnings * Fix top level key in .objects * Initial commit * mypy fix * Check .objects keys in the test * Black fix * Rework * Mypy fix * Fix name * Add xbf and start using xml xcaf * Test with a simpler model * Start on xml/xbf import * Fix path handling * Rework color handling * Fix subshapes for xbf/xml * Cleanup * Remove additional levels and add/fix roundtrip tests * Tweak tests * Eliminate dead code * Docs update * Typo fixes + clarification * typo fix --------- Co-authored-by: AU <adam-urbanczyk@users.noreply.github.com> Co-authored-by: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com>
1 parent d338160 commit 2da12fc

File tree

9 files changed

+1305
-324
lines changed

9 files changed

+1305
-324
lines changed

cadquery/assembly.py

Lines changed: 129 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
cast,
1212
get_args,
1313
)
14-
from typing_extensions import Literal
14+
from typing_extensions import Literal, Self
1515
from typish import instance_of
1616
from uuid import uuid1 as uuid
17+
from warnings import warn
1718

1819
from .cq import Workplane
19-
from .occ_impl.shapes import Shape, Compound
20+
from .occ_impl.shapes import Shape, Compound, isSubshape
2021
from .occ_impl.geom import Location
2122
from .occ_impl.assembly import Color
2223
from .occ_impl.solver import (
@@ -34,13 +35,15 @@
3435
exportGLTF,
3536
STEPExportModeLiterals,
3637
)
38+
from .occ_impl.importers.assembly import importStep as _importStep, importXbf, importXml
3739

3840
from .selectors import _expression_grammar as _selector_grammar
3941
from .utils import deprecate
4042

4143
# type definitions
4244
AssemblyObjects = Union[Shape, Workplane, None]
43-
ExportLiterals = Literal["STEP", "XML", "GLTF", "VTKJS", "VRML", "STL"]
45+
ImportLiterals = Literal["STEP", "XML", "XBF"]
46+
ExportLiterals = Literal["STEP", "XML", "XBF", "GLTF", "VTKJS", "VRML", "STL"]
4447

4548
PATH_DELIM = "/"
4649

@@ -155,6 +158,10 @@ def _copy(self) -> "Assembly":
155158

156159
rv = self.__class__(self.obj, self.loc, self.name, self.color, self.metadata)
157160

161+
rv._subshape_colors = dict(self._subshape_colors)
162+
rv._subshape_names = dict(self._subshape_names)
163+
rv._subshape_layers = dict(self._subshape_layers)
164+
158165
for ch in self.children:
159166
ch_copy = ch._copy()
160167
ch_copy.parent = rv
@@ -172,7 +179,7 @@ def add(
172179
loc: Optional[Location] = None,
173180
name: Optional[str] = None,
174181
color: Optional[Color] = None,
175-
) -> "Assembly":
182+
) -> Self:
176183
"""
177184
Add a subassembly to the current assembly.
178185
@@ -194,7 +201,7 @@ def add(
194201
name: Optional[str] = None,
195202
color: Optional[Color] = None,
196203
metadata: Optional[Dict[str, Any]] = None,
197-
) -> "Assembly":
204+
) -> Self:
198205
"""
199206
Add a subassembly to the current assembly with explicit location and name.
200207
@@ -344,11 +351,11 @@ def _subloc(self, name: str) -> Tuple[Location, str]:
344351
@overload
345352
def constrain(
346353
self, q1: str, q2: str, kind: ConstraintKind, param: Any = None
347-
) -> "Assembly":
354+
) -> Self:
348355
...
349356

350357
@overload
351-
def constrain(self, q1: str, kind: ConstraintKind, param: Any = None) -> "Assembly":
358+
def constrain(self, q1: str, kind: ConstraintKind, param: Any = None) -> Self:
352359
...
353360

354361
@overload
@@ -360,13 +367,13 @@ def constrain(
360367
s2: Shape,
361368
kind: ConstraintKind,
362369
param: Any = None,
363-
) -> "Assembly":
370+
) -> Self:
364371
...
365372

366373
@overload
367374
def constrain(
368375
self, id1: str, s1: Shape, kind: ConstraintKind, param: Any = None,
369-
) -> "Assembly":
376+
) -> Self:
370377
...
371378

372379
def constrain(self, *args, param=None):
@@ -411,7 +418,7 @@ def constrain(self, *args, param=None):
411418

412419
return self
413420

414-
def solve(self, verbosity: int = 0) -> "Assembly":
421+
def solve(self, verbosity: int = 0) -> Self:
415422
"""
416423
Solve the constraints.
417424
"""
@@ -506,7 +513,7 @@ def save(
506513
tolerance: float = 0.1,
507514
angularTolerance: float = 0.1,
508515
**kwargs,
509-
) -> "Assembly":
516+
) -> Self:
510517
"""
511518
Save assembly to a file.
512519
@@ -521,38 +528,9 @@ def save(
521528
:type ascii: bool
522529
"""
523530

524-
# Make sure the export mode setting is correct
525-
if mode not in get_args(STEPExportModeLiterals):
526-
raise ValueError(f"Unknown assembly export mode {mode} for STEP")
527-
528-
if exportType is None:
529-
t = path.split(".")[-1].upper()
530-
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
531-
exportType = cast(ExportLiterals, t)
532-
else:
533-
raise ValueError("Unknown extension, specify export type explicitly")
534-
535-
if exportType == "STEP":
536-
exportAssembly(self, path, mode, **kwargs)
537-
elif exportType == "XML":
538-
exportCAF(self, path)
539-
elif exportType == "VRML":
540-
exportVRML(self, path, tolerance, angularTolerance)
541-
elif exportType == "GLTF" or exportType == "GLB":
542-
exportGLTF(self, path, None, tolerance, angularTolerance)
543-
elif exportType == "VTKJS":
544-
exportVTKJS(self, path)
545-
elif exportType == "STL":
546-
# Handle the ascii setting for STL export
547-
export_ascii = False
548-
if "ascii" in kwargs:
549-
export_ascii = bool(kwargs.get("ascii"))
550-
551-
self.toCompound().exportStl(path, tolerance, angularTolerance, export_ascii)
552-
else:
553-
raise ValueError(f"Unknown format: {exportType}")
554-
555-
return self
531+
return self.export(
532+
path, exportType, mode, tolerance, angularTolerance, **kwargs
533+
)
556534

557535
def export(
558536
self,
@@ -562,7 +540,7 @@ def export(
562540
tolerance: float = 0.1,
563541
angularTolerance: float = 0.1,
564542
**kwargs,
565-
) -> "Assembly":
543+
) -> Self:
566544
"""
567545
Save assembly to a file.
568546
@@ -583,7 +561,7 @@ def export(
583561

584562
if exportType is None:
585563
t = path.split(".")[-1].upper()
586-
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
564+
if t in ("STEP", "XML", "XBF", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
587565
exportType = cast(ExportLiterals, t)
588566
else:
589567
raise ValueError("Unknown extension, specify export type explicitly")
@@ -592,6 +570,8 @@ def export(
592570
exportAssembly(self, path, mode, **kwargs)
593571
elif exportType == "XML":
594572
exportCAF(self, path)
573+
elif exportType == "XBF":
574+
exportCAF(self, path, binary=True)
595575
elif exportType == "VRML":
596576
exportVRML(self, path, tolerance, angularTolerance)
597577
elif exportType == "GLTF" or exportType == "GLB":
@@ -611,9 +591,39 @@ def export(
611591
return self
612592

613593
@classmethod
614-
def load(cls, path: str) -> "Assembly":
594+
def importStep(cls, path: str) -> Self:
595+
"""
596+
Reads an assembly from a STEP file.
615597
616-
raise NotImplementedError
598+
:param path: Path and filename for reading.
599+
:return: An Assembly object.
600+
"""
601+
602+
return cls.load(path, importType="STEP")
603+
604+
@classmethod
605+
def load(cls, path: str, importType: Optional[ImportLiterals] = None,) -> Self:
606+
"""
607+
Load step, xbf or xml.
608+
"""
609+
610+
if importType is None:
611+
t = path.split(".")[-1].upper()
612+
if t in ("STEP", "XML", "XBF"):
613+
importType = cast(ImportLiterals, t)
614+
else:
615+
raise ValueError("Unknown extension, specify export type explicitly")
616+
617+
assy = cls()
618+
619+
if importType == "STEP":
620+
_importStep(assy, path)
621+
elif importType == "XML":
622+
importXml(assy, path)
623+
elif importType == "XBF":
624+
importXbf(assy, path)
625+
626+
return assy
617627

618628
@property
619629
def shapes(self) -> List[Shape]:
@@ -714,12 +724,81 @@ def addSubshape(
714724
:return: The modified assembly.
715725
"""
716726

727+
# check if the subshape belongs to the stored object
728+
if any(isSubshape(s, obj) for obj in self.shapes):
729+
assy = self
730+
else:
731+
warn(
732+
"Current node does not contain any Shapes, searching in subnodes. In the future this will result in an error."
733+
)
734+
735+
found = False
736+
for ch in self.children:
737+
if any(isSubshape(s, obj) for obj in ch.shapes):
738+
assy = ch
739+
found = True
740+
break
741+
742+
if not found:
743+
raise ValueError(
744+
f"{s} is not a subshape of the current node or its children"
745+
)
746+
717747
# Handle any metadata we were passed
718748
if name:
719-
self._subshape_names[s] = name
749+
assy._subshape_names[s] = name
720750
if color:
721-
self._subshape_colors[s] = color
751+
assy._subshape_colors[s] = color
722752
if layer:
723-
self._subshape_layers[s] = layer
753+
assy._subshape_layers[s] = layer
724754

725755
return self
756+
757+
def __getitem__(self, name: str) -> "Assembly":
758+
"""
759+
[] based access to children.
760+
"""
761+
762+
return self.objects[name]
763+
764+
def _ipython_key_completions_(self) -> List[str]:
765+
"""
766+
IPython autocompletion helper.
767+
"""
768+
769+
return list(self.objects.keys())
770+
771+
def __contains__(self, name: str) -> bool:
772+
773+
return name in self.objects
774+
775+
def __getattr__(self, name: str) -> "Assembly":
776+
"""
777+
. based access to children.
778+
"""
779+
780+
if name in self.objects:
781+
return self.objects[name]
782+
783+
raise AttributeError
784+
785+
def __dir__(self):
786+
"""
787+
Modified __dir__ for autocompletion.
788+
"""
789+
790+
return list(self.__dict__) + list(ch.name for ch in self.children)
791+
792+
def __getstate__(self):
793+
"""
794+
Explicit getstate needed due to getattr.
795+
"""
796+
797+
return self.__dict__
798+
799+
def __setstate__(self, d):
800+
"""
801+
Explicit setstate needed due to getattr.
802+
"""
803+
804+
self.__dict__ = d

cadquery/func.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@
5151
setThreads,
5252
project,
5353
faceOn,
54+
isSubshape,
5455
)

0 commit comments

Comments
 (0)