Convert Shape methods to properties: is_null, is_valid, shape_type

This commit is contained in:
gumyr 2025-05-27 14:38:21 -04:00
parent f445de32c9
commit 560a5369b7
14 changed files with 75 additions and 82 deletions

View file

@ -304,11 +304,11 @@ def loft(
new_solid = Solid.make_loft(loft_wires, ruled)
# Try to recover an invalid loft
if not new_solid.is_valid():
if not new_solid.is_valid:
new_solid = Solid(Shell(new_solid.faces() + section_list))
if clean:
new_solid = new_solid.clean()
if not new_solid.is_valid():
if not new_solid.is_valid:
raise RuntimeError("Failed to create valid loft")
if context is not None:

View file

@ -422,6 +422,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
return True
@property
def is_null(self) -> bool:
"""Returns true if this shape is null. In other words, it references no
underlying shape with the potential to be given a location and an
orientation.
"""
return self.wrapped 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"""
@ -431,6 +439,18 @@ class Shape(NodeMixin, Generic[TOPODS]):
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
return is_face_planar.IsPlanar()
@property
def is_valid(self) -> bool:
"""Returns True if no defect is detected on the shape S or any of its
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked.
"""
if self.wrapped is None:
return True
chk = BRepCheck_Analyzer(self.wrapped)
chk.SetParallel(True)
return chk.IsValid()
@property
def location(self) -> Location | None:
"""Get this Shape's Location"""
@ -543,6 +563,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
(Vector(principal_props.ThirdAxisOfInertia()), principal_moments[2]),
]
@property
def shape_type(self) -> Shapes:
"""Return the shape type string for this class"""
return tcast(Shapes, Shape.shape_LUT[shapetype(self.wrapped)])
@property
def static_moments(self) -> tuple[float, float, float]:
"""
@ -1188,7 +1213,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"""fix - try to fix shape if not valid"""
if self.wrapped is None:
return self
if not self.is_valid():
if not self.is_valid:
shape_copy: Shape = copy.deepcopy(self, None)
shape_copy.wrapped = tcast(TOPODS, fix(self.wrapped))
@ -1332,7 +1357,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
return None
if (
not isinstance(shape_intersections, ShapeList)
and shape_intersections.is_null()
and shape_intersections.is_null
):
return None
return shape_intersections
@ -1352,18 +1377,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
return False
return self.wrapped.IsEqual(other.wrapped)
def is_null(self) -> bool:
"""Returns true if this shape is null. In other words, it references no
underlying shape with the potential to be given a location and an
orientation.
Args:
Returns:
"""
return self.wrapped is None or self.wrapped.IsNull()
def is_same(self, other: Shape) -> bool:
"""Returns True if other and this shape are same, i.e. if they share the
same TShape with the same Locations. Orientations may differ. Also see
@ -1379,22 +1392,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
return False
return self.wrapped.IsSame(other.wrapped)
def is_valid(self) -> bool:
"""Returns True if no defect is detected on the shape S or any of its
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked.
Args:
Returns:
"""
if self.wrapped is None:
return True
chk = BRepCheck_Analyzer(self.wrapped)
chk.SetParallel(True)
return chk.IsValid()
def locate(self, loc: Location) -> Self:
"""Apply a location in absolute sense to self
@ -1677,10 +1674,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
return self._apply_transform(transformation)
def shape_type(self) -> Shapes:
"""Return the shape type string for this class"""
return tcast(Shapes, Shape.shape_LUT[shapetype(self.wrapped)])
def shell(self) -> Shell | None:
"""Return the Shell"""
return None

View file

@ -259,7 +259,7 @@ class Mixin3D(Shape):
try:
new_shape = self.__class__(chamfer_builder.Shape())
if not new_shape.is_valid():
if not new_shape.is_valid:
raise Standard_Failure
except (StdFail_NotDone, Standard_Failure) as err:
raise ValueError(
@ -343,7 +343,7 @@ class Mixin3D(Shape):
try:
new_shape = self.__class__(fillet_builder.Shape())
if not new_shape.is_valid():
if not new_shape.is_valid:
raise Standard_Failure
except (StdFail_NotDone, Standard_Failure) as err:
raise ValueError(
@ -485,7 +485,7 @@ class Mixin3D(Shape):
# Do these numbers work? - if not try with the smaller window
try:
new_shape = self.__class__(fillet_builder.Shape())
if not new_shape.is_valid():
if not new_shape.is_valid:
raise fillet_exception
except fillet_exception:
return __max_fillet(window_min, window_mid, current_iteration + 1)
@ -499,7 +499,7 @@ class Mixin3D(Shape):
)
return return_value
if not self.is_valid():
if not self.is_valid:
raise ValueError("Invalid Shape")
native_edges = [e.wrapped for e in edge_list]

View file

@ -374,7 +374,7 @@ class Mixin2D(Shape):
raise RuntimeError(
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
)
if not wrapped_edge.is_valid():
if not wrapped_edge.is_valid:
raise RuntimeError("Wrapped edge is invalid")
if not snap_to_face:
@ -1016,7 +1016,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) from err
surface_face = surface_face.fix()
if not surface_face.is_valid():
if not surface_face.is_valid:
raise RuntimeError("non planar face is invalid")
return surface_face
@ -1443,7 +1443,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) from err
surface_face = surface_face.fix()
# if not surface_face.is_valid():
# if not surface_face.is_valid:
# raise RuntimeError("non planar face is invalid")
return surface_face
@ -2021,7 +2021,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
#
# Part 5: Validate
#
if not wrapped_wire.is_valid():
if not wrapped_wire.is_valid:
raise RuntimeError("wrapped wire is not valid")
return wrapped_wire

View file

@ -522,7 +522,7 @@ class OffsetTests(unittest.TestCase):
def test_face_offset_with_holes(self):
sk = Rectangle(100, 100) - GridLocations(80, 80, 2, 2) * Circle(5)
sk2 = offset(sk, -5)
self.assertTrue(sk2.face().is_valid())
self.assertTrue(sk2.face().is_valid)
self.assertLess(sk2.area, sk.area)
self.assertEqual(len(sk2), 1)
@ -881,7 +881,7 @@ class TestSweep(unittest.TestCase):
Rectangle(2 * lip, 2 * lip, align=(Align.CENTER, Align.CENTER))
sweep(sections=sk2.sketch, path=topedgs, mode=Mode.SUBTRACT)
self.assertTrue(p.part.is_valid())
self.assertTrue(p.part.is_valid)
def test_path_error(self):
e1 = Edge.make_line((0, 0), (1, 0))

View file

@ -51,10 +51,10 @@ class TestCompound(unittest.TestCase):
box1 = Solid.make_box(1, 1, 1)
box2 = Solid.make_box(1, 1, 1, Plane((1, 0, 0)))
combined = Compound([box1]).fuse(box2, glue=True)
self.assertTrue(combined.is_valid())
self.assertTrue(combined.is_valid)
self.assertAlmostEqual(combined.volume, 2, 5)
fuzzy = Compound([box1]).fuse(box2, tol=1e-6)
self.assertTrue(fuzzy.is_valid())
self.assertTrue(fuzzy.is_valid)
self.assertAlmostEqual(fuzzy.volume, 2, 5)
def test_remove(self):

View file

@ -65,7 +65,7 @@ class TestFace(unittest.TestCase):
bottom_edge = Edge.make_circle(radius=1, end_angle=90)
top_edge = Edge.make_circle(radius=1, plane=Plane((0, 0, 1)), end_angle=90)
curved = Face.make_surface_from_curves(bottom_edge, top_edge)
self.assertTrue(curved.is_valid())
self.assertTrue(curved.is_valid)
self.assertAlmostEqual(curved.area, math.pi / 2, 5)
self.assertAlmostEqual(
curved.normal_at(), (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5
@ -74,7 +74,7 @@ class TestFace(unittest.TestCase):
bottom_wire = Wire.make_circle(1)
top_wire = Wire.make_circle(1, Plane((0, 0, 1)))
curved = Face.make_surface_from_curves(bottom_wire, top_wire)
self.assertTrue(curved.is_valid())
self.assertTrue(curved.is_valid)
self.assertAlmostEqual(curved.area, 2 * math.pi, 5)
def test_center(self):
@ -303,7 +303,7 @@ class TestFace(unittest.TestCase):
for j in range(4 - i % 2)
]
cylinder_walls_with_holes = cylinder_wall.make_holes(projected_wires)
self.assertTrue(cylinder_walls_with_holes.is_valid())
self.assertTrue(cylinder_walls_with_holes.is_valid)
self.assertLess(cylinder_walls_with_holes.area, cylinder_wall.area)
def test_is_inside(self):
@ -377,7 +377,7 @@ class TestFace(unittest.TestCase):
surface_points=[Vector(0, 0, -5)],
interior_wires=[hole],
)
self.assertTrue(surface.is_valid())
self.assertTrue(surface.is_valid)
self.assertEqual(surface.geom_type, GeomType.BSPLINE)
bbox = surface.bounding_box()
self.assertAlmostEqual(bbox.min, (-50.5, -24.5, -5.113393280136395), 5)
@ -877,7 +877,7 @@ class TestFace(unittest.TestCase):
with self.assertRaises(RuntimeError):
surface.wrap(star.outer_wire(), target)
@patch.object(Wire, "is_valid", return_value=False)
@patch.object(Wire, "is_valid", new_callable=PropertyMock, return_value=False)
def test_wrap_invalid_wire(self, mock_is_valid):
surface = Cone(5, 2, 10).faces().filter_by(GeomType.PLANE, reverse=True)[0]
target = surface.location_at(0.5, 0.5, x_dir=(1, 0, 0))

View file

@ -40,11 +40,11 @@ class TestImportExport(unittest.TestCase):
original_box = Solid.make_box(1, 1, 1)
export_step(original_box, "test_box.step")
step_box = import_step("test_box.step")
self.assertTrue(step_box.is_valid())
self.assertTrue(step_box.is_valid)
self.assertAlmostEqual(step_box.volume, 1, 5)
export_brep(step_box, "test_box.brep")
brep_box = import_brep("test_box.brep")
self.assertTrue(brep_box.is_valid())
self.assertTrue(brep_box.is_valid)
self.assertAlmostEqual(brep_box.volume, 1, 5)
os.remove("test_box.step")
os.remove("test_box.brep")

View file

@ -27,7 +27,7 @@ license:
"""
import unittest
from unittest.mock import patch
from unittest.mock import patch, PropertyMock
from build123d.build_enums import CenterOf, Kind
from build123d.geometry import Axis, Plane
@ -67,7 +67,7 @@ class TestMixin3D(unittest.TestCase):
face = box.faces().sort_by(Axis.Z)[0]
self.assertRaises(ValueError, box.chamfer, 0.1, None, edge, face=face)
@patch.object(Shape, "is_valid", return_value=False)
@patch.object(Shape, "is_valid", new_callable=PropertyMock, return_value=False)
def test_chamfer_invalid_shape_raises_error(self, mock_is_valid):
box = Solid.make_box(1, 1, 1)
@ -111,7 +111,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [f], additive=False
)
self.assertTrue(d.is_valid())
self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5)
# face with depth
@ -119,7 +119,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [f], depth=0.5, thru_all=False, additive=False
)
self.assertTrue(d.is_valid())
self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**3, 5)
# face until
@ -128,7 +128,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [f], up_to_face=limit, thru_all=False, additive=False
)
self.assertTrue(d.is_valid())
self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**3, 5)
# wire
@ -136,7 +136,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [w], additive=False
)
self.assertTrue(d.is_valid())
self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5)
def test_center(self):

