Skip to content

Commit 568a875

Browse files
Enable bidirectional mapping of subshapes (#1949)
* Add assy changes * Extend special methods to handle subshape names --------- Co-authored-by: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com>
1 parent 80f69fb commit 568a875

5 files changed

Lines changed: 95 additions & 25 deletions

File tree

cadquery/assembly.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
from typish import instance_of
1616
from uuid import uuid1 as uuid
1717
from warnings import warn
18+
from itertools import chain
1819

1920
from .cq import Workplane
20-
from .occ_impl.shapes import Shape, Compound, isSubshape
21+
from .occ_impl.shapes import Shape, Compound, isSubshape, compound
2122
from .occ_impl.geom import Location
2223
from .occ_impl.assembly import Color
2324
from .occ_impl.solver import (
@@ -38,7 +39,7 @@
3839
from .occ_impl.importers.assembly import importStep as _importStep, importXbf, importXml
3940

4041
from .selectors import _expression_grammar as _selector_grammar
41-
from .utils import deprecate
42+
from .utils import deprecate, BiDict
4243

4344
# type definitions
4445
AssemblyObjects = Union[Shape, Workplane, None]
@@ -48,6 +49,8 @@
4849
PATH_DELIM = "/"
4950

5051
# entity selector grammar definition
52+
53+
5154
def _define_grammar():
5255

5356
from pyparsing import (
@@ -98,9 +101,9 @@ class Assembly(object):
98101
constraints: List[Constraint]
99102

100103
# Allows metadata to be stored for exports
101-
_subshape_names: dict[Shape, str]
102-
_subshape_colors: dict[Shape, Color]
103-
_subshape_layers: dict[Shape, str]
104+
_subshape_names: BiDict[Shape, str]
105+
_subshape_colors: BiDict[Shape, Color]
106+
_subshape_layers: BiDict[Shape, str]
104107

105108
_solve_result: Optional[Dict[str, Any]]
106109

@@ -147,9 +150,9 @@ def __init__(
147150

148151
self._solve_result = None
149152

150-
self._subshape_names = {}
151-
self._subshape_colors = {}
152-
self._subshape_layers = {}
153+
self._subshape_names = BiDict()
154+
self._subshape_colors = BiDict()
155+
self._subshape_layers = BiDict()
153156

154157
def _copy(self) -> "Assembly":
155158
"""
@@ -158,9 +161,9 @@ def _copy(self) -> "Assembly":
158161

159162
rv = self.__class__(self.obj, self.loc, self.name, self.color, self.metadata)
160163

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+
rv._subshape_colors = BiDict(self._subshape_colors)
165+
rv._subshape_names = BiDict(self._subshape_names)
166+
rv._subshape_layers = BiDict(self._subshape_layers)
164167

165168
for ch in self.children:
166169
ch_copy = ch._copy()
@@ -754,40 +757,54 @@ def addSubshape(
754757

755758
return self
756759

757-
def __getitem__(self, name: str) -> "Assembly":
760+
def __getitem__(self, name: str) -> Union["Assembly", Shape]:
758761
"""
759762
[] based access to children.
763+
760764
"""
761765

762-
return self.objects[name]
766+
if name in self.objects:
767+
return self.objects[name]
768+
elif name in self._subshape_names.inv:
769+
rv = self._subshape_names.inv[name]
770+
return rv[0] if len(rv) == 1 else compound(rv)
771+
772+
raise KeyError
763773

764774
def _ipython_key_completions_(self) -> List[str]:
765775
"""
766776
IPython autocompletion helper.
767777
"""
768778

769-
return list(self.objects.keys())
779+
return list(chain(self.objects.keys(), self._subshape_names.inv.keys()))
770780

771781
def __contains__(self, name: str) -> bool:
772782

773-
return name in self.objects
783+
return name in self.objects or name in self._subshape_names.inv
774784

775-
def __getattr__(self, name: str) -> "Assembly":
785+
def __getattr__(self, name: str) -> Union["Assembly", Shape]:
776786
"""
777787
. based access to children.
778788
"""
779789

780790
if name in self.objects:
781791
return self.objects[name]
792+
elif name in self._subshape_names.inv:
793+
rv = self._subshape_names.inv[name]
794+
return rv[0] if len(rv) == 1 else compound(rv)
782795

783-
raise AttributeError
796+
raise AttributeError(f"{name} is not an attribute of {self}")
784797

785798
def __dir__(self):
786799
"""
787800
Modified __dir__ for autocompletion.
788801
"""
789802

790-
return list(self.__dict__) + list(ch.name for ch in self.children)
803+
return (
804+
list(self.__dict__)
805+
+ list(ch.name for ch in self.children)
806+
+ list(self._subshape_names.inv.keys())
807+
)
791808

792809
def __getstate__(self):
793810
"""

cadquery/occ_impl/assembly.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from .shapes import Shape, Solid, Compound
5353
from .exporters.vtk import toString
5454
from ..cq import Workplane
55+
from ..utils import BiDict
5556

5657
# type definitions
5758
AssemblyObjects = Union[Shape, Workplane, None]
@@ -210,15 +211,15 @@ def children(self) -> Iterable["AssemblyProtocol"]:
210211
...
211212

212213
@property
213-
def _subshape_names(self) -> Dict[Shape, str]:
214+
def _subshape_names(self) -> BiDict[Shape, str]:
214215
...
215216

216217
@property
217-
def _subshape_colors(self) -> Dict[Shape, Color]:
218+
def _subshape_colors(self) -> BiDict[Shape, Color]:
218219
...
219220

220221
@property
221-
def _subshape_layers(self) -> Dict[Shape, str]:
222+
def _subshape_layers(self) -> BiDict[Shape, str]:
222223
...
223224

224225
@overload
@@ -276,7 +277,7 @@ def __iter__(
276277
) -> Iterator[Tuple[Shape, str, Location, Optional[Color]]]:
277278
...
278279

279-
def __getitem__(self, name: str) -> Self:
280+
def __getitem__(self, name: str) -> Self | Shape:
280281
...
281282

282283
def __contains__(self, name: str) -> bool:

cadquery/occ_impl/importers/assembly.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
253253
parent.add(tmp)
254254

255255
# change the current assy to handle subshape data
256-
current = parent[comp_name]
256+
current = cast(AssemblyProtocol, parent[comp_name])
257257

258258
# iterate over subshape and handle names, layers and colors
259259
subshape_labels = TDF_LabelSequence()
@@ -326,6 +326,7 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
326326
assy.objects.pop(assy.name)
327327
assy.name = str(name_attr.Get().ToExtString())
328328
assy.objects[assy.name] = assy
329+
329330
if cq_color:
330331
assy.color = cq_color
331332

@@ -350,7 +351,7 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
350351
# extras on successive round-trips. exportStepMeta does not add the extra top-level
351352
# node and so does not exhibit this behavior.
352353
if assy.name in imported_assy:
353-
imported_assy = imported_assy[assy.name]
354+
imported_assy = cast(AssemblyProtocol, imported_assy[assy.name])
354355
# comp_labels = TDF_LabelSequence()
355356
# shape_tool.GetComponents_s(top_level_label, comp_labels)
356357
# comp_label = comp_labels.Value(1)

cadquery/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from inspect import signature, isbuiltin
33
from typing import TypeVar, Callable, cast
44
from warnings import warn
5+
from collections import UserDict
56

67
from multimethod import multimethod, DispatchError
78

@@ -83,3 +84,35 @@ def get_arity(f: TCallable) -> int:
8384
rv = f.__code__.co_argcount - n_defaults
8485

8586
return rv
87+
88+
89+
K = TypeVar("K")
90+
V = TypeVar("V")
91+
92+
93+
class BiDict(UserDict[K, V]):
94+
"""
95+
Bi-directional dictionary.
96+
"""
97+
98+
_inv: dict[V, list[K]]
99+
100+
def __init__(self, *args, **kwargs):
101+
102+
self._inv = {}
103+
104+
super().__init__(*args, **kwargs)
105+
106+
def __setitem__(self, k: K, v: V):
107+
108+
super().__setitem__(k, v)
109+
110+
if v in self._inv:
111+
self._inv[v].append(k)
112+
else:
113+
self._inv[v] = [k]
114+
115+
@property
116+
def inv(self) -> dict[V, list[K]]:
117+
118+
return self._inv

tests/test_assembly.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
exportVRML,
1818
)
1919
from cadquery.occ_impl.assembly import toJSON, toCAF, toFusedCAF
20-
from cadquery.occ_impl.shapes import Face, box, cone, plane
20+
from cadquery.occ_impl.shapes import Face, box, cone, plane, Compound
2121

2222
from OCP.gp import gp_XYZ
2323
from OCP.TDocStd import TDocStd_Document
@@ -407,6 +407,10 @@ def subshape_assy():
407407
layer="cylinder_bottom_wire_layer",
408408
)
409409

410+
# Add two subshapes with the same name
411+
assy["cyl_1"].addSubshape(cyl_1.faces(">Z").val(), name="2_faces")
412+
assy["cyl_1"].addSubshape(cyl_1.faces("<Z").val(), name="2_faces")
413+
410414
return assy
411415

412416

@@ -2330,6 +2334,20 @@ def test_special_methods(subshape_assy):
23302334
subshape_assy.cube_123456
23312335

23322336

2337+
def test_subshape_access(subshape_assy):
2338+
"""
2339+
Smoke-test subshape access.
2340+
"""
2341+
2342+
assert "cube_1_top_face" in subshape_assy.cube_1.__dir__()
2343+
assert "cube_1_top_face" in subshape_assy.cube_1._ipython_key_completions_()
2344+
assert "cube_1_top_face" in subshape_assy.cube_1
2345+
2346+
assert isinstance(subshape_assy.cube_1.cube_1_top_face, Face)
2347+
assert isinstance(subshape_assy.cube_1["cube_1_top_face"], Face)
2348+
assert isinstance(subshape_assy.cyl_1["2_faces"], Compound)
2349+
2350+
23332351
def test_shallow_assy():
23342352
"""
23352353
toCAF edge case.

0 commit comments

Comments
 (0)