Add OBB clip-plane and planar tessellation to tool.Cad

Adds geometry primitives the viewport clip-box feature needs:

- obb_world_clip_planes / obb_clip_planes_from_matrix: derive the 6
  inward clip planes of an oriented bounding box (or unit cube under
  a matrix_world) in RegionView3D.clip_planes form. expand / expand_rel
  margins let callers visualising the box with overlapping geometry
  (an empty CUBE display sharing edges with the planes) keep the box's
  own wireframe inside the clip volume.
- point_is_inside_clip_planes / corners_might_cross_clip_planes: cheap
  reject tests for the per-mesh capping pass to skip the expensive
  bisect when an object's AABB is fully outside the box.
- newell_normal / plane_basis: robust planar-ring normal for thin
  near-degenerate cap rings where a two-edge cross product is unstable.
- tessellate_ring_planar: triangulate [outer, *inners] 3D rings in the
  outer ring's best-fit plane, with a shapely constrained-Delaunay
  fallback for the known failure mode of mathutils.tessellate_polygon
  on complex concave polygons-with-holes.

Tests cover unit-box, translated, rotated, and scaled cases for the
OBB-from-matrix builder + the rejection helpers.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-16 13:24:42 +02:00
parent a56b5660d0
commit 5cc9daa2f9
2 changed files with 318 additions and 1 deletions
+87 -1
View File
@@ -16,7 +16,9 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from mathutils import Vector
import math
from mathutils import Matrix, Vector
from bonsai.tool.cad import Cad as subject
from test.bim.bootstrap import NewFile
@@ -88,3 +90,87 @@ class TestClosestPoints(NewFile):
edge1 = (V(0, 0, 0), V(0, 0, 0))
edge2 = (V(1, 0, 1), V(2, 0, 2))
assert subject.closest_points(edge1, edge2)[0] == (edge1[0], edge2[0])
class TestObbWorldClipPlanes(NewFile):
def test_unit_box_at_origin_returns_axis_aligned_planes(self):
planes = subject.obb_world_clip_planes(
V(0, 0, 0),
(V(1, 0, 0), V(0, 1, 0), V(0, 0, 1)),
V(1, 1, 1),
)
assert planes[0] == (-1.0, 0.0, 0.0, 1.0)
assert planes[1] == (1.0, 0.0, 0.0, 1.0)
assert planes[2] == (0.0, -1.0, 0.0, 1.0)
assert planes[3] == (0.0, 1.0, 0.0, 1.0)
assert planes[4] == (0.0, 0.0, -1.0, 1.0)
assert planes[5] == (0.0, 0.0, 1.0, 1.0)
def test_center_is_inside_all_planes(self):
center = V(5, -3, 2)
planes = subject.obb_world_clip_planes(
center,
(V(1, 0, 0), V(0, 1, 0), V(0, 0, 1)),
V(2, 1, 0.5),
)
assert subject.point_is_inside_clip_planes(planes, center)
def test_point_just_outside_positive_x_face_rejected(self):
planes = subject.obb_world_clip_planes(
V(0, 0, 0),
(V(1, 0, 0), V(0, 1, 0), V(0, 0, 1)),
V(1, 1, 1),
)
assert subject.point_is_inside_clip_planes(planes, V(0.5, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(1.5, 0, 0))
def test_rotated_obb_clips_along_rotated_axes(self):
s = math.sin(math.radians(45))
planes = subject.obb_world_clip_planes(
V(0, 0, 0),
(V(s, s, 0), V(-s, s, 0), V(0, 0, 1)),
V(1, 1, 1),
)
assert subject.point_is_inside_clip_planes(planes, V(1.2, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(1.42, 0, 0))
def test_zero_extent_axis_does_not_raise(self):
planes = subject.obb_world_clip_planes(
V(0, 0, 0),
(V(1, 0, 0), V(0, 1, 0), V(0, 0, 1)),
V(1, 1, 0),
)
assert subject.point_is_inside_clip_planes(planes, V(0, 0, 0))
class TestObbClipPlanesFromMatrix(NewFile):
def test_identity_matches_unit_box(self):
planes = subject.obb_clip_planes_from_matrix(Matrix.Identity(4))
assert subject.point_is_inside_clip_planes(planes, V(0, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(2, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(0, -2, 0))
def test_translated_host_shifts_clip_region(self):
translated = Matrix.Translation(V(10, 0, 0))
planes = subject.obb_clip_planes_from_matrix(translated)
assert not subject.point_is_inside_clip_planes(planes, V(0, 0, 0))
assert subject.point_is_inside_clip_planes(planes, V(10, 0, 0))
def test_z_rotation_rotates_box(self):
rot = Matrix.Rotation(math.radians(45), 4, "Z")
planes = subject.obb_clip_planes_from_matrix(rot)
assert subject.point_is_inside_clip_planes(planes, V(1.2, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(1.42, 0, 0))
def test_host_scale_scales_box_extents(self):
scaled = Matrix.Diagonal((2.0, 2.0, 2.0, 1.0))
planes = subject.obb_clip_planes_from_matrix(scaled)
assert subject.point_is_inside_clip_planes(planes, V(1.9, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(2.1, 0, 0))
def test_non_uniform_scale_axis_independent(self):
scaled = Matrix.Diagonal((3.0, 1.0, 1.0, 1.0))
planes = subject.obb_clip_planes_from_matrix(scaled)
assert subject.point_is_inside_clip_planes(planes, V(2.9, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(3.1, 0, 0))
assert not subject.point_is_inside_clip_planes(planes, V(0, 1.1, 0))