Skip to content

Commit db62a1d

Browse files
STEP Export with Face Metadata (Names and Colors) (#1782)
* Moved name, color and layer metadata into cq.Assembly * Started trying to exert more control over the internal structure of the STEP structure * Working implementation that works with deeply nested assemblies * Added write_pcurves setting to increase test coverage * Added mention of the metadata export method in the documentation * Removed extra properties from docstring --------- Co-authored-by: AU <[email protected]>
1 parent 76ed228 commit db62a1d

File tree

5 files changed

+413
-2
lines changed

5 files changed

+413
-2
lines changed

cadquery/assembly.py

+36
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ class Assembly(object):
9494
objects: Dict[str, "Assembly"]
9595
constraints: List[Constraint]
9696

97+
# Allows metadata to be stored for exports
98+
_subshape_names: dict[Shape, str]
99+
_subshape_colors: dict[Shape, Color]
100+
_subshape_layers: dict[Shape, str]
101+
97102
_solve_result: Optional[Dict[str, Any]]
98103

99104
def __init__(
@@ -139,6 +144,10 @@ def __init__(
139144

140145
self._solve_result = None
141146

147+
self._subshape_names = {}
148+
self._subshape_colors = {}
149+
self._subshape_layers = {}
150+
142151
def _copy(self) -> "Assembly":
143152
"""
144153
Make a deep copy of an assembly
@@ -685,3 +694,30 @@ def _repr_javascript_(self):
685694
from .occ_impl.jupyter_tools import display
686695

687696
return display(self)._repr_javascript_()
697+
698+
def addSubshape(
699+
self,
700+
s: Shape,
701+
name: Optional[str] = None,
702+
color: Optional[Color] = None,
703+
layer: Optional[str] = None,
704+
) -> "Assembly":
705+
"""
706+
Handles name, color and layer metadata for subshapes.
707+
708+
:param s: The subshape to add metadata to.
709+
:param name: The name to assign to the subshape.
710+
:param color: The color to assign to the subshape.
711+
:param layer: The layer to assign to the subshape.
712+
:return: The modified assembly.
713+
"""
714+
715+
# Handle any metadata we were passed
716+
if name:
717+
self._subshape_names[s] = name
718+
if color:
719+
self._subshape_colors[s] = color
720+
if layer:
721+
self._subshape_layers[s] = layer
722+
723+
return self

cadquery/occ_impl/assembly.py

+12
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ def shapes(self) -> Iterable[Shape]:
160160
def children(self) -> Iterable["AssemblyProtocol"]:
161161
...
162162

163+
@property
164+
def _subshape_names(self) -> Dict[Shape, str]:
165+
...
166+
167+
@property
168+
def _subshape_colors(self) -> Dict[Shape, Color]:
169+
...
170+
171+
@property
172+
def _subshape_layers(self) -> Dict[Shape, str]:
173+
...
174+
163175
def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]:
164176
...
165177

cadquery/occ_impl/exporters/assembly.py

+169-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
from OCP.STEPCAFControl import STEPCAFControl_Writer
1515
from OCP.STEPControl import STEPControl_StepModelType
1616
from OCP.IFSelect import IFSelect_ReturnStatus
17+
from OCP.TDF import TDF_Label
18+
from OCP.TDataStd import TDataStd_Name
19+
from OCP.TDocStd import TDocStd_Document
1720
from OCP.XCAFApp import XCAFApp_Application
21+
from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorGen
1822
from OCP.XmlDrivers import (
1923
XmlDrivers_DocumentStorageDriver,
2024
XmlDrivers_DocumentRetrievalDriver,
@@ -28,6 +32,8 @@
2832

2933
from ..assembly import AssemblyProtocol, toCAF, toVTK, toFusedCAF
3034
from ..geom import Location
35+
from ..shapes import Shape, Compound
36+
from ..assembly import Color
3137

3238

3339
class ExportModes:
@@ -42,7 +48,7 @@ def exportAssembly(
4248
assy: AssemblyProtocol,
4349
path: str,
4450
mode: STEPExportModeLiterals = "default",
45-
**kwargs
51+
**kwargs,
4652
) -> bool:
4753
"""
4854
Export an assembly to a STEP file.
@@ -99,6 +105,168 @@ def exportAssembly(
99105
return status == IFSelect_ReturnStatus.IFSelect_RetDone
100106

101107

108+
def exportStepMeta(
109+
assy: AssemblyProtocol,
110+
path: str,
111+
write_pcurves: bool = True,
112+
precision_mode: int = 0,
113+
) -> bool:
114+
"""
115+
Export an assembly to a STEP file with faces tagged with names and colors. This is done as a
116+
separate method from the main STEP export because this is not compatible with the fused mode
117+
and also flattens the hierarchy of the STEP.
118+
119+
Layers are used because some software does not understand the ADVANCED_FACE entity and needs
120+
names attached to layers instead.
121+
122+
:param assy: assembly
123+
:param path: Path and filename for writing
124+
:param write_pcurves: Enable or disable writing parametric curves to the STEP file. Default True.
125+
If False, writes STEP file without pcurves. This decreases the size of the resulting STEP file.
126+
:param precision_mode: Controls the uncertainty value for STEP entities. Specify -1, 0, or 1. Default 0.
127+
See OCCT documentation.
128+
"""
129+
130+
pcurves = 1
131+
if not write_pcurves:
132+
pcurves = 0
133+
134+
# Initialize the XCAF document that will allow the STEP export
135+
app = XCAFApp_Application.GetApplication_s()
136+
doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf"))
137+
app.InitDocument(doc)
138+
139+
# Shape and color tools
140+
shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())
141+
color_tool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())
142+
layer_tool = XCAFDoc_DocumentTool.LayerTool_s(doc.Main())
143+
144+
def _process_child(child: AssemblyProtocol, assy_label: TDF_Label):
145+
"""
146+
Process a child part which is not a subassembly.
147+
:param child: Child part to process (we should already have filtered out subassemblies)
148+
:param assy_label: The label for the assembly to add this part to
149+
:return: None
150+
"""
151+
152+
child_items = None
153+
154+
# We combine these because the metadata could be stored at the parent or child level
155+
combined_names = {**assy._subshape_names, **child._subshape_names}
156+
combined_colors = {**assy._subshape_colors, **child._subshape_colors}
157+
combined_layers = {**assy._subshape_layers, **child._subshape_layers}
158+
159+
# Collect all of the shapes in the child object
160+
if child.obj:
161+
child_items = (
162+
child.obj
163+
if isinstance(child.obj, Shape)
164+
else Compound.makeCompound(
165+
s for s in child.obj.vals() if isinstance(s, Shape)
166+
),
167+
child.name,
168+
child.loc,
169+
child.color,
170+
)
171+
172+
if child_items:
173+
shape, name, loc, color = child_items
174+
175+
# Handle shape name, color and location
176+
part_label = shape_tool.AddShape(shape.wrapped, False)
177+
TDataStd_Name.Set_s(part_label, TCollection_ExtendedString(name))
178+
if color:
179+
color_tool.SetColor(part_label, color.wrapped, XCAFDoc_ColorGen)
180+
shape_tool.AddComponent(assy_label, part_label, loc.wrapped)
181+
182+
# If this assembly has shape metadata, add it to the shape
183+
if (
184+
len(combined_names) > 0
185+
or len(combined_colors) > 0
186+
or len(combined_layers) > 0
187+
):
188+
names = combined_names
189+
colors = combined_colors
190+
layers = combined_layers
191+
192+
# Step through every face in the shape, and see if any metadata needs to be attached to it
193+
for face in shape.Faces():
194+
if face in names or face in shape in colors or face in layers:
195+
# Add the face as a subshape
196+
face_label = shape_tool.AddSubShape(part_label, face.wrapped)
197+
198+
# In some cases the face may not be considered part of the shape, so protect
199+
# against that
200+
if not face_label.IsNull():
201+
# Set the ADVANCED_FACE label, even though the layer holds the same data
202+
if face in names:
203+
TDataStd_Name.Set_s(
204+
face_label, TCollection_ExtendedString(names[face])
205+
)
206+
207+
# Set the individual face color
208+
if face in colors:
209+
color_tool.SetColor(
210+
face_label, colors[face].wrapped, XCAFDoc_ColorGen,
211+
)
212+
213+
# Also add a layer to hold the face label data
214+
if face in layers:
215+
layer_label = layer_tool.AddLayer(
216+
TCollection_ExtendedString(layers[face])
217+
)
218+
layer_tool.SetLayer(face_label, layer_label)
219+
220+
def _process_assembly(
221+
assy: AssemblyProtocol, parent_label: Optional[TDF_Label] = None
222+
):
223+
"""
224+
Recursively process the assembly and its children.
225+
:param assy: Assembly to process
226+
:param parent_label: The parent label for the assembly
227+
:return: None
228+
"""
229+
# Use the assembly name if the user set it
230+
assembly_name = assy.name if assy.name else str(uuid.uuid1())
231+
232+
# Create the top level object that will hold all the subassemblies and parts
233+
assy_label = shape_tool.NewShape()
234+
TDataStd_Name.Set_s(assy_label, TCollection_ExtendedString(assembly_name))
235+
236+
# Handle subassemblies
237+
if parent_label:
238+
shape_tool.AddComponent(parent_label, assy_label, assy.loc.wrapped)
239+
240+
# The children may be parts or assemblies
241+
for child in assy.children:
242+
# Child is a part
243+
if len(list(child.children)) == 0:
244+
_process_child(child, assy_label)
245+
# Child is a subassembly
246+
else:
247+
_process_assembly(child, assy_label)
248+
249+
_process_assembly(assy)
250+
251+
# Update the assemblies
252+
shape_tool.UpdateAssemblies()
253+
254+
# Set up the writer and write the STEP file
255+
session = XSControl_WorkSession()
256+
writer = STEPCAFControl_Writer(session, False)
257+
Interface_Static.SetIVal_s("write.stepcaf.subshapes.name", 1)
258+
writer.SetColorMode(True)
259+
writer.SetLayerMode(True)
260+
writer.SetNameMode(True)
261+
Interface_Static.SetIVal_s("write.surfacecurve.mode", pcurves)
262+
Interface_Static.SetIVal_s("write.precision.mode", precision_mode)
263+
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
264+
265+
status = writer.Write(path)
266+
267+
return status == IFSelect_ReturnStatus.IFSelect_RetDone
268+
269+
102270
def exportCAF(assy: AssemblyProtocol, path: str) -> bool:
103271
"""
104272
Export an assembly to a OCAF xml file (internal OCCT format).

doc/importexport.rst

+26
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,32 @@ This is done by setting the name property of the assembly before calling :meth:`
205205
206206
If an assembly name is not specified, a UUID will be used to avoid name conflicts.
207207

208+
Exporting Assemblies to STEP with Metadata
209+
###########################################
210+
211+
It is possible to attach metadata to the assembly that will be included in the STEP file. This metadata can be attached to arbitrary shapes and includes names, colors and layers. This is done by using the :meth:`Assembly.addSubshape` method before calling :meth:`cadquery.occ_impl.exporters.assembly.exportStepMeta`.
212+
213+
.. code-block:: python
214+
215+
import cadquery as cq
216+
from cadquery.occ_impl.exporters.assembly import exportStepMeta
217+
218+
# Create a simple assembly
219+
assy = cq.Assembly(name="top-level")
220+
cube_1 = cq.Workplane().box(10.0, 10.0, 10.0)
221+
assy.add(cube_1, name="cube_1", color=cq.Color("green"))
222+
223+
# Add subshape name, color and layer
224+
assy.addSubshape(
225+
cube_1.faces(">Z").val(),
226+
name="cube_1_top_face",
227+
color=cq.Color("red"),
228+
layer="cube_1_top_face"
229+
)
230+
231+
# Export the assembly to STEP with metadata
232+
exportStepMeta(assy, "out.step")
233+
208234
Exporting Assemblies to glTF
209235
#############################
210236

0 commit comments

Comments
 (0)