chore: checkpoint web Blender migration source, tools and docs
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

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:
mes123456
2026-11-10 11:18:01 +08:00
parent 122142c26c
commit f3e21c6b98
6887 changed files with 257154 additions and 12294 deletions

View File

@@ -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: