mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Add parametric-draft undo-resync registry
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.
This commit is contained in:
committed by
Thomas Krijnen
parent
aba9986628
commit
dbb6cff723
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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
|
||||
Reference in New Issue
Block a user