from typing import (
Optional,
Tuple,
Union,
Iterable,
List,
Sequence,
Iterator,
Dict,
Any,
overload,
TypeVar,
cast as tcast,
)
from typing_extensions import Literal, Protocol
from io import BytesIO
from vtkmodules.vtkCommonDataModel import vtkPolyData
from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals
from .geom import Vector, VectorLike, BoundBox, Plane, Location, Matrix
from .shape_protocols import geom_LUT_FACE, geom_LUT_EDGE, Shapes, Geoms
from ..selectors import (
Selector,
StringSyntaxSelector,
)
from ..utils import cqmultimethod as multimethod
# change default OCCT logging level
from OCP.Message import Message, Message_Gravity
for printer in Message.DefaultMessenger_s().Printers():
printer.SetTraceLevel(Message_Gravity.Message_Fail)
import OCP.TopAbs as ta # Topology type enum
import OCP.GeomAbs as ga # Geometry type enum
from OCP.Precision import Precision
from OCP.gp import (
gp_Vec,
gp_Pnt,
gp_Ax1,
gp_Ax2,
gp_Ax3,
gp_Dir,
gp_Circ,
gp_Trsf,
gp_Pln,
gp_Pnt2d,
gp_Dir2d,
gp_Elips,
)
# Array of points (used for B-spline construction):
from OCP.TColgp import TColgp_HArray1OfPnt, TColgp_HArray2OfPnt, TColgp_Array1OfPnt
# Array of vectors (used for B-spline interpolation):
from OCP.TColgp import TColgp_Array1OfVec
# Array of booleans (used for B-spline interpolation):
from OCP.TColStd import TColStd_HArray1OfBoolean
# Array of floats (used for B-spline interpolation):
from OCP.TColStd import TColStd_HArray1OfReal
from OCP.BRepAdaptor import (
BRepAdaptor_Curve,
BRepAdaptor_CompCurve,
BRepAdaptor_Surface,
)
from OCP.BRepBuilderAPI import (
BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeFace,
BRepBuilderAPI_MakePolygon,
BRepBuilderAPI_MakeWire,
BRepBuilderAPI_Sewing,
BRepBuilderAPI_Copy,
BRepBuilderAPI_GTransform,
BRepBuilderAPI_Transform,
BRepBuilderAPI_Transformed,
BRepBuilderAPI_RightCorner,
BRepBuilderAPI_RoundCorner,
BRepBuilderAPI_MakeSolid,
)
# properties used to store mass calculation result
from OCP.GProp import GProp_GProps
from OCP.BRepGProp import BRepGProp_Face, BRepGProp # used for mass calculation
from OCP.BRepPrimAPI import (
BRepPrimAPI_MakeBox,
BRepPrimAPI_MakeCone,
BRepPrimAPI_MakeCylinder,
BRepPrimAPI_MakeTorus,
BRepPrimAPI_MakeWedge,
BRepPrimAPI_MakePrism,
BRepPrimAPI_MakeRevol,
BRepPrimAPI_MakeSphere,
)
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
from OCP.TopExp import TopExp # Topology explorer
# used for getting underlying geometry -- is this equivalent to brep adaptor?
from OCP.BRep import BRep_Tool, BRep_Builder
from OCP.TopoDS import (
TopoDS,
TopoDS_Shape,
TopoDS_Builder,
TopoDS_Compound,
TopoDS_Iterator,
TopoDS_Wire,
TopoDS_Face,
TopoDS_Edge,
TopoDS_Vertex,
TopoDS_Solid,
TopoDS_Shell,
TopoDS_CompSolid,
)
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,
GeomAPI_PointsToBSpline,
GeomAPI_PointsToBSplineSurface,
)
from OCP.BRepFill import BRepFill
from OCP.BRepAlgoAPI import (
BRepAlgoAPI_Common,
BRepAlgoAPI_Fuse,
BRepAlgoAPI_Cut,
BRepAlgoAPI_BooleanOperation,
BRepAlgoAPI_Splitter,
)
from OCP.Geom import (
Geom_BezierCurve,
Geom_ConicalSurface,
Geom_CylindricalSurface,
Geom_Surface,
Geom_Plane,
)
from OCP.Geom2d import Geom2d_Line
from OCP.BRepLib import BRepLib, BRepLib_FindSurface
from OCP.BRepOffsetAPI import (
BRepOffsetAPI_ThruSections,
BRepOffsetAPI_MakePipeShell,
BRepOffsetAPI_MakeThickSolid,
BRepOffsetAPI_MakeOffset,
)
from OCP.BRepFilletAPI import (
BRepFilletAPI_MakeChamfer,
BRepFilletAPI_MakeFillet,
BRepFilletAPI_MakeFillet2d,
)
from OCP.TopTools import (
TopTools_IndexedDataMapOfShapeListOfShape,
TopTools_ListOfShape,
TopTools_MapOfShape,
TopTools_IndexedMapOfShape,
)
from OCP.ShapeFix import ShapeFix_Shape, ShapeFix_Solid, ShapeFix_Face
from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.StlAPI import StlAPI_Writer
from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
from OCP.LocOpe import LocOpe_DPrism
from OCP.BRepCheck import BRepCheck_Analyzer
from OCP.Font import (
Font_FontMgr,
Font_FA_Regular,
Font_FA_Italic,
Font_FA_Bold,
Font_SystemFont,
)
from OCP.StdPrs import StdPrs_BRepFont, StdPrs_BRepTextBuilder as Font_BRepTextBuilder
from OCP.Graphic3d import (
Graphic3d_HTA_LEFT,
Graphic3d_HTA_CENTER,
Graphic3d_HTA_RIGHT,
Graphic3d_VTA_BOTTOM,
Graphic3d_VTA_CENTER,
Graphic3d_VTA_TOP,
)
from OCP.NCollection import NCollection_Utf8String
from OCP.BRepFeat import BRepFeat_MakeDPrism
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.TCollection import TCollection_AsciiString
from OCP.TopLoc import TopLoc_Location
from OCP.GeomAbs import (
GeomAbs_Shape,
GeomAbs_C0,
GeomAbs_Intersection,
GeomAbs_JoinType,
)
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling
from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Mode
from OCP.BOPAlgo import BOPAlgo_GlueEnum
from OCP.IFSelect import IFSelect_ReturnStatus
from OCP.TopAbs import TopAbs_ShapeEnum, TopAbs_Orientation
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
from OCP.TopTools import TopTools_HSequenceOfShape
from OCP.GCPnts import GCPnts_AbscissaPoint
from OCP.GeomFill import (
GeomFill_Frenet,
GeomFill_CorrectedFrenet,
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
# for catching exceptions
from OCP.Standard import Standard_NoSuchObject, Standard_Failure
from OCP.Prs3d import Prs3d_IsoAspect
from OCP.Quantity import Quantity_Color
from OCP.Aspect import Aspect_TOL_SOLID
from OCP.Interface import Interface_Static
from OCP.ShapeCustom import ShapeCustom, ShapeCustom_RestrictionParameters
from OCP.BRepAlgo import BRepAlgo
from OCP.ChFi2d import ChFi2d_FilletAPI # For Wire.Fillet()
from math import pi, sqrt, inf, radians, cos
import warnings
from ..utils import deprecate
Real = Union[float, int]
TOLERANCE = 1e-6
HASH_CODE_MAX = 2147483647 # max 32bit signed int, required by OCC.Core.HashCode
shape_LUT = {
ta.TopAbs_VERTEX: "Vertex",
ta.TopAbs_EDGE: "Edge",
ta.TopAbs_WIRE: "Wire",
ta.TopAbs_FACE: "Face",
ta.TopAbs_SHELL: "Shell",
ta.TopAbs_SOLID: "Solid",
ta.TopAbs_COMPSOLID: "CompSolid",
ta.TopAbs_COMPOUND: "Compound",
}
shape_properties_LUT = {
ta.TopAbs_VERTEX: None,
ta.TopAbs_EDGE: BRepGProp.LinearProperties_s,
ta.TopAbs_WIRE: BRepGProp.LinearProperties_s,
ta.TopAbs_FACE: BRepGProp.SurfaceProperties_s,
ta.TopAbs_SHELL: BRepGProp.SurfaceProperties_s,
ta.TopAbs_SOLID: BRepGProp.VolumeProperties_s,
ta.TopAbs_COMPOUND: BRepGProp.VolumeProperties_s,
}
inverse_shape_LUT = {v: k for k, v in shape_LUT.items()}
downcast_LUT = {
ta.TopAbs_VERTEX: TopoDS.Vertex_s,
ta.TopAbs_EDGE: TopoDS.Edge_s,
ta.TopAbs_WIRE: TopoDS.Wire_s,
ta.TopAbs_FACE: TopoDS.Face_s,
ta.TopAbs_SHELL: TopoDS.Shell_s,
ta.TopAbs_SOLID: TopoDS.Solid_s,
ta.TopAbs_COMPSOLID: TopoDS.CompSolid_s,
ta.TopAbs_COMPOUND: TopoDS.Compound_s,
}
geom_LUT = {
ta.TopAbs_VERTEX: "Vertex",
ta.TopAbs_EDGE: BRepAdaptor_Curve,
ta.TopAbs_WIRE: "Wire",
ta.TopAbs_FACE: BRepAdaptor_Surface,
ta.TopAbs_SHELL: "Shell",
ta.TopAbs_SOLID: "Solid",
ta.TopAbs_SOLID: "CompSolid",
ta.TopAbs_COMPOUND: "Compound",
}
ancestors_LUT = {
"Vertex": ta.TopAbs_EDGE,
"Edge": ta.TopAbs_WIRE,
"Wire": ta.TopAbs_FACE,
"Face": ta.TopAbs_SHELL,
"Shell": ta.TopAbs_SOLID,
}
T = TypeVar("T", bound="Shape")
def shapetype(obj: TopoDS_Shape) -> TopAbs_ShapeEnum:
if obj.IsNull():
raise ValueError("Null TopoDS_Shape object")
return obj.ShapeType()
def downcast(obj: TopoDS_Shape) -> TopoDS_Shape:
"""
Downcasts a TopoDS object to suitable specialized type
"""
f_downcast: Any = downcast_LUT[shapetype(obj)]
rv = f_downcast(obj)
return rv
def fix(obj: TopoDS_Shape) -> TopoDS_Shape:
"""
Fix a TopoDS object to suitable specialized type
"""
sf = ShapeFix_Shape(obj)
sf.Perform()
return downcast(sf.Shape())
[docs]class Shape(object):
"""
Represents a shape in the system. Wraps TopoDS_Shape.
"""
wrapped: TopoDS_Shape
forConstruction: bool
[docs] def __init__(self, obj: TopoDS_Shape):
self.wrapped = downcast(obj)
self.forConstruction = False
# Helps identify this solid through the use of an ID
self.label = ""
[docs] def clean(self: T) -> T:
"""Experimental clean using ShapeUpgrade"""
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
upgrader.AllowInternalEdges(False)
upgrader.Build()
return self.__class__(upgrader.Shape())
[docs] def fix(self: T) -> T:
"""Try to fix shape if not valid"""
if not self.isValid():
fixed = fix(self.wrapped)
return self.__class__(fixed)
return self
[docs] @classmethod
def cast(cls, obj: TopoDS_Shape, forConstruction: bool = False) -> "Shape":
"Returns the right type of wrapper, given a OCCT object"
tr = None
# define the shape lookup table for casting
constructor_LUT = {
ta.TopAbs_VERTEX: Vertex,
ta.TopAbs_EDGE: Edge,
ta.TopAbs_WIRE: Wire,
ta.TopAbs_FACE: Face,
ta.TopAbs_SHELL: Shell,
ta.TopAbs_SOLID: Solid,
ta.TopAbs_COMPSOLID: CompSolid,
ta.TopAbs_COMPOUND: Compound,
}
t = shapetype(obj)
# NB downcast is needed to handle TopoDS_Shape types
tr = constructor_LUT[t](downcast(obj))
tr.forConstruction = forConstruction
return tr
[docs] def exportStl(
self,
fileName: str,
tolerance: float = 1e-3,
angularTolerance: float = 0.1,
ascii: bool = False,
relative: bool = True,
parallel: bool = True,
) -> bool:
"""
Exports a shape to a specified STL file.
:param fileName: The path and file name to write the STL output to.
:param tolerance: A linear deflection setting which limits the distance between a curve and its tessellation.
Setting this value too low will result in large meshes that can consume computing resources.
Setting the value too high can result in meshes with a level of detail that is too low.
Default is 1e-3, which is a good starting point for a range of cases.
:param angularTolerance: Angular deflection setting which limits the angle between subsequent segments in a polyline. Default is 0.1.
:param ascii: Export the file as ASCII (True) or binary (False) STL format. Default is binary.
:param relative: If True, tolerance will be scaled by the size of the edge being meshed. Default is True.
Setting this value to True may cause large features to become faceted, or small features dense.
:param parallel: If True, OCCT will use parallel processing to mesh the shape. Default is True.
"""
# The constructor used here automatically calls mesh.Perform(). https://dev.opencascade.org/doc/refman/html/class_b_rep_mesh___incremental_mesh.html#a3a383b3afe164161a3aa59a492180ac6
BRepMesh_IncrementalMesh(
self.wrapped, tolerance, relative, angularTolerance, parallel
)
writer = StlAPI_Writer()
writer.ASCIIMode = ascii
return writer.Write(self.wrapped, fileName)
[docs] def exportStep(self, fileName: str, **kwargs) -> IFSelect_ReturnStatus:
"""
Export this shape to a STEP file.
kwargs is used to provide optional keyword arguments to configure the exporter.
:param fileName: Path and filename for writing.
:param write_pcurves: Enable or disable writing parametric curves to the STEP file. Default True.
If False, writes STEP file without pcurves. This decreases the size of the resulting STEP file.
:type write_pcurves: bool
:param precision_mode: Controls the uncertainty value for STEP entities. Specify -1, 0, or 1. Default 0.
See OCCT documentation.
:type precision_mode: int
"""
# Handle the extra settings for the STEP export
pcurves = 1
if "write_pcurves" in kwargs and not kwargs["write_pcurves"]:
pcurves = 0
precision_mode = kwargs["precision_mode"] if "precision_mode" in kwargs else 0
writer = STEPControl_Writer()
Interface_Static.SetIVal_s("write.surfacecurve.mode", pcurves)
Interface_Static.SetIVal_s("write.precision.mode", precision_mode)
writer.Transfer(self.wrapped, STEPControl_AsIs)
return writer.Write(fileName)
[docs] def exportBrep(self, f: Union[str, BytesIO]) -> bool:
"""
Export this shape to a BREP file
"""
rv = BRepTools.Write_s(self.wrapped, f)
return True if rv is None else rv
[docs] @classmethod
def importBrep(cls, f: Union[str, BytesIO]) -> "Shape":
"""
Import shape from a BREP file
"""
s = TopoDS_Shape()
builder = BRep_Builder()
BRepTools.Read_s(s, f, builder)
if s.IsNull():
raise ValueError(f"Could not import {f}")
return cls.cast(s)
[docs] def geomType(self) -> Geoms:
"""
Gets the underlying geometry type.
Implementations can return any values desired, but the values the user
uses in type filters should correspond to these.
As an example, if a user does::
CQ(object).faces("%mytype")
The expectation is that the geomType attribute will return 'mytype'
The return values depend on the type of the shape:
| Vertex: always 'Vertex'
| Edge: LINE, CIRCLE, ELLIPSE, HYPERBOLA, PARABOLA, BEZIER,
| BSPLINE, OFFSET, OTHER
| Face: PLANE, CYLINDER, CONE, SPHERE, TORUS, BEZIER, BSPLINE,
| REVOLUTION, EXTRUSION, OFFSET, OTHER
| Solid: 'Solid'
| Shell: 'Shell'
| Compound: 'Compound'
| Wire: 'Wire'
:returns: A string according to the geometry type
"""
tr: Any = geom_LUT[shapetype(self.wrapped)]
if isinstance(tr, str):
rv = tr
elif tr is BRepAdaptor_Curve:
rv = geom_LUT_EDGE[tr(self.wrapped).GetType()]
else:
rv = geom_LUT_FACE[tr(self.wrapped).GetType()]
return tcast(Geoms, rv)
[docs] def hashCode(self) -> int:
"""
Returns a hashed value denoting this shape. It is computed from the
TShape and the Location. The Orientation is not used.
"""
return self.wrapped.HashCode(HASH_CODE_MAX)
[docs] def isNull(self) -> bool:
"""
Returns true if this shape is null. In other words, it references no
underlying shape with the potential to be given a location and an
orientation.
"""
return self.wrapped.IsNull()
[docs] def isSame(self, other: "Shape") -> bool:
"""
Returns True if other and this shape are same, i.e. if they share the
same TShape with the same Locations. Orientations may differ. Also see
:py:meth:`isEqual`
"""
return self.wrapped.IsSame(other.wrapped)
[docs] def isEqual(self, other: "Shape") -> bool:
"""
Returns True if two shapes are equal, i.e. if they share the same
TShape with the same Locations and Orientations. Also see
:py:meth:`isSame`.
"""
return self.wrapped.IsEqual(other.wrapped)
[docs] def isValid(self) -> bool:
"""
Returns True if no defect is detected on the shape S or any of its
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked.
"""
return BRepCheck_Analyzer(self.wrapped).IsValid()
[docs] def BoundingBox(
self, tolerance: Optional[float] = None
) -> BoundBox: # need to implement that in GEOM
"""
Create a bounding box for this Shape.
:param tolerance: Tolerance value passed to :class:`BoundBox`
:returns: A :class:`BoundBox` object for this Shape
"""
return BoundBox._fromTopoDS(self.wrapped, tol=tolerance)
[docs] def mirror(
self,
mirrorPlane: Union[
Literal["XY", "YX", "XZ", "ZX", "YZ", "ZY"], VectorLike
] = "XY",
basePointVector: VectorLike = (0, 0, 0),
) -> "Shape":
"""
Applies a mirror transform to this Shape. Does not duplicate objects
about the plane.
:param mirrorPlane: The direction of the plane to mirror about - one of
'XY', 'XZ' or 'YZ'
:param basePointVector: The origin of the plane to mirror about
:returns: The mirrored shape
"""
if isinstance(mirrorPlane, str):
if mirrorPlane == "XY" or mirrorPlane == "YX":
mirrorPlaneNormalVector = gp_Dir(0, 0, 1)
elif mirrorPlane == "XZ" or mirrorPlane == "ZX":
mirrorPlaneNormalVector = gp_Dir(0, 1, 0)
elif mirrorPlane == "YZ" or mirrorPlane == "ZY":
mirrorPlaneNormalVector = gp_Dir(1, 0, 0)
else:
if isinstance(mirrorPlane, tuple):
mirrorPlaneNormalVector = gp_Dir(*mirrorPlane)
elif isinstance(mirrorPlane, Vector):
mirrorPlaneNormalVector = mirrorPlane.toDir()
if isinstance(basePointVector, tuple):
basePointVector = Vector(basePointVector)
T = gp_Trsf()
T.SetMirror(gp_Ax2(gp_Pnt(*basePointVector.toTuple()), mirrorPlaneNormalVector))
return self._apply_transform(T)
@staticmethod
def _center_of_mass(shape: "Shape") -> Vector:
Properties = GProp_GProps()
BRepGProp.VolumeProperties_s(shape.wrapped, Properties)
return Vector(Properties.CentreOfMass())
[docs] @staticmethod
def matrixOfInertia(obj: "Shape") -> List[List[float]]:
"""
Calculates the matrix of inertia of an object.
Since the part's density is unknown, this result is inertia/density with units of [1/length].
:param obj: Compute the matrix of inertia of this object
"""
Properties = GProp_GProps()
calc_function = shape_properties_LUT[shapetype(obj.wrapped)]
if calc_function:
calc_function(obj.wrapped, Properties)
moi = Properties.MatrixOfInertia()
return [[moi.Value(i, j) for j in range(1, 4)] for i in range(1, 4)]
raise NotImplementedError
[docs] def Center(self) -> Vector:
"""
:returns: The point of the center of mass of this Shape
"""
return Shape.centerOfMass(self)
[docs] def CenterOfBoundBox(self, tolerance: Optional[float] = None) -> Vector:
"""
:param tolerance: Tolerance passed to the :py:meth:`BoundingBox` method
:returns: Center of the bounding box of this shape
"""
return self.BoundingBox(tolerance=tolerance).center
[docs] @staticmethod
def CombinedCenter(objects: Iterable["Shape"]) -> Vector:
"""
Calculates the center of mass of multiple objects.
:param objects: A list of objects with mass
"""
total_mass = sum(Shape.computeMass(o) for o in objects)
weighted_centers = [
Shape.centerOfMass(o).multiply(Shape.computeMass(o)) for o in objects
]
sum_wc = weighted_centers[0]
for wc in weighted_centers[1:]:
sum_wc = sum_wc.add(wc)
return Vector(sum_wc.multiply(1.0 / total_mass))
[docs] @staticmethod
def computeMass(obj: "Shape") -> float:
"""
Calculates the 'mass' of an object.
:param obj: Compute the mass of this object
"""
Properties = GProp_GProps()
calc_function = shape_properties_LUT[shapetype(obj.wrapped)]
if calc_function:
calc_function(obj.wrapped, Properties)
return Properties.Mass()
else:
raise NotImplementedError
[docs] @staticmethod
def centerOfMass(obj: "Shape") -> Vector:
"""
Calculates the center of 'mass' of an object.
:param obj: Compute the center of mass of this object
"""
Properties = GProp_GProps()
calc_function = shape_properties_LUT[shapetype(obj.wrapped)]
if calc_function:
calc_function(obj.wrapped, Properties)
return Vector(Properties.CentreOfMass())
else:
raise NotImplementedError
[docs] @staticmethod
def CombinedCenterOfBoundBox(objects: List["Shape"]) -> Vector:
"""
Calculates the center of a bounding box of multiple objects.
:param objects: A list of objects
"""
total_mass = len(objects)
weighted_centers = []
for o in objects:
weighted_centers.append(BoundBox._fromTopoDS(o.wrapped).center)
sum_wc = weighted_centers[0]
for wc in weighted_centers[1:]:
sum_wc = sum_wc.add(wc)
return Vector(sum_wc.multiply(1.0 / total_mass))
[docs] def Closed(self) -> bool:
"""
:returns: The closedness flag
"""
return self.wrapped.Closed()
def ShapeType(self) -> Shapes:
return tcast(Shapes, shape_LUT[shapetype(self.wrapped)])
def _entities(self, topo_type: Shapes) -> Iterable[TopoDS_Shape]:
shape_set = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(self.wrapped, inverse_shape_LUT[topo_type], shape_set)
return tcast(Iterable[TopoDS_Shape], shape_set)
def _entitiesFrom(
self, child_type: Shapes, parent_type: Shapes
) -> Dict["Shape", List["Shape"]]:
res = TopTools_IndexedDataMapOfShapeListOfShape()
TopExp.MapShapesAndAncestors_s(
self.wrapped,
inverse_shape_LUT[child_type],
inverse_shape_LUT[parent_type],
res,
)
out: Dict[Shape, List[Shape]] = {}
for i in range(1, res.Extent() + 1):
out[Shape.cast(res.FindKey(i))] = [
Shape.cast(el) for el in res.FindFromIndex(i)
]
return out
[docs] def Vertices(self) -> List["Vertex"]:
"""
:returns: All the vertices in this Shape
"""
return [Vertex(i) for i in self._entities("Vertex")]
[docs] def Edges(self) -> List["Edge"]:
"""
:returns: All the edges in this Shape
"""
return [
Edge(i)
for i in self._entities("Edge")
if not BRep_Tool.Degenerated_s(TopoDS.Edge_s(i))
]
[docs] def Compounds(self) -> List["Compound"]:
"""
:returns: All the compounds in this Shape
"""
return [Compound(i) for i in self._entities("Compound")]
[docs] def Wires(self) -> List["Wire"]:
"""
:returns: All the wires in this Shape
"""
return [Wire(i) for i in self._entities("Wire")]
[docs] def Faces(self) -> List["Face"]:
"""
:returns: All the faces in this Shape
"""
return [Face(i) for i in self._entities("Face")]
[docs] def Shells(self) -> List["Shell"]:
"""
:returns: All the shells in this Shape
"""
return [Shell(i) for i in self._entities("Shell")]
[docs] def Solids(self) -> List["Solid"]:
"""
:returns: All the solids in this Shape
"""
return [Solid(i) for i in self._entities("Solid")]
[docs] def CompSolids(self) -> List["CompSolid"]:
"""
:returns: All the compsolids in this Shape
"""
return [CompSolid(i) for i in self._entities("CompSolid")]
def _filter(
self, selector: Optional[Union[Selector, str]], objs: Iterable["Shape"]
) -> "Shape":
selectorObj: Selector
if selector:
if isinstance(selector, str):
selectorObj = StringSyntaxSelector(selector)
else:
selectorObj = selector
selected = selectorObj.filter(list(objs))
else:
selected = list(objs)
if len(selected) == 1:
rv = selected[0]
else:
rv = Compound.makeCompound(selected)
return rv
[docs] def vertices(self, selector: Optional[Union[Selector, str]] = None) -> "Shape":
"""
Select vertices.
"""
return self._filter(selector, map(Shape.cast, self._entities("Vertex")))
[docs] def edges(self, selector: Optional[Union[Selector, str]] = None) -> "Shape":
"""
Select edges.
"""
return self._filter(selector, map(Shape.cast, self._entities("Edge")))
[docs] def wires(self, selector: Optional[Union[Selector, str]] = None) -> "Shape":
"""
Select wires.
"""
return self._filter(selector, map(Shape.cast, self._entities("Wire")))
[docs] def faces(self, selector: Optional[Union[Selector, str]] = None) -> "Shape":
"""
Select faces.
"""
return self._filter(selector, map(Shape.cast, self._entities("Face")))
[docs] def shells(self, selector: Optional[Union[Selector, str]] = None) -> "Shape":
"""
Select shells.
"""
return self._filter(selector, map(Shape.cast, self._entities("Shell")))
[docs] def solids(self, selector: Optional[Union[Selector, str]] = None) -> "Shape":
"""
Select solids.
"""
return self._filter(selector, map(Shape.cast, self._entities("Solid")))
[docs] def Area(self) -> float:
"""
:returns: The surface area of all faces in this Shape
"""
Properties = GProp_GProps()
BRepGProp.SurfaceProperties_s(self.wrapped, Properties)
return Properties.Mass()
[docs] def Volume(self) -> float:
"""
:returns: The volume of this Shape
"""
# when density == 1, mass == volume
return Shape.computeMass(self)
def _apply_transform(self: T, Tr: gp_Trsf) -> T:
return self.__class__(BRepBuilderAPI_Transform(self.wrapped, Tr, True).Shape())
[docs] def rotate(
self: T, startVector: VectorLike, endVector: VectorLike, angleDegrees: float
) -> T:
"""
Rotates a shape around an axis.
:param startVector: start point of rotation axis
:type startVector: either a 3-tuple or a Vector
:param endVector: end point of rotation axis
:type endVector: either a 3-tuple or a Vector
:param angleDegrees: angle to rotate, in degrees
:returns: a copy of the shape, rotated
"""
if type(startVector) == tuple:
startVector = Vector(startVector)
if type(endVector) == tuple:
endVector = Vector(endVector)
Tr = gp_Trsf()
Tr.SetRotation(
gp_Ax1(
Vector(startVector).toPnt(),
(Vector(endVector) - Vector(startVector)).toDir(),
),
radians(angleDegrees),
)
return self._apply_transform(Tr)
[docs] def translate(self: T, vector: VectorLike) -> T:
"""
Translates this shape through a transformation.
"""
T = gp_Trsf()
T.SetTranslation(Vector(vector).wrapped)
return self._apply_transform(T)
[docs] def scale(self, factor: float) -> "Shape":
"""
Scales this shape through a transformation.
"""
T = gp_Trsf()
T.SetScale(gp_Pnt(), factor)
return self._apply_transform(T)
[docs] def copy(self: T, mesh: bool = False) -> T:
"""
Creates a new object that is a copy of this object.
:param mesh: should I copy the triangulation too (default: False)
:returns: a copy of the object
"""
return self.__class__(BRepBuilderAPI_Copy(self.wrapped, True, mesh).Shape())
[docs] def location(self) -> Location:
"""
Return the current location
"""
return Location(self.wrapped.Location())
[docs] def locate(self: T, loc: Location) -> T:
"""
Apply a location in absolute sense to self
"""
self.wrapped.Location(loc.wrapped)
return self
[docs] def located(self: T, loc: Location) -> T:
"""
Apply a location in absolute sense to a copy of self
"""
r = self.__class__(self.wrapped.Located(loc.wrapped))
r.forConstruction = self.forConstruction
return r
[docs] def move(self: T, loc: Location) -> T:
"""
Apply a location in relative sense (i.e. update current location) to self
"""
self.wrapped.Move(loc.wrapped)
return self
[docs] def moved(self: T, loc: Location) -> T:
"""
Apply a location in relative sense (i.e. update current location) to a copy of self
"""
r = self.__class__(self.wrapped.Moved(loc.wrapped))
r.forConstruction = self.forConstruction
return r
[docs] def __hash__(self) -> int:
return self.hashCode()
[docs] def __eq__(self, other) -> bool:
return self.isSame(other) if isinstance(other, Shape) else False
def _bool_op(
self,
args: Iterable["Shape"],
tools: Iterable["Shape"],
op: Union[BRepAlgoAPI_BooleanOperation, BRepAlgoAPI_Splitter],
parallel: bool = True,
) -> "Shape":
"""
Generic boolean operation
:param parallel: Sets the SetRunParallel flag, which enables parallel execution of boolean operations in OCC kernel
"""
arg = TopTools_ListOfShape()
for obj in args:
arg.Append(obj.wrapped)
tool = TopTools_ListOfShape()
for obj in tools:
tool.Append(obj.wrapped)
op.SetArguments(arg)
op.SetTools(tool)
op.SetRunParallel(parallel)
op.Build()
return Shape.cast(op.Shape())
[docs] def cut(self, *toCut: "Shape", tol: Optional[float] = None) -> "Shape":
"""
Remove the positional arguments from this Shape.
:param tol: Fuzzy mode tolerance
"""
cut_op = BRepAlgoAPI_Cut()
if tol:
cut_op.SetFuzzyValue(tol)
return self._bool_op((self,), toCut, cut_op)
[docs] def fuse(
self, *toFuse: "Shape", glue: bool = False, tol: Optional[float] = None
) -> "Shape":
"""
Fuse the positional arguments with this Shape.
:param glue: Sets the glue option for the algorithm, which allows
increasing performance of the intersection of the input shapes
:param tol: Fuzzy mode tolerance
"""
fuse_op = BRepAlgoAPI_Fuse()
if glue:
fuse_op.SetGlue(BOPAlgo_GlueEnum.BOPAlgo_GlueShift)
if tol:
fuse_op.SetFuzzyValue(tol)
rv = self._bool_op((self,), toFuse, fuse_op)
return rv
[docs] def intersect(self, *toIntersect: "Shape", tol: Optional[float] = None) -> "Shape":
"""
Intersection of the positional arguments and this Shape.
:param tol: Fuzzy mode tolerance
"""
intersect_op = BRepAlgoAPI_Common()
if tol:
intersect_op.SetFuzzyValue(tol)
return self._bool_op((self,), toIntersect, intersect_op)
[docs] 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
:param point: Base point for defining a line
:param axis: Axis on which the line rests
:param tol: Intersection tolerance
:param 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]
[docs] def split(self, *splitters: "Shape") -> "Shape":
"""
Split this shape with the positional arguments.
"""
split_op = BRepAlgoAPI_Splitter()
return self._bool_op((self,), splitters, split_op)
[docs] def distance(self, other: "Shape") -> float:
"""
Minimal distance between two shapes
"""
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
[docs] 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()
[docs] def mesh(self, tolerance: float, angularTolerance: float = 0.1):
"""
Generate triangulation if none exists.
"""
if not BRepTools.Triangulation_s(self.wrapped, tolerance):
BRepMesh_IncrementalMesh(self.wrapped, tolerance, True, angularTolerance)
def tessellate(
self, tolerance: float, angularTolerance: float = 0.1
) -> Tuple[List[Vector], List[Tuple[int, int, int]]]:
self.mesh(tolerance, angularTolerance)
vertices: List[Vector] = []
triangles: List[Tuple[int, int, int]] = []
offset = 0
for f in self.Faces():
loc = TopLoc_Location()
poly = BRep_Tool.Triangulation_s(f.wrapped, loc)
Trsf = loc.Transformation()
reverse = (
True
if f.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
else False
)
# add vertices
vertices += [
Vector(v.X(), v.Y(), v.Z())
for v in (
poly.Node(i).Transformed(Trsf) for i in range(1, poly.NbNodes() + 1)
)
]
# add triangles
triangles += [
(
t.Value(1) + offset - 1,
t.Value(3) + offset - 1,
t.Value(2) + offset - 1,
)
if reverse
else (
t.Value(1) + offset - 1,
t.Value(2) + offset - 1,
t.Value(3) + offset - 1,
)
for t in poly.Triangles()
]
offset += poly.NbNodes()
return vertices, triangles
[docs] def toSplines(
self: T, degree: int = 3, tolerance: float = 1e-3, nurbs: bool = False
) -> T:
"""
Approximate shape with b-splines of the specified degree.
:param degree: Maximum degree.
:param tolerance: Approximation tolerance.
:param nurbs: Use rational splines.
"""
params = ShapeCustom_RestrictionParameters()
result = ShapeCustom.BSplineRestriction_s(
self.wrapped,
tolerance, # 3D tolerance
tolerance, # 2D tolerance
degree,
1, # dumy value, degree is leading
ga.GeomAbs_C0,
ga.GeomAbs_C0,
True, # set degree to be leading
not nurbs,
params,
)
return self.__class__(result)
[docs] def toVtkPolyData(
self,
tolerance: Optional[float] = None,
angularTolerance: Optional[float] = None,
normals: bool = False,
) -> vtkPolyData:
"""
Convert shape to vtkPolyData
"""
vtk_shape = IVtkOCC_Shape(self.wrapped)
shape_data = IVtkVTK_ShapeData()
shape_mesher = IVtkOCC_ShapeMesher()
drawer = vtk_shape.Attributes()
drawer.SetUIsoAspect(Prs3d_IsoAspect(Quantity_Color(), Aspect_TOL_SOLID, 1, 0))
drawer.SetVIsoAspect(Prs3d_IsoAspect(Quantity_Color(), Aspect_TOL_SOLID, 1, 0))
if tolerance:
drawer.SetDeviationCoefficient(tolerance)
if angularTolerance:
drawer.SetDeviationAngle(angularTolerance)
shape_mesher.Build(vtk_shape, shape_data)
rv = shape_data.getVtkPolyData()
# convert to triangles and split edges
t_filter = vtkTriangleFilter()
t_filter.SetInputData(rv)
t_filter.Update()
rv = t_filter.GetOutput()
# compute normals
if normals:
n_filter = vtkPolyDataNormals()
n_filter.SetComputePointNormals(True)
n_filter.SetComputeCellNormals(True)
n_filter.SetFeatureAngle(360)
n_filter.SetInputData(rv)
n_filter.Update()
rv = n_filter.GetOutput()
return rv
def _repr_javascript_(self):
"""
Jupyter 3D representation support
"""
from .jupyter_tools import display
return display(self)._repr_javascript_()
[docs] def __iter__(self) -> Iterator["Shape"]:
"""
Iterate over subshapes.
"""
it = TopoDS_Iterator(self.wrapped)
while it.More():
yield Shape.cast(it.Value())
it.Next()
[docs] def ancestors(self, shape: "Shape", kind: Shapes) -> "Compound":
"""
Iterate over ancestors, i.e. shapes of same kind within shape that contain self.
"""
shape_map = TopTools_IndexedDataMapOfShapeListOfShape()
TopExp.MapShapesAndAncestors_s(
shape.wrapped, shapetype(self.wrapped), inverse_shape_LUT[kind], shape_map
)
return Compound.makeCompound(
Shape.cast(s) for s in shape_map.FindFromKey(self.wrapped)
)
[docs] def siblings(self, shape: "Shape", kind: Shapes, level: int = 1) -> "Compound":
"""
Iterate over siblings, i.e. shapes within shape that share subshapes of kind with self.
"""
shape_map = TopTools_IndexedDataMapOfShapeListOfShape()
TopExp.MapShapesAndAncestors_s(
shape.wrapped, inverse_shape_LUT[kind], shapetype(self.wrapped), shape_map,
)
exclude = TopTools_MapOfShape()
def _siblings(shapes, level):
rv = set()
for s in shapes:
exclude.Add(s.wrapped)
for s in shapes:
rv.update(
Shape.cast(el)
for child in s._entities(kind)
for el in shape_map.FindFromKey(child)
if not exclude.Contains(el)
)
return rv if level == 1 else _siblings(rv, level - 1)
return Compound.makeCompound(_siblings([self], level))
class ShapeProtocol(Protocol):
@property
def wrapped(self) -> TopoDS_Shape:
...
def __init__(self, wrapped: TopoDS_Shape) -> None:
...
def Faces(self) -> List["Face"]:
...
def geomType(self) -> Geoms:
...
[docs]class Vertex(Shape):
"""
A Single Point in Space
"""
wrapped: TopoDS_Vertex
[docs] def __init__(self, obj: TopoDS_Shape, forConstruction: bool = False):
"""
Create a vertex
"""
super(Vertex, self).__init__(obj)
self.forConstruction = forConstruction
self.X, self.Y, self.Z = self.toTuple()
def toTuple(self) -> Tuple[float, float, float]:
geom_point = BRep_Tool.Pnt_s(self.wrapped)
return (geom_point.X(), geom_point.Y(), geom_point.Z())
[docs] def Center(self) -> Vector:
"""
The center of a vertex is itself!
"""
return Vector(self.toTuple())
@classmethod
def makeVertex(cls, x: float, y: float, z: float) -> "Vertex":
return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)).Vertex())
class Mixin1DProtocol(ShapeProtocol, Protocol):
def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]:
...
def paramAt(self, d: float) -> float:
...
def positionAt(
self, d: float, mode: Literal["length", "parameter"] = "length",
) -> Vector:
...
def locationAt(
self,
d: float,
mode: Literal["length", "parameter"] = "length",
frame: Literal["frenet", "corrected"] = "frenet",
planar: bool = False,
) -> Location:
...
T1D = TypeVar("T1D", bound=Mixin1DProtocol)
[docs]class Mixin1D(object):
def _bounds(self: Mixin1DProtocol) -> Tuple[float, float]:
curve = self._geomAdaptor()
return curve.FirstParameter(), curve.LastParameter()
[docs] def startPoint(self: Mixin1DProtocol) -> Vector:
"""
:return: a vector representing the start point of this edge
Note, circles may have the start and end points the same
"""
curve = self._geomAdaptor()
umin = curve.FirstParameter()
return Vector(curve.Value(umin))
[docs] def endPoint(self: Mixin1DProtocol) -> Vector:
"""
:return: a vector representing the end point of this edge.
Note, circles may have the start and end points the same
"""
curve = self._geomAdaptor()
umax = curve.LastParameter()
return Vector(curve.Value(umax))
[docs] def paramAt(self: Mixin1DProtocol, d: float) -> float:
"""
Compute parameter value at the specified normalized distance.
:param d: normalized distance [0, 1]
:return: parameter value
"""
curve = self._geomAdaptor()
l = GCPnts_AbscissaPoint.Length_s(curve)
return GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter()
[docs] def tangentAt(
self: Mixin1DProtocol,
locationParam: float = 0.5,
mode: Literal["length", "parameter"] = "length",
) -> Vector:
"""
Compute tangent vector at the specified location.
:param locationParam: distance or parameter value (default: 0.5)
:param mode: position calculation mode (default: parameter)
:return: tangent vector
"""
curve = self._geomAdaptor()
tmp = gp_Pnt()
res = gp_Vec()
if mode == "length":
param = self.paramAt(locationParam)
else:
param = locationParam
curve.D1(param, tmp, res)
return Vector(gp_Dir(res))
[docs] def normal(self: Mixin1DProtocol) -> Vector:
"""
Calculate the normal Vector. Only possible for planar curves.
:return: normal vector
"""
curve = self._geomAdaptor()
gtype = self.geomType()
if gtype == "CIRCLE":
circ = curve.Circle()
rv = Vector(circ.Axis().Direction())
elif gtype == "ELLIPSE":
ell = curve.Ellipse()
rv = Vector(ell.Axis().Direction())
else:
fs = BRepLib_FindSurface(self.wrapped, OnlyPlane=True)
surf = fs.Surface()
if isinstance(surf, Geom_Plane):
pln = surf.Pln()
rv = Vector(pln.Axis().Direction())
else:
raise ValueError("Normal not defined")
return rv
def Center(self: Mixin1DProtocol) -> Vector:
Properties = GProp_GProps()
BRepGProp.LinearProperties_s(self.wrapped, Properties)
return Vector(Properties.CentreOfMass())
def Length(self: Mixin1DProtocol) -> float:
return GCPnts_AbscissaPoint.Length_s(self._geomAdaptor())
[docs] def radius(self: Mixin1DProtocol) -> float:
"""
Calculate the radius.
Note that when applied to a Wire, the radius is simply the radius of the first edge.
:return: radius
:raises ValueError: if kernel can not reduce the shape to a circular edge
"""
geom = self._geomAdaptor()
try:
circ = geom.Circle()
except (Standard_NoSuchObject, Standard_Failure) as e:
raise ValueError("Shape could not be reduced to a circle") from e
return circ.Radius()
def IsClosed(self: Mixin1DProtocol) -> bool:
return BRep_Tool.IsClosed_s(self.wrapped)
[docs] def positionAt(
self: Mixin1DProtocol,
d: float,
mode: Literal["length", "parameter"] = "length",
) -> Vector:
"""Generate a position along the underlying curve.
:param d: distance or parameter value
:param mode: position calculation mode (default: length)
:return: A Vector on the underlying curve located at the specified d value.
"""
curve = self._geomAdaptor()
if mode == "length":
param = self.paramAt(d)
else:
param = d
return Vector(curve.Value(param))
[docs] def positions(
self: Mixin1DProtocol,
ds: Iterable[float],
mode: Literal["length", "parameter"] = "length",
) -> List[Vector]:
"""Generate positions along the underlying curve
:param ds: distance or parameter values
:param mode: position calculation mode (default: length)
:return: A list of Vector objects.
"""
return [self.positionAt(d, mode) for d in ds]
[docs] def locationAt(
self: Mixin1DProtocol,
d: float,
mode: Literal["length", "parameter"] = "length",
frame: Literal["frenet", "corrected"] = "frenet",
planar: bool = False,
) -> Location:
"""Generate a location along the underlying curve.
:param d: distance or parameter value
:param mode: position calculation mode (default: length)
:param frame: moving frame calculation method (default: frenet)
:param planar: planar mode
:return: A Location object representing local coordinate system at the specified distance.
"""
curve = self._geomAdaptor()
if mode == "length":
param = self.paramAt(d)
else:
param = d
law: GeomFill_TrihedronLaw
if frame == "frenet":
law = GeomFill_Frenet()
else:
law = GeomFill_CorrectedFrenet()
law.SetCurve(curve)
tangent, normal, binormal = gp_Vec(), gp_Vec(), gp_Vec()
law.D0(param, tangent, normal, binormal)
pnt = curve.Value(param)
T = gp_Trsf()
if planar:
T.SetTransformation(
gp_Ax3(pnt, gp_Dir(0, 0, 1), gp_Dir(normal.XYZ())), gp_Ax3()
)
else:
T.SetTransformation(
gp_Ax3(pnt, gp_Dir(tangent.XYZ()), gp_Dir(normal.XYZ())), gp_Ax3()
)
return Location(TopLoc_Location(T))
[docs] def locations(
self: Mixin1DProtocol,
ds: Iterable[float],
mode: Literal["length", "parameter"] = "length",
frame: Literal["frenet", "corrected"] = "frenet",
planar: bool = False,
) -> List[Location]:
"""Generate location along the curve
:param ds: distance or parameter values
:param mode: position calculation mode (default: length)
:param frame: moving frame calculation method (default: frenet)
:param planar: planar mode
:return: A list of Location objects representing local coordinate systems at the specified distances.
"""
return [self.locationAt(d, mode, frame, planar) for d in ds]
[docs] 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
[docs]class Edge(Shape, Mixin1D):
"""
A trimmed curve that represents the border of a face
"""
wrapped: TopoDS_Edge
def _geomAdaptor(self) -> BRepAdaptor_Curve:
"""
Return the underlying geometry
"""
return BRepAdaptor_Curve(self.wrapped)
[docs] def close(self) -> Union["Edge", "Wire"]:
"""
Close an Edge
"""
rv: Union[Wire, Edge]
if not self.IsClosed():
rv = Wire.assembleEdges((self,)).close()
else:
rv = self
return rv
[docs] def arcCenter(self) -> Vector:
"""
Center of an underlying circle or ellipse geometry.
"""
g = self.geomType()
a = self._geomAdaptor()
if g == "CIRCLE":
rv = Vector(a.Circle().Position().Location())
elif g == "ELLIPSE":
rv = Vector(a.Ellipse().Position().Location())
else:
raise ValueError(f"{g} has no arc center")
return rv
@classmethod
def makeCircle(
cls,
radius: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angle1: float = 360.0,
angle2: float = 360,
orientation=True,
) -> "Edge":
pnt = Vector(pnt)
dir = Vector(dir)
circle_gp = gp_Circ(gp_Ax2(pnt.toPnt(), dir.toDir()), radius)
if angle1 == angle2: # full circle case
return cls(BRepBuilderAPI_MakeEdge(circle_gp).Edge())
else: # arc case
circle_geom = GC_MakeArcOfCircle(
circle_gp, radians(angle1), radians(angle2), orientation
).Value()
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
[docs] @classmethod
def makeEllipse(
cls,
x_radius: float,
y_radius: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
xdir: VectorLike = Vector(1, 0, 0),
angle1: float = 360.0,
angle2: float = 360.0,
sense: Literal[-1, 1] = 1,
) -> "Edge":
"""
Makes an Ellipse centered at the provided point, having normal in the provided direction.
:param cls:
:param x_radius: x radius of the ellipse (along the x-axis of plane the ellipse should lie in)
:param y_radius: y radius of the ellipse (along the y-axis of plane the ellipse should lie in)
:param pnt: vector representing the center of the ellipse
:param dir: vector representing the direction of the plane the ellipse should lie in
:param angle1: start angle of arc
:param angle2: end angle of arc (angle2 == angle1 return closed ellipse = default)
:param sense: clockwise (-1) or counter clockwise (1)
:return: an Edge
"""
pnt_p = Vector(pnt).toPnt()
dir_d = Vector(dir).toDir()
xdir_d = Vector(xdir).toDir()
ax1 = gp_Ax1(pnt_p, dir_d)
ax2 = gp_Ax2(pnt_p, dir_d, xdir_d)
if y_radius > x_radius:
# swap x and y radius and rotate by 90° afterwards to create an ellipse with x_radius < y_radius
correction_angle = radians(90.0)
ellipse_gp = gp_Elips(ax2, y_radius, x_radius).Rotated(
ax1, correction_angle
)
else:
correction_angle = 0.0
ellipse_gp = gp_Elips(ax2, x_radius, y_radius)
if angle1 == angle2: # full ellipse case
ellipse = cls(BRepBuilderAPI_MakeEdge(ellipse_gp).Edge())
else: # arc case
# take correction_angle into account
ellipse_geom = GC_MakeArcOfEllipse(
ellipse_gp,
radians(angle1) - correction_angle,
radians(angle2) - correction_angle,
sense == 1,
).Value()
ellipse = cls(BRepBuilderAPI_MakeEdge(ellipse_geom).Edge())
return ellipse
[docs] @classmethod
def makeSpline(
cls,
listOfVector: List[Vector],
tangents: Optional[Sequence[Vector]] = None,
periodic: bool = False,
parameters: Optional[Sequence[float]] = None,
scale: bool = True,
tol: float = 1e-6,
) -> "Edge":
"""
Interpolate a spline through the provided points.
:param listOfVector: a list of Vectors that represent the points
:param tangents: tuple of Vectors specifying start and finish tangent
:param periodic: creation of periodic curves
:param parameters: the value of the parameter at each interpolation point. (The interpolated
curve is represented as a vector-valued function of a scalar parameter.) If periodic ==
True, then len(parameters) must be len(intepolation points) + 1, otherwise len(parameters)
must be equal to len(interpolation points).
:param scale: whether to scale the specified tangent vectors before interpolating. Each
tangent is scaled, so it's length is equal to the derivative of the Lagrange interpolated
curve. I.e., set this to True, if you want to use only the direction of the tangent
vectors specified by ``tangents``, but not their magnitude.
:param tol: tolerance of the algorithm (consult OCC documentation). Used to check that the
specified points are not too close to each other, and that tangent vectors are not too
short. (In either case interpolation may fail.)
:return: an Edge
"""
pnts = TColgp_HArray1OfPnt(1, len(listOfVector))
for ix, v in enumerate(listOfVector):
pnts.SetValue(ix + 1, v.toPnt())
if parameters is None:
spline_builder = GeomAPI_Interpolate(pnts, periodic, tol)
else:
if len(parameters) != (len(listOfVector) + periodic):
raise ValueError(
"There must be one parameter for each interpolation point "
"(plus one if periodic), or none specified. Parameter count: "
f"{len(parameters)}, point count: {len(listOfVector)}"
)
parameters_array = TColStd_HArray1OfReal(1, len(parameters))
for p_index, p_value in enumerate(parameters):
parameters_array.SetValue(p_index + 1, p_value)
spline_builder = GeomAPI_Interpolate(pnts, parameters_array, periodic, tol)
if tangents:
if len(tangents) == 2 and len(listOfVector) != 2:
# Specify only initial and final tangent:
t1, t2 = tangents
spline_builder.Load(t1.wrapped, t2.wrapped, scale)
else:
if len(tangents) != len(listOfVector):
raise ValueError(
f"There must be one tangent for each interpolation point, "
f"or just two end point tangents. Tangent count: "
f"{len(tangents)}, point count: {len(listOfVector)}"
)
# Specify a tangent for each interpolation point:
tangents_array = TColgp_Array1OfVec(1, len(tangents))
tangent_enabled_array = TColStd_HArray1OfBoolean(1, len(tangents))
for t_index, t_value in enumerate(tangents):
tangent_enabled_array.SetValue(t_index + 1, t_value is not None)
tangent_vec = t_value if t_value is not None else Vector()
tangents_array.SetValue(t_index + 1, tangent_vec.wrapped)
spline_builder.Load(tangents_array, tangent_enabled_array, scale)
spline_builder.Perform()
if not spline_builder.IsDone():
raise ValueError("B-spline interpolation failed")
spline_geom = spline_builder.Curve()
return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge())
[docs] @classmethod
def makeSplineApprox(
cls,
listOfVector: List[Vector],
tol: float = 1e-3,
smoothing: Optional[Tuple[float, float, float]] = None,
minDeg: int = 1,
maxDeg: int = 6,
) -> "Edge":
"""
Approximate a spline through the provided points.
:param listOfVector: a list of Vectors that represent the points
:param tol: tolerance of the algorithm (consult OCC documentation).
:param smoothing: optional tuple of 3 weights use for variational smoothing (default: None)
:param minDeg: minimum spline degree. Enforced only when smothing is None (default: 1)
:param maxDeg: maximum spline degree (default: 6)
:return: an Edge
"""
pnts = TColgp_HArray1OfPnt(1, len(listOfVector))
for ix, v in enumerate(listOfVector):
pnts.SetValue(ix + 1, v.toPnt())
if smoothing:
spline_builder = GeomAPI_PointsToBSpline(
pnts, *smoothing, DegMax=maxDeg, Tol3D=tol
)
else:
spline_builder = GeomAPI_PointsToBSpline(
pnts, DegMin=minDeg, DegMax=maxDeg, Tol3D=tol
)
if not spline_builder.IsDone():
raise ValueError("B-spline approximation failed")
spline_geom = spline_builder.Curve()
return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge())
[docs] @classmethod
def makeThreePointArc(
cls, v1: VectorLike, v2: VectorLike, v3: VectorLike
) -> "Edge":
"""
Makes a three point arc through the provided points
:param cls:
:param v1: start vector
:param v2: middle vector
:param v3: end vector
:return: an edge object through the three points
"""
circle_geom = GC_MakeArcOfCircle(
Vector(v1).toPnt(), Vector(v2).toPnt(), Vector(v3).toPnt()
).Value()
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
[docs] @classmethod
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.
:param cls:
:param v1: start vector
:param v2: tangent vector
:param v3: end vector
:return: an edge
"""
circle_geom = GC_MakeArcOfCircle(
Vector(v1).toPnt(), Vector(v2).wrapped, Vector(v3).toPnt()
).Value()
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
[docs] @classmethod
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(Vector(v1).toPnt(), Vector(v2).toPnt()).Edge()
)
[docs] @classmethod
def makeBezier(cls, points: List[Vector]) -> "Edge":
"""
Create a cubic Bézier Curve from the points.
:param points: a list of Vectors that represent the points.
The edge will pass through the first and the last point,
and the inner points are Bézier control points.
:return: An edge
"""
# Convert to a TColgp_Array1OfPnt
arr = TColgp_Array1OfPnt(1, len(points))
for i, v in enumerate(points):
arr.SetValue(i + 1, Vector(v).toPnt())
bez = Geom_BezierCurve(arr)
return cls(BRepBuilderAPI_MakeEdge(bez).Edge())
[docs]class Wire(Shape, Mixin1D):
"""
A series of connected, ordered Edges, that typically bounds a Face
"""
wrapped: TopoDS_Wire
def _geomAdaptor(self) -> BRepAdaptor_CompCurve:
"""
Return the underlying geometry
"""
return BRepAdaptor_CompCurve(self.wrapped)
[docs] def close(self) -> "Wire":
"""
Close a Wire
"""
if not self.IsClosed():
e = Edge.makeLine(self.endPoint(), self.startPoint())
rv = Wire.combine((self, e))[0]
else:
rv = self
return rv
[docs] @classmethod
def combine(
cls, listOfWires: Iterable[Union["Wire", Edge]], tol: float = 1e-9
) -> List["Wire"]:
"""
Attempt to combine a list of wires and edges into a new wire.
:param cls:
:param listOfWires:
:param tol: default 1e-9
:return: List[Wire]
"""
edges_in = TopTools_HSequenceOfShape()
wires_out = TopTools_HSequenceOfShape()
for e in Compound.makeCompound(listOfWires).Edges():
edges_in.Append(e.wrapped)
ShapeAnalysis_FreeBounds.ConnectEdgesToWires_s(edges_in, tol, False, wires_out)
return [cls(el) for el in wires_out]
[docs] @classmethod
def assembleEdges(cls, listOfEdges: Iterable[Edge]) -> "Wire":
"""
Attempts to build a wire that consists of the edges in the provided list
:param cls:
:param listOfEdges: a list of Edge objects. The edges are not to be consecutive.
:return: a wire with the edges assembled
BRepBuilderAPI_MakeWire::Error() values:
* BRepBuilderAPI_WireDone = 0
* BRepBuilderAPI_EmptyWire = 1
* BRepBuilderAPI_DisconnectedWire = 2
* BRepBuilderAPI_NonManifoldWire = 3
"""
wire_builder = BRepBuilderAPI_MakeWire()
occ_edges_list = TopTools_ListOfShape()
for e in listOfEdges:
occ_edges_list.Append(e.wrapped)
wire_builder.Add(occ_edges_list)
wire_builder.Build()
if not wire_builder.IsDone():
w = (
"BRepBuilderAPI_MakeWire::Error(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = "
+ str(wire_builder.Error())
)
warnings.warn(w)
return cls(wire_builder.Wire())
[docs] @classmethod
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
:param center: vector representing the center of the circle
:param normal: vector representing the direction of the plane the circle should lie in
"""
circle_edge = Edge.makeCircle(radius, center, normal)
w = cls.assembleEdges([circle_edge])
return w
[docs] @classmethod
def makeEllipse(
cls,
x_radius: float,
y_radius: float,
center: VectorLike,
normal: VectorLike,
xDir: VectorLike,
angle1: float = 360.0,
angle2: float = 360.0,
rotation_angle: float = 0.0,
closed: bool = True,
) -> "Wire":
"""
Makes an Ellipse centered at the provided point, having normal in the provided direction
:param x_radius: floating point major radius of the ellipse (x-axis), must be > 0
:param y_radius: floating point minor radius of the ellipse (y-axis), must be > 0
:param center: vector representing the center of the circle
:param normal: vector representing the direction of the plane the circle should lie in
:param angle1: start angle of arc
:param angle2: end angle of arc
:param rotation_angle: angle to rotate the created ellipse / arc
"""
ellipse_edge = Edge.makeEllipse(
x_radius, y_radius, center, normal, xDir, angle1, angle2
)
if angle1 != angle2 and closed:
line = Edge.makeLine(ellipse_edge.endPoint(), ellipse_edge.startPoint())
w = cls.assembleEdges([ellipse_edge, line])
else:
w = cls.assembleEdges([ellipse_edge])
if rotation_angle != 0.0:
w = w.rotate(center, Vector(center) + Vector(normal), rotation_angle)
return w
[docs] @classmethod
def makePolygon(
cls,
listOfVertices: Iterable[VectorLike],
forConstruction: bool = False,
close: bool = False,
) -> "Wire":
"""
Construct a polygonal wire from points.
"""
wire_builder = BRepBuilderAPI_MakePolygon()
for v in listOfVertices:
wire_builder.Add(Vector(v).toPnt())
if close:
wire_builder.Close()
w = cls(wire_builder.Wire())
w.forConstruction = forConstruction
return w
[docs] @classmethod
def makeHelix(
cls,
pitch: float,
height: float,
radius: float,
center: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angle: float = 360.0,
lefthand: bool = False,
) -> "Wire":
"""
Make a helix with a given pitch, height and radius
By default a cylindrical surface is used to create the helix. If
the fourth parameter is set (the apex given in degree) a conical surface is used instead'
"""
# 1. build underlying cylindrical/conical surface
if angle == 360.0:
geom_surf: Geom_Surface = Geom_CylindricalSurface(
gp_Ax3(Vector(center).toPnt(), Vector(dir).toDir()), radius
)
else:
geom_surf = Geom_ConicalSurface(
gp_Ax3(Vector(center).toPnt(), Vector(dir).toDir()),
radians(angle),
radius,
)
# 2. construct an segment in the u,v domain
if lefthand:
geom_line = Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(-2 * pi, pitch))
else:
geom_line = Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(2 * pi, pitch))
# 3. put it together into a wire
n_turns = height / pitch
u_start = geom_line.Value(0.0)
u_stop = geom_line.Value(n_turns * sqrt((2 * pi) ** 2 + pitch ** 2))
geom_seg = GCE2d_MakeSegment(u_start, u_stop).Value()
e = BRepBuilderAPI_MakeEdge(geom_seg, geom_surf).Edge()
# 4. Convert to wire and fix building 3d geom from 2d geom
w = BRepBuilderAPI_MakeWire(e).Wire()
BRepLib.BuildCurves3d_s(w, 1e-6, MaxSegment=2000) # NB: preliminary values
return cls(w)
[docs] def stitch(self, other: "Wire") -> "Wire":
"""Attempt to stitch wires"""
wire_builder = BRepBuilderAPI_MakeWire()
wire_builder.Add(TopoDS.Wire_s(self.wrapped))
wire_builder.Add(TopoDS.Wire_s(other.wrapped))
wire_builder.Build()
return self.__class__(wire_builder.Wire())
[docs] def offset2D(
self, d: float, kind: Literal["arc", "intersection", "tangent"] = "arc"
) -> List["Wire"]:
"""Offsets a planar wire"""
kind_dict = {
"arc": GeomAbs_JoinType.GeomAbs_Arc,
"intersection": GeomAbs_JoinType.GeomAbs_Intersection,
"tangent": GeomAbs_JoinType.GeomAbs_Tangent,
}
offset = BRepOffsetAPI_MakeOffset()
offset.Init(kind_dict[kind])
offset.AddWire(self.wrapped)
offset.Perform(d)
obj = downcast(offset.Shape())
if isinstance(obj, TopoDS_Compound):
rv = [self.__class__(el.wrapped) for el in Compound(obj)]
else:
rv = [self.__class__(obj)]
return rv
[docs] def fillet2D(self, radius: float, vertices: Iterable[Vertex]) -> "Wire":
"""
Apply 2D fillet to a wire
"""
f = Face.makeFromWires(self)
return f.fillet2D(radius, vertices).outerWire()
[docs] def chamfer2D(self, d: float, vertices: Iterable[Vertex]) -> "Wire":
"""
Apply 2D chamfer to a wire
"""
f = Face.makeFromWires(self)
return f.chamfer2D(d, vertices).outerWire()
[docs] def fillet(
self, radius: float, vertices: Optional[Iterable[Vertex]] = None
) -> "Wire":
"""
Apply 2D or 3D fillet to a wire
:param wire: The input wire to fillet. Currently only open wires are supported
:param radius: the radius of the fillet, must be > zero
:param vertices: Optional list of vertices to fillet. By default all vertices are fillet.
:return: A wire with filleted corners
"""
edges = list(self)
all_vertices = self.Vertices()
newEdges = []
currentEdge = edges[0]
verticesSet = set(vertices) if vertices else set()
for i in range(len(edges) - 1):
nextEdge = edges[i + 1]
# Create a plane that is spanned by currentEdge and nextEdge
currentDir = currentEdge.tangentAt(1)
nextDir = nextEdge.tangentAt(0)
normalDir = currentDir.cross(nextDir)
# Check conditions for skipping fillet:
# 1. The edges are parallel
# 2. The vertex is not in the vertices white list
if normalDir.Length == 0 or (
all_vertices[i + 1] not in verticesSet and bool(verticesSet)
):
newEdges.append(currentEdge)
currentEdge = nextEdge
continue
# Prepare for using ChFi2d_FilletAPI
pointInPlane = currentEdge.Center().toPnt()
cornerPlane = gp_Pln(pointInPlane, normalDir.toDir())
filletMaker = ChFi2d_FilletAPI(
currentEdge.wrapped, nextEdge.wrapped, cornerPlane
)
ok = filletMaker.Perform(radius)
if not ok:
raise ValueError(f"Failed fillet at vertex {i+1}!")
# Get the result of the fillet operation
thePoint = next(iter(nextEdge)).Center().toPnt()
res_arc = filletMaker.Result(
thePoint, currentEdge.wrapped, nextEdge.wrapped
)
newEdges.append(currentEdge)
newEdges.append(Edge(res_arc))
currentEdge = nextEdge
# Add the last edge
newEdges.append(currentEdge)
return Wire.assembleEdges(newEdges)
[docs] def Vertices(self) -> List[Vertex]:
"""
Ordered list of vertices of the wire.
"""
rv = []
exp = BRepTools_WireExplorer(self.wrapped)
rv.append(Vertex(exp.CurrentVertex()))
while exp.More():
exp.Next()
rv.append(Vertex(exp.CurrentVertex()))
# handle closed wires correclty
if self.IsClosed():
rv = rv[:-1]
return rv
[docs] def __iter__(self) -> Iterator[Edge]:
"""
Iterate over edges in an ordered way.
"""
exp = BRepTools_WireExplorer(self.wrapped)
while exp.Current():
yield Edge(exp.Current())
exp.Next()
[docs]class Face(Shape):
"""
a bounded surface that represents part of the boundary of a solid
"""
wrapped: TopoDS_Face
def _geomAdaptor(self) -> Geom_Surface:
"""
Return the underlying geometry
"""
return BRep_Tool.Surface_s(self.wrapped)
def _uvBounds(self) -> Tuple[float, float, float, float]:
return BRepTools.UVBounds_s(self.wrapped)
[docs] def normalAt(self, locationVector: Optional[Vector] = None) -> Vector:
"""
Computes the normal vector at the desired location on the face.
:returns: a vector representing the direction
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
:type locationVector: a vector that lies on the surface.
"""
# get the geometry
surface = self._geomAdaptor()
if locationVector is None:
u0, u1, v0, v1 = self._uvBounds()
u = 0.5 * (u0 + u1)
v = 0.5 * (v0 + v1)
else:
# project point on surface
projector = GeomAPI_ProjectPointOnSurf(locationVector.toPnt(), surface)
u, v = projector.LowerDistanceParameters()
p = gp_Pnt()
vn = gp_Vec()
BRepGProp_Face(self.wrapped).Normal(u, v, p, vn)
return Vector(vn)
[docs] def Center(self) -> Vector:
Properties = GProp_GProps()
BRepGProp.SurfaceProperties_s(self.wrapped, Properties)
return Vector(Properties.CentreOfMass())
def outerWire(self) -> Wire:
return Wire(BRepTools.OuterWire_s(self.wrapped))
def innerWires(self) -> List[Wire]:
outer = self.outerWire()
return [w for w in self.Wires() if not w.isSame(outer)]
[docs] @classmethod
def makeNSidedSurface(
cls,
edges: Iterable[Union[Edge, Wire]],
constraints: Iterable[Union[Edge, Wire, VectorLike, gp_Pnt]],
continuity: GeomAbs_Shape = GeomAbs_C0,
degree: int = 3,
nbPtsOnCur: int = 15,
nbIter: int = 2,
anisotropy: bool = False,
tol2d: float = 0.00001,
tol3d: float = 0.0001,
tolAng: float = 0.01,
tolCurv: float = 0.1,
maxDeg: int = 8,
maxSegments: int = 9,
) -> "Face":
"""
Returns a surface enclosed by a closed polygon defined by 'edges' and 'constraints'.
:param edges: edges
:type edges: list of edges or wires
:param constraints: constraints
:type constraints: list of points or edges
:param continuity: OCC.Core.GeomAbs continuity condition
:param degree: >=2
:param nbPtsOnCur: number of points on curve >= 15
:param nbIter: number of iterations >= 2
:param anisotropy: bool Anisotropy
:param tol2d: 2D tolerance >0
:param tol3d: 3D tolerance >0
:param tolAng: angular tolerance
:param tolCurv: tolerance for curvature >0
:param maxDeg: highest polynomial degree >= 2
:param maxSegments: greatest number of segments >= 2
"""
n_sided = BRepOffsetAPI_MakeFilling(
degree,
nbPtsOnCur,
nbIter,
anisotropy,
tol2d,
tol3d,
tolAng,
tolCurv,
maxDeg,
maxSegments,
)
# 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
def makePlane(
cls,
length: Optional[float] = None,
width: Optional[float] = None,
basePnt: VectorLike = (0, 0, 0),
dir: VectorLike = (0, 0, 1),
) -> "Face":
basePnt = Vector(basePnt)
dir = Vector(dir)
pln_geom = gp_Pln(basePnt.toPnt(), dir.toDir())
if length and width:
pln_shape = BRepBuilderAPI_MakeFace(
pln_geom, -width * 0.5, width * 0.5, -length * 0.5, length * 0.5
).Face()
else:
pln_shape = BRepBuilderAPI_MakeFace(pln_geom).Face()
return cls(pln_shape)
@overload
@classmethod
def makeRuledSurface(cls, edgeOrWire1: Edge, edgeOrWire2: Edge) -> "Face":
...
@overload
@classmethod
def makeRuledSurface(cls, edgeOrWire1: Wire, edgeOrWire2: Wire) -> "Face":
...
[docs] @classmethod
def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2):
"""
makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface
Create a ruled surface out of two edges or wires. If wires are used then
these must have the same number of edges
"""
if isinstance(edgeOrWire1, Wire):
return cls.cast(BRepFill.Shell_s(edgeOrWire1.wrapped, edgeOrWire2.wrapped))
else:
return cls.cast(BRepFill.Face_s(edgeOrWire1.wrapped, edgeOrWire2.wrapped))
[docs] @classmethod
def makeFromWires(cls, outerWire: Wire, innerWires: List[Wire] = []) -> "Face":
"""
Makes a planar face from one or more wires
"""
if innerWires and not outerWire.IsClosed():
raise ValueError("Cannot build face(s): outer wire is not closed")
# check if wires are coplanar
ws = Compound.makeCompound([outerWire] + innerWires)
if not BRepLib_FindSurface(ws.wrapped, OnlyPlane=True).Found():
raise ValueError("Cannot build face(s): wires not planar")
# fix outer wire
sf_s = ShapeFix_Shape(outerWire.wrapped)
sf_s.Perform()
wo = TopoDS.Wire_s(sf_s.Shape())
face_builder = BRepBuilderAPI_MakeFace(wo, True)
for w in innerWires:
if not w.IsClosed():
raise ValueError("Cannot build face(s): inner wire is not closed")
face_builder.Add(w.wrapped)
face_builder.Build()
if not face_builder.IsDone():
raise ValueError(f"Cannot build face(s): {face_builder.Error()}")
face = face_builder.Face()
sf_f = ShapeFix_Face(face)
sf_f.FixOrientation()
sf_f.Perform()
return cls(sf_f.Result())
[docs] @classmethod
def makeSplineApprox(
cls,
points: List[List[Vector]],
tol: float = 1e-2,
smoothing: Optional[Tuple[float, float, float]] = None,
minDeg: int = 1,
maxDeg: int = 3,
) -> "Face":
"""
Approximate a spline surface through the provided points.
:param points: a 2D list of Vectors that represent the points
:param tol: tolerance of the algorithm (consult OCC documentation).
:param smoothing: optional tuple of 3 weights use for variational smoothing (default: None)
:param minDeg: minimum spline degree. Enforced only when smothing is None (default: 1)
:param maxDeg: maximum spline degree (default: 6)
"""
points_ = TColgp_HArray2OfPnt(1, len(points), 1, len(points[0]))
for i, vi in enumerate(points):
for j, v in enumerate(vi):
points_.SetValue(i + 1, j + 1, v.toPnt())
if smoothing:
spline_builder = GeomAPI_PointsToBSplineSurface(
points_, *smoothing, DegMax=maxDeg, Tol3D=tol
)
else:
spline_builder = GeomAPI_PointsToBSplineSurface(
points_, DegMin=minDeg, DegMax=maxDeg, Tol3D=tol
)
if not spline_builder.IsDone():
raise ValueError("B-spline approximation failed")
spline_geom = spline_builder.Surface()
return cls(BRepBuilderAPI_MakeFace(spline_geom, Precision.Confusion_s()).Face())
[docs] def fillet2D(self, radius: float, vertices: Iterable[Vertex]) -> "Face":
"""
Apply 2D fillet to a face
"""
fillet_builder = BRepFilletAPI_MakeFillet2d(self.wrapped)
for v in vertices:
fillet_builder.AddFillet(v.wrapped, radius)
fillet_builder.Build()
return self.__class__(fillet_builder.Shape())
[docs] def chamfer2D(self, d: float, vertices: Iterable[Vertex]) -> "Face":
"""
Apply 2D chamfer to a face
"""
chamfer_builder = BRepFilletAPI_MakeFillet2d(self.wrapped)
edge_map = self._entitiesFrom("Vertex", "Edge")
for v in vertices:
edges = edge_map[v]
if len(edges) < 2:
raise ValueError("Cannot chamfer at this location")
e1, e2 = edges
chamfer_builder.AddChamfer(
TopoDS.Edge_s(e1.wrapped), TopoDS.Edge_s(e2.wrapped), d, d
)
chamfer_builder.Build()
return self.__class__(chamfer_builder.Shape()).fix()
[docs] def toPln(self) -> gp_Pln:
"""
Convert this face to a gp_Pln.
Note the Location of the resulting plane may not equal the center of this face,
however the resulting plane will still contain the center of this face.
"""
adaptor = BRepAdaptor_Surface(self.wrapped)
return adaptor.Plane()
[docs] 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))
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)
[docs] def toArcs(self, tolerance: float = 1e-3) -> "Face":
"""
Approximate planar face with arcs and straight line segments.
:param tolerance: Approximation tolerance.
"""
return self.__class__(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
[docs]class Shell(Shape):
"""
the outer boundary of a surface
"""
wrapped: TopoDS_Shell
[docs] @classmethod
def makeShell(cls, listOfFaces: Iterable[Face]) -> "Shell":
"""
Makes a shell from faces.
"""
shell_builder = BRepBuilderAPI_Sewing()
for face in listOfFaces:
shell_builder.Add(face.wrapped)
shell_builder.Perform()
s = shell_builder.SewedShape()
return cls(s)
TS = TypeVar("TS", bound=ShapeProtocol)
[docs]class Mixin3D(object):
[docs] def fillet(self: Any, radius: float, edgeList: Iterable[Edge]) -> Any:
"""
Fillets the specified edges of this solid.
:param radius: float > 0, the radius of the fillet
:param edgeList: a list of Edge objects, which must belong to this solid
:return: Filleted solid
"""
nativeEdges = [e.wrapped for e in edgeList]
fillet_builder = BRepFilletAPI_MakeFillet(self.wrapped)
for e in nativeEdges:
fillet_builder.Add(radius, e)
return self.__class__(fillet_builder.Shape())
[docs] def chamfer(
self: Any, length: float, length2: Optional[float], edgeList: Iterable[Edge]
) -> Any:
"""
Chamfers the specified edges of this solid.
:param length: length > 0, the length (length) of the chamfer
:param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required.
:param edgeList: a list of Edge objects, which must belong to this solid
:return: Chamfered solid
"""
nativeEdges = [e.wrapped for e in edgeList]
# make a edge --> faces mapping
edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape()
TopExp.MapShapesAndAncestors_s(
self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map
)
# note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API
chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped)
if length2:
d1 = length
d2 = length2
else:
d1 = length
d2 = length
for e in nativeEdges:
face = edge_face_map.FindFromKey(e).First()
chamfer_builder.Add(
d1, d2, e, TopoDS.Face_s(face)
) # NB: edge_face_map return a generic TopoDS_Shape
return self.__class__(chamfer_builder.Shape())
[docs] def shell(
self: Any,
faceList: Optional[Iterable[Face]],
thickness: float,
tolerance: float = 0.0001,
kind: Literal["arc", "intersection"] = "arc",
) -> Any:
"""
Make a shelled solid of self.
:param faceList: List of faces to be removed, which must be part of the solid. Can
be an empty list.
:param thickness: Floating point thickness. Positive shells outwards, negative
shells inwards.
:param tolerance: Modelling tolerance of the method, default=0.0001.
:return: A shelled solid.
"""
kind_dict = {
"arc": GeomAbs_JoinType.GeomAbs_Arc,
"intersection": GeomAbs_JoinType.GeomAbs_Intersection,
}
occ_faces_list = TopTools_ListOfShape()
shell_builder = BRepOffsetAPI_MakeThickSolid()
if faceList:
for f in faceList:
occ_faces_list.Append(f.wrapped)
shell_builder.MakeThickSolidByJoin(
self.wrapped,
occ_faces_list,
thickness,
tolerance,
Intersection=True,
Join=kind_dict[kind],
)
shell_builder.Build()
if faceList:
rv = self.__class__(shell_builder.Shape())
else: # if no faces provided a watertight solid will be constructed
s1 = self.__class__(shell_builder.Shape()).Shells()[0].wrapped
s2 = self.Shells()[0].wrapped
# s1 can be outer or inner shell depending on the thickness sign
if thickness > 0:
sol = BRepBuilderAPI_MakeSolid(s1, s2)
else:
sol = BRepBuilderAPI_MakeSolid(s2, s1)
# fix needed for the orientations
rv = self.__class__(sol.Shape()).fix()
return rv
[docs] def isInside(
self: ShapeProtocol, point: VectorLike, tolerance: float = 1.0e-6
) -> bool:
"""
Returns whether or not the point is inside a solid or compound
object within the specified tolerance.
:param point: tuple or Vector representing 3D point to be tested
:param tolerance: tolerance for inside determination, default=1.0e-6
:return: bool indicating whether or not point is within solid
"""
if isinstance(point, Vector):
point = point.toTuple()
solid_classifier = BRepClass3d_SolidClassifier(self.wrapped)
solid_classifier.Perform(gp_Pnt(*point), tolerance)
return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace()
@multimethod
def dprism(
self: TS,
basis: Optional[Face],
profiles: List[Wire],
depth: Optional[Real] = None,
taper: Real = 0,
upToFace: Optional[Face] = None,
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 upToFace: a face to extrude until
:param thruAll: cut thruAll
:return: a Solid object
"""
sorted_profiles = sortWiresByBuildOrder(profiles)
faces = [Face.makeFromWires(p[0], p[1:]) for p in sorted_profiles]
return self.dprism(basis, faces, depth, taper, upToFace, thruAll, additive)
[docs] @dprism.register
def dprism(
self: TS,
basis: Optional[Face],
faces: List[Face],
depth: Optional[Real] = None,
taper: Real = 0,
upToFace: Optional[Face] = None,
thruAll: bool = True,
additive: bool = True,
) -> "Solid":
shape: Union[TopoDS_Shape, TopoDS_Solid] = self.wrapped
for face in faces:
feat = BRepFeat_MakeDPrism(
shape,
face.wrapped,
basis.wrapped if basis else TopoDS_Face(),
radians(taper),
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)
[docs]class Solid(Shape, Mixin3D):
"""
a single solid
"""
wrapped: TopoDS_Solid
[docs] @classmethod
@deprecate()
def interpPlate(
cls,
surf_edges,
surf_pts,
thickness,
degree=3,
nbPtsOnCur=15,
nbIter=2,
anisotropy=False,
tol2d=0.00001,
tol3d=0.0001,
tolAng=0.01,
tolCurv=0.1,
maxDeg=8,
maxSegments=9,
) -> Union["Solid", Face]:
"""
Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going through 'surf_pts' points.
:param surf_edges:
list of [x,y,z] float ordered coordinates
or list of ordered or unordered wires
:param surf_pts: list of [x,y,z] float coordinates (uses only edges if [])
:param thickness: thickness may be negative or positive depending on direction, (returns 2D surface if 0)
:param degree: >=2
:param nbPtsOnCur: number of points on curve >= 15
:param nbIter: number of iterations >= 2
:param anisotropy: bool Anisotropy
:param tol2d: 2D tolerance >0
:param tol3d: 3D tolerance >0
:param tolAng: angular tolerance
:param tolCurv: tolerance for curvature >0
:param maxDeg: highest polynomial degree >= 2
:param maxSegments: greatest number of segments >= 2
"""
# POINTS CONSTRAINTS: list of (x,y,z) points, optional.
pts_array = [gp_Pnt(*pt) for pt in surf_pts]
# EDGE CONSTRAINTS
# If a list of wires is provided, make a closed wire
if not isinstance(surf_edges, list):
surf_edges = [o.vals()[0] for o in surf_edges.all()]
surf_edges = Wire.assembleEdges(surf_edges)
w = surf_edges.wrapped
# If a list of (x,y,z) points provided, build closed polygon
if isinstance(surf_edges, list):
e_array = [Vector(*e) for e in surf_edges]
wire_builder = BRepBuilderAPI_MakePolygon()
for e in e_array: # Create polygon from edges
wire_builder.Add(e.toPnt())
wire_builder.Close()
w = wire_builder.Wire()
edges = [i for i in Shape(w).Edges()]
# MAKE SURFACE
continuity = GeomAbs_C0 # Fixed, changing to anything else crashes.
face = Face.makeNSidedSurface(
edges,
pts_array,
continuity,
degree,
nbPtsOnCur,
nbIter,
anisotropy,
tol2d,
tol3d,
tolAng,
tolCurv,
maxDeg,
maxSegments,
)
# THICKEN SURFACE
if (
abs(thickness) > 0
): # abs() because negative values are allowed to set direction of thickening
return face.thicken(thickness)
else: # Return 2D surface only
return face
[docs] @staticmethod
def isSolid(obj: Shape) -> bool:
"""
Returns true if the object is a solid, false otherwise
"""
if hasattr(obj, "ShapeType"):
if obj.ShapeType == "Solid" or (
obj.ShapeType == "Compound" and len(obj.Solids()) > 0
):
return True
return False
[docs] @classmethod
def makeSolid(cls, shell: Shell) -> "Solid":
"""
Makes a solid from a single shell.
"""
return cls(ShapeFix_Solid().SolidFromShell(shell.wrapped))
[docs] @classmethod
def makeBox(
cls,
length: float,
width: float,
height: float,
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)
By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)
"""
return cls(
BRepPrimAPI_MakeBox(
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()), length, width, height
).Shape()
)
[docs] @classmethod
def makeCone(
cls,
radius1: float,
radius2: float,
height: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees: float = 360,
) -> "Solid":
"""
Make a cone with given radii and height
By default pnt=Vector(0,0,0),
dir=Vector(0,0,1) and angle=360
"""
return cls(
BRepPrimAPI_MakeCone(
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius1,
radius2,
height,
radians(angleDegrees),
).Shape()
)
[docs] @classmethod
def makeCylinder(
cls,
radius: float,
height: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees: float = 360,
) -> "Solid":
"""
makeCylinder(radius,height,[pnt,dir,angle]) --
Make a cylinder with a given radius and height
By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360
"""
return cls(
BRepPrimAPI_MakeCylinder(
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius,
height,
radians(angleDegrees),
).Shape()
)
[docs] @classmethod
def makeTorus(
cls,
radius1: float,
radius2: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees1: float = 0,
angleDegrees2: float = 360,
) -> "Solid":
"""
makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) --
Make a torus with a given radii and angles
By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0
,angle1=360 and angle=360
"""
return cls(
BRepPrimAPI_MakeTorus(
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius1,
radius2,
radians(angleDegrees1),
radians(angleDegrees2),
).Shape()
)
[docs] @classmethod
def makeLoft(cls, listOfWire: List[Wire], ruled: bool = False) -> "Solid":
"""
makes a loft from a list of wires
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
wants to make an infinitely thin shell for a real FreeCADPart.
"""
# the True flag requests building a solid instead of a shell.
if len(listOfWire) < 2:
raise ValueError("More than one wire is required")
loft_builder = BRepOffsetAPI_ThruSections(True, ruled)
for w in listOfWire:
loft_builder.AddWire(w.wrapped)
loft_builder.Build()
return cls(loft_builder.Shape())
[docs] @classmethod
def makeWedge(
cls,
dx: float,
dy: float,
dz: float,
xmin: float,
zmin: float,
xmax: float,
zmax: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
) -> "Solid":
"""
Make a wedge located in pnt
By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)
"""
return cls(
BRepPrimAPI_MakeWedge(
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
dx,
dy,
dz,
xmin,
zmin,
xmax,
zmax,
).Solid()
)
[docs] @classmethod
def makeSphere(
cls,
radius: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
angleDegrees1: float = 0,
angleDegrees2: float = 90,
angleDegrees3: float = 360,
) -> "Shape":
"""
Make a sphere with a given radius
By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360
"""
return cls(
BRepPrimAPI_MakeSphere(
gp_Ax2(Vector(pnt).toPnt(), Vector(dir).toDir()),
radius,
radians(angleDegrees1),
radians(angleDegrees2),
radians(angleDegrees3),
).Shape()
)
@classmethod
def _extrudeAuxSpine(
cls, wire: TopoDS_Wire, spine: TopoDS_Wire, auxSpine: TopoDS_Wire
) -> TopoDS_Shape:
"""
Helper function for extrudeLinearWithRotation
"""
extrude_builder = BRepOffsetAPI_MakePipeShell(spine)
extrude_builder.SetMode(auxSpine, False) # auxiliary spine
extrude_builder.Add(wire)
extrude_builder.Build()
extrude_builder.MakeSolid()
return extrude_builder.Shape()
@multimethod
def extrudeLinearWithRotation(
cls,
outerWire: Wire,
innerWires: List[Wire],
vecCenter: VectorLike,
vecNormal: VectorLike,
angleDegrees: Real,
) -> "Solid":
"""
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
construction methods used here are different enough that they should be separate.
At a high level, the steps followed are:
(1) accept a set of wires
(2) create another set of wires like this one, but which are transformed and rotated
(3) create a ruledSurface between the sets of wires
(4) create a shell and compute the resulting object
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param vecCenter: the center point about which to rotate. the axis of rotation is defined by
vecNormal, located at vecCenter.
:param vecNormal: a vector along which to extrude the wires
:param angleDegrees: the angle to rotate through while extruding
:return: a Solid object
"""
# make straight spine
straight_spine_e = Edge.makeLine(vecCenter, vecCenter.add(vecNormal))
straight_spine_w = Wire.combine([straight_spine_e,])[0].wrapped
# make an auxiliary spine
pitch = 360.0 / angleDegrees * vecNormal.Length
radius = 1
aux_spine_w = Wire.makeHelix(
pitch, vecNormal.Length, radius, center=vecCenter, dir=vecNormal
).wrapped
# extrude the outer wire
outer_solid = cls._extrudeAuxSpine(
outerWire.wrapped, straight_spine_w, aux_spine_w
)
# extrude inner wires
inner_solids = [
cls._extrudeAuxSpine(w.wrapped, straight_spine_w, aux_spine_w)
for w in innerWires
]
# combine the inner solids into compound
inner_comp = Compound._makeCompound(inner_solids)
# subtract from the outer solid
return cls(BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape())
[docs] @classmethod
@extrudeLinearWithRotation.register
def extrudeLinearWithRotation(
cls,
face: Face,
vecCenter: VectorLike,
vecNormal: VectorLike,
angleDegrees: Real,
) -> "Solid":
return cls.extrudeLinearWithRotation(
face.outerWire(), face.innerWires(), vecCenter, vecNormal, angleDegrees
)
@multimethod
def extrudeLinear(
cls,
outerWire: Wire,
innerWires: List[Wire],
vecNormal: VectorLike,
taper: Real = 0,
) -> "Solid":
"""
Attempt to extrude the list of wires into a prismatic solid in the provided direction
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param vecNormal: a vector along which to extrude the wires
:param taper: taper angle, default=0
:return: a Solid object
The wires must not intersect
Extruding wires is very non-trivial. Nested wires imply very different geometry, and
there are many geometries that are invalid. In general, the following conditions must be met:
* all wires must be closed
* there cannot be any intersecting or self-intersecting wires
* wires must be listed from outside in
* more than one levels of nesting is not supported reliably
This method will attempt to sort the wires, but there is much work remaining to make this method
reliable.
"""
if taper == 0:
face = Face.makeFromWires(outerWire, innerWires)
else:
face = Face.makeFromWires(outerWire)
return cls.extrudeLinear(face, vecNormal, taper)
[docs] @classmethod
@extrudeLinear.register
def extrudeLinear(
cls, face: Face, vecNormal: VectorLike, taper: Real = 0,
) -> "Solid":
if taper == 0:
prism_builder: Any = BRepPrimAPI_MakePrism(
face.wrapped, Vector(vecNormal).wrapped, True
)
else:
faceNormal = face.normalAt()
d = 1 if vecNormal.getAngle(faceNormal) < radians(90.0) else -1
# Divided by cos of taper angle to ensure the height chosen by the user is respected
prism_builder = LocOpe_DPrism(
face.wrapped,
(d * vecNormal.Length) / cos(radians(taper)),
d * radians(taper),
)
return cls(prism_builder.Shape())
@multimethod
def revolve(
cls,
outerWire: Wire,
innerWires: List[Wire],
angleDegrees: Real,
axisStart: VectorLike,
axisEnd: VectorLike,
) -> "Solid":
"""
Attempt to revolve the list of wires into a solid in the provided direction
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param angleDegrees: the angle to revolve through.
:type angleDegrees: float, anything less than 360 degrees will leave the shape open
:param axisStart: the start point of the axis of rotation
:param axisEnd: the end point of the axis of rotation
:return: a Solid object
The wires must not intersect
* all wires must be closed
* there cannot be any intersecting or self-intersecting wires
* wires must be listed from outside in
* more than one levels of nesting is not supported reliably
* the wire(s) that you're revolving cannot be centered
This method will attempt to sort the wires, but there is much work remaining to make this method
reliable.
"""
face = Face.makeFromWires(outerWire, innerWires)
return cls.revolve(face, angleDegrees, axisStart, axisEnd)
[docs] @classmethod
@revolve.register
def revolve(
cls, face: Face, angleDegrees: Real, axisStart: VectorLike, axisEnd: VectorLike,
) -> "Solid":
v1 = Vector(axisStart)
v2 = Vector(axisEnd)
v2 = v2 - v1
revol_builder = BRepPrimAPI_MakeRevol(
face.wrapped, gp_Ax1(v1.toPnt(), v2.toDir()), radians(angleDegrees), True
)
return cls(revol_builder.Shape())
_transModeDict = {
"transformed": BRepBuilderAPI_Transformed,
"round": BRepBuilderAPI_RoundCorner,
"right": BRepBuilderAPI_RightCorner,
}
@classmethod
def _setSweepMode(
cls,
builder: BRepOffsetAPI_MakePipeShell,
path: Union[Wire, Edge],
mode: Union[Vector, Wire, Edge],
) -> bool:
rotate = False
if isinstance(mode, Vector):
ax = gp_Ax2()
ax.SetLocation(path.startPoint().toPnt())
ax.SetDirection(mode.toDir())
builder.SetMode(ax)
rotate = True
elif isinstance(mode, (Wire, Edge)):
builder.SetMode(cls._toWire(mode).wrapped, True)
return rotate
@staticmethod
def _toWire(p: Union[Edge, Wire]) -> Wire:
if isinstance(p, Edge):
rv = Wire.assembleEdges([p,])
else:
rv = p
return rv
@multimethod
def sweep(
cls,
outerWire: Wire,
innerWires: List[Wire],
path: Union[Wire, Edge],
makeSolid: bool = True,
isFrenet: bool = False,
mode: Union[Vector, Wire, Edge, None] = None,
transitionMode: Literal["transformed", "round", "right"] = "transformed",
) -> "Shape":
"""
Attempt to sweep the list of wires into a prismatic solid along the provided path
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param path: The wire to sweep the face resulting from the wires over
:param makeSolid: return Solid or Shell (default True)
:param isFrenet: Frenet mode (default False)
:param mode: additional sweep mode parameters
:param transitionMode:
handling of profile orientation at C1 path discontinuities.
Possible values are {'transformed','round', 'right'} (default: 'right').
:return: a Solid object
"""
p = cls._toWire(path)
shapes = []
for w in [outerWire] + innerWires:
builder = BRepOffsetAPI_MakePipeShell(p.wrapped)
translate = False
rotate = False
# handle sweep mode
if mode:
rotate = cls._setSweepMode(builder, path, mode)
else:
builder.SetMode(isFrenet)
builder.SetTransitionMode(cls._transModeDict[transitionMode])
builder.Add(w.wrapped, translate, rotate)
builder.Build()
if makeSolid:
builder.MakeSolid()
shapes.append(Shape.cast(builder.Shape()))
rv, inner_shapes = shapes[0], shapes[1:]
if inner_shapes:
rv = rv.cut(*inner_shapes)
return rv
[docs] @classmethod
@sweep.register
def sweep(
cls,
face: Face,
path: Union[Wire, Edge],
makeSolid: bool = True,
isFrenet: bool = False,
mode: Union[Vector, Wire, Edge, None] = None,
transitionMode: Literal["transformed", "round", "right"] = "transformed",
) -> "Shape":
return cls.sweep(
face.outerWire(),
face.innerWires(),
path,
makeSolid,
isFrenet,
mode,
transitionMode,
)
[docs] @classmethod
def sweep_multi(
cls,
profiles: Iterable[Union[Wire, Face]],
path: Union[Wire, Edge],
makeSolid: bool = True,
isFrenet: bool = False,
mode: Union[Vector, Wire, Edge, None] = None,
) -> "Solid":
"""
Multi section sweep. Only single outer profile per section is allowed.
:param profiles: list of profiles
:param path: The wire to sweep the face resulting from the wires over
:param mode: additional sweep mode parameters.
:return: a Solid object
"""
if isinstance(path, Edge):
w = Wire.assembleEdges([path,]).wrapped
else:
w = path.wrapped
builder = BRepOffsetAPI_MakePipeShell(w)
translate = False
rotate = False
if mode:
rotate = cls._setSweepMode(builder, path, mode)
else:
builder.SetMode(isFrenet)
for p in profiles:
w = p.wrapped if isinstance(p, Wire) else p.outerWire().wrapped
builder.Add(w, translate, rotate)
builder.Build()
if makeSolid:
builder.MakeSolid()
return cls(builder.Shape())
class CompSolid(Shape, Mixin3D):
"""
a single compsolid
"""
wrapped: TopoDS_CompSolid
[docs]class Compound(Shape, Mixin3D):
"""
a collection of disconnected solids
"""
wrapped: TopoDS_Compound
@staticmethod
def _makeCompound(listOfShapes: Iterable[TopoDS_Shape]) -> TopoDS_Compound:
comp = TopoDS_Compound()
comp_builder = TopoDS_Builder()
comp_builder.MakeCompound(comp)
for s in listOfShapes:
comp_builder.Add(comp, s)
return comp
[docs] def remove(self, shape: Shape):
"""
Remove the specified shape.
"""
comp_builder = TopoDS_Builder()
comp_builder.Remove(self.wrapped, shape.wrapped)
[docs] @classmethod
def makeCompound(cls, listOfShapes: Iterable[Shape]) -> "Compound":
"""
Create a compound out of a list of shapes
"""
return cls(cls._makeCompound((s.wrapped for s in listOfShapes)))
[docs] @classmethod
def makeText(
cls,
text: str,
size: float,
height: float,
font: str = "Arial",
fontPath: Optional[str] = None,
kind: Literal["regular", "bold", "italic"] = "regular",
halign: Literal["center", "left", "right"] = "center",
valign: Literal["center", "top", "bottom"] = "center",
position: Plane = Plane.XY(),
) -> "Shape":
"""
Create a 3D text
"""
font_kind = {
"regular": Font_FA_Regular,
"bold": Font_FA_Bold,
"italic": Font_FA_Italic,
}[kind]
mgr = Font_FontMgr.GetInstance_s()
if fontPath and mgr.CheckFont(TCollection_AsciiString(fontPath).ToCString()):
font_t = Font_SystemFont(TCollection_AsciiString(fontPath))
font_t.SetFontPath(font_kind, TCollection_AsciiString(fontPath))
mgr.RegisterFont(font_t, True)
else:
font_t = mgr.FindFont(TCollection_AsciiString(font), font_kind)
builder = Font_BRepTextBuilder()
font_i = StdPrs_BRepFont(
NCollection_Utf8String(font_t.FontName().ToCString()),
font_kind,
float(size),
)
if halign == "left":
theHAlign = Graphic3d_HTA_LEFT
elif halign == "center":
theHAlign = Graphic3d_HTA_CENTER
else: # halign == "right"
theHAlign = Graphic3d_HTA_RIGHT
if valign == "bottom":
theVAlign = Graphic3d_VTA_BOTTOM
elif valign == "center":
theVAlign = Graphic3d_VTA_CENTER
else: # valign == "top":
theVAlign = Graphic3d_VTA_TOP
text_flat = Shape(
builder.Perform(
font_i,
NCollection_Utf8String(text),
theHAlign=theHAlign,
theVAlign=theVAlign,
)
)
if height != 0:
vecNormal = text_flat.Faces()[0].normalAt() * height
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 rv
[docs] def __bool__(self) -> bool:
"""
Check if empty.
"""
return TopoDS_Iterator(self.wrapped).More()
[docs] def cut(self, *toCut: "Shape", tol: Optional[float] = None) -> "Compound":
"""
Remove the positional arguments from this Shape.
:param tol: Fuzzy mode tolerance
"""
cut_op = BRepAlgoAPI_Cut()
if tol:
cut_op.SetFuzzyValue(tol)
return tcast(Compound, self._bool_op(self, toCut, cut_op))
[docs] def fuse(
self, *toFuse: Shape, glue: bool = False, tol: Optional[float] = None
) -> "Compound":
"""
Fuse shapes together
"""
fuse_op = BRepAlgoAPI_Fuse()
if glue:
fuse_op.SetGlue(BOPAlgo_GlueEnum.BOPAlgo_GlueShift)
if tol:
fuse_op.SetFuzzyValue(tol)
args = tuple(self) + toFuse
if len(args) <= 1:
rv: Shape = args[0]
else:
rv = self._bool_op(args[:1], args[1:], fuse_op)
# fuse_op.RefineEdges()
# fuse_op.FuseEdges()
return tcast(Compound, rv)
[docs] def intersect(
self, *toIntersect: "Shape", tol: Optional[float] = None
) -> "Compound":
"""
Intersection of the positional arguments and this Shape.
:param tol: Fuzzy mode tolerance
"""
intersect_op = BRepAlgoAPI_Common()
if tol:
intersect_op.SetFuzzyValue(tol)
return tcast(Compound, self._bool_op(self, toIntersect, intersect_op))
[docs] def ancestors(self, shape: "Shape", kind: Shapes) -> "Compound":
"""
Iterate over ancestors, i.e. shapes of same kind within shape that contain elements of self.
"""
shape_map = TopTools_IndexedDataMapOfShapeListOfShape()
shapetypes = set(shapetype(ch.wrapped) for ch in self)
for t in shapetypes:
TopExp.MapShapesAndAncestors_s(
shape.wrapped, t, inverse_shape_LUT[kind], shape_map
)
return Compound.makeCompound(
Shape.cast(a) for s in self for a in shape_map.FindFromKey(s.wrapped)
)
[docs] def siblings(self, shape: "Shape", kind: Shapes, level: int = 1) -> "Compound":
"""
Iterate over siblings, i.e. shapes within shape that share subshapes of kind with the elements of self.
"""
shape_map = TopTools_IndexedDataMapOfShapeListOfShape()
shapetypes = set(shapetype(ch.wrapped) for ch in self)
for t in shapetypes:
TopExp.MapShapesAndAncestors_s(
shape.wrapped, inverse_shape_LUT[kind], t, shape_map,
)
exclude = TopTools_MapOfShape()
def _siblings(shapes, level):
rv = set()
for s in shapes:
exclude.Add(s.wrapped)
for s in shapes:
rv.update(
Shape.cast(el)
for child in s._entities(kind)
for el in shape_map.FindFromKey(child)
if not exclude.Contains(el)
)
return rv if level == 1 else _siblings(rv, level - 1)
return Compound.makeCompound(_siblings(self, level))
[docs]def sortWiresByBuildOrder(wireList: List[Wire]) -> List[List[Wire]]:
"""Tries to determine how wires should be combined into faces.
Assume:
The wires make up one or more faces, which could have 'holes'
Outer wires are listed ahead of inner wires
there are no wires inside wires inside wires
( IE, islands -- we can deal with that later on )
none of the wires are construction wires
Compute:
one or more sets of wires, with the outer wire listed first, and inner
ones
Returns, list of lists.
"""
# check if we have something to sort at all
if len(wireList) < 2:
return [
wireList,
]
# make a Face, NB: this might return a compound of faces
faces = Face.makeFromWires(wireList[0], wireList[1:])
rv = []
for face in faces.Faces():
rv.append([face.outerWire(),] + face.innerWires())
return rv
def wiresToFaces(wireList: List[Wire]) -> List[Face]:
"""
Convert wires to a list of faces.
"""
return Face.makeFromWires(wireList[0], wireList[1:]).Faces()
def edgesToWires(edges: Iterable[Edge], tol: float = 1e-6) -> List[Wire]:
"""
Convert edges to a list of wires.
"""
edges_in = TopTools_HSequenceOfShape()
wires_out = TopTools_HSequenceOfShape()
for e in edges:
edges_in.Append(e.wrapped)
ShapeAnalysis_FreeBounds.ConnectEdgesToWires_s(edges_in, tol, False, wires_out)
return [Wire(el) for el in wires_out]