mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +00:00
Cache array-child + wall topology by IFC generation
Two hot paths the gizmo polls fire every viewport event memoise their result against tool.Parametric.get_geom_generation(): - tool.Blender.Modifier.any_selected_array_child caches the per-selection scan against the selection identity-set + the IFC generation token so a stable selection during a drag doesn't re-walk every selected object's BBIM_Array pset every frame. - bim/module/model/wall.py grows a pair-predicate + connection cache that the wall topology gizmos hit; both keyed on (pair_uids, predicate_kind, generation) so a wall split or axis edit invalidates correctly via the generation bump. Behavioural contract is unchanged — stale entries are evicted on generation bump; cache miss returns the same value the un-cached path returned. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# 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.
|
||||
|
||||
"""Cache-invalidation tests for the wall-topology gizmo helpers.
|
||||
|
||||
``GizmoWallUnjoinSingle`` and ``GizmoWallJoinIntersection`` re-run
|
||||
``_iter_path_connections``, ``_are_walls_joined``, ``_are_walls_collinear``,
|
||||
and ``core.project_axis_intersection`` every viewport redraw without the
|
||||
cache helpers wrapping them. These tests pin that:
|
||||
|
||||
- Repeat calls within one IFC generation reuse the cached result.
|
||||
- An IFC-generation bump invalidates the cache.
|
||||
- ``refresh()`` (the Blender state-change hook on the mixin) drops the cache."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
def test_get_wall_connections_cached_returns_cached_within_generation():
|
||||
from bonsai.bim.module.model import wall
|
||||
|
||||
group = Mock(spec=[])
|
||||
elem = Mock()
|
||||
elem.GlobalId = "0AAAAAAAAAAAAAAAAAAAAA"
|
||||
expected = [(Mock(), "ATEND", "ATSTART")]
|
||||
|
||||
call_count = {"n": 0}
|
||||
|
||||
def counting_iter(e):
|
||||
call_count["n"] += 1
|
||||
return expected
|
||||
|
||||
with patch.object(wall, "_iter_path_connections", side_effect=counting_iter), patch(
|
||||
"bonsai.bim.module.model.wall.tool.Parametric.get_geom_generation", return_value=7
|
||||
):
|
||||
first = wall._get_wall_connections_cached(group, elem)
|
||||
second = wall._get_wall_connections_cached(group, elem)
|
||||
|
||||
assert first is second
|
||||
assert call_count["n"] == 1
|
||||
|
||||
|
||||
def test_get_wall_connections_cached_invalidates_on_generation_bump():
|
||||
from bonsai.bim.module.model import wall
|
||||
|
||||
group = Mock(spec=[])
|
||||
elem = Mock()
|
||||
elem.GlobalId = "0AAAAAAAAAAAAAAAAAAAAA"
|
||||
|
||||
call_count = {"n": 0}
|
||||
|
||||
def counting_iter(e):
|
||||
call_count["n"] += 1
|
||||
return []
|
||||
|
||||
gen_state = {"gen": 1}
|
||||
with patch.object(wall, "_iter_path_connections", side_effect=counting_iter), patch(
|
||||
"bonsai.bim.module.model.wall.tool.Parametric.get_geom_generation", side_effect=lambda: gen_state["gen"]
|
||||
):
|
||||
wall._get_wall_connections_cached(group, elem)
|
||||
gen_state["gen"] = 2
|
||||
wall._get_wall_connections_cached(group, elem)
|
||||
|
||||
assert call_count["n"] == 2
|
||||
|
||||
|
||||
def test_get_wall_pair_predicate_cached_reuses_value_within_generation():
|
||||
from bonsai.bim.module.model import wall
|
||||
|
||||
group = Mock(spec=[])
|
||||
call_count = {"n": 0}
|
||||
|
||||
def compute():
|
||||
call_count["n"] += 1
|
||||
return "result"
|
||||
|
||||
with patch("bonsai.bim.module.model.wall.tool.Parametric.get_geom_generation", return_value=3):
|
||||
first = wall._get_wall_pair_predicate_cached(group, ("joined", ("guid_a", "guid_b")), compute)
|
||||
second = wall._get_wall_pair_predicate_cached(group, ("joined", ("guid_a", "guid_b")), compute)
|
||||
|
||||
assert first == second == "result"
|
||||
assert call_count["n"] == 1
|
||||
|
||||
|
||||
def test_get_wall_pair_predicate_cached_distinguishes_predicate_kind():
|
||||
"""The cache key includes a tag string ("joined" vs "collinear" vs
|
||||
"intersection") so adding a second predicate for the same pair doesn't
|
||||
return the first predicate's value."""
|
||||
from bonsai.bim.module.model import wall
|
||||
|
||||
group = Mock(spec=[])
|
||||
pair = ("guid_a", "guid_b")
|
||||
with patch("bonsai.bim.module.model.wall.tool.Parametric.get_geom_generation", return_value=3):
|
||||
a = wall._get_wall_pair_predicate_cached(group, ("joined", pair), lambda: "JOINED")
|
||||
b = wall._get_wall_pair_predicate_cached(group, ("collinear", pair), lambda: "COLLINEAR")
|
||||
|
||||
assert a == "JOINED"
|
||||
assert b == "COLLINEAR"
|
||||
|
||||
|
||||
def test_get_wall_pair_predicate_cached_invalidates_on_generation_bump():
|
||||
from bonsai.bim.module.model import wall
|
||||
|
||||
group = Mock(spec=[])
|
||||
call_count = {"n": 0}
|
||||
|
||||
def compute():
|
||||
call_count["n"] += 1
|
||||
return call_count["n"]
|
||||
|
||||
gen_state = {"gen": 1}
|
||||
with patch(
|
||||
"bonsai.bim.module.model.wall.tool.Parametric.get_geom_generation", side_effect=lambda: gen_state["gen"]
|
||||
):
|
||||
first = wall._get_wall_pair_predicate_cached(group, ("joined", ("a", "b")), compute)
|
||||
gen_state["gen"] = 2
|
||||
second = wall._get_wall_pair_predicate_cached(group, ("joined", ("a", "b")), compute)
|
||||
|
||||
assert first == 1
|
||||
assert second == 2
|
||||
assert call_count["n"] == 2
|
||||
|
||||
|
||||
def test_mixin_refresh_clears_pair_and_connection_caches():
|
||||
"""``refresh()`` is Blender's "state changed" signal — typically a
|
||||
selection change. Both the connection list and pair predicate caches
|
||||
must drop alongside the geometry cache, otherwise the next frame would
|
||||
read predicates that targeted the previously-selected pair."""
|
||||
from bonsai.bim.module.model import wall
|
||||
|
||||
class _Group(wall._WallGeomCachedBillboardingMixin):
|
||||
def position_gizmos(self, context):
|
||||
pass
|
||||
|
||||
group = _Group()
|
||||
group._wall_geom_cache = {"x": "geom"}
|
||||
group._wall_connections_cache = {"guid": []}
|
||||
group._wall_pair_predicate_cache = {"key": "value"}
|
||||
|
||||
group.refresh(context=Mock())
|
||||
|
||||
assert group._wall_geom_cache is None
|
||||
assert group._wall_connections_cache is None
|
||||
assert group._wall_pair_predicate_cache is None
|
||||
@@ -0,0 +1,152 @@
|
||||
# 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.
|
||||
|
||||
"""Cache-invalidation tests for ``tool.Blender.Modifier.any_selected_is_array_child``.
|
||||
|
||||
The wall-topology gizmo gate calls this on every viewport input event. The
|
||||
underlying ``is_array_child`` check is a BBIM_Array pset lookup per selected
|
||||
object; without memoisation that runs N_selected times per event. These
|
||||
tests pin that the cache reuses results across identical (selection, IFC
|
||||
generation) pairs and invalidates on either change."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_memo():
|
||||
from bonsai import tool
|
||||
|
||||
saved = getattr(tool.Blender.Modifier, "_any_selected_array_child_memo", None)
|
||||
tool.Blender.Modifier._any_selected_array_child_memo = None
|
||||
yield
|
||||
tool.Blender.Modifier._any_selected_array_child_memo = saved
|
||||
|
||||
|
||||
def _mock_obj(name: str) -> Mock:
|
||||
obj = Mock()
|
||||
obj.name = name
|
||||
return obj
|
||||
|
||||
|
||||
def test_repeat_call_within_generation_reuses_cache():
|
||||
from bonsai import tool
|
||||
|
||||
obj_a = _mock_obj("Wall.001")
|
||||
obj_b = _mock_obj("Wall.002")
|
||||
|
||||
is_array_child_calls = {"n": 0}
|
||||
|
||||
def counting_is_array_child(elem):
|
||||
is_array_child_calls["n"] += 1
|
||||
return False
|
||||
|
||||
with patch("bonsai.tool.blender.tool.Blender.get_selected_objects", return_value=[obj_a, obj_b]), patch(
|
||||
"bonsai.tool.blender.tool.Parametric.get_geom_generation", return_value=5
|
||||
), patch("bonsai.tool.blender.tool.Ifc.get_entity", return_value=Mock()), patch.object(
|
||||
tool.Blender.Modifier, "is_array_child", side_effect=counting_is_array_child
|
||||
):
|
||||
first = tool.Blender.Modifier.any_selected_is_array_child()
|
||||
second = tool.Blender.Modifier.any_selected_is_array_child()
|
||||
|
||||
assert first is False
|
||||
assert second is False
|
||||
assert is_array_child_calls["n"] == 2, "First call walks N_selected; second call must reuse cached result"
|
||||
|
||||
|
||||
def test_generation_advance_invalidates_cache():
|
||||
from bonsai import tool
|
||||
|
||||
obj = _mock_obj("Wall.001")
|
||||
gen_state = {"gen": 1}
|
||||
|
||||
call_count = {"n": 0}
|
||||
|
||||
def counting_is_array_child(elem):
|
||||
call_count["n"] += 1
|
||||
return False
|
||||
|
||||
with patch("bonsai.tool.blender.tool.Blender.get_selected_objects", return_value=[obj]), patch(
|
||||
"bonsai.tool.blender.tool.Parametric.get_geom_generation", side_effect=lambda: gen_state["gen"]
|
||||
), patch("bonsai.tool.blender.tool.Ifc.get_entity", return_value=Mock()), patch.object(
|
||||
tool.Blender.Modifier, "is_array_child", side_effect=counting_is_array_child
|
||||
):
|
||||
tool.Blender.Modifier.any_selected_is_array_child()
|
||||
first = call_count["n"]
|
||||
gen_state["gen"] = 2
|
||||
tool.Blender.Modifier.any_selected_is_array_child()
|
||||
|
||||
assert call_count["n"] > first
|
||||
|
||||
|
||||
def test_selection_change_invalidates_cache():
|
||||
from bonsai import tool
|
||||
|
||||
obj_a = _mock_obj("Wall.001")
|
||||
obj_b = _mock_obj("Wall.002")
|
||||
selection = {"sel": [obj_a]}
|
||||
|
||||
call_count = {"n": 0}
|
||||
|
||||
def counting_is_array_child(elem):
|
||||
call_count["n"] += 1
|
||||
return False
|
||||
|
||||
with patch("bonsai.tool.blender.tool.Blender.get_selected_objects", side_effect=lambda: selection["sel"]), patch(
|
||||
"bonsai.tool.blender.tool.Parametric.get_geom_generation", return_value=1
|
||||
), patch("bonsai.tool.blender.tool.Ifc.get_entity", return_value=Mock()), patch.object(
|
||||
tool.Blender.Modifier, "is_array_child", side_effect=counting_is_array_child
|
||||
):
|
||||
tool.Blender.Modifier.any_selected_is_array_child()
|
||||
first = call_count["n"]
|
||||
selection["sel"] = [obj_a, obj_b]
|
||||
tool.Blender.Modifier.any_selected_is_array_child()
|
||||
|
||||
assert call_count["n"] > first
|
||||
|
||||
|
||||
def test_short_circuits_on_first_hit():
|
||||
"""``is_array_child`` returning True for the first selected object must
|
||||
short-circuit; the rest of the selection isn't walked. Belt-and-suspenders
|
||||
test — the early-return existed before the cache wrap and must survive it."""
|
||||
from bonsai import tool
|
||||
|
||||
obj_a = _mock_obj("Wall.001")
|
||||
obj_b = _mock_obj("Wall.002")
|
||||
obj_c = _mock_obj("Wall.003")
|
||||
|
||||
call_count = {"n": 0}
|
||||
|
||||
def counting_is_array_child(elem):
|
||||
call_count["n"] += 1
|
||||
return True
|
||||
|
||||
with patch("bonsai.tool.blender.tool.Blender.get_selected_objects", return_value=[obj_a, obj_b, obj_c]), patch(
|
||||
"bonsai.tool.blender.tool.Parametric.get_geom_generation", return_value=1
|
||||
), patch("bonsai.tool.blender.tool.Ifc.get_entity", return_value=Mock()), patch.object(
|
||||
tool.Blender.Modifier, "is_array_child", side_effect=counting_is_array_child
|
||||
):
|
||||
result = tool.Blender.Modifier.any_selected_is_array_child()
|
||||
|
||||
assert result is True
|
||||
assert call_count["n"] == 1
|
||||
Reference in New Issue
Block a user