Skip to content

Commit f19c35c

Browse files
Jojainadam-urbanczykmarcus7070
authored
until extrude/cutblind (#875)
* Move dprism to Mixin3D Co-authored-by: AU <[email protected]> Co-authored-by: Marcus Boyd <[email protected]>
1 parent 04e3dd9 commit f19c35c

File tree

9 files changed

+573
-102
lines changed

9 files changed

+573
-102
lines changed

cadquery/assembly.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,7 @@ def toPOD(self) -> ConstraintPOD:
168168

169169

170170
class Assembly(object):
171-
"""Nested assembly of Workplane and Shape objects defining their relative positions.
172-
"""
171+
"""Nested assembly of Workplane and Shape objects defining their relative positions."""
173172

174173
loc: Location
175174
name: str

cadquery/cq.py

Lines changed: 152 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,7 +2011,7 @@ def parametricSurface(
20112011
:param maxDeg: maximum spline degree (default: 3)
20122012
:param smoothing: optional parameters for the variational smoothing algorithm (default: (1,1,1))
20132013
:return: a Workplane object with the current point unchanged
2014-
2014+
20152015
This method might be unstable and may require tuning of the tol parameter.
20162016
20172017
"""
@@ -2978,7 +2978,7 @@ def twistExtrude(
29782978

29792979
def extrude(
29802980
self: T,
2981-
distance: float,
2981+
until: Union[float, Literal["next", "last"], Face],
29822982
combine: bool = True,
29832983
clean: bool = True,
29842984
both: bool = False,
@@ -2987,8 +2987,12 @@ def extrude(
29872987
"""
29882988
Use all un-extruded wires in the parent chain to create a prismatic solid.
29892989
2990-
:param distance: the distance to extrude, normal to the workplane plane
2991-
:type distance: float, negative means opposite the normal direction
2990+
:param until: the distance to extrude, normal to the workplane plane
2991+
:param until: The distance to extrude, normal to the workplane plane. When a float is
2992+
passed, the extrusion extends this far and a negative value is in the opposite direction
2993+
to the normal of the plane. The string "next" extrudes until the next face orthogonal to
2994+
the wire normal. "last" extrudes to the last face. If a object of type Face is passed then
2995+
the extrusion will extend until this face.
29922996
:param boolean combine: True to combine the resulting solid with parent solids if found.
29932997
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
29942998
:param boolean both: extrude in both directions symmetrically
@@ -3000,18 +3004,35 @@ def extrude(
30003004
The returned object is always a CQ object, and depends on whether combine is True, and
30013005
whether a context solid is already defined:
30023006
3003-
* if combine is False, the new value is pushed onto the stack.
3007+
* if combine is False, the new value is pushed onto the stack. Note that when extruding
3008+
until a specified face, combine can not be False
30043009
* if combine is true, the value is combined with the context solid if it exists,
30053010
and the resulting solid becomes the new context solid.
3006-
3007-
FutureEnhancement:
3008-
Support for non-prismatic extrusion ( IE, sweeping along a profile, not just
3009-
perpendicular to the plane extrude to surface. this is quite tricky since the surface
3010-
selected may not be planar
30113011
"""
3012-
r = self._extrude(
3013-
distance, both=both, taper=taper
3014-
) # returns a Solid (or a compound if there were multiple)
3012+
# Handle `until` multiple values
3013+
if isinstance(until, str) and until in ("next", "last") and combine:
3014+
if until == "next":
3015+
faceIndex = 0
3016+
elif until == "last":
3017+
faceIndex = -1
3018+
3019+
r = self._extrude(distance=None, both=both, taper=taper, upToFace=faceIndex)
3020+
3021+
elif isinstance(until, Face) and combine:
3022+
r = self._extrude(None, both=both, taper=taper, upToFace=until)
3023+
3024+
elif isinstance(until, (int, float)):
3025+
r = self._extrude(until, both=both, taper=taper, upToFace=None)
3026+
3027+
elif isinstance(until, (str, Face)) and combine is False:
3028+
raise ValueError(
3029+
"`combine` can't be set to False when extruding until a face"
3030+
)
3031+
3032+
else:
3033+
raise ValueError(
3034+
f"Do not know how to handle until argument of type {type(until)}"
3035+
)
30153036

30163037
if combine:
30173038
newS = self._combineWithBase(r)
@@ -3355,37 +3376,50 @@ def __and__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:
33553376

33563377
def cutBlind(
33573378
self: T,
3358-
distanceToCut: float,
3379+
until: Union[float, Literal["next", "last"], Face],
33593380
clean: bool = True,
33603381
taper: Optional[float] = None,
33613382
) -> T:
33623383
"""
33633384
Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
3385+
You must define either :distance: , :untilNextFace: or :untilLastFace:
33643386
33653387
Similar to extrude, except that a solid in the parent chain is required to remove material
33663388
from. cutBlind always removes material from a part.
33673389
3368-
:param distanceToCut: distance to extrude before cutting
3369-
:type distanceToCut: float, >0 means in the positive direction of the workplane normal,
3370-
<0 means in the negative direction
3390+
:param until: The distance to cut to, normal to the workplane plane. When a negative float
3391+
is passed the cut extends this far in the opposite direction to the normal of the plane
3392+
(i.e in the solid). The string "next" cuts until the next face orthogonal to the wire
3393+
normal. "last" cuts to the last face. If a object of type Face is passed then the cut
3394+
will extend until this face.
33713395
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
33723396
:param float taper: angle for optional tapered extrusion
33733397
:raises ValueError: if there is no solid to subtract from in the chain
33743398
:return: a CQ object with the resulting object selected
33753399
33763400
see :py:meth:`cutThruAll` to cut material from the entire part
3377-
3378-
Future Enhancements:
3379-
Cut Up to Surface
33803401
"""
3381-
# first, make the object
3382-
toCut = self._extrude(distanceToCut, taper=taper)
3402+
# Handling of `until` passed values
3403+
s: Union[Compound, Solid, Shape]
3404+
if isinstance(until, str) and until in ("next", "last"):
3405+
if until == "next":
3406+
faceIndex = 0
3407+
elif until == "last":
3408+
faceIndex = -1
33833409

3384-
# now find a solid in the chain
3385-
solidRef = self.findSolid()
3410+
s = self._extrude(None, taper=taper, upToFace=faceIndex, additive=False)
33863411

3387-
s = solidRef.cut(toCut)
3412+
elif isinstance(until, Face):
3413+
s = self._extrude(None, taper=taper, upToFace=until, additive=False)
33883414

3415+
elif isinstance(until, (int, float)):
3416+
toCut = self._extrude(until, taper=taper, upToFace=None, additive=False)
3417+
solidRef = self.findSolid()
3418+
s = solidRef.cut(toCut)
3419+
else:
3420+
raise ValueError(
3421+
f"Do not know how to handle until argument of type {type(until)}"
3422+
)
33893423
if clean:
33903424
s = s.clean()
33913425

@@ -3441,48 +3475,130 @@ def loft(
34413475
return self.newObject([r])
34423476

34433477
def _extrude(
3444-
self, distance: float, both: bool = False, taper: Optional[float] = None
3478+
self,
3479+
distance: Optional[float] = None,
3480+
both: bool = False,
3481+
taper: Optional[float] = None,
3482+
upToFace: Optional[Union[int, Face]] = None,
3483+
additive: bool = True,
34453484
) -> Compound:
34463485
"""
34473486
Make a prismatic solid from the existing set of pending wires.
34483487
34493488
:param distance: distance to extrude
3450-
:param boolean both: extrude in both directions symmetrically
3489+
:param boolean both: extrude in both directions symetrically
3490+
:param upToFace: if specified extrude up to the :upToFace: face, 0 for the next, -1 for the last
3491+
:param additive: specify if extruding or cutting, required param for uptoface algorithm
3492+
34513493
:return: OCCT solid(s), suitable for boolean operations.
34523494
34533495
This method is a utility method, primarily for plugin and internal use.
34543496
It is the basis for cutBlind, extrude, cutThruAll, and all similar methods.
34553497
"""
34563498

3499+
def getFacesList(eDir, direction, both=False):
3500+
"""
3501+
Utility function to make the code further below more clean and tidy
3502+
Performs some test and raise appropriate error when no Faces are found for extrusion
3503+
"""
3504+
facesList = self.findSolid().facesIntersectedByLine(
3505+
ws[0].Center(), eDir, direction=direction
3506+
)
3507+
if len(facesList) == 0 and both:
3508+
raise ValueError(
3509+
"Couldn't find a face to extrude/cut to for at least one of the two required directions of extrusion/cut."
3510+
)
3511+
3512+
if len(facesList) == 0:
3513+
# if we don't find faces in the workplane normal direction we try the other
3514+
# direction (as the user might have created a workplane with wrong orientation)
3515+
facesList = self.findSolid().facesIntersectedByLine(
3516+
ws[0].Center(), eDir.multiply(-1.0), direction=direction
3517+
)
3518+
if len(facesList) == 0:
3519+
raise ValueError(
3520+
"Couldn't find a face to extrude/cut to. Check your workplane orientation."
3521+
)
3522+
return facesList
3523+
34573524
# group wires together into faces based on which ones are inside the others
34583525
# result is a list of lists
34593526

34603527
wireSets = sortWiresByBuildOrder(self.ctx.popPendingWires())
34613528

34623529
# compute extrusion vector and extrude
3463-
eDir = self.plane.zDir.multiply(distance)
3530+
if upToFace is not None:
3531+
eDir = self.plane.zDir
3532+
elif distance is not None:
3533+
eDir = self.plane.zDir.multiply(distance)
3534+
3535+
if additive:
3536+
direction = "AlongAxis"
3537+
else:
3538+
direction = "Opposite"
34643539

34653540
# one would think that fusing faces into a compound and then extruding would work,
3466-
# but it doesn't-- the resulting compound appears to look right, ( right number of faces, etc)
3541+
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
34673542
# but then cutting it from the main solid fails with BRep_NotDone.
34683543
# the work around is to extrude each and then join the resulting solids, which seems to work
34693544

34703545
# underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are
34713546
# multiple sets
3547+
thisObj: Union[Solid, Compound]
34723548

34733549
toFuse = []
3550+
taper = 0.0 if taper is None else taper
3551+
baseSolid = None
34743552

3475-
if taper:
3476-
for ws in wireSets:
3477-
thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper)
3553+
for ws in wireSets:
3554+
if upToFace is not None:
3555+
baseSolid = self.findSolid() if baseSolid is None else thisObj
3556+
if isinstance(upToFace, int):
3557+
facesList = getFacesList(eDir, direction, both=both)
3558+
if (
3559+
baseSolid.isInside(ws[0].Center())
3560+
and additive
3561+
and upToFace == 0
3562+
):
3563+
upToFace = 1 # extrude until next face outside the solid
3564+
3565+
limitFace = facesList[upToFace]
3566+
else:
3567+
limitFace = upToFace
3568+
3569+
thisObj = Solid.dprism(
3570+
baseSolid,
3571+
Face.makeFromWires(ws[0]),
3572+
ws,
3573+
taper=taper,
3574+
upToFace=limitFace,
3575+
additive=additive,
3576+
)
3577+
3578+
if both:
3579+
facesList2 = getFacesList(eDir.multiply(-1.0), direction, both=both)
3580+
limitFace2 = facesList2[upToFace]
3581+
thisObj2 = Solid.dprism(
3582+
self.findSolid(),
3583+
Face.makeFromWires(ws[0]),
3584+
ws,
3585+
taper=taper,
3586+
upToFace=limitFace2,
3587+
additive=additive,
3588+
)
3589+
thisObj = Compound.makeCompound([thisObj, thisObj2])
3590+
toFuse = [thisObj]
3591+
elif taper != 0.0:
3592+
thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper=taper)
34783593
toFuse.append(thisObj)
3479-
else:
3480-
for ws in wireSets:
3481-
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
3594+
else:
3595+
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir, taper=taper)
34823596
toFuse.append(thisObj)
34833597

34843598
if both:
3485-
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.0))
3599+
thisObj = Solid.extrudeLinear(
3600+
ws[0], ws[1:], eDir.multiply(-1.0), taper=taper
3601+
)
34863602
toFuse.append(thisObj)
34873603

34883604
return Compound.makeCompound(toFuse)

cadquery/cqgi.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,9 @@ class NoOutputError(Exception):
382382

383383
class ScriptExecutionError(Exception):
384384
"""
385-
Represents a script syntax error.
386-
Useful for helping clients pinpoint issues with the script
387-
interactively
385+
Represents a script syntax error.
386+
Useful for helping clients pinpoint issues with the script
387+
interactively
388388
"""
389389

390390
def __init__(self, line=None, message=None):
@@ -448,8 +448,8 @@ def __init__(self, cq_model):
448448

449449
def visit_Call(self, node):
450450
"""
451-
Called when we see a function call. Is it describe_parameter?
452-
"""
451+
Called when we see a function call. Is it describe_parameter?
452+
"""
453453
try:
454454
if node.func.id == "describe_parameter":
455455
# looks like we have a call to our function.

cadquery/occ_impl/geom.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@
2727
class Vector(object):
2828
"""Create a 3-dimensional vector
2929
30-
:param args: a 3D vector, with x-y-z parts.
31-
32-
you can either provide:
33-
* nothing (in which case the null vector is return)
34-
* a gp_Vec
35-
* a vector ( in which case it is copied )
36-
* a 3-tuple
37-
* a 2-tuple (z assumed to be 0)
38-
* three float values: x, y, and z
39-
* two float values: x,y
30+
:param args: a 3D vector, with x-y-z parts.
31+
32+
you can either provide:
33+
* nothing (in which case the null vector is return)
34+
* a gp_Vec
35+
* a vector ( in which case it is copied )
36+
* a 3-tuple
37+
* a 2-tuple (z assumed to be 0)
38+
* three float values: x, y, and z
39+
* two float values: x,y
4040
"""
4141

4242
_wrapped: gp_Vec
@@ -347,8 +347,7 @@ def multiply(self, other):
347347
return Matrix(self.wrapped.Multiplied(other.wrapped))
348348

349349
def transposed_list(self) -> Sequence[float]:
350-
"""Needed by the cqparts gltf exporter
351-
"""
350+
"""Needed by the cqparts gltf exporter"""
352351

353352
trsf = self.wrapped
354353
data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [

cadquery/occ_impl/importers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class UNITS:
3535
def importShape(importType, fileName, *args, **kwargs):
3636
"""
3737
Imports a file based on the type (STEP, STL, etc)
38-
38+
3939
:param importType: The type of file that we're importing
4040
:param fileName: THe name of the file that we're importing
4141
"""
@@ -53,7 +53,7 @@ def importShape(importType, fileName, *args, **kwargs):
5353
def importStep(fileName):
5454
"""
5555
Accepts a file name and loads the STEP file into a cadquery Workplane
56-
56+
5757
:param fileName: The path and name of the STEP file to be imported
5858
"""
5959

@@ -217,7 +217,7 @@ def _dxf_convert(elements, tol):
217217
def importDXF(filename, tol=1e-6, exclude=[]):
218218
"""
219219
Loads a DXF file into a cadquery Workplane.
220-
220+
221221
:param fileName: The path and name of the DXF file to be imported
222222
:param tol: The tolerance used for merging edges into wires (default: 1e-6)
223223
:param exclude: a list of layer names not to import (default: [])

0 commit comments

Comments
 (0)