Add clear_preview_state helper + DRY preview cleanup

Every preview operator (commit + cancel for both bend and wall
fillet) was inlining the same 3-4 line cleanup: set is_active to
False, zero every *_id IntProperty. The new clear_preview_state
helper in preview_base.py introspects bl_rna and applies that
contract generically — adopters become a single call. Two new tests
pin the contract: every *_id IntProperty zeroes, non-id fields stay.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-08 10:25:59 +02:00
parent 7b9af9f533
commit db7591a867
4 changed files with 66 additions and 14 deletions
+2 -6
View File
@@ -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"}
@@ -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], ...] = (
+2 -8
View File
@@ -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"}
@@ -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