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; 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(); 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 { return (await this.request({ type: "init" })).status; } async openBlend(buffer: ArrayBuffer, onProgress?: (progress: ProgressEvent) => void, signal?: AbortSignal): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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) => 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 { 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((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 }; } }