mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-10 22:16:41 +00:00
Swap merge convention to active-is-survivor
bim.merge_wall now consumes the non-active selection into the active one — matching Blender's OBJECT_OT_join (Ctrl+J) and MESH_OT_merge "at last" convention. The wall the user clicks last absorbs the other; users following Blender muscle-memory get the result they expect. DumbWallJoiner.merge is already structurally asymmetric (wall1 = survivor); only the caller in MergeWall._perform needed flipping. Audit confirmed the previous call site was the sole caller of DumbWallJoiner.merge in production code. Drive-by tidies on adjacent code: collapse two over-length comprehensions under black's 120-char budget, and switch ``any(True for _ in gen)`` to ``any(gen)`` since the iterable yields tuples that are always truthy. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
|
||||
"""Pins the active-is-survivor merge convention.
|
||||
|
||||
``bim.merge_wall`` must consume the non-active selection into the active
|
||||
one — matching Blender's ``OBJECT_OT_join`` / ``MESH_OT_merge`` "at
|
||||
last" convention. Users following Ctrl+J muscle-memory click the
|
||||
surviving wall last; the operator must align with that expectation."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.wall
|
||||
|
||||
|
||||
def _run_perform(active, other):
|
||||
"""Invoke ``MergeWall._perform`` as an unbound function with the
|
||||
two wall stubs in the selection, patching the heavy IFC / Blender
|
||||
side effects. Returns the ``(merger_arg_1, merger_arg_2)`` actually
|
||||
passed to ``DumbWallJoiner.merge``."""
|
||||
from bonsai.bim.module.model.wall import MergeWall
|
||||
|
||||
context = SimpleNamespace(active_object=active)
|
||||
captured_call = {}
|
||||
|
||||
def _capture_merge(self, a, b):
|
||||
captured_call["wall1"] = a
|
||||
captured_call["wall2"] = b
|
||||
|
||||
fake_self = SimpleNamespace()
|
||||
|
||||
with (
|
||||
patch("bonsai.bim.module.model.wall.tool.Ifc.get", return_value=MagicMock(name="ifc_file")),
|
||||
patch("bonsai.bim.module.model.wall.tool.Model.get_selected_mesh_objects", return_value=[active, other]),
|
||||
patch("bonsai.bim.module.model.wall.DumbWallJoiner.__init__", return_value=None),
|
||||
patch("bonsai.bim.module.model.wall.DumbWallJoiner.merge", new=_capture_merge),
|
||||
patch("bonsai.bim.module.model.wall._maybe_resync_wall_props_from_ifc"),
|
||||
patch("bonsai.bim.module.model.wall._regenerate_walls") as regen_walls,
|
||||
):
|
||||
result = MergeWall._perform(fake_self, context)
|
||||
|
||||
return captured_call, regen_walls, result
|
||||
|
||||
|
||||
def test_active_wall_is_passed_as_survivor_to_merge():
|
||||
"""The first argument to ``DumbWallJoiner.merge`` is the survivor;
|
||||
the active object must occupy that slot so the wall the user clicked
|
||||
last absorbs the other."""
|
||||
active = SimpleNamespace(name="active")
|
||||
other = SimpleNamespace(name="other")
|
||||
|
||||
captured, _regen, _ = _run_perform(active, other)
|
||||
|
||||
assert captured["wall1"] is active
|
||||
assert captured["wall2"] is other
|
||||
|
||||
|
||||
def test_post_merge_resync_targets_active_not_consumed():
|
||||
"""After the merge ``_regenerate_walls`` rebuilds the survivor's
|
||||
body. Targeting the consumed wall would crash on a freed ``bpy_struct``;
|
||||
the survivor (active) is the only valid target."""
|
||||
active = SimpleNamespace(name="active")
|
||||
other = SimpleNamespace(name="other")
|
||||
|
||||
_, regen_walls, _ = _run_perform(active, other)
|
||||
|
||||
regen_walls.assert_called_once_with([active])
|
||||
Reference in New Issue
Block a user