Tolerate stale array child/parent GUIDs (#8177)

* Tolerate stale array child/parent GUIDs

A real-world IFC project (an arrayed door whose host got deleted
externally) crashed Bonsai's project load with "Instance with
GlobalId not found" inside setup_arrays.

tool.Blender.get_object_from_guid declared Optional return but let
RuntimeError propagate; callers iterating BBIM_Array child lists then
crashed instead of skipping. Honour the documented contract by
returning None on miss, matching the convention used by every other
by_guid lookup helper in tool/array.py, tool/ifc.py, tool/geometry.py.

Sweep the four user-action sites that resolve array child/parent
GUIDs without a guard - they shared the same bug class but were
reachable from different operators (regenerate_array, RegenerateArray
clear, duplicate_ifc_objects, process_arrays). An already-missing
entity is the desired terminal state for each, so the fix is
try/except RuntimeError: continue/skip.

setup_arrays now also collects each parent with at least one stale
child GUID into IfcImporter.broken_arrays, surfaced via a new Project
panel banner mirroring the existing pending_opening_recut UX. The
banner reports the count and offers "Select Elements" to navigate to
the affected array parents and a Dismiss button.

constrain_children_to_parent was being called once per layer inside
setup_arrays' for loop even though it always iterates all layers
internally - lifted out of the loop (pre-existing N x perf bug
that the stale-GUID print exposed).

Regression tests:
- test_returns_none_when_guid_not_in_file pins the get_object_from_guid
  Optional contract.
- test_remove_array_tolerates_stale_child_guid injects a fake child
  GUID into BBIM_Array.Data and asserts bim.remove_array completes
  cleanly.

Generated with the assistance of an AI coding tool.

* Black: wrap long bl_description in dismiss_pending_array_repair

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-16 10:33:56 +02:00
committed by GitHub
parent 074021de70
commit 5f1efeffaf
12 changed files with 152 additions and 6 deletions
@@ -1236,6 +1236,17 @@ class LoadProjectElements(bpy.types.Operator):
f"Apply manually from the Project panel.",
)
props.pending_array_repair.clear()
if ifc_importer.broken_arrays:
for element in ifc_importer.broken_arrays:
item = props.pending_array_repair.add()
item.ifc_definition_id = element.id()
self.report(
{"WARNING"},
f"{len(ifc_importer.broken_arrays)} array parent(s) reference missing child GUIDs. "
f"Inspect from the Project panel.",
)
tool.Project.load_default_thumbnails()
tool.Project.set_default_context()
tool.Project.set_default_modeling_dimensions()
@@ -3539,3 +3550,44 @@ class BIM_OT_select_pending_opening_cuts(bpy.types.Operator):
tool.Blender.set_objects_selection(context, active_object=objects[0], selected_objects=objects)
self.report({"INFO"}, f"Selected {len(objects)} element(s).")
return {"FINISHED"}
class BIM_OT_select_pending_array_repair(bpy.types.Operator):
bl_idname = "bim.select_pending_array_repair"
bl_label = "Select Array Parents With Missing Children"
bl_description = "Select the Blender objects of array parents whose BBIM_Array.Data references children that don't resolve in the file."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context: bpy.types.Context) -> set[str]:
ifc_file = tool.Ifc.get()
if ifc_file is None:
self.report({"INFO"}, "No IFC file loaded.")
return {"CANCELLED"}
objects: list[bpy.types.Object] = []
for item in tool.Project.get_project_props().pending_array_repair:
try:
element = ifc_file.by_id(item.ifc_definition_id)
except RuntimeError:
continue
obj = tool.Ifc.get_object(element)
if obj is not None:
objects.append(obj)
if not objects:
self.report({"INFO"}, "No matching Blender objects found for the pending list.")
return {"CANCELLED"}
tool.Blender.set_objects_selection(context, active_object=objects[0], selected_objects=objects)
self.report({"INFO"}, f"Selected {len(objects)} array parent(s).")
return {"FINISHED"}
class BIM_OT_dismiss_pending_array_repair(bpy.types.Operator):
bl_idname = "bim.dismiss_pending_array_repair"
bl_label = "Dismiss Pending Array Repair"
bl_description = (
"Clear the pending array-repair list without acting on it. The underlying BBIM_Array.Data stays unchanged."
)
bl_options = {"REGISTER", "UNDO"}
def execute(self, context: bpy.types.Context) -> set[str]:
tool.Project.get_project_props().pending_array_repair.clear()
return {"FINISHED"}