// Comprime con Draco i modelli GLB in fase di build. // // Ogni src/assets/models/.glb viene scritto compresso in /assets/models/.glb // (stesso URL di prima). Nel browser li decomprime il DRACOLoader di three.js (vedi starter.js). // // La compressione è lenta, quindi i risultati sono in cache (node_modules/.cache/draco-models) e // ricalcolati solo quando cambia il sorgente o le opzioni di compressione. import crypto from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; import { NodeIO } from '@gltf-transform/core'; import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; import { draco, weld } from '@gltf-transform/functions'; import draco3d from 'draco3dgltf'; // Bit di quantizzazione: valori più alti = più fedeltà e file più grandi. // Le posizioni a 14 bit sono sotto il decimo di millimetro sulle dimensioni di un infisso. const DRACO_OPTIONS = { method: 'edgebreaker', encodeSpeed: 5, decodeSpeed: 5, quantizePosition: 14, quantizeNormal: 10, quantizeTexcoord: 12, quantizeColor: 8, quantizeGeneric: 12, }; const CACHE_VERSION = 1; async function listGlb(dir, base = dir) { const out = []; for (const entry of await fs.readdir(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) out.push(...(await listGlb(full, base))); else if (entry.name.toLowerCase().endsWith('.glb')) out.push(path.relative(base, full)); } return out; } async function removePlainGlb(dir) { let entries; try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) await removePlainGlb(full); else if (entry.name.toLowerCase().endsWith('.glb')) await fs.rm(full); } } export default function dracoModels({ modelsDir, cacheDir }) { let outDir; let ioPromise = null; const getIO = () => (ioPromise ??= (async () => new NodeIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({ 'draco3d.encoder': await draco3d.createEncoderModule(), 'draco3d.decoder': await draco3d.createDecoderModule(), }))()); async function compress(src) { const io = await getIO(); const doc = await io.read(src); // Draco lavora su primitive indicizzate: weld unisce solo i vertici identici in tutti gli // attributi (posizione, normale, uv...), quindi non cambia l'aspetto del modello. await doc.transform(weld(), draco(DRACO_OPTIONS)); return Buffer.from(await io.writeBinary(doc)); } return { name: 'kreios-draco-models', apply: 'build', configResolved(config) { outDir = config.build.outDir; }, // sequential: parte dopo il writeBundle di vite-plugin-static-copy. writeBundle: { sequential: true, order: 'post', async handler() { // Nessun GLB non compresso deve finire online (es. file archiviati in assets/data). await removePlainGlb(path.join(outDir, 'assets')); await fs.mkdir(cacheDir, { recursive: true }); const files = await listGlb(modelsDir); let compressed = 0; let before = 0; let after = 0; for (const rel of files) { const src = path.join(modelsDir, rel); const data = await fs.readFile(src); const hash = crypto .createHash('sha256') .update(JSON.stringify({ v: CACHE_VERSION, DRACO_OPTIONS })) .update(data) .digest('hex'); const cached = path.join(cacheDir, `${hash}.glb`); let out = await fs.readFile(cached).catch(() => null); if (!out) { out = await compress(src); await fs.writeFile(cached, out); compressed++; } const dest = path.join(outDir, 'assets/models', rel); await fs.mkdir(path.dirname(dest), { recursive: true }); await fs.writeFile(dest, out); before += data.length; after += out.length; } const mb = (n) => (n / 1024 / 1024).toFixed(1); console.log( `[draco-models] ${files.length} modelli (${compressed} ricompressi): ` + `${mb(before)} MB -> ${mb(after)} MB`, ); }, }, }; }