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
+13
View File
@@ -183,3 +183,16 @@ class TestNpFrombufferLegacy(NewFile):
result = subject.np_frombuffer_legacy(data, n)
assert result.shape == (n,)
np.testing.assert_allclose(result, np.arange(n))
class TestGetObjectFromGuidMissing(NewFile):
"""``get_object_from_guid`` must honour its ``Optional[Object]`` return
contract: a GUID that does not resolve in the current IFC file yields
``None``, not a ``RuntimeError``. Callers iterate stored GUID lists
(array children, library refs, …) and rely on the falsy return to
skip stale entries."""
def test_returns_none_when_guid_not_in_file(self):
bpy.ops.bim.create_project()
assert tool.Ifc.get() is not None
assert subject.get_object_from_guid("3iyt7r$Hf4_hQYNhBIDJI4") is None
+24
View File
@@ -672,6 +672,30 @@ class TestUsingArrays(NewFile):
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
assert pset is None, (obj, pset)
def test_remove_array_tolerates_stale_child_guid(self):
"""``bim.remove_array`` and the underlying ``regenerate_array`` must
survive a child GUID in ``BBIM_Array.Data`` that no longer resolves
in the file. Real-world IFC files can carry dangling array refs
from external edits — the remove path is meant to delete those
children, so an already-missing entity is the desired terminal
state, not a fatal error."""
self.setup_array()
parent_obj = bpy.context.active_object
parent_element = tool.Ifc.get_entity(parent_obj)
ifc_file = tool.Ifc.get()
pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array")
data = json.loads(pset["Data"])
data[0]["children"].append("3iyt7r$Hf4_hQYNhBIDJI4")
ifcopenshell.api.pset.edit_pset(
ifc_file,
pset=ifc_file.by_id(pset["id"]),
properties={"Data": json.dumps(data)},
)
bpy.ops.bim.remove_array(item=0)
assert ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array") is None
class TestApplyIfcMaterialChanges(NewFile):
def get_used_styles(self, obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: