Skip to content

Commit 6d7d1d6

Browse files
Sketch and Workplane improvements (#1633)
* Add special methods to Sketch a clean up * Sketch support in DXF exporter * Use the new __iter__ interface * Fix failing tests * Add some exporter tests * More tests * Typo fix * Fix test * Bool ops for Sketch * Use else * Better annotations * Implement add/subtract/replace * Extend _getFaces() * Annotation tweaks * Additional operators * Add deprecations * Allow 0 spacing * Implement __iter__ * Decouple exporters and Workplane * Export methods * Solve circular import * More args for export * Add brep * Add some tests * Fix stack searching * Some tests * Implement moved overloads for Sketch * extract get_arity to utils * Workplane __iter__ tests and tweaks * Assy export tests * Fix test * Coverage tweaks * Coverage tweaks 2 * Test for Sketch additions * Black fix * Make _selection optional * More tests * More sketch coverage * Some doc improvements * More docs * Mention special methods for Sketch * Doc fixes * Coverage tweaks * Doc tweaks * Typo fix * Spelling Co-authored-by: Jeremy Wright <[email protected]> * Rework cq.py * Rework sketch.py * Rephrase * Set spacing to 0 --------- Co-authored-by: Jeremy Wright <[email protected]>
1 parent 23ec959 commit 6d7d1d6

19 files changed

+1084
-195
lines changed

cadquery/assembly.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
exportVRML,
3434
exportGLTF,
3535
STEPExportModeLiterals,
36-
ExportModes,
3736
)
3837

3938
from .selectors import _expression_grammar as _selector_grammar
39+
from .utils import deprecate
4040

4141
# type definitions
4242
AssemblyObjects = Union[Shape, Workplane, None]
@@ -451,6 +451,7 @@ def solve(self, verbosity: int = 0) -> "Assembly":
451451

452452
return self
453453

454+
@deprecate()
454455
def save(
455456
self,
456457
path: str,
@@ -507,6 +508,62 @@ def save(
507508

508509
return self
509510

511+
def export(
512+
self,
513+
path: str,
514+
exportType: Optional[ExportLiterals] = None,
515+
mode: STEPExportModeLiterals = "default",
516+
tolerance: float = 0.1,
517+
angularTolerance: float = 0.1,
518+
**kwargs,
519+
) -> "Assembly":
520+
"""
521+
Save assembly to a file.
522+
523+
:param path: Path and filename for writing.
524+
:param exportType: export format (default: None, results in format being inferred form the path)
525+
:param mode: STEP only - See :meth:`~cadquery.occ_impl.exporters.assembly.exportAssembly`.
526+
:param tolerance: the deflection tolerance, in model units. Only used for glTF, VRML. Default 0.1.
527+
:param angularTolerance: the angular tolerance, in radians. Only used for glTF, VRML. Default 0.1.
528+
:param \\**kwargs: Additional keyword arguments. Only used for STEP, glTF and STL.
529+
See :meth:`~cadquery.occ_impl.exporters.assembly.exportAssembly`.
530+
:param ascii: STL only - Sets whether or not STL export should be text or binary
531+
:type ascii: bool
532+
"""
533+
534+
# Make sure the export mode setting is correct
535+
if mode not in get_args(STEPExportModeLiterals):
536+
raise ValueError(f"Unknown assembly export mode {mode} for STEP")
537+
538+
if exportType is None:
539+
t = path.split(".")[-1].upper()
540+
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
541+
exportType = cast(ExportLiterals, t)
542+
else:
543+
raise ValueError("Unknown extension, specify export type explicitly")
544+
545+
if exportType == "STEP":
546+
exportAssembly(self, path, mode, **kwargs)
547+
elif exportType == "XML":
548+
exportCAF(self, path)
549+
elif exportType == "VRML":
550+
exportVRML(self, path, tolerance, angularTolerance)
551+
elif exportType == "GLTF" or exportType == "GLB":
552+
exportGLTF(self, path, None, tolerance, angularTolerance)
553+
elif exportType == "VTKJS":
554+
exportVTKJS(self, path)
555+
elif exportType == "STL":
556+
# Handle the ascii setting for STL export
557+
export_ascii = False
558+
if "ascii" in kwargs:
559+
export_ascii = bool(kwargs.get("ascii"))
560+
561+
self.toCompound().exportStl(path, tolerance, angularTolerance, export_ascii)
562+
else:
563+
raise ValueError(f"Unknown format: {exportType}")
564+
565+
return self
566+
510567
@classmethod
511568
def load(cls, path: str) -> "Assembly":
512569

cadquery/cq.py

Lines changed: 111 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
List,
3434
cast,
3535
Dict,
36+
Iterator,
3637
)
3738
from typing_extensions import Literal
38-
from inspect import Parameter, Signature, isbuiltin
39+
from inspect import Parameter, Signature
3940

4041

4142
from .occ_impl.geom import Vector, Plane, Location
@@ -51,8 +52,9 @@
5152
)
5253

