Skip to content

Commit 0986754

Browse files
theoryshawclaude
andcommitted
Guard stale representation item ids in the items panel
BIMObjectGeometryProperties.items is stored in the .blend, so its ifc_definition_ids can outlive the instances they point at - reopening a .blend next to its .ifc replays a list built against a different state of the file. Every operator behind the Representation Items panel then called by_id() on a dead id and raised "RuntimeError: Instance #N not found". Route the eight unguarded call sites through a resolve_representation_item() helper built on tool.Ifc.get_entity_by_id. On a dead id it reports the error and rebuilds the list with the disable/enable pair these operators already use to reload the items UI, so the panel heals itself and the retry works. UnassignRepresentationItemStyle already reported instead of raising; it now refreshes the list too. This guards the point of use only. Per-object item editing state still survives an IFC load, which is the underlying condition. Refs IfcOpenShell#9447 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c5b85c7 commit 0986754

1 file changed

Lines changed: 39 additions & 14 deletions

File tree

‎src/bonsai/bonsai/bim/module/geometry/operator.py‎

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3463,6 +3463,27 @@ def _execute(self, context):
34633463
props.is_editing = False
34643464

34653465

3466+
def resolve_representation_item(
3467+
operator: bpy.types.Operator, item_id: int
3468+
) -> Union[ifcopenshell.entity_instance, None]:
3469+
"""Resolve an item id from the representation items UI list.
3470+
3471+
``BIMObjectGeometryProperties.items`` is stored in the .blend, so its ids can
3472+
outlive the instances they point to - reopening a .blend next to its .ifc
3473+
replays a list that was built against a different state of the file. Refresh
3474+
the list and report instead of letting ``by_id`` raise
3475+
``RuntimeError: Instance #N not found``.
3476+
"""
3477+
if item := tool.Ifc.get_entity_by_id(item_id):
3478+
return item
3479+
operator.report(
3480+
{"ERROR"}, f"Representation item #{item_id} no longer exists, items list has been refreshed - try again."
3481+
)
3482+
bpy.ops.bim.disable_editing_representation_items()
3483+
bpy.ops.bim.enable_editing_representation_items()
3484+
return None
3485+
3486+
34663487
class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
34673488
bl_idname = "bim.remove_representation_item"
34683489
bl_label = "Remove Representation Item"
@@ -3486,9 +3507,9 @@ def poll(cls, context):
34863507
def _execute(self, context):
34873508
assert (obj := tool.Geometry.get_active_or_representation_obj())
34883509
assert (element := tool.Ifc.get_entity(obj))
3489-
ifc_file = tool.Ifc.get()
34903510

3491-
representation_item = ifc_file.by_id(self.representation_item_id)
3511+
if not (representation_item := resolve_representation_item(self, self.representation_item_id)):
3512+
return {"CANCELLED"}
34923513
tool.Geometry.remove_representation_item(representation_item, element)
34933514
tool.Geometry.reload_representation(obj)
34943515

@@ -3514,7 +3535,8 @@ def execute(self, context):
35143535
obj = tool.Geometry.get_active_or_representation_obj()
35153536
obj_props = tool.Geometry.get_object_geometry_props(obj)
35163537
assert obj_props.active_item
3517-
item = tool.Ifc.get().by_id(obj_props.active_item.ifc_definition_id)
3538+
if not (item := resolve_representation_item(self, obj_props.active_item.ifc_definition_id)):
3539+
return {"CANCELLED"}
35183540
item_ids = self.get_nested_item_ids(item)
35193541

35203542
props = tool.Geometry.get_geometry_props()
@@ -3577,13 +3599,13 @@ def _execute(self, context):
35773599
obj = tool.Geometry.get_active_or_representation_obj()
35783600
assert obj
35793601
props = tool.Geometry.get_object_geometry_props(obj)
3580-
props.is_editing_item_style = True
3581-
ifc_file = tool.Ifc.get()
35823602

