Skip to content

Commit abdb3cc

Browse files
Implement exporting to Gltf (#802)
* Implement GLTF export * Extend the save method * Implement tests * Update tests/test_assembly.py Co-authored-by: Marcus Boyd <[email protected]> * Fix gltf export * Improve tests * refactor tests with pytest parameterize Co-authored-by: Marcus Boyd <[email protected]>
1 parent 2bf9116 commit abdb3cc

File tree

3 files changed

+94
-17
lines changed

3 files changed

+94
-17
lines changed

cadquery/assembly.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
ConstraintMarker,
1313
Constraint as ConstraintPOD,
1414
)
15-
from .occ_impl.exporters.assembly import exportAssembly, exportCAF
15+
from .occ_impl.exporters.assembly import (
16+
exportAssembly,
17+
exportCAF,
18+
exportVTKJS,
19+
exportVRML,
20+
exportGLTF,
21+
)
1622

1723
from .selectors import _expression_grammar as _selector_grammar
1824
from OCP.BRepTools import BRepTools
@@ -22,7 +28,7 @@
2228
# type definitions
2329
AssemblyObjects = Union[Shape, Workplane, None]
2430
ConstraintKinds = Literal["Plane", "Point", "Axis", "PointInPlane"]
25-
ExportLiterals = Literal["STEP", "XML"]
31+
ExportLiterals = Literal["STEP", "XML", "GLTF", "VTKJS", "VRML"]
2632

2733
PATH_DELIM = "/"
2834

@@ -462,18 +468,24 @@ def solve(self) -> "Assembly":
462468
return self
463469

464470
def save(
465-
self, path: str, exportType: Optional[ExportLiterals] = None
471+
self,
472+
path: str,
473+
exportType: Optional[ExportLiterals] = None,
474+
tolerance: float = 0.1,
475+
angularTolerance: float = 0.1,
466476
) -> "Assembly":
467477
"""
468478
save as STEP or OCCT native XML file
469479
470480
:param path: filepath
471481
:param exportType: export format (default: None, results in format being inferred form the path)
482+
:param tolerance: the deflection tolerance, in model units. Only used for GLTF. Default 0.1.
483+
:param angularTolerance: the angular tolerance, in radians. Only used for GLTF. Default 0.1.
472484
"""
473485

474486
if exportType is None:
475487
t = path.split(".")[-1].upper()
476-
if t in ("STEP", "XML"):
488+
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF"):
477489
exportType = cast(ExportLiterals, t)
478490
else:
479491
raise ValueError("Unknown extension, specify export type explicitly")
@@ -482,6 +494,12 @@ def save(
482494
exportAssembly(self, path)
483495
elif exportType == "XML":
484496
exportCAF(self, path)
497+
elif exportType == "VRML":
498+
exportVRML(self, path)
499+
elif exportType == "GLTF":
500+
exportGLTF(self, path, True, tolerance, angularTolerance)
501+
elif exportType == "VTKJS":
502+
exportVTKJS(self, path)
485503
else:
486504
raise ValueError(f"Unknown format: {exportType}")
487505

cadquery/occ_impl/exporters/assembly.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from tempfile import TemporaryDirectory
44
from shutil import make_archive
5+
from itertools import chain
56

67
from vtkmodules.vtkIOExport import vtkJSONSceneExporter, vtkVRMLExporter
78
from vtkmodules.vtkRenderingCore import vtkRenderer, vtkRenderWindow
@@ -17,6 +18,9 @@
1718
)
1819
from OCP.TCollection import TCollection_ExtendedString, TCollection_AsciiString
1920
from OCP.PCDM import PCDM_StoreStatus
21+
from OCP.RWGltf import RWGltf_CafWriter
22+
from OCP.TColStd import TColStd_IndexedDataMapOfStringString
23+
from OCP.Message import Message_ProgressRange
2024

2125
from ..assembly import AssemblyProtocol, toCAF, toVTK
2226

