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

until extrude/cutblind #875

Merged
merged 29 commits into from
Sep 26, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
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
3 changes: 1 addition & 2 deletions cadquery/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,7 @@ def toPOD(self) -> ConstraintPOD:


class Assembly(object):
"""Nested assembly of Workplane and Shape objects defining their relative positions.
"""
"""Nested assembly of Workplane and Shape objects defining their relative positions."""

loc: Location
name: str
Expand Down
188 changes: 155 additions & 33 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -2011,7 +2011,7 @@ def parametricSurface(
:param maxDeg: maximum spline degree (default: 3)
:param smoothing: optional parameters for the variational smoothing algorithm (default: (1,1,1))
:return: a Workplane object with the current point unchanged

This method might be unstable and may require tuning of the tol parameter.

"""
Expand Down Expand Up @@ -2978,7 +2978,7 @@ def twistExtrude(

def extrude(
self: T,
distance: float,
until: Union[float, Literal["next", "last"], Face],
combine: bool = True,
clean: bool = True,
both: bool = False,
Expand All @@ -2987,8 +2987,10 @@ def extrude(
"""
Use all un-extruded wires in the parent chain to create a prismatic solid.

:param distance: the distance to extrude, normal to the workplane plane
:type distance: float, negative means opposite the normal direction
:param until: the distance to extrude, normal to the workplane plane
:type until: float, negative means opposite the normal direction
:type until: Literal, 'next' set the extrude until the next face orthogonal to the wire normal, 'last' sets the extrusion until the last face
:type until: Face, extrude until the provided face
:param boolean combine: True to combine the resulting solid with 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
Expand All @@ -3000,18 +3002,37 @@ def extrude(
The returned object is always a CQ object, and depends on whether combine is True, and
whether a context solid is already defined:

* if combine is False, the new value is pushed onto the stack.
* if combine is False, the new value is pushed onto the stack. Note that when extruding until a specified face, combine can be False
* if combine is true, the value is combined with the context solid if it exists,
and the resulting solid becomes the new context solid.

FutureEnhancement:
Support for non-prismatic extrusion ( IE, sweeping along a profile, not just
perpendicular to the plane extrude to surface. this is quite tricky since the surface
selected may not be planar




"""
r = self._extrude(
distance, both=both, taper=taper
) # returns a Solid (or a compound if there were multiple)
# Handle `until` multiple values
if isinstance(until, str) and until in ("next", "last") and combine:
if until == "next":
faceIndex = 0
elif until == "last":
faceIndex = -1

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

elif isinstance(until, Face):
r = self._extrude(None, both=both, taper=taper, upToFace=until)
elif isinstance(until, (int, float)):
r = self._extrude(until, both=both, taper=taper, upToFace=None)

elif isinstance(until, str) and combine is False:
raise ValueError(
"`combine` can't be set to False when extruding until a face"
)
else:
raise ValueError(
"Valid option for until face extrusion are 'next' and 'last'"
Jojain marked this conversation as resolved.
Show resolved Hide resolved
)

if combine:
newS = self._combineWithBase(r)
Expand Down Expand Up @@ -3355,37 +3376,53 @@ def __and__(self: T, toUnion: Union["Workplane", Solid, Compound]) -> T:

def cutBlind(
self: T,
distanceToCut: float,
until: Union[float, Literal["next", "last"], Face],
clean: bool = True,
taper: Optional[float] = None,
) -> T:
"""
Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
You must define either :distance: , :untilNextFace: or :untilLastFace:

Similar to extrude, except that a solid in the parent chain is required to remove material
from. cutBlind always removes material from a part.

:param distanceToCut: distance to extrude before cutting
:type distanceToCut: float, >0 means in the positive direction of the workplane normal,
:param until: distance to extrude before cutting
:type until: float, >0 means in the positive direction of the workplane normal,
<0 means in the negative direction
:type until: Literal, 'next' set the cut until the next face orthogonal to the wire normal, 'last' sets the extrusion until the last face
:type until: Face, cut until the provided face
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
:param float taper: angle for optional tapered extrusion
:raises ValueError: if there is no solid to subtract from in the chain
:return: a CQ object with the resulting object selected

see :py:meth:`cutThruAll` to cut material from the entire part

Future Enhancements:
Cut Up to Surface


"""
# first, make the object
toCut = self._extrude(distanceToCut, taper=taper)
# Handling of `until` passed values
s: Union[Compound, Solid, Shape]
if isinstance(until, str) and until in ("next", "last"):
if until == "next":
faceIndex = 0
elif until == "last":
faceIndex = -1

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

s = solidRef.cut(toCut)
elif isinstance(until, Face):
s = self._extrude(None, taper=taper, upToFace=until, additive=False)

elif isinstance(until, (int, float)):
toCut = self._extrude(until, taper=taper, upToFace=None, additive=False)
solidRef = self.findSolid()
s = solidRef.cut(toCut)
else:
raise ValueError(
"Valid options for until face extrusion are 'next' and 'last'"
Jojain marked this conversation as resolved.
Show resolved Hide resolved
)
if clean:
s = s.clean()

Expand Down Expand Up @@ -3441,48 +3478,133 @@ def loft(
return self.newObject([r])

def _extrude(
self, distance: float, both: bool = False, taper: Optional[float] = None
self,
distance: Optional[float] = None,
both: bool = False,
taper: Optional[float] = None,
upToFace: Optional[Union[int, Face]] = None,
additive: bool = True,
) -> Compound:
"""
Make a prismatic solid from the existing set of pending wires.

:param distance: distance to extrude
:param boolean both: extrude in both directions symmetrically
:param boolean both: extrude in both directions symetrically
:param upToFace: if specified extrude up to the :upToFace: face, 0 for the next, -1 for the last
:param additive: specify if extruding or cutting, required param for uptoface algorithm

:return: OCCT solid(s), suitable for boolean operations.

This method is a utility method, primarily for plugin and internal use.
It is the basis for cutBlind, extrude, cutThruAll, and all similar methods.
"""

def getFacesList(eDir, additive, both=False):
"""
Utility function to make the code further below more clean and tidy
Performs some test and raise appropriate error when no Faces are found for extrusion
"""
if additive:
direction = "AlongAxis"
else:
direction = "Opposite"

facesList = self.findSolid().facesIntersectedByLine(
ws[0].Center(), eDir, direction=direction
)
if len(facesList) == 0 and both:
raise ValueError(
adam-urbanczyk marked this conversation as resolved.
Show resolved Hide resolved
"Couldn't find a face to extrude/cut to for at least one of the two required directions of extrusion/cut."
)

if len(facesList) == 0:
# if we don't find faces in the workplane normal direction we try the other direction (as the user might have created a workplane with wrong orientation)
facesList = self.findSolid().facesIntersectedByLine(
ws[0].Center(), eDir.multiply(-1.0), direction=direction
)
if len(facesList) == 0:
raise ValueError(
"Couldn't find a face to extrude/cut to. Check your workplane orientation."
)
return facesList

# group wires together into faces based on which ones are inside the others
# result is a list of lists

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

# compute extrusion vector and extrude
eDir = self.plane.zDir.multiply(distance)
if upToFace is not None:
eDir = self.plane.zDir
elif distance is not None:
eDir = self.plane.zDir.multiply(distance)

if additive:
direction = "AlongAxis"
else:
direction = "Opposite"
marcus7070 marked this conversation as resolved.
Show resolved Hide resolved

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

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

toFuse = []
taper = 0.0 if taper is None else taper
baseSolid = None
marcus7070 marked this conversation as resolved.
Show resolved Hide resolved

if taper:
for ws in wireSets:
thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper)
for ws in wireSets:
if upToFace is not None:
baseSolid = self.findSolid() if baseSolid is None else thisObj
marcus7070 marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(upToFace, int):
facesList = getFacesList(eDir, additive, both=both)
limitFace = facesList[upToFace]
if (
baseSolid.isInside(ws[0].Center())
and additive
and upToFace == 0
):
upToFace = 1 # extrude until next face outside the solid
Copy link
Member

Choose a reason for hiding this comment

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

I may be misunderstanding your intention here, but I think you meant:

Suggested change
limitFace = facesList[upToFace]
if (
baseSolid.isInside(ws[0].Center())
and additive
and upToFace == 0
):
upToFace = 1 # extrude until next face outside the solid
if (
baseSolid.isInside(ws[0].Center())
and additive
and upToFace == 0
):
upToFace = 1 # extrude until next face outside the solid
limitFace = facesList[upToFace]

I would suggest a test is added for this as well.

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 think you misunderstood, this part of the code does :

  • A check if upToface is of type int or Face , if it's type int it checks that if an extrusion is required and the extrusion until the next face is required, if the wire is insinde the solid then we want to extrude until the next face Outside the solid. Hence the upToFace = 1
  • If one of these conditions isn't met then we are fine with upToFace = 0
  • And finally if ``upToFace isn't of type int then it's of type `Face` and we pass the face to extrude to directly

There is indeed no check if upToFace is actually of type Union[int, Face]. Would you want to add a type check at the beginning and raise an error if that is not the case ? (And add a test associated with it)

I don't know if it's relevant since the allowed type are given in the method signature and it's a private method. A type check is already performed on the public method extrude

Copy link
Member

Choose a reason for hiding this comment

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

I didn't mean types.

if the wire is insinde the solid then we want to extrude until the next face Outside the solid. Hence the upToFace = 1

Then you probably want to set upToFace = 1 before you do limitFace = facesList[upToFace]?

else:
limitFace = upToFace

thisObj = Solid.dprism(
baseSolid,
Face.makeFromWires(ws[0]),
ws,
taper=taper,
upToFace=limitFace,
additive=additive,
)

if both:
facesList2 = getFacesList(eDir.multiply(-1.0), additive, both=both)
limitFace2 = facesList2[upToFace]
thisObj2 = Solid.dprism(
self.findSolid(),
Face.makeFromWires(ws[0]),
ws,
taper=taper,
upToFace=limitFace2,
additive=additive,
)
thisObj = Compound.makeCompound([thisObj, thisObj2])
toFuse = [thisObj]
elif taper != 0.0:
thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper=taper)
toFuse.append(thisObj)
else:
for ws in wireSets:
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
else:
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir, taper=taper)
adam-urbanczyk marked this conversation as resolved.
Show resolved Hide resolved
toFuse.append(thisObj)

if both:
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.0))
thisObj = Solid.extrudeLinear(
ws[0], ws[1:], eDir.multiply(-1.0), taper=taper
)
toFuse.append(thisObj)

return Compound.makeCompound(toFuse)
Expand Down
10 changes: 5 additions & 5 deletions cadquery/cqgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,9 @@ class NoOutputError(Exception):

class ScriptExecutionError(Exception):
"""
Represents a script syntax error.
Useful for helping clients pinpoint issues with the script
interactively
Represents a script syntax error.
Useful for helping clients pinpoint issues with the script
interactively
"""

def __init__(self, line=None, message=None):
Expand Down Expand Up @@ -448,8 +448,8 @@ def __init__(self, cq_model):

def visit_Call(self, node):
"""
Called when we see a function call. Is it describe_parameter?
"""
Called when we see a function call. Is it describe_parameter?
"""
try:
if node.func.id == "describe_parameter":
# looks like we have a call to our function.
Expand Down
23 changes: 11 additions & 12 deletions cadquery/occ_impl/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@
class Vector(object):
"""Create a 3-dimensional vector

:param args: a 3D vector, with x-y-z parts.

you can either provide:
* nothing (in which case the null vector is return)
* a gp_Vec
* a vector ( in which case it is copied )
* a 3-tuple
* a 2-tuple (z assumed to be 0)
* three float values: x, y, and z
* two float values: x,y
:param args: a 3D vector, with x-y-z parts.

you can either provide:
* nothing (in which case the null vector is return)
* a gp_Vec
* a vector ( in which case it is copied )
* a 3-tuple
* a 2-tuple (z assumed to be 0)
* three float values: x, y, and z
* two float values: x,y
"""

_wrapped: gp_Vec
Expand Down Expand Up @@ -334,8 +334,7 @@ def multiply(self, other):
return Matrix(self.wrapped.Multiplied(other.wrapped))

def transposed_list(self) -> Sequence[float]:
"""Needed by the cqparts gltf exporter
"""
"""Needed by the cqparts gltf exporter"""

trsf = self.wrapped
data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [
Expand Down
6 changes: 3 additions & 3 deletions cadquery/occ_impl/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class UNITS:
def importShape(importType, fileName, *args, **kwargs):
"""
Imports a file based on the type (STEP, STL, etc)

:param importType: The type of file that we're importing
:param fileName: THe name of the file that we're importing
"""
Expand All @@ -53,7 +53,7 @@ def importShape(importType, fileName, *args, **kwargs):
def importStep(fileName):
"""
Accepts a file name and loads the STEP file into a cadquery Workplane

:param fileName: The path and name of the STEP file to be imported
"""

Expand Down Expand Up @@ -217,7 +217,7 @@ def _dxf_convert(elements, tol):
def importDXF(filename, tol=1e-6, exclude=[]):
"""
Loads a DXF file into a cadquery Workplane.

:param fileName: The path and name of the DXF file to be imported
:param tol: The tolerance used for merging edges into wires (default: 1e-6)
:param exclude: a list of layer names not to import (default: [])
Expand Down
Loading