View file

@ -29,7 +29,7 @@ license:
# Always equal to any other object, to test that __eq__ cooperation is working
import unittest
from random import uniform
from unittest.mock import patch
from unittest.mock import patch, PropertyMock
import numpy as np
from build123d.build_enums import CenterOf, Keep
@ -100,7 +100,7 @@ class TestShape(unittest.TestCase):
Shape.combined_center(objs, center_of=CenterOf.GEOMETRY)
def test_shape_type(self):
self.assertEqual(Vertex().shape_type(), "Vertex")
self.assertEqual(Vertex().shape_type, "Vertex")
def test_scale(self):
self.assertAlmostEqual(Solid.make_box(1, 1, 1).scale(2).volume, 2**3, 5)
@ -109,10 +109,10 @@ class TestShape(unittest.TestCase):
box1 = Solid.make_box(1, 1, 1)
box2 = Solid.make_box(1, 1, 1, Plane((1, 0, 0)))
combined = box1.fuse(box2, glue=True)
self.assertTrue(combined.is_valid())
self.assertTrue(combined.is_valid)
self.assertAlmostEqual(combined.volume, 2, 5)
fuzzy = box1.fuse(box2, tol=1e-6)
self.assertTrue(fuzzy.is_valid())
self.assertTrue(fuzzy.is_valid)
self.assertAlmostEqual(fuzzy.volume, 2, 5)
def test_faces_intersected_by_axis(self):
@ -245,7 +245,7 @@ class TestShape(unittest.TestCase):
# invalid_object = box.fillet(0.75, box.edges())
# invalid_object.max_fillet(invalid_object.edges())
@patch.object(Shape, "is_valid", return_value=False)
@patch.object(Shape, "is_valid", new_callable=PropertyMock, return_value=False)
def test_max_fillet_invalid_shape_raises_error(self, mock_is_valid):
box = Solid.make_box(1, 1, 1)
@ -526,7 +526,7 @@ class TestShape(unittest.TestCase):
self.assertEqual(hash(empty), 0)
self.assertFalse(empty.is_same(Solid()))
self.assertFalse(empty.is_equal(Solid()))
self.assertTrue(empty.is_valid())
self.assertTrue(empty.is_valid)
empty_bbox = empty.bounding_box()
self.assertEqual(tuple(empty_bbox.size), (0, 0, 0))
self.assertIs(empty, empty.mirror(Plane.XY))

