From 05c9df74f9e1015e57ee5a3e51cbbb535d6f48de Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Thu, 11 Jun 2026 20:49:27 +0200 Subject: [PATCH] Migrate railing terminal type to PickType menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches the IfcRailingType terminal-type selector from cycle-on-click to a popup menu of all terminal-type literals — 5+ values trip the §2.8 menu-pick threshold. Updates classes registration; removes EditRailingTerminalType in favour of PickRailingTerminalType which inherits PickTypeMixin. Adapts the cherry-pick from db016d881 to post-PR5 framework state: - Imports CycleTypeMixin / PickTypeMixin / PathPreservingEditMixin from bim.parametric_lifecycle (PR5 moved them off gizmos.py). - Routes is_railing through tool.Parametric (predicates moved off tool.Blender.Modifier between PR3-PR5). Skips the parametric_lifecycle.py framework refactor the source commit shipped — HEAD has the more-evolved post-PR5 framework that already covers it. Adds the _FakePropsBase + make_lifecycle_obj test helpers to test/bim/conftest.py so the new test_railing_lifecycle.py can exercise the edit triad without a real bpy.types.Object. Brings the test_railing_schematic.py marker in line with the rest of the model lane. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/model/__init__.py | 2 +- src/bonsai/bonsai/bim/module/model/railing.py | 153 ++++++------------ src/bonsai/test/bim/conftest.py | 41 +++++ .../module/model/test_railing_lifecycle.py | 144 ++++++++++------- .../module/model/test_railing_schematic.py | 8 +- 5 files changed, 172 insertions(+), 176 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index edac430309..08806ad8fb 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -247,8 +247,8 @@ classes = ( railing.AddRailing, railing.CancelEditingRailing, railing.CycleRailingType, - railing.EditRailingTerminalType, railing.FinishEditingRailing, + railing.PickRailingTerminalType, railing.FlipRailingPathOrder, railing.EnableEditingRailing, railing.GizmoRailingSchematic, diff --git a/src/bonsai/bonsai/bim/module/model/railing.py b/src/bonsai/bonsai/bim/module/model/railing.py index 072b7d562d..825cc339ac 100644 --- a/src/bonsai/bonsai/bim/module/model/railing.py +++ b/src/bonsai/bonsai/bim/module/model/railing.py @@ -19,7 +19,7 @@ import json import math -from typing import Any, get_args +from typing import Any import bmesh import bpy @@ -38,7 +38,11 @@ from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig from bonsai.bim.module.model import prop from bonsai.bim.module.model.data import RailingData, refresh from bonsai.bim.module.model.decorator import ProfileDecorator -from bonsai.bim.parametric_lifecycle import PathPreservingEditMixin +from bonsai.bim.parametric_lifecycle import ( + CycleTypeMixin, + PathPreservingEditMixin, + PickTypeMixin, +) from bonsai.tool.cad import WELD_TOLERANCE V_ = tool.Blender.V_ @@ -497,53 +501,9 @@ class _RailingEditMixin(PathPreservingEditMixin): update_railing_modifier_ifc_data(context) @classmethod - def _update_modifier_bmesh(cls, obj: bpy.types.Object, context: bpy.types.Context) -> None: - update_railing_modifier_bmesh(context) - - @classmethod - def _finish_one(cls, obj: bpy.types.Object, context: bpy.types.Context) -> None: - """Skip the IFC commit when the draft matches the stored pset (no-op edit).""" - resolved = cls._resolve(obj) - if resolved is None: - return - element, props = resolved - pset_data = tool.Model.get_modeling_bbim_pset_data(obj, cls.pset_name) - stored = pset_data["data_dict"] - path_data = stored["path_data"] - draft = props.get_general_kwargs(convert_to_project_units=True) - draft["path_data"] = path_data - - if draft == stored: - props.is_editing = False - return - - cls._update_pset(element, draft) - cls._update_modifier_ifc_data(obj, context) - props.is_editing = False - - @classmethod - def _cancel_one(cls, obj: bpy.types.Object, context: bpy.types.Context) -> None: - """WALL_MOUNTED_HANDRAIL switches the representation back to Body on cancel - (the cylinder preview is lower-poly than the committed swept-disk solid). - Skip the switch when the draft matches the stored pset.""" - resolved = cls._resolve(obj) - if resolved is None: - return - _element, props = resolved - pset_data = tool.Model.get_modeling_bbim_pset_data(obj, cls.pset_name) - stored = pset_data["data_dict"] - - draft = props.get_general_kwargs(convert_to_project_units=True) - draft["path_data"] = stored["path_data"] - nothing_changed = draft == stored - - data = cls._post_load_data(stored) - props.set_props_kwargs_from_ifc_data(data) - - if nothing_changed: - props.is_editing = False - return - + def _restore_viewport_after_cancel(cls, obj: bpy.types.Object, context: bpy.types.Context) -> None: + """WALL_MOUNTED_HANDRAIL reloads the committed Body; others rebuild the preview bmesh.""" + props = tool.Model.get_railing_props(obj) if props.railing_type == "WALL_MOUNTED_HANDRAIL": element = tool.Ifc.get_entity(obj) assert element @@ -555,10 +515,8 @@ class _RailingEditMixin(PathPreservingEditMixin): obj=obj, representation=body, ) - else: - cls._update_modifier_bmesh(obj, context) - - props.is_editing = False + return + update_railing_modifier_bmesh(context) class EnableEditingRailing(_RailingEditMixin, bpy.types.Operator, tool.Ifc.Operator): @@ -588,14 +546,14 @@ class FinishEditingRailing(_RailingEditMixin, bpy.types.Operator, tool.Ifc.Opera return self._finish_targets(context) -class CycleRailingType(bpy.types.Operator, tool.Ifc.Operator, gizmo.CycleTypeMixin): +class CycleRailingType(bpy.types.Operator, tool.Ifc.Operator, CycleTypeMixin): """Cycle railing_type (FRAMELESS_PANEL ↔ WALL_MOUNTED_HANDRAIL). Shift+click reverses.""" bl_idname = "bim.cycle_railing_type" bl_label = "Cycle Railing Type" bl_options = {"REGISTER", "UNDO"} - element_checker = tool.Blender.Modifier.is_railing + element_checker = tool.Parametric.is_railing props_getter = tool.Model.get_railing_props type_literal = tool.Model.RailingType type_attr = "railing_type" @@ -616,58 +574,42 @@ class ToggleRailingUseManualSupports(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = context.active_object - if not obj: - return {"CANCELLED"} - props = tool.Model.get_railing_props(obj) - if not props.is_editing or props.railing_type != "WALL_MOUNTED_HANDRAIL": + resolved = tool.Model.resolve_active_props_for_edit( + context, + tool.Model.get_railing_props, + subtype=("railing_type", "WALL_MOUNTED_HANDRAIL"), + ) + if resolved is None: return {"CANCELLED"} + _obj, props = resolved props.use_manual_supports = not props.use_manual_supports return {"FINISHED"} -class EditRailingTerminalType(bpy.types.Operator): - """Popup menu for terminal_type; writes the picked value via a HIDDEN string property.""" +class PickRailingTerminalType(bpy.types.Operator, tool.Ifc.Operator, PickTypeMixin): + """Pick ``terminal_type`` for the active WALL_MOUNTED_HANDRAIL railing.""" - bl_idname = "bim.edit_railing_terminal_type" - bl_label = "Choose Railing Terminal Type" + bl_idname = "bim.pick_railing_terminal_type" + bl_label = "Pick Railing Terminal Type" bl_description = "Pick the cap geometry applied at the rail ends" bl_options = {"REGISTER", "UNDO"} - terminal_type: bpy.props.StringProperty(name="Terminal Type", default="", options={"HIDDEN", "SKIP_SAVE"}) + skip_element_check = True + props_getter = tool.Model.get_railing_props + type_literal = prop.CapType + type_attr = "terminal_type" - def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: - obj = context.active_object - if not obj: + def _execute(self, context: bpy.types.Context) -> set[str]: + if ( + tool.Model.resolve_active_props_for_edit( + context, + tool.Model.get_railing_props, + subtype=("railing_type", "WALL_MOUNTED_HANDRAIL"), + ) + is None + ): return {"CANCELLED"} - props = tool.Model.get_railing_props(obj) - if not props.is_editing or props.railing_type != "WALL_MOUNTED_HANDRAIL": - return {"CANCELLED"} - - choices = [v for v in get_args(prop.CapType)] - - def draw(menu_self, _menu_context): - layout = menu_self.layout - for v in choices: - op = layout.operator(self.bl_idname, text=v) - op.terminal_type = v - - context.window_manager.popup_menu(draw, title="Terminal Type", icon="MOD_LATTICE") - return {"FINISHED"} - - def execute(self, context: bpy.types.Context) -> set[str]: - # Re-open the popup if called without a value (e.g. from the command palette). - if not self.terminal_type: - return self.invoke(context, None) # type: ignore[arg-type] - obj = context.active_object - if not obj: - return {"CANCELLED"} - props = tool.Model.get_railing_props(obj) - if self.terminal_type not in get_args(prop.CapType): - self.report({"ERROR"}, f"Unknown terminal_type: {self.terminal_type!r}") - return {"CANCELLED"} - props.terminal_type = self.terminal_type # type: ignore[assignment] - return {"FINISHED"} + return self._pick_type(context) def _format_attr_distance(attr_name: str): @@ -822,7 +764,7 @@ class GizmoRailingSchematic(bpy.types.GizmoGroup, gizmo.BaseSchematicGizmoGroup) @classmethod def is_element_type(cls, element: ifcopenshell.entity_instance) -> bool: - return tool.Blender.Modifier.is_railing(element) + return tool.Parametric.is_railing(element) @classmethod def schematic_cache_key(cls, props) -> tuple: @@ -846,22 +788,17 @@ class GizmoRailingSchematic(bpy.types.GizmoGroup, gizmo.BaseSchematicGizmoGroup) """ default_color, highlight_color = self.get_decoration_colors() - for slot in ("lock_open_gizmo", "lock_closed_gizmo"): - bl_idname = "VIEW3D_GT_lock_open" if slot == "lock_open_gizmo" else "VIEW3D_GT_lock_closed" - gz = self.gizmos.new(bl_idname) - gz.color = default_color - gz.color_highlight = highlight_color - gz.use_draw_scale = False - gz.alpha = 0.8 - gz.target_set_operator("bim.toggle_railing_use_manual_supports") - setattr(self, slot, gz) + self.lock_open_gizmo, self.lock_closed_gizmo = self.create_icon_gizmo_lock_pair( + "bim.toggle_railing_use_manual_supports", + open_color=default_color, + ) - self.terminal_gizmo = self.gizmos.new("VIEW3D_GT_cycle") + self.terminal_gizmo = self.gizmos.new("VIEW3D_GT_menu") self.terminal_gizmo.color = default_color self.terminal_gizmo.color_highlight = highlight_color self.terminal_gizmo.use_draw_scale = False self.terminal_gizmo.alpha = 0.8 - self.terminal_gizmo.target_set_operator("bim.edit_railing_terminal_type") + self.terminal_gizmo.target_set_operator("bim.pick_railing_terminal_type") def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None: """Position and gate the WALL_MOUNTED_HANDRAIL-only gizmos. diff --git a/src/bonsai/test/bim/conftest.py b/src/bonsai/test/bim/conftest.py index 2d69fe415a..a957b479ba 100644 --- a/src/bonsai/test/bim/conftest.py +++ b/src/bonsai/test/bim/conftest.py @@ -1,5 +1,46 @@ import pytest + +class _FakePropsBase: + """Base for parametric-edit PropertyGroup stand-ins used in lifecycle tests. + + The parametric-edit lifecycle mixins read/write a common contract: + ``is_editing`` (bool), ``last_kwargs`` (dict | None — capture of the last + data written via ``set_props_kwargs_from_ifc_data``), + ``set_props_kwargs_from_ifc_data(data)``, and + ``get_general_kwargs(convert_to_project_units=True)``. Per-type stand-ins + (door, railing, roof) subclass this and add their own kwargs accessors + and per-type fields.""" + + def __init__(self, general: dict | None = None): + self.is_editing = False + self.last_kwargs: dict | None = None + self.general = dict(general) if general is not None else {} + + def set_props_kwargs_from_ifc_data(self, data): + self.last_kwargs = dict(data) + + def get_general_kwargs(self, convert_to_project_units=True): + return dict(self.general) + + +def make_lifecycle_obj(props, *, name="obj"): + """Build a ``bpy.types.Object`` stand-in for parametric-lifecycle tests. + + The mixin code under test reads ``obj.props`` (the PropertyGroup + stand-in) and ``obj.name`` (used in error reports). ``spec=bpy.types.Object`` + catches typo'd attribute access at test time. ``bpy`` is imported inside + the function so this conftest stays importable when bpy is absent.""" + from unittest import mock + + import bpy + + obj = mock.Mock(spec=bpy.types.Object, name=name) + obj.props = props + obj.name = name + return obj + + # pytest by default doesn't print steps and where it failed. Let's fix that. diff --git a/src/bonsai/test/bim/module/model/test_railing_lifecycle.py b/src/bonsai/test/bim/module/model/test_railing_lifecycle.py index bcc7005bf9..0aad3534b7 100644 --- a/src/bonsai/test/bim/module/model/test_railing_lifecycle.py +++ b/src/bonsai/test/bim/module/model/test_railing_lifecycle.py @@ -18,23 +18,24 @@ # # This file was generated with the assistance of an AI coding tool. -"""Unit coverage for the ``_RailingEditMixin`` lifecycle overrides. +"""Unit coverage for the ``_RailingEditMixin`` overrides and the lifecycle +behaviour railing inherits from ``PathPreservingEditMixin``. -The generic ``PathPreservingEditMixin`` lifecycle is tested in -``test_parametric_lifecycle.py``. This file pins the **railing-specific -overrides** that subclass it: +The parent short-circuit (skip the IFC commit / viewport rebuild when the +draft is identical to the stored pset) lives in +``PathPreservingEditMixin``; the tests below verify railing's subclass +honours that contract by inheritance, then pin the railing-specific +viewport-restore dispatch: -- ``_RailingEditMixin._finish_one`` short-circuit: when the draft equals - the stored pset, ``_update_pset`` and ``_update_modifier_ifc_data`` are - skipped so an Enable → Finish-without-changes cycle creates no new - ``IfcShapeRepresentation``. -- ``_RailingEditMixin._cancel_one`` short-circuit: same logic guards the - expensive ``bonsai.core.geometry.switch_representation`` call (which - re-tessellates the swept-disk solid) when nothing actually changed. -- ``_RailingEditMixin._cancel_one`` WALL_MOUNTED_HANDRAIL branch: when - changes WERE made, the cancel reloads the IFC body via - ``switch_representation`` instead of running ``update_modifier_bmesh`` - (which would leave the low-poly cylinder-segment preview on screen). +- Finish / Cancel no-op short-circuit: inherited from the parent — verified + here because railing was the original consumer that motivated the + optimisation. +- ``_RailingEditMixin._restore_viewport_after_cancel`` dispatch: WALL_MOUNTED_HANDRAIL + reloads the high-poly Body representation via ``switch_representation``; + FRAMELESS_PANEL rebuilds the bmesh preview via + ``update_railing_modifier_bmesh``. This is the per-type branch that used + to live in ``_cancel_one`` and now lives in the viewport-restore hook the + parent's ``_cancel_one`` calls. """ from unittest import mock @@ -68,10 +69,16 @@ class _FakeRailingProps(_FakePropsBase): def patched_railing(): """Patch the railing module's external references for unit testing. - ``_RailingEditMixin`` calls ``tool.Model.get_modeling_bbim_pset_data``, - ``tool.Ifc.get_entity``, ``ifcopenshell.util.representation.get_representation``, - and ``bonsai.core.geometry.switch_representation`` — each looked up - through the railing module's own bindings, so we patch them there. + ``_RailingEditMixin`` and the parent lifecycle reach for + ``tool.Model.get_modeling_bbim_pset_data``, ``tool.Ifc.get_entity``, + ``ifcopenshell.util.representation.get_representation``, + ``bonsai.core.geometry.switch_representation``, and the module-level + ``update_railing_modifier_bmesh`` — each looked up through the railing + module's own bindings, so we patch them there. + + ``parametric_lifecycle.tool`` is patched separately so the parent's + ``_resolve`` and ``_cancel_one`` can read ``tool.Model.get_modeling_bbim_pset_data`` + without falling through to the real Blender bindings. Uses ``mock.patch.object`` with a direct module reference rather than the dotted-string form: ``mock.patch("bonsai.bim.module.model.railing.bonsai")`` @@ -81,17 +88,28 @@ def patched_railing(): Returns a dict for tests to seed return values and assert call sites. """ + from bonsai.bim import parametric_lifecycle from bonsai.bim.module.model import railing with ( mock.patch.object(railing, "tool") as mock_tool, mock.patch.object(railing, "ifcopenshell") as mock_ifc, mock.patch.object(railing, "bonsai") as mock_bonsai, + mock.patch.object(railing, "update_railing_modifier_bmesh") as mock_update_bmesh, + mock.patch.object(parametric_lifecycle, "tool") as mock_pl_tool, ): # _resolve will be overridden on the test subclass below so the - # parametric_lifecycle.tool patch isn't needed. + # parametric_lifecycle.tool patch isn't needed for that path, but the + # parent's _cancel_one / _finish_one still call + # tool.Model.get_modeling_bbim_pset_data and would otherwise miss. mock_tool.Ifc.get_entity.return_value = mock.Mock(name="entity") - yield {"tool": mock_tool, "ifcopenshell": mock_ifc, "bonsai": mock_bonsai} + yield { + "tool": mock_tool, + "ifcopenshell": mock_ifc, + "bonsai": mock_bonsai, + "update_bmesh": mock_update_bmesh, + "pl_tool": mock_pl_tool, + } def _railing_test_subclass(props): @@ -108,7 +126,7 @@ def _railing_test_subclass(props): class _TestRailingMixin(_RailingEditMixin): pset_updates: mock.MagicMock = mock.MagicMock(name="_update_pset") ifc_data_updates: mock.MagicMock = mock.MagicMock(name="_update_modifier_ifc_data") - bmesh_updates: mock.MagicMock = mock.MagicMock(name="_update_modifier_bmesh") + bmesh_updates: mock.MagicMock = mock.MagicMock(name="_restore_viewport_after_cancel") @classmethod def _resolve(cls, obj): @@ -123,7 +141,7 @@ def _railing_test_subclass(props): cls.ifc_data_updates(obj, context) @classmethod - def _update_modifier_bmesh(cls, obj, context): + def _restore_viewport_after_cancel(cls, obj, context): cls.bmesh_updates(obj, context) # The base _post_load_data JSON-serialises path_data; bypass that @@ -148,11 +166,14 @@ def test_finish_one_short_circuits_when_draft_matches_stored(patched_railing): would create a fresh ``IfcShapeRepresentation``, pollute the file's representation list, and burn an undo entry — the user-visible regression that motivated the short-circuit. + + Behaviour now inherited from ``PathPreservingEditMixin``; railing keeps + the coverage as the original consumer of the contract. """ stored = {"railing_type": "WALL_MOUNTED_HANDRAIL", "height": 1.0} props = _FakeRailingProps(general=dict(stored)) obj = _make_obj(props) - patched_railing["tool"].Model.get_modeling_bbim_pset_data.return_value = { + patched_railing["pl_tool"].Model.get_modeling_bbim_pset_data.return_value = { "data_dict": {**stored, "path_data": {"verts": [], "edges": []}}, } @@ -171,7 +192,7 @@ def test_finish_one_writes_when_draft_differs(patched_railing): # Draft height differs: simulating a user edit. props = _FakeRailingProps(general={"railing_type": "WALL_MOUNTED_HANDRAIL", "height": 1.5}) obj = _make_obj(props) - patched_railing["tool"].Model.get_modeling_bbim_pset_data.return_value = { + patched_railing["pl_tool"].Model.get_modeling_bbim_pset_data.return_value = { "data_dict": {**stored, "path_data": {"verts": [], "edges": []}}, } @@ -198,11 +219,14 @@ def test_cancel_one_short_circuits_when_draft_matches_stored(patched_railing): visibly slow on a long handrail. When nothing changed, the mesh on screen is still the committed IFC representation (the preview only builds on a property change) — skip the reload entirely. + + Behaviour now inherited from ``PathPreservingEditMixin``; railing keeps + the coverage as the original consumer of the contract. """ stored = {"railing_type": "WALL_MOUNTED_HANDRAIL", "height": 1.0} props = _FakeRailingProps(general=dict(stored)) obj = _make_obj(props) - patched_railing["tool"].Model.get_modeling_bbim_pset_data.return_value = { + patched_railing["pl_tool"].Model.get_modeling_bbim_pset_data.return_value = { "data_dict": {**stored, "path_data": {"verts": [], "edges": []}}, } @@ -211,60 +235,56 @@ def test_cancel_one_short_circuits_when_draft_matches_stored(patched_railing): assert props.is_editing is False patched_railing["bonsai"].core.geometry.switch_representation.assert_not_called() + patched_railing["update_bmesh"].assert_not_called() cls.bmesh_updates.assert_not_called() -def test_cancel_one_wall_mounted_handrail_switches_representation(patched_railing): - """Cancel after a real edit on a WALL_MOUNTED_HANDRAIL must reload the - committed Body representation (high-poly, IFC-derived) rather than - re-running the low-poly bmesh preview — that preview is a viewport-only - approximation and would persist visibly after Cancel without this. - """ - stored = {"railing_type": "WALL_MOUNTED_HANDRAIL", "height": 1.0} - # Differs → not a no-op → cancel must take the real branch. - props = _FakeRailingProps( - railing_type="WALL_MOUNTED_HANDRAIL", - general={"railing_type": "WALL_MOUNTED_HANDRAIL", "height": 1.5}, - ) +# --------------------------------------------------------------------------- +# _RailingEditMixin._restore_viewport_after_cancel — per-type viewport-restore dispatch +# +# The parent's _cancel_one calls cls._restore_viewport_after_cancel whenever +# the draft differs from the stored pset. Railing's override branches on +# railing_type so WALL_MOUNTED_HANDRAIL reloads the high-poly Body +# representation rather than rebuilding the low-poly cylinder-segment preview. +# --------------------------------------------------------------------------- + + +def test_restore_viewport_wall_mounted_handrail_switches_representation(patched_railing): + """WALL_MOUNTED_HANDRAIL restore must call ``switch_representation`` with + the Body representation — the preview is viewport-only (low-poly cylinder) + and would persist visibly without the reload.""" + from bonsai.bim.module.model.railing import _RailingEditMixin + + props = _FakeRailingProps(railing_type="WALL_MOUNTED_HANDRAIL") obj = _make_obj(props) - patched_railing["tool"].Model.get_modeling_bbim_pset_data.return_value = { - "data_dict": {**stored, "path_data": {"verts": [], "edges": []}}, - } + patched_railing["tool"].Model.get_railing_props.return_value = props body_repr = mock.Mock(name="body_representation") patched_railing["ifcopenshell"].util.representation.get_representation.return_value = body_repr - cls, _element = _railing_test_subclass(props) - cls._cancel_one(obj, mock.Mock(name="context")) + _RailingEditMixin._restore_viewport_after_cancel(obj, mock.Mock(name="context")) - assert props.is_editing is False - # Must call switch_representation with the Body representation; must NOT - # call _update_modifier_bmesh (that's the FRAMELESS branch). patched_railing["bonsai"].core.geometry.switch_representation.assert_called_once() kwargs = patched_railing["bonsai"].core.geometry.switch_representation.call_args.kwargs assert kwargs["obj"] is obj assert kwargs["representation"] is body_repr - cls.bmesh_updates.assert_not_called() + # Must NOT fall through to the FRAMELESS bmesh-rebuild path. + patched_railing["update_bmesh"].assert_not_called() -def test_cancel_one_frameless_panel_runs_bmesh_preview(patched_railing): +def test_restore_viewport_frameless_panel_calls_module_bmesh_rebuild(patched_railing): """FRAMELESS_PANEL's bmesh IS the canonical mesh — there's no IFC - swept-disk solid to reload. Cancel must run the bmesh rebuild instead - of switch_representation, which would no-op or worse.""" - stored = {"railing_type": "FRAMELESS_PANEL", "height": 1.0, "thickness": 0.05} - props = _FakeRailingProps( - railing_type="FRAMELESS_PANEL", - general={"railing_type": "FRAMELESS_PANEL", "height": 1.0, "thickness": 0.08}, - ) + swept-disk solid to reload. The restore must delegate to the module-level + ``update_railing_modifier_bmesh`` rebuilder rather than swap representations.""" + from bonsai.bim.module.model.railing import _RailingEditMixin + + props = _FakeRailingProps(railing_type="FRAMELESS_PANEL") obj = _make_obj(props) - patched_railing["tool"].Model.get_modeling_bbim_pset_data.return_value = { - "data_dict": {**stored, "path_data": {"verts": [], "edges": []}}, - } + patched_railing["tool"].Model.get_railing_props.return_value = props + ctx = mock.Mock(name="context") - cls, _element = _railing_test_subclass(props) - cls._cancel_one(obj, mock.Mock(name="context")) + _RailingEditMixin._restore_viewport_after_cancel(obj, ctx) - assert props.is_editing is False - cls.bmesh_updates.assert_called_once_with(obj, mock.ANY) + patched_railing["update_bmesh"].assert_called_once_with(ctx) patched_railing["bonsai"].core.geometry.switch_representation.assert_not_called() diff --git a/src/bonsai/test/bim/module/model/test_railing_schematic.py b/src/bonsai/test/bim/module/model/test_railing_schematic.py index 270df8db78..7ba12a7a70 100644 --- a/src/bonsai/test/bim/module/model/test_railing_schematic.py +++ b/src/bonsai/test/bim/module/model/test_railing_schematic.py @@ -32,7 +32,7 @@ from bonsai.bim.module.drawing.gizmos import ( ) from bonsai.bim.module.model.railing import GizmoRailingSchematic -pytestmark = pytest.mark.railing +pytestmark = pytest.mark.model @pytest.fixture(autouse=True) @@ -167,10 +167,8 @@ def test_schematic_dim_visible_length_is_constant(): def test_schematic_no_compute_schematic_scale_override(): - """The constant-length design has no need for a scale factor. If a - subclass redefines ``_compute_schematic_scale``, it indicates the - scale-based proportional sizing was reintroduced — which is the design - we deliberately stepped away from.""" + """The constant-length schematic must not reintroduce scale-based + proportional sizing via a ``_compute_schematic_scale`` override.""" assert "_compute_schematic_scale" not in GizmoRailingSchematic.__dict__