Skip to content

Commit

Permalink
Sketch and Workplane improvements (#1633)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
adam-urbanczyk and jmwright authored Aug 21, 2024
1 parent 23ec959 commit 6d7d1d6
Show file tree
Hide file tree
Showing 19 changed files with 1,084 additions and 195 deletions.
59 changes: 58 additions & 1 deletion cadquery/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
exportVRML,
exportGLTF,
STEPExportModeLiterals,
ExportModes,
)

from .selectors import _expression_grammar as _selector_grammar
from .utils import deprecate

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

return self

@deprecate()
def save(
self,
path: str,
Expand Down Expand Up @@ -507,6 +508,62 @@ def save(

return self

def export(
self,
path: str,
exportType: Optional[ExportLiterals] = None,
mode: STEPExportModeLiterals = "default",
tolerance: float = 0.1,
angularTolerance: float = 0.1,
**kwargs,
) -> "Assembly":
"""
Save assembly to a file.
:param path: Path and filename for writing.
:param exportType: export format (default: None, results in format being inferred form the path)
:param mode: STEP only - See :meth:`~cadquery.occ_impl.exporters.assembly.exportAssembly`.
:param tolerance: the deflection tolerance, in model units. Only used for glTF, VRML. Default 0.1.
:param angularTolerance: the angular tolerance, in radians. Only used for glTF, VRML. Default 0.1.
:param \\**kwargs: Additional keyword arguments. Only used for STEP, glTF and STL.
See :meth:`~cadquery.occ_impl.exporters.assembly.exportAssembly`.
:param ascii: STL only - Sets whether or not STL export should be text or binary
:type ascii: bool
"""

# Make sure the export mode setting is correct
if mode not in get_args(STEPExportModeLiterals):
raise ValueError(f"Unknown assembly export mode {mode} for STEP")

if exportType is None:
t = path.split(".")[-1].upper()
if t in ("STEP", "XML", "VRML", "VTKJS", "GLTF", "GLB", "STL"):
exportType = cast(ExportLiterals, t)
else:
raise ValueError("Unknown extension, specify export type explicitly")

if exportType == "STEP":
exportAssembly(self, path, mode, **kwargs)
elif exportType == "XML":
exportCAF(self, path)
elif exportType == "VRML":
exportVRML(self, path, tolerance, angularTolerance)
elif exportType == "GLTF" or exportType == "GLB":
exportGLTF(self, path, None, tolerance, angularTolerance)
elif exportType == "VTKJS":
exportVTKJS(self, path)
elif exportType == "STL":
# Handle the ascii setting for STL export
export_ascii = False
if "ascii" in kwargs:
export_ascii = bool(kwargs.get("ascii"))

self.toCompound().exportStl(path, tolerance, angularTolerance, export_ascii)
else:
raise ValueError(f"Unknown format: {exportType}")

return self

@classmethod
def load(cls, path: str) -> "Assembly":

Expand Down
145 changes: 111 additions & 34 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
List,
cast,
Dict,
Iterator,
)
from typing_extensions import Literal
from inspect import Parameter, Signature, isbuiltin
from inspect import Parameter, Signature


from .occ_impl.geom import Vector, Plane, Location
Expand All @@ -51,8 +52,9 @@
)

from .occ_impl.exporters.svg import getSVG, exportSVG
from .occ_impl.exporters import export

from .utils import deprecate, deprecate_kwarg_name
from .utils import deprecate, deprecate_kwarg_name, get_arity

