feat(web): complete Blender route runtime execution
This commit is contained in:
@@ -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[];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" } } : {}) };
|
||||
|
||||
Reference in New Issue
Block a user