fixed exports

This commit is contained in:
Andreas Wilms
2026-03-10 11:46:59 +01:00
parent 0b63bbfed3
commit 21e5be3e0b
3 changed files with 233 additions and 129 deletions

View File

@@ -1,19 +1,14 @@
/* eslint-disable no-restricted-globals */ /* eslint-disable no-restricted-globals */
import JSZip from "jszip";
// --- BINARY PARSERS ---
/**
* Converts XGrids .lci binary to PLY Mesh
*/
function parseLci(buffer: ArrayBuffer): Blob { function parseLci(buffer: ArrayBuffer): Blob {
const view = new DataView(buffer); const view = new DataView(buffer);
const LCI_MAGIC = 0x6c6c6f63; // 'coll' const LCI_MAGIC = 0x6c6c6f63;
if (view.getUint32(0, true) !== LCI_MAGIC) throw new Error("Invalid LCI");
if (view.getUint32(0, true) !== LCI_MAGIC) {
throw new Error("Invalid LCI Magic Number");
}
const meshNum = view.getUint32(44, true); const meshNum = view.getUint32(44, true);
const meshes = []; const meshes = [];
for (let i = 0; i < meshNum; i++) { for (let i = 0; i < meshNum; i++) {
const offset = 48 + i * 40; const offset = 48 + i * 40;
meshes.push({ meshes.push({
@@ -22,40 +17,28 @@ function parseLci(buffer: ArrayBuffer): Blob {
faceNum: view.getUint32(offset + 28, true), faceNum: view.getUint32(offset + 28, true),
}); });
} }
let vStr = "";
let verticesStr = ""; let fStr = "";
let facesStr = ""; let vOff = 0;
let vOffset = 0;
for (const m of meshes) { for (const m of meshes) {
let pos = m.dataOffset; let pos = m.dataOffset;
for (let j = 0; j < m.vertexNum; j++) { for (let j = 0; j < m.vertexNum; j++) {
verticesStr += `${view.getFloat32(pos, true).toFixed(6)} ${view.getFloat32(pos + 4, true).toFixed(6)} ${view.getFloat32(pos + 8, true).toFixed(6)}\n`; vStr += `${view.getFloat32(pos, true).toFixed(6)} ${view.getFloat32(pos + 4, true).toFixed(6)} ${view.getFloat32(pos + 8, true).toFixed(6)}\n`;
pos += 12; pos += 12;
} }
for (let j = 0; j < m.faceNum; j++) { for (let j = 0; j < m.faceNum; j++) {
facesStr += `3 ${view.getUint32(pos, true) + vOffset} ${view.getUint32(pos + 4, true) + vOffset} ${view.getUint32(pos + 8, true) + vOffset}\n`; fStr += `3 ${view.getUint32(pos, true) + vOff} ${view.getUint32(pos + 4, true) + vOff} ${view.getUint32(pos + 8, true) + vOff}\n`;
pos += 12; pos += 12;
} }
vOffset += m.vertexNum; 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`;
const header = `ply\nformat ascii 1.0\nelement vertex ${vOffset}\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" });
return new Blob([header + verticesStr + facesStr], {
type: "application/octet-stream",
});
} }
/**
* Converts XGrids environment.bin to PlayCanvas Gaussian Splat PLY
*/
/**
* Converts XGrids environment.bin to a Full Gaussian Splatting PLY
* Extracts: Position, Scale, Rotation (Quaternions), and Opacity
*/
function parseEnvironment(buffer: ArrayBuffer): Blob { function parseEnvironment(buffer: ArrayBuffer): Blob {
const view = new DataView(buffer); const view = new DataView(buffer);
const POINT_SIZE = 44; // 11 floats * 4 bytes const POINT_SIZE = 44;
const numPoints = Math.floor(buffer.byteLength / POINT_SIZE); const numPoints = Math.floor(buffer.byteLength / POINT_SIZE);
let plyHeader = [ let plyHeader = [
@@ -65,6 +48,9 @@ function parseEnvironment(buffer: ArrayBuffer): Blob {
"property float x", "property float x",
"property float y", "property float y",
"property float z", "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_0",
"property float scale_1", "property float scale_1",
"property float scale_2", "property float scale_2",
@@ -77,29 +63,34 @@ function parseEnvironment(buffer: ArrayBuffer): Blob {
"", "",
].join("\n"); ].join("\n");
// 2. Extract all 11 properties for every point
// We use an array of strings to build the body efficiently
const rows: string[] = []; const rows: string[] = [];
for (let i = 0; i < numPoints; i++) { for (let i = 0; i < numPoints; i++) {
const offset = i * POINT_SIZE; const offset = i * POINT_SIZE;
const pointData = [];
for (let j = 0; j < 11; j++) { // X, Y, Z (first 3 floats)
// getFloat32(offset, littleEndian: true) const x = view.getFloat32(offset, true).toFixed(6);
const val = view.getFloat32(offset + j * 4, true); const y = view.getFloat32(offset + 4, true).toFixed(6);
pointData.push(val.toFixed(6)); const z = view.getFloat32(offset + 8, true).toFixed(6);
} const f_dc = "1.000000 1.000000 1.000000";
rows.push(pointData.join(" ")); // 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);
// Periodically clear memory pressure if the scene is massive // Rotations (floats 7, 8, 9, 10)
if (i % 100000 === 0 && i > 0) { const r0 = view.getFloat32(offset + 24, true).toFixed(6);
self.postMessage({ const r1 = view.getFloat32(offset + 28, true).toFixed(6);
type: "LOG", const r2 = view.getFloat32(offset + 32, true).toFixed(6);
message: `Processing Environment: ${i.toLocaleString()} splats...`, 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")], { return new Blob([plyHeader + rows.join("\n")], {
@@ -122,118 +113,146 @@ self.onmessage = async (e: MessageEvent) => {
if (type === "START_CONVERSION") { if (type === "START_CONVERSION") {
try { try {
const generatedFiles: { name: string; blob: Blob }[] = []; const zip = new JSZip();
self.postMessage({ type: "LOG", message: "Initialisiere Pipeline..." });
// 1. Process LCI (Collision) // 1. Parse and add collision + environment meshes to ZIP
const lciData = filesData.find((f: any) => f.name === "collision.lci"); const lciFile = filesData.find((f: any) => f.name === "collision.lci");
if (lciData) { if (lciFile) zip.file("collision.ply", parseLci(lciFile.buffer));
self.postMessage({ type: "LOG", message: "Parsing Collision Mesh..." });
generatedFiles.push({
name: "collision_mesh.ply",
blob: parseLci(lciData.buffer),
});
}
// 2. Process Environment (Point Cloud) const envFile = filesData.find((f: any) => f.name === "environment.bin");
const envData = filesData.find((f: any) => f.name === "environment.bin"); if (envFile)
if (envData) { zip.file("environment.ply", parseEnvironment(envFile.buffer));
self.postMessage({
type: "LOG",
message: "Parsing Environment Cloud...",
});
generatedFiles.push({
name: "environment_reference.ply",
blob: parseEnvironment(envData.buffer),
});
}
// 3. Process Splat Transformation (SOG / LODs)
self.postMessage({
type: "LOG",
message: "Initializing PlayCanvas Splat Transform...",
});
// 2. Import splat-transform API
const { const {
readFile, readFile,
writeFile, writeFile,
processDataTable,
combine,
MemoryReadFileSystem, MemoryReadFileSystem,
MemoryFileSystem, MemoryFileSystem,
getInputFormat, getInputFormat,
getOutputFormat,
} = await import("@playcanvas/splat-transform"); } = await import("@playcanvas/splat-transform");
// 3. Build in-memory read filesystem from all uploaded files
const readFs = new MemoryReadFileSystem(); const readFs = new MemoryReadFileSystem();
for (const file of filesData) { for (const f of filesData) readFs.set(f.name, new Uint8Array(f.buffer));
readFs.set(file.name, new Uint8Array(file.buffer));
}
const commonOptions = { // 4. Read each LOD level separately from the LCC
iterations: 10, const lccFile = filesData.find((f: any) => f.name === mainLccName);
lodSelect: [0, 1, 2, 3, 4], 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, unbundled: false,
lodChunkCount: 0, lodChunkCount: 0,
lodChunkExtent: 0, lodChunkExtent: 0,
}; };
const taggedTables = [];
for (let i = 0; i < totalLevels; i++) {
self.postMessage({ type: "LOG", message: `Lese LOD ${i}...` });
const tables = await readFile({ const tables = await readFile({
filename: mainLccName, filename: mainLccName,
fileSystem: readFs, fileSystem: readFs,
inputFormat: getInputFormat(mainLccName), inputFormat: getInputFormat(mainLccName),
params: [], params: [],
options: { ...commonOptions, iterations: 0 }, options: {
}); ...lodOptions,
lodSelect: [i],
const mainTable = tables[0];
if (!mainTable) throw new Error("No Splat data found.");
// PASS: Single SOG
self.postMessage({ type: "LOG", message: "Compiling High-Res SOG..." });
const writeFsSingle = new MemoryFileSystem();
await writeFile(
{
filename: `${fileName}.sog`,
outputFormat: "sog-bundle",
dataTable: mainTable,
options: commonOptions,
}, },
writeFsSingle,
);
const singleData = writeFsSingle.results.get(`${fileName}.sog`);
if (singleData) {
generatedFiles.push({
name: `${fileName}.sog`,
blob: new Blob([new Uint8Array(singleData).buffer]),
}); });
if (!tables || tables.length === 0) {
self.postMessage({
type: "LOG",
message: `LOD ${i} nicht gefunden, überspringe...`,
});
continue;
} }
// PASS: LOD Chunks const tagged = processDataTable(tables[0], [{ kind: "lod", value: i }]);
self.postMessage({ type: "LOG", message: "Compiling LOD Chunks..." }); 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 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( await writeFile(
{ {
filename: "meta.json", filename: "lod-meta.json",
outputFormat: "sog", outputFormat,
dataTable: mainTable, dataTable: combined,
options: { options: {
...commonOptions, iterations: 10,
lodSelect: lodSelectAll,
unbundled: true, unbundled: true,
lodChunkCount: 512, lodChunkCount: 512, // default value from CLI tool
lodChunkExtent: 16, lodChunkExtent: 16, // default value from CLI tool
}, },
}, },
writeFsLods, writeFsLods,
); );
for (const [name, data] of writeFsLods.results.entries()) { // 8. Organize output files into ZIP folder structure
generatedFiles.push({ // The API emits files named like: "0_0_meta.json", "0_0_means_l.webp", "env_meta.json" etc.
name, // We reorganize these into: 0_0/meta.json, 0_0/means_l.webp, env/meta.json
blob: new Blob([new Uint8Array(data).buffer]), self.postMessage({
type: "LOG",
message: "Organisiere Ordnerstruktur...",
}); });
for (const [name, data] of writeFsLods.results.entries()) {
zip.file(name, data);
} }
self.postMessage({ type: "DONE", data: { files: generatedFiles } }); // 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) { } catch (err: any) {
self.postMessage({ type: "LOG", message: `Error: ${err.message}` }); self.postMessage({ type: "LOG", message: `Fehler: ${err.message}` });
} }
} }
}; };

96
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@playcanvas/splat-transform": "^1.8.0", "@playcanvas/splat-transform": "^1.8.0",
"jszip": "^3.10.1",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
@@ -3068,6 +3069,12 @@
"webpack": "^5.1.0" "webpack": "^5.1.0"
} }
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4492,6 +4499,12 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -4523,9 +4536,7 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC", "license": "ISC"
"optional": true,
"peer": true
}, },
"node_modules/ini": { "node_modules/ini": {
"version": "1.3.8", "version": "1.3.8",
@@ -5128,6 +5139,54 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jszip/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/jszip/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5172,6 +5231,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.31.1", "version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
@@ -6029,6 +6097,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6183,6 +6257,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6649,6 +6729,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/sharp": { "node_modules/sharp": {
"version": "0.34.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -7563,9 +7649,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT", "license": "MIT"
"optional": true,
"peer": true
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.5.1", "version": "2.5.1",

View File

@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@playcanvas/splat-transform": "^1.8.0", "@playcanvas/splat-transform": "^1.8.0",
"jszip": "^3.10.1",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"