From dbb6cff723c84b4687896abaaf79f7591894615e Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Wed, 27 May 2026 15:56:19 +0200 Subject: [PATCH] Add parametric-draft undo-resync registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ctrl+Z / Ctrl+Shift+Z on an in-progress parametric draft (wall / stair / roof) used to leave the preview mesh frozen in its pre-undo shape — the IFC mutation rolls back but the bmesh built from draft props doesn't repaint. Add a registry of per-type regenerator functions (``UNDO_REGENERATORS``) that re-build each type's preview mesh from its current props. The dispatcher ``resync_parametric_drafts_after_undo`` walks all objects, skips any without an active parametric edit, looks up the regenerator by feature name, and calls it. Tagged 3D viewports for redraw. Types without an entry (door / window / railing / etc.) are intentionally absent — they're IFC-derived, so the undo's representation rollback + next-frame refresh already repaints correctly without a draft-side regenerator. Undo/redo wiring is self-installed by ``bonsai.bim.parametric_lifecycle``: a ``@persistent`` ``_resync_on_undo`` callback dispatches into the registry, and ``install_parametric_lifecycle_handlers()`` / ``uninstall_parametric_lifecycle_handlers()`` append/remove it from ``bpy.app.handlers.undo_post`` and ``redo_post``. ``bim/__init__.py``'s ``register()`` calls the install function *after* the central ``handler.undo_post`` / ``redo_post`` appends so the regenerators see restored IFC state — ``bpy.app.handlers`` fire in append order. ``handler.py`` itself stays ignorant of the parametric subsystem. The lazy function-local imports in each regenerator break the addon-load cycle — ``bonsai.bim.parametric_lifecycle`` loads before ``bim/module/model/*``. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/__init__.py | 5 +- src/bonsai/bonsai/bim/parametric_lifecycle.py | 90 +++++++++++++++++++ src/bonsai/test/bim/test_addon_lifecycle.py | 60 ------------- 3 files changed, 94 insertions(+), 61 deletions(-) delete mode 100644 src/bonsai/test/bim/test_addon_lifecycle.py diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index b1e105a079..d9055d1f79 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -27,7 +27,7 @@ import bpy import bpy.utils.previews from bpy_extras.io_utils import ExportHelper, ImportHelper -from . import handler, operator, prop, ui +from . import handler, operator, parametric_lifecycle, prop, ui def _parametric_gizmo_preference_classes() -> list[type]: @@ -283,6 +283,8 @@ def register(): bpy.app.handlers.depsgraph_update_post.append(on_register) bpy.app.handlers.undo_post.append(handler.undo_post) bpy.app.handlers.redo_post.append(handler.redo_post) + # Must follow the two appends above so regenerators see restored IFC state. + parametric_lifecycle.install_parametric_lifecycle_handlers() bpy.app.handlers.load_post.append(handler.load_post) bpy.app.handlers.load_post.append(handler.loadIfcStore) bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) @@ -340,6 +342,7 @@ def unregister(): unregister_classes(classes) + parametric_lifecycle.uninstall_parametric_lifecycle_handlers() bpy.app.handlers.load_post.remove(handler.load_post) bpy.app.handlers.load_post.remove(handler.loadIfcStore) del bpy.types.Scene.BIMProperties diff --git a/src/bonsai/bonsai/bim/parametric_lifecycle.py b/src/bonsai/bonsai/bim/parametric_lifecycle.py index da95b9d256..414f11173b 100644 --- a/src/bonsai/bonsai/bim/parametric_lifecycle.py +++ b/src/bonsai/bonsai/bim/parametric_lifecycle.py @@ -70,10 +70,12 @@ free of such imports and lives separately in ``tool/parametric.py``.""" from __future__ import annotations import json +from collections.abc import Callable from typing import TYPE_CHECKING, ClassVar import bpy import ifcopenshell.util.element +from bpy.app.handlers import persistent import bonsai.core.geometry import bonsai.tool as tool @@ -375,3 +377,91 @@ class PathPreservingEditMixin(ParametricEditMixinBase): for obj in self._iter_targets(context): self._cancel_one(obj, context) return {"FINISHED"} + + +# --- Undo-resync registry ---------------------------------------------------- +# +# Per-type regenerators called from ``resync_parametric_drafts_after_undo`` +# (wired into ``bim/handler.py:undo_post`` and ``redo_post``) so the preview +# mesh of an in-progress parametric draft repaints after Ctrl+Z / Ctrl+Shift+Z. +# +# Each regenerator is a one-line lazy-import + call. Lazy imports because +# ``bonsai.bim.parametric_lifecycle`` loads before ``bim/module/model/*`` +# at addon enable; a module-level import would cycle. Each function-local +# import lands at first call, after the feature module has registered. +# +# Types with no entry — door, window, railing, etc. — are IFC-derived: undo +# of an IFC mutation already restores the entity, and ``switch_representation`` +# repaints the mesh as a side effect of the next refresh. They don't need a +# bespoke preview regenerator. + + +def _wall_undo_regenerator(obj: bpy.types.Object) -> None: + from bonsai.bim.module.model.wall import regenerate_wall_mesh_from_props + + regenerate_wall_mesh_from_props(obj) + + +def _stair_undo_regenerator(obj: bpy.types.Object) -> None: + from bonsai.bim.module.model.stair import regenerate_stair_mesh + + regenerate_stair_mesh(obj) + + +def _roof_undo_regenerator(obj: bpy.types.Object) -> None: + from bonsai.bim.module.model.roof import update_roof_modifier_bmesh + + update_roof_modifier_bmesh(obj) + + +UNDO_REGENERATORS: dict[str, Callable[[bpy.types.Object], None]] = { + "wall": _wall_undo_regenerator, + "stair": _stair_undo_regenerator, + "roof": _roof_undo_regenerator, +} + + +def resync_parametric_drafts_after_undo() -> None: + """Re-render preview meshes for every parametric draft currently active. + + Walks all objects, skips any not in a registered parametric edit, + dispatches to the per-type regenerator in ``UNDO_REGENERATORS``. A type + without an entry is left alone — its preview is either already correct + (IFC-derived) or has no draft preview mesh.""" + for obj in bpy.data.objects: + feature = tool.Parametric.is_object_editing(obj) + if feature is None: + continue + regenerator = UNDO_REGENERATORS.get(feature.name) + if regenerator is None: + continue + regenerator(obj) + screen = getattr(bpy.context, "screen", None) + if screen is not None: + for area in screen.areas: + if area.type == "VIEW_3D": + area.tag_redraw() + + +@persistent +def _resync_on_undo(scene: bpy.types.Scene) -> None: + resync_parametric_drafts_after_undo() + + +def install_parametric_lifecycle_handlers() -> None: + """Append the undo-resync callback to undo_post and redo_post; idempotent. + + Caller must invoke this AFTER appending the central undo/redo handlers so + regenerators see restored IFC state — bpy.app.handlers fire in append order.""" + for hook in (bpy.app.handlers.undo_post, bpy.app.handlers.redo_post): + if _resync_on_undo not in hook: + hook.append(_resync_on_undo) + + +def uninstall_parametric_lifecycle_handlers() -> None: + for hook in (bpy.app.handlers.undo_post, bpy.app.handlers.redo_post): + try: + hook.remove(_resync_on_undo) + except ValueError: + pass +>>>>>>> 8e305588d (fixup! Add parametric-draft undo-resync registry + handler hooks) diff --git a/src/bonsai/test/bim/test_addon_lifecycle.py b/src/bonsai/test/bim/test_addon_lifecycle.py deleted file mode 100644 index 16ea0b9f0c..0000000000 --- a/src/bonsai/test/bim/test_addon_lifecycle.py +++ /dev/null @@ -1,60 +0,0 @@ -# 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 . -# -# This file was generated with the assistance of an AI coding tool. - -"""Addon-load smoke for ``bonsai``. - -Pins the registration/unregistration cycle as a runnable contract. The cycle -exercises every ``register()`` site across ``bim/__init__.py``'s modules dict, -every ``PointerProperty`` attachment, every gizmo-prefs auto-registration, and -every ``bpy.app.handlers`` install. A regression in any of those surfaces here -as an exception with a traceback that points at the failing site, instead of -the silent ``addon failed to enable`` users see in a fresh Blender.""" - -import types - -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)") - - -def test_addon_unregister_then_register_does_not_raise(): - """Running the suite has already enabled the addon. Cycle through one - unregister + register to exercise both halves, then leave the addon - enabled so downstream tests in the same Blender session keep working.""" - import bonsai - - bonsai.unregister() - try: - bonsai.register() - except Exception: - # Re-raise after attempting to leave the session in a usable state for - # any tests that run after this one. - try: - bonsai.register() - except Exception: - pass - raise