Merge branch 'dev' into intersections-2d

This commit is contained in:
Jonathan Wagenet 2025-11-14 14:41:37 -05:00
commit 5ea2dab174
14 changed files with 173 additions and 148 deletions

View file

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img alt="build123d logo" src="docs/assets/build123d_logo/logo-banner.svg"> <img alt="build123d logo" src="https://github.com/gumyr/build123d/raw/dev/docs/assets/build123d_logo/logo-banner.svg">
</p> </p>
[![Documentation Status](https://readthedocs.org/projects/build123d/badge/?version=latest)](https://build123d.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/build123d/badge/?version=latest)](https://build123d.readthedocs.io/en/latest/?badge=latest)

View file

@ -68,7 +68,7 @@ development = [
"black", "black",
"mypy", "mypy",
"pylint", "pylint",
"pytest", "pytest==8.4.2", # TODO: unpin on resolution of pytest-dev/pytest-xdist/issues/1273
"pytest-benchmark", "pytest-benchmark",
"pytest-cov", "pytest-cov",
"pytest-xdist", "pytest-xdist",

View file

@ -758,7 +758,7 @@ class ExportDXF(Export2D):
) )
# need to apply the transform on the geometry level # need to apply the transform on the geometry level
if edge.wrapped is None or edge.location is None: if not edge or edge.location is None:
raise ValueError(f"Edge is empty {edge}.") raise ValueError(f"Edge is empty {edge}.")
t = edge.location.wrapped.Transformation() t = edge.location.wrapped.Transformation()
spline.Transform(t) spline.Transform(t)
@ -1345,7 +1345,7 @@ class ExportSVG(Export2D):
u2 = adaptor.LastParameter() u2 = adaptor.LastParameter()
# Apply the shape location to the geometry. # Apply the shape location to the geometry.
if edge.wrapped is None or edge.location is None: if not edge or edge.location is None:
raise ValueError(f"Edge is empty {edge}.") raise ValueError(f"Edge is empty {edge}.")
t = edge.location.wrapped.Transformation() t = edge.location.wrapped.Transformation()
spline.Transform(t) spline.Transform(t)
@ -1411,7 +1411,7 @@ class ExportSVG(Export2D):
} }
def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]: def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
if edge.wrapped is None: if not edge:
raise ValueError(f"Edge is empty {edge}.") raise ValueError(f"Edge is empty {edge}.")
edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
geom_type = edge.geom_type geom_type = edge.geom_type

View file

@ -295,7 +295,7 @@ class Mesher:
ocp_mesh_vertices.append(pnt) ocp_mesh_vertices.append(pnt)
# Store the triangles from the triangulated faces # Store the triangles from the triangulated faces
if facet.wrapped is None: if not facet:
continue continue
facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED
order = [1, 3, 2] if facet_reversed else [1, 2, 3] order = [1, 3, 2] if facet_reversed else [1, 2, 3]

View file

