Skip to content

Commit e0b4c94

Browse files
authored
Merge pull request #954 from Jojain/cut_kw_arg
added combine="cut" option for 3D operations
2 parents fdc5d0f + 9217b3f commit e0b4c94

File tree

5 files changed

+223
-71
lines changed

5 files changed

+223
-71
lines changed

cadquery/cq.py

Lines changed: 90 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from typing_extensions import Literal
3838
from inspect import Parameter, Signature
3939

40+
4041
from .occ_impl.geom import Vector, Plane, Location
4142
from .occ_impl.shapes import (
4243
Shape,
@@ -50,7 +51,7 @@
5051

5152
from .occ_impl.exporters.svg import getSVG, exportSVG
5253

53-
from .utils import deprecate
54+
from .utils import deprecate, deprecate_kwarg_name
5455

5556
from .selectors import (
5657
Selector,
@@ -61,6 +62,7 @@
6162

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

6567
T = TypeVar("T", bound="Workplane")
6668
"""A type variable used to make the return type of a method the same as the
@@ -2391,6 +2393,8 @@ def each(
23912393
self: T,
23922394
callback: Callable[[CQObject], Shape],
23932395
useLocalCoordinates: bool = False,
2396+
combine: CombineMode = True,
2397+
clean: bool = True,
23942398
) -> T:
23952399
"""
23962400
Runs the provided function on each value in the stack, and collects the return values into
@@ -2401,6 +2405,9 @@ def each(
24012405
:param callBackFunction: the function to call for each item on the current stack.
24022406
:param useLocalCoordinates: should values be converted from local coordinates first?
24032407
:type useLocalCoordinates: boolean
2408+
: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.
2409+
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
2410+
24042411
24052412
The callback function must accept one argument, which is the item on the stack, and return
24062413
one object, which is collected. If the function returns None, nothing is added to the stack.
@@ -2436,15 +2443,16 @@ def each(
24362443
if isinstance(r, Wire):
24372444
if not r.forConstruction:
24382445
self._addPendingWire(r)
2439-
24402446
results.append(r)
24412447

2442-
return self.newObject(results)
2448+
return self._combineWithBase(results, combine, clean)
24432449

24442450
def eachpoint(
24452451
self: T,
24462452
callback: Callable[[Location], Shape],
24472453
useLocalCoordinates: bool = False,
2454+
combine: CombineMode = False,
2455+
clean: bool = True,
24482456
) -> T:
24492457
"""
24502458
Same as each(), except each item on the stack is converted into a point before it
@@ -2454,6 +2462,9 @@ def eachpoint(
24542462
24552463
:param useLocalCoordinates: should points be in local or global coordinates
24562464
:type useLocalCoordinates: boolean
2465+
: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.
2466+
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
2467+
24572468
24582469
The resulting object has a point on the stack for each object on the original stack.
24592470
Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted
@@ -2489,7 +2500,7 @@ def eachpoint(
24892500
if isinstance(r, Wire) and not r.forConstruction:
24902501
self._addPendingWire(r)
24912502

2492-
return self.newObject(res)
2503+
return self._combineWithBase(res, combine, clean)
24932504

24942505
def rect(
24952506
self: T,
@@ -2948,7 +2959,7 @@ def twistExtrude(
29482959
self: T,
29492960
distance: float,
29502961
angleDegrees: float,
2951-
combine: bool = True,
2962+
combine: CombineMode = True,
29522963
clean: bool = True,
29532964
) -> T:
29542965
"""
@@ -2965,7 +2976,7 @@ def twistExtrude(
29652976
29662977
:param distance: the distance to extrude normal to the workplane
29672978
:param angle: angle (in degrees) to rotate through the extrusion
2968-
:param boolean combine: True to combine the resulting solid with parent solids if found.
2979+
: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.
29692980
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
29702981
:return: a CQ object with the resulting solid selected.
29712982
"""
@@ -2990,39 +3001,30 @@ def twistExtrude(
29903001

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

2993-
if combine:
2994-
newS = self._combineWithBase(r)
2995-
else:
2996-
newS = self.newObject([r])
2997-
if clean:
2998-
newS = newS.clean()
2999-
return newS
3004+
return self._combineWithBase(r, combine, clean)
30003005

30013006
def extrude(
30023007
self: T,
30033008
until: Union[float, Literal["next", "last"], Face],
3004-
combine: bool = True,
3009+
combine: CombineMode = True,
30053010
clean: bool = True,
30063011
both: bool = False,
30073012
taper: Optional[float] = None,
30083013
) -> T:
30093014
"""
30103015
Use all un-extruded wires in the parent chain to create a prismatic solid.
30113016
3012-
:param until: the distance to extrude, normal to the workplane plane
30133017
:param until: The distance to extrude, normal to the workplane plane. When a float is
30143018
passed, the extrusion extends this far and a negative value is in the opposite direction
30153019
to the normal of the plane. The string "next" extrudes until the next face orthogonal to
30163020
the wire normal. "last" extrudes to the last face. If a object of type Face is passed then
3017-
the extrusion will extend until this face.
3018-
: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)
3021+
the extrusion will extend until this face. **Note that the Workplane must contain a Solid for extruding to a given face.**
3022+
: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.
30193023
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
30203024
:param boolean both: extrude in both directions symmetrically
30213025
:param float taper: angle for optional tapered extrusion
30223026
:return: a CQ object with the resulting solid selected.
30233027
3024-
extrude always *adds* material to a part.
3025-
30263028
The returned object is always a CQ object, and depends on whether combine is True, and
30273029
whether a context solid is already defined:
30283030
@@ -3031,14 +3033,19 @@ def extrude(
30313033
* if combine is true, the value is combined with the context solid if it exists,
30323034
and the resulting solid becomes the new context solid.
30333035
"""
3036+
3037+
# If subtractive mode is requested, use cutBlind
3038+
if combine in ("cut", "s"):
3039+
return self.cutBlind(until, clean, taper)
3040+
30343041
# Handle `until` multiple values
3035-
if isinstance(until, str) and until in ("next", "last") and combine:
3042+
elif until in ("next", "last") and combine in (True, "a"):
30363043
if until == "next":
30373044
faceIndex = 0
30383045
elif until == "last":
30393046
faceIndex = -1
30403047

3041-
r = self._extrude(distance=None, both=both, taper=taper, upToFace=faceIndex)
3048+
r = self._extrude(None, both=both, taper=taper, upToFace=faceIndex)
30423049

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

3059-
if combine:
3060-
newS = self._combineWithBase(r)
3061-
else:
3062-
newS = self.newObject([r])
3063-
if clean:
3064-
newS = newS.clean()
3065-
return newS
3066+
return self._combineWithBase(r, combine, clean)
30663067

30673068
def revolve(
30683069
self: T,
30693070
angleDegrees: float = 360.0,
30703071
axisStart: Optional[VectorLike] = None,
30713072
axisEnd: Optional[VectorLike] = None,
3072-
combine: bool = True,
3073+
combine: CombineMode = True,
30733074
clean: bool = True,
30743075
) -> T:
30753076
"""
@@ -3081,8 +3082,7 @@ def revolve(
30813082
:type axisStart: tuple, a two tuple
30823083
:param axisEnd: the end point of the axis of rotation
30833084
:type axisEnd: tuple, a two tuple
3084-
:param combine: True to combine the resulting solid with parent solids if found.
3085-
:type combine: boolean, combine with parent solid
3085+
: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.
30863086
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
30873087
:return: a CQ object with the resulting solid selected.
30883088
@@ -3126,13 +3126,8 @@ def revolve(
31263126

31273127
# returns a Solid (or a compound if there were multiple)
31283128
r = self._revolve(angleDegrees, axisStart, axisEnd)
3129-
if combine:
3130-
newS = self._combineWithBase(r)
3131-
else:
3132-
newS = self.newObject([r])
3133-
if clean:
3134-
newS = newS.clean()
3135-
return newS
3129+
3130+
return self._combineWithBase(r, combine, clean)
31363131

31373132
def sweep(
31383133
self: T,
@@ -3141,7 +3136,7 @@ def sweep(
31413136
sweepAlongWires: Optional[bool] = None,
31423137
makeSolid: bool = True,
31433138
isFrenet: bool = False,
3144-
combine: bool = True,
3139+
combine: CombineMode = True,
31453140
clean: bool = True,
31463141
transition: Literal["right", "round", "transformed"] = "right",
31473142
normal: Optional[VectorLike] = None,
@@ -3152,7 +3147,7 @@ def sweep(
31523147
31533148
:param path: A wire along which the pending wires will be swept
31543149
: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
3155-
:param boolean combine: True to combine the resulting solid with parent solids if found.
3150+
: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.
31563151
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
31573152
:param transition: handling of profile orientation at C1 path discontinuities. Possible values are {'transformed','round', 'right'} (default: 'right').
31583153
:param normal: optional fixed normal for extrusion
@@ -3181,18 +3176,48 @@ def sweep(
31813176
auxSpine,
31823177
) # returns a Solid (or a compound if there were multiple)
31833178

3184-
newS: T
3185-
if combine:
3186-
newS = self._combineWithBase(r)
3179+
return self._combineWithBase(r, combine, clean)
3180+
3181+
def _combineWithBase(
3182+
self: T,
3183+
obj: Union[Shape, Iterable[Shape]],
3184+
mode: CombineMode = True,
3185+
clean: bool = False,
3186+
) -> T:
3187+
"""
3188+
Combines the provided object with the base solid, if one can be found.
3189+
:param obj: The object to be combined with the context solid
3190+
:param mode: The mode to combine with the base solid (True, False, "cut", "a" or "s")
3191+
:return: a new object that represents the result of combining the base object with obj,
3192+
or obj if one could not be found
3193+
"""
3194+
3195+
if mode:
3196+
# since we are going to do something convert the iterable if needed
3197+
if not isinstance(obj, Shape):
3198+
obj = Compound.makeCompound(obj)
3199+
3200+
# dispatch on the mode
3201+
if mode in ("cut", "s"):
3202+
newS = self._cutFromBase(obj)
3203+
elif mode in (True, "a"):
3204+
newS = self._fuseWithBase(obj)
3205+
31873206
else:
3188-
newS = self.newObject([r])
3207+
# do not combine branch
3208+
newS = self.newObject(obj if not isinstance(obj, Shape) else [obj])
3209+
31893210
if clean:
3190-
newS = newS.clean()
3211+
# NB: not calling self.clean() to not pollute the parents
3212+
newS.objects = [
3213+
obj.clean() if isinstance(obj, Shape) else obj for obj in newS.objects
3214+
]
3215+
31913216
return newS
31923217

3193-
def _combineWithBase(self: T, obj: Shape) -> T:
3218+
def _fuseWithBase(self: T, obj: Shape) -> T:
31943219
"""
3195-
Combines the provided object with the base solid, if one can be found.
3220+
Fuse the provided object with the base solid, if one can be found.
31963221
:param obj:
31973222
:return: a new object that represents the result of combining the base object with obj,
31983223
or obj if one could not be found
@@ -3205,7 +3230,6 @@ def _combineWithBase(self: T, obj: Shape) -> T:
32053230
r = baseSolid.fuse(obj)
32063231
elif isinstance(obj, Compound):
32073232
r = obj.fuse()
3208-
32093233
return self.newObject([r])
32103234

32113235
def _cutFromBase(self: T, obj: Shape) -> T:
@@ -3215,9 +3239,8 @@ def _cutFromBase(self: T, obj: Shape) -> T:
32153239
:return: a new object that represents the result of combining the base object with obj,
32163240
or obj if one could not be found
32173241
"""
3218-
baseSolid = self._findType(
3219-
(Solid, Compound), searchStack=True, searchParents=True
3220-
)
3242+
baseSolid = self._findType((Solid, Compound), True, True)
3243+
32213244
r = obj
32223245
if baseSolid is not None:
32233246
r = baseSolid.cut(obj)
@@ -3490,11 +3513,17 @@ def cutThruAll(self: T, clean: bool = True, taper: float = 0) -> T:
34903513
return self.newObject([s])
34913514

34923515
def loft(
3493-
self: T, filled: bool = True, ruled: bool = False, combine: bool = True
3516+
self: T, ruled: bool = False, combine: CombineMode = True, clean: bool = True
34943517
) -> T:
34953518
"""
34963519
Make a lofted solid, through the set of wires.
3497-
:return: a CQ object containing the created loft
3520+
3521+
:param boolean ruled: When set to `True` connects each section linearly and without continuity
3522+
: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.
3523+
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
3524+
3525+
:return: a Workplane object containing the created loft
3526+
34983527
"""
34993528

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

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

3510-
if combine:
3511-
parentSolid = self._findType(
3512-
(Solid, Compound), searchStack=False, searchParents=True
3513-
)
3514-
if parentSolid is not None:
3515-
r = parentSolid.fuse(r)
3539+
newS = self._combineWithBase(r, combine, clean)
35163540

3517-
return self.newObject([r])
3541+
return newS
35183542

35193543
def _getFaces(self) -> List[Face]:
35203544
"""
@@ -4115,13 +4139,14 @@ def clean(self: T) -> T:
41154139

41164140
return self.newObject(cleanObjects)
41174141

4142+
@deprecate_kwarg_name("cut", "combine='cut'")
41184143
def text(
41194144
self: T,
41204145
txt: str,
41214146
fontsize: float,
41224147
distance: float,
41234148
cut: bool = True,
4124-
combine: bool = False,
4149+
combine: CombineMode = False,
41254150
clean: bool = True,
41264151
font: str = "Arial",
41274152
fontPath: Optional[str] = None,
@@ -4137,7 +4162,7 @@ def text(
41374162
:param distance: the distance to extrude or cut, normal to the workplane plane
41384163
:type distance: float, negative means opposite the normal direction
41394164
:param cut: True to cut the resulting solid from the parent solids if found
4140-
:param combine: True to combine the resulting solid with parent solids if found
4165+
: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.
41414166
:param clean: call :py:meth:`clean` afterwards to have a clean shape
41424167
:param font: font name
41434168
:param fontPath: path to font file
@@ -4183,14 +4208,9 @@ def text(
41834208
)
41844209

41854210
if cut:
4186-
newS = self._cutFromBase(r)
4187-
elif combine:
4188-
newS = self._combineWithBase(r)
4189-
else:
4190-
newS = self.newObject([r])
4191-
if clean:
4192-
newS = newS.clean()
4193-
return newS
4211+
combine = "cut"
4212+
4213+
return self._combineWithBase(r, combine, clean)
41944214

41954215
def section(self: T, height: float = 0.0) -> T:
41964216
"""

0 commit comments

Comments
 (0)