Bonsai: pick the true sweep edge in detect_extrusion_edge (#5898)

When re-deriving an IfcExtrudedAreaSolid (e.g. converting slabs to
IfcArbitraryProfileDefWithVoids), detect_extrusion_edge returned the first
mesh edge that leaves the profile plane by more than 1e-6. On slanted or
stepped meshes a nearly in-plane boundary edge can clear that threshold
first, so the sweep direction ended up lying in the profile plane and the
extrusion collapsed to a zero-thickness sliver (12 of 69 slabs in the
report went from ~2500 volume to ~0).

Scan all candidate edges and return the one whose direction is most
aligned with the profile-face normal, i.e. the genuine through-thickness
sweep edge, keeping the same [in_profile_vertex, off_plane_vertex] return
orientation. Benefits all auto-detect callers.

Verified in headless Blender 5.1 / Bonsai 0.8.6 against the reported
sample: after the fix all 69 slabs match their Brep ground truth to 5 to
6 significant figures (worst diff 0.01%), the previously degenerate 12 are
restored, and the 57 already-correct slabs are volume-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Petru Conduraru
2026-07-11 08:12:54 +03:00
parent ade03b171a
commit f4dba8bd38
@@ -316,10 +316,18 @@ class Helper:
return {"outer_curve": outer_loop, "inner_curves": inner_loops}
# An extrusion edge is an edge that shares a single vertex with a profile
# face and is not on the plane of the face.
# face and is not on the plane of the face. Multiple such edges may exist
# (e.g. a slanted/stepped mesh has boundary edges that leave the profile
# plane only marginally); the genuine sweep edge is the one whose direction
# is most aligned with the profile normal. Picking the first candidate in
# arbitrary bmesh order could select a nearly in-plane edge, yielding a
# degenerate (zero-thickness) extrusion. So we pick the best-aligned edge.
def detect_extrusion_edge(self, bm: bmesh.types.BMesh, profile_face: bmesh.types.BMFace) -> Union[list[int], None]:
bm.edges.ensure_lookup_table()
face_verts_set = set(profile_face.verts)
normal = profile_face.normal
best_edge = None
best_alignment = 0.0
for edge in bm.edges:
unshared_verts = set(edge.verts) - face_verts_set
if len(unshared_verts) == 1:
@@ -327,14 +335,24 @@ class Helper:
if (
abs(
mathutils.geometry.distance_point_to_plane(
unshared_vert.co, profile_face.verts[0].co, profile_face.normal
unshared_vert.co, profile_face.verts[0].co, normal
)
)
> 1e-6
):
if unshared_vert == edge.verts[1]:
return [edge.verts[0].index, edge.verts[1].index]
return [edge.verts[1].index, edge.verts[0].index]
direction = edge.verts[1].co - edge.verts[0].co
length = direction.length
if length < 1e-9:
continue
# 1.0 == parallel to the profile normal (a true sweep edge).
alignment = abs(direction.dot(normal)) / length
if alignment > best_alignment:
best_alignment = alignment
if unshared_vert == edge.verts[1]:
best_edge = [edge.verts[0].index, edge.verts[1].index]
else:
best_edge = [edge.verts[1].index, edge.verts[0].index]
return best_edge
def create_extruded_area_solid(
self, mesh: bpy.types.Mesh, extrusion_indices: list[int], profile_def: dict[str, Any]