Skip to content

Commit 372ae55

Browse files
Materials Support for STEP Import/Export (#1956)
* Added material import for round-trip STEP export/import * Added None to _get_material call * Parameterized the test for other formats * Fixed default material description and comment --------- Co-authored-by: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com>
1 parent 2e05819 commit 372ae55

4 files changed

Lines changed: 120 additions & 9 deletions

File tree

cadquery/assembly.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def add(
195195
loc: Optional[Location] = None,
196196
name: Optional[str] = None,
197197
color: Optional[Color] = None,
198+
material: Optional[Union[Material, str]] = None,
198199
) -> Self:
199200
"""
200201
Add a subassembly to the current assembly.

cadquery/occ_impl/assembly.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def __init__(self, name: str | None = None, **kwargs):
8383

8484
# Default values in case the user did not set any others
8585
aName = "Default"
86-
aDescription = "Default material with properties similar to low carbon steel"
86+
aDescription = ""
8787
aDensity = 7.85
8888
aDensityName = "Mass density"
8989
aDensityTypeName = "g/cm^3"
@@ -285,6 +285,7 @@ def __init__(
285285
loc: Optional[Location] = None,
286286
name: Optional[str] = None,
287287
color: Optional[Color] = None,
288+
material: Optional[Material] = None,
288289
):
289290
...
290291

@@ -316,6 +317,14 @@ def color(self) -> Optional[Color]:
316317
def color(self, value: Optional[Color]) -> None:
317318
...
318319

320+
@property
321+
def material(self) -> Optional[Material]:
322+
...
323+
324+
@material.setter
325+
def material(self, value: Optional[Material]) -> None:
326+
...
327+
319328
@property
320329
def obj(self) -> AssemblyObjects:
321330
...
@@ -355,6 +364,7 @@ def add(
355364
loc: Optional[Location] = None,
356365
name: Optional[str] = None,
357366
color: Optional[Color] = None,
367+
material: Optional[Union[Material, str]] = None,
358368
) -> Self:
359369
...
360370

@@ -422,6 +432,18 @@ def setColor(l: TDF_Label, color: Color, tool):
422432
tool.SetColor(l, color.wrapped, XCAFDoc_ColorType.XCAFDoc_ColorSurf)
423433

424434

435+
def setMaterial(l: TDF_Label, material: Material, tool):
436+
437+
tool.SetMaterial(
438+
l,
439+
TCollection_HAsciiString(material.name),
440+
TCollection_HAsciiString(material.description),
441+
material.density,
442+
TCollection_HAsciiString("MassDensity"),
443+
TCollection_HAsciiString(material.densityUnit),
444+
)
445+
446+
425447
def toCAF(
426448
assy: AssemblyProtocol,
427449
coloredSTEP: bool = False,
@@ -447,6 +469,7 @@ def toCAF(
447469
tool.SetAutoNaming_s(False)
448470
ctool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())
449471
ltool = XCAFDoc_DocumentTool.LayerTool_s(doc.Main())
472+
mtool = XCAFDoc_DocumentTool.MaterialTool_s(doc.Main())
450473

451474
# used to store labels with unique part-color combinations
452475
unique_objs: Dict[Tuple[Color | None, AssemblyObjects], TDF_Label] = {}
@@ -463,6 +486,9 @@ def _toCAF(el: AssemblyProtocol, ancestor: TDF_Label | None) -> TDF_Label:
463486
# define the current color
464487
current_color = el.color if el.color else None
465488

489+
# define the current material
490+
current_material = el.material if el.material else None
491+
466492
# add a leaf with the actual part if needed
467493
if el.obj:
468494
# get/register unique parts referenced in the assy
@@ -490,6 +516,10 @@ def _toCAF(el: AssemblyProtocol, ancestor: TDF_Label | None) -> TDF_Label:
490516
if coloredSTEP and current_color:
491517
setColor(lab, current_color, ctool)
492518

519+
# Handle materials when exporting to STEP
520+
if current_material:
521+
setMaterial(lab, current_material, mtool)
522+
493523
# handle subshape names/colors/layers
494524
subshape_colors = el._subshape_colors
495525
subshape_names = el._subshape_names

cadquery/occ_impl/importers/assembly.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
XCAFDoc_ColorTool,
1616
XCAFDoc,
1717
XCAFDoc_ColorType,
18+
XCAFDoc_Material,
1819
)
1920
from OCP.TDocStd import TDocStd_Application
2021
from OCP.XmlXCAFDrivers import XmlXCAFDrivers
2122
from OCP.BinXCAFDrivers import BinXCAFDrivers
2223
from OCP.Interface import Interface_Static
2324
from OCP.PCDM import PCDM_ReaderStatus
2425

25-
from ..assembly import AssemblyProtocol, Color
26+
from ..assembly import AssemblyProtocol, Color, Material
2627
from ..geom import Location
2728
from ..shapes import Shape
2829

@@ -42,6 +43,38 @@ def _get_name(label: TDF_Label) -> str:
4243
return rv
4344

4445

46+
def _get_material(label: TDF_Label) -> Material | None:
47+
"""
48+
Helper to get the material for a given label.
49+
"""
50+
51+
rv = None
52+
53+
material_ref_guid = XCAFDoc.MaterialRefGUID_s()
54+
55+
if label.IsAttribute(material_ref_guid):
56+
attr = TDataStd_TreeNode()
57+
label.FindAttribute(material_ref_guid, attr)
58+
material_label = attr.Father().Label()
59+
60+
material_attr = XCAFDoc_Material()
61+
62+
material_label.FindAttribute(XCAFDoc_Material.GetID_s(), material_attr)
63+
name = material_attr.GetName().ToCString()
64+
description = material_attr.GetDescription().ToCString()
65+
density = material_attr.GetDensity()
66+
density_unit = material_attr.GetDensValType().ToCString()
67+
68+
rv = Material(
69+
name=name,
70+
description=description,
71+
density=density,
72+
densityUnit=density_unit,
73+
)
74+
75+
return rv
76+
77+
4578
def _get_ref_color(label: TDF_Label) -> Color | None:
4679
"""
4780
Helper to get the instance color of a given label.
@@ -211,6 +244,7 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
211244

212245
# get (if it exists the color of the comp label)
213246
color = _get_ref_color(comp_label)
247+
material = _get_material(comp_label)
214248

215249
if shape_tool.IsAssembly_s(ref_label):
216250
# Find the name of this referenced part
@@ -222,7 +256,13 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
222256
_ = _process_label(ref_label, sub_assy)
223257

224258
# Add the subassy
225-
parent.add(sub_assy, name=ref_name, loc=cq_loc, color=color)
259+
parent.add(
260+
sub_assy,
261+
loc=cq_loc,
262+
name=ref_name,
263+
color=color,
264+
material=material,
265+
)
226266

227267
elif shape_tool.IsSimpleShape_s(ref_label):
228268
# Find the name of this referenced part
@@ -236,6 +276,9 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
236276
if color is None:
237277
color = _get_shape_color(final_shape, color_tool)
238278

279+
if material is None:
280+
material = _get_material(ref_label)
281+
239282
# this if/else is needed to handle different structures of STEP files
240283
# "*"/"*_part" based naming is the default structure produced by CQ
241284
# with an object and child nodes at the same time
@@ -248,7 +291,11 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
248291
current = parent
249292
else:
250293
tmp = assy.__class__(
251-
cq_shape, loc=cq_loc, name=comp_name, color=color
294+
cq_shape,
295+
loc=cq_loc,
296+
name=comp_name,
297+
color=color,
298+
material=material,
252299
)
253300
parent.add(tmp)
254301

@@ -287,6 +334,7 @@ def _process_label(lbl: TDF_Label, parent: AssemblyProtocol):
287334

288335
# try the instance first
289336
color = _get_ref_color(child_label)
337+
material = _get_material(child_label)
290338

291339
if color:
292340
# Save the color info via the assembly subshape mechanism

tests/test_assembly.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,41 @@ def test_meta_step_export_edge_cases(tmp_path_factory):
878878
assert success
879879

880880

881+
@pytest.mark.parametrize("kind", ["step", "xml", "xbf"])
882+
def test_step_roundtrip_with_materials(kind, tmp_path_factory):
883+
"""
884+
Tests to make sure that once materials have been exported to a file format
885+
such as STEP, the materials can be imported again.
886+
"""
887+
888+
materials_assy = cq.Assembly()
889+
materials_assy.add(
890+
cq.Workplane().box(10, 10, 10),
891+
name="cube_1",
892+
material=cq.Material(name="copper"),
893+
)
894+
895+
# Use a temporary directory
896+
tmpdir = tmp_path_factory.mktemp("out")
897+
materials_path = os.path.join(tmpdir, f"roundtrip_materials.{kind}")
898+
899+
exportAssembly(materials_assy, materials_path)
900+
901+
# Read the contents as a step file as a string so we can check the outputs
902+
with open(materials_path, "r") as f:
903+
step_contents = f.read()
904+
905+
# Make sure that the face name string is present in the exported STEP contents
906+
assert "copper" in step_contents
907+
908+
# Import the STEP file back in as an assembly
909+
test_assy = cq.Assembly.importStep(materials_path)
910+
911+
# Make sure that the material was re-imported
912+
assert test_assy.children[0].material is not None
913+
assert test_assy.children[0].material.name == "copper"
914+
915+
881916
def test_assembly_step_import(tmp_path_factory, subshape_assy):
882917
"""
883918
Test if the STEP import works correctly for an assembly with subshape data attached.
@@ -1545,10 +1580,7 @@ def test_materials():
15451580
mat_1 = cq.Material()
15461581
assy.add(wp_1, material=mat_1)
15471582
assert assy.children[0].material.name == "Default"
1548-
assert (
1549-
assy.children[0].material.description
1550-
== "Default material with properties similar to low carbon steel"
1551-
)
1583+
assert assy.children[0].material.description == ""
15521584
assert assy.children[0].material.density == 7.85
15531585
assert assy.children[0].material.densityUnit == "g/cm^3"
15541586

@@ -1568,7 +1600,7 @@ def test_materials():
15681600
# Test the ability to convert a material to a tuple
15691601
assert mat_2.toTuple() == ("test", "Test material", 1.0, "lb/in^3")
15701602

1571-
# Test the ability to has a material
1603+
# Test the ability to have a material
15721604
assert mat_2.__hash__() == hash(("test", "Test material", 1.0, "lb/in^3"))
15731605

15741606
# Test the equality operator with material

0 commit comments

Comments
 (0)