/* eslint-disable no-restricted-globals */ import JSZip from "jszip"; // --- BINARY PARSERS --- function parseLci(buffer: ArrayBuffer): Blob { const view = new DataView(buffer); const LCI_MAGIC = 0x6c6c6f63; if (view.getUint32(0, true) !== LCI_MAGIC) throw new Error("Invalid LCI"); const meshNum = view.getUint32(44, true); const meshes = []; for (let i = 0; i < meshNum; i++) { const offset = 48 + i * 40; meshes.push({ dataOffset: Number(view.getBigUint64(offset + 8, true)), vertexNum: view.getUint32(offset + 24, true), faceNum: view.getUint32(offset + 28, true), }); } let vStr = ""; let fStr = ""; let vOff = 0; for (const m of meshes) { let pos = m.dataOffset; for (let j = 0; j < m.vertexNum; j++) { vStr += `${view.getFloat32(pos, true).toFixed(6)} ${view.getFloat32(pos + 4, true).toFixed(6)} ${view.getFloat32(pos + 8, true).toFixed(6)}\n`; pos += 12; } for (let j = 0; j < m.faceNum; j++) { fStr += `3 ${view.getUint32(pos, true) + vOff} ${view.getUint32(pos + 4, true) + vOff} ${view.getUint32(pos + 8, true) + vOff}\n`; pos += 12; } vOff += m.vertexNum; } const header = `ply\nformat ascii 1.0\nelement vertex ${vOff}\nproperty float x\nproperty float y\nproperty float z\nelement face ${meshes.reduce((a, b) => a + b.faceNum, 0)}\nproperty list uchar int vertex_indices\nend_header\n`; return new Blob([header + vStr + fStr], { type: "application/octet-stream" }); } function parseEnvironment(buffer: ArrayBuffer): Blob { const view = new DataView(buffer); const POINT_SIZE = 44; const numPoints = Math.floor(buffer.byteLength / POINT_SIZE); let plyHeader = [ "ply", "format ascii 1.0", `element vertex ${numPoints}`, "property float x", "property float y", "property float z", "property float f_dc_0", // Red "property float f_dc_1", // Green "property float f_dc_2", // Blue "property float scale_0", "property float scale_1", "property float scale_2", "property float rot_0", "property float rot_1", "property float rot_2", "property float rot_3", "property float opacity", "end_header", "", ].join("\n"); const rows: string[] = []; for (let i = 0; i < numPoints; i++) { const offset = i * POINT_SIZE; // X, Y, Z (first 3 floats) const x = view.getFloat32(offset, true).toFixed(6); const y = view.getFloat32(offset + 4, true).toFixed(6); const z = view.getFloat32(offset + 8, true).toFixed(6); const f_dc = "1.000000 1.000000 1.000000"; // Scales (floats 4, 5, 6) const s0 = view.getFloat32(offset + 12, true).toFixed(6); const s1 = view.getFloat32(offset + 16, true).toFixed(6); const s2 = view.getFloat32(offset + 20, true).toFixed(6); // Rotations (floats 7, 8, 9, 10) const r0 = view.getFloat32(offset + 24, true).toFixed(6); const r1 = view.getFloat32(offset + 28, true).toFixed(6); const r2 = view.getFloat32(offset + 32, true).toFixed(6); const r3 = view.getFloat32(offset + 36, true).toFixed(6); // Opacity (float 11) const alpha = view.getFloat32(offset + 40, true).toFixed(6); rows.push( `${x} ${y} ${z} ${f_dc} ${s0} ${s1} ${s2} ${r0} ${r1} ${r2} ${r3} ${alpha}`, ); } return new Blob([plyHeader + rows.join("\n")], { type: "application/octet-stream", }); } // --- WORKER INFRASTRUCTURE --- const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { const url = input instanceof Request ? input.url : input.toString(); if (url.includes("webp.wasm")) return originalFetch("/workers/webp.wasm", init); return originalFetch(input, init); }; self.onmessage = async (e: MessageEvent) => { const { type, filesData, mainLccName, fileName } = e.data; if (type === "START_CONVERSION") { try { const zip = new JSZip(); self.postMessage({ type: "LOG", message: "Initialisiere Pipeline..." }); // 1. Parse and add collision + environment meshes to ZIP const lciFile = filesData.find((f: any) => f.name === "collision.lci"); if (lciFile) zip.file("collision.ply", parseLci(lciFile.buffer)); const envFile = filesData.find((f: any) => f.name === "environment.bin"); if (envFile) zip.file("environment.ply", parseEnvironment(envFile.buffer)); // 2. Import splat-transform API const { readFile, writeFile, processDataTable, combine, MemoryReadFileSystem, MemoryFileSystem, getInputFormat, getOutputFormat, } = await import("@playcanvas/splat-transform"); // 3. Build in-memory read filesystem from all uploaded files const readFs = new MemoryReadFileSystem(); for (const f of filesData) readFs.set(f.name, new Uint8Array(f.buffer)); // 4. Read each LOD level separately from the LCC const lccFile = filesData.find((f: any) => f.name === mainLccName); if (!lccFile) throw new Error("LCC-Datei nicht gefunden."); const lccText = new TextDecoder().decode(lccFile.buffer); const lccJson = JSON.parse(lccText); const totalLevels: number = lccJson?.totalLevel ?? 5; self.postMessage({ type: "LOG", message: `LCC enthält ${totalLevels} LOD-Level.`, }); // 5. Read each LOD level separately from the LCC const lodOptions = { iterations: 0, unbundled: false, lodChunkCount: 0, lodChunkExtent: 0, }; const taggedTables = []; for (let i = 0; i < totalLevels; i++) { self.postMessage({ type: "LOG", message: `Lese LOD ${i}...` }); const tables = await readFile({ filename: mainLccName, fileSystem: readFs, inputFormat: getInputFormat(mainLccName), params: [], options: { ...lodOptions, lodSelect: [i], }, }); if (!tables || tables.length === 0) { self.postMessage({ type: "LOG", message: `LOD ${i} nicht gefunden, überspringe...`, }); continue; } const tagged = processDataTable(tables[0], [{ kind: "lod", value: i }]); taggedTables.push(tagged); } if (taggedTables.length === 0) throw new Error("Keine LOD-Daten gefunden."); self.postMessage({ type: "LOG", message: `${taggedTables.length} LOD-Level geladen. Kombiniere...`, }); // 5. Combine all tagged LOD tables const combined = combine(taggedTables); // 7. Write LOD streaming format // CRITICAL: output filename MUST be exactly "lod-meta.json" // The format is determined by the filename, not a format string. self.postMessage({ type: "LOG", message: "Kompiliere LOD Chunks (World Mode)...", }); const writeFsLods = new MemoryFileSystem(); const lodSelectAll = Array.from({ length: totalLevels }, (_, i) => i); const outputFormat = getOutputFormat("lod-meta.json", { iterations: 10, lodSelect: lodSelectAll, unbundled: true, lodChunkCount: 512, // default value from CLI tool lodChunkExtent: 16, // default value from CLI tool }); await writeFile( { filename: "lod-meta.json", outputFormat, dataTable: combined, options: { iterations: 10, lodSelect: lodSelectAll, unbundled: true, lodChunkCount: 512, // default value from CLI tool lodChunkExtent: 16, // default value from CLI tool }, }, writeFsLods, ); // 8. Organize output files into ZIP folder structure // The API emits files named like: "0_0_meta.json", "0_0_means_l.webp", "env_meta.json" etc. // We reorganize these into: 0_0/meta.json, 0_0/means_l.webp, env/meta.json self.postMessage({ type: "LOG", message: "Organisiere Ordnerstruktur...", }); for (const [name, data] of writeFsLods.results.entries()) { zip.file(name, data); } // 9. Generate and emit ZIP self.postMessage({ type: "LOG", message: "Erstelle ZIP-Datei..." }); const zipBlob = await zip.generateAsync({ type: "blob" }); self.postMessage({ type: "DONE", data: { files: [{ name: `${fileName}_pack.zip`, blob: zipBlob }] }, }); } catch (err: any) { self.postMessage({ type: "LOG", message: `Fehler: ${err.message}` }); } } };