feat(web): complete Blender route runtime execution
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-07 00:12:32 +08:00
parent e2e81c7c00
commit e9bfef0c5a
35 changed files with 57527 additions and 75 deletions

View File

@@ -29,6 +29,11 @@ export interface BlenderOperatorDescriptor {
properties: BlenderOperatorPropertySpec[];
}
export interface BlenderServerOperatorSchema {
operator: BlenderOperatorDescriptor;
blenderBuild: { version: string; buildSha256: string };
}
export interface BlenderOperatorInvocation {
operator: string;
properties?: Record<string, BlenderOperatorPropertyValue>;
@@ -37,6 +42,22 @@ export interface BlenderOperatorInvocation {
export type BlenderFeatureRoute = "WEB_MAIN" | "SERVER_NATIVE" | "BROWSER_EQUIVALENT" | "UNAVAILABLE";
/** Browser-owned replacements for desktop context/window operators. */
export type BlenderBrowserActionGroup = "OPEN" | "SAVE" | "WORKSPACE" | "SELECTION" | "VIEW" | "EDITOR";
export interface BlenderBrowserEquivalentInvocation {
command: "invokeBrowserEquivalent";
route: string;
actionGroup: BlenderBrowserActionGroup;
browserCommand: string;
}
export interface BlenderFeatureRouteRuntimeAudit {
command: "featureRouteRuntimeAudit";
status: "PASS" | "FAILED" | "STATIC_DECLARATION";
behavioralEvidence: { separate: true };
}
export interface BlenderFeatureRouteLocation {
path: string;
line?: number;
@@ -51,6 +72,10 @@ export interface BlenderFeatureRouteEntry {
family: string;
route: BlenderFeatureRoute;
reason: string;
/** HTTP provider used by SERVER_NATIVE routes. */
httpRoute?: string;
/** Existing browser workflow used by BROWSER_EQUIVALENT routes. */
browserActionGroup?: BlenderBrowserActionGroup;
label?: string;
description?: string;
properties?: BlenderOperatorPropertySpec[];

View File

@@ -5,6 +5,20 @@ export type ErrorCode =
| "PROTOCOL_MISMATCH"
| "REVISION_CONFLICT"
| "INVALID_ARGUMENT"
| "OBJECT_EDITOR_INVALID"
| "ANIMATION_ACTION_INVALID"
| "ANIMATION_KEYFRAME_INVALID"
| "ANIMATION_FCURVE_INVALID"
| "ANIMATION_DRIVER_INVALID"
| "ANIMATION_GROUP_INVALID"
| "ANIMATION_KEYING_SET_INVALID"
| "ANIMATION_BAKE_INVALID"
| "ANIMATION_MARKER_INVALID"
| "ARMATURE_EDIT_INVALID"
| "ARMATURE_ROLL_INVALID"
| "ARMATURE_SYMMETRY_INVALID"
| "ARMATURE_PARENT_INVALID"
| "POSE_CONSTRAINT_INVALID"
| "WASM_INIT_FAILED"
| "WASM_OUT_OF_MEMORY"
| "BLEND_READ_FAILED"
@@ -100,6 +114,9 @@ export type ErrorCode =
| "PAINT_PBVH_CONTEXT_UNAVAILABLE"
| "PAINT_PBVH_BRUSH_UNVERIFIED"
| "PHYSICS_MANIFEST_INVALID"
| "PHYSICS_RESOURCE_UNAVAILABLE"
| "PHYSICS_SETTINGS_INVALID"
| "PHYSICS_CACHE_INVALID"
| "PHYSICS_BUDGET_EXCEEDED"
| "PHYSICS_DEPENDENCY_CYCLE"
| "PHYSICS_CACHE_FRAME_MISMATCH"
@@ -110,6 +127,7 @@ export type ErrorCode =
| "PHYSICS_SERVER_UNAVAILABLE"
| "RENDER_PROPERTY_INVALID"
| "RENDER_REFERENCE_MISMATCH"
| "RENDER_PASS_UNAVAILABLE"
| "SERVER_RENDER_REQUEST_INVALID"
| "SERVER_RENDER_SOURCE_INVALID"
| "SERVER_RENDER_SOURCE_HASH_MISMATCH"

View File

@@ -46,4 +46,4 @@ export const ioMediaCommands = Object.freeze({
setTextureProperties(dataId: string, properties: IOCommand<"setTextureProperties">["properties"]): IOCommand<"setTextureProperties"> {
return { type: "setTextureProperties", dataId, properties: { ...properties } };
},
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand>);
} satisfies Record<string, (...args: never[]) => WebEngineEditCommand | IOSceneCommand>);

View File

@@ -1,5 +1,5 @@
import type { ErrorCode } from "./error";
import type { BlenderOperatorPropertyValue } from "./blender-operator";
import type { BlenderOperatorDescriptor, BlenderOperatorPropertyValue } from "./blender-operator";
export const SERVER_OPERATOR_JOB_SCHEMA = 1 as const;
export const SERVER_OPERATOR_JOB_ROUTE = "SERVER_NATIVE" as const;
@@ -14,6 +14,7 @@ export const SERVER_OPERATOR_JOB_BUDGET = {
maxTimeoutMs: 10 * 60 * 1000,
minMemoryBytes: 64 * 1024 * 1024,
maxMemoryBytes: 4 * 1024 * 1024 * 1024,
maxEnvelopeBytes: 512 * 1024 * 1024 + 1024 * 1024,
} as const;
export const SERVER_OPERATOR_JOB_STATUSES = ["QUEUED", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED"] as const;
export type ServerOperatorJobStatus = typeof SERVER_OPERATOR_JOB_STATUSES[number];
@@ -23,6 +24,13 @@ export interface ServerOperatorBuildIdentityIR {
version: string;
buildSha256: string;
}
export interface ServerOperatorSchemaResultIR {
schemaVersion: typeof SERVER_OPERATOR_JOB_SCHEMA;
route: typeof SERVER_OPERATOR_JOB_ROUTE;
operator: BlenderOperatorDescriptor;
blenderBuild: ServerOperatorBuildIdentityIR;
}
export interface ServerOperatorJobRequestIR {
schemaVersion: typeof SERVER_OPERATOR_JOB_SCHEMA;
route: typeof SERVER_OPERATOR_JOB_ROUTE;
@@ -52,11 +60,57 @@ export interface ServerOperatorJobResultIR {
propertiesSha256: string;
requestSha256: string;
resultSha256: string;
resultBlendSha256?: string;
resultBlendByteLength?: number;
resultBlendBase64?: string;
result?: unknown;
errorCode?: ErrorCode;
errorMessage?: string;
}
const SERVER_OPERATOR_ENVELOPE_MAGIC = "WBJ1";
const SERVER_OPERATOR_ENVELOPE_HEADER_BYTES = 16;
export function canonicalServerOperatorProperties(properties: Record<string, BlenderOperatorPropertyValue>): string {
return stableJSON(properties);
}
export function encodeServerOperatorJobEnvelope(request: ServerOperatorJobRequestIR, sourceBlend: ArrayBuffer | Uint8Array): ArrayBuffer {
const validated = parseServerOperatorJobRequest(request);
const source = sourceBlend instanceof Uint8Array ? sourceBlend : new Uint8Array(sourceBlend);
if (source.byteLength !== validated.sourceBlendByteLength) throw new ServerOperatorJobValidationError("source blend byte length does not match request");
const requestBytes = new TextEncoder().encode(JSON.stringify(validated));
const total = SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes.byteLength + source.byteLength;
if (total > SERVER_OPERATOR_JOB_BUDGET.maxEnvelopeBytes) throw new ServerOperatorJobValidationError("binary envelope exceeds the budget");
const envelope = new Uint8Array(total);
envelope.set(new TextEncoder().encode(SERVER_OPERATOR_ENVELOPE_MAGIC), 0);
const view = new DataView(envelope.buffer);
view.setUint32(4, SERVER_OPERATOR_JOB_SCHEMA, true);
view.setUint32(8, requestBytes.byteLength, true);
view.setUint32(12, source.byteLength, true);
envelope.set(requestBytes, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES);
envelope.set(source, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes.byteLength);
return envelope.buffer;
}
export function decodeServerOperatorJobEnvelope(value: ArrayBuffer | Uint8Array): { request: ServerOperatorJobRequestIR; sourceBlend: Uint8Array } {
const envelope = value instanceof Uint8Array ? value : new Uint8Array(value);
if (envelope.byteLength < SERVER_OPERATOR_ENVELOPE_HEADER_BYTES || new TextDecoder().decode(envelope.slice(0, 4)) !== SERVER_OPERATOR_ENVELOPE_MAGIC) {
throw new ServerOperatorJobValidationError("binary envelope header is invalid");
}
const view = new DataView(envelope.buffer, envelope.byteOffset, envelope.byteLength);
if (view.getUint32(4, true) !== SERVER_OPERATOR_JOB_SCHEMA) throw new ServerOperatorJobValidationError("binary envelope schema is invalid");
const requestBytes = view.getUint32(8, true);
const sourceBytes = view.getUint32(12, true);
if (requestBytes < 1 || sourceBytes < 1 || SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes + sourceBytes !== envelope.byteLength || envelope.byteLength > SERVER_OPERATOR_JOB_BUDGET.maxEnvelopeBytes) {
throw new ServerOperatorJobValidationError("binary envelope lengths are invalid");
}
const request = parseServerOperatorJobRequest(JSON.parse(new TextDecoder().decode(envelope.slice(SERVER_OPERATOR_ENVELOPE_HEADER_BYTES, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes))));
const sourceBlend = envelope.slice(SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes);
if (sourceBlend.byteLength !== request.sourceBlendByteLength) throw new ServerOperatorJobValidationError("binary envelope source length is invalid");
return { request, sourceBlend };
}
export class ServerOperatorJobValidationError extends Error {
readonly code: ErrorCode;

View File

@@ -23,7 +23,7 @@ function uniqueId(base: string, ids: ReadonlySet<string>): string {
/** Build a valid bounded graph for the source-backed Principled writer. */
export const shaderGraphCommands = Object.freeze({
addNode(graph: ShaderGraphIR, type: ShaderNodeType, name = type): ShaderGraphIR {
addNode(graph: ShaderGraphIR, type: ShaderNodeType, name: string = type): ShaderGraphIR {
const next = clone(graph);
const id = uniqueId(`node-${type.toLowerCase()}`, new Set(next.nodes.map((node) => node.id)));
const node: ShaderNodeIR = { id, type, name: name.trim() || type, sockets: nodeSockets(type), ...(type === "MATH" ? { properties: { operation: "ADD" } } : {}) };