Skip to content

Commit

Permalink
Adding Face.remove_holes and Face.total_area property
Browse files Browse the repository at this point in the history
  • Loading branch information
gumyr committed Feb 4, 2025
1 parent 0e3dbbe commit c728124
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 1 deletion.
41 changes: 40 additions & 1 deletion src/build123d/topology/two_d.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
from OCP.BRepGProp import BRepGProp, BRepGProp_Face
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling, BRepOffsetAPI_MakePipeShell
from OCP.BRepTools import BRepTools
from OCP.BRepTools import BRepTools, BRepTools_ReShape
from OCP.GProp import GProp_GProps
from OCP.Geom import Geom_BezierSurface, Geom_Surface
from OCP.GeomAPI import GeomAPI_PointsToBSplineSurface, GeomAPI_ProjectPointOnSurf
Expand Down Expand Up @@ -405,6 +405,23 @@ def length(self) -> None | float:
result = face_vertices[-1].X - face_vertices[0].X
return result

@property
def total_area(self) -> float:
"""
Calculate the total surface area of the face, including the areas of any holes.
This property returns the overall area of the face as if the inner boundaries (holes)
were filled in.
Returns:
float: The total surface area, including the area of holes. Returns 0.0 if
the face is empty.
"""
if self.wrapped is None:
return 0.0

return self.remove_holes().area

@property
def volume(self) -> float:
"""volume - the volume of this Face, which is always zero"""
Expand Down Expand Up @@ -1201,6 +1218,28 @@ def project_to_shape(
projected_shapes.append(shape)
return projected_shapes

def remove_holes(self) -> Face:
"""remove_holes
Remove all of the holes from this face.
Returns:
Face: A new Face instance identical to the original but without any holes.
"""
if self.wrapped is None:
raise ValueError("Cannot remove holes from an empty face")

if not (inner_wires := self.inner_wires()):
return self

holeless = copy.deepcopy(self)
reshaper = BRepTools_ReShape()
for hole_wire in inner_wires:
reshaper.Remove(hole_wire.wrapped)
modified_shape = downcast(reshaper.Apply(self.wrapped))
holeless.wrapped = modified_shape
return holeless

def to_arcs(self, tolerance: float = 1e-3) -> Face:
"""to_arcs
Expand Down
35 changes: 35 additions & 0 deletions tests/test_direct_api/test_face.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,41 @@ def test_normal_at(self):
face = Cylinder(1, 1).faces().filter_by(GeomType.CYLINDER)[0]
self.assertAlmostEqual(face.normal_at(0, 1), (1, 0, 0), 5)

def test_remove_holes(self):
# Planar test
frame = (Rectangle(1, 1) - Rectangle(0.5, 0.5)).face()
filled = frame.remove_holes()
self.assertEqual(len(frame.inner_wires()), 1)
self.assertEqual(len(filled.inner_wires()), 0)
self.assertAlmostEqual(frame.area, 0.75, 5)
self.assertAlmostEqual(filled.area, 1.0, 5)

# Errors
frame.wrapped = None
with self.assertRaises(ValueError):
frame.remove_holes()

# No holes
rect = Face.make_rect(1, 1)
self.assertEqual(rect, rect.remove_holes())

# Non-planar test
cyl_face = (
(Cylinder(1, 3) - Cylinder(0.5, 3, rotation=(90, 0, 0)))
.faces()
.sort_by(Face.area)[-1]
)
filled = cyl_face.remove_holes()
self.assertEqual(len(cyl_face.inner_wires()), 2)
self.assertEqual(len(filled.inner_wires()), 0)
self.assertTrue(cyl_face.area < filled.area)
self.assertAlmostEqual(cyl_face.total_area, filled.area, 5)

def test_total_area(self):
frame = (Rectangle(1, 1) - Rectangle(0.5, 0.5)).face()
frame.wrapped = None
self.assertAlmostEqual(frame.total_area, 0.0, 5)


if __name__ == "__main__":
unittest.main()

0 comments on commit c728124

Please sign in to comment.