@ -365,7 +365,7 @@ def chamfer(
if target._dim == 1: if target._dim == 1:
if isinstance(target, BaseLineObject): if isinstance(target, BaseLineObject):
if target.wrapped is None: if not target:
target = Wire([]) # empty wire target = Wire([]) # empty wire
else: else:
target = Wire(target.wrapped) target = Wire(target.wrapped)
@ -465,7 +465,7 @@ def fillet(
if target._dim == 1: if target._dim == 1:
if isinstance(target, BaseLineObject): if isinstance(target, BaseLineObject):
if target.wrapped is None: if not target:
target = Wire([]) # empty wire target = Wire([]) # empty wire
else: else:
target = Wire(target.wrapped) target = Wire(target.wrapped)

View file

@ -128,7 +128,7 @@ from .utils import (
from .zero_d import Vertex from .zero_d import Vertex
class Compound(Mixin3D, Shape[TopoDS_Compound]): class Compound(Mixin3D[TopoDS_Compound]):
"""A Compound in build123d is a topological entity representing a collection of """A Compound in build123d is a topological entity representing a collection of
geometric shapes grouped together within a single structure. It serves as a geometric shapes grouped together within a single structure. It serves as a
container for organizing diverse shapes like edges, faces, or solids. This container for organizing diverse shapes like edges, faces, or solids. This
@ -453,7 +453,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
will be a Wire, otherwise a Shape. will be a Wire, otherwise a Shape.
""" """
if self._dim == 1: if self._dim == 1:
curve = Curve() if self.wrapped is None else Curve(self.wrapped) curve = Curve() if self._wrapped is None else Curve(self.wrapped)
sum1d: Edge | Wire | ShapeList[Edge] = curve + other sum1d: Edge | Wire | ShapeList[Edge] = curve + other
if isinstance(sum1d, ShapeList): if isinstance(sum1d, ShapeList):
result1d: Curve | Wire = Curve(sum1d) result1d: Curve | Wire = Curve(sum1d)
@ -515,7 +515,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
Check if empty. Check if empty.
""" """
return TopoDS_Iterator(self.wrapped).More() return self._wrapped is not None and TopoDS_Iterator(self.wrapped).More()
def __iter__(self) -> Iterator[Shape]: def __iter__(self) -> Iterator[Shape]:
""" """
@ -532,7 +532,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
def __len__(self) -> int: def __len__(self) -> int:
"""Return the number of subshapes""" """Return the number of subshapes"""
count = 0 count = 0
if self.wrapped is not None: if self._wrapped is not None:
for _ in self: for _ in self:
count += 1 count += 1
return count return count
@ -600,7 +600,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
def compounds(self) -> ShapeList[Compound]: def compounds(self) -> ShapeList[Compound]:
"""compounds - all the compounds in this Shape""" """compounds - all the compounds in this Shape"""
if self.wrapped is None: if self._wrapped is None:
return ShapeList() return ShapeList()
if isinstance(self.wrapped, TopoDS_Compound): if isinstance(self.wrapped, TopoDS_Compound):
# pylint: disable=not-an-iterable # pylint: disable=not-an-iterable

View file

@ -174,7 +174,7 @@ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[
- Edge -> (QualifiedCurve, h2d, first, last, True) - Edge -> (QualifiedCurve, h2d, first, last, True)
- Vector -> (CartesianPoint, None, None, None, False) - Vector -> (CartesianPoint, None, None, None, False)
""" """
if obj.wrapped is None: if not obj:
raise TypeError("Can't create a qualified curve from empty edge") raise TypeError("Can't create a qualified curve from empty edge")
if isinstance(obj.wrapped, TopoDS_Edge): if isinstance(obj.wrapped, TopoDS_Edge):

View file

@ -216,6 +216,7 @@ from build123d.geometry import (
) )
from .shape_core import ( from .shape_core import (
TOPODS,
Shape, Shape,
ShapeList, ShapeList,
SkipClean, SkipClean,
@ -249,7 +250,7 @@ if TYPE_CHECKING: # pragma: no cover
from .two_d import Face, Shell # pylint: disable=R0801 from .two_d import Face, Shell # pylint: disable=R0801
class Mixin1D(Shape): class Mixin1D(Shape[TOPODS]):
"""Methods to add to the Edge and Wire classes""" """Methods to add to the Edge and Wire classes"""
# ---- Properties ---- # ---- Properties ----
@ -262,14 +263,14 @@ class Mixin1D(Shape):
@property @property
def is_closed(self) -> bool: def is_closed(self) -> bool:
"""Are the start and end points equal?""" """Are the start and end points equal?"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't determine if empty Edge or Wire is closed") raise ValueError("Can't determine if empty Edge or Wire is closed")
return BRep_Tool.IsClosed_s(self.wrapped) return BRep_Tool.IsClosed_s(self.wrapped)
@property @property
def is_forward(self) -> bool: def is_forward(self) -> bool:
"""Does the Edge/Wire loop forward or reverse""" """Does the Edge/Wire loop forward or reverse"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't determine direction of empty Edge or Wire") raise ValueError("Can't determine direction of empty Edge or Wire")
return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD
@ -387,8 +388,7 @@ class Mixin1D(Shape):
shape shape
# for o in (other if isinstance(other, (list, tuple)) else [other]) # for o in (other if isinstance(other, (list, tuple)) else [other])
for o in ([other] if isinstance(other, Shape) else other) for o in ([other] if isinstance(other, Shape) else other)
if o is not None for shape in get_top_level_topods_shapes(o.wrapped if o else None)
for shape in get_top_level_topods_shapes(o.wrapped)
] ]
# If there is nothing to add return the original object # If there is nothing to add return the original object
if not topods_summands: if not topods_summands:
@ -403,7 +403,7 @@ class Mixin1D(Shape):
) )
summand_edges = [e for summand in summands for e in summand.edges()] summand_edges = [e for summand in summands for e in summand.edges()]
if self.wrapped is None: # an empty object if self._wrapped is None: # an empty object
if len(summands) == 1: if len(summands) == 1:
sum_shape: Edge | Wire | ShapeList[Edge] = summands[0] sum_shape: Edge | Wire | ShapeList[Edge] = summands[0]
else: else:
@ -451,7 +451,7 @@ class Mixin1D(Shape):
Returns: Returns:
Vector: center Vector: center
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find center of empty edge/wire") raise ValueError("Can't find center of empty edge/wire")
if center_of == CenterOf.GEOMETRY: if center_of == CenterOf.GEOMETRY:
@ -577,7 +577,7 @@ class Mixin1D(Shape):
>>> show(my_wire, Curve(comb)) >>> show(my_wire, Curve(comb))
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't create curvature_comb for empty curve") raise ValueError("Can't create curvature_comb for empty curve")
pln = self.common_plane() pln = self.common_plane()
if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE): if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE):
@ -971,7 +971,7 @@ class Mixin1D(Shape):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find normal of empty edge/wire") raise ValueError("Can't find normal of empty edge/wire")
curve = self.geom_adaptor() curve = self.geom_adaptor()
@ -1205,7 +1205,7 @@ class Mixin1D(Shape):
Returns: Returns:
""" """
if self.wrapped is None or face.wrapped is None: if self._wrapped is None or not face:
raise ValueError("Can't project an empty Edge or Wire onto empty Face") raise ValueError("Can't project an empty Edge or Wire onto empty Face")
bldr = BRepProj_Projection( bldr = BRepProj_Projection(
@ -1277,7 +1277,7 @@ class Mixin1D(Shape):
return edges return edges
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't project empty edge/wire") raise ValueError("Can't project empty edge/wire")
# Setup the projector # Setup the projector
@ -1380,7 +1380,7 @@ class Mixin1D(Shape):
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is - **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
either a `Self` or `list[Self]`, or `None` if no corresponding part is found. either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
""" """
if self.wrapped is None or tool.wrapped is None: if self._wrapped is None or not tool:
raise ValueError("Can't split an empty edge/wire/tool") raise ValueError("Can't split an empty edge/wire/tool")
shape_list = TopTools_ListOfShape() shape_list = TopTools_ListOfShape()
@ -1546,7 +1546,7 @@ class Mixin1D(Shape):
return Shape.get_shape_list(self, "Wire") return Shape.get_shape_list(self, "Wire")
class Edge(Mixin1D, Shape[TopoDS_Edge]): class Edge(Mixin1D[TopoDS_Edge]):
"""An Edge in build123d is a fundamental element in the topological data structure """An Edge in build123d is a fundamental element in the topological data structure
representing a one-dimensional geometric entity within a 3D model. It encapsulates representing a one-dimensional geometric entity within a 3D model. It encapsulates
information about a curve, which could be a line, arc, or other parametrically information about a curve, which could be a line, arc, or other parametrically
@ -1627,7 +1627,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
Edge: extruded shape Edge: extruded shape
""" """
if obj.wrapped is None: if not obj:
raise ValueError("Can't extrude empty vertex") raise ValueError("Can't extrude empty vertex")
return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction))) return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction)))
@ -2518,7 +2518,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
extension_factor: float = 0.1, extension_factor: float = 0.1,
): ):
"""Helper method to slightly extend an edge that is bound to a surface""" """Helper method to slightly extend an edge that is bound to a surface"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't extend empty spline") raise ValueError("Can't extend empty spline")
if self.geom_type != GeomType.BSPLINE: if self.geom_type != GeomType.BSPLINE:
raise TypeError("_extend_spline only works with splines") raise TypeError("_extend_spline only works with splines")
@ -2575,7 +2575,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
ShapeList[Vector]: list of intersection points ShapeList[Vector]: list of intersection points
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find intersections of empty edge") raise ValueError("Can't find intersections of empty edge")
# Convert an Axis into an edge at least as large as self and Axis start point # Convert an Axis into an edge at least as large as self and Axis start point
@ -2703,7 +2703,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
def geom_adaptor(self) -> BRepAdaptor_Curve: def geom_adaptor(self) -> BRepAdaptor_Curve:
"""Return the Geom Curve from this Edge""" """Return the Geom Curve from this Edge"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find adaptor for empty edge") raise ValueError("Can't find adaptor for empty edge")
return BRepAdaptor_Curve(self.wrapped) return BRepAdaptor_Curve(self.wrapped)
@ -2791,7 +2791,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
float: Normalized parameter in [0.0, 1.0] corresponding to the point's float: Normalized parameter in [0.0, 1.0] corresponding to the point's
closest location on the edge. closest location on the edge.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find param on empty edge") raise ValueError("Can't find param on empty edge")
pnt = Vector(point) pnt = Vector(point)
@ -2925,7 +2925,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
Edge: reversed Edge: reversed
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("An empty edge can't be reversed") raise ValueError("An empty edge can't be reversed")
assert isinstance(self.wrapped, TopoDS_Edge) assert isinstance(self.wrapped, TopoDS_Edge)
@ -3005,7 +3005,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
# if start_u >= end_u: # if start_u >= end_u:
# raise ValueError(f"start ({start_u}) must be less than end ({end_u})") # raise ValueError(f"start ({start_u}) must be less than end ({end_u})")
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't trim empty edge") raise ValueError("Can't trim empty edge")
self_copy = copy.deepcopy(self) self_copy = copy.deepcopy(self)
@ -3040,7 +3040,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
Edge: trimmed edge Edge: trimmed edge
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't trim empty edge") raise ValueError("Can't trim empty edge")
start_u = Mixin1D._to_param(self, start, "start") start_u = Mixin1D._to_param(self, start, "start")
@ -3069,7 +3069,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
return Edge(new_edge) return Edge(new_edge)
class Wire(Mixin1D, Shape[TopoDS_Wire]): class Wire(Mixin1D[TopoDS_Wire]):
"""A Wire in build123d is a topological entity representing a connected sequence """A Wire in build123d is a topological entity representing a connected sequence
of edges forming a continuous curve or path in 3D space. Wires are essential of edges forming a continuous curve or path in 3D space. Wires are essential
components in modeling complex objects, defining boundaries for surfaces or components in modeling complex objects, defining boundaries for surfaces or
@ -3603,7 +3603,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: chamfered wire Wire: chamfered wire
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't chamfer empty wire") raise ValueError("Can't chamfer empty wire")
reference_edge = edge reference_edge = edge
@ -3618,7 +3618,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
) )
for v in vertices: for v in vertices:
if v.wrapped is None: if not v:
continue continue
edge_list = vertex_edge_map.FindFromKey(v.wrapped) edge_list = vertex_edge_map.FindFromKey(v.wrapped)
@ -3675,7 +3675,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: filleted wire Wire: filleted wire
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't fillet an empty wire") raise ValueError("Can't fillet an empty wire")
# Create a face to fillet # Create a face to fillet
@ -3703,7 +3703,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: fixed wire Wire: fixed wire
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't fix an empty edge") raise ValueError("Can't fix an empty edge")
sf_w = ShapeFix_Wireframe(self.wrapped) sf_w = ShapeFix_Wireframe(self.wrapped)
@ -3715,7 +3715,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
def geom_adaptor(self) -> BRepAdaptor_CompCurve: def geom_adaptor(self) -> BRepAdaptor_CompCurve:
"""Return the Geom Comp Curve for this Wire""" """Return the Geom Comp Curve for this Wire"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't get geom adaptor of empty wire") raise ValueError("Can't get geom adaptor of empty wire")
return BRepAdaptor_CompCurve(self.wrapped) return BRepAdaptor_CompCurve(self.wrapped)
@ -3759,7 +3759,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
float: Normalized parameter in [0.0, 1.0] representing the relative float: Normalized parameter in [0.0, 1.0] representing the relative
position of the projected point along the wire. position of the projected point along the wire.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find point on empty wire") raise ValueError("Can't find point on empty wire")
point_on_curve = Vector(point) point_on_curve = Vector(point)
@ -3912,7 +3912,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
""" """
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
if self.wrapped is None or target_object.wrapped is None: if self._wrapped is None or not target_object:
raise ValueError("Can't project empty Wires or to empty Shapes") raise ValueError("Can't project empty Wires or to empty Shapes")
if direction is not None and center is None: if direction is not None and center is None:
@ -4001,7 +4001,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns: Returns:
Wire: stitched wires Wire: stitched wires
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
raise ValueError("Can't stitch empty wires") raise ValueError("Can't stitch empty wires")
wire_builder = BRepBuilderAPI_MakeWire() wire_builder = BRepBuilderAPI_MakeWire()
@ -4045,7 +4045,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
""" """
# Build a single Geom_BSplineCurve from the wire, in *topological order* # Build a single Geom_BSplineCurve from the wire, in *topological order*
builder = GeomConvert_CompCurveToBSplineCurve() builder = GeomConvert_CompCurveToBSplineCurve()
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't convert an empty wire") raise ValueError("Can't convert an empty wire")
wire_explorer = BRepTools_WireExplorer(self.wrapped) wire_explorer = BRepTools_WireExplorer(self.wrapped)
@ -4197,9 +4197,9 @@ def topo_explore_connected_edges(
parent = parent if parent is not None else edge.topo_parent parent = parent if parent is not None else edge.topo_parent
if parent is None: if parent is None:
raise ValueError("edge has no valid parent") raise ValueError("edge has no valid parent")
given_topods_edge = edge.wrapped if not edge:
if given_topods_edge is None:
raise ValueError("edge is empty") raise ValueError("edge is empty")
given_topods_edge = edge.wrapped
connected_edges = set() connected_edges = set()
# Find all the TopoDS_Edges for this Shape # Find all the TopoDS_Edges for this Shape
@ -4242,11 +4242,11 @@ def topo_explore_connected_faces(
) -> list[TopoDS_Face]: ) -> list[TopoDS_Face]:
"""Given an edge extracted from a Shape, return the topods_faces connected to it""" """Given an edge extracted from a Shape, return the topods_faces connected to it"""
if edge.wrapped is None: if not edge:
raise ValueError("Can't explore from an empty edge") raise ValueError("Can't explore from an empty edge")
parent = parent if parent is not None else edge.topo_parent parent = parent if parent is not None else edge.topo_parent
if parent is None or parent.wrapped is None: if not parent:
raise ValueError("edge has no valid parent") raise ValueError("edge has no valid parent")
# make a edge --> faces mapping # make a edge --> faces mapping

View file

@ -287,7 +287,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
color: ColorLike | None = None, color: ColorLike | None = None,
parent: Compound | None = None, parent: Compound | None = None,
): ):
self.wrapped: TOPODS | None = ( self._wrapped: TOPODS | None = (
tcast(Optional[TOPODS], downcast(obj)) if obj is not None else None tcast(Optional[TOPODS], downcast(obj)) if obj is not None else None
) )
self.for_construction = False self.for_construction = False
@ -304,6 +304,18 @@ class Shape(NodeMixin, Generic[TOPODS]):
# pylint: disable=too-many-instance-attributes, too-many-public-methods # pylint: disable=too-many-instance-attributes, too-many-public-methods
@property
def wrapped(self):
assert self._wrapped
return self._wrapped
@wrapped.setter
def wrapped(self, shape: TOPODS):
self._wrapped = shape
def __bool__(self):
return self._wrapped is not None
@property @property
@abstractmethod @abstractmethod
def _dim(self) -> int | None: def _dim(self) -> int | None:
@ -312,7 +324,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property @property
def area(self) -> float: def area(self) -> float:
"""area -the surface area of all faces in this Shape""" """area -the surface area of all faces in this Shape"""
if self.wrapped is None: if self._wrapped is None:
return 0.0 return 0.0
properties = GProp_GProps() properties = GProp_GProps()
BRepGProp.SurfaceProperties_s(self.wrapped, properties) BRepGProp.SurfaceProperties_s(self.wrapped, properties)
@ -351,7 +363,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
GeomType: The geometry type of the shape GeomType: The geometry type of the shape
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot determine geometry type of an empty shape") raise ValueError("Cannot determine geometry type of an empty shape")
shape: TopAbs_ShapeEnum = shapetype(self.wrapped) shape: TopAbs_ShapeEnum = shapetype(self.wrapped)
@ -380,7 +392,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
bool: is the shape manifold or water tight bool: is the shape manifold or water tight
""" """
# Extract one or more (if a Compound) shape from self # Extract one or more (if a Compound) shape from self
if self.wrapped is None: if self._wrapped is None:
return False return False
shape_stack = get_top_level_topods_shapes(self.wrapped) shape_stack = get_top_level_topods_shapes(self.wrapped)
@ -431,12 +443,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
underlying shape with the potential to be given a location and an underlying shape with the potential to be given a location and an
orientation. orientation.
""" """
return self.wrapped is None or self.wrapped.IsNull() return self._wrapped is None or self.wrapped.IsNull()
@property @property
def is_planar_face(self) -> bool: def is_planar_face(self) -> bool:
"""Is the shape a planar face even though its geom_type may not be PLANE""" """Is the shape a planar face even though its geom_type may not be PLANE"""
if self.wrapped is None or not isinstance(self.wrapped, TopoDS_Face): if self._wrapped is None or not isinstance(self.wrapped, TopoDS_Face):
return False return False
surface = BRep_Tool.Surface_s(self.wrapped) surface = BRep_Tool.Surface_s(self.wrapped)
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE) is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
@ -448,7 +460,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked. description of what is checked.
""" """
if self.wrapped is None: if self._wrapped is None:
return True return True
chk = BRepCheck_Analyzer(self.wrapped) chk = BRepCheck_Analyzer(self.wrapped)
chk.SetParallel(True) chk.SetParallel(True)
@ -474,7 +486,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property @property
def location(self) -> Location: def location(self) -> Location:
"""Get this Shape's Location""" """Get this Shape's Location"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't find the location of an empty shape") raise ValueError("Can't find the location of an empty shape")
return Location(self.wrapped.Location()) return Location(self.wrapped.Location())
@ -518,7 +530,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
- It is commonly used in structural analysis, mechanical simulations, - It is commonly used in structural analysis, mechanical simulations,
and physics-based motion calculations. and physics-based motion calculations.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate matrix for empty shape") raise ValueError("Can't calculate matrix for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
BRepGProp.VolumeProperties_s(self.wrapped, properties) BRepGProp.VolumeProperties_s(self.wrapped, properties)
@ -546,7 +558,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property @property
def position(self) -> Vector: def position(self) -> Vector:
"""Get the position component of this Shape's Location""" """Get the position component of this Shape's Location"""
if self.wrapped is None or self.location is None: if self._wrapped is None or self.location is None:
raise ValueError("Can't find the position of an empty shape") raise ValueError("Can't find the position of an empty shape")
return self.location.position return self.location.position
@ -575,7 +587,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(Vector(0, 1, 0), 1000.0), (Vector(0, 1, 0), 1000.0),
(Vector(0, 0, 1), 300.0)] (Vector(0, 0, 1), 300.0)]
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate properties for empty shape") raise ValueError("Can't calculate properties for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
@ -615,7 +627,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(150.0, 200.0, 50.0) (150.0, 200.0, 50.0)
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate moments for empty shape") raise ValueError("Can't calculate moments for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
@ -785,7 +797,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if obj.wrapped is None: if not obj:
return 0.0 return 0.0
properties = GProp_GProps() properties = GProp_GProps()
@ -805,7 +817,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
], ],
) -> ShapeList: ) -> ShapeList:
"""Helper to extract entities of a specific type from a shape.""" """Helper to extract entities of a specific type from a shape."""
if shape.wrapped is None: if not shape:
return ShapeList() return ShapeList()
shape_list = ShapeList( shape_list = ShapeList(
[shape.__class__.cast(i) for i in shape.entities(entity_type)] [shape.__class__.cast(i) for i in shape.entities(entity_type)]
@ -859,7 +871,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
if not all(summand._dim == addend_dim for summand in summands): if not all(summand._dim == addend_dim for summand in summands):
raise ValueError("Only shapes with the same dimension can be added") raise ValueError("Only shapes with the same dimension can be added")
if self.wrapped is None: # an empty object if self._wrapped is None: # an empty object
if len(summands) == 1: if len(summands) == 1:
sum_shape = summands[0] sum_shape = summands[0]
else: else:
@ -876,7 +888,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"""intersect shape with self operator &""" """intersect shape with self operator &"""
others = other if isinstance(other, (list, tuple)) else [other] others = other if isinstance(other, (list, tuple)) else [other]
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None): if not self or (isinstance(other, Shape) and not other):
raise ValueError("Cannot intersect shape with empty compound") raise ValueError("Cannot intersect shape with empty compound")
new_shape = self.intersect(*others) new_shape = self.intersect(*others)
@ -948,7 +960,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def __hash__(self) -> int: def __hash__(self) -> int:
"""Return hash code""" """Return hash code"""
if self.wrapped is None: if self._wrapped is None:
return 0 return 0
return hash(self.wrapped) return hash(self.wrapped)
@ -966,7 +978,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]: def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]:
"""cut shape from self operator -""" """cut shape from self operator -"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot subtract shape from empty compound") raise ValueError("Cannot subtract shape from empty compound")
# Convert `other` to list of base objects and filter out None values # Convert `other` to list of base objects and filter out None values
@ -1014,7 +1026,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
BoundBox: A box sized to contain this Shape BoundBox: A box sized to contain this Shape
""" """
if self.wrapped is None: if self._wrapped is None:
return BoundBox(Bnd_Box()) return BoundBox(Bnd_Box())
tolerance = TOLERANCE if tolerance is None else tolerance tolerance = TOLERANCE if tolerance is None else tolerance
return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal) return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal)
@ -1033,7 +1045,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: Original object with extraneous internal edges removed Shape: Original object with extraneous internal edges removed
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True) upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
upgrader.AllowInternalEdges(False) upgrader.AllowInternalEdges(False)
@ -1112,7 +1124,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value() return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
@ -1125,7 +1137,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
self, other: Shape | VectorLike self, other: Shape | VectorLike
) -> tuple[float, Vector, Vector]: ) -> tuple[float, Vector, Vector]:
"""Minimal distance between two shapes and the points on each shape""" """Minimal distance between two shapes and the points on each shape"""
if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None): if self._wrapped is None or (isinstance(other, Shape) and not other):
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
if isinstance(other, Shape): if isinstance(other, Shape):
@ -1155,14 +1167,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
dist_calc = BRepExtrema_DistShapeShape() dist_calc = BRepExtrema_DistShapeShape()
dist_calc.LoadS1(self.wrapped) dist_calc.LoadS1(self.wrapped)
for other_shape in others: for other_shape in others:
if other_shape.wrapped is None: if not other_shape:
raise ValueError("Cannot calculate distance to or from an empty shape") raise ValueError("Cannot calculate distance to or from an empty shape")
dist_calc.LoadS2(other_shape.wrapped) dist_calc.LoadS2(other_shape.wrapped)
dist_calc.Perform() dist_calc.Perform()
@ -1181,7 +1193,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]: def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
"""Return all of the TopoDS sub entities of the given type""" """Return all of the TopoDS sub entities of the given type"""
if self.wrapped is None: if self._wrapped is None:
return [] return []
return _topods_entities(self.wrapped, topo_type) return _topods_entities(self.wrapped, topo_type)
@ -1209,7 +1221,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
list[Face]: A list of intersected faces sorted by distance from axis.position list[Face]: A list of intersected faces sorted by distance from axis.position
""" """
if self.wrapped is None: if self._wrapped is None:
return ShapeList() return ShapeList()
line = gce_MakeLin(axis.wrapped).Value() line = gce_MakeLin(axis.wrapped).Value()
@ -1239,7 +1251,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def fix(self) -> Self: def fix(self) -> Self:
"""fix - try to fix shape if not valid""" """fix - try to fix shape if not valid"""
if self.wrapped is None: if self._wrapped is None:
return self return self
if not self.is_valid: if not self.is_valid:
shape_copy: Shape = copy.deepcopy(self, None) shape_copy: Shape = copy.deepcopy(self, None)
@ -1281,7 +1293,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
# self, child_type: Shapes, parent_type: Shapes # self, child_type: Shapes, parent_type: Shapes
# ) -> Dict[Shape, list[Shape]]: # ) -> Dict[Shape, list[Shape]]:
# """This function is very slow on M1 macs and is currently unused""" # """This function is very slow on M1 macs and is currently unused"""
# if self.wrapped is None: # if self._wrapped is None:
# return {} # return {}
# res = TopTools_IndexedDataMapOfShapeListOfShape() # res = TopTools_IndexedDataMapOfShapeListOfShape()
@ -1319,7 +1331,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(e.g., edges, vertices) and other compounds, the method returns a list (e.g., edges, vertices) and other compounds, the method returns a list
of only the simple shapes directly contained at the top level. of only the simple shapes directly contained at the top level.
""" """
if self.wrapped is None: if self._wrapped is None:
return ShapeList() return ShapeList()
return ShapeList( return ShapeList(
self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped) self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped)
@ -1398,7 +1410,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
return False return False
return self.wrapped.IsEqual(other.wrapped) return self.wrapped.IsEqual(other.wrapped)
@ -1413,7 +1425,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
return False return False
return self.wrapped.IsSame(other.wrapped) return self.wrapped.IsSame(other.wrapped)
@ -1426,7 +1438,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot locate an empty shape") raise ValueError("Cannot locate an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot locate a shape at an empty location") raise ValueError("Cannot locate a shape at an empty location")
@ -1445,7 +1457,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of Shape at location Shape: copy of Shape at location
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot locate an empty shape") raise ValueError("Cannot locate an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot locate a shape at an empty location") raise ValueError("Cannot locate a shape at an empty location")
@ -1463,7 +1475,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot mesh an empty shape") raise ValueError("Cannot mesh an empty shape")
if not BRepTools.Triangulation_s(self.wrapped, tolerance): if not BRepTools.Triangulation_s(self.wrapped, tolerance):
@ -1484,7 +1496,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
if not mirror_plane: if not mirror_plane:
mirror_plane = Plane.XY mirror_plane = Plane.XY
if self.wrapped is None: if self._wrapped is None:
return self return self
transformation = gp_Trsf() transformation = gp_Trsf()
transformation.SetMirror( transformation.SetMirror(
@ -1502,7 +1514,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot move an empty shape") raise ValueError("Cannot move an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot move a shape at an empty location") raise ValueError("Cannot move a shape at an empty location")
@ -1522,7 +1534,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of Shape moved to relative location Shape: copy of Shape moved to relative location
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot move an empty shape") raise ValueError("Cannot move an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot move a shape at an empty location") raise ValueError("Cannot move a shape at an empty location")
@ -1536,7 +1548,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
OrientedBoundBox: A box oriented and sized to contain this Shape OrientedBoundBox: A box oriented and sized to contain this Shape
""" """
if self.wrapped is None: if self._wrapped is None:
return OrientedBoundBox(Bnd_OBB()) return OrientedBoundBox(Bnd_OBB())
return OrientedBoundBox(self) return OrientedBoundBox(self)
@ -1638,7 +1650,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
- The radius of gyration is computed based on the shapes mass properties. - The radius of gyration is computed based on the shapes mass properties.
- It is useful for evaluating structural stability and rotational behavior. - It is useful for evaluating structural stability and rotational behavior.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't calculate radius of gyration for empty shape") raise ValueError("Can't calculate radius of gyration for empty shape")
properties = GProp_GProps() properties = GProp_GProps()
@ -1657,7 +1669,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot relocate an empty shape") raise ValueError("Cannot relocate an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
raise ValueError("Cannot relocate a shape at an empty location") raise ValueError("Cannot relocate a shape at an empty location")
@ -1852,7 +1864,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH" "keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH"
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot split an empty shape") raise ValueError("Cannot split an empty shape")
# Process the perimeter # Process the perimeter
@ -1860,7 +1872,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
raise ValueError("perimeter must be a closed Wire or Edge") raise ValueError("perimeter must be a closed Wire or Edge")
perimeter_edges = TopTools_SequenceOfShape() perimeter_edges = TopTools_SequenceOfShape()
for perimeter_edge in perimeter.edges(): for perimeter_edge in perimeter.edges():
if perimeter_edge.wrapped is None: if not perimeter_edge:
continue continue
perimeter_edges.Append(perimeter_edge.wrapped) perimeter_edges.Append(perimeter_edge.wrapped)
@ -1868,7 +1880,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
lefts: list[Shell] = [] lefts: list[Shell] = []
rights: list[Shell] = [] rights: list[Shell] = []
for target_shell in self.shells(): for target_shell in self.shells():
if target_shell.wrapped is None: if not target_shell:
continue continue
constructor = BRepFeat_SplitShape(target_shell.wrapped) constructor = BRepFeat_SplitShape(target_shell.wrapped)
constructor.Add(perimeter_edges) constructor.Add(perimeter_edges)
@ -1897,7 +1909,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
self, tolerance: float, angular_tolerance: float = 0.1 self, tolerance: float, angular_tolerance: float = 0.1
) -> tuple[list[Vector], list[tuple[int, int, int]]]: ) -> tuple[list[Vector], list[tuple[int, int, int]]]:
"""General triangulated approximation""" """General triangulated approximation"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot tessellate an empty shape") raise ValueError("Cannot tessellate an empty shape")
self.mesh(tolerance, angular_tolerance) self.mesh(tolerance, angular_tolerance)
@ -1959,7 +1971,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Self: Approximated shape Self: Approximated shape
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot approximate an empty shape") raise ValueError("Cannot approximate an empty shape")
params = ShapeCustom_RestrictionParameters() params = ShapeCustom_RestrictionParameters()
@ -1996,7 +2008,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: a copy of the object, but with geometry transformed Shape: a copy of the object, but with geometry transformed
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
new_shape = copy.deepcopy(self, None) new_shape = copy.deepcopy(self, None)
transformed = downcast( transformed = downcast(
@ -2019,7 +2031,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of transformed shape with all objects keeping their type Shape: copy of transformed shape with all objects keeping their type
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
new_shape = copy.deepcopy(self, None) new_shape = copy.deepcopy(self, None)
transformed = downcast( transformed = downcast(
@ -2092,7 +2104,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
Shape: copy of transformed Shape Shape: copy of transformed Shape
""" """
if self.wrapped is None: if self._wrapped is None:
return self return self
shape_copy: Shape = copy.deepcopy(self, None) shape_copy: Shape = copy.deepcopy(self, None)
transformed_shape = BRepBuilderAPI_Transform( transformed_shape = BRepBuilderAPI_Transform(
@ -2201,7 +2213,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns: Returns:
tuple[ShapeList[Vertex], ShapeList[Edge]]: section results tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
""" """
if self.wrapped is None or other.wrapped is None: if self._wrapped is None or not other:
return (ShapeList(), ShapeList()) return (ShapeList(), ShapeList())
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped) section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
@ -2702,11 +2714,12 @@ class ShapeList(list[T]):
tol_digits, tol_digits,
) )
elif hasattr(group_by, "wrapped"): elif not group_by:
if group_by.wrapped is None:
raise ValueError("Cannot group by an empty object") raise ValueError("Cannot group by an empty object")
if isinstance(group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)): elif hasattr(group_by, "wrapped") and isinstance(
group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
):
def key_f(obj): def key_f(obj):
pnt1, _pnt2 = group_by.closest_points(obj.center()) pnt1, _pnt2 = group_by.closest_points(obj.center())
@ -2816,11 +2829,11 @@ class ShapeList(list[T]):
).position.Z, ).position.Z,
reverse=reverse, reverse=reverse,
) )
elif hasattr(sort_by, "wrapped"): elif not sort_by:
if sort_by.wrapped is None:
raise ValueError("Cannot sort by an empty object") raise ValueError("Cannot sort by an empty object")
elif hasattr(sort_by, "wrapped") and isinstance(
if isinstance(sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)): sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
):
def u_of_closest_center(obj) -> float: def u_of_closest_center(obj) -> float:
"""u-value of closest point between object center and sort_by""" """u-value of closest point between object center and sort_by"""

