From 5d485ee705acf3315757bd4cec553d5eec458d30 Mon Sep 17 00:00:00 2001 From: snoyer Date: Tue, 21 Oct 2025 08:12:29 +0400 Subject: [PATCH 1/9] use `_wrapped: TOPODS | None` member and `wrapped: TOPODS` property --- src/build123d/topology/composite.py | 6 +- src/build123d/topology/one_d.py | 57 ++++++++------- src/build123d/topology/shape_core.py | 102 +++++++++++++++------------ src/build123d/topology/two_d.py | 14 ++-- 4 files changed, 96 insertions(+), 83 deletions(-) diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py index 823eece..a34fa23 100644 --- a/src/build123d/topology/composite.py +++ b/src/build123d/topology/composite.py @@ -455,7 +455,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]): will be a Wire, otherwise a Shape. """ 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 if isinstance(sum1d, ShapeList): result1d: Curve | Wire = Curve(sum1d) @@ -517,7 +517,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]): 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]: """ @@ -602,7 +602,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]): def compounds(self) -> ShapeList[Compound]: """compounds - all the compounds in this Shape""" - if self.wrapped is None: + if self._wrapped is None: return ShapeList() if isinstance(self.wrapped, TopoDS_Compound): # pylint: disable=not-an-iterable diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index 25b817a..c149f1e 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -263,14 +263,14 @@ class Mixin1D(Shape): @property def is_closed(self) -> bool: """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") return BRep_Tool.IsClosed_s(self.wrapped) @property def is_forward(self) -> bool: """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") return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD @@ -388,8 +388,7 @@ class Mixin1D(Shape): shape # for o in (other if isinstance(other, (list, tuple)) 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) + for shape in get_top_level_topods_shapes(o.wrapped if o else None) ] # If there is nothing to add return the original object if not topods_summands: @@ -404,7 +403,7 @@ class Mixin1D(Shape): ) 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: sum_shape: Edge | Wire | ShapeList[Edge] = summands[0] else: @@ -452,7 +451,7 @@ class Mixin1D(Shape): Returns: Vector: center """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't find center of empty edge/wire") if center_of == CenterOf.GEOMETRY: @@ -578,7 +577,7 @@ class Mixin1D(Shape): >>> 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") pln = self.common_plane() if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE): @@ -991,7 +990,7 @@ class Mixin1D(Shape): Returns: """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't find normal of empty edge/wire") curve = self.geom_adaptor() @@ -1225,7 +1224,7 @@ class Mixin1D(Shape): Returns: """ - if self.wrapped is None or face.wrapped is None: + if self._wrapped is None or face.wrapped is None: raise ValueError("Can't project an empty Edge or Wire onto empty Face") bldr = BRepProj_Projection( @@ -1297,7 +1296,7 @@ class Mixin1D(Shape): return edges - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't project empty edge/wire") # Setup the projector @@ -1400,7 +1399,7 @@ class Mixin1D(Shape): - **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. """ - if self.wrapped is None or tool.wrapped is None: + if self._wrapped is None or tool.wrapped is None: raise ValueError("Can't split an empty edge/wire/tool") shape_list = TopTools_ListOfShape() @@ -2538,7 +2537,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): extension_factor: float = 0.1, ): """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") if self.geom_type != GeomType.BSPLINE: raise TypeError("_extend_spline only works with splines") @@ -2595,7 +2594,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): Returns: 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") # Convert an Axis into an edge at least as large as self and Axis start point @@ -2723,7 +2722,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): def geom_adaptor(self) -> BRepAdaptor_Curve: """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") return BRepAdaptor_Curve(self.wrapped) @@ -2811,7 +2810,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): float: Normalized parameter in [0.0, 1.0] corresponding to the point's closest location on the edge. """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't find param on empty edge") pnt = Vector(point) @@ -2945,7 +2944,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): Returns: Edge: reversed """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("An empty edge can't be reversed") assert isinstance(self.wrapped, TopoDS_Edge) @@ -3025,7 +3024,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): # if start_u >= 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") self_copy = copy.deepcopy(self) @@ -3060,7 +3059,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): Returns: Edge: trimmed edge """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't trim empty edge") start_u = Mixin1D._to_param(self, start, "start") @@ -3623,7 +3622,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): Returns: Wire: chamfered wire """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't chamfer empty wire") reference_edge = edge @@ -3695,7 +3694,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): Returns: Wire: filleted wire """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't fillet an empty wire") # Create a face to fillet @@ -3723,7 +3722,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): Returns: Wire: fixed wire """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't fix an empty edge") sf_w = ShapeFix_Wireframe(self.wrapped) @@ -3735,7 +3734,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): def geom_adaptor(self) -> BRepAdaptor_CompCurve: """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") return BRepAdaptor_CompCurve(self.wrapped) @@ -3779,7 +3778,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): float: Normalized parameter in [0.0, 1.0] representing the relative 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") point_on_curve = Vector(point) @@ -3932,7 +3931,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): """ # pylint: disable=too-many-branches - if self.wrapped is None or target_object.wrapped is None: + if self._wrapped is None or target_object.wrapped is None: raise ValueError("Can't project empty Wires or to empty Shapes") if direction is not None and center is None: @@ -4021,7 +4020,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): Returns: Wire: stitched wires """ - if self.wrapped is None or other.wrapped is None: + if self._wrapped is None or other.wrapped is None: raise ValueError("Can't stitch empty wires") wire_builder = BRepBuilderAPI_MakeWire() @@ -4065,7 +4064,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): """ # Build a single Geom_BSplineCurve from the wire, in *topological order* builder = GeomConvert_CompCurveToBSplineCurve() - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't convert an empty wire") wire_explorer = BRepTools_WireExplorer(self.wrapped) @@ -4217,9 +4216,9 @@ def topo_explore_connected_edges( parent = parent if parent is not None else edge.topo_parent if parent is None: raise ValueError("edge has no valid parent") - given_topods_edge = edge.wrapped - if given_topods_edge is None: + if not edge: raise ValueError("edge is empty") + given_topods_edge = edge.wrapped connected_edges = set() # Find all the TopoDS_Edges for this Shape @@ -4262,7 +4261,7 @@ def topo_explore_connected_faces( ) -> list[TopoDS_Face]: """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") parent = parent if parent is not None else edge.topo_parent diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py index 6402c3e..6a270e8 100644 --- a/src/build123d/topology/shape_core.py +++ b/src/build123d/topology/shape_core.py @@ -287,7 +287,7 @@ class Shape(NodeMixin, Generic[TOPODS]): color: ColorLike | 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 ) self.for_construction = False @@ -304,6 +304,18 @@ class Shape(NodeMixin, Generic[TOPODS]): # 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 @abstractmethod def _dim(self) -> int | None: @@ -312,7 +324,7 @@ class Shape(NodeMixin, Generic[TOPODS]): @property def area(self) -> float: """area -the surface area of all faces in this Shape""" - if self.wrapped is None: + if self._wrapped is None: return 0.0 properties = GProp_GProps() BRepGProp.SurfaceProperties_s(self.wrapped, properties) @@ -351,7 +363,7 @@ class Shape(NodeMixin, Generic[TOPODS]): 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") shape: TopAbs_ShapeEnum = shapetype(self.wrapped) @@ -380,7 +392,7 @@ class Shape(NodeMixin, Generic[TOPODS]): bool: is the shape manifold or water tight """ # Extract one or more (if a Compound) shape from self - if self.wrapped is None: + if self._wrapped is None: return False 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 orientation. """ - return self.wrapped is None or self.wrapped.IsNull() + return self._wrapped is None or self.wrapped.IsNull() @property def is_planar_face(self) -> bool: """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 surface = BRep_Tool.Surface_s(self.wrapped) 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 description of what is checked. """ - if self.wrapped is None: + if self._wrapped is None: return True chk = BRepCheck_Analyzer(self.wrapped) chk.SetParallel(True) @@ -474,7 +486,7 @@ class Shape(NodeMixin, Generic[TOPODS]): @property def location(self) -> 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") return Location(self.wrapped.Location()) @@ -518,7 +530,7 @@ class Shape(NodeMixin, Generic[TOPODS]): - It is commonly used in structural analysis, mechanical simulations, and physics-based motion calculations. """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Can't calculate matrix for empty shape") properties = GProp_GProps() BRepGProp.VolumeProperties_s(self.wrapped, properties) @@ -546,7 +558,7 @@ class Shape(NodeMixin, Generic[TOPODS]): @property def position(self) -> Vector: """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") return self.location.position @@ -575,7 +587,7 @@ class Shape(NodeMixin, Generic[TOPODS]): (Vector(0, 1, 0), 1000.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") properties = GProp_GProps() @@ -615,7 +627,7 @@ class Shape(NodeMixin, Generic[TOPODS]): (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") properties = GProp_GProps() @@ -859,7 +871,7 @@ class Shape(NodeMixin, Generic[TOPODS]): if not all(summand._dim == addend_dim for summand in summands): 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: sum_shape = summands[0] else: @@ -876,7 +888,7 @@ class Shape(NodeMixin, Generic[TOPODS]): """intersect shape with self operator &""" 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") new_shape = self.intersect(*others) @@ -948,7 +960,7 @@ class Shape(NodeMixin, Generic[TOPODS]): def __hash__(self) -> int: """Return hash code""" - if self.wrapped is None: + if self._wrapped is None: return 0 return hash(self.wrapped) @@ -966,7 +978,7 @@ class Shape(NodeMixin, Generic[TOPODS]): def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]: """cut shape from self operator -""" - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot subtract shape from empty compound") # Convert `other` to list of base objects and filter out None values @@ -1014,7 +1026,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: BoundBox: A box sized to contain this Shape """ - if self.wrapped is None: + if self._wrapped is None: return BoundBox(Bnd_Box()) tolerance = TOLERANCE if tolerance is None else tolerance return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal) @@ -1033,7 +1045,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: Shape: Original object with extraneous internal edges removed """ - if self.wrapped is None: + if self._wrapped is None: return self upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True) upgrader.AllowInternalEdges(False) @@ -1112,7 +1124,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self.wrapped is None or other.wrapped is None: + if self._wrapped is None or other.wrapped is None: raise ValueError("Cannot calculate distance to or from an empty shape") return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value() @@ -1125,7 +1137,9 @@ class Shape(NodeMixin, Generic[TOPODS]): self, other: Shape | VectorLike ) -> tuple[float, Vector, Vector]: """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 other.wrapped is None + ): raise ValueError("Cannot calculate distance to or from an empty shape") if isinstance(other, Shape): @@ -1155,7 +1169,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot calculate distance to or from an empty shape") dist_calc = BRepExtrema_DistShapeShape() @@ -1181,7 +1195,7 @@ class Shape(NodeMixin, Generic[TOPODS]): def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]: """Return all of the TopoDS sub entities of the given type""" - if self.wrapped is None: + if self._wrapped is None: return [] return _topods_entities(self.wrapped, topo_type) @@ -1209,7 +1223,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: 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() line = gce_MakeLin(axis.wrapped).Value() @@ -1239,7 +1253,7 @@ class Shape(NodeMixin, Generic[TOPODS]): def fix(self) -> Self: """fix - try to fix shape if not valid""" - if self.wrapped is None: + if self._wrapped is None: return self if not self.is_valid: shape_copy: Shape = copy.deepcopy(self, None) @@ -1281,7 +1295,7 @@ class Shape(NodeMixin, Generic[TOPODS]): # self, child_type: Shapes, parent_type: Shapes # ) -> Dict[Shape, list[Shape]]: # """This function is very slow on M1 macs and is currently unused""" - # if self.wrapped is None: + # if self._wrapped is None: # return {} # res = TopTools_IndexedDataMapOfShapeListOfShape() @@ -1319,7 +1333,7 @@ class Shape(NodeMixin, Generic[TOPODS]): (e.g., edges, vertices) and other compounds, the method returns a list 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( self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped) @@ -1401,7 +1415,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self.wrapped is None or other.wrapped is None: + if self._wrapped is None or other.wrapped is None: return False return self.wrapped.IsEqual(other.wrapped) @@ -1416,7 +1430,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self.wrapped is None or other.wrapped is None: + if self._wrapped is None or other.wrapped is None: return False return self.wrapped.IsSame(other.wrapped) @@ -1429,7 +1443,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot locate an empty shape") if loc.wrapped is None: raise ValueError("Cannot locate a shape at an empty location") @@ -1448,7 +1462,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: Shape: copy of Shape at location """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot locate an empty shape") if loc.wrapped is None: raise ValueError("Cannot locate a shape at an empty location") @@ -1466,7 +1480,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot mesh an empty shape") if not BRepTools.Triangulation_s(self.wrapped, tolerance): @@ -1487,7 +1501,7 @@ class Shape(NodeMixin, Generic[TOPODS]): if not mirror_plane: mirror_plane = Plane.XY - if self.wrapped is None: + if self._wrapped is None: return self transformation = gp_Trsf() transformation.SetMirror( @@ -1505,7 +1519,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot move an empty shape") if loc.wrapped is None: raise ValueError("Cannot move a shape at an empty location") @@ -1525,7 +1539,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: 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") if loc.wrapped is None: raise ValueError("Cannot move a shape at an empty location") @@ -1539,7 +1553,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: 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(self) @@ -1641,7 +1655,7 @@ class Shape(NodeMixin, Generic[TOPODS]): - The radius of gyration is computed based on the shape’s mass properties. - 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") properties = GProp_GProps() @@ -1660,7 +1674,7 @@ class Shape(NodeMixin, Generic[TOPODS]): DeprecationWarning, stacklevel=2, ) - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot relocate an empty shape") if loc.wrapped is None: raise ValueError("Cannot relocate a shape at an empty location") @@ -1855,7 +1869,7 @@ class Shape(NodeMixin, Generic[TOPODS]): "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") # Process the perimeter @@ -1900,7 +1914,7 @@ class Shape(NodeMixin, Generic[TOPODS]): self, tolerance: float, angular_tolerance: float = 0.1 ) -> tuple[list[Vector], list[tuple[int, int, int]]]: """General triangulated approximation""" - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot tessellate an empty shape") self.mesh(tolerance, angular_tolerance) @@ -1962,7 +1976,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: Self: Approximated shape """ - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot approximate an empty shape") params = ShapeCustom_RestrictionParameters() @@ -1999,7 +2013,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: Shape: a copy of the object, but with geometry transformed """ - if self.wrapped is None: + if self._wrapped is None: return self new_shape = copy.deepcopy(self, None) transformed = downcast( @@ -2022,7 +2036,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: Shape: copy of transformed shape with all objects keeping their type """ - if self.wrapped is None: + if self._wrapped is None: return self new_shape = copy.deepcopy(self, None) transformed = downcast( @@ -2095,7 +2109,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: Shape: copy of transformed Shape """ - if self.wrapped is None: + if self._wrapped is None: return self shape_copy: Shape = copy.deepcopy(self, None) transformed_shape = BRepBuilderAPI_Transform( @@ -2200,7 +2214,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: tuple[ShapeList[Vertex], ShapeList[Edge]]: section results """ - if self.wrapped is None or other.wrapped is None: + if self._wrapped is None or other.wrapped is None: return (ShapeList(), ShapeList()) section = BRepAlgoAPI_Section(self.wrapped, other.wrapped) diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py index 8b8f264..c6102a7 100644 --- a/src/build123d/topology/two_d.py +++ b/src/build123d/topology/two_d.py @@ -213,7 +213,7 @@ class Mixin2D(ABC, Shape): def __neg__(self) -> Self: """Reverse normal operator -""" - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Invalid Shape") new_surface = copy.deepcopy(self) new_surface.wrapped = downcast(self.wrapped.Complemented()) @@ -244,7 +244,7 @@ class Mixin2D(ABC, Shape): Returns: list[tuple[Vector, Vector]]: Point and normal of intersection """ - if self.wrapped is None: + if self._wrapped is None: return [] intersection_line = gce_MakeLin(other.wrapped).Value() @@ -350,7 +350,7 @@ class Mixin2D(ABC, Shape): 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") # Initial setup @@ -545,7 +545,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]): float: The total surface area, including the area of holes. Returns 0.0 if the face is empty. """ - if self.wrapped is None: + if self._wrapped is None: return 0.0 return self.without_holes().area @@ -605,7 +605,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]): ValueError: If the face or its underlying representation is empty. 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") if not self.is_planar_face: @@ -1940,7 +1940,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]): DeprecationWarning, stacklevel=2, ) - if self.wrapped is None: + if self._wrapped is None: raise ValueError("Cannot approximate an empty shape") return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance)) @@ -1953,7 +1953,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]): Returns: 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") if not (inner_wires := self.inner_wires()): From 0013b9fa872e3806e533ee31d8115812d8a65911 Mon Sep 17 00:00:00 2001 From: snoyer Date: Tue, 21 Oct 2025 08:28:24 +0400 Subject: [PATCH 2/9] fix Mixins generic types --- src/build123d/topology/composite.py | 2 +- src/build123d/topology/one_d.py | 7 ++++--- src/build123d/topology/three_d.py | 6 +++--- src/build123d/topology/two_d.py | 7 ++++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py index a34fa23..4faeb36 100644 --- a/src/build123d/topology/composite.py +++ b/src/build123d/topology/composite.py @@ -130,7 +130,7 @@ from .utils import ( 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 geometric shapes grouped together within a single structure. It serves as a container for organizing diverse shapes like edges, faces, or solids. This diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index c149f1e..8e9939f 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -217,6 +217,7 @@ from build123d.geometry import ( ) from .shape_core import ( + TOPODS, Shape, ShapeList, SkipClean, @@ -250,7 +251,7 @@ if TYPE_CHECKING: # pragma: no cover 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""" # ---- Properties ---- @@ -1565,7 +1566,7 @@ class Mixin1D(Shape): 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 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 @@ -3088,7 +3089,7 @@ class Edge(Mixin1D, Shape[TopoDS_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 of edges forming a continuous curve or path in 3D space. Wires are essential components in modeling complex objects, defining boundaries for surfaces or diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py index e4131ce..ed3ba34 100644 --- a/src/build123d/topology/three_d.py +++ b/src/build123d/topology/three_d.py @@ -107,7 +107,7 @@ from build123d.geometry import ( from typing_extensions import Self 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 .utils import ( _extrude_topods_shape, @@ -122,7 +122,7 @@ if TYPE_CHECKING: # pragma: no cover 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""" project_to_viewport = Mixin1D.project_to_viewport @@ -590,7 +590,7 @@ class Mixin3D(Shape): 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 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 diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py index c6102a7..65cee95 100644 --- a/src/build123d/topology/two_d.py +++ b/src/build123d/topology/two_d.py @@ -139,6 +139,7 @@ from build123d.geometry import ( from .one_d import Edge, Mixin1D, Wire from .shape_core import ( + TOPODS, Shape, ShapeList, SkipClean, @@ -165,7 +166,7 @@ if TYPE_CHECKING: # pragma: no cover T = TypeVar("T", Edge, Wire, "Face") -class Mixin2D(ABC, Shape): +class Mixin2D(ABC, Shape[TOPODS]): """Additional methods to add to Face and Shell class""" project_to_viewport = Mixin1D.project_to_viewport @@ -434,7 +435,7 @@ class Mixin2D(ABC, Shape): 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 structure. It encapsulates geometric information, defining a face of a 3D shape. These faces are integral components of complex structures, such as solids and @@ -2327,7 +2328,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]): 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 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 From a6d8f9bdc16a18d9ca82491db194f2b1bbc71386 Mon Sep 17 00:00:00 2001 From: snoyer Date: Tue, 21 Oct 2025 10:15:47 +0400 Subject: [PATCH 3/9] refactor `.wrapped is None` usages --- src/build123d/exporters.py | 6 +- src/build123d/mesher.py | 2 +- src/build123d/operations_generic.py | 4 +- src/build123d/topology/composite.py | 2 +- src/build123d/topology/constrained_lines.py | 2 +- src/build123d/topology/one_d.py | 16 ++--- src/build123d/topology/shape_core.py | 65 ++++++++++----------- src/build123d/topology/two_d.py | 14 ++--- src/build123d/vtk_tools.py | 2 +- 9 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/build123d/exporters.py b/src/build123d/exporters.py index 49339ee..a229fa2 100644 --- a/src/build123d/exporters.py +++ b/src/build123d/exporters.py @@ -758,7 +758,7 @@ class ExportDXF(Export2D): ) # 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}.") t = edge.location.wrapped.Transformation() spline.Transform(t) @@ -1345,7 +1345,7 @@ class ExportSVG(Export2D): u2 = adaptor.LastParameter() # 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}.") t = edge.location.wrapped.Transformation() spline.Transform(t) @@ -1411,7 +1411,7 @@ class ExportSVG(Export2D): } 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}.") edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED geom_type = edge.geom_type diff --git a/src/build123d/mesher.py b/src/build123d/mesher.py index 5fb9a54..5433848 100644 --- a/src/build123d/mesher.py +++ b/src/build123d/mesher.py @@ -295,7 +295,7 @@ class Mesher: ocp_mesh_vertices.append(pnt) # Store the triangles from the triangulated faces - if facet.wrapped is None: + if not facet: continue facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED order = [1, 3, 2] if facet_reversed else [1, 2, 3] diff --git a/src/build123d/operations_generic.py b/src/build123d/operations_generic.py index 69d75cc..2a2f007 100644 --- a/src/build123d/operations_generic.py +++ b/src/build123d/operations_generic.py @@ -365,7 +365,7 @@ def chamfer( if target._dim == 1: if isinstance(target, BaseLineObject): - if target.wrapped is None: + if not target: target = Wire([]) # empty wire else: target = Wire(target.wrapped) @@ -465,7 +465,7 @@ def fillet( if target._dim == 1: if isinstance(target, BaseLineObject): - if target.wrapped is None: + if not target: target = Wire([]) # empty wire else: target = Wire(target.wrapped) diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py index 4faeb36..4e42b60 100644 --- a/src/build123d/topology/composite.py +++ b/src/build123d/topology/composite.py @@ -534,7 +534,7 @@ class Compound(Mixin3D[TopoDS_Compound]): def __len__(self) -> int: """Return the number of subshapes""" count = 0 - if self.wrapped is not None: + if self._wrapped is not None: for _ in self: count += 1 return count diff --git a/src/build123d/topology/constrained_lines.py b/src/build123d/topology/constrained_lines.py index 9c316b6..4e53ddb 100644 --- a/src/build123d/topology/constrained_lines.py +++ b/src/build123d/topology/constrained_lines.py @@ -174,7 +174,7 @@ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[ - Edge -> (QualifiedCurve, h2d, first, last, True) - 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") if isinstance(obj.wrapped, TopoDS_Edge): diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index 8e9939f..c12880f 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -809,7 +809,7 @@ class Mixin1D(Shape[TOPODS]): case Edge() as obj, Plane() as plane: # Find any edge / plane intersection points & edges # Find point intersections - if obj.wrapped is None: + if not obj: continue geom_line = BRep_Tool.Curve_s( obj.wrapped, obj.param_at(0), obj.param_at(1) @@ -1225,7 +1225,7 @@ class Mixin1D(Shape[TOPODS]): 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") bldr = BRepProj_Projection( @@ -1400,7 +1400,7 @@ class Mixin1D(Shape[TOPODS]): - **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. """ - 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") shape_list = TopTools_ListOfShape() @@ -1647,7 +1647,7 @@ class Edge(Mixin1D[TopoDS_Edge]): Returns: Edge: extruded shape """ - if obj.wrapped is None: + if not obj: raise ValueError("Can't extrude empty vertex") return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction))) @@ -3638,7 +3638,7 @@ class Wire(Mixin1D[TopoDS_Wire]): ) for v in vertices: - if v.wrapped is None: + if not v: continue edge_list = vertex_edge_map.FindFromKey(v.wrapped) @@ -3932,7 +3932,7 @@ class Wire(Mixin1D[TopoDS_Wire]): """ # 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") if direction is not None and center is None: @@ -4021,7 +4021,7 @@ class Wire(Mixin1D[TopoDS_Wire]): Returns: 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") wire_builder = BRepBuilderAPI_MakeWire() @@ -4266,7 +4266,7 @@ def topo_explore_connected_faces( raise ValueError("Can't explore from an empty edge") 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") # make a edge --> faces mapping diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py index 6a270e8..1e18261 100644 --- a/src/build123d/topology/shape_core.py +++ b/src/build123d/topology/shape_core.py @@ -797,7 +797,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if obj.wrapped is None: + if not obj: return 0.0 properties = GProp_GProps() @@ -817,7 +817,7 @@ class Shape(NodeMixin, Generic[TOPODS]): ], ) -> ShapeList: """Helper to extract entities of a specific type from a shape.""" - if shape.wrapped is None: + if not shape: return ShapeList() shape_list = ShapeList( [shape.__class__.cast(i) for i in shape.entities(entity_type)] @@ -1124,7 +1124,7 @@ class Shape(NodeMixin, Generic[TOPODS]): 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") return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value() @@ -1137,9 +1137,7 @@ class Shape(NodeMixin, Generic[TOPODS]): self, other: Shape | VectorLike ) -> tuple[float, Vector, Vector]: """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") if isinstance(other, Shape): @@ -1176,7 +1174,7 @@ class Shape(NodeMixin, Generic[TOPODS]): dist_calc.LoadS1(self.wrapped) 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") dist_calc.LoadS2(other_shape.wrapped) dist_calc.Perform() @@ -1415,7 +1413,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self._wrapped is None or other.wrapped is None: + if self._wrapped is None or not other: return False return self.wrapped.IsEqual(other.wrapped) @@ -1430,7 +1428,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: """ - if self._wrapped is None or other.wrapped is None: + if self._wrapped is None or not other: return False return self.wrapped.IsSame(other.wrapped) @@ -1877,7 +1875,7 @@ class Shape(NodeMixin, Generic[TOPODS]): raise ValueError("perimeter must be a closed Wire or Edge") perimeter_edges = TopTools_SequenceOfShape() for perimeter_edge in perimeter.edges(): - if perimeter_edge.wrapped is None: + if not perimeter_edge: continue perimeter_edges.Append(perimeter_edge.wrapped) @@ -1885,7 +1883,7 @@ class Shape(NodeMixin, Generic[TOPODS]): lefts: list[Shell] = [] rights: list[Shell] = [] for target_shell in self.shells(): - if target_shell.wrapped is None: + if not target_shell: continue constructor = BRepFeat_SplitShape(target_shell.wrapped) constructor.Add(perimeter_edges) @@ -2214,7 +2212,7 @@ class Shape(NodeMixin, Generic[TOPODS]): Returns: 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()) section = BRepAlgoAPI_Section(self.wrapped, other.wrapped) @@ -2715,15 +2713,16 @@ class ShapeList(list[T]): tol_digits, ) - elif hasattr(group_by, "wrapped"): - if group_by.wrapped is None: - raise ValueError("Cannot group by an empty object") + elif not group_by: + 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): - pnt1, _pnt2 = group_by.closest_points(obj.center()) - return round(group_by.param_at_point(pnt1), tol_digits) + def key_f(obj): + pnt1, _pnt2 = group_by.closest_points(obj.center()) + return round(group_by.param_at_point(pnt1), tol_digits) elif isinstance(group_by, SortBy): if group_by == SortBy.LENGTH: @@ -2829,22 +2828,22 @@ class ShapeList(list[T]): ).position.Z, reverse=reverse, ) - elif hasattr(sort_by, "wrapped"): - if sort_by.wrapped is None: - raise ValueError("Cannot sort by an empty object") + elif not sort_by: + raise ValueError("Cannot sort by an empty object") + elif hasattr(sort_by, "wrapped") and isinstance( + sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire) + ): - if isinstance(sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)): + def u_of_closest_center(obj) -> float: + """u-value of closest point between object center and sort_by""" + assert not isinstance(sort_by, SortBy) + pnt1, _pnt2 = sort_by.closest_points(obj.center()) + return sort_by.param_at_point(pnt1) - def u_of_closest_center(obj) -> float: - """u-value of closest point between object center and sort_by""" - assert not isinstance(sort_by, SortBy) - pnt1, _pnt2 = sort_by.closest_points(obj.center()) - return sort_by.param_at_point(pnt1) - - # pylint: disable=unnecessary-lambda - objects = sorted( - self, key=lambda o: u_of_closest_center(o), reverse=reverse - ) + # pylint: disable=unnecessary-lambda + objects = sorted( + self, key=lambda o: u_of_closest_center(o), reverse=reverse + ) elif isinstance(sort_by, SortBy): if sort_by == SortBy.LENGTH: diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py index 65cee95..8c340a5 100644 --- a/src/build123d/topology/two_d.py +++ b/src/build123d/topology/two_d.py @@ -412,7 +412,7 @@ class Mixin2D(ABC, Shape[TOPODS]): raise RuntimeError( 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") if not snap_to_face: @@ -872,7 +872,7 @@ class Face(Mixin2D[TopoDS_Face]): Returns: Face: extruded shape """ - if obj.wrapped is None: + if not obj: raise ValueError("Can't extrude empty object") return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction))) @@ -982,7 +982,7 @@ class Face(Mixin2D[TopoDS_Face]): ) return single_point_curve - if shape.wrapped is None: + if not shape: raise ValueError("input Edge cannot be empty") adaptor = BRepAdaptor_Curve(shape.wrapped) @@ -1105,7 +1105,7 @@ class Face(Mixin2D[TopoDS_Face]): raise ValueError("exterior must be a Wire or list of Edges") for edge in outside_edges: - if edge.wrapped is None: + if not edge: raise ValueError("exterior contains empty edges") surface.Add(edge.wrapped, GeomAbs_C0) @@ -1136,7 +1136,7 @@ class Face(Mixin2D[TopoDS_Face]): if interior_wires: makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped) for wire in interior_wires: - if wire.wrapped is None: + if not wire: raise ValueError("interior_wires contain an empty wire") makeface_object.Add(wire.wrapped) try: @@ -1330,7 +1330,7 @@ class Face(Mixin2D[TopoDS_Face]): ) from err 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") return result @@ -2360,7 +2360,7 @@ class Shell(Mixin2D[TopoDS_Shell]): obj = obj_list[0] if isinstance(obj, Face): - if obj.wrapped is None: + if not obj.wrapped: raise ValueError(f"Can't create a Shell from empty Face") builder = BRep_Builder() shell = TopoDS_Shell() diff --git a/src/build123d/vtk_tools.py b/src/build123d/vtk_tools.py index 9d22185..a4af54a 100644 --- a/src/build123d/vtk_tools.py +++ b/src/build123d/vtk_tools.py @@ -80,7 +80,7 @@ def to_vtk_poly_data( if not HAS_VTK: warnings.warn("VTK not supported", stacklevel=2) - if obj.wrapped is None: + if not obj: raise ValueError("Cannot convert an empty shape") vtk_shape = IVtkOCC_Shape(obj.wrapped) From 6ce4a31355825233181cca478735133b138afa97 Mon Sep 17 00:00:00 2001 From: snoyer Date: Tue, 21 Oct 2025 10:31:41 +0400 Subject: [PATCH 4/9] appease mypy --- src/build123d/topology/three_d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py index ed3ba34..143e257 100644 --- a/src/build123d/topology/three_d.py +++ b/src/build123d/topology/three_d.py @@ -1269,7 +1269,7 @@ class Solid(Mixin3D[TopoDS_Solid]): outer_wire = section inner_wires = inner_wires if inner_wires else [] - shapes = [] + shapes: list[Mixin3D[TopoDS_Shape]] = [] for wire in [outer_wire] + inner_wires: builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped) From 9a6c382ced3df76f2f12dea14f5e81fa78ee9f64 Mon Sep 17 00:00:00 2001 From: Jonathan Wagenet Date: Tue, 21 Oct 2025 13:31:14 -0400 Subject: [PATCH 5/9] Replace Face.make_plane() with Face(Plane) to match Edge(Axis) --- src/build123d/topology/two_d.py | 20 ++++++++------------ tests/test_direct_api/test_face.py | 4 ++-- tests/test_direct_api/test_shape.py | 4 ++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py index 8b8f264..7c89746 100644 --- a/src/build123d/topology/two_d.py +++ b/src/build123d/topology/two_d.py @@ -449,7 +449,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]): @overload def __init__( self, - obj: TopoDS_Face, + obj: TopoDS_Face | Plane, label: str = "", color: Color | None = None, parent: Compound | None = None, @@ -457,7 +457,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]): """Build a Face from an OCCT TopoDS_Shape/TopoDS_Face Args: - obj (TopoDS_Shape, optional): OCCT Face. + obj (TopoDS_Shape | Plane, optional): OCCT Face or Plane. label (str, optional): Defaults to ''. color (Color, optional): Defaults to None. parent (Compound, optional): assembly parent. Defaults to None. @@ -487,7 +487,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]): if 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) elif isinstance(args[0], Wire): outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * ( @@ -516,6 +518,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]): color = kwargs.get("color", color) parent = kwargs.get("parent", parent) + if isinstance(obj, Plane): + obj = BRepBuilderAPI_MakeFace(obj.wrapped).Face() + if outer_wire is not None: inner_topods_wires = ( [w.wrapped for w in inner_wires] if inner_wires is not None else [] @@ -1009,15 +1014,6 @@ class Face(Mixin2D, Shape[TopoDS_Face]): ).Face() ) - @classmethod - def make_plane( - cls, - plane: Plane = Plane.XY, - ) -> Face: - """Create a unlimited size Face aligned with plane""" - pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face() - return cls(pln_shape) - @classmethod def make_rect(cls, width: float, height: float, plane: Plane = Plane.XY) -> Face: """make_rect diff --git a/tests/test_direct_api/test_face.py b/tests/test_direct_api/test_face.py index f8619c5..2b71763 100644 --- a/tests/test_direct_api/test_face.py +++ b/tests/test_direct_api/test_face.py @@ -130,8 +130,8 @@ class TestFace(unittest.TestCase): distance=1, distance2=2, vertices=[vertex], edge=other_edge ) - def test_make_rect(self): - test_face = Face.make_plane() + def test_plane_as_face(self): + test_face = Face(Plane.XY) self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5) def test_length_width(self): diff --git a/tests/test_direct_api/test_shape.py b/tests/test_direct_api/test_shape.py index 2c0bb3c..4f69dbf 100644 --- a/tests/test_direct_api/test_shape.py +++ b/tests/test_direct_api/test_shape.py @@ -475,7 +475,7 @@ class TestShape(unittest.TestCase): self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5) 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.assertListEqual(edges, []) @@ -493,7 +493,7 @@ class TestShape(unittest.TestCase): self.assertEqual(len(edges1), 1) 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(edges2), 1) self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5) From 44faaae5a7ba1de7bb8467a2f94dac9cd2799896 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Wed, 5 Nov 2025 13:25:27 -0600 Subject: [PATCH 6/9] README.md -> use an absolute image link to fix logo on pypi --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b17f25..cb7c309 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- build123d logo + build123d logo

