diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index c110734ef7..084499c956 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -1373,9 +1373,7 @@ class FinishBendPreview(bpy.types.Operator): self.report({"ERROR"}, str(exc)) return {"CANCELLED"} if "FINISHED" in result: - props.is_active = False - props.start_segment_id = 0 - props.end_segment_id = 0 + preview_base.clear_preview_state(props) return result @@ -1393,7 +1391,5 @@ class CancelBendPreview(bpy.types.Operator): props = preview_base.get_preview_props(context, "bend") if props is None or not props.is_active: return {"CANCELLED"} - props.is_active = False - props.start_segment_id = 0 - props.end_segment_id = 0 + preview_base.clear_preview_state(props) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/model/preview_base.py b/src/bonsai/bonsai/bim/module/model/preview_base.py index 13cde4e68a..52c8044ba1 100644 --- a/src/bonsai/bonsai/bim/module/model/preview_base.py +++ b/src/bonsai/bonsai/bim/module/model/preview_base.py @@ -163,6 +163,19 @@ def sync_uncommitted_moves(objects: list) -> None: tool.Geometry.commit_placement_if_moved(obj, apply_scale=False) +def clear_preview_state(props: bpy.types.PropertyGroup) -> None: + """Reset a preview PropertyGroup to its idle state on commit / cancel. + + Sets ``is_active`` to False and zeros every ``IntProperty`` whose name + ends in ``_id`` (the entity-reference convention every preview follows). + Other fields are left at their last value — defaults are re-applied on + the next enable, so leaving them alone avoids a redundant write.""" + props.is_active = False + for name, rna in props.bl_rna.properties.items(): + if name.endswith("_id") and rna.type == "INT": + setattr(props, name, 0) + + # --- Esc dispatch ------------------------------------------------------------ PREVIEW_CANCEL_OPS: tuple[tuple[str, str], ...] = ( diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index e37e8eb751..169f94f01a 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -2962,10 +2962,7 @@ class FinishWallFilletPreview(bpy.types.Operator): self.report({"ERROR"}, str(exc)) return {"CANCELLED"} if "FINISHED" in result: - props.is_active = False - props.wall_a_id = 0 - props.wall_b_id = 0 - props.editing_corner_id = 0 + preview_base.clear_preview_state(props) return result @@ -2983,10 +2980,7 @@ class CancelWallFilletPreview(bpy.types.Operator): props = preview_base.get_preview_props(context, "wall_fillet") if props is None or not props.is_active: return {"CANCELLED"} - props.is_active = False - props.wall_a_id = 0 - props.wall_b_id = 0 - props.editing_corner_id = 0 + preview_base.clear_preview_state(props) return {"FINISHED"} diff --git a/src/bonsai/test/bim/module/model/test_preview_base.py b/src/bonsai/test/bim/module/model/test_preview_base.py index b784ab280e..9cc602e0b6 100644 --- a/src/bonsai/test/bim/module/model/test_preview_base.py +++ b/src/bonsai/test/bim/module/model/test_preview_base.py @@ -139,6 +139,55 @@ class TestActivationCycle: assert props.is_active is False, f"discard_pending_previews left '{attr}' active" +class TestClearPreviewState: + """``clear_preview_state`` is the shared cleanup routine every preview + operator calls on commit / cancel. The contract is: ``is_active`` flips + to False, every ``*_id`` IntProperty zeroes, everything else stays.""" + + def test_clears_is_active_and_id_fields_on_real_property_groups(self): + from bonsai.bim.module.model.preview_base import clear_preview_state + + registered = _registered_previews() + if not registered: + pytest.skip("No previews wired in this build — registry-only entries") + + for attr, _, props in registered: + # Seed every *_id IntProperty with a non-zero sentinel and flip + # the activity flag so the helper has something to clear. + id_fields = [ + name for name, rna in props.bl_rna.properties.items() if name.endswith("_id") and rna.type == "INT" + ] + assert id_fields, f"Preview '{attr}' has no *_id IntProperty — registry shape changed" + for name in id_fields: + setattr(props, name, 42) + props.is_active = True + + clear_preview_state(props) + + assert props.is_active is False, f"Preview '{attr}' is_active not cleared" + for name in id_fields: + assert getattr(props, name) == 0, f"Preview '{attr}' field '{name}' not zeroed" + + def test_leaves_non_id_fields_untouched(self): + """Non-``*_id`` fields (FloatProperty params like ``radius``, + ``start_length``) must survive the reset — they re-seed on the next + enable, so untouching them here avoids a redundant write.""" + from bonsai.bim.module.model.preview_base import clear_preview_state + + bend = getattr(_preview_umbrella(), "bend", None) + if bend is None: + pytest.skip("Bend preview not wired in this build") + + bend.is_active = True + bend.start_length = 0.42 + bend.radius = 0.99 + clear_preview_state(bend) + + assert bend.is_active is False + assert bend.start_length == pytest.approx(0.42) + assert bend.radius == pytest.approx(0.99) + + class TestSaveOnDiscardWired: """Pins that the SaveProject operator clears preview state before writing the IFC file — a stuck is_active flag persisted through the save would