View file

@ -107,7 +107,7 @@ from build123d.geometry import (
) )
from .one_d import Edge, Wire, Mixin1D from .one_d import Edge, Wire, Mixin1D
from .shape_core import Shape, ShapeList, Joint, downcast, shapetype from .shape_core import TOPODS, Shape, ShapeList, Joint, downcast, shapetype
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
from .utils import ( from .utils import (
_extrude_topods_shape, _extrude_topods_shape,
@ -122,7 +122,7 @@ if TYPE_CHECKING: # pragma: no cover
from .composite import Compound, Curve, Sketch, Part # pylint: disable=R0801 from .composite import Compound, Curve, Sketch, Part # pylint: disable=R0801
class Mixin3D(Shape): class Mixin3D(Shape[TOPODS]):
"""Additional methods to add to 3D Shape classes""" """Additional methods to add to 3D Shape classes"""
project_to_viewport = Mixin1D.project_to_viewport project_to_viewport = Mixin1D.project_to_viewport
@ -714,7 +714,7 @@ class Mixin3D(Shape):
return Shape.get_shape_list(self, "Solid") return Shape.get_shape_list(self, "Solid")
class Solid(Mixin3D, Shape[TopoDS_Solid]): class Solid(Mixin3D[TopoDS_Solid]):
"""A Solid in build123d represents a three-dimensional solid geometry """A Solid in build123d represents a three-dimensional solid geometry
in a topological structure. A solid is a closed and bounded volume, enclosing in a topological structure. A solid is a closed and bounded volume, enclosing
a region in 3D space. It comprises faces, edges, and vertices connected in a a region in 3D space. It comprises faces, edges, and vertices connected in a
@ -1393,7 +1393,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
outer_wire = section outer_wire = section
inner_wires = inner_wires if inner_wires else [] inner_wires = inner_wires if inner_wires else []
shapes = [] shapes: list[Mixin3D[TopoDS_Shape]] = []
for wire in [outer_wire] + inner_wires: for wire in [outer_wire] + inner_wires:
builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped) builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)

