chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,10 @@ def _test_import(module_name, loaded_modules):
|
||||
try:
|
||||
mod = __import__(module_name)
|
||||
except Exception:
|
||||
# Embedded WM cannot proceed to region drawing with missing startup
|
||||
# classes. Preserve the real error for its native startup boundary.
|
||||
if _sys.platform == "emscripten":
|
||||
raise
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
@@ -209,6 +213,8 @@ def _register_module_call(mod):
|
||||
try:
|
||||
register()
|
||||
except Exception:
|
||||
if _sys.platform == "emscripten":
|
||||
raise
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
@@ -376,6 +382,186 @@ def _on_exit():
|
||||
_unregister_module_call(mod)
|
||||
|
||||
|
||||
# The native WebEngine owns this transaction. These are the real Blender Python
|
||||
# registries, not replacement RNA objects. Keep strong references until native
|
||||
# callbacks/types have been removed, and never invoke add-on unregister hooks
|
||||
# while recovering a failed first desktop attach.
|
||||
_web_startup_state = None
|
||||
|
||||
|
||||
class _WebStartupState:
|
||||
def __init__(self):
|
||||
self.modules = _sys.modules.copy()
|
||||
self.path = _sys.path[:]
|
||||
self.registered = _registered_module_names[:]
|
||||
self.draw = {}
|
||||
self.tool_containers = {}
|
||||
self.tool_helpers = {}
|
||||
self.tool_classes = {}
|
||||
self.closing = False
|
||||
self.missing = object()
|
||||
self.module_flags = [
|
||||
(mod, {key: vars(mod).get(key, self.missing) for key in (
|
||||
"__addon_enabled__", "__addon_persistent__", "__template_enabled__",
|
||||
)})
|
||||
for mod in self.modules.values() if type(mod) is type(_sys)
|
||||
]
|
||||
self.module_bindings = []
|
||||
for name in self.modules:
|
||||
parent_name, _, child = name.rpartition(".")
|
||||
parent = self.modules.get(parent_name)
|
||||
if type(parent) is type(_sys):
|
||||
self.module_bindings.append((parent, child, vars(parent).get(child, self.missing)))
|
||||
self.addon_once = vars(_addon_utils).get("_initialize_once", self.missing)
|
||||
template = self.modules.get("bl_app_template_utils")
|
||||
self.template = None if template is None else (
|
||||
template, template._app_template.copy(), template._modules.copy(),
|
||||
)
|
||||
# Capture Blender's already registered toolbar consumers before an
|
||||
# extension can mutate a shared ToolDef.keymap list or group dictionary.
|
||||
# Do not expand dynamic tool generators or invoke extension callbacks.
|
||||
tool_module = self.modules.get("bl_ui.space_toolsystem_common")
|
||||
self.tool_def_type = None if tool_module is None else tool_module.ToolDef
|
||||
if tool_module is not None:
|
||||
for cls in tool_module.ToolSelectPanelHelper.__subclasses__():
|
||||
self.track_tool_helper(cls)
|
||||
|
||||
def ensure_open(self):
|
||||
if self.closing:
|
||||
raise RuntimeError("BLENDER_WEB_STARTUP_CLEANUP_ACTIVE")
|
||||
|
||||
def track_draw(self, cls):
|
||||
self.ensure_open()
|
||||
if cls not in self.draw:
|
||||
draw = cls.draw
|
||||
funcs = getattr(draw, "_draw_funcs", None)
|
||||
self.draw[cls] = (
|
||||
vars(cls).get("draw", self.missing), draw, funcs,
|
||||
None if funcs is None else funcs[:],
|
||||
)
|
||||
|
||||
def track_tool(self, tool_cls, tools):
|
||||
self.ensure_open()
|
||||
self.track_tool_container(tools)
|
||||
if tool_cls not in self.tool_classes:
|
||||
tool = vars(tool_cls).get("_bl_tool", self.missing)
|
||||
self.tool_classes[tool_cls] = tool
|
||||
self.track_tool_container(tool)
|
||||
|
||||
def track_tool_container(self, value):
|
||||
self.ensure_open()
|
||||
if type(value) is self.tool_def_type:
|
||||
self.track_tool_container(value.keymap)
|
||||
elif type(value) in {list, dict}:
|
||||
if id(value) in self.tool_containers:
|
||||
return
|
||||
self.tool_containers[id(value)] = (value, value.copy())
|
||||
for item in value.values() if type(value) is dict else value:
|
||||
self.track_tool_container(item)
|
||||
elif type(value) is tuple:
|
||||
for item in value:
|
||||
self.track_tool_container(item)
|
||||
|
||||
def track_tool_helper(self, cls):
|
||||
self.ensure_open()
|
||||
if cls in self.tool_helpers:
|
||||
return
|
||||
names = ("_tools", "_tool_group_active", "_has_keymap_data")
|
||||
self.tool_helpers[cls] = {name: vars(cls).get(name, self.missing) for name in names}
|
||||
for name in names:
|
||||
self.track_tool_container(getattr(cls, name, self.missing))
|
||||
|
||||
def restore_attribute(self, obj, name, value):
|
||||
if value is self.missing:
|
||||
if name in vars(obj):
|
||||
delattr(obj, name)
|
||||
else:
|
||||
setattr(obj, name, value)
|
||||
|
||||
def rollback_ui(self):
|
||||
self.closing = True
|
||||
# Restore the original list objects as callers may have cached them.
|
||||
for cls, (draw, resolved_draw, funcs, before) in self.draw.items():
|
||||
current_funcs = getattr(cls.draw, "_draw_funcs", None)
|
||||
if current_funcs is not None and current_funcs is not funcs:
|
||||
# A retained newly-created wrapper must not keep dispatching the
|
||||
# failed add-on even after the class's original draw is restored.
|
||||
current_funcs[:] = [resolved_draw]
|
||||
if funcs is not None:
|
||||
funcs[:] = before
|
||||
self.restore_attribute(cls, "draw", draw)
|
||||
for container, before in self.tool_containers.values():
|
||||
if type(container) is dict:
|
||||
container.clear()
|
||||
container.update(before)
|
||||
else:
|
||||
container[:] = before
|
||||
for cls, attributes in self.tool_helpers.items():
|
||||
for name, value in attributes.items():
|
||||
self.restore_attribute(cls, name, value)
|
||||
for cls, tool in self.tool_classes.items():
|
||||
self.restore_attribute(cls, "_bl_tool", tool)
|
||||
|
||||
def finish(self, commit):
|
||||
global _web_startup_state
|
||||
self.closing = True
|
||||
try:
|
||||
if not commit:
|
||||
for mod, flags in self.module_flags:
|
||||
for name, value in flags.items():
|
||||
self.restore_attribute(mod, name, value)
|
||||
self.restore_attribute(_addon_utils, "_initialize_once", self.addon_once)
|
||||
if self.template is not None:
|
||||
template, state, modules = self.template
|
||||
template._app_template.clear()
|
||||
template._app_template.update(state)
|
||||
template._modules.clear()
|
||||
template._modules.update(modules)
|
||||
elif (template := _sys.modules.get("bl_app_template_utils")) is not None:
|
||||
template._app_template.clear()
|
||||
template._app_template["id"] = ""
|
||||
template._modules.clear()
|
||||
_registered_module_names[:] = self.registered
|
||||
_sys.path[:] = self.path
|
||||
# Remove imports and parent-package bindings introduced by the
|
||||
# failed pass, without calling extension code or unloading bpy.
|
||||
for name, mod in tuple(_sys.modules.items()):
|
||||
if name not in self.modules:
|
||||
if type(mod) is type(_sys):
|
||||
for flag in ("__addon_enabled__", "__template_enabled__"):
|
||||
if flag in vars(mod):
|
||||
setattr(mod, flag, False)
|
||||
parent_name, _, child = name.rpartition(".")
|
||||
parent = _sys.modules.get(parent_name)
|
||||
if type(parent) is type(_sys) and vars(parent).get(child) is mod:
|
||||
delattr(parent, child)
|
||||
del _sys.modules[name]
|
||||
_sys.modules.update(self.modules)
|
||||
for parent, child, value in self.module_bindings:
|
||||
self.restore_attribute(parent, child, value)
|
||||
finally:
|
||||
# Release snapshots while the native reset/context guards still
|
||||
# apply. Reentrant menu/tool registration remains rejected here.
|
||||
self.draw.clear()
|
||||
self.tool_containers.clear()
|
||||
self.tool_helpers.clear()
|
||||
self.tool_classes.clear()
|
||||
self.module_flags.clear()
|
||||
self.module_bindings.clear()
|
||||
self.modules.clear()
|
||||
self.template = None
|
||||
_web_startup_state = None
|
||||
|
||||
|
||||
def _web_startup_begin():
|
||||
global _web_startup_state
|
||||
if _sys.platform != "emscripten" or _web_startup_state is not None:
|
||||
raise RuntimeError("BLENDER_WEB_STARTUP_CONTEXT_INVALID")
|
||||
state = _WebStartupState()
|
||||
_web_startup_state = state
|
||||
return state
|
||||
|
||||
|
||||
def load_scripts_extensions(*, reload_scripts=False):
|
||||
"""
|
||||
Load extensions scripts (add-ons and app-templates)
|
||||
@@ -859,13 +1045,17 @@ def keyconfig_init():
|
||||
|
||||
# Load the default key configuration.
|
||||
filepath = preset_find(default_config, "keyconfig")
|
||||
keyconfig_set(filepath)
|
||||
loaded = keyconfig_set(filepath)
|
||||
if not loaded and _sys.platform == "emscripten":
|
||||
raise RuntimeError("Unable to load default key configuration: " + str(filepath))
|
||||
|
||||
# Set the active key configuration if different.
|
||||
if default_config != active_config:
|
||||
filepath = preset_find(active_config, "keyconfig")
|
||||
if filepath:
|
||||
keyconfig_set(filepath)
|
||||
loaded = keyconfig_set(filepath)
|
||||
if not loaded and _sys.platform == "emscripten":
|
||||
raise RuntimeError("Unable to load active key configuration: " + str(filepath))
|
||||
|
||||
|
||||
def keyconfig_set(filepath, *, report=None):
|
||||
@@ -1088,6 +1278,8 @@ def register_tool(tool_cls, *, after=None, separator=False, group=False):
|
||||
if cls is None:
|
||||
raise Exception("Space type {!r} has no toolbar".format(space_type))
|
||||
tools = cls._tools[context_mode]
|
||||
if _web_startup_state is not None:
|
||||
_web_startup_state.track_tool(tool_cls, tools)
|
||||
|
||||
# First sanity check
|
||||
from bpy.types import WorkSpaceTool
|
||||
@@ -1206,6 +1398,8 @@ def unregister_tool(tool_cls):
|
||||
if cls is None:
|
||||
raise Exception("Space type {!r} has no toolbar".format(space_type))
|
||||
tools = cls._tools[context_mode]
|
||||
if _web_startup_state is not None:
|
||||
_web_startup_state.track_tool(tool_cls, tools)
|
||||
|
||||
tool_def = tool_cls._bl_tool
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user