From 62257e0aff0d8d89e9f20cf976ae09de023e9064 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sat, 18 Jul 2026 23:01:59 +0300 Subject: [PATCH] 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. --- .../ifcopenshell/util/shape.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index 753d1b424f..a82bfe97e5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -71,29 +71,32 @@ def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool: def is_manifold(geometry: W.Triangulation) -> bool: """Checks whether a triangulated geometry is a closed, consistently oriented manifold - Two conditions are checked for every edge of every triangle: - - - Unoriented use: as an unordered pair of vertices, an edge must be shared - by exactly two triangles. A count of 1 means an open hole or boundary, - a count above 2 means more than two triangles meet at that edge. - - Oriented use: as an ordered pair of vertices, an edge must be used by at - 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. + Every edge, as an unordered pair of vertices, must be shared by exactly + two triangles, and those two triangles must traverse it in opposite + directions. A single use means an open hole or boundary; a third use, or + two uses in the same direction, means the winding is inconsistent (e.g. a + flipped or duplicated face) - both invalidate volume calculations that + rely on a closed, consistently wound mesh. :param geometry: Geometry output calculated by IfcOpenShell :return: ``True`` if the geometry is a closed, consistently oriented manifold """ faces = geometry.faces - directed_use: dict[tuple[int, int], int] = {} - undirected_use: dict[tuple[int, int], int] = {} + edge_state: dict[tuple[int, int], int] = {} for i in range(0, len(faces), 3): - tri = (faces[i], faces[i + 1], faces[i + 2]) - for a, b in ((tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])): - directed_use[(a, b)] = directed_use.get((a, b), 0) + 1 - edge = (a, b) if a < b else (b, a) - undirected_use[edge] = undirected_use.get(edge, 0) + 1 - return all(count == 2 for count in undirected_use.values()) and all(count == 1 for count in directed_use.values()) + a, b, c = faces[i], faces[i + 1], faces[i + 2] + for u, v in ((a, b), (b, c), (c, a)): + if u == v: + return False + edge, direction = ((u, v), 1) if u < v else ((v, u), -1) + 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: