From 4795bf79ffff4ed7fdfdd1d7fd120e7447782f6b Mon Sep 17 00:00:00 2001 From: gumyr Date: Wed, 16 Jul 2025 11:41:55 -0400 Subject: [PATCH] Added continuity to topo_explore_connected_edges --- src/build123d/build_enums.py | 24 ++++++++- src/build123d/topology/one_d.py | 47 ++++++++++++++++-- tests/test_topo_explore.py | 88 ++++++++++++++++++++++++++++++++- 3 files changed, 153 insertions(+), 6 deletions(-) diff --git a/src/build123d/build_enums.py b/src/build123d/build_enums.py index 62babad..8cca982 100644 --- a/src/build123d/build_enums.py +++ b/src/build123d/build_enums.py @@ -28,7 +28,7 @@ license: from __future__ import annotations -from enum import Enum, auto +from enum import Enum, auto, IntEnum, unique from typing import Union from typing import TypeAlias @@ -89,6 +89,28 @@ class CenterOf(Enum): return f"<{self.__class__.__name__}.{self.name}>" +@unique +class ContinuityLevel(IntEnum): + """ + Continuity level for evaluating geometric connections. + + Used to determine how smoothly adjacent geometry joins together, + such as at shared vertices between edges or shared edges between faces. + + Levels: + + - C0 (G0): Positional continuity—elements meet at a point but may have sharp angles. + - C1 (G1): Tangent continuity—elements have the same tangent direction at the junction. + - C2 (G2): Curvature continuity—elements have matching curvature at the junction. + + These levels correspond to common CAD definitions and are compatible with OCCT's GeomAbs_Shape. + """ + + C0 = 0 + C1 = 1 + C2 = 2 + + class Extrinsic(Enum): """Order to apply extrinsic rotations by axis""" diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index e7b02e4..a17ff24 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -84,6 +84,7 @@ from OCP.BRepExtrema import BRepExtrema_DistShapeShape from OCP.BRepFilletAPI import BRepFilletAPI_MakeFillet2d from OCP.BRepGProp import BRepGProp, BRepGProp_Face from OCP.BRepLib import BRepLib, BRepLib_FindSurface +from OCP.BRepLProp import BRepLProp_CLProps, BRepLProp from OCP.BRepOffset import BRepOffset_MakeOffset from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeOffset from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace @@ -103,6 +104,7 @@ from OCP.Geom import ( ) from OCP.Geom2d import Geom2d_Curve, Geom2d_Line, Geom2d_TrimmedCurve from OCP.Geom2dAPI import Geom2dAPI_InterCurveCurve +from OCP.GeomAbs import GeomAbs_C0, GeomAbs_G1, GeomAbs_C1, GeomAbs_G2, GeomAbs_C2 from OCP.GeomAPI import ( GeomAPI_IntCS, GeomAPI_Interpolate, @@ -165,6 +167,7 @@ from OCP.gp import ( ) from build123d.build_enums import ( AngularDirection, + ContinuityLevel, CenterOf, FrameMethod, GeomType, @@ -3213,10 +3216,28 @@ def offset_topods_face(face: TopoDS_Face, amount: float) -> TopoDS_Shape: def topo_explore_connected_edges( - edge: Edge, parent: Shape | None = None + edge: Edge, + parent: Shape | None = None, + continuity: ContinuityLevel = ContinuityLevel.C0, ) -> ShapeList[Edge]: - """Given an edge extracted from a Shape, return the edges connected to it""" + """ + Find edges connected to the given edge with at least the requested continuity. + Args: + edge: The reference edge to explore from. + parent: Optional parent Shape. If None, uses edge.topo_parent. + continuity: Minimum required continuity (C0/G0, C1/G1, C2/G2). + + Returns: + ShapeList[Edge]: Connected edges meeting the continuity requirement. + """ + continuity_map = { + GeomAbs_C0: ContinuityLevel.C0, + GeomAbs_G1: ContinuityLevel.C1, + GeomAbs_C1: ContinuityLevel.C1, + GeomAbs_G2: ContinuityLevel.C2, + GeomAbs_C2: ContinuityLevel.C2, + } parent = parent if parent is not None else edge.topo_parent if parent is None: raise ValueError("edge has no valid parent") @@ -3233,8 +3254,26 @@ def topo_explore_connected_edges( if given_topods_edge.IsSame(topods_edge): continue # If the edge shares a vertex with the given edge they are connected - if topo_explore_common_vertex(given_topods_edge, topods_edge) is not None: - connected_edges.add(topods_edge) + common_topods_vertex: Vertex | None = topo_explore_common_vertex( + given_topods_edge, topods_edge + ) + if common_topods_vertex is not None: + # shared_vertex is the TopoDS_Vertex common to edge1 and edge2 + u1 = BRep_Tool.Parameter_s(common_topods_vertex.wrapped, given_topods_edge) + u2 = BRep_Tool.Parameter_s(common_topods_vertex.wrapped, topods_edge) + + # Build adaptors so OCCT can work on the curves + curve1 = BRepAdaptor_Curve(given_topods_edge) + curve2 = BRepAdaptor_Curve(topods_edge) + + # Get the GeomAbs_Shape enum continuity at the vertex + actual_continuity = BRepLProp.Continuity_s( + curve1, curve2, u1, u2, TOLERANCE, TOLERANCE + ) + actual_level = continuity_map.get(actual_continuity, ContinuityLevel.C2) + + if actual_level >= continuity: + connected_edges.add(topods_edge) return ShapeList(Edge(e) for e in connected_edges) diff --git a/tests/test_topo_explore.py b/tests/test_topo_explore.py index 5c686df..76e7e0c 100644 --- a/tests/test_topo_explore.py +++ b/tests/test_topo_explore.py @@ -6,7 +6,7 @@ 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 SortBy +from build123d.build_enums import ContinuityLevel, GeomType, SortBy from build123d.objects_part import Box from build123d.geometry import ( @@ -17,6 +17,7 @@ from build123d.geometry import ( from build123d.topology import ( Edge, Face, + ShapeList, Shell, Wire, offset_topods_face, @@ -48,6 +49,9 @@ class DirectApiTestCase(unittest.TestCase): self.assertAlmostEqual(first.Z, second_vector.Z, places, msg=msg) +from ocp_vscode import show, show_all + + class TestTopoExplore(DirectApiTestCase): def test_topo_explore_connected_edges(self): @@ -97,6 +101,88 @@ class TestTopoExplore(DirectApiTestCase): 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 + ) + show_all() + 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(