Skip to content

Commit 21243ee

Browse files
committed
Merge branch 'master' into docs-install
2 parents d58f0a9 + 33bf83e commit 21243ee

File tree

12 files changed

+756
-57
lines changed

12 files changed

+756
-57
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ nested.stl
3232
out1.3mf
3333
out2.3mf
3434
out3.3mf
35+
orig.dxf

appveyor.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,8 @@ test_script:
3838

3939
on_success:
4040
- codecov
41+
42+
#on_finish:
43+
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
44+
# - sh: export APPVEYOR_SSH_BLOCK=true
45+
# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -

azure-pipelines.yml

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,10 @@ parameters:
1818
- name: minor
1919
type: object
2020
default:
21-
- 8
22-
- 9
23-
- 10
2421
- 11
2522

2623
jobs:
2724
- ${{ each minor in parameters.minor }}:
28-
- template: conda-build.yml@templates
29-
parameters:
30-
name: Linux
31-
vmImage: 'ubuntu-18.04'
32-
py_maj: 3
33-
py_min: ${{minor}}
34-
conda_bld: 3.21.6
35-
36-
- template: conda-build.yml@templates
37-
parameters:
38-
name: macOS
39-
vmImage: 'macOS-10.15'
40-
py_maj: 3
41-
py_min: ${{minor}}
42-
conda_bld: 3.21.6
43-
4425
- template: conda-build.yml@templates
4526
parameters:
4627
name: Windows

cadquery/assembly.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
from functools import reduce
2-
from typing import Union, Optional, List, Dict, Any, overload, Tuple, Iterator, cast
2+
from typing import (
3+
Union,
4+
Optional,
5+
List,
6+
Dict,
7+
Any,
8+
overload,
9+
Tuple,
10+
Iterator,
11+
cast,
12+
get_args,
13+
)
314
from typing_extensions import Literal
415
from typish import instance_of
516
from uuid import uuid1 as uuid
@@ -21,6 +32,8 @@
2132
exportVTKJS,
2233
exportVRML,
2334
exportGLTF,
35+
STEPExportModeLiterals,
36+
ExportModes,
2437
)
2538