View file

@ -139,6 +139,7 @@ from build123d.geometry import (
from .one_d import Edge, Mixin1D, Wire from .one_d import Edge, Mixin1D, Wire
from .shape_core import ( from .shape_core import (
TOPODS,
Shape, Shape,
ShapeList, ShapeList,
SkipClean, SkipClean,
@ -165,7 +166,7 @@ if TYPE_CHECKING: # pragma: no cover
T = TypeVar("T", Edge, Wire, "Face") T = TypeVar("T", Edge, Wire, "Face")
class Mixin2D(ABC, Shape): class Mixin2D(ABC, Shape[TOPODS]):
"""Additional methods to add to Face and Shell class""" """Additional methods to add to Face and Shell class"""
project_to_viewport = Mixin1D.project_to_viewport project_to_viewport = Mixin1D.project_to_viewport
@ -213,7 +214,7 @@ class Mixin2D(ABC, Shape):
def __neg__(self) -> Self: def __neg__(self) -> Self:
"""Reverse normal operator -""" """Reverse normal operator -"""
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Invalid Shape") raise ValueError("Invalid Shape")
new_surface = copy.deepcopy(self) new_surface = copy.deepcopy(self)
new_surface.wrapped = downcast(self.wrapped.Complemented()) new_surface.wrapped = downcast(self.wrapped.Complemented())
@ -244,7 +245,7 @@ class Mixin2D(ABC, Shape):
Returns: Returns:
list[tuple[Vector, Vector]]: Point and normal of intersection list[tuple[Vector, Vector]]: Point and normal of intersection
""" """
if self.wrapped is None: if self._wrapped is None:
return [] return []
intersection_line = gce_MakeLin(other.wrapped).Value() intersection_line = gce_MakeLin(other.wrapped).Value()
@ -470,7 +471,7 @@ class Mixin2D(ABC, Shape):
world_point, world_point - target_object_center world_point, world_point - target_object_center
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't wrap around an empty face") raise ValueError("Can't wrap around an empty face")
# Initial setup # Initial setup
@ -531,7 +532,7 @@ class Mixin2D(ABC, Shape):
raise RuntimeError( raise RuntimeError(
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}" f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
) )
if wrapped_edge.wrapped is None or not wrapped_edge.is_valid: if not wrapped_edge or not wrapped_edge.is_valid:
raise RuntimeError("Wrapped edge is invalid") raise RuntimeError("Wrapped edge is invalid")
if not snap_to_face: if not snap_to_face:
@ -554,7 +555,7 @@ class Mixin2D(ABC, Shape):
return projected_edge return projected_edge
class Face(Mixin2D, Shape[TopoDS_Face]): class Face(Mixin2D[TopoDS_Face]):
"""A Face in build123d represents a 3D bounded surface within the topological data """A Face in build123d represents a 3D bounded surface within the topological data
structure. It encapsulates geometric information, defining a face of a 3D shape. structure. It encapsulates geometric information, defining a face of a 3D shape.
These faces are integral components of complex structures, such as solids and These faces are integral components of complex structures, such as solids and
@ -569,7 +570,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
@overload @overload
def __init__( def __init__(
self, self,
obj: TopoDS_Face, obj: TopoDS_Face | Plane,
label: str = "", label: str = "",
color: Color | None = None, color: Color | None = None,
parent: Compound | None = None, parent: Compound | None = None,
@ -577,7 +578,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
"""Build a Face from an OCCT TopoDS_Shape/TopoDS_Face """Build a Face from an OCCT TopoDS_Shape/TopoDS_Face
Args: Args:
obj (TopoDS_Shape, optional): OCCT Face. obj (TopoDS_Shape | Plane, optional): OCCT Face or Plane.
label (str, optional): Defaults to ''. label (str, optional): Defaults to ''.
color (Color, optional): Defaults to None. color (Color, optional): Defaults to None.
parent (Compound, optional): assembly parent. Defaults to None. parent (Compound, optional): assembly parent. Defaults to None.
@ -607,7 +608,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if args: if args:
l_a = len(args) l_a = len(args)
if isinstance(args[0], TopoDS_Shape): if isinstance(args[0], Plane):
obj = args[0]
elif isinstance(args[0], TopoDS_Shape):
obj, label, color, parent = args[:4] + (None,) * (4 - l_a) obj, label, color, parent = args[:4] + (None,) * (4 - l_a)
elif isinstance(args[0], Wire): elif isinstance(args[0], Wire):
outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * ( outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (
@ -636,6 +639,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
color = kwargs.get("color", color) color = kwargs.get("color", color)
parent = kwargs.get("parent", parent) parent = kwargs.get("parent", parent)
if isinstance(obj, Plane):
obj = BRepBuilderAPI_MakeFace(obj.wrapped).Face()
if outer_wire is not None: if outer_wire is not None:
inner_topods_wires = ( inner_topods_wires = (
[w.wrapped for w in inner_wires] if inner_wires is not None else [] [w.wrapped for w in inner_wires] if inner_wires is not None else []
@ -665,7 +671,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
float: The total surface area, including the area of holes. Returns 0.0 if float: The total surface area, including the area of holes. Returns 0.0 if
the face is empty. the face is empty.
""" """
if self.wrapped is None: if self._wrapped is None:
return 0.0 return 0.0
return self.without_holes().area return self.without_holes().area
@ -725,7 +731,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
ValueError: If the face or its underlying representation is empty. ValueError: If the face or its underlying representation is empty.
ValueError: If the face is not planar. ValueError: If the face is not planar.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Can't determine axes_of_symmetry of empty face") raise ValueError("Can't determine axes_of_symmetry of empty face")
if not self.is_planar_face: if not self.is_planar_face:
@ -989,7 +995,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Returns: Returns:
Face: extruded shape Face: extruded shape
""" """
if obj.wrapped is None: if not obj:
raise ValueError("Can't extrude empty object") raise ValueError("Can't extrude empty object")
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction))) return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
@ -1099,7 +1105,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) )
return single_point_curve return single_point_curve
if shape.wrapped is None: if not shape:
raise ValueError("input Edge cannot be empty") raise ValueError("input Edge cannot be empty")
adaptor = BRepAdaptor_Curve(shape.wrapped) adaptor = BRepAdaptor_Curve(shape.wrapped)
@ -1133,6 +1139,12 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
plane: Plane = Plane.XY, plane: Plane = Plane.XY,
) -> Face: ) -> Face:
"""Create a unlimited size Face aligned with plane""" """Create a unlimited size Face aligned with plane"""
warnings.warn(
"The 'make_plane' method is deprecated and will be removed in a future version.",
DeprecationWarning,
stacklevel=2,
)
pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face() pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face()
return cls(pln_shape) return cls(pln_shape)
@ -1222,7 +1234,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
raise ValueError("exterior must be a Wire or list of Edges") raise ValueError("exterior must be a Wire or list of Edges")
for edge in outside_edges: for edge in outside_edges:
if edge.wrapped is None: if not edge:
raise ValueError("exterior contains empty edges") raise ValueError("exterior contains empty edges")
surface.Add(edge.wrapped, GeomAbs_C0) surface.Add(edge.wrapped, GeomAbs_C0)
@ -1253,7 +1265,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if interior_wires: if interior_wires:
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped) makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
for wire in interior_wires: for wire in interior_wires:
if wire.wrapped is None: if not wire:
raise ValueError("interior_wires contain an empty wire") raise ValueError("interior_wires contain an empty wire")
makeface_object.Add(wire.wrapped) makeface_object.Add(wire.wrapped)
try: try:
@ -1447,7 +1459,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) from err ) from err
result = result.fix() result = result.fix()
if not result.is_valid or result.wrapped is None: if not result.is_valid or not result:
raise RuntimeError("Non planar face is invalid") raise RuntimeError("Non planar face is invalid")
return result return result
@ -2058,7 +2070,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot approximate an empty shape") raise ValueError("Cannot approximate an empty shape")
return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance)) return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
@ -2071,7 +2083,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Returns: Returns:
Face: A new Face instance identical to the original but without any holes. Face: A new Face instance identical to the original but without any holes.
""" """
if self.wrapped is None: if self._wrapped is None:
raise ValueError("Cannot remove holes from an empty face") raise ValueError("Cannot remove holes from an empty face")
if not (inner_wires := self.inner_wires()): if not (inner_wires := self.inner_wires()):
@ -2445,7 +2457,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
return wrapped_wire return wrapped_wire
class Shell(Mixin2D, Shape[TopoDS_Shell]): class Shell(Mixin2D[TopoDS_Shell]):
"""A Shell is a fundamental component in build123d's topological data structure """A Shell is a fundamental component in build123d's topological data structure
representing a connected set of faces forming a closed surface in 3D space. As representing a connected set of faces forming a closed surface in 3D space. As
part of a geometric model, it defines a watertight enclosure, commonly encountered part of a geometric model, it defines a watertight enclosure, commonly encountered
@ -2477,7 +2489,7 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
obj = obj_list[0] obj = obj_list[0]
if isinstance(obj, Face): if isinstance(obj, Face):
if obj.wrapped is None: if not obj:
raise ValueError(f"Can't create a Shell from empty Face") raise ValueError(f"Can't create a Shell from empty Face")
builder = BRep_Builder() builder = BRep_Builder()
shell = TopoDS_Shell() shell = TopoDS_Shell()

