-
Notifications
You must be signed in to change notification settings - Fork 291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added combine="cut" option for 3D operations #954
Changes from 31 commits
5fb8823
0dab2be
1460545
0329d21
973dfda
4bf22a9
f9b5b07
db070a9
699ffa1
b8c0063
0cf408a
daa994b
3631e07
502dc3a
59df1c0
5a7c254
42e144b
2c3a2fd
e2108d2
ee96ca7
4ff5f1a
6ee6843
b917810
8c63fc4
f95227c
1b4fb27
b4623a7
4f5bd96
92e1488
1a9e40d
82b7f58
9217b3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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, | ||
|
@@ -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 | ||
|
@@ -2391,6 +2393,8 @@ def each( | |
self: T, | ||
callback: Callable[[CQObject], Shape], | ||
useLocalCoordinates: bool = False, | ||
combine: CombineMode = True, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already discussed my opinion on adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Do you think it would make sense to set it to False? I'm kind of hesitating about it. What do you think @jmwright? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting it to False would make things transparent but we would loose consistency with other methods. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I added a note in changes.md |
||
clean: bool = True, | ||
) -> T: | ||
""" | ||
Runs the provided function on each value in the stack, and collects the return values into | ||
|
@@ -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 boolean or string combine: True to combine the resulting solid with parent solids if found, "cut" to remove the resulting solid from the parent solids if found. | ||
: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. | ||
|
@@ -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 | ||
|
@@ -2454,6 +2462,9 @@ def eachpoint( | |
|
||
:param useLocalCoordinates: should points be in local or global coordinates | ||
:type useLocalCoordinates: boolean | ||
:param boolean or string combine: True to combine the resulting solid with parent solids if found, "cut" to remove the resulting solid from the parent solids if found. | ||
: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 | ||
|
@@ -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, | ||
|
@@ -2948,7 +2959,7 @@ def twistExtrude( | |
self: T, | ||
distance: float, | ||
angleDegrees: float, | ||
combine: bool = True, | ||
combine: CombineMode = True, | ||
clean: bool = True, | ||
) -> T: | ||
""" | ||
|
@@ -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 boolean or string combine: True to combine the resulting solid with parent solids if found, "cut" to remove the resulting solid from the parent solids if found. | ||
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape | ||
:return: a CQ object with the resulting solid selected. | ||
""" | ||
|
@@ -2990,18 +3001,12 @@ 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, | ||
|
@@ -3015,7 +3020,7 @@ def extrude( | |
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) | ||
:param boolean or string combine: True to combine the resulting solid with parent solids if found, "cut" to remove the resulting solid from the parent solids if found. | ||
: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 | ||
|
@@ -3031,14 +3036,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) | ||
|
@@ -3056,20 +3066,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: | ||
""" | ||
|
@@ -3082,7 +3086,7 @@ def revolve( | |
: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 | ||
:type combine: boolean or string, defines how the result of the operation is combined with the base solid | ||
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape | ||
:return: a CQ object with the resulting solid selected. | ||
|
||
|
@@ -3126,13 +3130,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, | ||
|
@@ -3141,7 +3140,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, | ||
|
@@ -3152,7 +3151,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 boolean or string combine: True to combine the resulting solid with parent solids if found, "cut" to remove the resulting solid from the parent solids if found. | ||
: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 | ||
|
@@ -3181,18 +3180,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]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that that is the optimal way. We might want to rewrite the failing test |
||
|
||
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 | ||
|
@@ -3205,7 +3234,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: | ||
|
@@ -3215,9 +3243,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) | ||
|
@@ -3490,11 +3517,11 @@ 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 | ||
:return: a Workplane object containing the created loft | ||
""" | ||
|
||
if self.ctx.pendingWires: | ||
|
@@ -3507,14 +3534,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]: | ||
""" | ||
|
@@ -4115,13 +4137,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, | ||
|
@@ -4137,7 +4160,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 boolean or string combine: True to combine the resulting solid with parent solids if found, "cut" to remove the resulting solid from the parent solids if found. | ||
:param clean: call :py:meth:`clean` afterwards to have a clean shape | ||
:param font: font name | ||
:param fontPath: path to font file | ||
|
@@ -4183,14 +4206,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: | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added also modes used in Sketch. Note that it is better to use Literal for typing.