import type { ErrorReport } from "../../../protocol/error"; import type { ProgressEvent } from "../../../protocol/progress"; import type { AssetRequestResult, MeshGeometryBuffer, WebEngineEditCommand, WebEngineLODResult, WebEngineRequest, WebEngineResponse, WebEngineResult, WebEngineStatus, } from "../../../protocol/web-engine"; import type { LODGenerationRequest } from "../../../protocol/lod"; import type { SceneSnapshotIR } from "../../../protocol/scene-ir"; 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"; interface PendingRequest { resolve: (result: WebEngineResult) => void; reject: (error: ErrorReport) => void; onProgress?: (progress: ProgressEvent) => void; timer: ReturnType; } export interface WebEngineClientOptions { timeoutMs?: number; workerFactory?: () => Worker; } 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 worker: Worker | 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; } async init(): Promise { return (await this.request({ type: "init" })).status; } async openBlend(buffer: ArrayBuffer, onProgress?: (progress: ProgressEvent) => void): Promise { if (buffer.byteLength === 0) { throw this.report("INVALID_ARGUMENT", "无法打开空的 .blend 文件", true); } const result = await this.request({ type: "openBlend", buffer }, [buffer], onProgress); 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 { 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, }; } 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({ type: "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; } terminate(): void { this.failPending(this.report("WORKER_TERMINATED", "WebEngineWorker 已关闭", true)); this.worker?.terminate(); this.worker = null; this.geometryBuffers = []; this.nonMeshGeometryBuffers = []; } private start(): Worker { if (this.worker) return this.worker; const worker = this.workerFactory(); worker.onmessage = (event: MessageEvent) => this.handleResponse(event.data); worker.onerror = (event) => { this.failPending(this.report("WORKER_TERMINATED", event.message || "WebEngineWorker 发生异常", true)); }; this.worker = worker; return worker; } private request( command: WebEngineRequest["command"], transfer: Transferable[] = [], onProgress?: (progress: ProgressEvent) => void, ): Promise { const worker = this.start(); const requestId = `web-engine-${++this.requestCounter}`; const request = { requestId, command } as WebEngineRequest; return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(requestId); reject(this.report("WORKER_TERMINATED", `WebEngine 请求超时: ${command.type}`, true)); }, this.timeoutMs); this.pending.set(requestId, { resolve, reject, onProgress, timer }); 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); if (response.ok) 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.reject(error); } this.pending.clear(); } private report(code: ErrorReport["code"], message: string, recoverable: boolean): ErrorReport { return { code, severity: "error", message, recoverable }; } }