mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-23 06:12:38 +00:00
Add readonly door swing arc preview
Selecting a Bonsai-parametric IfcDoor now shows the swing arc(s) without entering edit mode. A new viewport decorator polls on the active object, reads the door's BBIM_Door pset, and draws the same arcs the parametric door swing gizmo would draw — matching the hinge / panel-width / x-mirror contract minus the is_editing gate. A forward-compat test walks every door operation type and cross- checks the readonly decorator's arc selection against the gizmo's swing-arc config table, so future enum additions fail in both surfaces simultaneously. Also disables the inherited 8-pass dark halo on GizmoArc: an open curve has no enclosed silhouette, so the offset passes read as ghost arcs rather than a uniform outline. The arc's own cross- section thickness keeps it legible without the halo. Generated with the assistance of an AI coding tool.
This commit is contained in:
committed by
Thomas Krijnen
parent
4a6087699b
commit
2c3935bf9d
@@ -0,0 +1,190 @@
|
||||
# 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.
|
||||
|
||||
"""Contract tests for the door swing-arc readonly decorator.
|
||||
|
||||
Two layers:
|
||||
|
||||
- Pure tests on ``_visible_arcs`` pin the readonly decorator's arc selection
|
||||
per ``door_type`` enum value.
|
||||
- A forward-compat guard walks ``GizmoDoorEdition.swing_arc_props`` and
|
||||
asserts the readonly decorator picks the same arcs (hinge / width / mirror)
|
||||
the edit-mode gizmo would, so the two surfaces stay visually identical
|
||||
even when a new ``door_type`` is added."""
|
||||
|
||||
import types
|
||||
from types import SimpleNamespace
|
||||
from typing import get_args
|
||||
|
||||
import bpy
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_real_bpy():
|
||||
if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"):
|
||||
pytest.skip("requires real Blender (bpy is mocked or absent)")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# _visible_arcs — per-door-type arc selection
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _arcs(door_type, overall_width=0.9, lining_offset=0.05):
|
||||
from bonsai.bim.module.model.decorator import _visible_arcs
|
||||
|
||||
return _visible_arcs(door_type, overall_width, lining_offset)
|
||||
|
||||
|
||||
def test_single_swing_left_one_arc_hinged_at_origin():
|
||||
arcs = _arcs("SINGLE_SWING_LEFT")
|
||||
assert len(arcs) == 1
|
||||
arc = arcs[0]
|
||||
assert arc.hinge_x == pytest.approx(0.0)
|
||||
assert arc.hinge_y == pytest.approx(0.05)
|
||||
assert arc.panel_width == pytest.approx(0.9)
|
||||
assert arc.x_mirror is False
|
||||
|
||||
|
||||
def test_single_swing_right_one_arc_hinged_at_right_edge_x_mirrored():
|
||||
arcs = _arcs("SINGLE_SWING_RIGHT")
|
||||
assert len(arcs) == 1
|
||||
arc = arcs[0]
|
||||
assert arc.hinge_x == pytest.approx(0.9)
|
||||
assert arc.panel_width == pytest.approx(0.9)
|
||||
assert arc.x_mirror is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("door_type", ["DOUBLE_SWING_LEFT", "DOUBLE_SWING_RIGHT"])
|
||||
def test_double_swing_shares_recipe_with_single_swing(door_type):
|
||||
# DOUBLE_SWING_* is still a single panel (the hinge is on one side,
|
||||
# the panel swings both ways) — visually identical to SINGLE_SWING_*.
|
||||
single_type = door_type.replace("DOUBLE_SWING", "SINGLE_SWING")
|
||||
assert _arcs(door_type) == _arcs(single_type)
|
||||
|
||||
|
||||
def test_double_door_single_swing_emits_two_half_width_arcs():
|
||||
arcs = _arcs("DOUBLE_DOOR_SINGLE_SWING")
|
||||
assert len(arcs) == 2
|
||||
left, right = arcs
|
||||
assert left.hinge_x == pytest.approx(0.0)
|
||||
assert left.panel_width == pytest.approx(0.45)
|
||||
assert left.x_mirror is False
|
||||
assert right.hinge_x == pytest.approx(0.9)
|
||||
assert right.panel_width == pytest.approx(0.45)
|
||||
assert right.x_mirror is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("door_type", ["SLIDING_TO_LEFT", "SLIDING_TO_RIGHT", "DOUBLE_DOOR_SLIDING"])
|
||||
def test_sliding_doors_emit_no_arcs(door_type):
|
||||
assert _arcs(door_type) == []
|
||||
|
||||
|
||||
def test_unknown_door_type_falls_back_to_single_left_swing_arc():
|
||||
# Only ``"SLIDING"`` substrings short-circuit the swing predicate; any
|
||||
# other novel ``door_type`` falls through to the default left-hinged arc.
|
||||
arcs = _arcs("FUTURE_OPERATION_TYPE_42")
|
||||
assert len(arcs) == 1
|
||||
arc = arcs[0]
|
||||
assert arc.hinge_x == pytest.approx(0.0)
|
||||
assert arc.panel_width == pytest.approx(0.9)
|
||||
assert arc.x_mirror is False
|
||||
|
||||
|
||||
def test_lining_offset_drives_hinge_y_for_every_visible_arc():
|
||||
for door_type in ("SINGLE_SWING_LEFT", "SINGLE_SWING_RIGHT", "DOUBLE_DOOR_SINGLE_SWING"):
|
||||
for arc in _arcs(door_type, overall_width=0.9, lining_offset=0.12):
|
||||
assert arc.hinge_y == pytest.approx(0.12)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Forward-compat: readonly decorator and edit-mode gizmo agree per door_type
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _gizmo_expected(door_type, overall_width, lining_offset):
|
||||
"""What ``GizmoDoorEdition.swing_arc_props`` would render for the props
|
||||
snapshot, with ``is_editing=True`` so its visibility predicates pass."""
|
||||
from bonsai.bim.module.model.door import GizmoDoorEdition
|
||||
|
||||
props = SimpleNamespace(
|
||||
door_type=door_type,
|
||||
overall_width=overall_width,
|
||||
lining_offset=lining_offset,
|
||||
is_editing=True,
|
||||
)
|
||||
expected = []
|
||||
for cfg in GizmoDoorEdition.swing_arc_props:
|
||||
if cfg.visibility_condition(props):
|
||||
expected.append(
|
||||
(
|
||||
cfg.hinge_x(props),
|
||||
cfg.hinge_y(props),
|
||||
cfg.panel_width(props),
|
||||
cfg.x_mirror(props),
|
||||
)
|
||||
)
|
||||
return expected
|
||||
|
||||
|
||||
def test_visible_arcs_matches_gizmo_swing_arc_props_for_every_door_type():
|
||||
import bonsai.tool as tool
|
||||
|
||||
overall_width, lining_offset = 0.9, 0.05
|
||||
for door_type in get_args(tool.Model.DoorType):
|
||||
expected = _gizmo_expected(door_type, overall_width, lining_offset)
|
||||
actual = _arcs(door_type, overall_width, lining_offset)
|
||||
actual_tuples = [(a.hinge_x, a.hinge_y, a.panel_width, a.x_mirror) for a in actual]
|
||||
assert actual_tuples == expected, (
|
||||
f"Readonly decorator drifted from edit-mode gizmo for {door_type!r}: "
|
||||
f"expected {expected}, got {actual_tuples}"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Decorator gating contract (draw() early-returns)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_decorator_stub():
|
||||
"""Build a fresh ``DoorSwingReadonlyDecorator`` instance without going
|
||||
through ``install`` (which would attach a draw handler)."""
|
||||
from bonsai.bim.module.model.decorator import DoorSwingReadonlyDecorator
|
||||
|
||||
return DoorSwingReadonlyDecorator()
|
||||
|
||||
|
||||
def _draw_with_active(decorator, active_obj):
|
||||
"""Call ``draw`` with a minimal ``context`` stub."""
|
||||
ctx = SimpleNamespace(active_object=active_obj)
|
||||
decorator.draw(ctx)
|
||||
|
||||
|
||||
def test_draw_early_returns_when_no_active_object():
|
||||
# Should not raise; nothing to draw.
|
||||
_draw_with_active(_make_decorator_stub(), None)
|
||||
|
||||
|
||||
def test_draw_early_returns_when_active_not_selected():
|
||||
obj = SimpleNamespace(select_get=lambda: False)
|
||||
_draw_with_active(_make_decorator_stub(), obj)
|
||||
Reference in New Issue
Block a user