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):
|
||||
"""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
|
||||
|
188
cadquery/cq.py
188
cadquery/cq.py
@ -2011,7 +2011,7 @@ class Workplane(object):
|
||||
: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.
|
||||
|
||||
"""
|
||||
@ -2978,7 +2978,7 @@ class Workplane(object):
|
||||
|
||||
def extrude(
|
||||
self: T,
|
||||
distance: float,
|
||||
until: Union[float, Literal["next", "last"], Face],
|
||||
combine: bool = True,
|
||||
clean: bool = True,
|
||||
both: bool = False,
|
||||
@ -2987,8 +2987,12 @@ class Workplane(object):
|
||||
"""
|
||||
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
|
||||
: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 clean: call :py:meth:`clean` afterwards to have a clean shape
|
||||
: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
|
||||
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,
|
||||
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) 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:
|
||||
newS = self._combineWithBase(r)
|
||||
@ -3355,37 +3376,50 @@ class Workplane(object):
|
||||
|
||||
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,
|
||||
<0 means in the negative direction
|
||||
:param until: The distance to cut to, normal to the workplane plane. When a negative float
|
||||
is passed the cut extends this far in the opposite direction to the normal of the plane
|
||||
(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 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(
|
||||
f"Do not know how to handle until argument of type {type(until)}"
|
||||
)
|
||||
if clean:
|
||||
s = s.clean()
|
||||
|
||||
@ -3441,48 +3475,130 @@ class Workplane(object):
|
||||
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, 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
|
||||
# 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"
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
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)
|
||||
else:
|
||||
for ws in wireSets:
|
||||
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
|
||||
else:
|
||||
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir, taper=taper)
|
||||
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)
|
||||
|
@ -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):
|
||||
@ -448,8 +448,8 @@ class ParameterDescriptionFinder(ast.NodeTransformer):
|
||||
|
||||
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.
|
||||
|
@ -27,16 +27,16 @@ TOL = 1e-2
|
||||
class Vector(object):
|
||||
"""Create a 3-dimensional vector
|
||||
|
||||
:param args: a 3D vector, with x-y-z parts.
|
||||
: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
|
||||
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
|
||||
@ -347,8 +347,7 @@ class Matrix:
|
||||
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)] + [
|
||||
|
@ -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
|
||||
"""
|
||||
@ -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
|
||||
"""
|
||||
|
||||
@ -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: [])
|
||||
|
@ -95,6 +95,7 @@ from OCP.BRepPrimAPI import (
|
||||
BRepPrimAPI_MakeRevol,
|
||||
BRepPrimAPI_MakeSphere,
|
||||
)
|
||||
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
|
||||
|
||||
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.GCE2d import GCE2d_MakeSegment
|
||||
from OCP.gce import gce_MakeLin, gce_MakeDir
|
||||
from OCP.GeomAPI import (
|
||||
GeomAPI_Interpolate,
|
||||
GeomAPI_ProjectPointOnSurf,
|
||||
@ -1034,6 +1036,85 @@ class Shape(object):
|
||||
|
||||
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":
|
||||
"""
|
||||
Split this shape with the positional arguments.
|
||||
@ -2297,6 +2378,9 @@ class Shell(Shape):
|
||||
return cls(s)
|
||||
|
||||
|
||||
TS = TypeVar("TS", bound=ShapeProtocol)
|
||||
|
||||
|
||||
class Mixin3D(object):
|
||||
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()
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -3015,47 +3144,6 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -54,7 +54,7 @@ class TestCadObjects(BaseTest):
|
||||
|
||||
def testVertex(self):
|
||||
"""
|
||||
Tests basic vertex functions
|
||||
Tests basic vertex functions
|
||||
"""
|
||||
v = Vertex.makeVertex(1, 1, 1)
|
||||
self.assertEqual(1, v.X)
|
||||
|
@ -3136,6 +3136,275 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
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):
|
||||
"""
|
||||
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.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.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:
|
||||
@ -801,7 +801,7 @@ class TestCQSelectors(BaseTest):
|
||||
|
||||
def testLengthNthSelector_UnitEdgeAndWire(self):
|
||||
"""
|
||||
Checks that key() method of LengthNthSelector
|
||||
Checks that key() method of LengthNthSelector
|
||||
calculates lengths of unit edge correctly
|
||||
"""
|
||||
unit_edge = Edge.makeLine(Vector(0, 0, 0), Vector(0, 0, 1))
|
||||
|
Reference in New Issue
Block a user