View file

@ -41,12 +41,12 @@ class TestShells(unittest.TestCase):
def test_shell_init(self):
box_faces = Solid.make_box(1, 1, 1).faces()
box_shell = Shell(box_faces)
self.assertTrue(box_shell.is_valid())
self.assertTrue(box_shell.is_valid)
def test_shell_init_single_face(self):
face = Solid.make_cone(1, 0, 2).faces().filter_by(GeomType.CONE).first
shell = Shell(face)
self.assertTrue(shell.is_valid())
self.assertTrue(shell.is_valid)
def test_center(self):
box_faces = Solid.make_box(1, 1, 1).faces()
@ -71,9 +71,9 @@ class TestShells(unittest.TestCase):
x_section = Rot(90) * Spline((0, -5), (-3, -2), (-2, 0), (-3, 2), (0, 5))
surface = sweep(x_section, Circle(5).wire())
single_face = Shell(surface.face())
self.assertTrue(single_face.is_valid())
self.assertTrue(single_face.is_valid)
single_face = Shell(surface.faces())
self.assertTrue(single_face.is_valid())
self.assertTrue(single_face.is_valid)
def test_sweep(self):
path_c1 = JernArc((0, 0), (-1, 0), 1, 180)

View file

@ -59,7 +59,7 @@ class TestSolid(unittest.TestCase):
box = Solid(box_shell)
self.assertAlmostEqual(box.area, 6, 5)
self.assertAlmostEqual(box.volume, 1, 5)
self.assertTrue(box.is_valid())
self.assertTrue(box.is_valid)
def test_extrude(self):
v = Edge.extrude(Vertex(1, 1, 1), (0, 0, 1))

