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

@@ -4,7 +4,7 @@ import { parseCompositorGraph, type CompositorGraphIR } from "./compositor";
import { parseSequencerTimeline, type SequencerTimelineIR } from "./sequencer";
import { parseTrackingMaskProject, type TrackingMaskProjectIR } from "./tracking-mask";
import { normalizeProjectAssetPath } from "./asset-path";
import { parseEditorWorkflow, type EditorWorkflowIR } from "./editor-workflow";
import { parseEditorWorkflow, type EditorWorkflowIR, type GraphInterpolation } from "./editor-workflow";
import { parseScriptSourceInventory, type ScriptSourceInventoryIR } from "./scripting-platform";
import { parsePhysicsSimulationManifest, type PhysicsSimulationManifestIR } from "./physics-simulation";
import { GEOMETRY_NODE_GRAPH_BUDGET, parseGeometryNodeGraph, type GeometryNodeGraphIR } from "./geometry-nodes";
@@ -31,6 +31,8 @@ export type SceneNodeType =
export interface SceneTransformIR {
translation: [number, number, number];
rotationEuler: [number, number, number];
rotationQuaternion?: [number, number, number, number];
rotationAxisAngle?: [number, number, number, number];
scale: [number, number, number];
rotationMode: number;
}
@@ -38,6 +40,8 @@ export interface SceneTransformIR {
export interface SceneNodeIR {
id: string;
name: string;
/** Native Library ID name within this Main; null is local, absent is legacy. */
sourceLibrary?: string | null;
type: SceneNodeType;
parentId: string | null;
dataId: string | null;
@@ -107,6 +111,11 @@ export interface DriverIR {
}
export interface ModifierIR {
editableParameters?: Array<{ key: string; kind: "boolean" | "number" | "enum" | "enum_set" | "object" | "texture" | "cachefile" | "curve_mapping" | "string" | "profile"; value: unknown;
suggestionsOnly?: boolean; maxLength?: number; unit?: 'ANGLE';
label?: string; description?: string; editable?: boolean; nullable?: boolean; visible?: boolean; enabled?: boolean;
integer: boolean; length: number; minimum?: number; maximum?: number;
items?: Array<{ id: string; label: string }> }>;
uuid: string;
type: string;
name: string;
@@ -183,6 +192,7 @@ export interface UVLayerIR {
export interface MeshSummaryIR {
id: string;
name: string;
sourceLibrary?: string | null;
vertexCount: number;
edgeCount: number;
faceCount: number;
@@ -223,6 +233,8 @@ export interface MaterialNodeIR {
id: string;
type: "RGB" | "VALUE" | "MATH" | "PRINCIPLED" | "IMAGE_TEXTURE" | "NORMAL_MAP" | "OUTPUT" | "UNSUPPORTED";
name: string;
location?: [number, number];
sockets?: Array<{id:string;name:string;direction:'INPUT'|'OUTPUT';dataType:'VALUE'|'VECTOR'|'COLOR'|'BOOLEAN'|'SHADER'|'UNSUPPORTED';value?:number|boolean|number[];linked:boolean;integer?:boolean}>;
imageId?: string | null;
defaultValue?: number[];
properties?: { operation?: "ADD" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" | "MINIMUM" | "MAXIMUM" };
@@ -238,6 +250,7 @@ export interface MaterialLinkIR {
export interface MaterialIR {
id: string;
name: string;
sourceLibrary?: string | null;
baseColor: [number, number, number, number];
roughness: number;
metallic: number;
@@ -261,6 +274,8 @@ export interface MaterialIR {
export interface CameraIR {
id: string;
name: string;
/** Saved Library source; linked camera data remains read-only. */
sourceLibrary?: string | null;
projection: "PERSPECTIVE" | "ORTHOGRAPHIC" | "PANORAMIC" | "CUSTOM";
lensMm: number;
sensorWidthMm: number;
@@ -323,7 +338,7 @@ export interface WorldIR {
color: [number, number, number];
exposure: number;
environmentImageId?: string | null;
environmentStrength?: number;
environmentStrength?: number | null;
environmentRotation?: number;
ambientOcclusionDistance?: number;
ambientOcclusionEnergy?: number;
@@ -540,6 +555,8 @@ export interface NonMeshVolumePropertiesIR {
export interface NonMeshDataIR {
id: string;
name: string;
/** Saved Library ID of legacy Curve data, independently of its Object owner. */
sourceLibrary?: string | null;
type: NonMeshDataType;
geometryStatus: "summary-only" | "available" | "binary" | "blocked";
pointCount: number;
@@ -577,18 +594,23 @@ export interface NonMeshDataIR {
export interface AnimationIR {
id: string;
sourceActionId?: string;
name: string;
targetId: string;
frameStart: number;
frameEnd: number;
slots?: Array<{ identifier: string; idtype: number; handle: number; flags: number }>;
strips?: Array<{ name: string; type: "KEYFRAME"; frameStart: number; frameEnd: number; dataIndex: number }>;
channels: Array<{ path: string; editable?: boolean; enabled?: boolean; selected?: boolean; group?: string; expanded?: boolean; keyframeSource?: "BEZIER" | "SAMPLED"; modifiers?: Array<{ name: string; type: string; typeCode: number; flags: number; expanded: boolean; influence: number; frameStart: number; frameEnd: number; blendIn: number; blendOut: number }>; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" | "MIXED"; keyframes: Array<{ frame: number; value: number[]; handleLeft?: [number, number]; handleRight?: [number, number]; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" }> }>;
channels: Array<{ path: string; editable?: boolean; enabled?: boolean; selected?: boolean; group?: string; expanded?: boolean; keyframeSource?: "BEZIER" | "SAMPLED"; modifiers?: Array<{ name: string; type: string; typeCode: number; flags: number; expanded: boolean; influence: number; frameStart: number; frameEnd: number; blendIn: number; blendOut: number }>; interpolation?: GraphInterpolation | "MIXED"; keyframes: Array<{ frame: number; selected?: boolean; value: number[]; handleLeft?: [number, number]; handleRight?: [number, number]; interpolation?: GraphInterpolation }> }>;
}
export interface CollectionIR {
id: string;
name: string;
sourceLibrary?: string | null;
isMaster?: boolean;
viewportVisible?: boolean;
renderVisible?: boolean;
objectIds: string[];
childCollectionIds: string[];
}
@@ -651,7 +673,7 @@ export interface SceneIR {
saveOutput: boolean;
alphaPremultiplied: boolean;
};
viewLayers?: Array<{ name: string; useRender: boolean; useFreestyle: boolean; samples: number; passMask?: number; cryptomatteMask?: number; cryptomatteLevels?: number; alphaThreshold?: number; aovs?: Array<{ name: string; type: number }>; lightgroups?: string[] }>;
viewLayers?: Array<{ name: string; useRender: boolean; useFreestyle: boolean; samples: number; passMask?: number; cryptomatteMask?: number; cryptomatteLevels?: number; alphaThreshold?: number; aovs?: Array<{ name: string; type: 0 | 1 }>; lightgroups?: string[] }>;
borderSettings?: { enabled: boolean; minX: number; maxX: number; minY: number; maxY: number };
simplifySettings?: { subsurfViewport: number; subsurfRender: number; particlesViewport: number; particlesRender: number; volumes: number };
imageFormatSettings?: { fileType: string; colorMode: number; depth: number; quality: number; compression: number; exrCodec: number };
@@ -870,6 +892,8 @@ export interface SceneSnapshotIR {
collections: CollectionIR[];
scenes: SceneIR[];
activeObjectId: string | null;
/** Native ViewLayer Base selection, including when no editor workspace is available. */
selectedObjectIds?: string[];
frame: { current: number; start: number; end: number };
}
@@ -1221,6 +1245,9 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
if (!isRecord(node)) throw new Error(`SceneIR.nodes[${index}] must be an object`);
requireString(node.id, `nodes[${index}].id`);
requireString(node.name, `nodes[${index}].name`);
if (node.sourceLibrary !== undefined && node.sourceLibrary !== null) {
requireString(node.sourceLibrary, `nodes[${index}].sourceLibrary`);
}
requireBoolean(node.visible, `nodes[${index}].visible`);
requireBoolean(node.selectable, `nodes[${index}].selectable`);
if (!Array.isArray(node.localMatrix) || node.localMatrix.length !== 16) {
@@ -1231,6 +1258,8 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
if (!isRecord(node.transform)) throw new Error(`SceneIR.nodes[${index}].transform must be an object`);
requireTuple(node.transform.translation, 3, `nodes[${index}].transform.translation`);
requireTuple(node.transform.rotationEuler, 3, `nodes[${index}].transform.rotationEuler`);
if (node.transform.rotationQuaternion !== undefined) requireTuple(node.transform.rotationQuaternion, 4, `nodes[${index}].transform.rotationQuaternion`);
if (node.transform.rotationAxisAngle !== undefined) requireTuple(node.transform.rotationAxisAngle, 4, `nodes[${index}].transform.rotationAxisAngle`);
requireTuple(node.transform.scale, 3, `nodes[${index}].transform.scale`);
requireNumber(node.transform.rotationMode, `nodes[${index}].transform.rotationMode`);
if (node.drivers !== undefined) {
@@ -1458,6 +1487,9 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
if (nonMeshIds.has(dataId)) throw new Error(`SceneIR.nonMeshData contains duplicate id: ${dataId}`);
nonMeshIds.add(dataId);
requireString(data.name, `nonMeshData[${index}].name`);
if (data.sourceLibrary !== undefined && data.sourceLibrary !== null) {
requireString(data.sourceLibrary, "nonMeshData[" + index + "].sourceLibrary");
}
if (!["CURVE", "SURFACE", "FONT", "METABALL", "POINT_CLOUD", "CURVES", "VOLUME", "HAIR"].includes(data.type as string)) throw new Error(`SceneIR.nonMeshData[${index}].type is invalid`);
if (!["summary-only", "available", "binary", "blocked"].includes(data.geometryStatus as string)) throw new Error(`SceneIR.nonMeshData[${index}].geometryStatus is invalid`);
const pointCount = requireNumber(data.pointCount, `nonMeshData[${index}].pointCount`);
@@ -1701,7 +1733,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
requireNumber(world.exposure, `worlds[${index}].exposure`);
if (world.lightgroup !== undefined && world.lightgroup !== null) requireString(world.lightgroup, `worlds[${index}].lightgroup`);
if (world.environmentImageId !== undefined && world.environmentImageId !== null) requireString(world.environmentImageId, `worlds[${index}].environmentImageId`);
if (world.environmentStrength !== undefined) requireNumber(world.environmentStrength, `worlds[${index}].environmentStrength`);
if (world.environmentStrength !== undefined && world.environmentStrength !== null) requireNumber(world.environmentStrength, `worlds[${index}].environmentStrength`);
if (world.environmentRotation !== undefined) requireNumber(world.environmentRotation, `worlds[${index}].environmentRotation`);
for (const field of ["ambientOcclusionDistance", "ambientOcclusionEnergy", "sunThreshold", "sunAngle", "sunShadowMaximumResolution", "sunShadowJitterOverblur", "sunShadowFilterRadius", "probeResolution"] as const) if (world[field] !== undefined) requireNumber(world[field], `worlds[${index}].${field}`);
for (const field of ["useSunShadow", "useSunShadowJitter"] as const) if (world[field] !== undefined) requireBoolean(world[field], `worlds[${index}].${field}`);
@@ -1769,7 +1801,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
for (const field of ["passMask", "cryptomatteMask", "cryptomatteLevels", "alphaThreshold"] as const) if (layer[field] !== undefined) requireNumber(layer[field], `scenes[${index}].viewLayers[${layerIndex}].${field}`);
if (layer.aovs !== undefined) {
if (!Array.isArray(layer.aovs)) throw new Error(`SceneIR.scenes[${index}].viewLayers[${layerIndex}].aovs must be an array`);
for (const [aovIndex, aov] of layer.aovs.entries()) { if (!isRecord(aov)) throw new Error(`SceneIR.scenes[${index}].viewLayers[${layerIndex}].aovs[${aovIndex}] must be an object`); requireString(aov.name, `scenes[${index}].viewLayers[${layerIndex}].aovs[${aovIndex}].name`); requireNumber(aov.type, `scenes[${index}].viewLayers[${layerIndex}].aovs[${aovIndex}].type`); }
for (const [aovIndex, aov] of layer.aovs.entries()) { if (!isRecord(aov)) throw new Error(`SceneIR.scenes[${index}].viewLayers[${layerIndex}].aovs[${aovIndex}] must be an object`); requireString(aov.name, `scenes[${index}].viewLayers[${layerIndex}].aovs[${aovIndex}].name`); if (aov.type !== 0 && aov.type !== 1) throw new Error(`SceneIR.scenes[${index}].viewLayers[${layerIndex}].aovs[${aovIndex}].type must be Value (0) or Color (1)`); }
}
if (layer.lightgroups !== undefined) { if (!Array.isArray(layer.lightgroups)) throw new Error(`SceneIR.scenes[${index}].viewLayers[${layerIndex}].lightgroups must be an array`); for (const [groupIndex, group] of layer.lightgroups.entries()) requireString(group, `scenes[${index}].viewLayers[${layerIndex}].lightgroups[${groupIndex}]`); }
}
@@ -1845,6 +1877,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
for (const [index, animation] of (value.animations as unknown[]).entries()) {
if (!isRecord(animation)) throw new Error(`SceneIR.animations[${index}] must be an object`);
requireString(animation.id, `animations[${index}].id`);
if (animation.sourceActionId !== undefined) requireString(animation.sourceActionId, `animations[${index}].sourceActionId`);
requireString(animation.name, `animations[${index}].name`);
requireString(animation.targetId, `animations[${index}].targetId`);
requireNumber(animation.frameStart, `animations[${index}].frameStart`);
@@ -1892,6 +1925,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
for (const [keyframeIndex, keyframe] of keyframes.entries()) {
if (!isRecord(keyframe)) throw new Error(`SceneIR.animations[${index}].channels[${channelIndex}].keyframes[${keyframeIndex}] must be an object`);
requireNumber(keyframe.frame, `animations[${index}].channels[${channelIndex}].keyframes[${keyframeIndex}].frame`);
if (keyframe.selected !== undefined && typeof keyframe.selected !== 'boolean') throw new Error(`animations[${index}].channels[${channelIndex}].keyframes[${keyframeIndex}].selected must be boolean`);
const values = requireArray(keyframe.value, `animations[${index}].channels[${channelIndex}].keyframes[${keyframeIndex}].value`);
if (values.some((item) => typeof item !== "number" || !Number.isFinite(item))) {
throw new Error(`SceneIR.animations[${index}].channels[${channelIndex}].keyframes[${keyframeIndex}].value must contain finite numbers`);
@@ -1960,6 +1994,17 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
};
byId.forEach((_dependencies, id) => visit(id));
}
if (value.selectedObjectIds !== undefined) {
const selection = requireArray(value.selectedObjectIds, "selectedObjectIds");
const nodeIds = new Set(nodes.map((node) => isRecord(node) ? node.id : undefined));
const selected = new Set<string>();
for (const id of selection) {
if (typeof id !== "string" || !nodeIds.has(id) || selected.has(id)) {
throw new Error("SceneIR.selectedObjectIds must contain unique existing node IDs");
}
selected.add(id);
}
}
if (value.editorWorkflowStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.editorWorkflowStatus as string)) {
throw new Error("SceneIR.editorWorkflowStatus is invalid");
}