Skip to content

Commit

Permalink
Added Edge.is_interior property Issue #816
Browse files Browse the repository at this point in the history
  • Loading branch information
gumyr committed Jan 24, 2025
1 parent 0625c77 commit 4aee76f
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 4 deletions.
14 changes: 12 additions & 2 deletions src/build123d/topology/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,16 @@
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

Expand All @@ -79,7 +87,9 @@
"Edge",
"Wire",
"edges_to_wires",
"offset_topods_face",
"topo_explore_connected_edges",
"topo_explore_connected_faces",
"Face",
"Shell",
"sort_wires_by_build_order",
Expand Down
73 changes: 72 additions & 1 deletion src/build123d/topology/one_d.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@
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,
Expand All @@ -80,6 +84,7 @@
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
Expand Down Expand Up @@ -134,6 +139,7 @@
TopoDS,
TopoDS_Compound,
TopoDS_Edge,
TopoDS_Face,
TopoDS_Shape,
TopoDS_Shell,
TopoDS_Wire,
Expand Down Expand Up @@ -223,6 +229,34 @@ def is_forward(self) -> bool:
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"""
Expand Down Expand Up @@ -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]:
Expand All @@ -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
12 changes: 11 additions & 1 deletion tests/test_direct_api/test_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
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


Expand Down Expand Up @@ -284,6 +286,14 @@ def test_init(self):
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()
84 changes: 84 additions & 0 deletions tests/test_topo_explore.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,8 +17,11 @@
from build123d.topology import (
Edge,
Face,
Shell,
Wire,
offset_topods_face,
topo_explore_connected_edges,
topo_explore_connected_faces,
topo_explore_common_vertex,
)

Expand Down Expand Up @@ -78,6 +86,17 @@ def test_topo_explore_connected_edges(self):
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(
Expand All @@ -98,5 +117,70 @@ def test_topo_explore_common_vertex(self):
)


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()

0 comments on commit 4aee76f

Please sign in to comment.