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:
Petru Conduraru
2026-07-18 23:01:59 +03:00
parent de40b5f685
commit 62257e0aff
@@ -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: