build123d/tests/test_topo_explore.py
2025-08-30 10:16:39 -04:00

264 lines
9.5 KiB
Python

from typing import Optional
import unittest
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace
from OCP.GProp import GProp_GProps
from OCP.BRepGProp import BRepGProp
from OCP.gp import gp_Pnt, gp_Pln
from OCP.TopoDS import TopoDS_Face, TopoDS_Shape
from build123d.build_enums import ContinuityLevel, GeomType, SortBy
from build123d.objects_part import Box
from build123d.geometry import (
Axis,
Vector,
VectorLike,
)
from build123d.topology import (
Edge,
Face,
ShapeList,
Shell,
Wire,
offset_topods_face,
topo_explore_connected_edges,
topo_explore_connected_faces,
topo_explore_common_vertex,
)
class DirectApiTestCase(unittest.TestCase):
def assertTupleAlmostEquals(
self,
first: tuple[float, ...],
second: tuple[float, ...],
places: int,
msg: Optional[str] = None,
):
"""Check Tuples"""
self.assertEqual(len(second), len(first))
for i, j in zip(second, first):
self.assertAlmostEqual(i, j, places, msg=msg)
def assertVectorAlmostEquals(
self, first: Vector, second: VectorLike, places: int, msg: Optional[str] = None
):
second_vector = Vector(second)
self.assertAlmostEqual(first.X, second_vector.X, places, msg=msg)
self.assertAlmostEqual(first.Y, second_vector.Y, places, msg=msg)
self.assertAlmostEqual(first.Z, second_vector.Z, places, msg=msg)
class TestTopoExplore(DirectApiTestCase):
def test_topo_explore_connected_edges(self):
# 2D
triangle = Face(
Wire(
[
Edge.make_line((0, 0), (2.5, 0)),
Edge.make_line((2.5, 0), (4.0)),
Edge.make_line((4, 0), (0, 3)),
Edge.make_line((0, 3), (0, 1.25)),
Edge.make_line((0, 1.25), (0, 0)),
]
)
)
hypotenuse = triangle.edges().sort_by(SortBy.LENGTH)[-1]
connected_edges = topo_explore_connected_edges(hypotenuse).sort_by(
SortBy.LENGTH
)
self.assertAlmostEqual(connected_edges[0].length, 1.5, 5)
self.assertAlmostEqual(connected_edges[1].length, 1.75, 5)
# 3D
box = Box(1, 1, 1)
first_edge = box.edges()[0]
connected_edges = topo_explore_connected_edges(first_edge)
self.assertEqual(len(connected_edges), 4)
self.assertEqual(len(topo_explore_connected_edges(hypotenuse, parent=box)), 0)
# 2 Edges
l1 = Edge.make_spline([(-1, 0), (1, 0)], tangents=((0, -8), (0, 8)), scale=True)
l2 = Edge.make_line(l1 @ 0, l1 @ 1)
face = Face(Wire([l1, l2]))
connected_edges = topo_explore_connected_edges(face.edges()[0])
self.assertEqual(len(connected_edges), 1)
def test_topo_explore_connected_edges_errors(self):
# No parent case
with self.assertRaises(ValueError):
topo_explore_connected_edges(Edge())
# Null edge case
null_edge = Wire.make_rect(1, 1).edges()[0]
null_edge.wrapped = None
with self.assertRaises(ValueError):
topo_explore_connected_edges(null_edge)
def test_topo_explore_connected_edges_continuity(self):
# Create a 3-edge wire: straight line + smooth spline + sharp corner
# First edge: straight line
e1 = Edge.make_line((0, 0), (1, 0))
# Second edge: spline tangent-aligned to e1 (G1 continuous)
e2 = Edge.make_spline([e1 @ 1, (1, 1)], tangents=[(1, 0), (-1, 0)])
# Third edge: sharp corner from e2 (no G1 continuity)
e3 = Edge.make_line(e2 @ 1, e1 @ 0)
face = Face(Wire([e1, e2, e3]))
extracted_e1 = face.edges().sort_by(Axis.Y)[0]
extracted_e2 = face.edges().filter_by(GeomType.LINE, reverse=True)[0]
# Test C0: Should find both e2 and e3 connected to e1 and e2 respectively
connected_c0 = topo_explore_connected_edges(
extracted_e1, continuity=ContinuityLevel.C0
)
self.assertEqual(len(connected_c0), 2)
self.assertTrue(
connected_c0.filter_by(GeomType.LINE, reverse=True)[0].is_same(extracted_e2)
)
# Test C1: Should still find e2 connected to e1 (they're tangent aligned)
connected_c1 = topo_explore_connected_edges(
extracted_e1, continuity=ContinuityLevel.C1
)
self.assertEqual(len(connected_c1), 1)
self.assertTrue(connected_c1[0].is_same(extracted_e2))
# Test C2: No edges are curvature continuous at the junctions
connected_c2 = topo_explore_connected_edges(
extracted_e1, continuity=ContinuityLevel.C2
)
self.assertEqual(len(connected_c2), 0)
# Also test e2 to e3 continuity
connected_e2_c0 = topo_explore_connected_edges(
extracted_e2, continuity=ContinuityLevel.C0
)
self.assertEqual(len(connected_e2_c0), 2) # e1 and e3 connected by vertex
connected_e2_c1 = topo_explore_connected_edges(
extracted_e2, continuity=ContinuityLevel.C1
)
# e3 should be excluded due to sharp corner
self.assertEqual(len(connected_e2_c1), 1)
self.assertTrue(connected_e2_c1[0].is_same(extracted_e1))
connected_e2_c2 = topo_explore_connected_edges(
extracted_e2, continuity=ContinuityLevel.C2
)
self.assertEqual(len(connected_e2_c2), 0)
def test_topo_explore_connected_edges_continuity_loop(self):
# Perfect circle: all edges G2 continuous at their junctions
circle = Edge.make_circle(1)
edges = ShapeList([circle.edge().trim(0, 0.5), circle.edge().trim(0.5, 1.0)])
circle = Face(Wire(edges))
edges = circle.edges()
for e in edges:
connected_c2 = topo_explore_connected_edges(
e, parent=circle, continuity=ContinuityLevel.C2
)
self.assertEqual(len(connected_c2), 1)
connected_c1 = topo_explore_connected_edges(
e, parent=circle, continuity=ContinuityLevel.C1
)
self.assertEqual(len(connected_c1), 1)
connected_c0 = topo_explore_connected_edges(
e, parent=circle, continuity=ContinuityLevel.C0
)
self.assertEqual(len(connected_c0), 1)
def test_topo_explore_common_vertex(self):
triangle = Face(
Wire(
[
Edge.make_line((0, 0), (4, 0)),
Edge.make_line((4, 0), (0, 3)),
Edge.make_line((0, 3), (0, 0)),
]
)
)
hypotenuse = triangle.edges().sort_by(SortBy.LENGTH)[-1]
base = triangle.edges().sort_by(Axis.Y)[0]
common_vertex = topo_explore_common_vertex(hypotenuse, base)
self.assertIsNotNone(common_vertex)
self.assertVectorAlmostEquals(Vector(common_vertex), (4, 0, 0), 5)
self.assertIsNone(
topo_explore_common_vertex(hypotenuse, Edge.make_line((0, 0), (4, 0)))
)
class TestOffsetTopodsFace(unittest.TestCase):
def setUp(self):
# Create a simple planar face for testing
self.face = Face.make_rect(1, 1).wrapped
def get_face_center(self, face: TopoDS_Face) -> tuple:
"""Calculate the center of a face"""
props = GProp_GProps()
BRepGProp.SurfaceProperties_s(face, props)
center = props.CentreOfMass()
return (center.X(), center.Y(), center.Z())
def test_offset_topods_face(self):
# Offset the face by a positive amount
offset_amount = 1.0
original_center = self.get_face_center(self.face)
offset_shape = offset_topods_face(self.face, offset_amount)
offset_center = self.get_face_center(offset_shape)
self.assertIsInstance(offset_shape, TopoDS_Shape)
self.assertAlmostEqual(Vector(0, 0, 1), offset_center)
# Offset the face by a negative amount
offset_amount = -1.0
offset_shape = offset_topods_face(self.face, offset_amount)
offset_center = self.get_face_center(offset_shape)
self.assertIsInstance(offset_shape, TopoDS_Shape)
self.assertAlmostEqual(Vector(0, 0, -1), offset_center)
def test_offset_topods_face_zero(self):
# Offset the face by zero amount
offset_amount = 0.0
original_center = self.get_face_center(self.face)
offset_shape = offset_topods_face(self.face, offset_amount)
offset_center = self.get_face_center(offset_shape)
self.assertIsInstance(offset_shape, TopoDS_Shape)
self.assertAlmostEqual(Vector(original_center), offset_center)
class TestTopoExploreConnectedFaces(unittest.TestCase):
def setUp(self):
# Create a shell with 4 faces
walls = Shell.extrude(Wire.make_rect(1, 1), (0, 0, 1))
diagonal = Axis((0, 0, 0), (1, 1, 0))
# Extract the edge that is connected to two faces
self.connected_edge = walls.edges().filter_by(Axis.Z).sort_by(diagonal)[-1]
# Create an edge that is only connected to one face
self.unconnected_edge = Face.make_rect(1, 1).edges()[0]
def test_topo_explore_connected_faces(self):
# Add the edge to the faces
faces = topo_explore_connected_faces(self.connected_edge)
self.assertEqual(len(faces), 2)
def test_topo_explore_connected_faces_invalid(self):
# No parent case
with self.assertRaises(ValueError):
topo_explore_connected_faces(Edge())
if __name__ == "__main__":
unittest.main()