View file

@ -191,26 +191,26 @@ class TestWire(unittest.TestCase):
e1 = Edge.make_line((1, 0), (1, 1))
w0 = Wire.make_circle(1)
w1 = Wire(e0)
self.assertTrue(w1.is_valid())
self.assertTrue(w1.is_valid)
w2 = Wire([e0])
self.assertAlmostEqual(w2.length, 1, 5)
self.assertTrue(w2.is_valid())
self.assertTrue(w2.is_valid)
w3 = Wire([e0, e1])
self.assertTrue(w3.is_valid())
self.assertTrue(w3.is_valid)
self.assertAlmostEqual(w3.length, 2, 5)
w4 = Wire(w0.wrapped)
self.assertTrue(w4.is_valid())
self.assertTrue(w4.is_valid)
w5 = Wire(obj=w0.wrapped)
self.assertTrue(w5.is_valid())
self.assertTrue(w5.is_valid)
w6 = Wire(obj=w0.wrapped, label="w6", color=Color("red"))
self.assertTrue(w6.is_valid())
self.assertTrue(w6.is_valid)
self.assertEqual(w6.label, "w6")
np.testing.assert_allclose(tuple(w6.color), (1.0, 0.0, 0.0, 1.0), 1e-5)
w7 = Wire(w6)
self.assertTrue(w7.is_valid())
self.assertTrue(w7.is_valid)
c0 = Polyline((0, 0), (1, 0), (1, 1))
w8 = Wire(c0)
self.assertTrue(w8.is_valid())
self.assertTrue(w8.is_valid)
with self.assertRaises(ValueError):
Wire(bob="fred")

View file

@ -208,7 +208,7 @@ class TestHollowImport(unittest.TestCase):
export_stl(test_shape, "test.stl")
importer = Mesher()
stl = importer.read("test.stl")
self.assertTrue(stl[0].is_valid())
self.assertTrue(stl[0].is_valid)
class TestImportDegenerateTriangles(unittest.TestCase):
@ -221,7 +221,7 @@ class TestImportDegenerateTriangles(unittest.TestCase):
stl = importer.read("cyl_w_rect_hole.stl")[0]
self.assertEqual(type(stl), Solid)
self.assertTrue(stl.is_manifold)
self.assertTrue(stl.is_valid())
self.assertTrue(stl.is_valid)
self.assertEqual(sum(f.area == 0 for f in stl.faces()), 0)