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
@@ -0,0 +1,19 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated with the assistance of an AI coding tool.
@@ -0,0 +1,108 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated with the assistance of an AI coding tool.
from unittest.mock import patch
import bpy
import ifcopenshell
import pytest
import bonsai.tool as tool
from test.bim.bootstrap import NewIfc
pytestmark = pytest.mark.project
def _populate_pending(*element_ids: int) -> None:
pending = tool.Project.get_project_props().pending_opening_recut
pending.clear()
for eid in element_ids:
pending.add().ifc_definition_id = eid
def _make_linked_wall(name: str = "Wall") -> tuple[ifcopenshell.entity_instance, bpy.types.Object]:
ifc_file = tool.Ifc.get()
element = ifc_file.create_entity("IfcWall", GlobalId=ifcopenshell.guid.new(), Name=name)
obj = bpy.data.objects.new(name, bpy.data.meshes.new(name))
bpy.context.scene.collection.objects.link(obj)
tool.Ifc.link(element, obj)
return element, obj
class TestApplyPendingOpeningCuts(NewIfc):
def test_clears_pending_and_calls_reimport_with_apply_openings(self):
element, obj = _make_linked_wall()
_populate_pending(element.id())
with patch.object(tool.Geometry, "reimport_element_representations") as mock_reimport, patch(
"ifcopenshell.util.representation.get_representation",
return_value=object(),
):
result = bpy.ops.bim.apply_pending_opening_cuts()
assert result == {"FINISHED"}
assert len(tool.Project.get_project_props().pending_opening_recut) == 0
mock_reimport.assert_called_once()
_, kwargs = mock_reimport.call_args
assert kwargs.get("apply_openings") is True
def test_skips_entries_whose_entity_is_gone(self):
_populate_pending(99999) # ID guaranteed not present
with patch.object(tool.Geometry, "reimport_element_representations") as mock_reimport:
result = bpy.ops.bim.apply_pending_opening_cuts()
assert result == {"FINISHED"}
assert len(tool.Project.get_project_props().pending_opening_recut) == 0
mock_reimport.assert_not_called()
class TestDismissPendingOpeningCuts(NewIfc):
def test_clears_collection_without_calling_reimport(self):
element, _obj = _make_linked_wall()
_populate_pending(element.id())
with patch.object(tool.Geometry, "reimport_element_representations") as mock_reimport:
result = bpy.ops.bim.dismiss_pending_opening_cuts()
assert result == {"FINISHED"}
assert len(tool.Project.get_project_props().pending_opening_recut) == 0
mock_reimport.assert_not_called()
class TestSelectPendingOpeningCuts(NewIfc):
def test_selects_objects_for_each_pending_entry(self):
e1, o1 = _make_linked_wall("WallA")
e2, o2 = _make_linked_wall("WallB")
_populate_pending(e1.id(), e2.id())
for obj in bpy.context.view_layer.objects:
obj.select_set(False)
result = bpy.ops.bim.select_pending_opening_cuts()
assert result == {"FINISHED"}
assert o1.select_get() and o2.select_get()
assert bpy.context.view_layer.objects.active in (o1, o2)
def test_cancels_when_no_objects_match(self):
_populate_pending(99999)
result = bpy.ops.bim.select_pending_opening_cuts()
assert result == {"CANCELLED"}