View file

@ -80,7 +80,7 @@ def to_vtk_poly_data(
if not HAS_VTK: if not HAS_VTK:
warnings.warn("VTK not supported", stacklevel=2) warnings.warn("VTK not supported", stacklevel=2)
if obj.wrapped is None: if not obj:
raise ValueError("Cannot convert an empty shape") raise ValueError("Cannot convert an empty shape")
vtk_shape = IVtkOCC_Shape(obj.wrapped) vtk_shape = IVtkOCC_Shape(obj.wrapped)

View file

@ -130,8 +130,8 @@ class TestFace(unittest.TestCase):
distance=1, distance2=2, vertices=[vertex], edge=other_edge distance=1, distance2=2, vertices=[vertex], edge=other_edge
) )
def test_make_rect(self): def test_plane_as_face(self):
test_face = Face.make_plane() test_face = Face(Plane.XY)
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5) self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
def test_length_width(self): def test_length_width(self):

View file

@ -476,7 +476,7 @@ class TestShape(unittest.TestCase):
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5) self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, []) self.assertListEqual(edges, [])
verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_plane(Plane.XY)) verts, edges = Vertex(1, 2, 0)._ocp_section(Face(Plane.XY))
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5) self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, []) self.assertListEqual(edges, [])
@ -494,7 +494,7 @@ class TestShape(unittest.TestCase):
self.assertEqual(len(edges1), 1) self.assertEqual(len(edges1), 1)
self.assertAlmostEqual(edges1[0].length, 20, 5) self.assertAlmostEqual(edges1[0].length, 20, 5)
vertices2, edges2 = cylinder._ocp_section(Face.make_plane(pln)) vertices2, edges2 = cylinder._ocp_section(Face(pln))
self.assertEqual(len(vertices2), 1) self.assertEqual(len(vertices2), 1)
self.assertEqual(len(edges2), 1) self.assertEqual(len(edges2), 1)
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5) self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)