until extrude/cutblind (#875)
* Move dprism to Mixin3D Co-authored-by: AU <adam-urbanczyk@users.noreply.github.com> Co-authored-by: Marcus Boyd <mwb@geosol.com.au>
This commit is contained in:
@ -168,8 +168,7 @@ class Constraint(object):
|
|||||||
|
|
||||||
|
|
||||||
class Assembly(object):
|
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
|
loc: Location
|
||||||
name: str
|
name: str
|
||||||
|
180
cadquery/cq.py
180
cadquery/cq.py
@ -2978,7 +2978,7 @@ class Workplane(object):
|
|||||||
|
|
||||||
def extrude(
|
def extrude(
|
||||||
self: T,
|
self: T,
|
||||||
distance: float,
|
until: Union[float, Literal["next", "last"], Face],
|
||||||
combine: bool = True,
|
combine: bool = True,
|
||||||
clean: bool = True,
|
clean: bool = True,
|
||||||
both: bool = False,
|
both: bool = False,
|
||||||
@ -2987,8 +2987,12 @@ class Workplane(object):
|
|||||||
"""
|
"""
|
||||||
Use all un-extruded wires in the parent chain to create a prismatic solid.
|
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
|
:param until: 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. When a float is
|
||||||
|
passed, the extrusion extends this far and a negative value is in the opposite direction
|
||||||
|
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.
|
: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 clean: call :py:meth:`clean` afterwards to have a clean shape
|
||||||
:param boolean both: extrude in both directions symmetrically
|
:param boolean both: extrude in both directions symmetrically
|
||||||
@ -3000,18 +3004,35 @@ class Workplane(object):
|
|||||||
The returned object is always a CQ object, and depends on whether combine is True, and
|
The returned object is always a CQ object, and depends on whether combine is True, and
|
||||||
whether a context solid is already defined:
|
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 not be False
|
||||||
* if combine is true, the value is combined with the context solid if it exists,
|
* if combine is true, the value is combined with the context solid if it exists,
|
||||||
and the resulting solid becomes the new context solid.
|
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(
|
# Handle `until` multiple values
|
||||||
distance, both=both, taper=taper
|
if isinstance(until, str) and until in ("next", "last") and combine:
|
||||||
) # returns a Solid (or a compound if there were multiple)
|
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) and combine:
|
||||||
|
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, Face)) and combine is False:
|
||||||
|
raise ValueError(
|
||||||
|
"`combine` can't be set to False when extruding until a face"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Do not know how to handle until argument of type {type(until)}"
|
||||||
|
)
|
||||||
|
|
||||||
if combine:
|
if combine:
|
||||||
newS = self._combineWithBase(r)
|
newS = self._combineWithBase(r)
|
||||||
@ -3355,37 +3376,50 @@ class Workplane(object):
|
|||||||
|
|
||||||
def cutBlind(
|
def cutBlind(
|
||||||
self: T,
|
self: T,
|
||||||
distanceToCut: float,
|
until: Union[float, Literal["next", "last"], Face],
|
||||||
clean: bool = True,
|
clean: bool = True,
|
||||||
taper: Optional[float] = None,
|
taper: Optional[float] = None,
|
||||||
) -> T:
|
) -> T:
|
||||||
"""
|
"""
|
||||||
Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
|
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
|
Similar to extrude, except that a solid in the parent chain is required to remove material
|
||||||
from. cutBlind always removes material from a part.
|
from. cutBlind always removes material from a part.
|
||||||
|
|
||||||
:param distanceToCut: distance to extrude before cutting
|
:param until: The distance to cut to, normal to the workplane plane. When a negative float
|
||||||
:type distanceToCut: float, >0 means in the positive direction of the workplane normal,
|
is passed the cut extends this far in the opposite direction to the normal of the plane
|
||||||
<0 means in the negative direction
|
(i.e in the solid). The string "next" cuts until the next face orthogonal to the wire
|
||||||
|
normal. "last" cuts to the last face. If a object of type Face is passed then the cut
|
||||||
|
will extend until this face.
|
||||||
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
|
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
|
||||||
:param float taper: angle for optional tapered extrusion
|
:param float taper: angle for optional tapered extrusion
|
||||||
:raises ValueError: if there is no solid to subtract from in the chain
|
:raises ValueError: if there is no solid to subtract from in the chain
|
||||||
:return: a CQ object with the resulting object selected
|
:return: a CQ object with the resulting object selected
|
||||||
|
|
||||||
see :py:meth:`cutThruAll` to cut material from the entire part
|
see :py:meth:`cutThruAll` to cut material from the entire part
|
||||||
|
|
||||||
Future Enhancements:
|
|
||||||
Cut Up to Surface
|
|
||||||
"""
|
"""
|
||||||
# first, make the object
|
# Handling of `until` passed values
|
||||||
toCut = self._extrude(distanceToCut, taper=taper)
|
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
|
s = self._extrude(None, taper=taper, upToFace=faceIndex, additive=False)
|
||||||
|
|
||||||
|
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()
|
solidRef = self.findSolid()
|
||||||
|
|
||||||
s = solidRef.cut(toCut)
|
s = solidRef.cut(toCut)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Do not know how to handle until argument of type {type(until)}"
|
||||||
|
)
|
||||||
if clean:
|
if clean:
|
||||||
s = s.clean()
|
s = s.clean()
|
||||||
|
|
||||||
@ -3441,48 +3475,130 @@ class Workplane(object):
|
|||||||
return self.newObject([r])
|
return self.newObject([r])
|
||||||
|
|
||||||
def _extrude(
|
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:
|
) -> Compound:
|
||||||
"""
|
"""
|
||||||
Make a prismatic solid from the existing set of pending wires.
|
Make a prismatic solid from the existing set of pending wires.
|
||||||
|
|
||||||
:param distance: distance to extrude
|
: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.
|
:return: OCCT solid(s), suitable for boolean operations.
|
||||||
|
|
||||||
This method is a utility method, primarily for plugin and internal use.
|
This method is a utility method, primarily for plugin and internal use.
|
||||||
It is the basis for cutBlind, extrude, cutThruAll, and all similar methods.
|
It is the basis for cutBlind, extrude, cutThruAll, and all similar methods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def getFacesList(eDir, direction, 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
|
||||||
|
"""
|
||||||
|
facesList = self.findSolid().facesIntersectedByLine(
|
||||||
|
ws[0].Center(), eDir, direction=direction
|
||||||
|
)
|
||||||
|
if len(facesList) == 0 and both:
|
||||||
|
raise ValueError(
|
||||||
|
"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
|
# group wires together into faces based on which ones are inside the others
|
||||||
# result is a list of lists
|
# result is a list of lists
|
||||||
|
|
||||||
wireSets = sortWiresByBuildOrder(self.ctx.popPendingWires())
|
wireSets = sortWiresByBuildOrder(self.ctx.popPendingWires())
|
||||||
|
|
||||||
# compute extrusion vector and extrude
|
# compute extrusion vector and extrude
|
||||||
|
if upToFace is not None:
|
||||||
|
eDir = self.plane.zDir
|
||||||
|
elif distance is not None:
|
||||||
eDir = self.plane.zDir.multiply(distance)
|
eDir = self.plane.zDir.multiply(distance)
|
||||||
|
|
||||||
|
if additive:
|
||||||
|
direction = "AlongAxis"
|
||||||
|
else:
|
||||||
|
direction = "Opposite"
|
||||||
|
|
||||||
# one would think that fusing faces into a compound and then extruding would work,
|
# 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.
|
# 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
|
# 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
|
# underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are
|
||||||
# multiple sets
|
# multiple sets
|
||||||
|
thisObj: Union[Solid, Compound]
|
||||||
|
|
||||||
toFuse = []
|
toFuse = []
|
||||||
|
taper = 0.0 if taper is None else taper
|
||||||
|
baseSolid = None
|
||||||
|
|
||||||
if taper:
|
|
||||||
for ws in wireSets:
|
for ws in wireSets:
|
||||||
thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper)
|
if upToFace is not None:
|
||||||
|
baseSolid = self.findSolid() if baseSolid is None else thisObj
|
||||||
|
if isinstance(upToFace, int):
|
||||||
|
facesList = getFacesList(eDir, direction, both=both)
|
||||||
|
if (
|
||||||
|
baseSolid.isInside(ws[0].Center())
|
||||||
|
and additive
|
||||||
|
and upToFace == 0
|
||||||
|
):
|
||||||
|
upToFace = 1 # extrude until next face outside the solid
|
||||||
|
|
||||||
|
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), direction, 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)
|
toFuse.append(thisObj)
|
||||||
else:
|
else:
|
||||||
for ws in wireSets:
|
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir, taper=taper)
|
||||||
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
|
|
||||||
toFuse.append(thisObj)
|
toFuse.append(thisObj)
|
||||||
|
|
||||||
if both:
|
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)
|
toFuse.append(thisObj)
|
||||||
|
|
||||||
return Compound.makeCompound(toFuse)
|
return Compound.makeCompound(toFuse)
|
||||||
|
@ -347,8 +347,7 @@ class Matrix:
|
|||||||
return Matrix(self.wrapped.Multiplied(other.wrapped))
|
return Matrix(self.wrapped.Multiplied(other.wrapped))
|
||||||
|
|
||||||
def transposed_list(self) -> Sequence[float]:
|
def transposed_list(self) -> Sequence[float]:
|
||||||
"""Needed by the cqparts gltf exporter
|
"""Needed by the cqparts gltf exporter"""
|
||||||
"""
|
|
||||||
|
|
||||||
trsf = self.wrapped
|
trsf = self.wrapped
|
||||||
data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [
|
data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [
|
||||||
|
@ -95,6 +95,7 @@ from OCP.BRepPrimAPI import (
|
|||||||
BRepPrimAPI_MakeRevol,
|
BRepPrimAPI_MakeRevol,
|
||||||
BRepPrimAPI_MakeSphere,
|
BRepPrimAPI_MakeSphere,
|
||||||
)
|
)
|
||||||
|
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
||||||
|
|
||||||
from OCP.TopExp import TopExp_Explorer # Toplogy explorer
|
from OCP.TopExp import TopExp_Explorer # Toplogy explorer
|
||||||
|
|
||||||
@ -118,6 +119,7 @@ from OCP.TopoDS import (
|
|||||||
|
|
||||||
from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse # geometry construction
|
from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse # geometry construction
|
||||||
from OCP.GCE2d import GCE2d_MakeSegment
|
from OCP.GCE2d import GCE2d_MakeSegment
|
||||||
|
from OCP.gce import gce_MakeLin, gce_MakeDir
|
||||||
from OCP.GeomAPI import (
|
from OCP.GeomAPI import (
|
||||||
GeomAPI_Interpolate,
|
GeomAPI_Interpolate,
|
||||||
GeomAPI_ProjectPointOnSurf,
|
GeomAPI_ProjectPointOnSurf,
|
||||||
@ -1034,6 +1036,85 @@ class Shape(object):
|
|||||||
|
|
||||||
return self._bool_op((self,), toIntersect, intersect_op)
|
return self._bool_op((self,), toIntersect, intersect_op)
|
||||||
|
|
||||||
|
def facesIntersectedByLine(
|
||||||
|
self,
|
||||||
|
point: VectorLike,
|
||||||
|
axis: VectorLike,
|
||||||
|
tol: float = 1e-4,
|
||||||
|
direction: Optional[Literal["AlongAxis", "Opposite"]] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Computes the intersections between the provided line and the faces of this Shape
|
||||||
|
|
||||||
|
:point: Base point for defining a line
|
||||||
|
:axis: Axis on which the line rest
|
||||||
|
:tol: Intersection tolerance
|
||||||
|
:direction: Valid values : "AlongAxis", "Opposite", if specified will ignore all faces that are not in the specified direction
|
||||||
|
including the face where the :point: lies if it is the case
|
||||||
|
|
||||||
|
:returns: A list of intersected faces sorted by distance from :point:
|
||||||
|
"""
|
||||||
|
|
||||||
|
oc_point = (
|
||||||
|
gp_Pnt(*point.toTuple()) if isinstance(point, Vector) else gp_Pnt(*point)
|
||||||
|
)
|
||||||
|
oc_axis = (
|
||||||
|
gp_Dir(Vector(axis).wrapped)
|
||||||
|
if not isinstance(axis, Vector)
|
||||||
|
else gp_Dir(axis.wrapped)
|
||||||
|
)
|
||||||
|
|
||||||
|
line = gce_MakeLin(oc_point, oc_axis).Value()
|
||||||
|
shape = self.wrapped
|
||||||
|
|
||||||
|
intersectMaker = BRepIntCurveSurface_Inter()
|
||||||
|
intersectMaker.Init(shape, line, tol)
|
||||||
|
|
||||||
|
faces_dist = [] # using a list instead of a dictionary to be able to sort it
|
||||||
|
while intersectMaker.More():
|
||||||
|
interPt = intersectMaker.Pnt()
|
||||||
|
interDirMk = gce_MakeDir(oc_point, interPt)
|
||||||
|
|
||||||
|
distance = oc_point.SquareDistance(interPt)
|
||||||
|
|
||||||
|
# interDir is not done when `oc_point` and `oc_axis` have the same coord
|
||||||
|
if interDirMk.IsDone():
|
||||||
|
interDir: Any = interDirMk.Value()
|
||||||
|
else:
|
||||||
|
interDir = None
|
||||||
|
|
||||||
|
if direction == "AlongAxis":
|
||||||
|
if (
|
||||||
|
interDir is not None
|
||||||
|
and not interDir.IsOpposite(oc_axis, tol)
|
||||||
|
and distance > tol
|
||||||
|
):
|
||||||
|
faces_dist.append((intersectMaker.Face(), distance))
|
||||||
|
|
||||||
|
elif direction == "Opposite":
|
||||||
|
if (
|
||||||
|
interDir is not None
|
||||||
|
and interDir.IsOpposite(oc_axis, tol)
|
||||||
|
and distance > tol
|
||||||
|
):
|
||||||
|
faces_dist.append((intersectMaker.Face(), distance))
|
||||||
|
|
||||||
|
elif direction is None:
|
||||||
|
faces_dist.append(
|
||||||
|
(intersectMaker.Face(), abs(distance))
|
||||||
|
) # will sort all intersected faces by distance whatever the direction is
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid direction specification.\nValid specification are 'AlongAxis' and 'Opposite'."
|
||||||
|
)
|
||||||
|
|
||||||
|
intersectMaker.Next()
|
||||||
|
|
||||||
|
faces_dist.sort(key=lambda x: x[1])
|
||||||
|
faces = [face[0] for face in faces_dist]
|
||||||
|
|
||||||
|
return [Face(face) for face in faces]
|
||||||
|
|
||||||
def split(self, *splitters: "Shape") -> "Shape":
|
def split(self, *splitters: "Shape") -> "Shape":
|
||||||
"""
|
"""
|
||||||
Split this shape with the positional arguments.
|
Split this shape with the positional arguments.
|
||||||
@ -2297,6 +2378,9 @@ class Shell(Shape):
|
|||||||
return cls(s)
|
return cls(s)
|
||||||
|
|
||||||
|
|
||||||
|
TS = TypeVar("TS", bound=ShapeProtocol)
|
||||||
|
|
||||||
|
|
||||||
class Mixin3D(object):
|
class Mixin3D(object):
|
||||||
def fillet(self: Any, radius: float, edgeList: Iterable[Edge]) -> Any:
|
def fillet(self: Any, radius: float, edgeList: Iterable[Edge]) -> Any:
|
||||||
"""
|
"""
|
||||||
@ -2426,6 +2510,51 @@ class Mixin3D(object):
|
|||||||
|
|
||||||
return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace()
|
return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace()
|
||||||
|
|
||||||
|
def dprism(
|
||||||
|
self: TS,
|
||||||
|
basis: Optional[Face],
|
||||||
|
profiles: List[Wire],
|
||||||
|
depth: Optional[float] = None,
|
||||||
|
taper: float = 0,
|
||||||
|
upToFace: Optional[Face] = None,
|
||||||
|
thruAll: bool = True,
|
||||||
|
additive: bool = True,
|
||||||
|
) -> TS:
|
||||||
|
"""
|
||||||
|
Make a prismatic feature (additive or subtractive)
|
||||||
|
|
||||||
|
:param basis: face to perfrom the operation on
|
||||||
|
:param profiles: list of profiles
|
||||||
|
:param depth: depth of the cut or extrusion
|
||||||
|
:param upToFace: a face to extrude until
|
||||||
|
:param thruAll: cut thruAll
|
||||||
|
:param additive: set the kind of operation (additive or subtractive)
|
||||||
|
:return: a Solid object
|
||||||
|
"""
|
||||||
|
|
||||||
|
sorted_profiles = sortWiresByBuildOrder(profiles)
|
||||||
|
shape: Union[TopoDS_Shape, TopoDS_Solid] = self.wrapped
|
||||||
|
for p in sorted_profiles:
|
||||||
|
face = Face.makeFromWires(p[0], p[1:])
|
||||||
|
feat = BRepFeat_MakeDPrism(
|
||||||
|
shape,
|
||||||
|
face.wrapped,
|
||||||
|
basis.wrapped if basis else TopoDS_Face(),
|
||||||
|
taper * DEG2RAD,
|
||||||
|
additive,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
if upToFace is not None:
|
||||||
|
feat.Perform(upToFace.wrapped)
|
||||||
|
elif thruAll or depth is None:
|
||||||
|
feat.PerformThruAll()
|
||||||
|
else:
|
||||||
|
feat.Perform(depth)
|
||||||
|
|
||||||
|
shape = feat.Shape()
|
||||||
|
|
||||||
|
return self.__class__(shape)
|
||||||
|
|
||||||
|
|
||||||
class Solid(Shape, Mixin3D):
|
class Solid(Shape, Mixin3D):
|
||||||
"""
|
"""
|
||||||
@ -3015,47 +3144,6 @@ class Solid(Shape, Mixin3D):
|
|||||||
|
|
||||||
return cls(builder.Shape())
|
return cls(builder.Shape())
|
||||||
|
|
||||||
def dprism(
|
|
||||||
self,
|
|
||||||
basis: Optional[Face],
|
|
||||||
profiles: List[Wire],
|
|
||||||
depth: Optional[float] = None,
|
|
||||||
taper: float = 0,
|
|
||||||
thruAll: bool = True,
|
|
||||||
additive: bool = True,
|
|
||||||
) -> "Solid":
|
|
||||||
"""
|
|
||||||
Make a prismatic feature (additive or subtractive)
|
|
||||||
|
|
||||||
:param basis: face to perform the operation on
|
|
||||||
:param profiles: list of profiles
|
|
||||||
:param depth: depth of the cut or extrusion
|
|
||||||
:param thruAll: cut thruAll
|
|
||||||
:return: a Solid object
|
|
||||||
"""
|
|
||||||
|
|
||||||
sorted_profiles = sortWiresByBuildOrder(profiles)
|
|
||||||
shape: Union[TopoDS_Shape, TopoDS_Solid] = self.wrapped
|
|
||||||
for p in sorted_profiles:
|
|
||||||
face = Face.makeFromWires(p[0], p[1:])
|
|
||||||
feat = BRepFeat_MakeDPrism(
|
|
||||||
shape,
|
|
||||||
face.wrapped,
|
|
||||||
basis.wrapped if basis else TopoDS_Face(),
|
|
||||||
taper * DEG2RAD,
|
|
||||||
additive,
|
|
||||||
False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if thruAll or depth is None:
|
|
||||||
feat.PerformThruAll()
|
|
||||||
else:
|
|
||||||
feat.Perform(depth)
|
|
||||||
|
|
||||||
shape = feat.Shape()
|
|
||||||
|
|
||||||
return Solid(shape)
|
|
||||||
|
|
||||||
|
|
||||||
class CompSolid(Shape, Mixin3D):
|
class CompSolid(Shape, Mixin3D):
|
||||||
"""
|
"""
|
||||||
|
@ -3136,6 +3136,275 @@ class TestCadQuery(BaseTest):
|
|||||||
|
|
||||||
self.saveModel(result)
|
self.saveModel(result)
|
||||||
|
|
||||||
|
def testExtrudeUntilFace(self):
|
||||||
|
"""
|
||||||
|
Test untilNextFace and untilLastFace options of Workplane.extrude()
|
||||||
|
"""
|
||||||
|
# Basic test to see if it yields same results as regular extrude for similar use case
|
||||||
|
# Also test if the extrusion worked well by counting the number of faces before and after extrusion
|
||||||
|
wp_ref = Workplane("XY").box(10, 10, 10).center(20, 0).box(10, 10, 10)
|
||||||
|
|
||||||
|
wp_ref_extrude = wp_ref.faces(">X[1]").workplane().rect(1, 1).extrude(10)
|
||||||
|
|
||||||
|
wp = Workplane("XY").box(10, 10, 10).center(20, 0).box(10, 10, 10)
|
||||||
|
nb_faces = wp.faces().size()
|
||||||
|
wp = wp_ref.faces(">X[1]").workplane().rect(1, 1).extrude("next")
|
||||||
|
|
||||||
|
self.assertAlmostEquals(wp_ref_extrude.val().Volume(), wp.val().Volume())
|
||||||
|
self.assertTrue(wp.faces().size() - nb_faces == 4)
|
||||||
|
|
||||||
|
# Test tapered option and both option
|
||||||
|
wp = (
|
||||||
|
wp_ref.faces(">X[1]")
|
||||||
|
.workplane(centerOption="CenterOfMass", offset=5)
|
||||||
|
.polygon(5, 3)
|
||||||
|
.extrude("next", both=True)
|
||||||
|
)
|
||||||
|
wp_both_volume = wp.val().Volume()
|
||||||
|
self.assertTrue(wp.val().isValid())
|
||||||
|
|
||||||
|
# taper
|
||||||
|
wp = (
|
||||||
|
wp_ref.faces(">X[1]")
|
||||||
|
.workplane(centerOption="CenterOfMass")
|
||||||
|
.polygon(5, 3)
|
||||||
|
.extrude("next", taper=5)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(wp.val().Volume() < wp_both_volume)
|
||||||
|
self.assertTrue(wp.val().isValid())
|
||||||
|
|
||||||
|
# Test extrude until with more that one wire in context
|
||||||
|
wp = (
|
||||||
|
wp_ref.faces(">X[1]")
|
||||||
|
.workplane(centerOption="CenterOfMass")
|
||||||
|
.pushPoints([(0, 0), (3, 3)])
|
||||||
|
.rect(2, 3)
|
||||||
|
.extrude("next")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(wp.solids().size() == 1)
|
||||||
|
self.assertTrue(wp.val().isValid())
|
||||||
|
|
||||||
|
# Test until last surf
|
||||||
|
wp_ref = wp_ref.workplane().move(10, 0).box(5, 5, 5)
|
||||||
|
wp = (
|
||||||
|
wp_ref.faces(">X[1]")
|
||||||
|
.workplane(centerOption="CenterOfMass")
|
||||||
|
.circle(2)
|
||||||
|
.extrude("last")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(wp.solids().size() == 1)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Workplane("XY").box(10, 10, 10).center(20, 0).box(10, 10, 10).faces(
|
||||||
|
">X[1]"
|
||||||
|
).workplane().rect(1, 1).extrude("test")
|
||||||
|
|
||||||
|
# Test extrude until arbitrary face
|
||||||
|
arbitrary_face = (
|
||||||
|
Workplane("XZ", origin=(0, 30, 0))
|
||||||
|
.transformed((20, 0, 0))
|
||||||
|
.box(10, 10, 10)
|
||||||
|
.faces("<Y")
|
||||||
|
.val()
|
||||||
|
)
|
||||||
|
wp = (
|
||||||
|
Workplane()
|
||||||
|
.box(5, 5, 5)
|
||||||
|
.faces(">Y")
|
||||||
|
.workplane()
|
||||||
|
.circle(2)
|
||||||
|
.extrude(until=arbitrary_face)
|
||||||
|
)
|
||||||
|
extremity_face_area = wp.faces(">Y").val().Area()
|
||||||
|
|
||||||
|
self.assertAlmostEqual(extremity_face_area, 13.372852288495501, 5)
|
||||||
|
|
||||||
|
# Test that a ValueError is raised if no face can be found to extrude until
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
wp = (
|
||||||
|
Workplane()
|
||||||
|
.box(5, 5, 5)
|
||||||
|
.faces(">X")
|
||||||
|
.workplane(offset=10)
|
||||||
|
.transformed((90, 0, 0))
|
||||||
|
.circle(2)
|
||||||
|
.extrude(until="next")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that a ValueError for:
|
||||||
|
# Extrusion in both direction while having a face to extrude only in one
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
wp = (
|
||||||
|
Workplane()
|
||||||
|
.box(5, 5, 5)
|
||||||
|
.faces(">X")
|
||||||
|
.workplane(offset=10)
|
||||||
|
.transformed((90, 0, 0))
|
||||||
|
.circle(2)
|
||||||
|
.extrude(until="next", both=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that a ValueError for:
|
||||||
|
# Extrusion in both direction while having no faces to extrude
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
wp = Workplane().circle(2).extrude(until="next", both=True)
|
||||||
|
|
||||||
|
# Check that a ValueError is raised if the user want to use `until` with a face and `combine` = False
|
||||||
|
# This isn't possible as the result of the extrude operation automatically combine the result with the base solid
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
wp = (
|
||||||
|
Workplane()
|
||||||
|
.box(5, 5, 5)
|
||||||
|
.faces(">X")
|
||||||
|
.workplane(offset=10)
|
||||||
|
.transformed((90, 0, 0))
|
||||||
|
.circle(2)
|
||||||
|
.extrude(until="next", combine=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same as previous test, but use an object of type Face
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
wp = Workplane().box(5, 5, 5).faces(">X")
|
||||||
|
face0 = wp.val()
|
||||||
|
wp = (
|
||||||
|
wp.workplane(offset=10)
|
||||||
|
.transformed((90, 0, 0))
|
||||||
|
.circle(2)
|
||||||
|
.extrude(until=face0, combine=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test extrude up to next face when workplane is inside a solid (which should still extrude
|
||||||
|
# past solid surface and up to next face)
|
||||||
|
# make an I-beam shape
|
||||||
|
part = (
|
||||||
|
Workplane()
|
||||||
|
.tag("base")
|
||||||
|
.box(10, 1, 1, centered=True)
|
||||||
|
.faces(">Z")
|
||||||
|
.workplane()
|
||||||
|
.box(1, 1, 10, centered=(True, True, False))
|
||||||
|
.faces(">Z")
|
||||||
|
.workplane()
|
||||||
|
.box(10, 1, 1, centered=(True, True, False))
|
||||||
|
# make an extrusion that starts inside the existing solid
|
||||||
|
.workplaneFromTagged("base")
|
||||||
|
.center(3, 0)
|
||||||
|
.circle(0.4)
|
||||||
|
# "next" should extrude to the top of the I-beam, not the bottom (0.5 units away)
|
||||||
|
.extrude("next")
|
||||||
|
)
|
||||||
|
part_section = part.faces("<Z").workplane().section(-5)
|
||||||
|
self.assertEqual(part_section.faces().size(), 2)
|
||||||
|
|
||||||
|
def testCutBlindUntilFace(self):
|
||||||
|
"""
|
||||||
|
Test untilNextFace and untilLastFace options of Workplane.cutBlind()
|
||||||
|
"""
|
||||||
|
# Basic test to see if it yields same results as regular cutBlind for similar use case
|
||||||
|
wp_ref = (
|
||||||
|
Workplane("XY")
|
||||||
|
.box(40, 10, 2)
|
||||||
|
.pushPoints([(-20, 0, 5), (0, 0, 5), (20, 0, 5)])
|
||||||
|
.box(10, 10, 10)
|
||||||
|
)
|
||||||
|
|
||||||
|
wp_ref_regular_cut = (
|
||||||
|
wp_ref.faces(">X[2]")
|
||||||
|
.workplane(centerOption="CenterOfMass")
|
||||||
|
.rect(2, 2)
|
||||||
|
.cutBlind(-10)
|
||||||
|
)
|
||||||
|
wp = (
|
||||||
|
wp_ref.faces(">X[2]")
|
||||||
|
.workplane(centerOption="CenterOfMass")
|
||||||
|
.rect(2, 2)
|
||||||
|
.cutBlind("last")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertAlmostEquals(wp_ref_regular_cut.val().Volume(), wp.val().Volume())
|
||||||
|
|
||||||
|
wp_last = (
|
||||||
|
wp_ref.faces(">X[4]")
|
||||||
|
.workplane(centerOption="CenterOfMass")
|
||||||
|
.rect(2, 2)
|
||||||
|
.cutBlind("last")
|
||||||
|
)
|
||||||
|
wp_next = (
|
||||||
|
wp_ref.faces(">X[4]")
|
||||||
|
.workplane(centerOption="CenterOfMass")
|
||||||
|
.rect(2, 2)
|
||||||
|
.cutBlind("next")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(wp_last.val().Volume() < wp_next.val().Volume())
|
||||||
|
|
||||||
|
# multiple wire cuts
|
||||||
|
|
||||||
|
wp = (
|
||||||
|
wp_ref.faces(">X[4]")
|
||||||
|
.workplane(centerOption="CenterOfMass", offset=0)
|
||||||
|
.rect(2.5, 2.5, forConstruction=True)
|
||||||
|
.vertices()
|
||||||
|
.rect(1, 1)
|
||||||
|
.cutBlind("last")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(wp.faces().size() == 50)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Workplane("XY").box(10, 10, 10).center(20, 0).box(10, 10, 10).faces(
|
||||||
|
">X[1]"
|
||||||
|
).workplane().rect(1, 1).cutBlind("test")
|
||||||
|
|
||||||
|
# Test extrusion to an arbitrary face
|
||||||
|
|
||||||
|
arbitrary_face = (
|
||||||
|
Workplane("XZ", origin=(0, 5, 0))
|
||||||
|
.transformed((20, 0, 0))
|
||||||
|
.box(10, 10, 10)
|
||||||
|
.faces("<Y")
|
||||||
|
.val()
|
||||||
|
)
|
||||||
|
wp = (
|
||||||
|
Workplane()
|
||||||
|
.box(5, 5, 5)
|
||||||
|
.faces(">Y")
|
||||||
|
.workplane()
|
||||||
|
.circle(2)
|
||||||
|
.cutBlind(until=arbitrary_face)
|
||||||
|
)
|
||||||
|
inner_face_area = wp.faces("<<Y[3]").val().Area()
|
||||||
|
|
||||||
|
self.assertAlmostEqual(inner_face_area, 13.372852288495503, 5)
|
||||||
|
|
||||||
|
def testFaceIntersectedByLine(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Workplane().box(5, 5, 5).val().facesIntersectedByLine(
|
||||||
|
(0, 0, 0), (0, 0, 1), direction="Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
pts = [(-10, 0), (-5, 0), (0, 0), (5, 0), (10, 0)]
|
||||||
|
shape = (
|
||||||
|
Workplane()
|
||||||
|
.box(20, 10, 5)
|
||||||
|
.faces(">Z")
|
||||||
|
.workplane()
|
||||||
|
.pushPoints(pts)
|
||||||
|
.box(1, 10, 10)
|
||||||
|
)
|
||||||
|
faces = shape.val().facesIntersectedByLine((0, 0, 7.5), (1, 0, 0))
|
||||||
|
mx_face = shape.faces("<X").val()
|
||||||
|
px_face = shape.faces(">X").val()
|
||||||
|
|
||||||
|
self.assertTrue(len(faces) == 10)
|
||||||
|
# extremum faces are last or before last face
|
||||||
|
self.assertTrue(mx_face in faces[-2:])
|
||||||
|
self.assertTrue(px_face in faces[-2:])
|
||||||
|
|
||||||
def testExtrude(self):
|
def testExtrude(self):
|
||||||
"""
|
"""
|
||||||
Test extrude
|
Test extrude
|
||||||
|
@ -563,7 +563,7 @@ class TestCQSelectors(BaseTest):
|
|||||||
((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)),
|
((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)),
|
||||||
((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)),
|
((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)),
|
||||||
((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)),
|
((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)),
|
||||||
((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0)),
|
((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0),),
|
||||||
]
|
]
|
||||||
|
|
||||||
for d in test_data_edges:
|
for d in test_data_edges:
|
||||||
|
Reference in New Issue
Block a user