feat(web): complete product workflow implementation
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

This commit is contained in:
mes123456
2026-09-06 08:33:27 +08:00
parent 7e3f6da78b
commit 1184a51ddf
39 changed files with 3736 additions and 160 deletions

View File

@@ -48,6 +48,7 @@ export interface SceneNodeIR {
transform: SceneTransformIR;
assetData?: AssetMetadataIR;
constraints?: ConstraintIR[];
drivers?: DriverIR[];
lightgroup?: string | null;
}
@@ -72,6 +73,39 @@ export interface ConstraintIR {
targetObjectId?: string | null;
}
export interface DriverVariableIR {
name: string;
type: number;
targetCount: number;
targets?: DriverTargetIR[];
}
export interface DriverTargetIR {
idName: string;
dataPath: string;
boneTarget: string;
transformType: number;
rotationMode: number;
flags: number;
options: number;
idType: number;
contextProperty: number;
fallbackValue: number;
}
export interface DriverIR {
path: string;
arrayIndex: number;
editable: boolean;
enabled: boolean;
expression: string;
type: string;
typeCode: number;
flags: number;
influence: number;
variables: DriverVariableIR[];
}
export interface ModifierIR {
uuid: string;
type: string;
@@ -101,6 +135,8 @@ export interface ArmatureBoneIR {
parentId: string | null;
head: [number, number, number];
tail: [number, number, number];
roll?: number;
connected?: boolean;
restMatrix: number[];
poseMatrix?: number[];
}
@@ -444,6 +480,12 @@ export interface SpeakerResourceIR {
pitch: number;
}
export interface TextLineIR {
index: number;
body: string;
byteLength: number;
}
export interface TextResourceIR {
id: string;
name: string;
@@ -451,6 +493,7 @@ export interface TextResourceIR {
sourceSha256: string;
byteLength: number;
lineCount: number;
lines?: TextLineIR[];
internal: boolean;
sourcePath?: string;
isDirty: boolean;
@@ -530,7 +573,9 @@ export interface AnimationIR {
targetId: string;
frameStart: number;
frameEnd: number;
channels: Array<{ path: string; editable?: boolean; enabled?: boolean; selected?: boolean; group?: string; expanded?: boolean; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" | "MIXED"; keyframes: Array<{ frame: number; value: number[]; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" }> }>;
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" }> }>;
}
export interface CollectionIR {
@@ -590,6 +635,7 @@ export interface SceneIR {
previewStart: number;
previewEnd: number;
};
timelineMarkers?: Array<{ frame: number; name: string }>;
pipelineSettings?: {
useCompositor: boolean;
useSequencer: boolean;
@@ -654,7 +700,7 @@ export interface SceneSnapshotIR {
editorWorkflowStatus?: "AVAILABLE" | "BLOCKED";
scriptSources?: ScriptSourceInventoryIR;
scriptSourceStatus?: "AVAILABLE" | "BLOCKED";
renderRoutingStatus?: "AVAILABLE" | "BLOCKED";
renderRoutingStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
renderRouting?: {
schemaVersion: 1;
receiptId: "workflow:render_capability_routing";
@@ -665,7 +711,7 @@ export interface SceneSnapshotIR {
recovery: { cancellation: "OPEN_CANCELLED"; deviceLoss: "RELEASED"; [key: string]: unknown };
[key: string]: unknown;
};
serverJobBindingStatus?: "AVAILABLE" | "BLOCKED";
serverJobBindingStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
serverJobBinding?: {
schemaVersion: 1;
receiptId: "workflow:server_job_result_binding";
@@ -677,7 +723,7 @@ export interface SceneSnapshotIR {
lifecycle: { duplicate: "REUSED"; stale: "BLOCKED"; cancel: "CANCELLED"; timeout: "TIMEOUT"; oom: "OOM"; publishedOnFailure: false; [key: string]: unknown };
[key: string]: unknown;
};
scriptingPolicyStatus?: "AVAILABLE" | "BLOCKED";
scriptingPolicyStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
scriptingPolicy?: {
schemaVersion: 1;
policyId: "workflow:scripting_default_deny_audit";
@@ -697,7 +743,7 @@ export interface SceneSnapshotIR {
policyHash: string;
[key: string]: unknown;
};
performanceFaultMatrixStatus?: "AVAILABLE" | "BLOCKED";
performanceFaultMatrixStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
performanceFaultMatrix?: {
schemaVersion: 1;
receiptId: "workflow:performance_fault_recovery_matrix";
@@ -708,7 +754,7 @@ export interface SceneSnapshotIR {
matrixHash: string;
[key: string]: unknown;
};
releaseCandidateStatus?: "AVAILABLE" | "BLOCKED";
releaseCandidateStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
releaseCandidate?: {
schemaVersion: 1;
receiptId: "workflow:offline_ci_sbom_and_rollback_release";
@@ -720,7 +766,7 @@ export interface SceneSnapshotIR {
rollback: { runbookSha256: string; [key: string]: unknown };
[key: string]: unknown;
};
gapInventoryStatus?: "AVAILABLE" | "BLOCKED";
gapInventoryStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
gapInventory?: {
schemaVersion: 1;
receiptId: "workflow:cluster_full_blender_gap_inventory";
@@ -751,7 +797,7 @@ export interface SceneSnapshotIR {
nextTask: "WBV2-P7-002";
[key: string]: unknown;
};
atomicGapQueueStatus?: "AVAILABLE" | "BLOCKED";
atomicGapQueueStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
atomicGapQueue?: {
schemaVersion: 1;
receiptId: "workflow:generate_atomic_family_gap_queue";
@@ -762,7 +808,7 @@ export interface SceneSnapshotIR {
nextTask: "WBV2-P7-003";
[key: string]: unknown;
};
parityReleaseGateStatus?: "AVAILABLE" | "BLOCKED";
parityReleaseGateStatus?: "AVAILABLE" | "BLOCKED" | "MISSING";
parityReleaseGate?: {
schemaVersion: 1;
receiptId: "workflow:full_parity_candidate_release_gate";
@@ -1072,6 +1118,16 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
const lineCount = requireNumber(text.lineCount, `texts[${index}].lineCount`);
if (!Number.isSafeInteger(byteLength) || byteLength !== new TextEncoder().encode(source).byteLength || byteLength > 1_048_576) throw new Error(`SceneIR.texts[${index}].byteLength is invalid`);
if (!Number.isSafeInteger(lineCount) || lineCount < 1 || lineCount > 65_536) throw new Error(`SceneIR.texts[${index}].lineCount is invalid`);
if (text.lines !== undefined) {
if (!Array.isArray(text.lines) || text.lines.length !== lineCount) throw new Error(`SceneIR.texts[${index}].lines is invalid`);
text.lines.forEach((line, lineIndex) => {
if (!isRecord(line)) throw new Error(`SceneIR.texts[${index}].lines[${lineIndex}] must be an object`);
if (line.index !== lineIndex) throw new Error(`SceneIR.texts[${index}].lines[${lineIndex}].index is invalid`);
const body = requireString(line.body, `texts[${index}].lines[${lineIndex}].body`);
const lineByteLength = requireNumber(line.byteLength, `texts[${index}].lines[${lineIndex}].byteLength`);
if (!Number.isSafeInteger(lineByteLength) || lineByteLength !== new TextEncoder().encode(body).byteLength) throw new Error(`SceneIR.texts[${index}].lines[${lineIndex}].byteLength is invalid`);
});
}
requireBoolean(text.internal, `texts[${index}].internal`);
if (text.sourcePath !== undefined) requireString(text.sourcePath, `texts[${index}].sourcePath`);
requireBoolean(text.isDirty, `texts[${index}].isDirty`);
@@ -1153,6 +1209,34 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
requireTuple(node.transform.rotationEuler, 3, `nodes[${index}].transform.rotationEuler`);
requireTuple(node.transform.scale, 3, `nodes[${index}].transform.scale`);
requireNumber(node.transform.rotationMode, `nodes[${index}].transform.rotationMode`);
if (node.drivers !== undefined) {
const drivers = requireArray(node.drivers, `nodes[${index}].drivers`);
if (drivers.length > 256) throw new Error(`SceneIR.nodes[${index}].drivers exceeds the budget`);
for (const [driverIndex, driver] of drivers.entries()) {
if (!isRecord(driver)) throw new Error(`SceneIR.nodes[${index}].drivers[${driverIndex}] must be an object`);
for (const field of ["path", "expression", "type"] as const) requireString(driver[field], `nodes[${index}].drivers[${driverIndex}].${field}`);
for (const field of ["arrayIndex", "typeCode", "flags", "influence"] as const) requireNumber(driver[field], `nodes[${index}].drivers[${driverIndex}].${field}`);
requireBoolean(driver.editable, `nodes[${index}].drivers[${driverIndex}].editable`);
requireBoolean(driver.enabled, `nodes[${index}].drivers[${driverIndex}].enabled`);
const variables = requireArray(driver.variables, `nodes[${index}].drivers[${driverIndex}].variables`);
if (variables.length > 64) throw new Error(`SceneIR.nodes[${index}].drivers[${driverIndex}].variables exceeds the budget`);
for (const [variableIndex, variable] of variables.entries()) {
if (!isRecord(variable)) throw new Error(`SceneIR.nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}] must be an object`);
requireString(variable.name, `nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].name`);
requireNumber(variable.type, `nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].type`);
requireNumber(variable.targetCount, `nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].targetCount`);
if (variable.targets !== undefined) {
const targets = requireArray(variable.targets, `nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].targets`);
if (targets.length !== variable.targetCount) throw new Error(`SceneIR.nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].targets count mismatch`);
for (const [targetIndex, target] of targets.entries()) {
if (!isRecord(target)) throw new Error(`SceneIR.nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].targets[${targetIndex}] must be an object`);
for (const field of ["idName", "dataPath", "boneTarget"] as const) requireString(target[field], `nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].targets[${targetIndex}].${field}`);
for (const field of ["transformType", "rotationMode", "flags", "options", "idType", "contextProperty", "fallbackValue"] as const) requireNumber(target[field], `nodes[${index}].drivers[${driverIndex}].variables[${variableIndex}].targets[${targetIndex}].${field}`);
}
}
}
}
}
}
for (const [index, mesh] of meshes.entries()) {
if (!isRecord(mesh)) throw new Error(`SceneIR.meshes[${index}] must be an object`);
@@ -1287,6 +1371,8 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
if (bone.parentId !== null) requireString(bone.parentId, `armatures[${index}].bones[${boneIndex}].parentId`);
requireTuple(bone.head, 3, `armatures[${index}].bones[${boneIndex}].head`);
requireTuple(bone.tail, 3, `armatures[${index}].bones[${boneIndex}].tail`);
if (bone.roll !== undefined) requireNumber(bone.roll, `armatures[${index}].bones[${boneIndex}].roll`);
if (bone.connected !== undefined) requireBoolean(bone.connected, `armatures[${index}].bones[${boneIndex}].connected`);
requireTuple(bone.restMatrix, 16, `armatures[${index}].bones[${boneIndex}].restMatrix`);
if (bone.poseMatrix !== undefined) requireTuple(bone.poseMatrix, 16, `armatures[${index}].bones[${boneIndex}].poseMatrix`);
}
@@ -1624,6 +1710,14 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
for (const field of ["fps", "fpsBase", "frameStep", "previewStart", "previewEnd"] as const) requireNumber(scene.timelineSettings[field], `scenes[${index}].timelineSettings.${field}`);
requireBoolean(scene.timelineSettings.usePreviewRange, `scenes[${index}].timelineSettings.usePreviewRange`);
}
if (scene.timelineMarkers !== undefined) {
const markers = requireArray(scene.timelineMarkers, `scenes[${index}].timelineMarkers`);
for (const [markerIndex, marker] of markers.entries()) {
if (!isRecord(marker)) throw new Error(`SceneIR.scenes[${index}].timelineMarkers[${markerIndex}] must be an object`);
requireNumber(marker.frame, `scenes[${index}].timelineMarkers[${markerIndex}].frame`);
requireString(marker.name, `scenes[${index}].timelineMarkers[${markerIndex}].name`);
}
}
if (scene.pipelineSettings !== undefined) {
if (!isRecord(scene.pipelineSettings)) throw new Error(`SceneIR.scenes[${index}].pipelineSettings must be an object`);
for (const field of ["useCompositor", "useSequencer", "noOverwrite", "saveOutput", "alphaPremultiplied"] as const) requireBoolean(scene.pipelineSettings[field], `scenes[${index}].pipelineSettings.${field}`);
@@ -1719,10 +1813,45 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
requireString(animation.targetId, `animations[${index}].targetId`);
requireNumber(animation.frameStart, `animations[${index}].frameStart`);
requireNumber(animation.frameEnd, `animations[${index}].frameEnd`);
if (animation.slots !== undefined) {
const slots = requireArray(animation.slots, `animations[${index}].slots`);
if (slots.length > 1024) throw new Error(`animations[${index}].slots exceeds the budget`);
for (const [slotIndex, slot] of slots.entries()) {
if (!isRecord(slot)) throw new Error(`animations[${index}].slots[${slotIndex}] must be an object`);
requireString(slot.identifier, `animations[${index}].slots[${slotIndex}].identifier`);
for (const field of ["idtype", "handle", "flags"]) requireNumber(slot[field], `animations[${index}].slots[${slotIndex}].${field}`);
}
}
if (animation.strips !== undefined) {
const strips = requireArray(animation.strips, `animations[${index}].strips`);
if (strips.length > 1024) throw new Error(`animations[${index}].strips exceeds the budget`);
for (const [stripIndex, strip] of strips.entries()) {
if (!isRecord(strip)) throw new Error(`animations[${index}].strips[${stripIndex}] must be an object`);
requireString(strip.name, `animations[${index}].strips[${stripIndex}].name`);
if (strip.type !== "KEYFRAME") throw new Error(`animations[${index}].strips[${stripIndex}].type is invalid`);
requireNumber(strip.frameStart, `animations[${index}].strips[${stripIndex}].frameStart`);
requireNumber(strip.frameEnd, `animations[${index}].strips[${stripIndex}].frameEnd`);
requireNumber(strip.dataIndex, `animations[${index}].strips[${stripIndex}].dataIndex`);
}
}
const channels = requireArray(animation.channels, `animations[${index}].channels`);
for (const [channelIndex, channel] of channels.entries()) {
if (!isRecord(channel)) throw new Error(`SceneIR.animations[${index}].channels[${channelIndex}] must be an object`);
requireString(channel.path, `animations[${index}].channels[${channelIndex}].path`);
if (channel.keyframeSource !== undefined && channel.keyframeSource !== "BEZIER" && channel.keyframeSource !== "SAMPLED") {
throw new Error(`animations[${index}].channels[${channelIndex}].keyframeSource is invalid`);
}
if (channel.modifiers !== undefined) {
const modifiers = requireArray(channel.modifiers, `animations[${index}].channels[${channelIndex}].modifiers`);
if (modifiers.length > 64) throw new Error(`animations[${index}].channels[${channelIndex}].modifiers exceeds the budget`);
for (const [modifierIndex, modifier] of modifiers.entries()) {
if (!isRecord(modifier)) throw new Error(`animations[${index}].channels[${channelIndex}].modifiers[${modifierIndex}] must be an object`);
requireString(modifier.name, `animations[${index}].channels[${channelIndex}].modifiers[${modifierIndex}].name`);
requireString(modifier.type, `animations[${index}].channels[${channelIndex}].modifiers[${modifierIndex}].type`);
for (const field of ["typeCode", "flags", "influence", "frameStart", "frameEnd", "blendIn", "blendOut"]) requireNumber(modifier[field], `animations[${index}].channels[${channelIndex}].modifiers[${modifierIndex}].${field}`);
if (typeof modifier.expanded !== "boolean") throw new Error(`animations[${index}].channels[${channelIndex}].modifiers[${modifierIndex}].expanded is invalid`);
}
}
const keyframes = requireArray(channel.keyframes, `animations[${index}].channels[${channelIndex}].keyframes`);
for (const [keyframeIndex, keyframe] of keyframes.entries()) {
if (!isRecord(keyframe)) throw new Error(`SceneIR.animations[${index}].channels[${channelIndex}].keyframes[${keyframeIndex}] must be an object`);
@@ -1809,7 +1938,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
parseScriptSourceInventory(value.scriptSources);
if (value.scriptSourceStatus !== "AVAILABLE") throw new Error("SceneIR.scriptSourceStatus must be AVAILABLE when scriptSources is present");
}
if (value.renderRoutingStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.renderRoutingStatus as string)) {
if (value.renderRoutingStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.renderRoutingStatus as string)) {
throw new Error("SceneIR.renderRoutingStatus is invalid");
}
if (value.renderRouting !== undefined) {
@@ -1825,7 +1954,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
}
if (value.renderRoutingStatus !== "AVAILABLE") throw new Error("SceneIR.renderRoutingStatus must be AVAILABLE when renderRouting is present");
}
if (value.serverJobBindingStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.serverJobBindingStatus as string)) {
if (value.serverJobBindingStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.serverJobBindingStatus as string)) {
throw new Error("SceneIR.serverJobBindingStatus is invalid");
}
if (value.serverJobBinding !== undefined) {
@@ -1843,7 +1972,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
}
if (value.serverJobBindingStatus !== "AVAILABLE") throw new Error("SceneIR.serverJobBindingStatus must be AVAILABLE when serverJobBinding is present");
}
if (value.scriptingPolicyStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.scriptingPolicyStatus as string)) {
if (value.scriptingPolicyStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.scriptingPolicyStatus as string)) {
throw new Error("SceneIR.scriptingPolicyStatus is invalid");
}
if (value.scriptingPolicy !== undefined) {
@@ -1871,7 +2000,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
}
if (value.scriptingPolicyStatus !== "AVAILABLE") throw new Error("SceneIR.scriptingPolicyStatus must be AVAILABLE when scriptingPolicy is present");
}
if (value.performanceFaultMatrixStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.performanceFaultMatrixStatus as string)) {
if (value.performanceFaultMatrixStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.performanceFaultMatrixStatus as string)) {
throw new Error("SceneIR.performanceFaultMatrixStatus is invalid");
}
if (value.performanceFaultMatrix !== undefined) {
@@ -1892,7 +2021,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
}
if (value.performanceFaultMatrixStatus !== "AVAILABLE") throw new Error("SceneIR.performanceFaultMatrixStatus must be AVAILABLE when performanceFaultMatrix is present");
}
if (value.releaseCandidateStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.releaseCandidateStatus as string)) {
if (value.releaseCandidateStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.releaseCandidateStatus as string)) {
throw new Error("SceneIR.releaseCandidateStatus is invalid");
}
if (value.releaseCandidate !== undefined) {
@@ -1917,7 +2046,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
}
if (value.releaseCandidateStatus !== "AVAILABLE") throw new Error("SceneIR.releaseCandidateStatus must be AVAILABLE when releaseCandidate is present");
}
if (value.gapInventoryStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.gapInventoryStatus as string)) {
if (value.gapInventoryStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.gapInventoryStatus as string)) {
throw new Error("SceneIR.gapInventoryStatus is invalid");
}
if (value.gapInventory !== undefined) {
@@ -1953,7 +2082,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
}
if (value.gapInventoryStatus !== "AVAILABLE") throw new Error("SceneIR.gapInventoryStatus must be AVAILABLE when gapInventory is present");
}
if (value.atomicGapQueueStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.atomicGapQueueStatus as string)) {
if (value.atomicGapQueueStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.atomicGapQueueStatus as string)) {
throw new Error("SceneIR.atomicGapQueueStatus is invalid");
}
if (value.atomicGapQueue !== undefined) {
@@ -1988,7 +2117,7 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
throw new Error("SceneIR.atomicGapQueueStatus must be AVAILABLE for a valid queue");
}
}
if (value.parityReleaseGateStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.parityReleaseGateStatus as string)) {
if (value.parityReleaseGateStatus !== undefined && !["AVAILABLE", "BLOCKED", "MISSING"].includes(value.parityReleaseGateStatus as string)) {
throw new Error("SceneIR.parityReleaseGateStatus is invalid");
}
if (value.parityReleaseGate !== undefined) {