Skip to content

Commit

Permalink
Merge pull request #954 from Jojain/cut_kw_arg
Browse files Browse the repository at this point in the history
added combine="cut" option for 3D operations
  • Loading branch information
jmwright authored Jan 25, 2022
2 parents fdc5d0f + 9217b3f commit e0b4c94
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 71 deletions.
160 changes: 90 additions & 70 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from typing_extensions import Literal
from inspect import Parameter, Signature


from .occ_impl.geom import Vector, Plane, Location
from .occ_impl.shapes import (
Shape,
Expand All @@ -50,7 +51,7 @@

from .occ_impl.exporters.svg import getSVG, exportSVG

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

from .selectors import (
Selector,
Expand All @@ -61,6 +62,7 @@

CQObject = Union[Vector, Location, Shape, Sketch]
VectorLike = Union[Tuple[float, float], Tuple[float, float, float], Vector]
CombineMode = Union[bool, Literal["cut", "a", "s"]] # a : additive, s: subtractive

T = TypeVar("T", bound="Workplane")
"""A type variable used to make the return type of a method the same as the
Expand Down Expand Up @@ -2391,6 +2393,8 @@ def each(
self: T,
callback: Callable[[CQObject], Shape],
useLocalCoordinates: bool = False,
combine: CombineMode = True,
clean: bool = True,
) -> T:
"""
Runs the provided function on each value in the stack, and collects the return values into
Expand All @@ -2401,6 +2405,9 @@ def each(
:param callBackFunction: the function to call for each item on the current stack.
:param useLocalCoordinates: should values be converted from local coordinates first?
:type useLocalCoordinates: boolean
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
The callback function must accept one argument, which is the item on the stack, and return
one object, which is collected. If the function returns None, nothing is added to the stack.
Expand Down Expand Up @@ -2436,15 +2443,16 @@ def each(
if isinstance(r, Wire):
if not r.forConstruction:
self._addPendingWire(r)

results.append(r)

return self.newObject(results)
return self._combineWithBase(results, combine, clean)

def eachpoint(
self: T,
callback: Callable[[Location], Shape],
useLocalCoordinates: bool = False,
combine: CombineMode = False,
clean: bool = True,
) -> T:
"""
Same as each(), except each item on the stack is converted into a point before it
Expand All @@ -2454,6 +2462,9 @@ def eachpoint(
:param useLocalCoordinates: should points be in local or global coordinates
:type useLocalCoordinates: boolean
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
The resulting object has a point on the stack for each object on the original stack.
Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted
Expand Down Expand Up @@ -2489,7 +2500,7 @@ def eachpoint(
if isinstance(r, Wire) and not r.forConstruction:
self._addPendingWire(r)

return self.newObject(res)
return self._combineWithBase(res, combine, clean)

def rect(
self: T,
Expand Down Expand Up @@ -2948,7 +2959,7 @@ def twistExtrude(
self: T,
distance: float,
angleDegrees: float,
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
) -> T:
"""
Expand All @@ -2965,7 +2976,7 @@ def twistExtrude(
:param distance: the distance to extrude normal to the workplane
:param angle: angle (in degrees) to rotate through the extrusion
:param boolean combine: True to combine the resulting solid with parent solids if found.
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
:return: a CQ object with the resulting solid selected.
"""
Expand All @@ -2990,39 +3001,30 @@ def twistExtrude(

r = Compound.makeCompound(shapes).fuse()

if combine:
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean:
newS = newS.clean()
return newS
return self._combineWithBase(r, combine, clean)

def extrude(
self: T,
until: Union[float, Literal["next", "last"], Face],
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
both: bool = False,
taper: Optional[float] = None,
) -> T:
"""
Use all un-extruded wires in the parent chain to create a prismatic solid.
:param until: the distance to extrude, normal to the workplane plane
:param until: The distance to extrude, normal to the workplane plane. When a float is
passed, the extrusion extends this far and a negative value is in the opposite direction
to the normal of the plane. The string "next" extrudes until the next face orthogonal to
the wire normal. "last" extrudes to the last face. If a object of type Face is passed then
the extrusion will extend until this face.
:param boolean combine: True to combine the resulting solid with parent solids if found. (Cannot be set to False when `until` is not set as a float)
the extrusion will extend until this face. **Note that the Workplane must contain a Solid for extruding to a given face.**
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
:param boolean both: extrude in both directions symmetrically
:param float taper: angle for optional tapered extrusion
:return: a CQ object with the resulting solid selected.
extrude always *adds* material to a part.
The returned object is always a CQ object, and depends on whether combine is True, and
whether a context solid is already defined:
Expand All @@ -3031,14 +3033,19 @@ def extrude(
* if combine is true, the value is combined with the context solid if it exists,
and the resulting solid becomes the new context solid.
"""

# If subtractive mode is requested, use cutBlind
if combine in ("cut", "s"):
return self.cutBlind(until, clean, taper)

# Handle `until` multiple values
if isinstance(until, str) and until in ("next", "last") and combine:
elif until in ("next", "last") and combine in (True, "a"):
if until == "next":
faceIndex = 0
elif until == "last":
faceIndex = -1

r = self._extrude(distance=None, both=both, taper=taper, upToFace=faceIndex)
r = self._extrude(None, both=both, taper=taper, upToFace=faceIndex)

elif isinstance(until, Face) and combine:
r = self._extrude(None, both=both, taper=taper, upToFace=until)
Expand All @@ -3056,20 +3063,14 @@ def extrude(
f"Do not know how to handle until argument of type {type(until)}"
)

if combine:
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean:
newS = newS.clean()
return newS
return self._combineWithBase(r, combine, clean)

def revolve(
self: T,
angleDegrees: float = 360.0,
axisStart: Optional[VectorLike] = None,
axisEnd: Optional[VectorLike] = None,
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
) -> T:
"""
Expand All @@ -3081,8 +3082,7 @@ def revolve(
:type axisStart: tuple, a two tuple
:param axisEnd: the end point of the axis of rotation
:type axisEnd: tuple, a two tuple
:param combine: True to combine the resulting solid with parent solids if found.
:type combine: boolean, combine with parent solid
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
:return: a CQ object with the resulting solid selected.
Expand Down Expand Up @@ -3126,13 +3126,8 @@ def revolve(

# returns a Solid (or a compound if there were multiple)
r = self._revolve(angleDegrees, axisStart, axisEnd)
if combine:
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean:
newS = newS.clean()
return newS

return self._combineWithBase(r, combine, clean)

def sweep(
self: T,
Expand All @@ -3141,7 +3136,7 @@ def sweep(
sweepAlongWires: Optional[bool] = None,
makeSolid: bool = True,
isFrenet: bool = False,
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
transition: Literal["right", "round", "transformed"] = "right",
normal: Optional[VectorLike] = None,
Expand All @@ -3152,7 +3147,7 @@ def sweep(
:param path: A wire along which the pending wires will be swept
:param boolean multiSection: False to create multiple swept from wires on the chain along path. True to create only one solid swept along path with shape following the list of wires on the chain
:param boolean combine: True to combine the resulting solid with parent solids if found.
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
:param transition: handling of profile orientation at C1 path discontinuities. Possible values are {'transformed','round', 'right'} (default: 'right').
:param normal: optional fixed normal for extrusion
Expand Down Expand Up @@ -3181,18 +3176,48 @@ def sweep(
auxSpine,
) # returns a Solid (or a compound if there were multiple)

newS: T
if combine:
newS = self._combineWithBase(r)
return self._combineWithBase(r, combine, clean)

def _combineWithBase(
self: T,
obj: Union[Shape, Iterable[Shape]],
mode: CombineMode = True,
clean: bool = False,
) -> T:
"""
Combines the provided object with the base solid, if one can be found.
:param obj: The object to be combined with the context solid
:param mode: The mode to combine with the base solid (True, False, "cut", "a" or "s")
:return: a new object that represents the result of combining the base object with obj,
or obj if one could not be found
"""

if mode:
# since we are going to do something convert the iterable if needed
if not isinstance(obj, Shape):
obj = Compound.makeCompound(obj)

# dispatch on the mode
if mode in ("cut", "s"):
newS = self._cutFromBase(obj)
elif mode in (True, "a"):
newS = self._fuseWithBase(obj)

else:
newS = self.newObject([r])
# do not combine branch
newS = self.newObject(obj if not isinstance(obj, Shape) else [obj])

if clean:
newS = newS.clean()
# NB: not calling self.clean() to not pollute the parents
newS.objects = [
obj.clean() if isinstance(obj, Shape) else obj for obj in newS.objects
]

return newS

def _combineWithBase(self: T, obj: Shape) -> T:
def _fuseWithBase(self: T, obj: Shape) -> T:
"""
Combines the provided object with the base solid, if one can be found.
Fuse the provided object with the base solid, if one can be found.
:param obj:
:return: a new object that represents the result of combining the base object with obj,
or obj if one could not be found
Expand All @@ -3205,7 +3230,6 @@ def _combineWithBase(self: T, obj: Shape) -> T:
r = baseSolid.fuse(obj)
elif isinstance(obj, Compound):
r = obj.fuse()

return self.newObject([r])

def _cutFromBase(self: T, obj: Shape) -> T:
Expand All @@ -3215,9 +3239,8 @@ 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), searchStack=True, searchParents=True
)
baseSolid = self._findType((Solid, Compound), True, True)

r = obj
if baseSolid is not None:
r = baseSolid.cut(obj)
Expand Down Expand Up @@ -3490,11 +3513,17 @@ def cutThruAll(self: T, clean: bool = True, taper: float = 0) -> T:
return self.newObject([s])

def loft(
self: T, filled: bool = True, ruled: bool = False, combine: bool = True
self: T, ruled: bool = False, combine: CombineMode = True, clean: bool = True
) -> T:
"""
Make a lofted solid, through the set of wires.
:return: a CQ object containing the created loft
:param boolean ruled: When set to `True` connects each section linearly and without continuity
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
:return: a Workplane object containing the created loft
"""

if self.ctx.pendingWires:
Expand All @@ -3507,14 +3536,9 @@ def loft(

r: Shape = Solid.makeLoft(wiresToLoft, ruled)

if combine:
parentSolid = self._findType(
(Solid, Compound), searchStack=False, searchParents=True
)
if parentSolid is not None:
r = parentSolid.fuse(r)
newS = self._combineWithBase(r, combine, clean)

return self.newObject([r])
return newS

def _getFaces(self) -> List[Face]:
"""
Expand Down Expand Up @@ -4115,13 +4139,14 @@ def clean(self: T) -> T:

return self.newObject(cleanObjects)

@deprecate_kwarg_name("cut", "combine='cut'")
def text(
self: T,
txt: str,
fontsize: float,
distance: float,
cut: bool = True,
combine: bool = False,
combine: CombineMode = False,
clean: bool = True,
font: str = "Arial",
fontPath: Optional[str] = None,
Expand All @@ -4137,7 +4162,7 @@ def text(
:param distance: the distance to extrude or cut, normal to the workplane plane
:type distance: float, negative means opposite the normal direction
:param cut: True to cut the resulting solid from the parent solids if found
:param combine: True to combine the resulting solid with parent solids if found
:param combine: True or "a" to combine the resulting solid with parent solids if found, "cut" or "s" to remove the resulting solid from the parent solids if found. False to keep the resulting solid separated from the parent solids.
:param clean: call :py:meth:`clean` afterwards to have a clean shape
:param font: font name
:param fontPath: path to font file
Expand Down Expand Up @@ -4183,14 +4208,9 @@ def text(
)

if cut:
newS = self._cutFromBase(r)
elif combine:
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean:
newS = newS.clean()
return newS
combine = "cut"

return self._combineWithBase(r, combine, clean)

def section(self: T, height: float = 0.0) -> T:
"""
Expand Down
Loading

0 comments on commit e0b4c94

Please sign in to comment.