mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-17 02:49:12 +00:00
Extract transform-modal gate + viewport helpers to tool.Blender
The transform-modal active check (Bonsai keymap macros + Blender's TRANSFORM_OT_* family) was a module-local helper in drawing/gizmos.py used by per-gizmo poll callbacks. It needs to be shared with other features that gate per-frame side effects on whether a drag is in progress (clip box plane re-arming, future modal-aware decorators). Move BONSAI_TRANSFORM_MACROS and the gate into tool.Blender as is_transform_modal_active classmethod; widen its window scan to all WM windows for callers without a window-bound context (depsgraph callbacks). Leave a thin module-local alias in drawing/gizmos.py so AST scans and existing call sites stay decoupled from the helper's home module. Also add generic Blender helpers needed by the clip-box feature (reusable by any future feature): - iter_view3d_regions: yield (area, region, region_3d) for every WINDOW region in every 3D viewport — for clip-plane / draw-handler fanout. - get_or_create_collection: idempotent named-collection lookup + link to a scene. - is_in_edit_mode: True iff the active object is in any EDIT_* mode — for features that need to suspend per-tick work during vert/edge/face manipulation. - serialize_matrix / deserialize_matrix / hash_matrix: round-trip a 4x4 matrix as a 16-float CSV string for IFC pset persistence + a matching hash for cache keys. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -159,40 +159,13 @@ _SPECIAL = {"=", " "} # Formula prefix, spaces
|
||||
NUMERIC_INPUT_CHARS = _DIGITS | _OPERATORS | _METRIC_UNITS | _IMPERIAL_UNITS | _SPECIAL
|
||||
|
||||
|
||||
_BONSAI_TRANSFORM_MACROS = frozenset(
|
||||
{
|
||||
# Bonsai overrides Blender's default move/duplicate keymaps with
|
||||
# macros that wrap TRANSFORM_OT_translate. While a macro is the outer
|
||||
# modal entry, the inner TRANSFORM_OT_translate does not surface in
|
||||
# window.modal_operators — the macro's own idname does. The
|
||||
# ``BIM_OT_`` prefix is what Blender returns from ``bl_idname`` at
|
||||
# runtime (the class declaration uses the dotted ``bim.`` form).
|
||||
"BIM_OT_override_move_macro", # G key
|
||||
"BIM_OT_override_object_duplicate_move_macro", # Shift+D
|
||||
"BIM_OT_override_object_duplicate_move_linked_macro", # Alt+D
|
||||
"BIM_OT_object_duplicate_move_linked_aggregate_macro", # Ctrl+Shift+D
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_transform_modal_active(context) -> bool:
|
||||
"""True iff a Blender transform modal (G/R/S and siblings, including
|
||||
Bonsai's macro overrides) is currently driving per-frame ``matrix_world``
|
||||
updates. Reads ``window.modal_operators`` — the Blender 4.2+ collection of
|
||||
running modal operators. Parametric gizmo groups gate poll + draw_prepare
|
||||
on this so they hide for the duration of the drag instead of sliding
|
||||
off-cursor as the matrix updates each frame."""
|
||||
window = getattr(context, "window", None)
|
||||
if window is None:
|
||||
return False
|
||||
modal_ops = getattr(window, "modal_operators", None)
|
||||
if not modal_ops:
|
||||
return False
|
||||
for op in modal_ops:
|
||||
idname = op.bl_idname
|
||||
if idname.startswith("TRANSFORM_OT_") or idname in _BONSAI_TRANSFORM_MACROS:
|
||||
return True
|
||||
return False
|
||||
"""Module-local alias for ``tool.Blender.is_transform_modal_active``.
|
||||
|
||||
Preserved as a name so AST scans and call sites in this file stay
|
||||
decoupled from the helper's home module.
|
||||
"""
|
||||
return tool.Blender.is_transform_modal_active(context)
|
||||
|
||||
|
||||
def _hide_all_non_modal_gizmos(group) -> None:
|
||||
|
||||
@@ -30,7 +30,15 @@ import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
import types
|
||||
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence, Sized
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Generator,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Sequence,
|
||||
Sized,
|
||||
)
|
||||
from datetime import datetime
|
||||
from functools import cache, lru_cache
|
||||
from pathlib import Path
|
||||
@@ -575,6 +583,120 @@ class Blender(bonsai.core.tool.Blender):
|
||||
else:
|
||||
decorator_cls.uninstall()
|
||||
|
||||
# Bonsai overrides Blender's default move/duplicate keymaps with macros
|
||||
# that wrap TRANSFORM_OT_translate. While a macro is the outer modal
|
||||
# entry, the inner TRANSFORM_OT_translate does not surface in
|
||||
# window.modal_operators — the macro's own idname does. The ``BIM_OT_``
|
||||
# prefix is what Blender returns from ``bl_idname`` at runtime (the
|
||||
# class declaration uses the dotted ``bim.`` form).
|
||||
BONSAI_TRANSFORM_MACROS: frozenset[str] = frozenset(
|
||||
{
|
||||
"BIM_OT_override_move_macro", # G key
|
||||
"BIM_OT_override_object_duplicate_move_macro", # Shift+D
|
||||
"BIM_OT_override_object_duplicate_move_linked_macro", # Alt+D
|
||||
"BIM_OT_object_duplicate_move_linked_aggregate_macro", # Ctrl+Shift+D
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_transform_modal_active(cls, context: bpy.types.Context) -> bool:
|
||||
"""True iff a Blender transform modal (G/R/S and siblings, including
|
||||
Bonsai's macro overrides) is currently driving per-frame
|
||||
``matrix_world`` updates. Reads ``window.modal_operators`` — the
|
||||
Blender 4.2+ collection of running modal operators. Callers gate
|
||||
per-frame side effects (gizmo positioning, IFC persistence, etc.)
|
||||
on this so they don't fire during the drag.
|
||||
|
||||
Falls back to scanning every window in the window manager when
|
||||
``context.window`` is ``None`` — depsgraph callbacks run with a
|
||||
limited context where ``context.window`` is typically missing,
|
||||
but the modal is still active on one of the WM's windows.
|
||||
"""
|
||||
window = getattr(context, "window", None)
|
||||
if window is not None and getattr(window, "modal_operators", None):
|
||||
windows = [window]
|
||||
else:
|
||||
wm = getattr(context, "window_manager", None) or bpy.context.window_manager
|
||||
if wm is None:
|
||||
return False
|
||||
windows = list(wm.windows)
|
||||
for w in windows:
|
||||
modal_ops = getattr(w, "modal_operators", None)
|
||||
if not modal_ops:
|
||||
continue
|
||||
for op in modal_ops:
|
||||
idname = op.bl_idname
|
||||
if idname.startswith("TRANSFORM_OT_") or idname in cls.BONSAI_TRANSFORM_MACROS:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_in_edit_mode(cls, context: Optional[bpy.types.Context] = None) -> bool:
|
||||
"""True iff the active object is in any edit-style mode.
|
||||
|
||||
Catches every ``EDIT_*`` variant (mesh, curve, armature,
|
||||
metaball, lattice, surface, text, grease pencil). Defaults to
|
||||
``OBJECT`` when the mode attribute is missing so background-mode
|
||||
callers (no UI context) don't false-positive.
|
||||
"""
|
||||
ctx = context if context is not None else bpy.context
|
||||
mode = getattr(ctx, "mode", "OBJECT")
|
||||
return mode.startswith("EDIT_")
|
||||
|
||||
@classmethod
|
||||
def iter_view3d_regions(cls) -> Iterator[tuple[bpy.types.Area, bpy.types.Region, bpy.types.RegionView3D]]:
|
||||
"""Yield ``(area, region, region_3d)`` for every WINDOW region in every 3D viewport.
|
||||
|
||||
Useful for features that need to act on every visible 3D viewport
|
||||
(clip planes, draw handlers, region redraw fanout). Empty
|
||||
generator when ``bpy.context.screen`` is unavailable (shutdown,
|
||||
background mode without a screen).
|
||||
"""
|
||||
screen = getattr(getattr(bpy, "context", None), "screen", None)
|
||||
if screen is None:
|
||||
return
|
||||
for area in screen.areas:
|
||||
if area.type != "VIEW_3D":
|
||||
continue
|
||||
for region in area.regions:
|
||||
if region.type != "WINDOW":
|
||||
continue
|
||||
region_3d = getattr(region, "data", None)
|
||||
if region_3d is None:
|
||||
continue
|
||||
yield area, region, region_3d
|
||||
|
||||
@classmethod
|
||||
def get_or_create_collection(cls, scene: bpy.types.Scene, name: str) -> bpy.types.Collection:
|
||||
"""Return the named collection, creating + linking it to ``scene`` if absent."""
|
||||
collection = bpy.data.collections.get(name)
|
||||
if collection is None:
|
||||
collection = bpy.data.collections.new(name)
|
||||
scene.collection.children.link(collection)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
def serialize_matrix(cls, matrix: Matrix) -> str:
|
||||
"""Serialize a 4x4 matrix as a 16-float comma-separated string.
|
||||
|
||||
Round-trip pair with :meth:`deserialize_matrix`. Used for storing
|
||||
a matrix in an IFC pset string property without losing precision
|
||||
(``%.9g`` carries ~9 significant digits, enough for ``float32``
|
||||
round-trip).
|
||||
"""
|
||||
return ",".join(f"{matrix[r][c]:.9g}" for r in range(4) for c in range(4))
|
||||
|
||||
@classmethod
|
||||
def deserialize_matrix(cls, text: str) -> Matrix:
|
||||
"""Inverse of :meth:`serialize_matrix`."""
|
||||
floats = [float(v) for v in text.split(",")]
|
||||
return Matrix([tuple(floats[r * 4 : r * 4 + 4]) for r in range(4)])
|
||||
|
||||
@classmethod
|
||||
def hash_matrix(cls, matrix: Matrix) -> int:
|
||||
"""Hash a 4x4 matrix by its 16 floats. Useful as a cache key."""
|
||||
return hash(tuple(matrix[r][c] for r in range(4) for c in range(4)))
|
||||
|
||||
@classmethod
|
||||
def is_view_top_down(cls, context: bpy.types.Context, threshold: float = 0.9659) -> bool:
|
||||
"""True when the viewport camera is looking ~straight down (or up) the world Z axis.
|
||||
|
||||
Reference in New Issue
Block a user