mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
util.shape.is_manifold: single-pass edge-orientation check per aothms's review
Replaced the two-dict (directed/undirected use count) implementation with a single dict tracking each undirected edge's orientation state, matching aothms's suggested implementation on PR #8503: fewer lookups, less memory, and returns False immediately on a definite defect (a degenerate zero-length edge, or an edge reused with the same or a third winding) instead of always scanning every face first. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -71,29 +71,32 @@ def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
|
|||||||
def is_manifold(geometry: W.Triangulation) -> bool:
|
def is_manifold(geometry: W.Triangulation) -> bool:
|
||||||
"""Checks whether a triangulated geometry is a closed, consistently oriented manifold
|
"""Checks whether a triangulated geometry is a closed, consistently oriented manifold
|
||||||
|
|
||||||
Two conditions are checked for every edge of every triangle:
|
Every edge, as an unordered pair of vertices, must be shared by exactly
|
||||||
|
two triangles, and those two triangles must traverse it in opposite
|
||||||
- Unoriented use: as an unordered pair of vertices, an edge must be shared
|
directions. A single use means an open hole or boundary; a third use, or
|
||||||
by exactly two triangles. A count of 1 means an open hole or boundary,
|
two uses in the same direction, means the winding is inconsistent (e.g. a
|
||||||
a count above 2 means more than two triangles meet at that edge.
|
flipped or duplicated face) - both invalidate volume calculations that
|
||||||
- Oriented use: as an ordered pair of vertices, an edge must be used by at
|
rely on a closed, consistently wound mesh.
|
||||||
most one triangle. If two triangles use the same ordered edge, their
|
|
||||||
windings are inconsistent (e.g. a flipped or duplicated face), which
|
|
||||||
also invalidates volume calculations that rely on consistent winding.
|
|
||||||
|
|
||||||
:param geometry: Geometry output calculated by IfcOpenShell
|
:param geometry: Geometry output calculated by IfcOpenShell
|
||||||
:return: ``True`` if the geometry is a closed, consistently oriented manifold
|
:return: ``True`` if the geometry is a closed, consistently oriented manifold
|
||||||
"""
|
"""
|
||||||
faces = geometry.faces
|
faces = geometry.faces
|
||||||
directed_use: dict[tuple[int, int], int] = {}
|
edge_state: dict[tuple[int, int], int] = {}
|
||||||
undirected_use: dict[tuple[int, int], int] = {}
|
|
||||||
for i in range(0, len(faces), 3):
|
for i in range(0, len(faces), 3):
|
||||||
tri = (faces[i], faces[i + 1], faces[i + 2])
|
a, b, c = faces[i], faces[i + 1], faces[i + 2]
|
||||||
for a, b in ((tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])):
|
for u, v in ((a, b), (b, c), (c, a)):
|
||||||
directed_use[(a, b)] = directed_use.get((a, b), 0) + 1
|
if u == v:
|
||||||
edge = (a, b) if a < b else (b, a)
|
return False
|
||||||
undirected_use[edge] = undirected_use.get(edge, 0) + 1
|
edge, direction = ((u, v), 1) if u < v else ((v, u), -1)
|
||||||
return all(count == 2 for count in undirected_use.values()) and all(count == 1 for count in directed_use.values())
|
state = edge_state.get(edge)
|
||||||
|
if state is None:
|
||||||
|
edge_state[edge] = direction
|
||||||
|
elif state == -direction:
|
||||||
|
edge_state[edge] = 0
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return all(state == 0 for state in edge_state.values())
|
||||||
|
|
||||||
|
|
||||||
def get_volume(geometry: W.Triangulation) -> float:
|
def get_volume(geometry: W.Triangulation) -> float:
|
||||||
|
|||||||
Reference in New Issue
Block a user