5354
from .occ_impl.exporters.svg import getSVG, exportSVG
55+
from .occ_impl.exporters import export
5456

55-
from .utils import deprecate, deprecate_kwarg_name
57+
from .utils import deprecate, deprecate_kwarg_name, get_arity
5658

5759
from .selectors import (
5860
Selector,
@@ -270,7 +272,7 @@ def split(self: T, keepTop: bool = False, keepBottom: bool = False) -> T:
270272
...
271273

272274
@overload
273-
def split(self: T, splitter: Union[T, Shape]) -> T:
275+
def split(self: T, splitter: Union["Workplane", Shape]) -> T:
274276
...
275277

276278
def split(self: T, *args, **kwargs) -> T:
@@ -383,9 +385,7 @@ def combineSolids(
383385
raise ValueError("Cannot Combine: at least one solid required!")
384386

385387
# get context solid and we don't want to find our own objects
386-
ctxSolid = self._findType(
387-
(Solid, Compound), searchStack=False, searchParents=True
388-
)
388+
ctxSolid = self._findType((Solid,), searchStack=False, searchParents=True)
389389
if ctxSolid is None:
390390
ctxSolid = toCombine.pop(0)
391391

@@ -748,8 +748,20 @@ def end(self, n: int = 1) -> "Workplane":
748748
def _findType(self, types, searchStack=True, searchParents=True):
749749

750750
if searchStack:
751-
rv = [s for s in self.objects if isinstance(s, types)]
752-
if rv and types == (Solid, Compound):
751+
rv = []
752+
for obj in self.objects:
753+
if isinstance(obj, types):
754+
rv.append(obj)
755+
# unpack compounds in a special way when looking for Solids
756+
elif isinstance(obj, Compound) and types == (Solid,):
757+
for T in types:
758+
# _entities(...) needed due to weird behavior with shelled object unpacking
759+
rv.extend(T(el) for el in obj._entities(T.__name__))
760+
# otherwise unpack compounds normally
761+
elif isinstance(obj, Compound):
762+
rv.extend(el for el in obj if isinstance(el, type))
763+
764+
if rv and types == (Solid,):
753765
return Compound.makeCompound(rv)
754766
elif rv:
755767
return rv[0]
@@ -781,7 +793,7 @@ def findSolid(
781793
results with an object already on the stack.
782794
"""
783795

784-
found = self._findType((Solid, Compound), searchStack, searchParents)
796+
found = self._findType((Solid,), searchStack, searchParents)
785797

786798
if found is None:
787799
message = "on the stack or " if searchStack else ""
@@ -802,7 +814,7 @@ def findFace(self, searchStack: bool = True, searchParents: bool = True) -> Face
802814
:returns: A face or None if no face is found.
803815
"""
804816

805-
found = self._findType(Face, searchStack, searchParents)
817+
found = self._findType((Face,), searchStack, searchParents)
806818

807819
if found is None:
808820
message = "on the stack or " if searchStack else ""
@@ -1460,8 +1472,8 @@ def rarray(
14601472
If you want to position the array at another point, create another workplane
14611473
that is shifted to the position you would like to use as a reference
14621474
1463-
:param xSpacing: spacing between points in the x direction ( must be > 0)
1464-
:param ySpacing: spacing between points in the y direction ( must be > 0)
1475+
:param xSpacing: spacing between points in the x direction ( must be >= 0)
1476+
:param ySpacing: spacing between points in the y direction ( must be >= 0)
14651477
:param xCount: number of points ( > 0 )
14661478
:param yCount: number of points ( > 0 )
14671479
:param center: If True, the array will be centered around the workplane center.
@@ -1470,8 +1482,8 @@ def rarray(
14701482
centering along each axis.
14711483
"""
14721484

1473-
if xSpacing <= 0 or ySpacing <= 0 or xCount < 1 or yCount < 1:
1474-
raise ValueError("Spacing and count must be > 0 ")
1485+
if (xSpacing <= 0 and ySpacing <= 0) or xCount < 1 or yCount < 1:
1486+
raise ValueError("Spacing and count must be > 0 in at least one direction")
14751487

14761488
if isinstance(center, bool):
14771489
center = (center, center)
@@ -3310,9 +3322,7 @@ def _fuseWithBase(self: T, obj: Shape) -> T:
33103322
:return: a new object that represents the result of combining the base object with obj,
33113323
or obj if one could not be found
33123324
"""
3313-
baseSolid = self._findType(
3314-
(Solid, Compound), searchStack=True, searchParents=True
3315-
)
3325+
baseSolid = self._findType((Solid,), searchStack=True, searchParents=True)
33163326
r = obj
33173327
if baseSolid is not None:
33183328
r = baseSolid.fuse(obj)
@@ -3328,7 +3338,7 @@ def _cutFromBase(self: T, obj: Shape) -> T:
33283338
:return: a new object that represents the result of combining the base object with obj,
33293339
or obj if one could not be found
33303340
"""
3331-
baseSolid = self._findType((Solid, Compound), True, True)
3341+
baseSolid = self._findType((Solid,), True, True)
33323342

33333343
r = obj
33343344
if baseSolid is not None:
@@ -3399,9 +3409,7 @@ def union(
33993409

34003410
# now combine with existing solid, if there is one
34013411
# look for parents to cut from
3402-
solidRef = self._findType(
3403-
(Solid, Compound), searchStack=True, searchParents=True
3404-
)
3412+
solidRef = self._findType((Solid,), searchStack=True, searchParents=True)
34053413
if solidRef is not None:
34063414
r = solidRef.fuse(*newS, glue=glue, tol=tol)
34073415
elif len(newS) > 1:
@@ -3414,7 +3422,8 @@ def union(
34143422

34153423
return self.newObject([r])
34163424

3417-
def __or__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
3425+
@deprecate()
3426+
def __or__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
34183427
"""
34193428
Syntactic sugar for union.
34203429
@@ -3426,15 +3435,15 @@ def __or__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
34263435
Sphere = Workplane("XY").sphere(1)
34273436
result = Box | Sphere
34283437
"""
3429-
return self.union(toUnion)
3438+
return self.union(other)
34303439

3431-
def __add__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
3440+
def __add__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
34323441
"""
34333442
Syntactic sugar for union.
34343443
34353444
Notice that :code:`r = a + b` is equivalent to :code:`r = a.union(b)` and :code:`r = a | b`.
34363445
"""
3437-
return self.union(toUnion)
3446+
return self.union(other)
34383447

34393448
def cut(
34403449
self: T,
@@ -3472,7 +3481,7 @@ def cut(
34723481

34733482
return self.newObject([newS])
34743483

3475-
def __sub__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
3484+
def __sub__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
34763485
"""
34773486
Syntactic sugar for cut.
34783487
@@ -3484,7 +3493,7 @@ def __sub__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
34843493
Sphere = Workplane("XY").sphere(1)
34853494
result = Box - Sphere
34863495
"""
3487-
return self.cut(toUnion)
3496+
return self.cut(other)
34883497

34893498
def intersect(
34903499
self: T,
@@ -3522,7 +3531,8 @@ def intersect(
35223531

35233532
return self.newObject([newS])
35243533

3525-
def __and__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
3534+
@deprecate()
3535+
def __and__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
35263536
"""
35273537
Syntactic sugar for intersect.
35283538
@@ -3534,7 +3544,38 @@ def __and__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
35343544
Sphere = Workplane("XY").sphere(1)
35353545
result = Box & Sphere
35363546
"""
3537-
return self.intersect(toUnion)
3547+
3548+
return self.intersect(other)
3549+
3550+
def __mul__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
3551+
"""
3552+
Syntactic sugar for intersect.
3553+
3554+
Notice that :code:`r = a * b` is equivalent to :code:`r = a.intersect(b)`.
3555+
3556+
Example::
3557+
3558+
Box = Workplane("XY").box(1, 1, 1, centered=(False, False, False))
3559+
Sphere = Workplane("XY").sphere(1)
3560+
result = Box * Sphere
3561+
"""
3562+
3563+
return self.intersect(other)
3564+
3565+
def __truediv__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
3566+
"""
3567+
Syntactic sugar for intersect.
3568+
3569+
Notice that :code:`r = a / b` is equivalent to :code:`r = a.split(b)`.
3570+
3571+
Example::
3572+
3573+
Box = Workplane("XY").box(1, 1, 1, centered=(False, False, False))
3574+
Sphere = Workplane("XY").sphere(1)
3575+
result = Box / Sphere
3576+
"""
3577+
3578+
return self.split(other)
35383579

35393580
def cutBlind(
35403581
self: T,
@@ -3680,6 +3721,10 @@ def _getFaces(self) -> List[Face]:
36803721
for el in self.objects:
36813722
if isinstance(el, Sketch):
36823723
rv.extend(el)
3724+
elif isinstance(el, Face):
3725+
rv.append(el)
3726+
elif isinstance(el, Compound):
3727+
rv.extend(subel for subel in el if isinstance(subel, Face))
36833728

36843729
if not rv:
36853730
rv.extend(wiresToFaces(self.ctx.popPendingWires()))
@@ -4441,6 +4486,19 @@ def __getitem__(self: T, item: Union[int, Sequence[int], slice]) -> T:
44414486

44424487
return rv
44434488

4489+
def __iter__(self: T) -> Iterator[Shape]:
4490+
"""
4491+
Special method for iterating over Shapes in objects
4492+
"""
4493+
4494+
for el in self.objects:
4495+
if isinstance(el, Compound):
4496+
yield from el
4497+
elif isinstance(el, Shape):
4498+
yield el
4499+
elif isinstance(el, Sketch):
4500+
yield from el
4501+
44444502
def filter(self: T, f: Callable[[CQObject], bool]) -> T:
44454503
"""
44464504
Filter items using a boolean predicate.
@@ -4488,11 +4546,7 @@ def invoke(
44884546
:return: Workplane object.
44894547
"""
44904548

4491-
if isbuiltin(f):
4492-
arity = 0 # assume 0 arity for builtins; they cannot be introspected
4493-
else:
4494-
arity = f.__code__.co_argcount # NB: this is not understood by mypy
4495-
4549+
arity = get_arity(f)
44964550
rv = self
44974551

44984552
if arity == 0:
@@ -4506,6 +4560,29 @@ def invoke(
45064560

45074561
return rv
45084562

4563+
def export(
4564+
self: T,
4565+
fname: str,
4566+
tolerance: float = 0.1,
4567+
angularTolerance: float = 0.1,
4568+
opt: Optional[Dict[str, Any]] = None,
4569+
) -> T:
4570+
"""
4571+
Export Workplane to file.
4572+
4573+
:param path: Filename.
4574+
:param tolerance: the deflection tolerance, in model units. Default 0.1.
4575+
:param angularTolerance: the angular tolerance, in radians. Default 0.1.
4576+
:param opt: additional options passed to the specific exporter. Default None.
4577+
:return: Self.
4578+
"""
4579+
4580+
export(
4581+
self, fname, tolerance=tolerance, angularTolerance=angularTolerance, opt=opt
4582+
)
4583+
4584+
return self
4585+
45094586

45104587
# alias for backward compatibility
45114588
CQ = Workplane

0 commit comments

Comments
 (0)