Backport pending-opening-cuts banner from gh8088

Extract the pending_opening_recut tracking, three operators (apply /
dismiss / select), Project-panel banner, and the sibling
multi-instance warning banner (its backend helpers already landed
on this branch) from commit a85ed6032 on gizmos-8088.

All tool.* dependencies (Geometry.reimport_element_representations,
Blender.set_objects_selection, Array.*) and IfcImporter.gross_elements
are already on this branch -- no other diffs from a85ed6032 are
pulled.

The source's narrow except-tuple paraphrase comments are trimmed
to keep only the durable "don't swallow programmer errors" note,
per CLAUDE.md s4a.

Tests: 5 bim-lane tests in test/bim/module/project/
test_pending_opening_cuts.py covering apply happy-path + missing
entity, dismiss, select happy-path + cancellation.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-08 10:18:37 +02:00
parent 704a2d36be
commit 7b9af9f533
6 changed files with 294 additions and 1 deletions
@@ -1223,6 +1223,19 @@ class LoadProjectElements(bpy.types.Operator):
props = tool.Project.get_project_props()
props.is_loading = False
# Stash elements the kernel skipped opening cuts on (HasOpenings > void_limit).
# The Project panel banner offers the user a one-click recut.
props.pending_opening_recut.clear()
if ifc_importer.gross_elements:
for element in ifc_importer.gross_elements:
item = props.pending_opening_recut.add()
item.ifc_definition_id = element.id()
self.report(
{"WARNING"},
f"{len(ifc_importer.gross_elements)} element(s) had too many openings and were loaded without cuts. "
f"Apply manually from the Project panel.",
)
tool.Project.load_default_thumbnails()
tool.Project.set_default_context()
tool.Project.set_default_modeling_dimensions()
@@ -3421,3 +3434,108 @@ class GenerateUVMap(bpy.types.Operator):
tool.Loader.load_generated_uv_map(obj.data)
self.report({"INFO"}, "Generated UV map for selected mesh.")
return {"FINISHED"}
class BIM_OT_apply_pending_opening_cuts(bpy.types.Operator, tool.Ifc.Operator):
"""Recompute the wall mesh including opening subtractions for every host
that the load-time ``void_limit`` filter skipped. Clears the deferred
list on completion so the panel banner disappears."""
bl_idname = "bim.apply_pending_opening_cuts"
bl_label = "Apply Pending Opening Cuts"
bl_description = (
"Recompute meshes for elements whose openings were skipped at load because they had too many openings"
)
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context: bpy.types.Context) -> set[str]:
pending = tool.Project.get_project_props().pending_opening_recut
applied = 0
skipped = 0
failed = 0
for item in pending:
try:
element = tool.Ifc.get().by_id(item.ifc_definition_id)
except RuntimeError:
skipped += 1
continue
obj = tool.Ifc.get_object(element)
if obj is None:
skipped += 1
continue
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body is None:
skipped += 1
continue
try:
tool.Geometry.reimport_element_representations(obj, body, apply_openings=True)
applied += 1
except (RuntimeError, OSError, AttributeError) as exc:
# Programmer errors (TypeError, ValueError, etc.) must surface — don't swallow them.
failed += 1
print(f"apply_pending_opening_cuts: failed to recompute {element} ({exc})")
pending.clear()
message = f"Applied opening cuts to {applied} element(s)."
if skipped:
message += f" {skipped} entry/entries skipped (entity or object no longer available)."
if failed:
message += f" {failed} entry/entries failed (see system console)."
self.report({"WARNING"}, message)
else:
self.report({"INFO"}, message)
return {"FINISHED"}
class BIM_OT_dismiss_pending_opening_cuts(bpy.types.Operator):
bl_idname = "bim.dismiss_pending_opening_cuts"
bl_label = "Dismiss Pending Opening Cuts"
bl_description = "Clear the pending opening-cut list without applying it. Walls stay solid where openings would have been subtracted."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context: bpy.types.Context) -> set[str]:
tool.Project.get_project_props().pending_opening_recut.clear()
return {"FINISHED"}
class BIM_OT_dismiss_multi_instance_warning(bpy.types.Operator):
bl_idname = "bim.dismiss_multi_instance_warning"
bl_label = "Dismiss Multi-Instance Warning"
bl_description = (
"Hide the warning that another Blender instance has this IFC file open. Sticky for the current session."
)
bl_options = {"REGISTER"}
def execute(self, context: bpy.types.Context) -> set[str]:
from bonsai.bim.ifc import dismiss_multi_instance_warning
dismiss_multi_instance_warning()
return {"FINISHED"}
class BIM_OT_select_pending_opening_cuts(bpy.types.Operator):
bl_idname = "bim.select_pending_opening_cuts"
bl_label = "Select Elements With Skipped Opening Cuts"
bl_description = "Select the Blender objects whose openings were skipped at load. Useful for locating which elements need attention."
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_opening_recut:
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)} element(s).")
return {"FINISHED"}