from .selectors import (
Selector,
Expand Down Expand Up @@ -270,7 +272,7 @@ def split(self: T, keepTop: bool = False, keepBottom: bool = False) -> T:
...

@overload
def split(self: T, splitter: Union[T, Shape]) -> T:
def split(self: T, splitter: Union["Workplane", Shape]) -> T:
...

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

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

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

if searchStack:
rv = [s for s in self.objects if isinstance(s, types)]
if rv and types == (Solid, Compound):
rv = []
for obj in self.objects:
if isinstance(obj, types):
rv.append(obj)
# unpack compounds in a special way when looking for Solids
elif isinstance(obj, Compound) and types == (Solid,):
for T in types:
# _entities(...) needed due to weird behavior with shelled object unpacking
rv.extend(T(el) for el in obj._entities(T.__name__))
# otherwise unpack compounds normally
elif isinstance(obj, Compound):
rv.extend(el for el in obj if isinstance(el, type))

if rv and types == (Solid,):
return Compound.makeCompound(rv)
elif rv:
return rv[0]
Expand Down Expand Up @@ -781,7 +793,7 @@ def findSolid(
results with an object already on the stack.
"""

found = self._findType((Solid, Compound), searchStack, searchParents)
found = self._findType((Solid,), searchStack, searchParents)

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

found = self._findType(Face, searchStack, searchParents)
found = self._findType((Face,), searchStack, searchParents)

if found is None:
message = "on the stack or " if searchStack else ""
Expand Down Expand Up @@ -1460,8 +1472,8 @@ def rarray(
If you want to position the array at another point, create another workplane
that is shifted to the position you would like to use as a reference
:param xSpacing: spacing between points in the x direction ( must be > 0)
:param ySpacing: spacing between points in the y direction ( must be > 0)
:param xSpacing: spacing between points in the x direction ( must be >= 0)
:param ySpacing: spacing between points in the y direction ( must be >= 0)
:param xCount: number of points ( > 0 )
:param yCount: number of points ( > 0 )
:param center: If True, the array will be centered around the workplane center.
Expand All @@ -1470,8 +1482,8 @@ def rarray(
centering along each axis.
"""

if xSpacing <= 0 or ySpacing <= 0 or xCount < 1 or yCount < 1:
raise ValueError("Spacing and count must be > 0 ")
if (xSpacing <= 0 and ySpacing <= 0) or xCount < 1 or yCount < 1:
raise ValueError("Spacing and count must be > 0 in at least one direction")

if isinstance(center, bool):
center = (center, center)
Expand Down Expand Up @@ -3310,9 +3322,7 @@ def _fuseWithBase(self: T, obj: Shape) -> T:
:return: a new object that represents the result of combining the base object with obj,
or obj if one could not be found
"""
baseSolid = self._findType(
(Solid, Compound), searchStack=True, searchParents=True
)
baseSolid = self._findType((Solid,), searchStack=True, searchParents=True)
r = obj
if baseSolid is not None:
r = baseSolid.fuse(obj)
Expand All @@ -3328,7 +3338,7 @@ def _cutFromBase(self: T, obj: Shape) -> T:
:return: a new object that represents the result of combining the base object with obj,
or obj if one could not be found
"""
baseSolid = self._findType((Solid, Compound), True, True)
baseSolid = self._findType((Solid,), True, True)

r = obj
if baseSolid is not None:
Expand Down Expand Up @@ -3399,9 +3409,7 @@ def union(

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

return self.newObject([r])

def __or__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
@deprecate()
def __or__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
"""
Syntactic sugar for union.
Expand All @@ -3426,15 +3435,15 @@ def __or__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
Sphere = Workplane("XY").sphere(1)
result = Box | Sphere
"""
return self.union(toUnion)
return self.union(other)

def __add__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
def __add__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
"""
Syntactic sugar for union.
Notice that :code:`r = a + b` is equivalent to :code:`r = a.union(b)` and :code:`r = a | b`.
"""
return self.union(toUnion)
return self.union(other)

def cut(
self: T,
Expand Down Expand Up @@ -3472,7 +3481,7 @@ def cut(

return self.newObject([newS])

def __sub__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
def __sub__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
"""
Syntactic sugar for cut.
Expand All @@ -3484,7 +3493,7 @@ def __sub__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
Sphere = Workplane("XY").sphere(1)
result = Box - Sphere
"""
return self.cut(toUnion)
return self.cut(other)

def intersect(
self: T,
Expand Down Expand Up @@ -3522,7 +3531,8 @@ def intersect(

return self.newObject([newS])

def __and__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
@deprecate()
def __and__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
"""
Syntactic sugar for intersect.
Expand All @@ -3534,7 +3544,38 @@ def __and__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
Sphere = Workplane("XY").sphere(1)
result = Box & Sphere
"""
return self.intersect(toUnion)

return self.intersect(other)

def __mul__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
"""
Syntactic sugar for intersect.
Notice that :code:`r = a * b` is equivalent to :code:`r = a.intersect(b)`.
Example::
Box = Workplane("XY").box(1, 1, 1, centered=(False, False, False))
Sphere = Workplane("XY").sphere(1)
result = Box * Sphere
"""

return self.intersect(other)

def __truediv__(self: T, other: Union["Workplane", Solid, Compound]) -> T:
"""
Syntactic sugar for intersect.
Notice that :code:`r = a / b` is equivalent to :code:`r = a.split(b)`.
Example::
Box = Workplane("XY").box(1, 1, 1, centered=(False, False, False))
Sphere = Workplane("XY").sphere(1)
result = Box / Sphere
"""

return self.split(other)

def cutBlind(
self: T,
Expand Down Expand Up @@ -3680,6 +3721,10 @@ def _getFaces(self) -> List[Face]:
for el in self.objects:
if isinstance(el, Sketch):
rv.extend(el)
elif isinstance(el, Face):
rv.append(el)
elif isinstance(el, Compound):
rv.extend(subel for subel in el if isinstance(subel, Face))

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

return rv

def __iter__(self: T) -> Iterator[Shape]:
"""
Special method for iterating over Shapes in objects
"""

for el in self.objects:
if isinstance(el, Compound):
yield from el
elif isinstance(el, Shape):
yield el
elif isinstance(el, Sketch):
yield from el

def filter(self: T, f: Callable[[CQObject], bool]) -> T:
"""
Filter items using a boolean predicate.
Expand Down Expand Up @@ -4488,11 +4546,7 @@ def invoke(
:return: Workplane object.
"""

if isbuiltin(f):
arity = 0 # assume 0 arity for builtins; they cannot be introspected
else:
arity = f.__code__.co_argcount # NB: this is not understood by mypy

arity = get_arity(f)
rv = self

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

return rv

def export(
self: T,
fname: str,
tolerance: float = 0.1,
angularTolerance: float = 0.1,
opt: Optional[Dict[str, Any]] = None,
) -> T:
"""
Export Workplane to file.
:param path: Filename.
:param tolerance: the deflection tolerance, in model units. Default 0.1.
:param angularTolerance: the angular tolerance, in radians. Default 0.1.
:param opt: additional options passed to the specific exporter. Default None.
:return: Self.
"""

export(
self, fname, tolerance=tolerance, angularTolerance=angularTolerance, opt=opt
)

return self


# alias for backward compatibility
CQ = Workplane
Loading

0 comments on commit 6d7d1d6

Please sign in to comment.