Skip to content
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

Merged
merged 32 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5fb8823
added cut for revolve and loft
Jojain Jan 7, 2022
0dab2be
added combine cut for all 3D ops
Jojain Jan 7, 2022
1460545
mypy correct
Jojain Jan 7, 2022
0329d21
Update cq.py
Jojain Jan 7, 2022
973dfda
added tests
Jojain Jan 10, 2022
4bf22a9
Update test_cadquery.py
Jojain Jan 10, 2022
f9b5b07
added test
Jojain Jan 10, 2022
db070a9
Update test_cadquery.py
Jojain Jan 10, 2022
699ffa1
Update test_cadquery.py
Jojain Jan 10, 2022
b8c0063
Trigger CI
Jojain Jan 10, 2022
0cf408a
Update cadquery/cq.py
Jojain Jan 11, 2022
daa994b
refactor _combineWithBase
Jojain Jan 12, 2022
3631e07
mypy correction
Jojain Jan 12, 2022
502dc3a
updated text and added deprecation warning
Jojain Jan 12, 2022
59df1c0
Update cq.py
Jojain Jan 12, 2022
5a7c254
resolved conflicts
Jojain Jan 12, 2022
42e144b
Update utils.py
Jojain Jan 12, 2022
2c3a2fd
Update cq.py
Jojain Jan 14, 2022
e2108d2
Update utils.py
Jojain Jan 14, 2022
ee96ca7
black fix
Jojain Jan 14, 2022
4ff5f1a
Merge branch 'master' into cut_kw_arg
adam-urbanczyk Jan 14, 2022
6ee6843
black on test_cadquery.py
Jojain Jan 14, 2022
b917810
mypy fixes
Jojain Jan 15, 2022
8c63fc4
remove unused import
Jojain Jan 15, 2022
f95227c
Changed annotations and simplified the code
adam-urbanczyk Jan 19, 2022
1b4fb27
Different clean handling
adam-urbanczyk Jan 19, 2022
b4623a7
Typo fix
adam-urbanczyk Jan 19, 2022
4f5bd96
Update docstring
adam-urbanczyk Jan 22, 2022
92e1488
Fix extrude in subtractive mode
adam-urbanczyk Jan 22, 2022
1a9e40d
Fix Shape.__eq__
adam-urbanczyk Jan 22, 2022
82b7f58
Update changes.md
adam-urbanczyk Jan 23, 2022
9217b3f
docstring update
Jojain Jan 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 83 additions & 65 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
Copy link
Member

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.


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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already discussed my opinion on adding combine arg for this method so I won't talk about it again but in any case if we decide to add it we should probably write somewhere that the new default behaviour is to combine with the base which wasnt the case before. Same remark goes for eachpoint

Copy link
Member

@adam-urbanczyk adam-urbanczyk Jan 22, 2022

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
I don't think each and eachpoint are that used so a breaking change here would probably not be a big deal.

Copy link
Member

Choose a reason for hiding this comment

The 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
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 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.
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 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
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 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.
"""
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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:
"""
Expand All @@ -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.

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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])
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand All @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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]:
"""
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down
4 changes: 3 additions & 1 deletion cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,10 +966,12 @@ def moved(self: T, loc: Location) -> T:
return r

def __hash__(self) -> int:

return self.hashCode()

def __eq__(self, other) -> bool:
return self.isSame(other)

return self.isSame(other) if isinstance(other, Shape) else False

def _bool_op(
self,
Expand Down
Loading