Skip to content

Commit 7535957

Browse files
Merge branch 'master' into adam-nurbs
2 parents e6b6c7c + 7221407 commit 7535957

23 files changed

+875
-442
lines changed

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ branch = True
33
omit =
44
cadquery/utils.py
55
cadquery/cq_directive.py
6+
cadquery/__init__.py
67
tests/*
8+
setup.py
79

810
[report]
911
exclude_lines =

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cff-version: 1.2.0
22
doi: 10.5281/zenodo.10513848
3-
license: "Apache 2.0"
3+
license: "Apache-2.0"
44
url: https://github.com/CadQuery/cadquery
55
title: "CadQuery"
66
message: "If you use this software, please cite it using these metadata."

cadquery/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
__version__ = version("cadquery")
55
except PackageNotFoundError:
66
# package is not installed
7-
__version__ = "2.6-dev"
7+
__version__ = "2.7-dev"
88

99
# these items point to the OCC implementation
1010
from .occ_impl.geom import Plane, BoundBox, Vector, Matrix, Location

cadquery/assembly.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535
exportGLTF,
3636
STEPExportModeLiterals,
3737
)
38-
from .occ_impl.importers.assembly import importStep as _importStep
38+
from .occ_impl.importers.assembly import importStep as _importStep, importXbf, importXml
3939

4040
from .selectors import _expression_grammar as _selector_grammar
4141
from .utils import deprecate, BiDict
4242

4343
# type definitions
4444
AssemblyObjects = Union[Shape, Workplane, None]
45-
ExportLiterals = Literal["STEP", "XML", "GLTF", "VTKJS", "VRML", "STL"]
45+
ImportLiterals = Literal["STEP", "XML", "XBF"]
46+
ExportLiterals = Literal["STEP", "XML", "XBF", "GLTF", "VTKJS", "VRML", "STL"]
4647

4748
PATH_DELIM = "/"
4849

@@ -226,7 +227,9 @@ def add(self, arg, **kwargs):
226227
# enforce unique names
227228
name = kwargs["name"] if kwargs.get("name") else arg.name
228229
if name in self.objects:
229-
raise ValueError("Unique name is required")
230+
raise ValueError(
231+
f"Unique name is required. {name} is already in the assembly"
232+
)
230233

231234
subassy = arg._copy()
232235

@@ -527,38 +530,9 @@ def save(
527530
:type ascii: bool
528531
"""
529532

530-
# Make sure the export mode setting is correct
531-
if mode not in get_args(STEPExportModeLiterals):
532-
raise ValueError(f"Unknown assembly export mode {mode} for STEP")
533-
534-
if exportType is None:
535-
t = path.split(".")[-1].upper()
536-
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
537-
exportType = cast(ExportLiterals, t)
538-
else:
539-
raise ValueError("Unknown extension, specify export type explicitly")
540-
541-
if exportType == "STEP":
542-
exportAssembly(self, path, mode, **kwargs)
543-
elif exportType == "XML":
544-
exportCAF(self, path)
545-
elif exportType == "VRML":
546-
exportVRML(self, path, tolerance, angularTolerance)
547-
elif exportType == "GLTF" or exportType == "GLB":
548-
exportGLTF(self, path, None, tolerance, angularTolerance)
549-
elif exportType == "VTKJS":
550-
exportVTKJS(self, path)
551-
elif exportType == "STL":
552-
# Handle the ascii setting for STL export
553-
export_ascii = False
554-
if "ascii" in kwargs:
555-
export_ascii = bool(kwargs.get("ascii"))
556-
557-
self.toCompound().exportStl(path, tolerance, angularTolerance, export_ascii)
558-
else:
559-
raise ValueError(f"Unknown format: {exportType}")
560-
561-
return self
533+
return self.export(
534+
path, exportType, mode, tolerance, angularTolerance, **kwargs
535+
)
562536

563537
def export(
564538
self,
@@ -589,7 +563,7 @@ def export(
589563

590564
if exportType is None:
591565
t = path.split(".")[-1].upper()
592-
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
566+
if t in ("STEP", "XML", "XBF", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
593567
exportType = cast(ExportLiterals, t)
594568
else:
595569
raise ValueError("Unknown extension, specify export type explicitly")
@@ -598,6 +572,8 @@ def export(
598572
exportAssembly(self, path, mode, **kwargs)
599573
elif exportType == "XML":
600574
exportCAF(self, path)
575+
elif exportType == "XBF":
576+
exportCAF(self, path, binary=True)
601577
elif exportType == "VRML":
602578
exportVRML(self, path, tolerance, angularTolerance)
603579
elif exportType == "GLTF" or exportType == "GLB":
@@ -621,22 +597,35 @@ def importStep(cls, path: str) -> Self:
621597
"""
622598
Reads an assembly from a STEP file.
623599
624-
:param path: Path and filename for writing.
600+
:param path: Path and filename for reading.
625601
:return: An Assembly object.
626602
"""
627603

628-
assy = cls()
629-
_importStep(assy, path)
630-
631-
return assy
604+
return cls.load(path, importType="STEP")
632605

633606
@classmethod
634-
def load(cls, path: str) -> Self:
607+
def load(cls, path: str, importType: Optional[ImportLiterals] = None,) -> Self:
635608
"""
636-
Alias of importStep for now.
609+
Load step, xbf or xml.
637610
"""
638611

639-
return cls.importStep(path)
612+
if importType is None:
613+
t = path.split(".")[-1].upper()
614+
if t in ("STEP", "XML", "XBF"):
615+
importType = cast(ImportLiterals, t)
616+
else:
617+
raise ValueError("Unknown extension, specify export type explicitly")
618+
619+
assy = cls()
620+
621+
if importType == "STEP":
622+
_importStep(assy, path)
623+
elif importType == "XML":
624+
importXml(assy, path)
625+
elif importType == "XBF":
626+
importXbf(assy, path)
627+
628+
return assy
640629

641630
@property
642631
def shapes(self) -> List[Shape]:

cadquery/cq.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1413,8 +1413,10 @@ def _findFromPoint(self, useLocalCoords: bool = False) -> Vector:
14131413
p = obj.endPoint()
14141414
elif isinstance(obj, Vector):
14151415
p = obj
1416+
elif isinstance(obj, Vertex):
1417+
p = obj.Center()
14161418
else:
1417-
raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj))
1419+
raise ValueError(f"Cannot convert object type {type(obj)} to vector.")
14181420

14191421
if useLocalCoords:
14201422
return self.plane.toLocalCoords(p)
@@ -2394,6 +2396,7 @@ def consolidateWires(self: T) -> T:
23942396
r = self.newObject(w)
23952397
r.ctx.pendingWires = w
23962398
r.ctx.pendingEdges = []
2399+
r.ctx.firstPoint = None
23972400

23982401
return r
23992402

cadquery/cq_directive.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
roll: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRollManipulator.newInstance(),
5555
};
5656
57+
manips.rot.setUseFocalPointAsCenterOfRotation(true);
5758
manips.zoom1.setControl(true);
5859
manips.zoom2.setButton(3);
5960
manips.roll.setShift(true);

cadquery/occ_impl/assembly.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,50 @@ def addSubshape(
266266
) -> Self:
267267
...
268268

269+
@overload
270+
def add(
271+
self,
272+
obj: Self,
273+
loc: Optional[Location] = None,
274+
name: Optional[str] = None,
275+
color: Optional[Color] = None,
276+
) -> Self:
277+
...
278+
279+
@overload
280+
def add(
281+
self,
282+
obj: AssemblyObjects,
283+
loc: Optional[Location] = None,
284+
name: Optional[str] = None,
285+
color: Optional[Color] = None,
286+
metadata: Optional[Dict[str, Any]] = None,
287+
) -> Self:
288+
...
289+
290+
def add(
291+
self,
292+
obj: Union[Self, AssemblyObjects],
293+
loc: Optional[Location] = None,
294+
name: Optional[str] = None,
295+
color: Optional[Color] = None,
296+
metadata: Optional[Dict[str, Any]] = None,
297+
**kwargs: Any,
298+
) -> Self:
299+
"""
300+
Add a subassembly to the current assembly.
301+
"""
302+
...
303+
304+
def addSubshape(
305+
self,
306+
s: Shape,
307+
name: Optional[str] = None,
308+
color: Optional[Color] = None,
309+
layer: Optional[str] = None,
310+
) -> Self:
311+
...
312+
269313
def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]:
270314
...
271315

@@ -414,6 +458,7 @@ def _toCAF(el: AssemblyProtocol, ancestor: TDF_Label | None) -> TDF_Label:
414458
for child in el.children:
415459
_toCAF(child, subassy)
416460

461+
# final rv construction
417462
if ancestor and el.children:
418463
tool.AddComponent(ancestor, subassy, el.loc.wrapped)
419464
rv = subassy

cadquery/occ_impl/geom.py

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from OCP.TopLoc import TopLoc_Location
2727
from OCP.BinTools import BinTools_LocationSet
2828

29+
from multimethod import multidispatch
30+
2931
from ..types import Real
3032
from ..utils import multimethod
3133

@@ -579,20 +581,17 @@ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
579581
plane._setPlaneDir(xDir)
580582
return plane
581583

584+
# Prefer multidispatch over multimethod, as that supports keyword
585+
# arguments. These are in use, since Plane.__init__ has not always
586+
# been a multimethod.
587+
@multidispatch
582588
def __init__(
583589
self,
584-
origin: Union[Tuple[float, float, float], Vector],
585-
xDir: Optional[Union[Tuple[float, float, float], Vector]] = None,
586-
normal: Union[Tuple[float, float, float], Vector] = (0, 0, 1),
590+
origin: Union[Tuple[Real, Real, Real], Vector],
591+
xDir: Optional[Union[Tuple[Real, Real, Real], Vector]] = None,
592+
normal: Union[Tuple[Real, Real, Real], Vector] = (0, 0, 1),
587593
):
588-
"""
589-
Create a Plane with an arbitrary orientation
590-
591-
:param origin: the origin in global coordinates
592-
:param xDir: an optional vector representing the xDirection.
593-
:param normal: the normal direction for the plane
594-
:raises ValueError: if the specified xDir is not orthogonal to the provided normal
595-
"""
594+
"""Create a Plane from origin in global coordinates, vector xDir, and normal direction for the plane."""
596595
zDir = Vector(normal)
597596
if zDir.Length == 0.0:
598597
raise ValueError("normal should be non null")
@@ -610,6 +609,50 @@ def __init__(
610609
self._setPlaneDir(xDir)
611610
self.origin = Vector(origin)
612611

612+
@__init__.register
613+
def __init__(
614+
self, loc: "Location",
615+
):
616+
"""Create a Plane from Location loc."""
617+
618+
# Ask location for its information
619+
origin, rotations = loc.toTuple()
620+
621+
# Origin is easy, but the rotational angles of the location need to be
622+
# turned into xDir and normal vectors.
623+
# This is done by multiplying a standard cooridnate system by the given
624+
# angles.
625+
# Rotation of vectors is done by a transformation matrix.
626+
# The order in which rotational angles are introduced is crucial:
627+
# If u is our vector, Rx is rotation around x axis etc, we want the
628+
# following:
629+
# u' = Rz * Ry * Rx * u = R * u
630+
# That way, all rotational angles refer to a global coordinate system,
631+
# and e.g. Ry does not refer to a rotation direction, which already
632+
# was rotated around Rx.
633+
# This definition in the global system is called extrinsic, and it is
634+
# how the Location class wants it to be done.
635+
# And this is why we introduce the rotations from left to right
636+
# and from Z to X.
637+
transformation = Matrix()
638+
transformation.rotateZ(rotations[2] * pi / 180.0)
639+
transformation.rotateY(rotations[1] * pi / 180.0)
640+
transformation.rotateX(rotations[0] * pi / 180.0)
641+
642+
# Apply rotation on vectors of the global plane
643+
# These vectors are already unit vectors and require no .normalized()
644+
globaldirs = ((1, 0, 0), (0, 0, 1))
645+
localdirs = (Vector(*i).transform(transformation) for i in globaldirs)
646+
647+
# Unpack vectors
648+
xDir, normal = localdirs
649+
650+
# Apply attributes as in other constructor.
651+
# Rememeber to set zDir before calling _setPlaneDir.
652+
self.zDir = normal
653+
self._setPlaneDir(xDir)
654+
self.origin = origin
655+
613656
def _eq_iter(self, other):
614657
"""Iterator to successively test equality"""
615658
cls = type(self)
@@ -1012,15 +1055,22 @@ def __init__(
10121055
) -> None:
10131056
"""Location with translation (x,y,z) and 3 rotation angles."""
10141057

1015-
T = gp_Trsf()
1058+
if any((x, y, z, rx, ry, rz)):
10161059

1017-
q = gp_Quaternion()
1018-
q.SetEulerAngles(gp_Extrinsic_XYZ, radians(rx), radians(ry), radians(rz))
1060+
T = gp_Trsf()
10191061

1020-
T.SetRotation(q)
1021-
T.SetTranslationPart(Vector(x, y, z).wrapped)
1062+
q = gp_Quaternion()
1063+
q.SetEulerAngles(gp_Extrinsic_XYZ, radians(rx), radians(ry), radians(rz))
10221064

1023-
self.wrapped = TopLoc_Location(T)
1065+
T.SetRotation(q)
1066+
T.SetTranslationPart(Vector(x, y, z).wrapped)
1067+
1068+
loc = TopLoc_Location(T)
1069+
else:
1070+
# in this case location is flagged as identity
1071+
loc = TopLoc_Location()
1072+
1073+
self.wrapped = loc
10241074

10251075
@__init__.register
10261076
def __init__(self, t: Plane) -> None:
@@ -1104,6 +1154,11 @@ def toTuple(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float
11041154

11051155
return rv_trans, (degrees(rx), degrees(ry), degrees(rz))
11061156

1157+
@property
1158+
def plane(self) -> "Plane":
1159+
1160+
return Plane(self)
1161+
11071162
def __getstate__(self) -> BytesIO:
11081163

11091164
rv = BytesIO()
@@ -1121,7 +1176,7 @@ def __setstate__(self, data: BytesIO):
11211176
ls = BinTools_LocationSet()
11221177
ls.Read(data)
11231178

1124-
if ls.NbLocations() == 0:
1125-
self.wrapped = TopLoc_Location()
1126-
else:
1179+
if ls.NbLocations() > 0:
11271180
self.wrapped = ls.Location(1)
1181+
else:
1182+
self.wrapped = TopLoc_Location() # identity location

0 commit comments

Comments
 (0)