Files
XgridConverter/app/workers/converter.worker.ts
Andreas Wilms 21e5be3e0b fixed exports
2026-03-10 11:46:59 +01:00

259 lines
8.4 KiB
TypeScript

/* 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}` });
}
}
};