Surface modeling functionality (#1007)

* Thicken and extend nsided

* Accept wires too

* Implement project

* Add distance(s), project and constructOn

* Convert to VectorLike

* Allow VectorLike everywhere

* Implement Location ** and allow VectorLike

* Additional tests for Location

* Refactor interpPlate

* Fix tests

* Project and distance tests

* More tests

* More tests

* Use Real for dispatch

* Better coverage
This commit is contained in:
AU
2022-07-08 08:15:25 +02:00
committed by GitHub
parent c9d3f1e693
commit 53045e7fd1
6 changed files with 457 additions and 189 deletions

View File

@ -3745,10 +3745,12 @@ class Workplane(object):
def interpPlate(
self: T,
surf_edges: Union[Sequence[VectorLike], Sequence[Edge]],
surf_edges: Union[
Sequence[VectorLike], Sequence[Union[Edge, Wire]], "Workplane"
],
surf_pts: Sequence[VectorLike] = [],
thickness: float = 0,
combine: bool = False,
combine: CombineMode = False,
clean: bool = True,
degree: int = 3,
nbPtsOnCur: int = 15,
@ -3797,34 +3799,39 @@ class Workplane(object):
:type MaxSegments: Integer >= 2 (?)
"""
# If thickness is 0, only a 2D surface will be returned.
if thickness == 0:
combine = False
# convert points to edges if needed
edges: List[Union[Edge, Wire]] = []
points = []
if isinstance(surf_edges, Workplane):
edges.extend(cast(Edge, el) for el in surf_edges.edges().objects)
else:
for el in surf_edges:
if isinstance(el, (Edge, Wire)):
edges.append(el)
else:
points.append(el)
# Creates interpolated plate
p = Solid.interpPlate(
surf_edges,
f: Face = Face.makeNSidedSurface(
edges if not points else [Wire.makePolygon(points).close()],
surf_pts,
thickness,
degree,
nbPtsOnCur,
nbIter,
anisotropy,
tol2d,
tol3d,
tolAng,
tolCurv,
maxDeg,
maxSegments,
degree=degree,
nbPtsOnCur=nbPtsOnCur,
nbIter=nbIter,
anisotropy=anisotropy,
tol2d=tol2d,
tol3d=tol3d,
tolAng=tolAng,
tolCurv=tolCurv,
maxDeg=maxDeg,
maxSegments=maxSegments,
)
plates = self.eachpoint(lambda loc: p.moved(loc), True)
# thicken if needed
s = f.thicken(thickness) if thickness > 0 else f
# if combination is not desired, just return the created boxes
if not combine:
return plates
else:
return self.union(plates, clean=clean)
return self.eachpoint(lambda loc: s.moved(loc), True, combine)
def box(
self: T,
@ -3832,7 +3839,7 @@ class Workplane(object):
width: float,
height: float,
centered: Union[bool, Tuple[bool, bool, bool]] = True,
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
) -> T:
"""
@ -3894,14 +3901,7 @@ class Workplane(object):
box = Solid.makeBox(length, width, height, offset)
boxes = self.eachpoint(lambda loc: box.moved(loc), True)
# if combination is not desired, just return the created boxes
if not combine:
return boxes
else:
# combine everything
return self.union(boxes, clean=clean)
return self.eachpoint(lambda loc: box.moved(loc), True, combine)
def sphere(
self: T,
@ -3911,7 +3911,7 @@ class Workplane(object):
angle2: float = 90,
angle3: float = 360,
centered: Union[bool, Tuple[bool, bool, bool]] = True,
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
) -> T:
"""
@ -3965,13 +3965,7 @@ class Workplane(object):
s = Solid.makeSphere(radius, offset, direct, angle1, angle2, angle3)
# We want a sphere for each point on the workplane
spheres = self.eachpoint(lambda loc: s.moved(loc), True)
# If we don't need to combine everything, just return the created spheres
if not combine:
return spheres
else:
return self.union(spheres, clean=clean)
return self.eachpoint(lambda loc: s.moved(loc), True, combine)
def cylinder(
self: T,
@ -3980,7 +3974,7 @@ class Workplane(object):
direct: Vector = Vector(0, 0, 1),
angle: float = 360,
centered: Union[bool, Tuple[bool, bool, bool]] = True,
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
) -> T:
"""
@ -4028,13 +4022,7 @@ class Workplane(object):
s = Solid.makeCylinder(radius, height, offset, direct, angle)
# We want a cylinder for each point on the workplane
cylinders = self.eachpoint(lambda loc: s.moved(loc), True)
# If we don't need to combine everything, just return the created cylinders
if not combine:
return cylinders
else:
return self.union(cylinders, clean=clean)
return self.eachpoint(lambda loc: s.moved(loc), True, combine)
def wedge(
self: T,
@ -4048,7 +4036,7 @@ class Workplane(object):
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
centered: Union[bool, Tuple[bool, bool, bool]] = True,
combine: bool = True,
combine: CombineMode = True,
clean: bool = True,
) -> T:
"""
@ -4104,13 +4092,7 @@ class Workplane(object):
w = Solid.makeWedge(dx, dy, dz, xmin, zmin, xmax, zmax, offset, dir)
# We want a wedge for each point on the workplane
wedges = self.eachpoint(lambda loc: w.moved(loc), True)
# If we don't need to combine everything, just return the created wedges
if not combine:
return wedges
else:
return self.union(wedges, clean=clean)
return self.eachpoint(lambda loc: w.moved(loc), True, combine)
def clean(self: T) -> T:
"""

View File

@ -21,8 +21,12 @@ from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.TopoDS import TopoDS_Shape
from OCP.TopLoc import TopLoc_Location
from ..types import Real
TOL = 1e-2
VectorLike = Union["Vector", Tuple[Real, Real], Tuple[Real, Real, Real]]
class Vector(object):
"""Create a 3-dimensional vector
@ -928,7 +932,7 @@ class Location(object):
...
@overload
def __init__(self, t: Vector) -> None:
def __init__(self, t: VectorLike) -> None:
"""Location with translation t with respect to the original location."""
...
@ -938,7 +942,7 @@ class Location(object):
...
@overload
def __init__(self, t: Plane, v: Vector) -> None:
def __init__(self, t: Plane, v: VectorLike) -> None:
"""Location corresponding to the angular location of the Plane t with translation v."""
...
@ -953,7 +957,7 @@ class Location(object):
...
@overload
def __init__(self, t: Vector, ax: Vector, angle: float) -> None:
def __init__(self, t: VectorLike, ax: VectorLike, angle: float) -> None:
"""Location with translation t and rotation around ax by angle
with respect to the original location."""
...
@ -967,8 +971,8 @@ class Location(object):
elif len(args) == 1:
t = args[0]
if isinstance(t, Vector):
T.SetTranslationPart(t.wrapped)
if isinstance(t, (Vector, tuple)):
T.SetTranslationPart(Vector(t).wrapped)
elif isinstance(t, Plane):
cs = gp_Ax3(t.origin.toPnt(), t.zDir.toDir(), t.xDir.toDir())
T.SetTransformation(cs)
@ -978,21 +982,19 @@ class Location(object):
return
elif isinstance(t, gp_Trsf):
T = t
elif isinstance(t, (tuple, list)):
raise TypeError(
"A tuple or list is not a valid parameter, use a Vector instead."
)
else:
raise TypeError("Unexpected parameters")
elif len(args) == 2:
t, v = args
cs = gp_Ax3(v.toPnt(), t.zDir.toDir(), t.xDir.toDir())
cs = gp_Ax3(Vector(v).toPnt(), t.zDir.toDir(), t.xDir.toDir())
T.SetTransformation(cs)
T.Invert()
else:
t, ax, angle = args
T.SetRotation(gp_Ax1(Vector().toPnt(), ax.toDir()), angle * math.pi / 180.0)
T.SetTranslationPart(t.wrapped)
T.SetRotation(
gp_Ax1(Vector().toPnt(), Vector(ax).toDir()), angle * math.pi / 180.0
)
T.SetTranslationPart(Vector(t).wrapped)
self.wrapped = TopLoc_Location(T)
@ -1005,6 +1007,10 @@ class Location(object):
return Location(self.wrapped * other.wrapped)
def __pow__(self, exponent: int) -> "Location":
return Location(self.wrapped.Powered(exponent))
def toTuple(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]:
"""Convert the location to a translation, rotation tuple."""

View File

@ -1,5 +1,4 @@
from typing import (
Type,
Optional,
Tuple,
Union,
@ -20,7 +19,8 @@ from io import BytesIO
from vtkmodules.vtkCommonDataModel import vtkPolyData
from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals
from .geom import Vector, BoundBox, Plane, Location, Matrix
from .geom import Vector, VectorLike, BoundBox, Plane, Location, Matrix
from ..utils import cqmultimethod as multimethod
@ -205,7 +205,7 @@ from OCP.GeomAbs import (
GeomAbs_JoinType,
)
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling
from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin
from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Mode
from OCP.BOPAlgo import BOPAlgo_GlueEnum
@ -224,6 +224,9 @@ from OCP.GeomFill import (
GeomFill_TrihedronLaw,
)
from OCP.BRepProj import BRepProj_Projection
from OCP.BRepExtrema import BRepExtrema_DistShapeShape
from OCP.IVtkOCC import IVtkOCC_Shape, IVtkOCC_ShapeMesher
from OCP.IVtkVTK import IVtkVTK_ShapeData
@ -232,9 +235,12 @@ from OCP.Standard import Standard_NoSuchObject, Standard_Failure
from OCP.Interface import Interface_Static
from math import pi, sqrt
from math import pi, sqrt, inf
import warnings
from ..utils import deprecate
Real = Union[float, int]
TOLERANCE = 1e-6
@ -338,7 +344,7 @@ Geoms = Literal[
"HYPERBOLA",
"PARABOLA",
]
VectorLike = Union[Vector, Tuple[float, float, float]]
T = TypeVar("T", bound="Shape")
@ -846,7 +852,7 @@ class Shape(object):
return self.__class__(BRepBuilderAPI_Transform(self.wrapped, Tr, True).Shape())
def rotate(
self: T, startVector: Vector, endVector: Vector, angleDegrees: float
self: T, startVector: VectorLike, endVector: VectorLike, angleDegrees: float
) -> T:
"""
Rotates a shape around an axis.
@ -866,22 +872,22 @@ class Shape(object):
Tr = gp_Trsf()
Tr.SetRotation(
gp_Ax1(startVector.toPnt(), (endVector - startVector).toDir()),
gp_Ax1(
Vector(startVector).toPnt(),
(Vector(endVector) - Vector(startVector)).toDir(),
),
angleDegrees * DEG2RAD,
)
return self._apply_transform(Tr)
def translate(self: T, vector: Vector) -> T:
def translate(self: T, vector: VectorLike) -> T:
"""
Translates this shape through a transformation.
"""
if type(vector) == tuple:
vector = Vector(vector)
T = gp_Trsf()
T.SetTranslation(vector.wrapped)
T.SetTranslation(Vector(vector).wrapped)
return self._apply_transform(T)
@ -1147,6 +1153,27 @@ class Shape(object):
return self._bool_op((self,), splitters, split_op)
def distance(self, other: "Shape") -> float:
"""
Minimal distance between two shapes
"""
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
def distances(self, *others: "Shape") -> Iterator[float]:
"""
Minimal distances to between self and other shapes
"""
dist_calc = BRepExtrema_DistShapeShape()
dist_calc.LoadS1(self.wrapped)
for s in others:
dist_calc.LoadS2(s.wrapped)
dist_calc.Perform()
yield dist_calc.Value()
def mesh(self, tolerance: float, angularTolerance: float = 0.1):
"""
Generate triangulation if none exists.
@ -1327,6 +1354,9 @@ class Mixin1DProtocol(ShapeProtocol, Protocol):
...
T1D = TypeVar("T1D", bound=Mixin1DProtocol)
class Mixin1D(object):
def _bounds(self: Mixin1DProtocol) -> Tuple[float, float]:
@ -1555,6 +1585,40 @@ class Mixin1D(object):
return [self.locationAt(d, mode, frame, planar) for d in ds]
def project(
self: T1D, face: "Face", d: VectorLike, closest: bool = True
) -> Union[T1D, List[T1D]]:
"""
Project onto a face along the specified direction
"""
bldr = BRepProj_Projection(self.wrapped, face.wrapped, Vector(d).toDir())
shapes = Compound(bldr.Shape())
# select the closest projection if requested
rv: Union[T1D, List[T1D]]
if closest:
dist_calc = BRepExtrema_DistShapeShape()
dist_calc.LoadS1(self.wrapped)
min_dist = inf
for el in shapes:
dist_calc.LoadS2(el.wrapped)
dist_calc.Perform()
dist = dist_calc.Value()
if dist < min_dist:
min_dist = dist
rv = tcast(T1D, el)
else:
rv = [tcast(T1D, el) for el in shapes]
return rv
class Edge(Shape, Mixin1D):
"""
@ -1808,7 +1872,9 @@ class Edge(Shape, Mixin1D):
return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge())
@classmethod
def makeThreePointArc(cls, v1: Vector, v2: Vector, v3: Vector) -> "Edge":
def makeThreePointArc(
cls, v1: VectorLike, v2: VectorLike, v3: VectorLike
) -> "Edge":
"""
Makes a three point arc through the provided points
:param cls:
@ -1817,12 +1883,14 @@ class Edge(Shape, Mixin1D):
:param v3: end vector
:return: an edge object through the three points
"""
circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2.toPnt(), v3.toPnt()).Value()
circle_geom = GC_MakeArcOfCircle(
Vector(v1).toPnt(), Vector(v2).toPnt(), Vector(v3).toPnt()
).Value()
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
@classmethod
def makeTangentArc(cls, v1: Vector, v2: Vector, v3: Vector) -> "Edge":
def makeTangentArc(cls, v1: VectorLike, v2: VectorLike, v3: VectorLike) -> "Edge":
"""
Makes a tangent arc from point v1, in the direction of v2 and ends at
v3.
@ -1832,19 +1900,23 @@ class Edge(Shape, Mixin1D):
:param v3: end vector
:return: an edge
"""
circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2.wrapped, v3.toPnt()).Value()
circle_geom = GC_MakeArcOfCircle(
Vector(v1).toPnt(), Vector(v2).wrapped, Vector(v3).toPnt()
).Value()
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
@classmethod
def makeLine(cls, v1: Vector, v2: Vector) -> "Edge":
def makeLine(cls, v1: VectorLike, v2: VectorLike) -> "Edge":
"""
Create a line between two points
:param v1: Vector that represents the first point
:param v2: Vector that represents the second point
:return: A linear edge between the two provided points
"""
return cls(BRepBuilderAPI_MakeEdge(v1.toPnt(), v2.toPnt()).Edge())
return cls(
BRepBuilderAPI_MakeEdge(Vector(v1).toPnt(), Vector(v2).toPnt()).Edge()
)
class Wire(Shape, Mixin1D):
@ -1937,7 +2009,9 @@ class Wire(Shape, Mixin1D):
return cls(wire_builder.Wire())
@classmethod
def makeCircle(cls, radius: float, center: Vector, normal: Vector) -> "Wire":
def makeCircle(
cls, radius: float, center: VectorLike, normal: VectorLike
) -> "Wire":
"""
Makes a Circle centered at the provided point, having normal in the provided direction
:param radius: floating point radius of the circle, must be > 0
@ -1955,9 +2029,9 @@ class Wire(Shape, Mixin1D):
cls,
x_radius: float,
y_radius: float,
center: Vector,
normal: Vector,
xDir: Vector,
center: VectorLike,
normal: VectorLike,
xDir: VectorLike,
angle1: float = 360.0,
angle2: float = 360.0,
rotation_angle: float = 0.0,
@ -1986,19 +2060,19 @@ class Wire(Shape, Mixin1D):
w = cls.assembleEdges([ellipse_edge])
if rotation_angle != 0.0:
w = w.rotate(center, center + normal, rotation_angle)
w = w.rotate(center, Vector(center) + Vector(normal), rotation_angle)
return w
@classmethod
def makePolygon(
cls, listOfVertices: Iterable[Vector], forConstruction: bool = False,
cls, listOfVertices: Iterable[VectorLike], forConstruction: bool = False,
) -> "Wire":
# convert list of tuples into Vectors.
wire_builder = BRepBuilderAPI_MakePolygon()
for v in listOfVertices:
wire_builder.Add(v.toPnt())
wire_builder.Add(Vector(v).toPnt())
w = cls(wire_builder.Wire())
w.forConstruction = forConstruction
@ -2011,8 +2085,8 @@ class Wire(Shape, Mixin1D):
pitch: float,
height: float,
radius: float,
center: Vector = Vector(0, 0, 0),
dir: Vector = Vector(0, 0, 1),
center: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angle: float = 360.0,
lefthand: bool = False,
) -> "Wire":
@ -2025,11 +2099,13 @@ class Wire(Shape, Mixin1D):
# 1. build underlying cylindrical/conical surface
if angle == 360.0:
geom_surf: Geom_Surface = Geom_CylindricalSurface(
gp_Ax3(center.toPnt(), dir.toDir()), radius
gp_Ax3(Vector(center).toPnt(), Vector(dir).toDir()), radius
)
else:
geom_surf = Geom_ConicalSurface(
gp_Ax3(center.toPnt(), dir.toDir()), angle * DEG2RAD, radius
gp_Ax3(Vector(center).toPnt(), Vector(dir).toDir()),
angle * DEG2RAD,
radius,
)
# 2. construct an segment in the u,v domain
@ -2170,8 +2246,8 @@ class Face(Shape):
@classmethod
def makeNSidedSurface(
cls,
edges: Iterable[Edge],
points: Iterable[gp_Pnt],
edges: Iterable[Union[Edge, Wire]],
constraints: Iterable[Union[Edge, Wire, VectorLike, gp_Pnt]],
continuity: GeomAbs_Shape = GeomAbs_C0,
degree: int = 3,
nbPtsOnCur: int = 15,
@ -2186,8 +2262,8 @@ class Face(Shape):
) -> "Face":
"""
Returns a surface enclosed by a closed polygon defined by 'edges' and going through 'points'.
:param points
:type points: list of gp_Pnt
:param constraints
:type points: list of constraints (points or edges)
:param edges
:type edges: list of Edge
:param continuity=GeomAbs_C0
@ -2226,12 +2302,36 @@ class Face(Shape):
maxDeg,
maxSegments,
)
for edge in edges:
n_sided.Add(edge.wrapped, continuity)
for pt in points:
n_sided.Add(pt)
# outer edges
for el in edges:
if isinstance(el, Edge):
n_sided.Add(el.wrapped, continuity)
else:
for el_edge in el.Edges():
n_sided.Add(el_edge.wrapped, continuity)
# (inner) constraints
for c in constraints:
if isinstance(c, gp_Pnt):
n_sided.Add(c)
elif isinstance(c, Vector):
n_sided.Add(c.toPnt())
elif isinstance(c, tuple):
n_sided.Add(Vector(c).toPnt())
elif isinstance(c, Edge):
n_sided.Add(c.wrapped, GeomAbs_C0, False)
elif isinstance(c, Wire):
for e in c.Edges():
n_sided.Add(e.wrapped, GeomAbs_C0, False)
else:
raise ValueError(f"Invalid constraint {c}")
# build, fix and return
n_sided.Build()
face = n_sided.Shape()
return Face(face).fix()
@classmethod
@ -2407,6 +2507,45 @@ class Face(Shape):
adaptor = BRepAdaptor_Surface(self.wrapped)
return adaptor.Plane()
def thicken(self, thickness: float) -> "Solid":
"""
Return a thickened face
"""
builder = BRepOffset_MakeOffset()
builder.Initialize(
self.wrapped,
thickness,
1.0e-6,
BRepOffset_Mode.BRepOffset_Skin,
False,
False,
GeomAbs_Intersection,
True,
) # The last True is important to make solid
builder.MakeOffsetShape()
return Solid(builder.Shape())
@classmethod
def constructOn(cls, f: "Face", outer: "Wire", *inner: "Wire") -> "Face":
bldr = BRepBuilderAPI_MakeFace(f._geomAdaptor(), outer.wrapped)
for w in inner:
bldr.Add(TopoDS.Wire_s(w.wrapped.Reversed()))
return cls(bldr.Face()).fix()
def project(self, other: "Face", d: VectorLike) -> "Face":
outer_p = tcast(Wire, self.outerWire().project(other, d))
inner_p = (tcast(Wire, w.project(other, d)) for w in self.innerWires())
return self.constructOn(other, outer_p, *inner_p)
class Shell(Shape):
"""
@ -2631,6 +2770,7 @@ class Solid(Shape, Mixin3D):
wrapped: TopoDS_Solid
@classmethod
@deprecate()
def interpPlate(
cls,
surf_edges,
@ -2722,19 +2862,8 @@ class Solid(Shape, Mixin3D):
if (
abs(thickness) > 0
): # abs() because negative values are allowed to set direction of thickening
solid = BRepOffset_MakeOffset()
solid.Initialize(
face.wrapped,
thickness,
1.0e-5,
BRepOffset_Skin,
False,
False,
GeomAbs_Intersection,
True,
) # The last True is important to make solid
solid.MakeOffsetShape()
return cls(solid.Shape())
return face.thicken(thickness)
else: # Return 2D surface only
return face
@ -2761,8 +2890,8 @@ class Solid(Shape, Mixin3D):
length: float,
width: float,
height: float,
pnt: Vector = Vector(0, 0, 0),
dir: Vector = Vector(0, 0, 1),
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
) -> "Solid":
"""
makeBox(length,width,height,[pnt,dir]) -- Make a box located in pnt with the dimensions (length,width,height)
@ -2770,7 +2899,7 @@ class Solid(Shape, Mixin3D):
"""
return cls(
BRepPrimAPI_MakeBox(
gp_Ax2(pnt.toPnt(), dir.toDir()), length, width, height
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()), length, width, height
).Shape()
)
@ -2780,8 +2909,8 @@ class Solid(Shape, Mixin3D):
radius1: float,
radius2: float,
height: float,
pnt: Vector = Vector(0, 0, 0),
dir: Vector = Vector(0, 0, 1),
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees: float = 360,
) -> "Solid":
"""
@ -2791,7 +2920,7 @@ class Solid(Shape, Mixin3D):
"""
return cls(
BRepPrimAPI_MakeCone(
gp_Ax2(pnt.toPnt(), dir.toDir()),
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius1,
radius2,
height,
@ -2804,8 +2933,8 @@ class Solid(Shape, Mixin3D):
cls,
radius: float,
height: float,
pnt: Vector = Vector(0, 0, 0),
dir: Vector = Vector(0, 0, 1),
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees: float = 360,
) -> "Solid":
"""
@ -2815,7 +2944,10 @@ class Solid(Shape, Mixin3D):
"""
return cls(
BRepPrimAPI_MakeCylinder(
gp_Ax2(pnt.toPnt(), dir.toDir()), radius, height, angleDegrees * DEG2RAD
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius,
height,
angleDegrees * DEG2RAD,
).Shape()
)
@ -2824,8 +2956,8 @@ class Solid(Shape, Mixin3D):
cls,
radius1: float,
radius2: float,
pnt: Vector = Vector(0, 0, 0),
dir: Vector = Vector(0, 0, 1),
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees1: float = 0,
angleDegrees2: float = 360,
) -> "Solid":
@ -2837,7 +2969,7 @@ class Solid(Shape, Mixin3D):
"""
return cls(
BRepPrimAPI_MakeTorus(
gp_Ax2(pnt.toPnt(), dir.toDir()),
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius1,
radius2,
angleDegrees1 * DEG2RAD,
@ -2874,8 +3006,8 @@ class Solid(Shape, Mixin3D):
zmin: float,
xmax: float,
zmax: float,
pnt: Vector = Vector(0, 0, 0),
dir: Vector = Vector(0, 0, 1),
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
) -> "Solid":
"""
Make a wedge located in pnt
@ -2884,7 +3016,14 @@ class Solid(Shape, Mixin3D):
return cls(
BRepPrimAPI_MakeWedge(
gp_Ax2(pnt.toPnt(), dir.toDir()), dx, dy, dz, xmin, zmin, xmax, zmax
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
dx,
dy,
dz,
xmin,
zmin,
xmax,
zmax,
).Solid()
)
@ -2892,8 +3031,8 @@ class Solid(Shape, Mixin3D):
def makeSphere(
cls,
radius: float,
pnt: Vector = Vector(0, 0, 0),
dir: Vector = Vector(0, 0, 1),
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees1: float = 0,
angleDegrees2: float = 90,
angleDegrees3: float = 360,
@ -2904,7 +3043,7 @@ class Solid(Shape, Mixin3D):
"""
return cls(
BRepPrimAPI_MakeSphere(
gp_Ax2(pnt.toPnt(), dir.toDir()),
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius,
angleDegrees1 * DEG2RAD,
angleDegrees2 * DEG2RAD,
@ -2931,8 +3070,8 @@ class Solid(Shape, Mixin3D):
cls,
outerWire: Wire,
innerWires: List[Wire],
vecCenter: Vector,
vecNormal: Vector,
vecCenter: VectorLike,
vecNormal: VectorLike,
angleDegrees: Real,
) -> "Solid":
"""
@ -2986,7 +3125,11 @@ class Solid(Shape, Mixin3D):
@classmethod
@extrudeLinearWithRotation.register
def extrudeLinearWithRotation(
cls, face: Face, vecCenter: Vector, vecNormal: Vector, angleDegrees: Real,
cls,
face: Face,
vecCenter: VectorLike,
vecNormal: VectorLike,
angleDegrees: Real,
) -> "Solid":
return cls.extrudeLinearWithRotation(
@ -2998,7 +3141,7 @@ class Solid(Shape, Mixin3D):
cls,
outerWire: Wire,
innerWires: List[Wire],
vecNormal: Vector,
vecNormal: VectorLike,
taper: Real = 0,
) -> "Solid":
"""
@ -3033,11 +3176,13 @@ class Solid(Shape, Mixin3D):
@classmethod
@extrudeLinear.register
def extrudeLinear(cls, face: Face, vecNormal: Vector, taper: Real = 0,) -> "Solid":
def extrudeLinear(
cls, face: Face, vecNormal: VectorLike, taper: Real = 0,
) -> "Solid":
if taper == 0:
prism_builder: Any = BRepPrimAPI_MakePrism(
face.wrapped, vecNormal.wrapped, True
face.wrapped, Vector(vecNormal).wrapped, True
)
else:
faceNormal = face.normalAt()
@ -3054,8 +3199,8 @@ class Solid(Shape, Mixin3D):
outerWire: Wire,
innerWires: List[Wire],
angleDegrees: Real,
axisStart: Vector,
axisEnd: Vector,
axisStart: VectorLike,
axisEnd: VectorLike,
) -> "Solid":
"""
Attempt to revolve the list of wires into a solid in the provided direction
@ -3088,7 +3233,7 @@ class Solid(Shape, Mixin3D):
@classmethod
@revolve.register
def revolve(
cls, face: Face, angleDegrees: Real, axisStart: Vector, axisEnd: Vector,
cls, face: Face, angleDegrees: Real, axisStart: VectorLike, axisEnd: VectorLike,
) -> "Solid":
v1 = Vector(axisStart)
@ -3360,11 +3505,15 @@ class Compound(Shape, Mixin3D):
text_flat = text_flat.translate(t)
vecNormal = text_flat.Faces()[0].normalAt() * height
if height != 0:
vecNormal = text_flat.Faces()[0].normalAt() * height
text_3d = BRepPrimAPI_MakePrism(text_flat.wrapped, vecNormal.wrapped)
text_3d = BRepPrimAPI_MakePrism(text_flat.wrapped, vecNormal.wrapped)
rv = cls(text_3d.Shape()).transformShape(position.rG)
else:
rv = text_flat.transformShape(position.rG)
return cls(text_3d.Shape()).transformShape(position.rG)
return rv
def __iter__(self) -> Iterator[Shape]:
"""

View File

@ -4,8 +4,8 @@ import cadquery as cq
# TEST_1
# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface.
thickness = 0
edge_points = [[0.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 10.0, 10.0], [0.0, 0.0, 10.0]]
surface_points = [[5.0, 5.0, 5.0]]
edge_points = [(0.0, 0.0, 0.0), (0.0, 10.0, 0.0), (0.0, 10.0, 10.0), (0.0, 0.0, 10.0)]
surface_points = [(5.0, 5.0, 5.0)]
plate_0 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness)
print("plate_0.val().Volume() = ", plate_0.val().Volume())
plate_0 = plate_0.translate((0, 6 * 12, 0))
@ -15,11 +15,11 @@ show_object(plate_0)
# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides
thickness = 0.1
edge_points = [
[-7.0, -7.0, 0.0],
[-3.0, -10.0, 3.0],
[7.0, -7.0, 0.0],
[7.0, 7.0, 0.0],
[-7.0, 7.0, 0.0],
(-7.0, -7.0, 0.0),
(-3.0, -10.0, 3.0),
(7.0, -7.0, 0.0),
(7.0, 7.0, 0.0),
(-7.0, 7.0, 0.0),
]
edge_wire = cq.Workplane("XY").polyline(
[(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)]
@ -32,7 +32,7 @@ edge_wire = edge_wire.add(
.transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0))
.spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)])
)
surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]]
surface_points = [(-3.0, -3.0, -3.0), (3.0, 3.0, 3.0)]
plate_1 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
# plate_1 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) # list of (x,y,z) points instead of wires for edges
print("plate_1.val().Volume() = ", plate_1.val().Volume())
@ -45,16 +45,16 @@ r2 = 10.0
fn = 6
thickness = 0.1
edge_points = [
[r1 * cos(i * pi / fn), r1 * sin(i * pi / fn)]
(r1 * cos(i * pi / fn), r1 * sin(i * pi / fn))
if i % 2 == 0
else [r2 * cos(i * pi / fn), r2 * sin(i * pi / fn)]
else (r2 * cos(i * pi / fn), r2 * sin(i * pi / fn))
for i in range(2 * fn + 1)
]
edge_wire = cq.Workplane("XY").polyline(edge_points)
r2 = 4.5
surface_points = [
[r2 * cos(i * pi / fn), r2 * sin(i * pi / fn), 1.0] for i in range(2 * fn)
] + [[0.0, 0.0, -2.0]]
(r2 * cos(i * pi / fn), r2 * sin(i * pi / fn), 1.0) for i in range(2 * fn)
] + [(0.0, 0.0, -2.0)]
plate_2 = cq.Workplane("XY").interpPlate(
edge_wire,
surface_points,
@ -106,20 +106,20 @@ pts = [
thickness = 0.1
fn = 6
edge_points = [
[
(
r1 * cos(i * 2 * pi / fn + 30 * pi / 180),
r1 * sin(i * 2 * pi / fn + 30 * pi / 180),
]
)
for i in range(fn + 1)
]
surface_points = [
[
(
r1 / 4 * cos(i * 2 * pi / fn + 30 * pi / 180),
r1 / 4 * sin(i * 2 * pi / fn + 30 * pi / 180),
0.75,
]
)
for i in range(fn + 1)
] + [[0, 0, 2]]
] + [(0, 0, 2)]
edge_wire = cq.Workplane("XY").polyline(edge_points)
plate_3 = (
cq.Workplane("XY")
@ -168,7 +168,7 @@ for i in range(len(edge_points) - 1):
.workplane(offset=-offset_list[i + 1])
.spline(edge_points[i + 1])
)
surface_points = [[0, 0, 0]]
surface_points = [(0, 0, 0)]
plate_4 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
print("plate_4.val().Volume() = ", plate_4.val().Volume())
plate_4 = plate_4.translate((0, 5 * 12, 0))

View File

@ -594,6 +594,12 @@ class TestCadObjects(BaseTest):
def testLocation(self):
# Tuple
loc0 = Location((0, 0, 1))
T = loc0.wrapped.Transformation().TranslationPart()
self.assertTupleAlmostEquals((T.X(), T.Y(), T.Z()), (0, 0, 1), 6)
# Vector
loc1 = Location(Vector(0, 0, 1))
@ -621,9 +627,34 @@ class TestCadObjects(BaseTest):
self.assertTupleAlmostEquals(loc4.toTuple()[0], (0, 0, 0), 7)
self.assertTupleAlmostEquals(loc4.toTuple()[1], (0, 0, 0), 7)
# Test composition
loc4 = Location((0, 0, 0), Vector(0, 0, 1), 15)
loc5 = loc1 * loc4
loc6 = loc4 * loc4
loc7 = loc4 ** 2
T = loc5.wrapped.Transformation().TranslationPart()
self.assertTupleAlmostEquals((T.X(), T.Y(), T.Z()), (0, 0, 1), 6)
angle5 = (
loc5.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG
)
self.assertAlmostEqual(15, angle5)
angle6 = (
loc6.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG
)
self.assertAlmostEqual(30, angle6)
angle7 = (
loc7.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG
)
self.assertAlmostEqual(30, angle7)
# Test error handling on creation
with self.assertRaises(TypeError):
Location((0, 0, 1))
Location([0, 0, 1])
with self.assertRaises(TypeError):
Location("xy_plane")

View File

@ -4187,12 +4187,12 @@ class TestCadQuery(BaseTest):
# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface.
thickness = 0
edge_points = [
[0.0, 0.0, 0.0],
[0.0, 10.0, 0.0],
[0.0, 10.0, 10.0],
[0.0, 0.0, 10.0],
(0.0, 0.0, 0.0),
(0.0, 10.0, 0.0),
(0.0, 10.0, 10.0),
(0.0, 0.0, 10.0),
]
surface_points = [[5.0, 5.0, 5.0]]
surface_points = [(5.0, 5.0, 5.0)]
plate_0 = Workplane("XY").interpPlate(edge_points, surface_points, thickness)
self.assertTrue(plate_0.val().isValid())
self.assertAlmostEqual(plate_0.val().Area(), 141.218823892, 1)
@ -4200,11 +4200,11 @@ class TestCadQuery(BaseTest):
# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides
thickness = 0.1
edge_points = [
[-7.0, -7.0, 0.0],
[-3.0, -10.0, 3.0],
[7.0, -7.0, 0.0],
[7.0, 7.0, 0.0],
[-7.0, 7.0, 0.0],
(-7.0, -7.0, 0.0),
(-3.0, -10.0, 3.0),
(7.0, -7.0, 0.0),
(7.0, 7.0, 0.0),
(-7.0, 7.0, 0.0),
]
edge_wire = Workplane("XY").polyline(
[(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)]
@ -4217,7 +4217,7 @@ class TestCadQuery(BaseTest):
.transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0))
.spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)])
)
surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]]
surface_points = [(-3.0, -3.0, -3.0), (3.0, 3.0, 3.0)]
plate_1 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
self.assertTrue(plate_1.val().isValid())
self.assertAlmostEqual(plate_1.val().Volume(), 26.124970206, 2)
@ -4228,17 +4228,17 @@ class TestCadQuery(BaseTest):
fn = 6
thickness = 0.1
edge_points = [
[r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)]
(r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn))
if i % 2 == 0
else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)]
else (r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn))
for i in range(2 * fn + 1)
]
edge_wire = Workplane("XY").polyline(edge_points)
r2 = 4.5
surface_points = [
[r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn), 1.0]
(r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn), 1.0)
for i in range(2 * fn)
] + [[0.0, 0.0, -2.0]]
] + [(0.0, 0.0, -2.0)]
plate_2 = Workplane("XY").interpPlate(
edge_wire,
surface_points,
@ -4287,20 +4287,20 @@ class TestCadQuery(BaseTest):
thickness = 0.1
fn = 6
edge_points = [
[
(
r1 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180),
r1 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180),
]
)
for i in range(fn + 1)
]
surface_points = [
[
(
r1 / 4 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180),
r1 / 4 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180),
0.75,
]
)
for i in range(fn + 1)
] + [[0, 0, 2]]
] + [(0, 0, 2)]
edge_wire = Workplane("XY").polyline(edge_points)
plate_3 = (
Workplane("XY")
@ -4349,11 +4349,22 @@ class TestCadQuery(BaseTest):
.workplane(offset=-offset_list[i + 1])
.spline(edge_points[i + 1])
)
surface_points = [[0, 0, 0]]
surface_points = [(0, 0, 0)]
plate_4 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
self.assertTrue(plate_4.val().isValid())
self.assertAlmostEqual(plate_4.val().Volume(), 7.760559490, 2)
plate_5 = Workplane().interpPlate(Workplane().slot2D(2, 1).vals())
assert plate_5.val().isValid()
plate_6 = Solid.interpPlate(
[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], [], thickness=1
)
assert plate_6.isValid()
self.assertAlmostEqual(plate_6.Volume(), 1, 2)
def testTangentArcToPoint(self):
# create a simple shape with tangents of straight edges and see if it has the correct area
@ -5315,3 +5326,92 @@ class TestCadQuery(BaseTest):
repr(wp.plane)
== "Plane(origin=(0.0, 0.0, 0.0), xDir=(1.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0))"
)
def test_distance(self):
w1 = Face.makePlane(2, 2).Wires()[0]
w2 = Face.makePlane(1, 1).Wires()[0]
w3 = Face.makePlane(3, 3).Wires()[0]
d12 = w1.distance(w2)
assert d12 == approx(0.5)
d12, d13 = w1.distances(w2, w3)
assert d12 == approx(0.5)
assert d13 == approx(0.5)
def test_project(self):
# project a single letter
t = Compound.makeText("T", 5, 0).Faces()[0]
f = Workplane("XZ", origin=(0, 0, -7)).sphere(6).faces("not %PLANE").val()
res = t.project(f, (0, 0, -1))
assert res.isValid()
assert len(res.Edges()) == 8
assert t.distance(res) == approx(1)
# extrude it
res_ex = Solid.extrudeLinear(t.project(f, (0, 0, -1)), (0.0, 0.0, 0.5))
assert res_ex.isValid()
assert len(res_ex.Faces()) == 10
# project a wire
w = t.outerWire()
res_w = w.project(f, (0, 0, -1))
assert len(res_w.Edges()) == 8
assert res_w.isValid()
res_w1, res_w2 = w.project(f, (0, 0, -1), False)
assert len(res_w1.Edges()) == 8
assert len(res_w2.Edges()) == 8
# project a single letter with openings
o = Compound.makeText("O", 5, 0).Faces()[0]
f = Workplane("XZ", origin=(0, 0, -7)).sphere(6).faces("not %PLANE").val()
res_o = o.project(f, (0, 0, -1))
assert res_o.isValid()
# extrude it
res_o_ex = Solid.extrudeLinear(o.project(f, (0, 0, -1)), (0.0, 0.0, 0.5))
assert res_o_ex.isValid()
def test_makeNSidedSurface(self):
# inner edge/wire constraint
outer_w = Workplane().slot2D(2, 1).wires().vals()
inner_e1 = (
Workplane(origin=(0, 0, 1)).moveTo(-0.5, 0).lineTo(0.5, 0.0).edges().vals()
)
inner_e2 = (
Workplane(origin=(0, 0, 1)).moveTo(0, -0.2).lineTo(0, 0.2).edges().vals()
)
inner_w = Workplane(origin=(0, 0, 1)).ellipse(0.5, 0.2).vals()
f1 = Face.makeNSidedSurface(outer_w, inner_e1 + inner_e2 + inner_w)
assert f1.isValid()
assert len(f1.Edges()) == 4
# inner points
f2 = Face.makeNSidedSurface(
outer_w, [Vector(-0.4, 0, 1).toPnt(), Vector(0.4, 0, 1)]
)
assert f2.isValid()
assert len(f2.Edges()) == 4
# exception on invalid constraint
with raises(ValueError):
Face.makeNSidedSurface(outer_w, [[0, 0, 1]])