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

@@ -0,0 +1,78 @@
"""File -> Export jobs using Blender's registered exporters and RNA validation."""
import hashlib
import json
from pathlib import Path
import shutil
import zipfile
import bpy
from server_operator_properties import validated_operator_properties
FORMATS = {
'GLB': ('export_scene.gltf', 'glb', 'use_selection', {'export_format': 'GLB'}),
'GLTF': ('export_scene.gltf', 'gltf', 'use_selection', {'export_format': 'GLTF_SEPARATE', 'export_texture_dir': 'textures'}),
'FBX': ('export_scene.fbx', 'fbx', 'use_selection', {'path_mode': 'COPY', 'embed_textures': True}),
'OBJ': ('wm.obj_export', 'obj', 'export_selected_objects', {'path_mode': 'COPY'}),
'STL': ('wm.stl_export', 'stl', 'export_selected_objects', {}),
'PLY': ('wm.ply_export', 'ply', 'export_selected_objects', {}),
'USD': ('wm.usd_export', 'usdc', 'selected_objects_only', {'export_textures': True, 'relative_paths': True}),
'ALEMBIC': ('wm.alembic_export', 'abc', 'selected', {}),
}
def export_scene(root, spec, artifact, output):
identifier, extension, selection_key, defaults = FORMATS[spec['format']]
family, name = identifier.split('.')
if family == 'export_scene':
import addon_utils
addon_utils.enable('io_scene_gltf2' if name == 'gltf' else 'io_scene_fbx', default_set=False, persistent=False)
operator = getattr(getattr(bpy.ops, family), name)
try:
rna = operator.get_rna_type()
except RuntimeError as error:
raise RuntimeError('NATIVE_EXPORTER_UNAVAILABLE: ' + identifier) from error
destination = root / 'export'
destination.mkdir()
filename = destination / ('scene.' + extension)
supplied = spec.get('properties', {})
# Output locations and batch modes belong to this job, never to caller input.
blocked = {'filepath', 'directory', 'filename', 'files', 'check_existing',
'filter_glob', 'batch_mode', 'use_batch_own_dir', 'as_background_job',
'export_format', 'export_texture_dir', 'export_animation',
'use_selection', 'export_selected_objects', 'selected_objects_only', 'selected'}
for key in supplied:
if key in blocked or any(token in key.lower() for token in ('path', 'directory', 'filename')):
raise RuntimeError('NATIVE_EXPORT_MANAGED_PROPERTY: ' + key)
values = dict(defaults, **supplied)
values.update(filepath=str(filename), **{selection_key: spec['selectedOnly']})
if 'check_existing' in rna.properties: values['check_existing'] = False
if 'as_background_job' in rna.properties: values['as_background_job'] = False
if spec['selectedOnly'] and not any(obj.select_get() for obj in bpy.context.view_layer.objects):
raise RuntimeError('NATIVE_EXPORT_SELECTION_EMPTY')
values = validated_operator_properties(operator, values)
if not operator.poll(): raise RuntimeError('NATIVE_EXPORT_CONTEXT_UNAVAILABLE: ' + identifier)
state = operator('EXEC_DEFAULT', **values)
if 'FINISHED' not in state or not filename.is_file() or filename.stat().st_size == 0:
raise RuntimeError('NATIVE_EXPORT_NO_OUTPUT: ' + identifier)
files = []
for file in sorted(destination.rglob('*')):
if file.is_symlink(): raise RuntimeError('NATIVE_EXPORT_SYMLINK')
if file.is_file():
files.append({'name': file.relative_to(destination).as_posix(), 'bytes': file.stat().st_size,
'sha256': hashlib.sha256(file.read_bytes()).hexdigest()})
if len(files) > 10000 or sum(item['bytes'] for item in files) > 128 * 1024 * 1024:
raise RuntimeError('NATIVE_EXPORT_PACKAGE_BUDGET')
receipt = {'format': spec['format'], 'operator': identifier, 'blenderVersion': bpy.app.version_string,
'selectedOnly': spec['selectedOnly'], 'files': files}
(destination / 'export-receipt.json').write_text(json.dumps(receipt, indent=2), encoding='utf8')
package = root / 'export-package.zip'
with zipfile.ZipFile(package, 'w', zipfile.ZIP_DEFLATED) as bundle:
for file in sorted(destination.rglob('*')):
if file.is_file(): bundle.write(file, file.relative_to(destination).as_posix())
artifact(package, 'application/zip')
# Multi-file formats must be downloaded as the package to retain relative resources.
if len(files) == 1:
standalone = root / filename.name
shutil.copyfile(filename, standalone)
artifact(standalone, 'model/gltf-binary' if extension == 'glb' else 'application/octet-stream')
output.write(json.dumps(receipt, indent=2) + '\n')