Skip to content

Commit 4745ad1

Browse files
Assembly export: do not add leaf component when shapes is empty (#993) (#1157)
* Assembly export: do not add leaf component when shapes is empty (#993) * Add test to validate number of leaf nodes in OCAF data * mypy fix * Remove len call * Improve assembly makeCompound and color handling for glTF, STEP * Create single compound for multiple instances of same shape * Fix glTF, STEP export color handling * Special handling for part names and naming convention for multiple instances of a shape * Change default glTF mesh tolerance to be consistent with other formats * Allow creation of default Color (when color name, tuple values unspecified) * Add tests of assembly colors including STEP export * Fix tests on Python 3.8 * Apply changes from code review * Add assembly children recursively * Remove STEP part naming special handling * Add glTF test to check for missing mesh * Extend copy * Refactor to use the updated copy method * Black fix Co-authored-by: AU <[email protected]>
1 parent 369fcd9 commit 4745ad1

File tree

4 files changed

+619
-42
lines changed

4 files changed

+619
-42
lines changed

cadquery/occ_impl/assembly.py

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Iterable, Tuple, Dict, overload, Optional, Any, List
1+
from typing import Union, Iterable, Tuple, Dict, overload, Optional, Any, List
22
from typing_extensions import Protocol
33
from math import degrees
44

@@ -20,6 +20,10 @@
2020
from .geom import Location
2121
from .shapes import Shape, Compound
2222
from .exporters.vtk import toString
23+
from ..cq import Workplane
24+
25+
# type definitions
26+
AssemblyObjects = Union[Shape, Workplane, None]
2327

2428

2529
class Color(object):
@@ -50,9 +54,18 @@ def __init__(self, r: float, g: float, b: float, a: float = 0):
5054
"""
5155
...
5256

57+
@overload
58+
def __init__(self):
59+
"""
60+
Construct a Color with default value.
61+
"""
62+
...
63+
5364
def __init__(self, *args, **kwargs):
5465

55-
if len(args) == 1:
66+
if len(args) == 0:
67+
self.wrapped = Quantity_ColorRGBA()
68+
elif len(args) == 1:
5669
self.wrapped = Quantity_ColorRGBA()
5770
exists = Quantity_ColorRGBA.ColorFromName_s(args[0], self.wrapped)
5871
if not exists:
@@ -122,7 +135,11 @@ def setColor(l: TDF_Label, color: Color, tool):
122135

123136

124137
def toCAF(
125-
assy: AssemblyProtocol, coloredSTEP: bool = False
138+
assy: AssemblyProtocol,
139+
coloredSTEP: bool = False,
140+
mesh: bool = False,
141+
tolerance: float = 1e-3,
142+
angularTolerance: float = 0.1,
126143
) -> Tuple[TDF_Label, TDocStd_Document]:
127144

128145
# prepare a doc
@@ -139,40 +156,63 @@ def toCAF(
139156
top = tool.NewShape()
140157
TDataStd_Name.Set_s(top, TCollection_ExtendedString("CQ assembly"))
141158

142-
# add leafs and subassemblies
143-
subassys: Dict[str, Tuple[TDF_Label, Location]] = {}
144-
for k, v in assy.traverse():
145-
# leaf part
146-
lab = tool.NewShape()
147-
tool.SetShape(lab, Compound.makeCompound(v.shapes).wrapped)
148-
setName(lab, f"{k}_part", tool)
159+
# used to store labels with unique part-color combinations
160+
unique_objs: Dict[Tuple[Color, AssemblyObjects], TDF_Label] = {}
161+
# used to cache unique, possibly meshed, compounds; allows to avoid redundant meshing operations if same object is referenced multiple times in an assy
162+
compounds: Dict[AssemblyObjects, Compound] = {}
163+
164+
def _toCAF(el, ancestor, color):
149165

150-
# assy part
166+
# create a subassy
151167
subassy = tool.NewShape()
152-
tool.AddComponent(subassy, lab, TopLoc_Location())
153-
setName(subassy, k, tool)
154-
155-
# handle colors - this logic is needed for proper STEP export
156-
color = v.color
157-
tmp = v
158-
if coloredSTEP:
159-
while not color and tmp.parent:
160-
tmp = tmp.parent
161-
color = tmp.color
162-
if color:
163-
setColor(lab, color, ctool)
164-
else:
165-
if color:
166-
setColor(subassy, color, ctool)
168+
setName(subassy, el.name, tool)
169+
170+
# define the current color
171+
current_color = el.color if el.color else color
172+
173+
# add a leaf with the actual part if needed
174+
if el.obj:
175+
# get/register unique parts referenced in the assy
176+
key0 = (current_color, el.obj) # (color, shape)
177+
key1 = el.obj # shape
178+
179+
if key0 in unique_objs:
180+
lab = unique_objs[key0]
181+
else:
182+
lab = tool.NewShape()
183+
if key1 in compounds:
184+
compound = compounds[key1].copy(mesh)
185+
else:
186+
compound = Compound.makeCompound(el.shapes)
187+
if mesh:
188+
compound.mesh(tolerance, angularTolerance)
189+
190+
compounds[key1] = compound
191+
192+
tool.SetShape(lab, compound.wrapped)
193+
setName(lab, f"{el.name}_part", tool)
194+
unique_objs[key0] = lab
195+
196+
# handle colors when exporting to STEP
197+
if coloredSTEP and current_color:
198+
setColor(lab, current_color, ctool)
199+
200+
tool.AddComponent(subassy, lab, TopLoc_Location())
201+
202+
# handle colors when *not* exporting to STEP
203+
if not coloredSTEP and current_color:
204+
setColor(subassy, current_color, ctool)
205+
206+
# add children recursively
207+
for child in el.children:
208+
_toCAF(child, subassy, current_color)
167209

168-
subassys[k] = (subassy, v.loc)
210+
# add the current subassy to the higher level assy
211+
tool.AddComponent(ancestor, subassy, el.loc.wrapped)
169212

170-
for ch in v.children:
171-
tool.AddComponent(
172-
subassy, subassys[ch.name][0], subassys[ch.name][1].wrapped
173-
)
213+
# process the whole assy recursively
214+
_toCAF(assy, top, None)
174215

175-
tool.AddComponent(top, subassys[assy.name][0], assy.loc.wrapped)
176216
tool.UpdateAssemblies()
177217

178218
return top, doc

cadquery/occ_impl/exporters/assembly.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def exportGLTF(
155155
assy: AssemblyProtocol,
156156
path: str,
157157
binary: bool = True,
158-
tolerance: float = 0.1,
158+
tolerance: float = 1e-3,
159159
angularTolerance: float = 0.1,
160160
):
161161
"""
@@ -167,12 +167,7 @@ def exportGLTF(
167167
orig_loc = assy.loc
168168
assy.loc *= Location((0, 0, 0), (1, 0, 0), -90)
169169

170-
# mesh all the shapes
171-
for _, el in assy.traverse():
172-
for s in el.shapes:
173-
s.mesh(tolerance, angularTolerance)
174-
175-
_, doc = toCAF(assy, True)
170+
_, doc = toCAF(assy, True, True, tolerance, angularTolerance)
176171

177172
writer = RWGltf_CafWriter(TCollection_AsciiString(path), binary)
178173
result = writer.Perform(

cadquery/occ_impl/shapes.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,12 +912,15 @@ def scale(self, factor: float) -> "Shape":
912912

913913
return self._apply_transform(T)
914914

915-
def copy(self: T) -> T:
915+
def copy(self: T, mesh: bool = False) -> T:
916916
"""
917917
Creates a new object that is a copy of this object.
918+
919+
:param mesh: should I copy the triangulation too (default: False)
920+
:returns: a copy of the object
918921
"""
919922

920-
return self.__class__(BRepBuilderAPI_Copy(self.wrapped).Shape())
923+
return self.__class__(BRepBuilderAPI_Copy(self.wrapped, True, mesh).Shape())
921924

922925
def transformShape(self, tMatrix: Matrix) -> "Shape":
923926
"""

0 commit comments

Comments
 (0)