"""
Copyright (c) 2016-2022, CodeLV.
Distributed under the terms of the GPL v3 License.
The full license is in the file LICENSE, distributed with this software.
Created on Sep 30, 2016
@author: jrm
"""
from typing import Any, Iterable, Optional, TypeVar, Union
from atom.api import Atom, Bool, Instance, List, Property, Typed
from OCCT import GeomAbs, TopoDS
from OCCT.Bnd import Bnd_Box
from OCCT.BRepAdaptor import (
BRepAdaptor_CompCurve,
BRepAdaptor_Curve,
BRepAdaptor_Surface,
)
from OCCT.BRepAlgoAPI import BRepAlgoAPI_Section
from OCCT.BRepBndLib import BRepBndLib
from OCCT.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeVertex
from OCCT.BRepExtrema import BRepExtrema_DistShapeShape
from OCCT.BRepGProp import BRepGProp
from OCCT.BRepTools import BRepTools, BRepTools_WireExplorer
from OCCT.Extrema import Extrema_ExtFlag_MAX, Extrema_ExtFlag_MIN
from OCCT.GC import GC_MakeSegment
from OCCT.GCPnts import (
GCPnts_QuasiUniformAbscissa,
GCPnts_QuasiUniformDeflection,
GCPnts_UniformAbscissa,
GCPnts_UniformDeflection,
)
from OCCT.Geom import (
Geom_Circle,
Geom_Curve,
Geom_Ellipse,
Geom_Hyperbola,
Geom_OffsetCurve,
Geom_Parabola,
Geom_Surface,
)
from OCCT.GeomAbs import (
GeomAbs_BezierCurve,
GeomAbs_BSplineCurve,
GeomAbs_Circle,
GeomAbs_Cone,
GeomAbs_CurveType,
GeomAbs_Cylinder,
GeomAbs_Ellipse,
GeomAbs_Hyperbola,
GeomAbs_Line,
GeomAbs_OffsetCurve,
GeomAbs_OtherCurve,
GeomAbs_Parabola,
GeomAbs_Plane,
GeomAbs_Sphere,
GeomAbs_SurfaceType,
GeomAbs_Torus,
)
from OCCT.GeomAPI import GeomAPI_ProjectPointOnCurve
from OCCT.gp import gp_Pnt, gp_Vec
from OCCT.GProp import GProp_GProps
from OCCT.ShapeAnalysis import ShapeAnalysis_FreeBounds
from OCCT.TopAbs import (
TopAbs_COMPOUND,
TopAbs_COMPSOLID,
TopAbs_EDGE,
TopAbs_FACE,
TopAbs_REVERSED,
TopAbs_ShapeEnum,
TopAbs_SHELL,
TopAbs_SOLID,
TopAbs_VERTEX,
TopAbs_WIRE,
)
from OCCT.TopExp import TopExp, TopExp_Explorer
from OCCT.TopoDS import (
TopoDS_Compound,
TopoDS_CompSolid,
TopoDS_Edge,
TopoDS_Face,
TopoDS_Shape,
TopoDS_Shell,
TopoDS_Solid,
TopoDS_Vertex,
TopoDS_Wire,
)
from OCCT.TopTools import (
TopTools_HSequenceOfShape,
TopTools_IndexedDataMapOfShapeListOfShape,
TopTools_ListIteratorOfListOfShape,
TopTools_ListOfShape,
)
from declaracad.occ.geom import BBox
from declaracad.occ.shape import Direction, Point, Shape, coerce_direction, coerce_point
T = TypeVar("T")
DISCRETIZE_METHODS = {
"deflection": GCPnts_UniformDeflection,
"quasi-deflection": GCPnts_QuasiUniformDeflection,
"quasi-abscissa": GCPnts_QuasiUniformAbscissa,
"abscissa": GCPnts_UniformAbscissa,
}
class WireExplorer(Atom):
"""Wire traversal ported from the pythonocc examples by @jf--"""
wire = Instance(TopoDS_Wire)
wire_explorer = Typed(BRepTools_WireExplorer)
def _loop_topo(self, edges: bool = True) -> list:
wexp = self.wire_explorer = BRepTools_WireExplorer(self.wire)
items = set() # list that stores hashes to avoid redundancy
occ_seq = TopTools_ListOfShape()
get_current = wexp.Current if edges else wexp.CurrentVertex
while wexp.More():
current_item = get_current()
if current_item not in items:
items.add(current_item)
occ_seq.Append(current_item)
wexp.Next()
# Convert occ_seq to python list
seq = []
topology_type = TopoDS.Edge if edges else TopoDS.Vertex
occ_iterator = TopTools_ListIteratorOfListOfShape(occ_seq)
while occ_iterator.More():
topo_to_add = topology_type(occ_iterator.Value())
seq.append(topo_to_add)
occ_iterator.Next()
return seq
def ordered_edges(self) -> list[TopoDS_Edge]:
return self._loop_topo(edges=True)
def ordered_vertices(self) -> list[TopoDS_Vertex]:
return self._loop_topo(edges=False)
[docs]
class Topology(Atom):
"""Topology traversal ported from the pythonocc examples by @jf---
Implements topology traversal from any TopoDS_Shape this class lets you
find how various topological entities are connected from one to another
find the faces connected to an edge, find the vertices this edge is
made from, get all faces connected to a vertex, and find out how many
topological elements are connected from a source
Note
----
when traversing TopoDS_Wire entities, its advised to use the
specialized ``WireExplorer`` class, which will return the vertices /
edges in the expected order
"""
#: Maps topology types and functions that can create this topology
topo_factory = {
TopAbs_VERTEX: TopoDS.Vertex,
TopAbs_EDGE: TopoDS.Edge,
TopAbs_FACE: TopoDS.Face,
TopAbs_WIRE: TopoDS.Wire,
TopAbs_SHELL: TopoDS.Shell,
TopAbs_SOLID: TopoDS.Solid,
TopAbs_COMPOUND: TopoDS.Compound,
TopAbs_COMPSOLID: TopoDS.CompSolid,
}
topo_types = {
TopAbs_VERTEX: TopoDS_Vertex,
TopAbs_EDGE: TopoDS_Edge,
TopAbs_FACE: TopoDS_Face,
TopAbs_WIRE: TopoDS_Wire,
TopAbs_SHELL: TopoDS_Shell,
TopAbs_SOLID: TopoDS_Solid,
TopAbs_COMPOUND: TopoDS_Compound,
TopAbs_COMPSOLID: TopoDS_CompSolid,
}
curve_factory = {
GeomAbs_Line: lambda c: GC_MakeSegment(
c.Line(), c.FirstParameter(), c.LastParameter()
).Value(),
GeomAbs_Circle: lambda c: Geom_Circle(c.Circle()),
GeomAbs_Ellipse: lambda c: Geom_Ellipse(c.Ellipse()),
GeomAbs_Hyperbola: lambda c: Geom_Hyperbola(c.Hyperbola()),
GeomAbs_Parabola: lambda c: Geom_Parabola(c.Parabola()),
GeomAbs_BezierCurve: BRepAdaptor_Curve.Bezier,
GeomAbs_BSplineCurve: BRepAdaptor_Curve.BSpline,
GeomAbs_OffsetCurve: BRepAdaptor_Curve.OffsetCurve,
GeomAbs_OtherCurve: lambda c: c,
}
surface_factory = {
GeomAbs_Cylinder: lambda s: s.Cylinder(),
GeomAbs_Cone: lambda s: s.Cone(),
GeomAbs_Plane: lambda s: s.Plane(),
GeomAbs_Sphere: lambda s: s.Sphere(),
GeomAbs_Torus: lambda s: s.Torus(),
}
#: The shape which topology will be traversed
shape = Instance(TopoDS_Shape)
#: Filter out TopoDS_* entities of similar TShape but different orientation
#: for instance, a cube has 24 edges, 4 edges for each of 6 faces
#: that results in 48 vertices, while there are only 8 vertices that have
#: a unique geometric coordinate
#: in certain cases ( computing a graph from the topology ) its preferable
#: to return topological entities that share similar geometry, though
#: differ in orientation by setting the ``ignore_orientation`` variable
#: to True, in case of a cube, just 12 edges and only 8 vertices will be
#: returned
#: for further reference see TopoDS_Shape IsEqual / IsSame methods
ignore_orientation = Bool()
def _loop_topo(
self, topology_type, topological_entity=None, topology_type_to_avoid=None
):
"""this could be a faces generator for a python TopoShape class
that way you can just do:
for face in srf.faces:
processFace(face)
"""
allowed_types = self.topo_types.keys()
if topology_type not in allowed_types:
raise TypeError("%s not one of %s" % (topology_type, allowed_types))
shape = self.shape
if shape is None:
return []
topo_exp = TopExp_Explorer()
# use self.myShape if nothing is specified
if topological_entity is None and topology_type_to_avoid is None:
topo_exp.Init(shape, topology_type)
elif topological_entity is None and topology_type_to_avoid is not None:
topo_exp.Init(shape, topology_type, topology_type_to_avoid)
elif topology_type_to_avoid is None:
topo_exp.Init(topological_entity, topology_type)
elif topology_type_to_avoid:
topo_exp.Init(topological_entity, topology_type, topology_type_to_avoid)
items = set() # list that stores hashes to avoid redundancy
occ_seq = TopTools_ListOfShape()
while topo_exp.More():
current_item = topo_exp.Current()
if current_item not in items:
items.add(current_item)
occ_seq.Append(current_item)
topo_exp.Next()
# Convert occ_seq to python list
seq = []
factory = self.topo_factory[topology_type]
occ_iterator = TopTools_ListIteratorOfListOfShape(occ_seq)
while occ_iterator.More():
topo_to_add = factory(occ_iterator.Value())
seq.append(topo_to_add)
occ_iterator.Next()
if not self.ignore_orientation:
return seq
return Topology.unique_shapes(seq)
@classmethod
def unique_shapes(cls, shapes: Iterable[TopoDS_Shape]) -> list[TopoDS_Shape]:
"""Filter out those entities that share the same TShape
but do *not* share the same orientation. For example if two
overlapping edges with different directions exist only one will be
in the result.
"""
used_shapes: list[TopoDS_Shape] = []
for shape in shapes:
if any(shape.IsSame(s) for s in used_shapes):
continue
used_shapes.append(shape)
return used_shapes
# -------------------------------------------------------------------------
# Shape Topology
# -------------------------------------------------------------------------
faces = List(TopoDS_Face)
def _default_faces(self):
return self._loop_topo(TopAbs_FACE)
vertices = List(TopoDS_Vertex)
# Note: This filters out duplicates
unique_vertices = List(TopoDS_Vertex)
def _default_vertices(self):
if isinstance(self.shape, TopoDS_Wire):
return WireExplorer(wire=self.shape).ordered_vertices()
else:
return self._loop_topo(TopAbs_VERTEX)
def _default_unique_vertices(self):
return Topology.unique_shapes(self.vertices)
#: Get a list of points from vertices
points = List(Point)
def _default_points(self):
return [coerce_point(v) for v in self.vertices]
edges = List(TopoDS_Edge)
def _default_edges(self):
if isinstance(self.shape, TopoDS_Wire):
return WireExplorer(wire=self.shape).ordered_edges()
else:
return self._loop_topo(TopAbs_EDGE)
wires = List(TopoDS_Wire)
def _default_wires(self):
return self._loop_topo(TopAbs_WIRE)
shells = List(TopoDS_Shell)
def _default_shells(self):
return self._loop_topo(TopAbs_SHELL)
solids = List()
def _default_solids(self):
return self._loop_topo(TopAbs_SOLID)
comp_solids = List(TopoDS_CompSolid)
def _default_comp_solids(self):
return self._loop_topo(TopAbs_COMPSOLID)
compounds = List(TopoDS_Compound)
def _default_compounds(self):
return self._loop_topo(TopAbs_COMPOUND)
@classmethod
def ordered_vertices_from_wire(cls, wire: TopoDS_Wire) -> list[TopoDS_Vertex]:
"""Get vertices from a wire.
Parameters
----------
wire: TopoDS_Wire
"""
return WireExplorer(wire=wire).ordered_vertices()
@classmethod
def ordered_edges_from_wire(cls, wire: TopoDS_Wire) -> list[TopoDS_Edge]:
"""Get edges from a wire.
Parameters
----------
wire: TopoDS_Wire
"""
return WireExplorer(wire=wire).ordered_edges()
def _map_shapes_and_ancestors(
self,
topo_type_a: TopAbs_ShapeEnum,
topo_type_b: TopAbs_ShapeEnum,
topo_entity: TopoDS_Shape,
) -> list[TopoDS_Shape]:
"""
using the same method
@param topoTypeA:
@param topoTypeB:
@param topological_entity:
"""
topo_set: set[TopoDS_Shape] = set()
items = []
topo_map = TopTools_IndexedDataMapOfShapeListOfShape()
TopExp.MapShapesAndAncestors_(self.shape, topo_type_a, topo_type_b, topo_map)
topo_results = topo_map.FindFromKey(topo_entity)
if topo_results.IsEmpty():
return []
topology_iterator = TopTools_ListIteratorOfListOfShape(topo_results)
factory = self.topo_factory[topo_type_b]
while topology_iterator.More():
topo_entity = factory(topology_iterator.Value())
# return the entity if not in set
# to assure we're not returning entities several times
if topo_entity not in topo_set:
if self.ignore_orientation:
unique = True
for i in topo_set:
if i.IsSame(topo_entity):
unique = False
break
if unique:
items.append(topo_entity)
else:
items.append(topo_entity)
topo_set.add(topo_entity)
topology_iterator.Next()
return items
# ----------------------------------------------------------------------
# EDGE <-> FACE
# ----------------------------------------------------------------------
def faces_from_edge(self, edge: TopoDS_Edge) -> list[TopoDS_Face]:
return self._map_shapes_and_ancestors(TopAbs_EDGE, TopAbs_FACE, edge)
def edges_from_face(self, face: TopoDS_Face) -> list[TopoDS_Edge]:
return self._loop_topo(TopAbs_EDGE, face)
def find_common_edges(self, shape: TopoDS_Shape) -> set[TopoDS_Edge]:
"""Find edges common to this shape and the given shape
Parameters
----------
shape: TopoDS_Shape
The other shape to find common edges with
Returns
-------
edges: List[TopoDS_Edge]
The edges that are considered the same in both shapes.
"""
common = set()
other_edges = Topology(shape=shape).edges
for edge in self.edges:
for other_edge in other_edges:
if edge.IsSame(other_edge):
common.add(edge)
return common
@classmethod
def find_unique_edges(cls, shapes: Iterable[TopoDS_Shape]) -> set[TopoDS_Edge]:
"""Find edges unique to only one shape in the set of shapes.
Parameters
----------
shapes: Iterable[TopoDS_Shape]
The shapes to find the set of unique edges from.
"""
unique_edges = set()
edge_map = {s: Topology(shape=s).edges for s in shapes}
remaining = [s for s in shapes]
while remaining:
shape = remaining.pop()
for edge in edge_map.pop(shape):
unique = True
for other_shape in remaining:
other_edges = edge_map[other_shape]
for other_edge in other_edges[:]:
if edge.IsSame(other_edge):
other_edges.remove(other_edge)
unique = False
if unique:
unique_edges.add(edge)
return unique_edges
@classmethod
def join_edges(
cls, edges: Iterable[TopoDS_Edge], tolerance=1e-6
) -> list[TopoDS_Wire]:
"""Join a set of edges into a set of wires.
Parameters
----------
edges: Iterable[TopoDS_Edge]
The edges to join
Returns
-------
wires: List[TopoDS_Wire]
The wires connected.
"""
seq = TopTools_HSequenceOfShape()
for e in edges:
seq.Append(e)
r = ShapeAnalysis_FreeBounds.ConnectEdgesToWires_(seq, tolerance, False)
return [Topology.cast_shape(r.Value(i)) for i in range(1, r.Size() + 1)]
# ----------------------------------------------------------------------
# VERTEX <-> EDGE
# ----------------------------------------------------------------------
def vertices_from_edge(self, edge: TopoDS_Edge) -> list[TopoDS_Vertex]:
return self._loop_topo(TopAbs_VERTEX, edge)
def edges_from_vertex(self, vertex: TopoDS_Vertex) -> list[TopoDS_Edge]:
return self._map_shapes_and_ancestors(TopAbs_VERTEX, TopAbs_EDGE, vertex)
def find_common_vertices(self, shape: TopoDS_Shape) -> set[TopoDS_Vertex]:
"""Find edges common to this shape and the given shape
Parameters
----------
shape: TopoDS_Shape
The other shape to find common edges with
Returns
-------
edges: List[TopoDS_Edge]
The edges that are considered the same in both shapes.
"""
common = set()
other_vertices = Topology(shape=shape).vertices
for vertex in self.vertices:
for other_vertex in other_vertices:
if vertex.IsSame(other_vertices):
common.add(vertex)
return common
# ----------------------------------------------------------------------
# WIRE <-> EDGE
# ----------------------------------------------------------------------
def edges_from_wire(self, wire: TopoDS_Wire) -> list[TopoDS_Edge]:
return self._loop_topo(TopAbs_EDGE, wire)
def wires_from_edge(self, edge: TopoDS_Edge) -> list[TopoDS_Wire]:
return self._map_shapes_and_ancestors(TopAbs_EDGE, TopAbs_WIRE, edge)
def wires_from_vertex(self, vertex: TopoDS_Vertex) -> list[TopoDS_Vertex]:
return self._map_shapes_and_ancestors(TopAbs_VERTEX, TopAbs_WIRE, vertex)
# ----------------------------------------------------------------------
# WIRE <-> FACE
# ----------------------------------------------------------------------
def wires_from_face(self, face: TopoDS_Face) -> list[TopoDS_Wire]:
return self._loop_topo(TopAbs_WIRE, face)
def faces_from_wire(self, wire: TopoDS_Wire) -> list[TopoDS_Face]:
return self._map_shapes_and_ancestors(TopAbs_WIRE, TopAbs_FACE, wire)
# ----------------------------------------------------------------------
# VERTEX <-> FACE
# ----------------------------------------------------------------------
def faces_from_vertex(self, vertex: TopoDS_Vertex) -> list[TopoDS_Face]:
return self._map_shapes_and_ancestors(TopAbs_VERTEX, TopAbs_FACE, vertex)
def vertices_from_face(self, face: TopoDS_Face) -> list[TopoDS_Vertex]:
return self._loop_topo(TopAbs_VERTEX, face)
# ----------------------------------------------------------------------
# FACE <-> SOLID
# ----------------------------------------------------------------------
def solids_from_face(self, face: TopoDS_Face) -> list[TopoDS_Solid]:
return self._map_shapes_and_ancestors(TopAbs_FACE, TopAbs_SOLID, face)
def faces_from_solids(self, solid: TopoDS_Solid) -> list[TopoDS_Face]:
return self._loop_topo(TopAbs_FACE, solid)
# -------------------------------------------------------------------------
# Surface Types
# -------------------------------------------------------------------------
def extract_surfaces(
self, surface_type: GeomAbs_SurfaceType
) -> list[dict[str, Any]]:
"""Returns a list of dicts containing the face and surface"""
surfaces = []
for f in self.faces:
surface = self.cast_surface(f, surface_type)
if surface is not None:
surfaces.append({"face": f, "surface": surface})
return surfaces
plane_surfaces = List()
def _default_plane_surfaces(self):
return self.extract_surfaces(GeomAbs_Plane)
sphere_surfaces = List()
def _default_sphere_surfaces(self):
return self.extract_surfaces(GeomAbs_Sphere)
torus_surfaces = List()
def _default_torus_surfaces(self):
return self.extract_surfaces(GeomAbs_Torus)
cone_surfaces = List()
def _default_cone_surfaces(self):
return self.extract_surfaces(GeomAbs_Cone)
cylinder_surfaces = List()
def _default_cylinder_surfaces(self):
return self.extract_surfaces(GeomAbs_Cylinder)
bezier_surfaces = List()
def _default_bezier_surfaces(self):
return self.extract_surfaces(GeomAbs.GeomAbs_BezierSurface)
bspline_surfaces = List()
def _default_bspline_surfaces(self):
return self.extract_surfaces(GeomAbs.GeomAbs_BSplineSurface)
offset_surfaces = List()
def _default_offset_surfaces(self):
return self.extract_surfaces(GeomAbs.GeomAbs_OffsetSurface)
# -------------------------------------------------------------------------
# Curve Types
# -------------------------------------------------------------------------
def extract_curves(self, curve_type):
"""Returns a list of tuples containing the edge and curve"""
curves = []
for e in self.edges:
curve = self.cast_curve(e, curve_type)
if curve is not None:
curves.append({"edge": e, "curve": curve})
return curves
line_curves = List()
def _default_line_curves(self):
return self.extract_curves(GeomAbs_Line)
circle_curves = List()
def _default_circle_curves(self):
return self.extract_curves(GeomAbs_Circle)
ellipse_curves = List()
def _default_ellipse_curves(self):
return self.extract_curves(GeomAbs_Ellipse)
hyperbola_curves = List()
def _default_hyperbola_curves(self):
return self.extract_curves(GeomAbs_Hyperbola)
parabola_cuves = List()
def _default_parabola_cuves(self):
return self.extract_curves(GeomAbs_Parabola)
bezier_curves = List()
def _default_bezier_curves(self):
return self.extract_curves(GeomAbs_BezierCurve)
bspline_curves = List()
def _default_bspline_curves(self):
return self.extract_curves(GeomAbs_BSplineCurve)
offset_curves = List()
def _default_offset_curves(self):
return self.extract_curves(GeomAbs_OffsetCurve)
curves = List()
def _default_curves(self):
return self.extract_curves(None)
# -------------------------------------------------------------------------
# Shape introspection
# -------------------------------------------------------------------------
@classmethod
def cast_shape(cls, shape: TopoDS_Shape) -> Optional[TopoDS_Shape]:
"""Convert a TopoDS_Shape into it's actual type, ex an TopoDS_Edge
Parameters
-----------
topods_shape: TopoDS_Shape
The shape to cas
Returns
-------
shape: Optional[TopoDS_Shape]
The actual shape or None if the value is not a TopoDS_Shape.
"""
if not isinstance(shape, TopoDS_Shape):
return None
return cls.topo_factory[shape.ShapeType()](shape)
@classmethod
def cast_curve(
cls,
shape: Union[TopoDS_Edge, TopoDS_Wire],
expected_type: Optional[GeomAbs_CurveType] = None,
convert: bool = True,
) -> Optional[Union[Geom_Curve, BRepAdaptor_Curve]]:
"""Attempt to cast the shape (an edge or wire) to a curve. If a cast
occurs the UV parameters will be lost.
Parameters
----------
shape: TopoDS_Edge
The shape to cast
expected_type: GeomAbs_CurveType
The type to restrict
Returns
-------
curve: Curve or None
The curve or None if it could not be created or if it was not
of the expected type (if given).
"""
shape = cls.cast_shape(shape)
if isinstance(shape, TopoDS_Edge):
curve = BRepAdaptor_Curve(shape)
elif isinstance(shape, TopoDS_Wire):
curve = BRepAdaptor_CompCurve(shape)
else:
return None
t = curve.GetType()
if expected_type is not None and t != expected_type:
return None
if convert:
factory = cls.curve_factory.get(t)
if factory:
return factory(curve)
return curve
@classmethod
def cast_surface(
cls,
shape: TopoDS_Face,
expected_type: Optional[GeomAbs_SurfaceType] = None,
convert: bool = True,
) -> Optional[Union[Geom_Surface, BRepAdaptor_Surface]]:
"""Attempt to cast the shape (a face) to a surface
Parameters
----------
shape: TopoDS_Face
The shape to cast
expected_type: GeomAbs_SurfaceType
The type to restrict
Returns
-------
surface: BRepAdaptor_Surface or None
The surface or None if it could not be created or did not
match the expected type (if given).
"""
try:
face = TopoDS.Face(shape)
except RuntimeError:
return None
surface = BRepAdaptor_Surface(face, True)
t = surface.GetType()
if expected_type is not None and t != expected_type:
return None
if convert:
factory = cls.surface_factory.get(t)
if factory:
return factory(surface)
return surface
@classmethod
def is_vertex(cls, shape) -> bool:
"""Check if the given shape is a vertex.
Returns
-------
result: Bool
Whether the shape is an vertex.
"""
if isinstance(shape, TopoDS_Shape):
return shape.ShapeType() == TopAbs_VERTEX
return False
@classmethod
def is_edge(cls, shape) -> bool:
"""Check if the given shape is an edge.
Returns
-------
result: Bool
Whether the shape is an edge.
"""
if isinstance(shape, TopoDS_Shape):
return shape.ShapeType() == TopAbs_EDGE
return False
@classmethod
def is_wire(cls, shape) -> bool:
"""Check if the given shape is a face.
Returns
-------
result: Bool
Whether the shape is a face.
"""
if isinstance(shape, TopoDS_Shape):
return shape.ShapeType() == TopAbs_WIRE
return False
@classmethod
def is_face(cls, shape) -> bool:
"""Check if the given shape is a face.
Returns
-------
result: Bool
Whether the shape is a face.
"""
if isinstance(shape, TopoDS_Shape):
return shape.ShapeType() == TopAbs_FACE
return False
@classmethod
def is_shell(cls, shape) -> bool:
"""Check if the given shape is a shell.
Returns
-------
result: Bool
Whether the shape is a shell.
"""
if isinstance(shape, TopoDS_Shape):
return shape.ShapeType() == TopAbs_SHELL
return False
@classmethod
def is_solid(cls, shape) -> bool:
"""Check if the given shape is a solid.
Returns
-------
result: Bool
Whether the shape is a solid.
"""
if isinstance(shape, TopoDS_Shape):
return shape.ShapeType() == TopAbs_SOLID
return False
@classmethod
def is_circle(cls, shape) -> bool:
"""Check if an edge or wire is a part of a circle.
This can be used to see if an edge can be used for radius dimensions.
Returns
-------
bool: Bool
Whether the shape is a part of circle
"""
return (
cls.cast_curve(shape, expected_type=GeomAbs_Circle, convert=False)
is not None
)
@classmethod
def is_ellipse(cls, shape) -> bool:
"""Check if an edge or wire is a part of an ellipse.
This can be used to see if an edge can be used for radius dimensions.
Returns
-------
bool: Bool
Whether the shape is a part of an ellipse
"""
return (
cls.cast_curve(shape, expected_type=GeomAbs_Ellipse, convert=False)
is not None
)
@classmethod
def is_line(cls, shape) -> bool:
"""Check if an edge or wire is a line.
This can be used to see if an edge can be used for length dimensions.
Returns
-------
bool: Bool
Whether the shape is a part of a line
"""
return (
cls.cast_curve(shape, expected_type=GeomAbs_Line, convert=False) is not None
)
@classmethod
def is_bezier_curve(cls, shape) -> bool:
"""Check if an edge or wire is a bezier curve.
Returns
-------
bool: Bool
Whether the shape is a bezier curve
"""
return (
cls.cast_curve(shape, expected_type=GeomAbs_BezierCurve, convert=False)
is not None
)
@classmethod
def is_bspline_curve(cls, shape) -> bool:
"""Check if an edge or wire is a bspline curve.
Returns
-------
bool: Bool
Whether the shape is a bspline curve
"""
return (
cls.cast_curve(shape, expected_type=GeomAbs_BSplineCurve, convert=False)
is not None
)
@classmethod
def is_plane(cls, shape) -> bool:
"""Check if a surface is a plane.
Returns
-------
bool: Bool
Whether the shape is a part of a line
"""
return (
cls.cast_surface(shape, expected_type=GeomAbs_Plane, convert=False)
is not None
)
@classmethod
def is_cylinder(cls, shape) -> bool:
"""Check if a surface is a cylinder.
Returns
-------
result: Bool
Whether the shape is a cylinder
"""
return (
cls.cast_surface(shape, expected_type=GeomAbs_Cylinder, convert=False)
is not None
)
@classmethod
def is_cone(cls, shape) -> bool:
"""Check if a surface is a cone.
Returns
-------
bool: Bool
Whether the shape is a cone
"""
return (
cls.cast_surface(shape, expected_type=GeomAbs_Cone, convert=False)
is not None
)
@classmethod
def is_reversed(cls, shape: TopoDS_Shape) -> bool:
"""Check if the shape's orentation is reversed.
Returns
-------
result: bool
Whether the shape's orentation is reversed.
"""
return shape.Orientation() == TopAbs_REVERSED
@classmethod
def is_clockwise(cls, shape: TopoDS_Shape) -> bool:
"""Check if the face or wire's orientation is clockwise relative
to the postive Z axis.
Returns
-------
result: bool
Whether the shape's orentation is reversed.
"""
if isinstance(shape, TopoDS_Wire):
face = BRepBuilderAPI_MakeFace(shape).Face()
else:
face = shape
return Topology(shape=face).area > 0
# @classmethod
# def is_internal(
# cls,
# shape: TopoDS_Shape,
# ) -> bool:
# """ Check if the edge or wire's orientation is internal or external
# Returns
# -------
# result: bool
# Whether the shape's orentation is internal.
# """
# if isinstance(shape, TopoDS_Wire):
# face = BRepBuilderAPI_MakeFace(shape).Face()
# else:
# face = shape
# props = BRepGProp_Face(face)
# prop.Bounds()
@classmethod
def is_shape_in_list(cls, shape, shapes):
"""Check if an shape is in a list of shapes using the IsSame method.
Parameters
----------
shape: TopoDS_Shape
The shape to check
shapes: Iterable[TopoDS_Shape]
An interable of shapes to check against
Returns
-------
bool: Bool
Whether the shape is in the list
"""
if not isinstance(shape, TopoDS_Shape):
raise TypeError("Expected a TopoDS_Shape instance")
return any(shape.IsSame(s) for s in shapes)
@classmethod
def are_faces_connected(
cls,
face: TopoDS_Face,
other_face: TopoDS_Face,
edges: Optional[Iterable[TopoDS_Edge]] = None,
) -> bool:
"""Check if two faces are connected by one of their edges. This does
NOT work for intersections or shared vertices!
Parameters
----------
face: TopoDS_Face
Face to check connection with
other_face: TopoDS_Face
Face to check connection to
edges: Optional[Iterable[TopoDS_Edge]]
An optional iterable of edges to check. If not provided all edges
from `face` will be used.
Returns
-------
result: bool
True if any of the edges are the same
"""
# TODO: Is there a OCCT function to do this?
other_edges = cls(shape=other_face).edges
for e in edges or cls(shape=face).edges:
if cls.is_shape_in_list(e, other_edges):
return True
return False
@classmethod
def faces_sharing_edges(
cls, face: TopoDS_Face, faces: Iterable[TopoDS_Face]
) -> set[TopoDS_Face]:
"""Return list of faces which have at least one shared
edge with face. The face itself is always excluded.
Parameters
----------
face: TopoDS_Face
The face to look check against
faces: Iterable[TopoDS_Face]
The list of faces to look through.
Returns
-------
results: set[TopoDS_Face]
The set of faces which share an edge
"""
edges = cls(shape=face).edges
return {
f
for f in faces
if f is not face and cls.are_faces_connected(face, f, edges)
}
# -------------------------------------------------------------------------
# Parametrization
# -------------------------------------------------------------------------
@classmethod
def get_value_at(cls, curve, t: float, derivative: int = 0):
return cls.curve_value_at(curve, t, derivative)
@classmethod
def curve_value_at(cls, curve, t: float, derivative: int = 0):
"""Get the value of the curve at parameter t with it's derivatives.
Parameters
----------
curve: BRepAdaptor_Curve
The curve to get the value from
t: Float
The parameter value from 0 to 1
derivative: Int
The derivative from 0 to 4
Returns
-------
results: Point or Tuple
If the derivative is 0 only the position at t is returned,
otherwise a tuple of the position and all deriviatives.
"""
p = gp_Pnt()
if derivative == 0:
curve.D0(t, p)
return coerce_point(p)
v1 = gp_Vec()
if derivative == 1:
curve.D1(t, p, v1)
return (coerce_point(p), coerce_direction(v1))
v2 = gp_Vec()
if derivative == 2:
curve.D1(t, p, v1, v2)
return (coerce_point(p), coerce_direction(v1), coerce_direction(v2))
v3 = gp_Vec()
if derivative == 3:
curve.D3(t, p, v1, v2, v3)
return (
coerce_point(p),
coerce_direction(v1),
coerce_direction(v2),
coerce_direction(v3),
)
raise ValueError(f"Invalid derivative n={derivative}")
@classmethod
def offset_curve_value_at(
cls, curve: BRepAdaptor_Curve, offset: float, t: float, direction: Direction
) -> Point:
"""Compute the offset value"""
offset_curve = Topology.offset_curve(curve, offset, direction)
return Topology.curve_value_at(offset_curve, t)
@classmethod
def offset_curve(
cls, curve: BRepAdaptor_Curve, offset: float, direction: Direction
) -> Geom_OffsetCurve:
"""Create an offset curve for the given curve"""
return Geom_OffsetCurve(curve, offset, direction.proxy)
@classmethod
def surface_value_at(cls, surface, u, v, derivative=0):
"""Get the value of the surface at parameter t with it's derivatives.
Parameters
----------
surface: BRepAdaptor_Surface
The curve to get the value from
t: Float
The parameter value from 0 to 1
derivative: Int
The derivative from 0 to 4
Returns
-------
results: Point or Tuple
If the derivative is 0 only the position at t is returned,
otherwise a tuple of the position and all deriviatives.
"""
p = gp_Pnt()
if derivative == 0:
surface.D0(u, v, p)
return coerce_point(p)
v1 = gp_Vec()
if derivative == 1:
surface.D1(u, v, p, v1)
return (coerce_point(p), coerce_direction(v1))
v2 = gp_Vec()
if derivative == 2:
surface.D1(u, v, p, v1, v2)
return (coerce_point(p), coerce_direction(v1), coerce_direction(v2))
v3 = gp_Vec()
if derivative == 3:
surface.D3(u, v, p, v1, v2, v3)
return (
coerce_point(p),
coerce_direction(v1),
coerce_direction(v2),
coerce_direction(v3),
)
raise ValueError("Invalid derivative")
curve_bounds = Property(cached=True)
def _get_curve_bounds(self) -> tuple[float, float]:
"""Get the U bounds of an edge or wire.
Returns
-------
bounds: Tuple[float, float]
Returns in UMin, UMax parametric space.
"""
curve = self.cast_curve(self.shape, convert=False)
if curve is None:
raise TypeError(f"Cannot get curve bounds of {self.shape}")
return (curve.FirstParameter(), curve.LastParameter())
surface_bounds = Property(cached=True)
def _get_surface_bounds(self) -> tuple[float, float, float, float]:
"""Get the UV bounds of a surface.
Returns
-------
bounds: Tuple[float, float, float, float]
Returns in UMin, UMax, VMin, VMax the bounding values in the
parametric space of F.
"""
shape = self.cast_shape(self.shape)
if not Topology.is_face(shape):
raise TypeError(f"Cannot get surface bounds of {self.shape}")
return BRepTools.UVBounds_(shape, 0, 0, 0, 0)
@classmethod
def discretize(
cls,
shape: Union[TopoDS_Wire, TopoDS_Edge],
deflection: Union[float, int],
method: str = "quasi-deflection",
) -> Iterable[Point]:
"""Convert an edge or wire to points.
Parameters
----------
deflection: Float or Int
Maximum deflection allowed if method is 'deflection' or
'quasi-'defelction' else this is the number of points
n: Int
Number of points to use
methode: Str
A value of either 'deflection' or 'abissca'
Yields
-------
points: Point
A list of points that make up the curve
"""
curve = Topology.cast_curve(shape, convert=False)
if curve is None:
raise TypeError(f"Cannot discretize {shape}")
return Topology.discretize_curve(curve, deflection, method)
@classmethod
def discretize_curve(
cls,
curve: Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve],
deflection: Union[float, int],
method: str = "quasi-deflection",
) -> Iterable[Point]:
"""Convert a curve to points.
Parameters
----------
deflection: Float or Int
Maximum deflection allowed if method is 'deflection' or
'quasi-'defelction' else this is the number of points
n: Int
Number of points to use
method: Str
A value of either 'deflection' or 'abissca'
Yields
-------
points: Point
A list of points that make up the curve
"""
start, end = curve.FirstParameter(), curve.LastParameter()
fn = DISCRETIZE_METHODS[method.lower().replace("uniform", "")]
a = fn(curve, deflection, start, end)
if method.endswith("abscissa"):
def param(i):
return curve.Value(a.Parameter(i))
else:
def param(i):
return a.Value(i)
for i in range(1, a.NbPoints() + 1):
yield coerce_point(param(i))
@classmethod
def parametrize_by_length(
cls, shape: Union[TopoDS_Wire, TopoDS_Edge], length: float
) -> Iterable[tuple[Union[BRepAdaptor_CompCurve, BRepAdaptor_Curve], float]]:
"""Parametrize an edge or wire.
Parameters
----------
length: float
The distance between each parameter
Yields
-------
param: Tuple[BRepAdaptor_Curve, float]
The curve and the parameter
"""
curve = Topology.cast_curve(shape, convert=False)
if curve is None:
raise TypeError(f"Cannot parametrize {shape}")
length = float(length)
start, end = curve.FirstParameter(), curve.LastParameter()
a = GCPnts_UniformAbscissa(curve, length, start, end)
for i in range(1, a.NbPoints() + 1):
yield (curve, a.Parameter(i))
@classmethod
def parametrize_curve_by_length(
cls, curve: Union[BRepAdaptor_CompCurve, BRepAdaptor_Curve], length: float
) -> Iterable[tuple[float]]:
"""Parametrize a curve
Yields
-------
param: Tuple[BRepAdaptor_Curve, float]
The curve and the parameter
"""
length = float(length)
start, end = curve.FirstParameter(), curve.LastParameter()
a = GCPnts_UniformAbscissa(curve, length, start, end)
for i in range(1, a.NbPoints() + 1):
yield a.Parameter(i)
@classmethod
def parameter_at(cls, curve, point: Point) -> list[float]:
"""Determine the parameter from a point on the curve"""
projection = GeomAPI_ProjectPointOnCurve(point.proxy, curve)
return [projection.Parameter(i + 1) for i in range(projection.NbPoints())]
def intersection_parameters(self, curve) -> list[float]:
"""Determine the parameter from a point on the curve"""
result = self.intersection(curve)
if not result:
return []
vertices = Topology(shape=result).vertices
if not vertices:
return []
point = Point(vertices[0])
projection = GeomAPI_ProjectPointOnCurve(
point.proxy, Topology.cast_curve(self.shape)
)
return [projection.Parameter(i + 1) for i in range(projection.NbPoints())]
@classmethod
def bbox(cls, shapes, optimal=False, tolerance=0, enlarge=0):
"""Compute the bounding box of the shape or list of shapes
Parameters
----------
shapes: Shape, TopoDS_Shape or list of them
The shapes to compute the bounding box for
Returns
-------
bbox: BBox
The bounding box of the given shapes
"""
from .occ_shape import coerce_shape
if not shapes:
return BBox()
bbox = Bnd_Box()
bbox.SetGap(tolerance)
if not isinstance(shapes, (list, tuple, set)):
shapes = [shapes]
add = BRepBndLib.AddOptimal_ if optimal else BRepBndLib.Add_
for s in shapes:
add(coerce_shape(s), bbox)
if enlarge:
bbox.Enlarge(enlarge)
pmin, pmax = Point(bbox.CornerMin()), Point(bbox.CornerMax())
return BBox(*pmin, *pmax)
# -------------------------------------------------------------------------
# Edge/Wire Properties
# -------------------------------------------------------------------------
start_point = Property(cached=True)
def _get_start_point(self) -> Point:
"""Get the first / start point of a TopoDS_Wire or TopoDS_Edge."""
shape = Topology.cast_shape(self.shape)
if isinstance(shape, TopoDS_Edge):
curve = BRepAdaptor_Curve(shape)
elif isinstance(shape, TopoDS_Wire):
curve = BRepAdaptor_CompCurve(shape)
else:
raise TypeError(f"Cannot get start point of {shape}")
if Topology.is_reversed(shape):
t = curve.LastParameter()
else:
t = curve.FirstParameter()
return self.get_value_at(curve, t)
end_point = Property(cached=True)
def _get_end_point(self) -> Point:
"""Get the end / last point of a TopoDS_Wire or TopoDS_Edge"""
shape = Topology.cast_shape(self.shape)
if isinstance(shape, TopoDS_Edge):
curve = BRepAdaptor_Curve(shape)
elif isinstance(shape, TopoDS_Wire):
curve = BRepAdaptor_CompCurve(shape)
else:
raise TypeError(f"Cannot get end point of {shape}")
if Topology.is_reversed(shape):
t = curve.FirstParameter()
else:
t = curve.LastParameter()
return self.get_value_at(curve, t)
start_tangent = Property(cached=True)
def _get_start_tangent(self) -> tuple[Point, Direction]:
"""Get the start tangent point and direction"""
shape = Topology.cast_shape(self.shape)
if isinstance(shape, TopoDS_Edge):
curve = BRepAdaptor_Curve(shape)
elif isinstance(shape, TopoDS_Wire):
curve = BRepAdaptor_CompCurve(shape)
else:
raise TypeError(f"Cannot get start tangent of {shape}")
if Topology.is_reversed(shape):
t = curve.LastParameter()
else:
t = curve.FirstParameter()
return self.get_value_at(curve, t, 1)
end_tangent = Property(cached=True)
def _get_end_tangent(self) -> tuple[Point, Direction]:
"""Get the end tangent point and direction"""
shape = Topology.cast_shape(self.shape)
if isinstance(shape, TopoDS_Edge):
curve = BRepAdaptor_Curve(shape)
elif isinstance(shape, TopoDS_Wire):
curve = BRepAdaptor_CompCurve(shape)
else:
raise TypeError(f"Cannot get end tangent of {shape}")
if Topology.is_reversed(shape):
t = curve.FirstParameter()
else:
t = curve.LastParameter()
return self.get_value_at(curve, t, 1)
outer_wire = Property(cached=True)
def _get_outer_wire(self) -> Optional[TopoDS_Wire]:
"""If the shape is a face, return the most outer wire, otherwise None.
Returns
-------
outer_wire: TopoDS_Wire
The outer wire of the face
"""
shape = Topology.cast_shape(self.shape)
if isinstance(shape, TopoDS_Face):
return BRepTools.OuterWire_(shape)
return None
# -------------------------------------------------------------------------
# Shape Properties
# -------------------------------------------------------------------------
length = Property(cached=True)
def _get_length(self):
props = GProp_GProps()
BRepGProp.LinearProperties_(self.shape, props, True)
return props.Mass() # Don't ask
mass = length
center_point = Property(cached=True)
def _get_center_point(self) -> Point:
"""Return the center point of this shape as computed by the bounding
box.
"""
return Topology.bbox(shapes=[self.shape]).center
area = Property(cached=True)
def _get_area(self) -> float:
"""Compute the area of the surface. It may be negative indicating
the direction is reversed.
Returns
-------
area: float
The area of the surface.
"""
props = GProp_GProps()
BRepGProp.SurfaceProperties_(self.shape, props, True)
return props.Mass()
volume = Property(cached=True)
def _get_volume(self) -> float:
"""Volume of a solid.
Properties
----------
"""
props = GProp_GProps()
BRepGProp.VolumeProperties_(self.shape, props, True)
return props.Mass()
# -------------------------------------------------------------------------
# Intersection
# -------------------------------------------------------------------------
def intersection(
self,
shape: Union[Shape, TopoDS_Shape],
multiple: bool = False,
tol: float = 1e-6,
) -> Optional[Union[TopoDS_Shape, list[TopoDS_Shape]]]:
"""Returns the resulting intersection of this and the given shape
or None if an error or an empty list there are no intersections.
Parameters
----------
shape: Union[Shape, TopoDS_Shape]
The shape to intersect with
multiple: bool
If true, return all results from section edges.
Returns
-------
results: Optional[Union[TopoDS_Shape, list[TopoDS_Shape]]]:
The single result or list of intersections depending on the multiple parameter.
"""
from .occ_shape import coerce_shape
other_shape = coerce_shape(shape)
if not other_shape:
return None
op = BRepAlgoAPI_Section(self.shape, other_shape, False)
op.SetFuzzyValue(tol)
op.Build()
if op.HasErrors():
return None
if multiple:
return [Topology.cast_shape(it) for it in op.SectionEdges()]
return Topology.cast_shape(op.Shape())
# -------------------------------------------------------------------------
# Distances
# -------------------------------------------------------------------------
def min_distance_between(self, other_shape: Union[Point, TopoDS_Shape]) -> float:
return self.distance_between(other_shape, "min").Value()
def max_distance_between(self, other_shape: Union[Point, TopoDS_Shape]) -> float:
return self.distance_between(other_shape, "max").Value()
def distance_between(
self, other_shape: Union[Point, TopoDS_Shape], min_max: str = "min"
) -> BRepExtrema_DistShapeShape:
"""Compute the min and max distance between this and the other shape.
Returns
-------
result: BRepExtrema_DistShapeShape
The object
"""
if isinstance(other_shape, Point):
other_shape = BRepBuilderAPI_MakeVertex(other_shape.proxy).Vertex()
if min_max == "min":
flag = Extrema_ExtFlag_MIN
else:
flag = Extrema_ExtFlag_MAX
r = BRepExtrema_DistShapeShape(self.shape, other_shape, flag)
r.SetMultiThread(True)
r.Perform()
return r