Added Edge.is_interior property Issue #816

This commit is contained in:
gumyr 2025-01-24 11:08:57 -05:00
parent 0625c77e4e
commit 4aee76f6c0
4 changed files with 179 additions and 4 deletions

View file

@ -51,8 +51,16 @@ from .utils import (
find_max_dimension,
)
from .zero_d import Vertex, topo_explore_common_vertex
from .one_d import Edge, Wire, Mixin1D, edges_to_wires, topo_explore_connected_edges
from .two_d import Face, Shell, Mixin2D,sort_wires_by_build_order
from .one_d import (
Edge,
Wire,
Mixin1D,
edges_to_wires,
topo_explore_connected_edges,
offset_topods_face,
topo_explore_connected_faces,
)
from .two_d import Face, Shell, Mixin2D, sort_wires_by_build_order
from .three_d import Solid, Mixin3D
from .composite import Compound, Curve, Sketch, Part
@ -79,7 +87,9 @@ __all__ = [
"Edge",
"Wire",
"edges_to_wires",
"offset_topods_face",
"topo_explore_connected_edges",
"topo_explore_connected_faces",
"Face",
"Shell",
"sort_wires_by_build_order",

View file

@ -66,7 +66,11 @@ from scipy.spatial import ConvexHull
import OCP.TopAbs as ta
from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_CompCurve, BRepAdaptor_Curve
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Splitter
from OCP.BRepAlgoAPI import (
BRepAlgoAPI_Common,
BRepAlgoAPI_Section,
BRepAlgoAPI_Splitter,
)
from OCP.BRepBuilderAPI import (
BRepBuilderAPI_DisconnectedWire,
BRepBuilderAPI_EmptyWire,
@ -80,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.BRepOffset import BRepOffset_MakeOffset
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeOffset
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
from OCP.BRepProj import BRepProj_Projection
@ -134,6 +139,7 @@ from OCP.TopoDS import (
TopoDS,
TopoDS_Compound,
TopoDS_Edge,
TopoDS_Face,
TopoDS_Shape,
TopoDS_Shell,
TopoDS_Wire,
@ -223,6 +229,34 @@ class Mixin1D(Shape):
raise ValueError("Can't determine direction of empty Edge or Wire")
return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD
@property
def is_interior(self) -> bool:
"""
Check if the edge is an interior edge.
An interior edge lies between surfaces that are part of the body (internal
to the geometry) and does not form part of the exterior boundary.
Returns:
bool: True if the edge is an interior edge, False otherwise.
"""
# Find the faces connected to this edge and offset them
topods_face_pair = topo_explore_connected_faces(self)
offset_face_pair = [
offset_topods_face(f, self.length / 100) for f in topods_face_pair
]
# Intersect the offset faces
sectionor = BRepAlgoAPI_Section(
offset_face_pair[0], offset_face_pair[1], PerformNow=False
)
sectionor.Build()
face_intersection_result = sectionor.Shape()
# If an edge was created the faces intersect and the edge is interior
explorer = TopExp_Explorer(face_intersection_result, ta.TopAbs_EDGE)
return explorer.More()
@property
def length(self) -> float:
"""Edge or Wire length"""
@ -3004,6 +3038,15 @@ def edges_to_wires(edges: Iterable[Edge], tol: float = 1e-6) -> ShapeList[Wire]:
return wires
def offset_topods_face(face: TopoDS_Face, amount: float) -> TopoDS_Shape:
"""Offset a topods_face"""
offsetor = BRepOffset_MakeOffset()
offsetor.Initialize(face, Offset=amount, Tol=TOLERANCE)
offsetor.MakeOffsetShape()
return offsetor.Shape()
def topo_explore_connected_edges(
edge: Edge, parent: Shape | None = None
) -> ShapeList[Edge]:
@ -3029,3 +3072,31 @@ def topo_explore_connected_edges(
connected_edges.add(topods_edge)
return ShapeList(Edge(e) for e in connected_edges)
def topo_explore_connected_faces(
edge: Edge, parent: Shape | None = None
) -> list[TopoDS_Face]:
"""Given an edge extracted from a Shape, return the topods_faces connected to it"""
parent = parent if parent is not None else edge.topo_parent
if parent is None:
raise ValueError("edge has no valid parent")
# make a edge --> faces mapping
edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape()
TopExp.MapShapesAndAncestors_s(
parent.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map
)
# Query the map
faces = []
if edge_face_map.Contains(edge.wrapped):
face_list = edge_face_map.FindFromKey(edge.wrapped)
for face in face_list:
faces.append(TopoDS.Face_s(face))
if len(faces) != 2:
raise RuntimeError("Invalid # of faces connected to this edge")
return faces

View file

@ -29,9 +29,11 @@ license:
import math
import unittest
from build123d.build_enums import AngularDirection
from build123d.build_enums import AngularDirection, GeomType, Transition
from build123d.geometry import Axis, Plane, Vector
from build123d.objects_curve import CenterArc, EllipticalCenterArc
from build123d.objects_sketch import Circle, Rectangle, RegularPolygon
from build123d.operations_generic import sweep
from build123d.topology import Edge
@ -284,6 +286,14 @@ class TestEdge(unittest.TestCase):
with self.assertRaises(TypeError):
Edge(direction=(1, 0, 0))
def test_is_interior(self):
path = RegularPolygon(5, 5).face().outer_wire()
profile = path.location_at(0) * (Circle(0.6) & Rectangle(2, 1))
target = sweep(profile, path, transition=Transition.RIGHT)
inside_edges = target.edges().filter_by(lambda e: e.is_interior)
self.assertEqual(len(inside_edges), 5)
self.assertTrue(all(e.geom_type == GeomType.ELLIPSE for e in inside_edges))
if __name__ == "__main__":
unittest.main()

View file

@ -1,6 +1,11 @@
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 SortBy
from build123d.objects_part import Box
@ -12,8 +17,11 @@ from build123d.geometry import (
from build123d.topology import (
Edge,
Face,
Shell,
Wire,
offset_topods_face,
topo_explore_connected_edges,
topo_explore_connected_faces,
topo_explore_common_vertex,
)
@ -78,6 +86,17 @@ class TestTopoExplore(DirectApiTestCase):
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_common_vertex(self):
triangle = Face(
Wire(
@ -98,5 +117,70 @@ class TestTopoExplore(DirectApiTestCase):
)
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):
# Test with an edge that is not connected to two faces
with self.assertRaises(RuntimeError):
topo_explore_connected_faces(self.unconnected_edge)
# No parent case
with self.assertRaises(ValueError):
topo_explore_connected_faces(Edge())
if __name__ == "__main__":
unittest.main()