Enhanced make_face so faces can have holes. Added BoundBox.measure

This commit is contained in:
gumyr 2025-11-20 11:15:12 -05:00
parent e6d272b2fa
commit a5e95fe72f
4 changed files with 42 additions and 10 deletions

View file

@ -41,7 +41,7 @@ import json
import logging import logging
import warnings import warnings
from collections.abc import Callable, Iterable, Sequence 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 from typing import TYPE_CHECKING, Any, TypeAlias, overload
import numpy as np import numpy as np
@ -1001,6 +1001,16 @@ class BoundBox:
self.max = Vector(x_max, y_max, z_max) #: location of maximum corner 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 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 @property
def diagonal(self) -> float: def diagonal(self) -> float:
"""body diagonal length (i.e. object maximum size)""" """body diagonal length (i.e. object maximum size)"""

View file

@ -44,6 +44,7 @@ from build123d.topology import (
Sketch, Sketch,
topo_explore_connected_edges, topo_explore_connected_edges,
topo_explore_common_vertex, topo_explore_common_vertex,
edges_to_wires,
) )
from build123d.geometry import Plane, Vector, TOLERANCE from build123d.geometry import Plane, Vector, TOLERANCE
from build123d.build_common import flatten_sequence, validate_inputs from build123d.build_common import flatten_sequence, validate_inputs
@ -200,26 +201,33 @@ def make_face(
) -> Sketch: ) -> Sketch:
"""Sketch Operation: make_face """Sketch Operation: make_face
Create a face from the given perimeter edges. Create a face from the given edges.
Args: Args:
edges (Edge): sequence of perimeter edges. Defaults to all edges (Edge): sequence of edges. Defaults to all sketch pending edges.
sketch pending edges.
mode (Mode, optional): combination mode. Defaults to Mode.ADD. mode (Mode, optional): combination mode. Defaults to Mode.ADD.
""" """
context: BuildSketch | None = BuildSketch._get_context("make_face") context: BuildSketch | None = BuildSketch._get_context("make_face")
if edges is not None: if edges is not None:
outer_edges = flatten_sequence(edges) raw_edges = flatten_sequence(edges)
elif context is not None: elif context is not None:
outer_edges = context.pending_edges raw_edges = context.pending_edges
else: else:
raise ValueError("No objects to create a face") raise ValueError("No objects to create a face")
if not outer_edges: if not raw_edges:
raise ValueError("No objects to create a hull") raise ValueError("No objects to create a face")
validate_inputs(context, "make_face", outer_edges) 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 if pending_face.normal_at().Z < 0: # flip up-side-down faces
pending_face = -pending_face pending_face = -pending_face

View file

@ -168,6 +168,17 @@ class TestUpSideDown(unittest.TestCase):
sketch = make_face(wire.edges()) sketch = make_face(wire.edges())
self.assertTrue(sketch.faces()[0].normal_at().Z > 0) 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): class TestBuildSketchExceptions(unittest.TestCase):
"""Test exception handling""" """Test exception handling"""

View file

@ -43,6 +43,7 @@ class TestBoundBox(unittest.TestCase):
# OCC uses some approximations # OCC uses some approximations
self.assertAlmostEqual(bb1.size.X, 1.0, 1) self.assertAlmostEqual(bb1.size.X, 1.0, 1)
self.assertAlmostEqual(bb1.measure, 1.0, 5)
# Test adding to an existing bounding box # Test adding to an existing bounding box
v0 = Vertex(0, 0, 0) v0 = Vertex(0, 0, 0)
@ -50,6 +51,7 @@ class TestBoundBox(unittest.TestCase):
bb3 = bb1.add(bb2) bb3 = bb1.add(bb2)
self.assertAlmostEqual(bb3.size, (2, 2, 2), 7) self.assertAlmostEqual(bb3.size, (2, 2, 2), 7)
self.assertAlmostEqual(bb3.measure, 8, 5)
bb3 = bb2.add((3, 3, 3)) bb3 = bb2.add((3, 3, 3))
self.assertAlmostEqual(bb3.size, (3, 3, 3), 7) 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()) 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()) 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()) 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 # Test that bb2 contains bb1
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2)) self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb1, bb2))
self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1)) self.assertEqual(bb2, BoundBox.find_outside_box_2d(bb2, bb1))