From 0a027d47a30ca81b3e3bc0250432a74118432619 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 19 Jul 2026 09:32:33 +0300 Subject: [PATCH] Bonsai: fix profile reconstruction after duplicating a circle/arc in Edit Mode Duplicating a circular or filleted-arc void in the profile CAD editor (Shift+D on the loop's vertices) reused the same IFCCIRCLE/IFCARCINDEX vertex group index for the new geometry, since Blender's mesh duplicate copies vertex group weights but does not allocate a new group. On exit from Edit Mode, auto_detect_profiles tallied group membership across the whole mesh rather than per loop, so a group meant to hold exactly 2 (circle) or 3 (arc) vertices ended up with double that, failing its sanity check and blocking the edit with an "INVALID PROFILE" popup. Fixes #6944. Scope the sanity check to each connected edge loop instead, matching how the loops are actually converted into IfcCircle/arc segments below. Also explicitly reject an arc/circle vertex tagged onto an isolated vertex with no edges at all, which the old whole-mesh count also caught. Verified live in headless Blender against the issue's repro file (IfcFurniture "Slab.004", IfcArbitraryProfileDefWithVoids with three IfcCircle voids): entering the profile editor, duplicating one void's 2-vertex loop and moving it produced an "INVALID PROFILE" popup before this change, and now produces a valid profile (the original 3 voids intact, plus the duplicate as a 4th void or a separate solid profile depending on whether it still falls inside the outer boundary). test/tool/test_model.py passes unchanged (32 passed, 1 pre-existing unrelated failure present on both before and after). Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/tool/model.py | 44 +++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index f54633c3cc..ba79840ba7 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -2292,32 +2292,18 @@ class Model(bonsai.core.tool.Model): deform_layer = bm.verts.layers.deform.active # Sanity check - group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}} if deform_layer: for vert in bm.verts: vert_group_indices = tool.Blender.bmesh_get_vertex_groups(vert, deform_layer) - is_circle = False - for group_index in vert_group_indices: - group_type = "IFCARCINDEX" if group_index in groups["IFCARCINDEX"] else "IFCCIRCLE" - group_verts[group_type].setdefault(group_index, 0) - group_verts[group_type][group_index] += 1 - if group_type == "IFCCIRCLE": - is_circle = True + is_circle = any(gi in groups["IFCCIRCLE"] for gi in vert_group_indices) + is_arc = any(gi in groups["IFCARCINDEX"] for gi in vert_group_indices) + if (is_circle or is_arc) and not vert.link_edges: + return (False, "CIRCLE" if is_circle else "3POINT_ARC") if is_circle: pass # Circles are allowed to be unclosed elif len(vert.link_edges) != 2: # Unclosed loop or forked loop return (False, "UNCLOSED_LOOP") - for group_type, group_counts in group_verts.items(): - if group_type == "IFCARCINDEX": - for group_count in group_counts.values(): - if group_count != 3: # Each arc needs 3 verts - return (False, "3POINT_ARC") - elif group_type == "IFCCIRCLE": - for group_count in group_counts.values(): - if group_count != 2: # Each circle needs 2 verts - return (False, "CIRCLE") - loop_edges = list(bm.edges) # Create loops from edges @@ -2340,6 +2326,28 @@ class Model(bonsai.core.tool.Model): has_found_connected_edge = True loops.append(loop) + # Sanity check, per loop rather than across the whole mesh + if deform_layer: + for loop in loops: + loop_group_counts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}} + loop_verts = {v for edge in loop for v in edge.verts} + for vert in loop_verts: + for group_index in tool.Blender.bmesh_get_vertex_groups(vert, deform_layer): + if group_index in groups["IFCARCINDEX"]: + group_type = "IFCARCINDEX" + elif group_index in groups["IFCCIRCLE"]: + group_type = "IFCCIRCLE" + else: + continue + loop_group_counts[group_type].setdefault(group_index, 0) + loop_group_counts[group_type][group_index] += 1 + for group_count in loop_group_counts["IFCARCINDEX"].values(): + if group_count != 3: # Each arc needs 3 verts + return (False, "3POINT_ARC") + for group_count in loop_group_counts["IFCCIRCLE"].values(): + if group_count != 2: # Each circle needs 2 verts + return (False, "CIRCLE") + tmp = ifcopenshell.file(schema=tool.Ifc.get().schema) def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool: