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:
Jojain
2021-09-26 12:35:56 +02:00
committed by GitHub
parent 04e3dd9175
commit f19c35c83c
9 changed files with 572 additions and 101 deletions

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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)] + [

View File

@ -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: [])

View File

@ -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):
"""

View File

@ -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)

View File

@ -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

View File

@ -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))