35833603
# set dropdown to currently active style
35843604
assert (active_item := props.active_item)
35853605
representation_item_id = active_item.ifc_definition_id
3586-
representation_item = ifc_file.by_id(representation_item_id)
3606+
if not (representation_item := resolve_representation_item(self, representation_item_id)):
3607+
return {"CANCELLED"}
3608+
props.is_editing_item_style = True
35873609
style = tool.Style.get_representation_item_style(representation_item)
35883610
if style:
35893611
props.representation_item_style = str(style.id())
@@ -3609,7 +3631,8 @@ def _execute(self, context):
36093631

36103632
assert (active_item := props.active_item)
36113633
representation_item_id = active_item.ifc_definition_id
3612-
representation_item = ifc_file.by_id(representation_item_id)
3634+
if not (representation_item := resolve_representation_item(self, representation_item_id)):
3635+
return {"CANCELLED"}
36133636

36143637
tool.Style.assign_style_to_representation_item(representation_item, surface_style)
36153638
tool.Geometry.reload_representation(obj)
@@ -3647,9 +3670,7 @@ def _execute(self, context):
36473670

36483671
# Get active representation item
36493672
active_representation_item_id = active_props.active_item.ifc_definition_id
3650-
active_representation_item = tool.Ifc.get_entity_by_id(active_representation_item_id)
3651-
if not active_representation_item:
3652-
self.report({"ERROR"}, f"Couldn't find representation item by id {active_representation_item_id}.")
3673+
if not (active_representation_item := resolve_representation_item(self, active_representation_item_id)):
36533674
return {"CANCELLED"}
36543675

36553676
# Retrieve styles applied to the active representation item
@@ -3732,7 +3753,8 @@ def _execute(self, context):
37323753

37333754
assert props.active_item
37343755
representation_item_id = props.active_item.ifc_definition_id
3735-
representation_item = ifc_file.by_id(representation_item_id)
3756+
if not (representation_item := resolve_representation_item(self, representation_item_id)):
3757+
return {"CANCELLED"}
37363758

37373759
if props.representation_item_shape_aspect == "NEW":
37383760
active_representation = tool.Geometry.get_active_representation(obj)
@@ -3803,7 +3825,8 @@ def _execute(self, context):
38033825

38043826
assert props.active_item
38053827
representation_item_id = props.active_item.ifc_definition_id
3806-
representation_item = ifc_file.by_id(representation_item_id)
3828+
if not (representation_item := resolve_representation_item(self, representation_item_id)):
3829+
return {"CANCELLED"}
38073830
shape_aspect = ifc_file.by_id(props.active_item.shape_aspect_id)
38083831

38093832
# unassign items before removing items as removing items
@@ -4406,7 +4429,8 @@ def _execute(self, context):
44064429
new_layer = ifc_file.by_id(int(props.representation_item_layer))
44074430

44084431
assert props.active_item
4409-
item = ifc_file.by_id(int(props.active_item.ifc_definition_id))
4432+
if not (item := resolve_representation_item(self, int(props.active_item.ifc_definition_id))):
4433+
return {"CANCELLED"}
44104434
item_layer = next(iter(item.LayerAssignment), None)
44114435

44124436
# We assume just 1 layer can be assigned to representation item.
@@ -4434,7 +4458,8 @@ def _execute(self, context):
44344458
obj = context.active_object
44354459
assert obj
44364460
props = tool.Geometry.get_object_geometry_props(obj)
4437-
item = ifc_file.by_id(props.active_item.ifc_definition_id)
4461+
if not (item := resolve_representation_item(self, props.active_item.ifc_definition_id)):
4462+
return {"CANCELLED"}
44384463
layer = item.LayerAssignment[0] # If there is no layer, then button is not visible in UI.
44394464
ifcopenshell.api.layer.unassign_layer(ifc_file, [item], layer)
44404465
bpy.ops.bim.enable_editing_representation_items()

0 commit comments

Comments
 (0)