@@ -116,3 +120,27 @@ def exportVRML(assy: AssemblyProtocol, path: str):
116120
exporter.SetFileName(path)
117121
exporter.SetRenderWindow(_vtkRenderWindow(assy))
118122
exporter.Write()
123+
124+
125+
def exportGLTF(
126+
assy: AssemblyProtocol,
127+
path: str,
128+
binary: bool = True,
129+
tolerance: float = 0.1,
130+
angularTolerance: float = 0.1,
131+
):
132+
"""
133+
Export an assembly to a gltf file.
134+
"""
135+
136+
# mesh all the shapes
137+
for _, el in assy.traverse():
138+
for s in el.shapes:
139+
s.mesh(tolerance, angularTolerance)
140+
141+
_, doc = toCAF(assy, True)
142+
143+
writer = RWGltf_CafWriter(TCollection_AsciiString(path), binary)
144+
return writer.Perform(
145+
doc, TColStd_IndexedDataMapOfStringString(), Message_ProgressRange()
146+
)

tests/test_assembly.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ def nested_assy():
5050
return assy
5151

5252

53+
@pytest.fixture
54+
def nested_assy_sphere():
55+
56+
b1 = cq.Workplane().box(1, 1, 1).faces("<Z").tag("top_face").end()
57+
b2 = cq.Workplane().box(1, 1, 1).faces("<Z").tag("bottom_face").end()
58+
b3 = cq.Workplane().pushPoints([(-2, 0), (2, 0)]).tag("pts").sphere(1).tag("boxes")
59+
60+
assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(0, 0, 0)), name="TOP")
61+
assy2 = cq.Assembly(b2, loc=cq.Location(cq.Vector(0, 4, 0)), name="SECOND")
62+
assy2.add(b3, loc=cq.Location(cq.Vector(0, 4, 0)), name="BOTTOM")
63+
64+
assy.add(assy2, color=cq.Color("green"))
65+
66+
return assy
67+
68+
5369
@pytest.fixture
5470
def empty_top_assy():
5571

@@ -168,28 +184,43 @@ def test_toJSON(simple_assy, nested_assy, empty_top_assy):
168184
assert len(r3) == 1
169185

170186

171-
def test_save(simple_assy, nested_assy):
187+
@pytest.mark.parametrize(
188+
"extension, args",
189+
[
190+
("step", ()),
191+
("xml", ()),
192+
("stp", ("STEP",)),
193+
("caf", ("XML",)),
194+
("wrl", ("VRML",)),
195+
],
196+
)
197+
def test_save(extension, args, nested_assy, nested_assy_sphere):
198+
199+
filename = "nested." + extension
200+
nested_assy.save(filename, *args)
201+
assert os.path.exists(filename)
202+
203+
204+
def test_save_gltf(nested_assy_sphere):
205+
206+
nested_assy_sphere.save("nested.glb", "GLTF")
207+
assert os.path.exists("nested.glb")
208+
assert os.path.getsize("nested.glb") > 50 * 1024
172209

173-
simple_assy.save("simple.step")
174-
assert os.path.exists("simple.step")
175210

176-
simple_assy.save("simple.xml")
177-
assert os.path.exists("simple.xml")
211+
def test_save_vtkjs(nested_assy):
178212

179-
simple_assy.save("simple.step")
180-
assert os.path.exists("simple.step")
213+
nested_assy.save("nested", "VTKJS")
214+
assert os.path.exists("nested.zip")
181215

182-
simple_assy.save("simple.stp", "STEP")
183-
assert os.path.exists("simple.stp")
184216

185-
simple_assy.save("simple.caf", "XML")
186-
assert os.path.exists("simple.caf")
217+
def test_save_raises(nested_assy):
187218

188219
with pytest.raises(ValueError):
189-
simple_assy.save("simple.dxf")
220+
nested_assy.save("nested.dxf")
190221

191222
with pytest.raises(ValueError):
192-
simple_assy.save("simple.step", "DXF")
223+
nested_assy.save("nested.step", "DXF")
193224

194225

195226
def test_constrain(simple_assy, nested_assy):

0 commit comments

Comments
 (0)