From aede574d0ee4d5b1bf79665427a5fe959a54588f Mon Sep 17 00:00:00 2001 From: CyrilWaechter Date: Sun, 2 Aug 2026 06:48:36 +0200 Subject: [PATCH] ifcopenshell.util.boundary: make face matrix robust against collinear first vertices _face_matrix_from_verts used only the first 3 vertices and sb.np_normal, which divides by zero when they are collinear. Triangulated meshes from generated spaces often start with collinear boundary vertices, producing NaN matrices and a shapely LinearRing error. Walk the polygon to find a non-degenerate normal and edge. Generated with the assistance of an AI coding tool. --- .../ifcopenshell/util/boundary.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/boundary.py b/src/ifcopenshell-python/ifcopenshell/util/boundary.py index 03348a4e66..d2a2f3a1ac 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/boundary.py +++ b/src/ifcopenshell-python/ifcopenshell/util/boundary.py @@ -261,7 +261,7 @@ def auto_generate_boundaries( ) space_face_normal_world = space_matrix_3x3 @ space_face_normal_local - face_matrix = _face_matrix_from_verts(space_verts_l[:3]) + face_matrix = _face_matrix_from_verts(space_verts_l) face_matrix_inv = np.linalg.inv(face_matrix) space_face_polygon = _verts_to_polygon(space_verts_l, face_matrix_inv, snap=1e-6) if not space_face_polygon.is_valid: @@ -870,11 +870,29 @@ def _face_normal(verts: np.ndarray) -> Optional[np.ndarray]: def _face_matrix_from_verts(verts3: np.ndarray) -> np.ndarray: - """Build a 4x4 face-local coordinate matrix from 3 vertices.""" - p1, p2, p3 = verts3[0], verts3[1], verts3[2] - z = sb.np_normal([p1, p2, p3]) - x = sb.np_normalized(p2 - p1) - return ifcopenshell.util.placement.a2p(o=p1, z=z, x=x) + """Build a 4x4 face-local coordinate matrix from a polygon's vertices. + + The first three vertices may be collinear in triangulated meshes, so the + normal is computed from the first non-degenerate triple and the X axis is + taken from the first non-degenerate edge. + """ + p1 = np.asarray(verts3[0]) + + normal = _face_normal(verts3) + if normal is None: + raise ValueError("Cannot build face matrix from a degenerate polygon") + + x = np.zeros(3) + for i in range(len(verts3) - 1): + edge = np.asarray(verts3[i + 1]) - np.asarray(verts3[i]) + edge_norm = np.linalg.norm(edge) + if edge_norm > 1e-8: + x = edge / edge_norm + break + if np.linalg.norm(x) < 1e-8: + raise ValueError("Cannot find a non-degenerate edge for the face matrix") + + return ifcopenshell.util.placement.a2p(o=p1, z=normal, x=x) def _verts_to_polygon(verts: np.ndarray, face_matrix_inv: np.ndarray, snap: float = 0) -> shapely.Polygon: