diff --git a/src/build123d/geometry.py b/src/build123d/geometry.py index ff8f264..3e3807f 100644 --- a/src/build123d/geometry.py +++ b/src/build123d/geometry.py @@ -41,7 +41,7 @@ import json import logging import warnings from collections.abc import Callable, Iterable, Sequence -from math import degrees, isclose, log10, pi, radians +from math import degrees, isclose, log10, pi, radians, prod from typing import TYPE_CHECKING, Any, TypeAlias, overload import numpy as np @@ -1001,6 +1001,16 @@ class BoundBox: self.max = Vector(x_max, y_max, z_max) #: location of maximum corner self.size = Vector(x_max - x_min, y_max - y_min, z_max - z_min) #: overall size + @property + def measure(self) -> float: + """Return the overall Lebesgue measure of the bounding box. + + - For 1D objects: length + - For 2D objects: area + - For 3D objects: volume + """ + return prod([x for x in self.size if x > TOLERANCE]) + @property def diagonal(self) -> float: """body diagonal length (i.e. object maximum size)""" diff --git a/src/build123d/operations_sketch.py b/src/build123d/operations_sketch.py index 6cdf780..e05a542 100644 --- a/src/build123d/operations_sketch.py +++ b/src/build123d/operations_sketch.py @@ -44,6 +44,7 @@ from build123d.topology import ( Sketch, topo_explore_connected_edges, topo_explore_common_vertex, + edges_to_wires, ) from build123d.geometry import Plane, Vector, TOLERANCE from build123d.build_common import flatten_sequence, validate_inputs @@ -200,26 +201,33 @@ def make_face( ) -> Sketch: """Sketch Operation: make_face - Create a face from the given perimeter edges. + Create a face from the given edges. Args: - edges (Edge): sequence of perimeter edges. Defaults to all - sketch pending edges. + edges (Edge): sequence of edges. Defaults to all sketch pending edges. mode (Mode, optional): combination mode. Defaults to Mode.ADD. """ context: BuildSketch | None = BuildSketch._get_context("make_face") if edges is not None: - outer_edges = flatten_sequence(edges) + raw_edges = flatten_sequence(edges) elif context is not None: - outer_edges = context.pending_edges + raw_edges = context.pending_edges else: raise ValueError("No objects to create a face") - if not outer_edges: - raise ValueError("No objects to create a hull") - validate_inputs(context, "make_face", outer_edges) + if not raw_edges: + raise ValueError("No objects to create a face") + validate_inputs(context, "make_face", raw_edges) - pending_face = Face(Wire.combine(outer_edges)[0]) + wires = list( + edges_to_wires(raw_edges).sort_by( + lambda w: w.bounding_box().measure, reverse=True + ) + ) + if len(wires) > 1: + pending_face = Face(wires[0], wires[1:]) + else: + pending_face = Face(wires[0]) if pending_face.normal_at().Z < 0: # flip up-side-down faces pending_face = -pending_face diff --git a/tests/test_build_sketch.py b/tests/test_build_sketch.py index c00a504..3909733 100644 --- a/tests/test_build_sketch.py +++ b/tests/test_build_sketch.py @@ -168,6 +168,17 @@ class TestUpSideDown(unittest.TestCase): sketch = make_face(wire.edges()) self.assertTrue(sketch.faces()[0].normal_at().Z > 0) + def test_make_face_with_holes(self): + with BuildSketch() as skt: + with BuildLine() as perimeter: + CenterArc((0, 0), 3, 0, 360) + with BuildLine() as hole1: + Polyline((-1, 1), (1, 1), (1, 2), (-1, 2), (-1, 1)) + with BuildLine() as hole2: + Airfoil("4020") + make_face() + self.assertEqual(len(skt.face().inner_wires()), 2) + class TestBuildSketchExceptions(unittest.TestCase): """Test exception handling""" diff --git a/tests/test_direct_api/test_bound_box.py b/tests/test_direct_api/test_bound_box.py index de4ebee..26e4ddf 100644 --- a/tests/test_direct_api/test_bound_box.py +++ b/tests/test_direct_api/test_bound_box.py @@ -43,6 +43,7 @@ class TestBoundBox(unittest.TestCase): # OCC uses some approximations self.assertAlmostEqual(bb1.size.X, 1.0, 1) + self.assertAlmostEqual(bb1.measure, 1.0, 5) # Test adding to an existing bounding box v0 = Vertex(0, 0, 0) @@ -50,6 +51,7 @@ class TestBoundBox(unittest.TestCase): bb3 = bb1.add(bb2) self.assertAlmostEqual(bb3.size, (2, 2, 2), 7) + self.assertAlmostEqual(bb3.measure, 8, 5) bb3 = bb2.add((3, 3, 3)) self.assertAlmostEqual(bb3.size, (3, 3, 3), 7) @@ -61,6 +63,7 @@ class TestBoundBox(unittest.TestCase): bb1 = Vertex(1, 1, 0).bounding_box().add(Vertex(2, 2, 0).bounding_box()) bb2 = Vertex(0, 0, 0).bounding_box().add(Vertex(3, 3, 0).bounding_box()) bb3 = Vertex(0, 0, 0).bounding_box().add(Vertex(1.5, 1.5, 0).bounding_box()) + self.assertAlmostEqual(bb2.measure, 9, 5) # Test that bb2 contains bb1 self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2)) self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1))