2639
from .selectors import _expression_grammar as _selector_grammar
@@ -436,6 +449,7 @@ def save(
436449
self,
437450
path: str,
438451
exportType: Optional[ExportLiterals] = None,
452+
mode: STEPExportModeLiterals = "default",
439453
tolerance: float = 0.1,
440454
angularTolerance: float = 0.1,
441455
**kwargs,
@@ -451,6 +465,10 @@ def save(
451465
See :meth:`~cadquery.occ_impl.exporters.assembly.exportAssembly`.
452466
"""
453467

468+
# Make sure the export mode setting is correct
469+
if mode not in get_args(STEPExportModeLiterals):
470+
raise ValueError(f"Unknown assembly export mode {mode} for STEP")
471+
454472
if exportType is None:
455473
t = path.split(".")[-1].upper()
456474
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF", "STL"):
@@ -459,7 +477,7 @@ def save(
459477
raise ValueError("Unknown extension, specify export type explicitly")
460478

461479
if exportType == "STEP":
462-
exportAssembly(self, path, **kwargs)
480+
exportAssembly(self, path, mode, **kwargs)
463481
elif exportType == "XML":
464482
exportCAF(self, path)
465483
elif exportType == "VRML":

cadquery/occ_impl/assembly.py

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

55
from OCP.TDocStd import TDocStd_Document
66
from OCP.TCollection import TCollection_ExtendedString
7-
from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorType
7+
from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorType, XCAFDoc_ColorGen
88
from OCP.XCAFApp import XCAFApp_Application
99
from OCP.TDataStd import TDataStd_Name
1010
from OCP.TDF import TDF_Label
1111
from OCP.TopLoc import TopLoc_Location
1212
from OCP.Quantity import Quantity_ColorRGBA
13+
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse
14+
from OCP.TopTools import TopTools_ListOfShape
15+
from OCP.BOPAlgo import BOPAlgo_GlueEnum
16+
from OCP.TopoDS import TopoDS_Shape
1317

1418
from vtkmodules.vtkRenderingCore import (
1519
vtkActor,
@@ -112,6 +116,10 @@ def parent(self) -> Optional["AssemblyProtocol"]:
112116
def color(self) -> Optional[Color]:
113117
...
114118

119+
@property
120+
def obj(self) -> AssemblyObjects:
121+
...
122+
115123
@property
116124
def shapes(self) -> Iterable[Shape]:
117125
...
@@ -289,3 +297,117 @@ def toJSON(
289297
rv.extend(toJSON(child, loc, color, tolerance))
290298

291299
return rv
300+
301+
302+
def toFusedCAF(
303+
assy: AssemblyProtocol, glue: bool = False, tol: Optional[float] = None,
304+
) -> Tuple[TDF_Label, TDocStd_Document]:
305+
"""
306+
Converts the assembly to a fused compound and saves that within the document
307+
to be exported in a way that preserves the face colors. Because of the use of
308+
boolean operations in this method, performance may be slow in some cases.
309+
310+
:param assy: Assembly that is being converted to a fused compound for the document.
311+
"""
312+
313+
# Prepare the document
314+
app = XCAFApp_Application.GetApplication_s()
315+
doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf"))
316+
app.InitDocument(doc)
317+
318+
# Shape and color tools
319+
shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())
320+
color_tool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())
321+
322+
# To fuse the parts of the assembly together
323+
fuse_op = BRepAlgoAPI_Fuse()
324+
args = TopTools_ListOfShape()
325+
tools = TopTools_ListOfShape()
326+
327+
# If there is only one solid, there is no reason to fuse, and it will likely cause problems anyway
328+
top_level_shape = None
329+
330+
# Walk the entire assembly, collecting the located shapes and colors
331+
shapes: List[Shape] = []
332+
colors = []
333+
334+
def extract_shapes(assy, parent_loc=None, parent_color=None):
335+
336+
loc = parent_loc * assy.loc if parent_loc else assy.loc
337+
color = assy.color if assy.color else parent_color
338+
339+
for shape in assy.shapes:
340+
shapes.append(shape.moved(loc).copy())
341+
colors.append(color)
342+
343+
for ch in assy.children:
344+
extract_shapes(ch, loc, color)
345+
346+
extract_shapes(assy)
347+
348+
# Initialize with a dummy value for mypy
349+
top_level_shape = cast(TopoDS_Shape, None)
350+
351+
# If the tools are empty, it means we only had a single shape and do not need to fuse
352+
if not shapes:
353+
raise Exception(f"Error: Assembly {assy.name} has no shapes.")
354+
elif len(shapes) == 1:
355+
# There is only one shape and we only need to make sure it is a Compound
356+
# This seems to be needed to be able to add subshapes (i.e. faces) correctly
357+
sh = shapes[0]
358+
if sh.ShapeType() != "Compound":
359+
top_level_shape = Compound.makeCompound((sh,)).wrapped
360+
elif sh.ShapeType() == "Compound":
361+
sh = sh.fuse(glue=glue, tol=tol)
362+
top_level_shape = Compound.makeCompound((sh,)).wrapped
363+
shapes = [sh]
364+
else:
365+
# Set the shape lists up so that the fuse operation can be performed
366+
args.Append(shapes[0].wrapped)
367+
368+
for shape in shapes[1:]:
369+
tools.Append(shape.wrapped)
370+
371+
# Allow the caller to configure the fuzzy and glue settings
372+
if tol:
373+
fuse_op.SetFuzzyValue(tol)
374+
if glue:
375+
fuse_op.SetGlue(BOPAlgo_GlueEnum.BOPAlgo_GlueShift)
376+
377+
fuse_op.SetArguments(args)
378+
fuse_op.SetTools(tools)
379+
fuse_op.Build()
380+
381+
top_level_shape = fuse_op.Shape()
382+
383+
# Add the fused shape as the top level object in the document
384+
top_level_lbl = shape_tool.AddShape(top_level_shape, False)
385+
TDataStd_Name.Set_s(top_level_lbl, TCollection_ExtendedString(assy.name))
386+
387+
# Walk the assembly->part->shape->face hierarchy and add subshapes for all the faces
388+
for color, shape in zip(colors, shapes):
389+
for face in shape.Faces():
390+
# See if the face can be treated as-is
391+
cur_lbl = shape_tool.AddSubShape(top_level_lbl, face.wrapped)
392+
if color and not cur_lbl.IsNull() and not fuse_op.IsDeleted(face.wrapped):
393+
color_tool.SetColor(cur_lbl, color.wrapped, XCAFDoc_ColorGen)
394+
395+
# Handle any modified faces
396+
modded_list = fuse_op.Modified(face.wrapped)
397+
398+
for mod in modded_list:
399+
# Add the face as a subshape and set its color to match the parent assembly component
400+
cur_lbl = shape_tool.AddSubShape(top_level_lbl, mod)
401+
if color and not cur_lbl.IsNull() and not fuse_op.IsDeleted(mod):
402+
color_tool.SetColor(cur_lbl, color.wrapped, XCAFDoc_ColorGen)
403+
404+
# Handle any generated faces
405+
gen_list = fuse_op.Generated(face.wrapped)
406+
407+
for gen in gen_list:
408+
# Add the face as a subshape and set its color to match the parent assembly component
409+
cur_lbl = shape_tool.AddSubShape(top_level_lbl, gen)
410+
if color and not cur_lbl.IsNull():
411+
color_tool.SetColor(cur_lbl, color.wrapped, XCAFDoc_ColorGen)
412+
413+
return top_level_lbl, doc

cadquery/occ_impl/exporters/assembly.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import os.path
2+
import uuid
23

34
from tempfile import TemporaryDirectory
45
from shutil import make_archive
56
from itertools import chain
7+
from typing_extensions import Literal
68

79
from vtkmodules.vtkIOExport import vtkJSONSceneExporter, vtkVRMLExporter
810
from vtkmodules.vtkRenderingCore import vtkRenderer, vtkRenderWindow
@@ -23,20 +25,41 @@
2325
from OCP.Message import Message_ProgressRange
2426
from OCP.Interface import Interface_Static
2527

26-
from ..assembly import AssemblyProtocol, toCAF, toVTK
28+
from ..assembly import AssemblyProtocol, toCAF, toVTK, toFusedCAF
2729
from ..geom import Location
2830

2931

30-
def exportAssembly(assy: AssemblyProtocol, path: str, **kwargs) -> bool:
32+
class ExportModes:
33+
DEFAULT = "default"
34+
FUSED = "fused"
35+
36+
37+
STEPExportModeLiterals = Literal["default", "fused"]
38+
39+
40+
def exportAssembly(
41+
assy: AssemblyProtocol,
42+
path: str,
43+
mode: STEPExportModeLiterals = "default",
44+
**kwargs
45+
) -> bool:
3146
"""
3247
Export an assembly to a STEP file.
3348
3449
kwargs is used to provide optional keyword arguments to configure the exporter.
3550
3651
:param assy: assembly
3752
:param path: Path and filename for writing
53+
:param mode: STEP export mode. The options are "default", and "fused" (a single fused compound).
54+
It is possible that fused mode may exhibit low performance.
55+
:param fuzzy_tol: OCCT fuse operation tolerance setting used only for fused assembly export.
56+
:type fuzzy_tol: float
57+
:param glue: Enable gluing mode for improved performance during fused assembly export.
58+
This option should only be used for non-intersecting shapes or those that are only touching or partially overlapping.
59+
Note that when glue is enabled, the resulting fused shape may be invalid if shapes are intersecting in an incompatible way.
60+
Defaults to False.
61+
:type glue: bool
3862
:param write_pcurves: Enable or disable writing parametric curves to the STEP file. Default True.
39-
4063
If False, writes STEP file without pcurves. This decreases the size of the resulting STEP file.
4164
:type write_pcurves: bool
4265
:param precision_mode: Controls the uncertainty value for STEP entities. Specify -1, 0, or 1. Default 0.
@@ -49,8 +72,17 @@ def exportAssembly(assy: AssemblyProtocol, path: str, **kwargs) -> bool:
4972
if "write_pcurves" in kwargs and not kwargs["write_pcurves"]:
5073
pcurves = 0
5174
precision_mode = kwargs["precision_mode"] if "precision_mode" in kwargs else 0
75+
fuzzy_tol = kwargs["fuzzy_tol"] if "fuzzy_tol" in kwargs else None
76+
glue = kwargs["glue"] if "glue" in kwargs else False
77+
78+
# Use the assembly name if the user set it
79+
assembly_name = assy.name if assy.name else str(uuid.uuid1())
5280

53-
_, doc = toCAF(assy, True)
81+
# Handle the doc differently based on which mode we are using
82+
if mode == "fused":
83+
_, doc = toFusedCAF(assy, glue, fuzzy_tol)
84+
else: # Includes "default"
85+
_, doc = toCAF(assy, True)
5486

5587
session = XSControl_WorkSession()
5688
writer = STEPCAFControl_Writer(session, False)

cadquery/occ_impl/shapes.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,8 +545,10 @@ def geomType(self) -> Geoms:
545545
The return values depend on the type of the shape:
546546
547547
| Vertex: always 'Vertex'
548-
| Edge: LINE, ARC, CIRCLE, SPLINE
549-
| Face: PLANE, SPHERE, CONE
548+
| Edge: LINE, CIRCLE, ELLIPSE, HYPERBOLA, PARABOLA, BEZIER,
549+
| BSPLINE, OFFSET, OTHER
550+
| Face: PLANE, CYLINDER, CONE, SPHERE, TORUS, BEZIER, BSPLINE,
551+
| REVOLUTION, EXTRUSION, OFFSET, OTHER
550552
| Solid: 'Solid'
551553
| Shell: 'Shell'
552554
| Compound: 'Compound'
@@ -2188,7 +2190,7 @@ def makeHelix(
21882190
return cls(w)
21892191

21902192
def stitch(self, other: "Wire") -> "Wire":
2191-
"""Attempt to stich wires"""
2193+
"""Attempt to stitch wires"""
21922194

21932195
wire_builder = BRepBuilderAPI_MakeWire()
21942196
wire_builder.Add(TopoDS.Wire_s(self.wrapped))

conda/meta.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ source:
66
path: ..
77

88
build:
9-
string: {{ 'py'+environ.get('PYTHON_VERSION')}}
9+
string: {{ GIT_DESCRIBE_TAG }}_{{ GIT_BUILD_STR }}
10+
noarch: python
1011
script: python setup.py install --single-version-externally-managed --record=record.txt
1112

1213
requirements:
1314
build:
14-
- python {{ environ.get('PYTHON_VERSION') }}
15+
- python >=3.8
1516
- setuptools
1617
run:
17-
- python {{ environ.get('PYTHON_VERSION') }}
18-
- ocp 7.7.*
18+
- python >=3.8
19+
- ocp 7.7.0
20+
- occt 7.7.0
1921
- pyparsing >=2.1.9
2022
- ezdxf
2123
- ipython

0 commit comments

Comments
 (0)