Files
workinf_Blender_Wasm/web/app/src/engine-client/WebEngineClient.ts
mes123456 81113159dc
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
feat(web): continue source-backed implementation waves
2026-08-29 05:46:53 +08:00

365 lines
16 KiB
TypeScript

import type { ErrorReport } from "../../../protocol/error";
import type { ProgressEvent } from "../../../protocol/progress";
import type {
AssetRequestResult,
MeshGeometryBuffer,
WebEngineEditCommand,
WebEngineLODResult,
WebEngineRequest,
WebEngineResponse,
WebEngineResult,
WebEngineOpenResourceStatus,
WebEngineStatus,
} from "../../../protocol/web-engine";
import type { LODGenerationRequest } from "../../../protocol/lod";
import type { SceneSnapshotIR } from "../../../protocol/scene-ir";
import type { ShaderCompileReport } from "../../../protocol/shader-compiler";
import type { SceneDelta } from "../../../protocol/scene-delta";
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
import type { SimplifyResult } from "../../../protocol/simplify";
import type { DepsgraphEvaluationIR } from "../../../protocol/depsgraph";
import type { RenderCapabilityRequest } from "../../../protocol/render-capabilities";
import type { CapabilityGateResult } from "../../../protocol/capability-gates";
import { applyMeshGeometryDelta } from "../../../protocol/mesh-geometry-delta";
import { createWorkerFault, type WorkerFault } from "../../../protocol/worker-fault";
import { getWebEngineCommandCapability } from "../../../protocol/web-engine-capabilities";
import { resourceCommands } from "../../../protocol/resource-commands";
import type {
PaintStrokeSessionBeginIR,
PaintStrokeSessionCancelIR,
PaintStrokeSessionChunkIR,
PaintStrokeSessionCommitIR,
PaintStrokeSessionReceiptIR,
} from "../../../protocol/paint-stroke-session";
import type { PaintPBVHCapabilityRequest } from "../../../protocol/paint-pbvh-capability";
import type { LibraryMainAppendReceiptIR, LibraryMainAppendRequestIR } from "../../../protocol/library-main-append";
interface PendingRequest {
resolve: (result: WebEngineResult) => void;
reject: (error: ErrorReport) => void;
onProgress?: (progress: ProgressEvent) => void;
timer: ReturnType<typeof setTimeout>;
cleanup?: () => void;
aborted?: boolean;
}
export interface WebEngineClientOptions {
timeoutMs?: number;
workerFactory?: () => Worker;
onWorkerFault?: (fault: WorkerFault) => void;
}
const defaultWorkerFactory = () =>
new Worker(new URL("../workers/web-engine.worker.ts", import.meta.url), { type: "module" });
export interface BlendOpenResult {
status: WebEngineStatus;
snapshot: SceneSnapshotIR;
geometryBuffers: MeshGeometryBuffer[];
nonMeshGeometryBuffers?: NonMeshGeometryChunk[];
simplify?: SimplifyResult;
}
export class WebEngineClient {
private readonly timeoutMs: number;
private readonly workerFactory: () => Worker;
private readonly onWorkerFault?: (fault: WorkerFault) => void;
private worker: Worker | null = null;
private workerFaulted = false;
private lastWorkerFault: WorkerFault | null = null;
private requestCounter = 0;
private pending = new Map<string, PendingRequest>();
private geometryBuffers: MeshGeometryBuffer[] = [];
private nonMeshGeometryBuffers: NonMeshGeometryChunk[] = [];
constructor(options: WebEngineClientOptions = {}) {
this.timeoutMs = options.timeoutMs ?? 30_000;
this.workerFactory = options.workerFactory ?? defaultWorkerFactory;
this.onWorkerFault = options.onWorkerFault;
}
async init(): Promise<WebEngineStatus> {
return (await this.request({ type: "init" })).status;
}
async openBlend(buffer: ArrayBuffer, onProgress?: (progress: ProgressEvent) => void, signal?: AbortSignal): Promise<BlendOpenResult> {
if (buffer.byteLength === 0) {
throw this.report("INVALID_ARGUMENT", "无法打开空的 .blend 文件", true);
}
const result = await this.request({ type: "openBlend", buffer }, [buffer], onProgress, signal);
if (!result.snapshot) throw this.report("BLEND_READ_FAILED", "WebEngine 未返回 SceneIR", true);
this.geometryBuffers = result.geometryBuffers ?? [];
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? [];
return { status: result.status, snapshot: result.snapshot, geometryBuffers: [...this.geometryBuffers], nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers] };
}
async snapshot(): Promise<BlendOpenResult> {
const result = await this.request({ type: "snapshot" });
if (!result.snapshot) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回 SceneIR", true);
this.geometryBuffers = result.geometryBuffers ?? this.geometryBuffers;
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? this.nonMeshGeometryBuffers;
return { status: result.status, snapshot: result.snapshot, geometryBuffers: [...this.geometryBuffers], nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers] };
}
async delta(): Promise<{ status: WebEngineStatus; delta: SceneDelta }> {
const result = await this.request({ type: "delta" });
if (!result.delta) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回 SceneDelta", true);
return { status: result.status, delta: result.delta };
}
async applyCommand(payload: WebEngineEditCommand): Promise<BlendOpenResult & { delta: SceneDelta; shaderCompile?: ShaderCompileReport }> {
if (!getWebEngineCommandCapability((payload as { type?: unknown }).type)) {
throw this.report("INVALID_ARGUMENT", `Unsupported WebEngine command: ${String((payload as { type?: unknown }).type)}`, true);
}
const result = await this.request({ type: "applyCommand", payload });
if (!result.snapshot || !result.delta) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回命令结果", true);
this.geometryBuffers = result.geometryDelta
? applyMeshGeometryDelta(this.geometryBuffers, result.geometryDelta)
: result.geometryBuffers ?? this.geometryBuffers;
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? this.nonMeshGeometryBuffers;
return {
status: result.status,
snapshot: result.snapshot,
geometryBuffers: [...this.geometryBuffers],
nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers],
delta: result.delta,
simplify: result.simplify,
shaderCompile: result.shaderCompile,
};
}
async appendLibraryObject(
source: ArrayBuffer,
request: LibraryMainAppendRequestIR,
): Promise<BlendOpenResult & { delta: SceneDelta; receipt: LibraryMainAppendReceiptIR }> {
const result = await this.request({ type: "appendLibraryObject", request, source }, [source]);
if (!result.snapshot || !result.delta || !result.libraryAppend) {
throw this.report("ASSET_MANIFEST_INVALID", "WebEngine did not return the committed library append", true);
}
this.geometryBuffers = result.geometryDelta
? applyMeshGeometryDelta(this.geometryBuffers, result.geometryDelta)
: result.geometryBuffers ?? this.geometryBuffers;
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? this.nonMeshGeometryBuffers;
return {
status: result.status,
snapshot: result.snapshot,
geometryBuffers: [...this.geometryBuffers],
nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers],
delta: result.delta,
receipt: result.libraryAppend,
};
}
async beginPaintStroke(session: PaintStrokeSessionBeginIR): Promise<PaintStrokeSessionReceiptIR> {
const result = await this.request({ type: "beginPaintStroke", session });
if (!result.paintStrokeSession) throw this.report("PAINT_SCHEMA_INVALID", "WebEngine did not open the paint pointer session", true);
return result.paintStrokeSession;
}
async appendPaintStrokeChunk(chunk: PaintStrokeSessionChunkIR): Promise<PaintStrokeSessionReceiptIR> {
const result = await this.request({ type: "appendPaintStrokeChunk", chunk });
if (!result.paintStrokeSession) throw this.report("PAINT_SCHEMA_INVALID", "WebEngine did not accept the paint stroke chunk", true);
return result.paintStrokeSession;
}
async commitPaintStroke(session: PaintStrokeSessionCommitIR): Promise<BlendOpenResult & { delta: SceneDelta; paintStrokeSession: PaintStrokeSessionReceiptIR }> {
const result = await this.request({ type: "commitPaintStroke", session });
if (!result.snapshot || !result.delta || !result.paintStrokeSession) {
throw this.report("PAINT_SCHEMA_INVALID", "WebEngine did not return the committed paint stroke", true);
}
this.geometryBuffers = result.geometryDelta
? applyMeshGeometryDelta(this.geometryBuffers, result.geometryDelta)
: result.geometryBuffers ?? this.geometryBuffers;
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? this.nonMeshGeometryBuffers;
return {
status: result.status,
snapshot: result.snapshot,
geometryBuffers: [...this.geometryBuffers],
nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers],
delta: result.delta,
paintStrokeSession: result.paintStrokeSession,
};
}
async cancelPaintStroke(session: PaintStrokeSessionCancelIR): Promise<PaintStrokeSessionReceiptIR> {
const result = await this.request({ type: "cancelPaintStroke", session });
if (!result.paintStrokeSession) throw this.report("PAINT_SCHEMA_INVALID", "WebEngine did not cancel the paint pointer session", true);
return result.paintStrokeSession;
}
async queryPaintPBVHCapability(request: PaintPBVHCapabilityRequest): Promise<CapabilityGateResult> {
const result = await this.request({ type: "queryPaintPBVHCapability", request });
if (!result.capabilityGate) throw this.report("CAPABILITY_MISSING", "WebEngine did not return the PBVH paint capability gate", true);
return result.capabilityGate;
}
async previewCommand(payload: Extract<WebEngineEditCommand, { type: "previewDecimateMesh" }>): Promise<BlendOpenResult> {
const result = await this.request({ type: "applyCommand", payload });
if (!result.snapshot) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回预览结果", true);
return { status: result.status, snapshot: result.snapshot, geometryBuffers: result.geometryBuffers ?? [], nonMeshGeometryBuffers: result.nonMeshGeometryBuffers ?? [], simplify: result.simplify };
}
async generateLOD(payload: LODGenerationRequest): Promise<{ status: WebEngineStatus; lod: WebEngineLODResult }> {
const result = await this.request({ type: "generateLOD", payload });
if (!result.lod) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回 LOD 结果", true);
return { status: result.status, lod: result.lod };
}
async requestAsset(assetId: string): Promise<AssetRequestResult> {
if (!assetId) throw this.report("INVALID_ARGUMENT", "Asset ID 不能为空", true);
const result = await this.request(resourceCommands.requestAsset(assetId));
if (!result.asset) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回资产结果", true);
return result.asset;
}
async queryRenderCapability(request: RenderCapabilityRequest): Promise<CapabilityGateResult> {
const result = await this.request({ type: "queryRenderCapability", request });
if (!result.capabilityGate) throw this.report("CAPABILITY_MISSING", "WebEngine 未返回渲染能力门", true);
return result.capabilityGate;
}
async queryNonMeshCapability(dataId: string): Promise<CapabilityGateResult> {
const result = await this.request({ type: "queryNonMeshCapability", dataId });
if (!result.capabilityGate) throw this.report("CAPABILITY_MISSING", "WebEngine did not return the non-mesh capability gate", true);
return result.capabilityGate;
}
async evaluateDepsgraph(): Promise<{ status: WebEngineStatus; depsgraph: DepsgraphEvaluationIR }> {
const result = await this.request({ type: "evaluateDepsgraph" });
if (!result.depsgraph) throw this.report("CAPABILITY_MISSING", "WebEngine 未返回 Blender Depsgraph 结果", true);
return { status: result.status, depsgraph: result.depsgraph };
}
async saveBlend(): Promise<ArrayBuffer> {
const result = await this.request({ type: "saveBlend" });
if (!result.blend) throw this.report("BLEND_WRITE_FAILED", "WebEngine 未返回 .blend 数据", true);
return result.blend;
}
async openResourceStatus(): Promise<WebEngineOpenResourceStatus> {
const result = await this.request({ type: "openResourceStatus" });
if (!result.openResources) throw this.report("INVALID_ARGUMENT", "WebEngine open resource status missing", true);
return result.openResources;
}
async restart(): Promise<WebEngineStatus> {
this.worker?.terminate();
this.worker = null;
this.workerFaulted = false;
this.lastWorkerFault = null;
this.geometryBuffers = [];
this.nonMeshGeometryBuffers = [];
return this.init();
}
crashForTest(): void {
this.worker?.postMessage({
requestId: `web-engine-crash-${++this.requestCounter}`,
command: { type: "crashForTest" },
} satisfies WebEngineRequest);
}
terminate(): void {
this.failPending(this.report("WORKER_TERMINATED", "WebEngineWorker 已关闭", true));
this.worker?.terminate();
this.worker = null;
this.workerFaulted = true;
this.geometryBuffers = [];
this.nonMeshGeometryBuffers = [];
}
private start(): Worker {
if (this.workerFaulted) {
throw this.lastWorkerFault?.error ?? this.report("WORKER_TERMINATED", "WebEngineWorker 需要重启", true);
}
if (this.worker) return this.worker;
const worker = this.workerFactory();
worker.onmessage = (event: MessageEvent<WebEngineResponse>) => this.handleResponse(event.data);
worker.onerror = (event) => this.handleWorkerFault(event.message || "WebEngineWorker 发生异常");
worker.onmessageerror = () => this.handleWorkerFault("WebEngineWorker 消息无法解析");
this.worker = worker;
return worker;
}
private request(
command: WebEngineRequest["command"],
transfer: Transferable[] = [],
onProgress?: (progress: ProgressEvent) => void,
signal?: AbortSignal,
): Promise<WebEngineResult> {
let worker: Worker;
try {
worker = this.start();
}
catch (error) {
return Promise.reject(error);
}
const requestId = `web-engine-${++this.requestCounter}`;
const request = { requestId, command } as WebEngineRequest;
return new Promise<WebEngineResult>((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.get(requestId)?.cleanup?.();
this.pending.delete(requestId);
reject(this.report("WORKER_TERMINATED", `WebEngine 请求超时: ${command.type}`, true));
}, this.timeoutMs);
const abort = (): void => {
const pending = this.pending.get(requestId);
if (pending) pending.aborted = true;
worker.postMessage({
requestId: `web-engine-cancel-${++this.requestCounter}`,
command: { type: "cancelOpen", targetRequestId: requestId },
} satisfies WebEngineRequest);
};
const cleanup = signal ? () => signal.removeEventListener("abort", abort) : undefined;
this.pending.set(requestId, { resolve, reject, onProgress, timer, cleanup });
if (signal) {
signal.addEventListener("abort", abort, { once: true });
if (signal.aborted) abort();
}
worker.postMessage(request, transfer);
});
}
private handleResponse(response: WebEngineResponse): void {
const pending = this.pending.get(response.requestId);
if (!pending) return;
if (response.kind === "progress") {
pending.onProgress?.(response.progress);
return;
}
this.pending.delete(response.requestId);
clearTimeout(pending.timer);
pending.cleanup?.();
if (response.ok) {
if (pending.aborted) pending.reject(this.report("OPEN_CANCELLED", "WebEngine open cancelled", true));
else pending.resolve(response.result);
}
else pending.reject(response.error);
}
private failPending(error: ErrorReport): void {
for (const pending of this.pending.values()) {
clearTimeout(pending.timer);
pending.cleanup?.();
pending.reject(error);
}
this.pending.clear();
}
private handleWorkerFault(message: string): void {
if (this.workerFaulted) return;
const fault = createWorkerFault("engine", message);
this.workerFaulted = true;
this.lastWorkerFault = fault;
const worker = this.worker;
this.worker = null;
this.failPending(fault.error);
worker?.terminate();
this.onWorkerFault?.(fault);
}
private report(code: ErrorReport["code"], message: string, recoverable: boolean): ErrorReport {
return { code, severity: "error", message, recoverable };
}
}