[![Documentation Status](https://readthedocs.org/projects/build123d/badge/?version=latest)](https://build123d.readthedocs.io/en/latest/?badge=latest) From 27567a10efe5d233acdac749d0582409e76800b5 Mon Sep 17 00:00:00 2001 From: snoyer Date: Fri, 7 Nov 2025 21:29:06 +0400 Subject: [PATCH 7/9] fix typo --- src/build123d/topology/two_d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py index 8c340a5..a41e249 100644 --- a/src/build123d/topology/two_d.py +++ b/src/build123d/topology/two_d.py @@ -2360,7 +2360,7 @@ class Shell(Mixin2D[TopoDS_Shell]): obj = obj_list[0] if isinstance(obj, Face): - if not obj.wrapped: + if not obj: raise ValueError(f"Can't create a Shell from empty Face") builder = BRep_Builder() shell = TopoDS_Shell() From 3bea4d32284b791cddff5ef4ee106b680eab22c2 Mon Sep 17 00:00:00 2001 From: Jonathan Wagenet Date: Fri, 7 Nov 2025 16:11:33 -0500 Subject: [PATCH 8/9] Re-add make_plane with depreciation warning --- src/build123d/topology/two_d.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py index 7c89746..82c09de 100644 --- a/src/build123d/topology/two_d.py +++ b/src/build123d/topology/two_d.py @@ -1014,6 +1014,21 @@ class Face(Mixin2D, Shape[TopoDS_Face]): ).Face() ) + @classmethod + def make_plane( + cls, + plane: Plane = Plane.XY, + ) -> Face: + """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() + return cls(pln_shape) + @classmethod def make_rect(cls, width: float, height: float, plane: Plane = Plane.XY) -> Face: """make_rect From 20854b3d4d3a3f6e4ecd35c4415807d4a106cf8a Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Wed, 12 Nov 2025 15:40:23 -0600 Subject: [PATCH 9/9] pyproject.toml -> pin to pytest==8.4.2 per pytest-dev/pytest-xdist/issues/1273 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fdb8bc0..22f6440 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ development = [ "black", "mypy", "pylint", - "pytest", + "pytest==8.4.2", # TODO: unpin on resolution of pytest-dev/pytest-xdist/issues/1273 "pytest-benchmark", "pytest-cov", "pytest-xdist",