UNPKG

three-virtual-geometry

Version:

Virtual geometry for three.js WebGPU: automatic cluster LOD and GPU-driven culling for billions of triangles. Independent implementation, not affiliated with Epic Games.

1,735 lines • 71 kB
#!/usr/bin/env node

// scripts/bake.ts
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, dirname, extname, join, resolve } from "node:path";
import { pathToFileURL, fileURLToPath } from "node:url";
import * as THREE2 from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
import { STLLoader } from "three/addons/loaders/STLLoader.js";
import { PLYLoader } from "three/addons/loaders/PLYLoader.js";
import { MeshoptDecoder as MeshoptDecoder2 } from "meshoptimizer";

// src/core/preprocess/buildVirtualMesh.ts
import { MeshoptClusterizer, MeshoptSimplifier as MeshoptSimplifier2 } from "meshoptimizer";

// src/core/constants.ts
var MAX_MESHLET_TRIANGLES = 128;
var MAX_MESHLET_VERTICES = 128;
var ERROR_INFINITY = 1e30;
var MESHLET_BOUNDS_STRIDE = 16;
var MESHLET_INFO_STRIDE = 4;

// src/core/preprocess/partition.ts
function partitionMeshlets(meshlets, groupSize) {
  const count = meshlets.length;
  if (count <= groupSize) return [meshlets.map((_, i) => i)];
  const edgeOwners = /* @__PURE__ */ new Map();
  for (let i = 0; i < count; i++) {
    for (const e of meshlets[i].boundaryEdges) {
      const owners = edgeOwners.get(e);
      if (owners) {
        if (owners[owners.length - 1] !== i) owners.push(i);
      } else edgeOwners.set(e, [i]);
    }
  }
  const adjacency = Array.from({ length: count }, () => /* @__PURE__ */ new Map());
  for (const owners of edgeOwners.values()) {
    for (let a = 0; a < owners.length; a++) {
      for (let b = a + 1; b < owners.length; b++) {
        const i = owners[a];
        const j = owners[b];
        adjacency[i].set(j, (adjacency[i].get(j) || 0) + 1);
        adjacency[j].set(i, (adjacency[j].get(i) || 0) + 1);
      }
    }
  }
  const order = mortonOrder(meshlets.map((m) => m.center));
  const assigned = new Uint8Array(count);
  const groups = [];
  for (const seed of order) {
    if (assigned[seed]) continue;
    const group = [seed];
    assigned[seed] = 1;
    const frontier = /* @__PURE__ */ new Map();
    const addNeighbours = (m) => {
      for (const [n, shared] of adjacency[m]) {
        if (!assigned[n]) frontier.set(n, (frontier.get(n) || 0) + shared);
      }
    };
    addNeighbours(seed);
    while (group.length < groupSize && frontier.size > 0) {
      let best = -1;
      let bestScore = -1;
      let bestDist = Infinity;
      for (const [n, score] of frontier) {
        if (assigned[n]) continue;
        const d = dist2(meshlets[n].center, meshlets[seed].center);
        if (score > bestScore || score === bestScore && d < bestDist) {
          best = n;
          bestScore = score;
          bestDist = d;
        }
      }
      if (best < 0) break;
      frontier.delete(best);
      assigned[best] = 1;
      group.push(best);
      addNeighbours(best);
    }
    groups.push(group);
  }
  const result = [];
  let pending = [];
  const minSize = Math.max(2, Math.floor(groupSize / 2));
  for (const group of groups) {
    if (group.length >= minSize) {
      result.push(group);
      continue;
    }
    if (pending.length + group.length > groupSize) {
      result.push(pending);
      pending = [];
    }
    pending.push(...group);
  }
  if (pending.length > 0) result.push(pending);
  return result;
}
function dist2(a, b) {
  const dx = a[0] - b[0];
  const dy = a[1] - b[1];
  const dz = a[2] - b[2];
  return dx * dx + dy * dy + dz * dz;
}
function mortonOrder(points) {
  const min = [Infinity, Infinity, Infinity];
  const max = [-Infinity, -Infinity, -Infinity];
  for (const p of points) {
    for (let k = 0; k < 3; k++) {
      min[k] = Math.min(min[k], p[k]);
      max[k] = Math.max(max[k], p[k]);
    }
  }
  const extent = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]) || 1;
  const codes = points.map((p) => {
    const q = (k) => Math.min(1023, Math.max(0, Math.floor((p[k] - min[k]) / extent * 1023)));
    return part1By2(q(0)) | part1By2(q(1)) << 1 | part1By2(q(2)) << 2;
  });
  return points.map((_, i) => i).sort((a, b) => codes[a] - codes[b]);
}
function part1By2(x) {
  x &= 1023;
  x = (x | x << 16) & 50331903;
  x = (x | x << 8) & 50393103;
  x = (x | x << 4) & 51130563;
  x = (x | x << 2) & 153391689;
  return x;
}

// src/core/preprocess/voxelProxy.ts
var VOXEL_ERROR_CELLS = 2;
function buildVoxelProxy(positions, colors, indices, resolution, uvs) {
  if (indices.length === 0) return null;
  const min = [Infinity, Infinity, Infinity];
  const max = [-Infinity, -Infinity, -Infinity];
  for (let i = 0; i < indices.length; i++) {
    const o = indices[i] * 3;
    for (let k = 0; k < 3; k++) {
      min[k] = Math.min(min[k], positions[o + k]);
      max[k] = Math.max(max[k], positions[o + k]);
    }
  }
  const extent = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
  if (!(extent > 0)) return null;
  const cell = extent / resolution;
  const PAD = 3;
  const n = [0, 1, 2].map((k) => Math.ceil((max[k] - min[k]) / cell) + 1 + PAD * 2);
  const origin = [0, 1, 2].map((k) => min[k] - PAD * cell);
  const [nx, ny, nz] = n;
  const sx = 1;
  const sy = nx;
  const sz = nx * ny;
  const total = nx * ny * nz;
  const occupied = new Uint8Array(total);
  const colorSum = colors ? new Float32Array(total * 3) : null;
  const colorCount = colors ? new Uint32Array(total) : null;
  const voxelUv = uvs ? new Float32Array(total * 2).fill(NaN) : null;
  let sampleU = 0;
  let sampleV = 0;
  const mark = (x, y, z, r, g, b) => {
    const i = Math.floor((x - origin[0]) / cell) * sx + Math.floor((y - origin[1]) / cell) * sy + Math.floor((z - origin[2]) / cell) * sz;
    occupied[i] = 1;
    if (voxelUv && Number.isNaN(voxelUv[i * 2])) {
      voxelUv[i * 2] = sampleU;
      voxelUv[i * 2 + 1] = sampleV;
    }
    if (colorSum) {
      colorSum[i * 3] += r;
      colorSum[i * 3 + 1] += g;
      colorSum[i * 3 + 2] += b;
      colorCount[i]++;
    }
  };
  for (let t = 0; t < indices.length; t += 3) {
    const a = indices[t] * 3;
    const b = indices[t + 1] * 3;
    const c = indices[t + 2] * 3;
    const longest = Math.max(dist(positions, a, b), dist(positions, b, c), dist(positions, c, a));
    const steps = Math.max(1, Math.ceil(longest / (cell * 0.5)));
    for (let i = 0; i <= steps; i++) {
      for (let j = 0; j <= steps - i; j++) {
        const u = i / steps;
        const v = j / steps;
        const w = 1 - u - v;
        const x = positions[a] * w + positions[b] * u + positions[c] * v;
        const y = positions[a + 1] * w + positions[b + 1] * u + positions[c + 1] * v;
        const z = positions[a + 2] * w + positions[b + 2] * u + positions[c + 2] * v;
        if (uvs) {
          const ta = indices[t] * 2;
          const tb = indices[t + 1] * 2;
          const tc = indices[t + 2] * 2;
          sampleU = uvs[ta] * w + uvs[tb] * u + uvs[tc] * v;
          sampleV = uvs[ta + 1] * w + uvs[tb + 1] * u + uvs[tc + 1] * v;
        }
        if (colors) {
          mark(
            x,
            y,
            z,
            colors[a] * w + colors[b] * u + colors[c] * v,
            colors[a + 1] * w + colors[b + 1] * u + colors[c + 1] * v,
            colors[a + 2] * w + colors[b + 2] * u + colors[c + 2] * v
          );
        } else mark(x, y, z, 0, 0, 0);
      }
    }
  }
  const closed = erode(dilate(occupied, n), n);
  const field = new Float32Array(total);
  for (let i = 0; i < total; i++) field[i] = closed[i];
  blurAxis(field, n, sx, nx);
  blurAxis(field, n, sy, ny);
  blurAxis(field, n, sz, nz);
  const ISO = 0.5;
  const cellsX = nx - 1;
  const cellsY = ny - 1;
  const cellsZ = nz - 1;
  const cellVertex = new Int32Array(cellsX * cellsY * cellsZ).fill(-1);
  const outPositions = [];
  const outNormals = [];
  const outColors = [];
  const outUvs = [];
  const corner = new Float32Array(8);
  for (let z = 0; z < cellsZ; z++) {
    for (let y = 0; y < cellsY; y++) {
      for (let x = 0; x < cellsX; x++) {
        const base = x * sx + y * sy + z * sz;
        let inside = 0;
        for (let c = 0; c < 8; c++) {
          corner[c] = field[base + (c & 1) * sx + (c >> 1 & 1) * sy + (c >> 2 & 1) * sz];
          if (corner[c] >= ISO) inside++;
        }
        if (inside === 0 || inside === 8) continue;
        let px = 0;
        let py = 0;
        let pz = 0;
        let crossings = 0;
        for (let c = 0; c < 8; c++) {
          for (const bit of [1, 2, 4]) {
            if (c & bit) continue;
            const d = c | bit;
            const f0 = corner[c];
            const f1 = corner[d];
            if (f0 >= ISO === f1 >= ISO) continue;
            const s = (ISO - f0) / (f1 - f0);
            px += (c & 1) + (bit === 1 ? s : 0);
            py += (c >> 1 & 1) + (bit === 2 ? s : 0);
            pz += (c >> 2 & 1) + (bit === 4 ? s : 0);
            crossings++;
          }
        }
        cellVertex[x + y * cellsX + z * cellsX * cellsY] = outPositions.length / 3;
        outPositions.push(origin[0] + (x + 0.5 + px / crossings) * cell, origin[1] + (y + 0.5 + py / crossings) * cell, origin[2] + (z + 0.5 + pz / crossings) * cell);
        const gx = corner[1] + corner[3] + corner[5] + corner[7] - corner[0] - corner[2] - corner[4] - corner[6];
        const gy = corner[2] + corner[3] + corner[6] + corner[7] - corner[0] - corner[1] - corner[4] - corner[5];
        const gz = corner[4] + corner[5] + corner[6] + corner[7] - corner[0] - corner[1] - corner[2] - corner[3];
        const len = Math.hypot(gx, gy, gz) || 1;
        outNormals.push(-gx / len, -gy / len, -gz / len);
        if (colorSum) {
          let r = 0;
          let g = 0;
          let b = 0;
          let count = 0;
          for (let c = 0; c < 8; c++) {
            const i = base + (c & 1) * sx + (c >> 1 & 1) * sy + (c >> 2 & 1) * sz;
            if (!colorCount[i]) continue;
            r += colorSum[i * 3];
            g += colorSum[i * 3 + 1];
            b += colorSum[i * 3 + 2];
            count += colorCount[i];
          }
          if (count) outColors.push(r / count, g / count, b / count);
          else outColors.push(NaN, NaN, NaN);
        }
        if (voxelUv) {
          let u = NaN;
          let v = NaN;
          for (let c = 0; c < 8 && Number.isNaN(u); c++) {
            const i = base + (c & 1) * sx + (c >> 1 & 1) * sy + (c >> 2 & 1) * sz;
            u = voxelUv[i * 2];
            v = voxelUv[i * 2 + 1];
          }
          outUvs.push(u, v);
        }
      }
    }
  }
  const outIndices = [];
  const cellIndex = (x, y, z) => cellVertex[x + y * cellsX + z * cellsX * cellsY];
  const quad = (a, b, c, d, nx_, ny_, nz_) => {
    const ux = outPositions[c * 3] - outPositions[a * 3];
    const uy = outPositions[c * 3 + 1] - outPositions[a * 3 + 1];
    const uz = outPositions[c * 3 + 2] - outPositions[a * 3 + 2];
    const vx = outPositions[d * 3] - outPositions[b * 3];
    const vy = outPositions[d * 3 + 1] - outPositions[b * 3 + 1];
    const vz = outPositions[d * 3 + 2] - outPositions[b * 3 + 2];
    const cx = uy * vz - uz * vy;
    const cy = uz * vx - ux * vz;
    const cz = ux * vy - uy * vx;
    if (cx * nx_ + cy * ny_ + cz * nz_ >= 0) outIndices.push(a, b, c, a, c, d);
    else outIndices.push(a, c, b, a, d, c);
  };
  for (let z = 1; z < nz - 1; z++) {
    for (let y = 1; y < ny - 1; y++) {
      for (let x = 1; x < nx - 1; x++) {
        const i = x * sx + y * sy + z * sz;
        const here = field[i] >= ISO;
        if (here !== field[i + sx] >= ISO) {
          const s = here ? 1 : -1;
          quad(cellIndex(x, y - 1, z - 1), cellIndex(x, y, z - 1), cellIndex(x, y, z), cellIndex(x, y - 1, z), s, 0, 0);
        }
        if (here !== field[i + sy] >= ISO) {
          const s = here ? 1 : -1;
          quad(cellIndex(x - 1, y, z - 1), cellIndex(x, y, z - 1), cellIndex(x, y, z), cellIndex(x - 1, y, z), 0, s, 0);
        }
        if (here !== field[i + sz] >= ISO) {
          const s = here ? 1 : -1;
          quad(cellIndex(x - 1, y - 1, z), cellIndex(x, y - 1, z), cellIndex(x, y, z), cellIndex(x - 1, y, z), 0, 0, s);
        }
      }
    }
  }
  if (outIndices.length === 0) return null;
  let proxyColors = null;
  if (colorSum) {
    proxyColors = Float32Array.from(outColors);
    fillMissing(proxyColors, outIndices, 3);
  }
  let proxyUvs = null;
  if (voxelUv) {
    proxyUvs = Float32Array.from(outUvs);
    fillMissing(proxyUvs, outIndices, 2);
  }
  return {
    positions: Float32Array.from(outPositions),
    normals: Float32Array.from(outNormals),
    colors: proxyColors,
    uvs: proxyUvs,
    indices: Uint32Array.from(outIndices),
    cellSize: cell
  };
}
function dist(p, a, b) {
  return Math.hypot(p[a] - p[b], p[a + 1] - p[b + 1], p[a + 2] - p[b + 2]);
}
function dilate(src, n) {
  let a = src;
  for (let axis = 0; axis < 3; axis++) a = morph(a, n, axis, 1);
  return a;
}
function erode(src, n) {
  let a = src;
  for (let axis = 0; axis < 3; axis++) a = morph(a, n, axis, 0);
  return a;
}
function morph(src, n, axis, grow) {
  const [nx, ny, nz] = n;
  const stride = axis === 0 ? 1 : axis === 1 ? nx : nx * ny;
  const size = n[axis];
  const out = new Uint8Array(src.length);
  for (let z = 0; z < nz; z++) {
    for (let y = 0; y < ny; y++) {
      for (let x = 0; x < nx; x++) {
        const i = x + y * nx + z * nx * ny;
        const c = axis === 0 ? x : axis === 1 ? y : z;
        const prev = c > 0 ? src[i - stride] : 0;
        const next = c < size - 1 ? src[i + stride] : 0;
        out[i] = grow ? src[i] | prev | next : src[i] & prev & next;
      }
    }
  }
  return out;
}
function blurAxis(field, n, stride, size) {
  const [nx, ny, nz] = n;
  const line = new Float32Array(size);
  const lines = nx * ny * nz / size;
  for (let l = 0; l < lines; l++) {
    let start;
    if (stride === 1) start = l * nx;
    else if (stride === nx) start = l % nx + Math.floor(l / nx) * nx * ny;
    else start = l;
    for (let k = 0; k < size; k++) line[k] = field[start + k * stride];
    for (let k = 0; k < size; k++) {
      const a = k > 0 ? line[k - 1] : 0;
      const b = k < size - 1 ? line[k + 1] : 0;
      field[start + k * stride] = (a + line[k] + b) / 3;
    }
  }
}
function fillMissing(values, indices, size) {
  const count = values.length / size;
  for (let pass = 0; pass < 8; pass++) {
    let missing = 0;
    const sum = new Float32Array(count * size);
    const hits = new Uint32Array(count);
    for (let t = 0; t < indices.length; t += 3) {
      for (let k = 0; k < 3; k++) {
        const v = indices[t + k];
        if (!Number.isNaN(values[v * size])) continue;
        for (let m = 1; m < 3; m++) {
          const u = indices[t + (k + m) % 3];
          if (Number.isNaN(values[u * size])) continue;
          for (let c = 0; c < size; c++) sum[v * size + c] += values[u * size + c];
          hits[v]++;
        }
      }
    }
    for (let v = 0; v < count; v++) {
      if (!Number.isNaN(values[v * size])) continue;
      if (hits[v]) {
        for (let c = 0; c < size; c++) values[v * size + c] = sum[v * size + c] / hits[v];
      } else missing++;
    }
    if (!missing) return;
  }
  for (let i = 0; i < values.length; i++) if (Number.isNaN(values[i])) values[i] = 0.5;
}

// src/core/preprocess/aggregate.ts
import { MeshoptSimplifier } from "meshoptimizer";
var PAIR_WINDOW = 24;
function simplifyAggregate(indices, positions, weld2, owner, group, targetTriangles) {
  const triangleCount = indices.length / 3;
  const parent = /* @__PURE__ */ new Map();
  const find = (x) => {
    let r = x;
    while (parent.get(r) !== r) r = parent.get(r);
    while (parent.get(x) !== r) {
      const next = parent.get(x);
      parent.set(x, r);
      x = next;
    }
    return r;
  };
  for (let i = 0; i < indices.length; i++) {
    const w = weld2[indices[i]];
    if (!parent.has(w)) parent.set(w, w);
  }
  for (let t = 0; t < indices.length; t += 3) {
    const a = find(weld2[indices[t]]);
    for (let k = 1; k < 3; k++) {
      const b = find(weld2[indices[t + k]]);
      if (a !== b) parent.set(b, a);
    }
  }
  const byRoot = /* @__PURE__ */ new Map();
  for (let t = 0; t < triangleCount; t++) {
    const root = find(weld2[indices[t * 3]]);
    let list = byRoot.get(root);
    if (!list) byRoot.set(root, list = []);
    list.push(t);
  }
  if (byRoot.size < 2) return null;
  const initial = [];
  for (const triangles of byRoot.values()) {
    const own = new Uint32Array(triangles.length * 3);
    let free = true;
    triangles.forEach((t, i) => {
      for (let k = 0; k < 3; k++) {
        const v = indices[t * 3 + k];
        own[i * 3 + k] = v;
        if (owner[weld2[v]] !== group) free = false;
      }
    });
    initial.push({ indices: own, free });
  }
  const candidates = [reduce(true), reduce(false)].filter((c) => c !== null);
  const reaches = candidates.filter((c) => c.indices.length / 3 <= targetTriangles * 1.25);
  const pool = reaches.length ? reaches : candidates;
  pool.sort((a, b) => reaches.length ? a.error - b.error : a.indices.length - b.indices.length);
  return pool[0] ?? null;
  function reduce(simplifyFirst) {
    let error = 0;
    const pieces = initial.map((p) => ({ ...p }));
    const ratio = targetTriangles / triangleCount;
    let total = 0;
    for (const piece of pieces) {
      const target = Math.max(6, Math.floor(piece.indices.length * ratio / 3) * 3);
      if (simplifyFirst && piece.free && piece.indices.length > target) {
        const local = compactLocal(piece.indices, positions);
        const [result, e] = MeshoptSimplifier.simplify(local.indices, local.positions, 3, target, 1e10, ["ErrorAbsolute"]);
        if (result.length > 0 && result.length < piece.indices.length) {
          piece.indices = result.map((i) => local.toGlobal[i]);
          error = Math.max(error, e);
        }
      }
      total += piece.indices.length / 3;
    }
    const newPositions = [];
    const newSources = [];
    const firstNewVertex = positions.length / 3;
    if (total > targetTriangles) {
      const free = pieces.map((p, i) => ({ p, i })).filter(({ p }) => p.free);
      const info = /* @__PURE__ */ new Map();
      for (const { p, i } of free) info.set(i, describe(p.indices, positions));
      const order = free.map(({ i }) => i);
      const bounds = boundsOf(order.map((i) => info.get(i).center));
      const span = Math.max(bounds[3] - bounds[0], bounds[4] - bounds[1], bounds[5] - bounds[2], 1e-9);
      const key = (c) => morton([(c[0] - bounds[0]) / span, (c[1] - bounds[1]) / span, (c[2] - bounds[2]) / span]);
      order.sort((a, b) => key(info.get(a).center) - key(info.get(b).center));
      const nearest = order.map((a, n) => {
        let d = Infinity;
        for (let m = Math.max(0, n - PAIR_WINDOW); m < Math.min(order.length, n + 1 + PAIR_WINDOW); m++) {
          if (m !== n) d = Math.min(d, distance(info.get(a).center, info.get(order[m]).center));
        }
        return d;
      });
      const spacing = [...nearest].sort((x, y) => x - y)[Math.floor(nearest.length / 2)];
      const taken = /* @__PURE__ */ new Set();
      for (let n = 0; n < order.length && total > targetTriangles; n++) {
        const a = order[n];
        if (taken.has(a)) continue;
        let best = -1;
        let bestDistance = spacing * 2;
        for (let m = n + 1; m < Math.min(order.length, n + 1 + PAIR_WINDOW); m++) {
          const b = order[m];
          if (taken.has(b)) continue;
          const d = distance(info.get(a).center, info.get(b).center);
          if (d <= bestDistance) {
            bestDistance = d;
            best = b;
          }
        }
        if (best < 0) continue;
        taken.add(a);
        taken.add(best);
        const [keep, drop] = info.get(a).area >= info.get(best).area ? [a, best] : [best, a];
        const k = info.get(keep);
        const dropped = info.get(drop);
        const pairArea = Math.max(k.area + dropped.area, 1e-12);
        const factor = pairArea / Math.max(k.area, 1e-12);
        const w = dropped.area / pairArea;
        const shift = [0, 1, 2].map((i) => (dropped.center[i] - k.center[i]) * w);
        const widened = widen(pieces[keep].indices, positions, k, factor, shift, firstNewVertex + newSources.length, newPositions, newSources);
        error = Math.max(error, bestDistance * Math.max(w, 1 - w), widened.growth);
        total -= pieces[drop].indices.length / 3;
        pieces[keep].indices = widened.indices;
        pieces[drop].indices = new Uint32Array(0);
      }
    }
    const out = new Uint32Array(pieces.reduce((n, p) => n + p.indices.length, 0));
    let at = 0;
    for (const p of pieces) {
      out.set(p.indices, at);
      at += p.indices.length;
    }
    if (out.length >= indices.length) return null;
    return { indices: out, error, newPositions: Float32Array.from(newPositions), newSources: Uint32Array.from(newSources), firstNewVertex };
  }
}
function widen(indices, positions, piece, areaFactor, shift, firstId, outPositions, outSources) {
  const elongated = piece.extent[0] > piece.extent[1] * 2;
  const scale = elongated ? [1, areaFactor] : [Math.sqrt(areaFactor), Math.sqrt(areaFactor)];
  const growth = Math.max((scale[0] - 1) * piece.extent[0], (scale[1] - 1) * piece.extent[1]) / 2;
  const map = /* @__PURE__ */ new Map();
  const out = new Uint32Array(indices.length);
  const c = piece.center;
  for (let i = 0; i < indices.length; i++) {
    const v = indices[i];
    let id = map.get(v);
    if (id === void 0) {
      id = firstId + map.size;
      map.set(v, id);
      const d = [positions[v * 3] - c[0], positions[v * 3 + 1] - c[1], positions[v * 3 + 2] - c[2]];
      const p = [positions[v * 3] + shift[0], positions[v * 3 + 1] + shift[1], positions[v * 3 + 2] + shift[2]];
      for (let a = 0; a < 2; a++) {
        const axis = piece.axes[a];
        const along = d[0] * axis[0] + d[1] * axis[1] + d[2] * axis[2];
        for (let k = 0; k < 3; k++) p[k] += (scale[a] - 1) * along * axis[k];
      }
      outPositions.push(p[0], p[1], p[2]);
      outSources.push(v);
    }
    out[i] = id;
  }
  return { indices: out, growth };
}
function describe(indices, positions) {
  const unique = [...new Set(indices)];
  const center = [0, 0, 0];
  for (const v of unique) for (let k = 0; k < 3; k++) center[k] += positions[v * 3 + k] / unique.length;
  const cov = [0, 0, 0, 0, 0, 0, 0, 0, 0];
  for (const v of unique) {
    const d = [positions[v * 3] - center[0], positions[v * 3 + 1] - center[1], positions[v * 3 + 2] - center[2]];
    for (let r = 0; r < 3; r++) for (let q = 0; q < 3; q++) cov[r * 3 + q] += d[r] * d[q];
  }
  const axes = eigenvectors(cov);
  const extent = [0, 0, 0];
  for (let a = 0; a < 3; a++) {
    let lo = Infinity;
    let hi = -Infinity;
    for (const v of unique) {
      const along = (positions[v * 3] - center[0]) * axes[a][0] + (positions[v * 3 + 1] - center[1]) * axes[a][1] + (positions[v * 3 + 2] - center[2]) * axes[a][2];
      lo = Math.min(lo, along);
      hi = Math.max(hi, along);
    }
    extent[a] = hi - lo;
  }
  let area = 0;
  for (let t = 0; t < indices.length; t += 3) {
    const a = indices[t] * 3;
    const b = indices[t + 1] * 3;
    const c = indices[t + 2] * 3;
    const u = [positions[b] - positions[a], positions[b + 1] - positions[a + 1], positions[b + 2] - positions[a + 2]];
    const w = [positions[c] - positions[a], positions[c + 1] - positions[a + 1], positions[c + 2] - positions[a + 2]];
    area += 0.5 * Math.hypot(u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]);
  }
  return { area, center, axes, extent };
}
function eigenvectors(m) {
  const a = [...m];
  const v = [1, 0, 0, 0, 1, 0, 0, 0, 1];
  for (let sweep = 0; sweep < 24; sweep++) {
    const off = Math.abs(a[1]) + Math.abs(a[2]) + Math.abs(a[5]);
    if (off < 1e-12 * (Math.abs(a[0]) + Math.abs(a[4]) + Math.abs(a[8]) + 1e-30)) break;
    for (const [p, q] of [
      [0, 1],
      [0, 2],
      [1, 2]
    ]) {
      const apq = a[p * 3 + q];
      if (Math.abs(apq) < 1e-30) continue;
      const theta = (a[q * 3 + q] - a[p * 3 + p]) / (2 * apq);
      const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
      const c = 1 / Math.sqrt(t * t + 1);
      const s = t * c;
      for (let k = 0; k < 3; k++) {
        const akp = a[k * 3 + p];
        const akq = a[k * 3 + q];
        a[k * 3 + p] = c * akp - s * akq;
        a[k * 3 + q] = s * akp + c * akq;
      }
      for (let k = 0; k < 3; k++) {
        const apk = a[p * 3 + k];
        const aqk = a[q * 3 + k];
        a[p * 3 + k] = c * apk - s * aqk;
        a[q * 3 + k] = s * apk + c * aqk;
      }
      for (let k = 0; k < 3; k++) {
        const vkp = v[k * 3 + p];
        const vkq = v[k * 3 + q];
        v[k * 3 + p] = c * vkp - s * vkq;
        v[k * 3 + q] = s * vkp + c * vkq;
      }
    }
  }
  const order = [0, 1, 2].sort((i, j) => a[j * 3 + j] - a[i * 3 + i]);
  return order.map((i) => {
    const x = v[i];
    const y = v[3 + i];
    const z = v[6 + i];
    const l = Math.hypot(x, y, z) || 1;
    return [x / l, y / l, z / l];
  });
}
function compactLocal(indices, positions) {
  const map = /* @__PURE__ */ new Map();
  const toGlobal = [];
  const local = new Uint32Array(indices.length);
  for (let i = 0; i < indices.length; i++) {
    let l = map.get(indices[i]);
    if (l === void 0) {
      l = toGlobal.length;
      map.set(indices[i], l);
      toGlobal.push(indices[i]);
    }
    local[i] = l;
  }
  const localPositions = new Float32Array(toGlobal.length * 3);
  toGlobal.forEach((g, i) => localPositions.set(positions.subarray(g * 3, g * 3 + 3), i * 3));
  return { indices: local, positions: localPositions, toGlobal };
}
function boundsOf(points) {
  const b = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
  for (const p of points) {
    for (let k = 0; k < 3; k++) {
      b[k] = Math.min(b[k], p[k]);
      b[k + 3] = Math.max(b[k + 3], p[k]);
    }
  }
  return b;
}
var distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
function morton(p) {
  const spread = (x) => {
    let v = Math.min(1023, Math.max(0, Math.floor(x * 1024))) & 1023;
    v = (v | v << 16) & 50331903;
    v = (v | v << 8) & 50393103;
    v = (v | v << 4) & 51130563;
    v = (v | v << 2) & 153391689;
    return v;
  };
  return (spread(p[0]) | spread(p[1]) << 1 | spread(p[2]) << 2) >>> 0;
}

// src/core/preprocess/buildVirtualMesh.ts
var DEFAULTS = {
  groupSize: 12,
  maxLodLevels: 24,
  maxGroupKeepRatio: 0.9,
  simplifyRatio: 0.5,
  minLevelReduction: 0.03,
  prune: false,
  tailSimplify: true,
  minRootTriangles: 4,
  voxelLods: true,
  voxelResolution: 64,
  aggregateLods: true
};
var TAIL_START_TRIANGLES = 1024;
var voxelResolutions = (finest) => {
  const list = [];
  for (let r = Math.max(4, Math.round(finest)); r >= 4; r = Math.floor(r / 2)) list.push(r);
  return list;
};
var yieldToBrowser = () => new Promise((r) => setTimeout(r, 0));
async function buildVirtualMesh(src, options = {}) {
  const opts = { ...DEFAULTS, ...options };
  const t0 = performance.now();
  await MeshoptClusterizer.ready;
  await MeshoptSimplifier2.ready;
  let positions = src.positions;
  let normals = src.normals;
  let colors = src.colors ?? null;
  let uvs = src.uvs ?? null;
  let weld2 = MeshoptSimplifier2.generatePositionRemap(positions, 3);
  let voxelLevels = 0;
  let aggregateLevels = 0;
  const flags = ["LockBorder", "ErrorAbsolute"];
  if (opts.prune) flags.push("Prune");
  const all = [];
  const proxyCache = /* @__PURE__ */ new Map();
  const leafs = splitIntoMeshlets(src.indices, 0, null, 0);
  let current = leafs;
  let lastTriangles = countTriangles(current);
  let lodLevels = 1;
  const estimatedTotal = Math.max(1, leafs.length * 2);
  for (let level = 1; level <= opts.maxLodLevels && current.length > 1; level++) {
    if (opts.tailSimplify && lastTriangles <= TAIL_START_TRIANGLES) break;
    if (opts.tailSimplify && opts.voxelLods && aggregateLevels > 0 && voxelBeats(current, lastTriangles)) break;
    const groups = partitionMeshlets(current, opts.groupSize);
    const next = [];
    const owner = opts.aggregateLods ? groupOwners(groups, current) : null;
    let voxelLimit = NaN;
    for (const [g, groupIdx] of groups.entries()) {
      const group = groupIdx.map((i) => current[i]);
      const merged = concatIndices(group);
      const local = compact(merged, positions);
      const target = Math.max(3, Math.floor(merged.length * opts.simplifyRatio / 3) * 3);
      const [simplified, simplifyError] = MeshoptSimplifier2.simplify(local.indices, local.positions, 3, target, 1e10, flags);
      let globalIndices = new Uint32Array(simplified.length);
      for (let i = 0; i < simplified.length; i++) globalIndices[i] = local.toGlobal[simplified[i]];
      let shapeError = Math.max(simplifyError, extentShrink(local.indices, simplified, local.positions));
      if (owner && simplified.length > target * 1.25) {
        const aggregate = simplifyAggregate(merged, positions, weld2, owner, g, target / 3);
        if (aggregate && Number.isNaN(voxelLimit)) voxelLimit = opts.tailSimplify && opts.voxelLods ? voxelError(lastTriangles) : Infinity;
        const childError2 = Math.max(...group.map((m) => m.error));
        if (aggregate && aggregate.indices.length < simplified.length && childError2 + aggregate.error < voxelLimit) {
          const s = aggregate.newSources;
          if (s.length > 0) {
            const pick = (src2, size) => {
              const out = new Float32Array(s.length * size);
              s.forEach((v, i) => out.set(src2.subarray(v * size, v * size + size), i * size));
              return out;
            };
            appendVertices(aggregate.newPositions, pick(normals, 3), colors ? pick(colors, 3) : null, uvs ? pick(uvs, 2) : null);
          }
          globalIndices = aggregate.indices;
          shapeError = Math.max(aggregate.error, extentShrink(merged, globalIndices, positions));
          aggregateLevels++;
        }
      }
      if (globalIndices.length > merged.length * opts.maxGroupKeepRatio) {
        continue;
      }
      const lodSphere = enclosingSphere(group.map((m) => m.lodSphere));
      const childError = Math.max(...group.map((m) => m.error));
      const error = childError + Math.max(shapeError, 1e-7);
      for (const child of group) {
        child.parentSphere = lodSphere;
        child.parentError = error;
      }
      if (globalIndices.length > 0) {
        const outputs = splitIntoMeshlets(globalIndices, level, lodSphere, error);
        next.push(...outputs);
        for (const child of group) child.replacements = outputs;
      }
    }
    await opts.onProgress?.(Math.min(0.99, all.length / estimatedTotal));
    await yieldToBrowser();
    if (next.length === 0) break;
    lodLevels = level + 1;
    current = next;
    const triangles = countTriangles(current);
    if (triangles > lastTriangles * (1 - opts.minLevelReduction)) break;
    lastTriangles = triangles;
  }
  if (opts.tailSimplify) lodLevels = simplifyTail(all.filter((m) => m.parentError >= ERROR_INFINITY), lodLevels);
  await opts.onProgress?.(1);
  return pack({ positions, normals, colors: colors ?? void 0, uvs: uvs ?? void 0, indices: src.indices }, all, lodLevels, voxelLevels, performance.now() - t0, aggregateLevels);
  function voxelError(triangles) {
    const target = Math.max(3, Math.floor(triangles / 2) * 3);
    let best = Infinity;
    for (const resolution of voxelResolutions(opts.voxelResolution)) {
      const proxy = voxelProxy(resolution);
      if (!proxy) continue;
      const [simplified, proxyError] = MeshoptSimplifier2.simplify(proxy.indices, proxy.positions, 3, Math.min(target, proxy.indices.length), 1e10, ["ErrorAbsolute"]);
      if (simplified.length > 0 && simplified.length <= target) best = Math.min(best, VOXEL_ERROR_CELLS * proxy.cellSize + proxyError);
    }
    return best;
  }
  function voxelBeats(meshlets, triangles) {
    let levelError = 0;
    for (const m of meshlets) levelError = Math.max(levelError, m.error);
    return voxelError(triangles) <= levelError;
  }
  function groupOwners(groups, meshlets) {
    const owner = new Int32Array(weld2.length).fill(-1);
    groups.forEach((group, g) => {
      for (const i of group) {
        const indices = meshlets[i].indices;
        for (let k = 0; k < indices.length; k++) {
          const w = weld2[indices[k]];
          if (owner[w] === -1) owner[w] = g;
          else if (owner[w] !== g) owner[w] = -2;
        }
      }
    });
    return owner;
  }
  function appendVertices(p, n, c, uv) {
    const first = positions.length / 3;
    const count = p.length / 3;
    const grow = (a, b) => {
      const out = new Float32Array(a.length + b.length);
      out.set(a);
      out.set(b, a.length);
      return out;
    };
    positions = grow(positions, p);
    normals = grow(normals, n);
    if (colors) colors = grow(colors, c ?? new Float32Array(count * 3).fill(1));
    if (uvs) uvs = grow(uvs, uv ?? new Float32Array(count * 2));
    const w = new Uint32Array(weld2.length + count);
    w.set(weld2);
    for (let i = 0; i < count; i++) w[weld2.length + i] = first + i;
    weld2 = w;
    return first;
  }
  function voxelProxy(resolution) {
    if (!proxyCache.has(resolution)) proxyCache.set(resolution, buildVoxelProxy(src.positions, src.colors, src.indices, resolution, src.uvs));
    return proxyCache.get(resolution);
  }
  function simplifyTail(roots, levels) {
    if (roots.length === 0) return levels;
    const tailFlags = ["ErrorAbsolute"];
    if (opts.prune) tailFlags.push("Prune");
    let nodes = roots;
    let level = levels;
    for (; level < levels + opts.maxLodLevels; level++) {
      const merged = concatIndices(nodes);
      if (merged.length / 3 <= opts.minRootTriangles) break;
      const local = compact(merged, positions);
      const target = Math.max(3, Math.floor(merged.length / 2 / 3) * 3);
      const keepLimit = merged.length * opts.maxGroupKeepRatio;
      const childError = Math.max(...nodes.map((m) => m.error));
      let [result, error] = MeshoptSimplifier2.simplify(local.indices, local.positions, 3, target, 1e10, tailFlags);
      if (result.length > keepLimit) {
        const [sloppy, relativeError] = MeshoptSimplifier2.simplifySloppy(local.indices, local.positions, 3, null, target, 1);
        if (sloppy.length < result.length) {
          result = sloppy;
          error = relativeError * MeshoptSimplifier2.getScale(local.positions, 3);
        }
      }
      let globalIndices = null;
      let parentError = Infinity;
      if (result.length <= keepLimit) {
        parentError = childError + Math.max(Math.max(error, extentShrink(local.indices, result, local.positions)), 1e-7);
        globalIndices = new Uint32Array(result.length);
        for (let i = 0; i < result.length; i++) globalIndices[i] = local.toGlobal[result[i]];
      }
      let voxel = null;
      if (opts.voxelLods) {
        for (const resolution of voxelResolutions(opts.voxelResolution)) {
          const proxy = voxelProxy(resolution);
          if (!proxy) continue;
          const proxyTarget = Math.min(target, proxy.indices.length);
          const [simplified, proxyError] = MeshoptSimplifier2.simplify(proxy.indices, proxy.positions, 3, proxyTarget, 1e10, ["ErrorAbsolute"]);
          if (simplified.length === 0 || simplified.length > keepLimit) continue;
          const voxelError2 = Math.max(childError * (1 + 1e-5) + 1e-7, VOXEL_ERROR_CELLS * proxy.cellSize + proxyError);
          if (voxelError2 < parentError) {
            parentError = voxelError2;
            voxel = { proxy, indices: simplified };
          }
        }
      }
      if (!Number.isFinite(parentError)) break;
      if (voxel) {
        const used = compact(voxel.indices, voxel.proxy.positions);
        const pick = (src2, size = 3) => {
          const out = new Float32Array(used.toGlobal.length * size);
          used.toGlobal.forEach((v, i) => out.set(src2.subarray(v * size, v * size + size), i * size));
          return out;
        };
        const { proxy } = voxel;
        const first = appendVertices(used.positions, pick(proxy.normals), proxy.colors ? pick(proxy.colors) : null, proxy.uvs ? pick(proxy.uvs, 2) : null);
        globalIndices = used.indices.map((i) => i + first);
        voxelLevels++;
      }
      const lodSphere = enclosingSphere(nodes.map((m) => m.lodSphere));
      for (const node of nodes) {
        node.parentSphere = lodSphere;
        node.parentError = parentError;
      }
      if (globalIndices.length === 0) return level + 1;
      const outputs = splitIntoMeshlets(globalIndices, level, lodSphere, parentError);
      for (const node of nodes) node.replacements = outputs;
      nodes = outputs;
    }
    return level;
  }
  function splitIntoMeshlets(indices, lodLevel, lodSphere, error) {
    const local = compact(indices, positions);
    const built = MeshoptClusterizer.buildMeshlets(local.indices, local.positions, 3, MAX_MESHLET_VERTICES, MAX_MESHLET_TRIANGLES, 0);
    const result = [];
    for (let m = 0; m < built.meshletCount; m++) {
      const vertexOffset = built.meshlets[m * 4 + 0];
      const triangleOffset = built.meshlets[m * 4 + 1];
      const vertexCount = built.meshlets[m * 4 + 2];
      const triangleCount = built.meshlets[m * 4 + 3];
      const vertices = new Uint32Array(vertexCount);
      for (let v = 0; v < vertexCount; v++) vertices[v] = local.toGlobal[built.vertices[vertexOffset + v]];
      const localTriangles = built.triangles.slice(triangleOffset, triangleOffset + triangleCount * 3);
      const meshletIndices = new Uint32Array(triangleCount * 3);
      for (let i = 0; i < triangleCount * 3; i++) meshletIndices[i] = vertices[localTriangles[i]];
      const cullSphere = boundingSphere(meshletIndices, positions);
      const meshlet = {
        indices: meshletIndices,
        lodLevel,
        lodSphere: lodSphere ?? cullSphere,
        error,
        parentSphere: [0, 0, 0, 0],
        parentError: ERROR_INFINITY,
        cullSphere,
        boundaryEdges: findBoundaryEdges(meshletIndices, weld2),
        center: [cullSphere[0], cullSphere[1], cullSphere[2]],
        replacements: [],
        vertices,
        localTriangles
      };
      result.push(meshlet);
      all.push(meshlet);
    }
    return result;
  }
}
function pack(src, unsorted, lodLevels, voxelLevels, buildMs, aggregateGroups) {
  const all = [...unsorted].sort((a, b) => a.lodLevel - b.lodLevel);
  const meshletCount = all.length;
  const indices = new Uint32Array(all.reduce((acc, m) => acc + m.indices.length, 0));
  const meshletBounds = new Float32Array(meshletCount * MESHLET_BOUNDS_STRIDE);
  const meshletInfo = new Uint32Array(meshletCount * MESHLET_INFO_STRIDE);
  const indexOf = /* @__PURE__ */ new Map();
  all.forEach((m, i) => indexOf.set(m, i));
  const replacementGroup = new Int32Array(meshletCount).fill(-1);
  const groupOf = /* @__PURE__ */ new Map();
  const groupStarts = [0];
  let replacementTotal = 0;
  all.forEach((m, i) => {
    if (m.replacements.length === 0) return;
    let g = groupOf.get(m.replacements);
    if (g === void 0) {
      g = groupOf.size;
      groupOf.set(m.replacements, g);
      replacementTotal += m.replacements.length;
      groupStarts.push(replacementTotal);
    }
    replacementGroup[i] = g;
  });
  const replacementStart = Uint32Array.from(groupStarts);
  const replacementIndices = new Uint32Array(replacementTotal);
  for (const [list, g] of groupOf) list.forEach((r, k) => replacementIndices[replacementStart[g] + k] = indexOf.get(r));
  const meshletVertices = new Uint32Array(all.reduce((acc, m) => acc + m.vertices.length, 0));
  const meshletTriangles = new Uint32Array(indices.length / 3);
  let firstIndex = 0;
  let vertexOffset = 0;
  const stats = { leafTriangles: 0, leafMeshlets: 0, rootTriangles: 0, rootMeshlets: 0, lodLevels, voxelLevels, aggregateGroups, buildMs };
  all.forEach((m, i) => {
    indices.set(m.indices, firstIndex);
    meshletVertices.set(m.vertices, vertexOffset);
    const t = m.localTriangles;
    for (let k = 0, tri = firstIndex / 3; k < t.length; k += 3, tri++) {
      meshletTriangles[tri] = t[k] | t[k + 1] << 8 | t[k + 2] << 16;
    }
    const b = i * MESHLET_BOUNDS_STRIDE;
    meshletBounds.set(m.lodSphere, b);
    meshletBounds.set(m.parentSphere, b + 4);
    meshletBounds[b + 8] = m.error;
    meshletBounds[b + 9] = m.parentError;
    meshletBounds.set(m.cullSphere, b + 12);
    const triangleCount = m.indices.length / 3;
    const o = i * MESHLET_INFO_STRIDE;
    meshletInfo[o + 0] = triangleCount;
    meshletInfo[o + 1] = firstIndex;
    meshletInfo[o + 2] = m.lodLevel;
    meshletInfo[o + 3] = vertexOffset;
    firstIndex += m.indices.length;
    vertexOffset += m.vertices.length;
    if (m.lodLevel === 0) {
      stats.leafTriangles += triangleCount;
      stats.leafMeshlets += 1;
    }
    if (m.parentError >= ERROR_INFINITY) {
      stats.rootTriangles += triangleCount;
      stats.rootMeshlets += 1;
    }
  });
  const whole = enclosingSphere(all.filter((m) => m.lodLevel === 0).map((m) => m.cullSphere));
  const levelCount = all.length ? all[all.length - 1].lodLevel + 1 : 0;
  const levelRanges = new Uint32Array(levelCount * 2);
  const levelErrors = new Float32Array(levelCount * 2);
  const levelGuardErrors = new Float32Array(levelCount * 2);
  for (let l = 0; l < levelCount; l++) {
    levelErrors[l * 2] = Infinity;
    levelErrors[l * 2 + 1] = 0;
    levelGuardErrors[l * 2] = 0;
    levelGuardErrors[l * 2 + 1] = Infinity;
  }
  all.forEach((m, i) => {
    const l = m.lodLevel;
    if (levelRanges[l * 2 + 1] === 0) levelRanges[l * 2] = i;
    levelRanges[l * 2 + 1]++;
    levelErrors[l * 2] = Math.min(levelErrors[l * 2], m.error);
    levelErrors[l * 2 + 1] = Math.max(levelErrors[l * 2 + 1], m.parentError);
    levelGuardErrors[l * 2] = Math.max(levelGuardErrors[l * 2], m.error);
    levelGuardErrors[l * 2 + 1] = Math.min(levelGuardErrors[l * 2 + 1], m.parentError);
  });
  const lodBoundsSphere = enclosingSphere([
    ...all.map((m) => m.lodSphere),
    ...all.filter((m) => m.parentError < ERROR_INFINITY).map((m) => m.parentSphere)
  ]);
  const cullBoundsSphere = enclosingSphere(all.map((m) => m.cullSphere));
  return {
    positions: src.positions,
    normals: src.normals,
    colors: src.colors ?? null,
    uvs: src.uvs ?? null,
    indices,
    meshletCount,
    meshletBounds,
    meshletInfo,
    replacementGroup,
    replacementStart,
    replacementIndices,
    meshletVertices,
    meshletTriangles,
    levelRanges,
    levelErrors,
    levelGuardErrors,
    lodBoundsSphere,
    cullBoundsSphere,
    boundingSphere: whole,
    stats
  };
}
function extentShrink(before, after, positions) {
  const box = (indices) => {
    const min = [Infinity, Infinity, Infinity];
    const max = [-Infinity, -Infinity, -Infinity];
    for (let i = 0; i < indices.length; i++) {
      const o = indices[i] * 3;
      for (let k = 0; k < 3; k++) {
        const v = positions[o + k];
        if (v < min[k]) min[k] = v;
        if (v > max[k]) max[k] = v;
      }
    }
    return { min, max };
  };
  const a = box(before);
  if (after.length === 0) return Math.max(a.max[0] - a.min[0], a.max[1] - a.min[1], a.max[2] - a.min[2]) / 2;
  const b = box(after);
  let shrink = 0;
  for (let k = 0; k < 3; k++) shrink = Math.max(shrink, b.min[k] - a.min[k], a.max[k] - b.max[k]);
  return shrink;
}
var localOf = new Int32Array(0);
function compact(indices, positions) {
  const vertexCount = positions.length / 3;
  if (localOf.length < vertexCount) localOf = new Int32Array(vertexCount).fill(-1);
  const toGlobalAll = new Uint32Array(indices.length);
  const localIndices = new Uint32Array(indices.length);
  let count = 0;
  for (let i = 0; i < indices.length; i++) {
    const g = indices[i];
    let l = localOf[g];
    if (l < 0) {
      l = count++;
      localOf[g] = l;
      toGlobalAll[l] = g;
    }
    localIndices[i] = l;
  }
  const toGlobal = toGlobalAll.subarray(0, count);
  const localPositions = new Float32Array(count * 3);
  for (let i = 0; i < count; i++) {
    const g = toGlobal[i];
    localOf[g] = -1;
    localPositions[i * 3 + 0] = positions[g * 3 + 0];
    localPositions[i * 3 + 1] = positions[g * 3 + 1];
    localPositions[i * 3 + 2] = positions[g * 3 + 2];
  }
  return { indices: localIndices, positions: localPositions, toGlobal };
}
var edgeTableKeys = new Float64Array(1024);
var edgeTableCounts = new Uint8Array(1024);
var edgeOrder = new Int32Array(512);
function findBoundaryEdges(indices, weld2) {
  const edgeCount = indices.length;
  let size = edgeTableKeys.length;
  if (size < edgeCount * 2) {
    while (size < edgeCount * 2) size <<= 1;
    edgeTableKeys = new Float64Array(size);
    edgeTableCounts = new Uint8Array(size);
    edgeOrder = new Int32Array(size / 2);
  }
  const mask = size - 1;
  let inserted = 0;
  for (let t = 0; t < indices.length; t += 3) {
    for (let k = 0; k < 3; k++) {
      const a = weld2[indices[t + k]];
      const b = weld2[indices[t + (k + 1) % 3]];
      const key = edgeKey(a, b);
      let h = (Math.imul(a < b ? a : b, 2654435761) ^ Math.imul(a < b ? b : a, 2246822507)) & mask;
      while (edgeTableCounts[h] !== 0 && edgeTableKeys[h] !== key) h = h + 1 & mask;
      if (edgeTableCounts[h] === 0) {
        edgeTableKeys[h] = key;
        edgeOrder[inserted++] = h;
      }
      if (edgeTableCounts[h] < 255) edgeTableCounts[h]++;
    }
  }
  const result = [];
  for (let i = 0; i < inserted; i++) {
    const h = edgeOrder[i];
    if (edgeTableCounts[h] === 1) result.push(edgeTableKeys[h]);
    edgeTableCounts[h] = 0;
  }
  return result;
}
var edgeKey = (a, b) => a < b ? a * 67108864 + b : b * 67108864 + a;
function concatIndices(meshlets) {
  const result = new Uint32Array(meshlets.reduce((acc, m) => acc + m.indices.length, 0));
  let offset = 0;
  for (const m of meshlets) {
    result.set(m.indices, offset);
    offset += m.indices.length;
  }
  return result;
}
var countTriangles = (meshlets) => meshlets.reduce((acc, m) => acc + m.indices.length / 3, 0);
function boundingSphere(indices, positions) {
  const min = [Infinity, Infinity, Infinity];
  const max = [-Infinity, -Infinity, -Infinity];
  for (let i = 0; i < indices.length; i++) {
    const o = indices[i] * 3;
    for (let k = 0; k < 3; k++) {
      const v = positions[o + k];
      if (v < min[k]) min[k] = v;
      if (v > max[k]) max[k] = v;
    }
  }
  const cx = (min[0] + max[0]) / 2;
  const cy = (min[1] + max[1]) / 2;
  const cz = (min[2] + max[2]) / 2;
  let r2 = 0;
  for (let i = 0; i < indices.length; i++) {
    const o = indices[i] * 3;
    const dx = positions[o] - cx;
    const dy = positions[o + 1] - cy;
    const dz = positions[o + 2] - cz;
    r2 = Math.max(r2, dx * dx + dy * dy + dz * dz);
  }
  return [cx, cy, cz, Math.sqrt(r2)];
}
function enclosingSphere(spheres) {
  if (spheres.length === 1) return [...spheres[0]];
  const centers = new Float32Array(spheres.length * 3);
  const radii = new Float32Array(spheres.length);
  spheres.forEach((s, i) => {
    centers.set([s[0], s[1], s[2]], i * 3);
    radii[i] = s[3];
  });
  const b = MeshoptClusterizer.computeSphereBounds(centers, 3, radii, 1);
  let radius = b.radius;
  for (const s of spheres) {
    const d = Math.hypot(s[0] - b.centerX, s[1] - b.centerY, s[2] - b.centerZ) + s[3];
    radius = Math.max(radius, d);
  }
  return [b.centerX, b.centerY, b.centerZ, radius * 1.0001];
}

// src/core/io/serialize.ts
import { MeshoptDecoder, MeshoptEncoder } from "meshoptimizer";
var VG_FORMAT_VERSION = 1;
var MAGIC = 1329940310;
var PREFIX_BYTES = 16;
var TYPED_ARRAYS = {
  Int8Array,
  Uint8Array,
  Uint8ClampedArray,
  Int16Array,
  Uint16Array,
  Int32Array,
  Uint32Array,
  Float32Array,
  Float64Array
};
var FIELD_HINTS = {
  positions: { strides: [12] },
  normals: { strides: [12] },
  colors: { strides: [12] },
  uvs: { strides: [8] },
  tangents: { strides: [16] },
  meshletBounds: { strides: [64], deflateRaw: true },
  meshletInfo: { strides: [16] }
};
var VirtualMeshFormatError = class extends Error {
  constructor(message) {
    super(`three-virtual-geometry: ${message}`);
    this.name = "VirtualMeshFormatError";
  }
};
async function encodeVirtualMesh(data, options = {}) {
  const compress = options.compress ?? true;
  if (compress) await MeshoptEncoder.ready;
  const record = data;
  const keys = Object.keys(record).filter((k) => record[k] !== void 0 && typeof record[k] !== "function");
  const fields = [];
  const blobs = [];
  const values = {};
  let offset = 0;
  for (const name of keys) {
    const value = record[name];
    if (!ArrayBuffer.isView(value)) {
      values[name] = value;
      continue;
    }
    const type = value.constructor.name;
    if (!(type in TYPED_ARRAYS)) throw new VirtualMeshFormatError(`cannot serialize field "${name}" of type ${type}`);
    const array = value;
    let blob;
    let entry;
    if (name === "indices" && !options.keepIndices && indicesMatchMeshlets(data)) {
      entry = { name, type, length: array.length, encoding: "meshlet-indices" };
      blob = new Uint8Array(0);
    } else if (compress) {
      ({ entry, blob } = await encodeField(name, type, array, data, options.deflate ?? true));
    } else {
      entry = { name, type, length: array.length, encoding: "raw" };
      blob = bytesOf(array);
    }
    fields.push({ ...entry, offset, byteLength: blob.length });
    blobs.push(blob);
    offset += align4(blob.length);
  }
  const headerJson = { generator: "three-virtual-geometry", keys, fields, values };
  const headerBytes = new TextEncoder().encode(JSON.stringify(headerJson, jsonReplacer));
  const headerPadded = align4(headerBytes.length);
  const out = new Uint8Array(PREFIX_BYTES + headerPadded + offset);
  out.set(headerBytes, PREFIX_BYTES);
  out.fill(32, PREFIX_BYTES + headerBytes.length, PREFIX_BYTES + headerPadded);
  const payloadStart = PREFIX_BYTES + headerPadded;
  fields.forEach((f, i) => out.set(blobs[i], payloadStart + f.offset));
  const view = new DataView(out.buffer);
  view.setUint32(0, MAGIC, true);
  view.setUint32(4, VG_FORMAT_VERSION, true);
  view.setUint32(8, headerBytes.length, true);
  view.setUint32(12, checksum(out, PREFIX_BYTES), true);
  return out;
}
async function encodeField(name, type, array, data, useDeflate) {
  const raw = bytesOf(array);
  const base = { name, type, length: array.length };
  const hint = FIELD_HINTS[name] ?? {};
  const rawCandidate = { entry: { ...base, encoding: "raw" }, blob: raw };
  const candidates = [rawCandidate];
  const triangles = name === "meshletTriangles" && array instanceof Uint32Array ? encodeMeshletTriangles(array, data.meshletInfo) : null;
  if (triangles) candidates.push({ entry: { ...base, encoding: "meshlet-triangles" }, blob: triangles });
  else if (raw.length % 4 === 0 && raw.length > 0) {
    const strides = (hint.strides ?? [Math.max(4, array.BYTES_PER_ELEMENT)]).filter((s) => s % 4 === 0 && s <= 256 && raw.length % s === 0);
    for (const stride of strides) {
      const blob = MeshoptEncoder.encodeVertexBufferLevel(raw, raw.length / stride, stride, 2, 1);
      candidates.push({ entry: { ...base, encoding: "meshopt-vertex", stride }, blob });
    }
  }
  candidates.sort((a, b) => a.blob.length - b.blob.length);
  if (useDeflate && canDeflate() && raw.length >= 64) {
    const toDeflate = hint.deflateRaw && candidates[0] !== rawCandidate ? [candidates[0], rawCandidate] : [candidates[0]];
    for (const c of toDeflate) candidates.push({ entry: { ...c.entry, deflate: true }, blob: await deflate(c.blob) });
    candidates.sort((a, b) => a.blob.length - b.blob.length);
  }
  for (const c of candidates) {
    const decoded = await decodeField({ ...c.entry, offset: 0, byteLength: c.blob.length }, c.blob, { meshletInfo: data.meshletInfo });
    if (sameBytes(bytesOf(decoded), raw)) return c;
  }
  return rawCandidate;
}
function encodeMeshletTriangles(triangles, meshletInfo) {
  if (!(meshletInfo instanceof Uint32Array)) return null;
  const slots = new Uint32Array(triangles.length * 3);
  const covered = new Uint8Array(triangles.length);
  for (let m = 0; m < meshletInfo.length; m += MESHLET_INFO_STRIDE) {
    const count = meshletInfo[m];
    const first = meshletInfo[m + 1] / 3;
    const vertexOffset = meshletInfo[m + 3];
    if (first + count > triangles.length) return null;
    for (let t = first; t < first + count; t++) {
      const packed = triangles[t];
      if (covered[t] || packed >>> 24 !== 0) return null;
      covered[t] = 1;
      slots[t * 3] = vertexOffset + (packed & 255);
      slots[t * 3 + 1] = vertexOffset + (packed >> 8 & 255);
      slots[t * 3 + 2] = vertexOffset + (packed >> 16 & 255);
    }
  }
  if (!covered.every((c) => c === 1)) return null;
  const codec = MeshoptEncoder.encodeIndexBuffer(bytesOf(slots), slots.length, 4);
  const decoded = new Uint32Array(slots.length);
  MeshoptDecoder.decodeIndexBuffer(bytesOf(decoded), slots.length, 4, codec);
  const rotations = new Uint8Array(Math.ceil(triangles.length / 4));
  for (let t = 0; t < triangles.length; t++) {
    const a = slots[t * 3], b = slots[t * 3 + 1], c = slots[t * 3 + 2];
    const x = decoded[t * 3], y = decoded[t * 3 + 1], z = decoded[t * 3 + 2];
    let r;
    if (a === x && b === y && c === z) r = 0;
    else if (a === y && b === z && c === x) r = 1;
    else if (a === z && b === x && c === y) r = 2;
    else return null;
    rotations[t >> 2] |= r << (t & 3) * 2;
  }
  const out = new Uint8Array(4 + align4(codec.length) + rotations.length);
  new DataView(out.buffer).setUint32(0, codec.length, true);
  out.set(codec, 4);
  out.set(rotations, 4 + align4(codec.length));
  return out;
}
function indicesMatchMeshlets(data) {
  const { indices, meshletInfo, meshletVertices, meshletTriangles } = data;
  if (!(indices instanceof Uint32Array && meshletInfo instanceof Uint32Array && meshletVertices instanceof Uint32Array && meshletTriangles instanceof Uint32Array)) {
    return false;
  }
  try {
    return sameBytes(bytesOf(rebuildIndices(indices.length, meshletInfo, meshletVertices, meshletTriangles)), bytesOf(indices));
  } catch {
    return false;
  }
}
async function decodeField(field, blob, arrays) {
  const Ctor = TYPED_ARRAYS[field.type];
  const out = new Ctor(field.length);
  const target = bytesOf(out);
  const src = field.deflate ? await inflate(blob) : blob;
  switch (field.encoding) {
    case "raw":
      if (src.length !== target.length) throw new VirtualMeshFormatError(`geometry file is corrupt (field "${field.name}" has the wrong size)`);
      target.set(src);
      break;
    case "meshopt-vertex": {
      const stride = field.stride ?? 0;
      if (stride <= 0 || stride % 4 !== 0 || target.length % stride !== 0) throw new VirtualMeshFormatError(`geometry file is corrupt (field "${field.name}" stride)`);
      MeshoptDecoder.decodeVertexBuffer(target, target.length / stride, stride, src);
      break;
    }
    case "meshlet-triangles":
      decodeMeshletTriangles(out, src, requireArray(arrays, "meshletInfo", field.name));
      break;
    case "meshlet-indices":
      out.set(
        rebuildIndices(
          field.length,
          requireArray(arrays, "meshletInfo", field.name),
          requireArray(arrays, "meshletVertices", field.name),
          requireArray(arrays, "meshletTriangles", field.name)
        )
      );
      break;
    default:
      throw new VirtualMeshFormatError(`unknown encoding "${field.encoding}" for field "${field.name}"`);
  }
  return out;
}
function requireArray(arrays, name, forField) {
  const a = arrays[name];
  if (!(a instanceof Uint32Array)) throw new VirtualMeshFormatError(`geometry file is corrupt ("${forField}" needs "${name}")`);
  return a;
}
function decodeMeshletTriangles(out, blob, meshletInfo) {
  const codecLength = new DataView(blob.buffer, blob.byteOffset, blob.byteLength).getUint32(0, true);
  const rotationsStart = 4 + align4(codecLength);
  if (rotationsStart + Math.ceil(out.length / 4) > blob.length) throw new Error("truncated triangle data");
  const slots = new Uint32Array(out.length * 3);
  MeshoptDecoder.decodeIndexBuffer(bytesOf(slots), slots.length, 4, blob.subarray(4, 4 + codecLength));
  const rotations = blob.subarray(rotationsStart);
  for (let m = 0; m < meshletInfo.length; m += MESHLET_INFO_STRIDE) {
    const count = meshletInfo[m];
    const first = meshletInfo[m + 1] / 3;
    const vertexOffset = meshletInfo[m + 3];
    if (first + count > out.length) throw new Error("meshlet range out of bounds");
    for (let t = first; t < first + count; t++) {
      const r = rotations[t >> 2] >> (t & 3) * 2 & 3;
      const x = slots[t * 3] - vertexOffset, y = slots[t * 3 + 1] - vertexOffset, z = slots[t * 3 + 2] - vertexOffset;
      const [a, b, c] = r === 0 ? [x, y, z] : r === 1 ? [y, z, x] : [z, x, y];
      if ((a | b | c) >>> 8 !== 0) throw new Error("meshlet-local vertex id out of range");
      out[t] = a | b << 8 | c << 16;
    }
  }
}
function rebuildIndices(length, meshletInfo, meshletVertices, meshletTriangles) {
  const indices = new Uint32Array(length);
  for (let m = 0; m < meshletInfo.length; m += MESHLET_INFO_STRIDE) {
    const count = meshletInfo[m];
    const first = meshletInfo[m + 1];
    const vertexOffset = meshletInfo[m + 3];
    if (first + count * 3 > length || first / 3 + count > meshletTriangles.length) throw new VirtualMeshFormatError("geometry file is corrupt (meshlet range out of bounds)");
    for (let t = 0; t < count; t++) {
      const packed = meshletTriangles[first / 3 + t];
      for (let k = 0; k < 3; k++) {
        const v = vertexOffset + (packed >> 8 * k & 255);
        if (v >= meshletVertices.length) throw new VirtualMeshFormatError("geometry file is corrupt (meshlet vertex out of bounds)");
        indices[first + t * 3 + k] = meshletVertices[v];
      }
    }
  }
  return indices;
}
var align4 = (n) => n + 3 & ~3;
function bytesOf(a) {
  return new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
}
function sameBytes(a, b) {
  if (a.length !== b.length) return false;
  if (a.byteOffset % 4 === 0 && b.byteOffset % 4 === 0 && a.length % 4 === 0) {
    const x = new Uint32Array(a.buffer, a.byteOffset, a.length / 4);
    const y = new Uint32Array(b.buffer, b.byteOffset, b.length / 4);
    for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
    return true;
  }
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
  return true;
}
function checksum(bytes, start) {
  const words = new Uint32Array(bytes.buffer, bytes.byteOffset + start, bytes.length - start >> 2);
  let h = 2166136261 ^ words.length;
  for (let i = 0; i < words.length; i++) {
    h = Math.imul(h ^ words[i], 16777619);
    h ^= h >>> 15;
  }
  return h >>> 0;
}
var canDeflate = () => typeof CompressionStream !== "undefined" && typeof DecompressionStream !== "undefined";
async function deflate(bytes) {
  const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
  return new Uint8Array(await new Response(stream).arrayBuffer());
}
async function inflate(bytes) {
  if (typeof DecompressionStream === "undefined") throw new VirtualMeshFormatError("this environment cannot decompress the geometry file (no DecompressionStream)");
  const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
  return new Uint8Array(await new Response(stream).arrayBuffer());
}
function jsonReplacer(key, value) {
  if (typeof value === "number" && !Number.isFinite(value)) return { $number: String(value) };
  if (ArrayBuffer.isView(value)) throw new VirtualMeshFormatError(`typed array nested inside field "${key}" cannot be serialized; store it as a top-level field`);
  return value;
}

// src/source.ts
import * as THREE from "three/webgpu";
var POSITION_TOLERANCE = 1e-6;
var ATTRIBUTE_TOLERANCE = 1e-4;
function fromBufferGeometry(geometry, options = {}) {
  const position = geometry.attributes.position;
  const uv = options.uvs ?? true ? geometry.attributes.uv : void 0;
  const color = options.colors ? geometry.attributes.color : void 0;
  const normal = options.normals === "keep" ? geometry.attributes.normal : void 0;
  const index = geometry.index;
  const total = index ? index.count : position.count;
  const start = Math.max(0, Math.min(total, options.range?.start ?? 0));
  const count = Math.floor((Math.min(total, start + (options.range?.count ?? total)) - start) / 3) * 3;
  const vertexOf = (corner) => index ? index.getX(start + corner) : start + corner;
  const byPosition = weld(count, vertexOf, [{ attribute: position, size: 3, tolerance: POSITION_TOLERANCE }]);
  const positions = gather(byPosition.firstVertex, position, 3);
  let smoothNormals = null;
  if (!normal) {
    const g = new THREE.BufferGeometry();
    g.setAttribute("position", new THREE.BufferAttribute(positions, 3));
    g.setIndex(new THREE.BufferAttribute(byPosition.indices, 1));
    g.computeVertexNormals();
    smoothNormals = g.attributes.normal.array;
  }
  if (!uv && !color && !normal) return { positions, normals: smoothNormals, indices: byPosition.indices };
  const channels = [{ attribute: position, size: 3, tolerance: POSITION_TOLERANCE }];
  if (uv) channels.push({ attribute: uv, size: 2, tolerance: POSITION_TOLERANCE });
  if (normal) channels.push({ attribute: normal, size: 3, tolerance: ATTRIBUTE_TOLERANCE });
  if (color) channels.push({ attribute: color, size: 3, tolerance: ATTRIBUTE_TOLERANCE });
  const full = weld(count, vertexOf, channels);
  const result = { positions: gather(full.firstVertex, position, 3), normals: new Float32Array(0), indices: full.indices };
  if (normal) {
    result.normals = gather(full.firstVertex, normal, 3);
    for (let i = 0; i < result.normals.length; i += 3) {
      const l = Math.hypot(result.normals[i], result.normals[i + 1], result.normals[i + 2]) || 1;
      result.normals[i] /= l;
      result.normals[i + 1] /= l;
      result.normals[i + 2] /= l;
    }
  } else {
    result.normals = new Float32Array(full.firstVertex.length * 3);
    for (let w = 0; w < full.firstVertex.length; w++) {
      const p = byPosition.indices[full.firstCorner[w]];
      result.normals.set(smoothNormals.subarray(p * 3, p * 3 + 3), w * 3);
    }
  }
  if (uv) result.uvs = gather(full.firstVertex, uv, 2);
  if (color) result.colors = gather(full.firstVertex, color, 3);
  return result;
}
function gather(vertices, attribute, size) {
  const out = new Float32Array(vertices.length * size);
  for (let i = 0; i < vertices.length; i++) {
    for (let k = 0; k < size; k++) out[i * size + k] = attribute.getComponent(vertices[i], k);
  }
  return out;
}
function weld(count, vertexOf, channels) {
  const width = channels.reduce((a, c) => a + c.size, 0);
  const quantize = channels.map(({ tolerance }) => {
    const multiplier = Math.pow(10, Math.log10(1 / tolerance));
    return { multiplier, additive: tolerance * 0.5 * multiplier };
  });
  let size = 1;
  while (size < count * 2) size <<= 1;
  const mask = size - 1;
  const table = new Int32Array(size).fill(-1);
  const keys = new Float64Array(count * width);
  const key = new Float64Array(width);
  const firstVertex = new Uint32Array(count);
  const firstCorner = new Uint32Array(count);
  const indices = new Uint32Array(count);
  let welded = 0;
  for (let i = 0; i < count; i++) {
    const v = vertexOf(i);
    let h = 0;
    let o = 0;
    for (let c = 0; c < channels.length; c++) {
      const { attribute, size: n } = channels[c];
      const { multiplier, additive } = quantize[c];
      for (let k = 0; k < n; k++, o++) {
        key[o] = Math.trunc(attribute.getComponent(v, k) * multiplier + additive);
        h = h ^ Math.imul(key[o] | 0, HASH_PRIMES[o % HASH_PRIMES.length]) | 0;
      }
    }
    h &= mask;
    for (; ; ) {
      const w = table[h];
      if (w < 0) {
        table[h] = welded;
        keys.set(key, welded * width);
        firstVertex[welded] = v;
        firstCorner[welded] = i;
        indices[i] = welded++;
        break;
      }
      let same = true;
      for (let k = 0; k < width && same; k++) same = keys[w * width + k] === key[k];
      if (same) {
        indices[i] = w;
        break;
      }
      h = h + 1 & mask;
    }
  }
  return { indices, firstVertex: firstVertex.slice(0, welded), firstCorner: firstCorner.slice(0, welded) };
}
var HASH_PRIMES = [73856093, 19349663, 83492791, 2654435761, 2246822519, 3266489917, 668265263, 374761393, 1181783497, 3735928559, 2028178513];

// scripts/bake.ts
var USAGE = "usage: npx vg-bake <model.glb|model.gltf|model.obj|model.stl|model.ply> [-o outDir] [--prune] [--no-voxel] [--no-deflate]";
function parseArgs(argv) {
  const args = { input: "", outDir: "", build: {}, deflate: true };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === "-o" || a === "--out") args.outDir = argv[++i] ?? "";
    else if (a === "--prune") args.build.prune = true;
    else if (a === "--no-voxel") args.build.voxelLods = false;
    else if (a === "--no-deflate") args.deflate = false;
    else if (a === "-h" || a === "--help") {
      console.log(USAGE);
      process.exit(0);
    } else if (a.startsWith("-")) throw new Error(`unknown option ${a}
${USAGE}`);
    else args.input = a;
  }
  if (!args.input) throw new Error(USAGE);
  args.outDir ||= `${basename(args.input, extname(args.input))}.vg`;
  return args;
}
function serveFileUrls() {
  const g = globalThis;
  g.ProgressEvent ??= class extends Event {
    constructor(type, init = {}) {
      super(type);
      Object.assign(this, init);
    }
  };
  const nodeFetch = globalThis.fetch;
  globalThis.fetch = (async (input, init) => {
    const url = input instanceof Request ? input.url : String(input);
    if (!url.startsWith("file:")) return nodeFetch(input, init);
    return new Response(readFileSync(fileURLToPath(url)));
  });
}
async function load(path) {
  const ext = extname(path).toLowerCase();
  if (ext === ".obj") return new OBJLoader().parse(readFileSync(path, "utf8"));
  if (ext === ".stl" || ext === ".ply") {
    const file2 = readFileSync(path);
    const bytes = file2.buffer.slice(file2.byteOffset, file2.byteOffset + file2.byteLength);
    const geometry = ext === ".stl" ? new STLLoader().parse(bytes) : new PLYLoader().parse(bytes);
    return new THREE2.Mesh(geometry, new THREE2.MeshStandardMaterial());
  }
  if (ext !== ".glb" && ext !== ".gltf") throw new Error(`unsupported file type ${ext} (expected .glb, .gltf, .obj, .stl or .ply)`);
  serveFileUrls();
  const loader = new GLTFLoader();
  loader.setMeshoptDecoder(MeshoptDecoder2);
  loader.register(
    (parser) => ({
      name: "three-virtual-geometry-skip-textures",
      beforeRoot: () => {
        for (const material of parser.json.materials ?? []) stripTextures(material);
        return null;
      }
    })
  );
  const file = readFileSync(path);
  const data = ext === ".gltf" ? file.toString("utf8") : file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
  const resourcePath = pathToFileURL(dirname(resolve(path))).href + "/";
  const gltf = await new Promise((ok, fail) => loader.parse(data, resourcePath, ok, fail));
  return gltf.scene;
}
function stripTextures(node) {
  const record = node;
  for (const key of Object.keys(record)) {
    if (key.endsWith("Texture")) delete record[key];
    else if (record[key] && typeof record[key] === "object") stripTextures(record[key]);
  }
}
function materialInfo(material) {
  const m = Array.isArray(material) ? material[0] : material;
  if (!m) return null;
  return {
    name: m.name || void 0,
    color: m.color ? `#${m.color.getHexString()}` : void 0,
    roughness: m.roughness === void 0 ? void 0 : Number(m.roughness.toFixed(4)),
    metalness: m.metalness === void 0 ? void 0 : Number(m.metalness.toFixed(4)),
    doubleSided: m.side === THREE2.DoubleSide || void 0
  };
}
var slug = (s) => s.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48) || "mesh";
async function main() {
  const args = parseArgs(process.argv.slice(2));
  const t0 = performance.now();
  const root = await load(args.input);
  root.updateMatrixWorld(true);
  const byGeometry = /* @__PURE__ */ new Map();
  root.traverse((o) => {
    const mesh = o;
    if (!mesh.isMesh || !mesh.geometry?.attributes.position) return;
    if (mesh.isPoints || mesh.isLine) return;
    let entry = byGeometry.get(mesh.geometry);
    if (!entry) byGeometry.set(mesh.geometry, entry = { name: mesh.name || mesh.geometry.name, material: mesh.material, instances: [] });
    const instanced = mesh;
    if (instanced.isInstancedMesh) {
      const m = new THREE2.Matrix4();
      for (let i = 0; i < instanced.count; i++) {
        instanced.getMatrixAt(i, m);
        entry.instances.push(m.premultiply(mesh.matrixWorld).toArray());
      }
    } else {
      entry.instances.push(mesh.matrixWorld.toArray());
    }
  });
  if (byGeometry.size === 0) throw new Error(`no triangle meshes found in ${args.input}`);
  console.log(`${args.input}: ${byGeometry.size} unique geometries (loaded in ${(performance.now() - t0).toFixed(0)} ms)`);
  mkdirSync(args.outDir, { recursive: true });
  const meshes = [];
  let index = 0;
  for (const [geometry, entry] of byGeometry) {
    const file = `${index++}-${slug(entry.name)}.vgeo`;
    const source = fromBufferGeometry(geometry);
    if (source.indices.length < 3) {
      console.log(`  ${file}: skipped (no triangles)`);
      continue;
    }
    const data = await buildVirtualMesh(source, args.build);
    const t = performance.now();
    const bytes = await encodeVirtualMesh(data, { deflate: args.deflate });
    const encodeMs = performance.now() - t;
    writeFileSync(join(args.outDir, file), bytes);
    const s = data.stats;
    console.log(
      `  ${file}: ${s.leafTriangles} triangles, ${data.meshletCount} meshlets, ${s.lodLevels} LODs, built in ${s.buildMs.toFixed(0)} ms, encoded in ${encodeMs.toFixed(0)} ms, ${(bytes.length / 1024).toFixed(0)} KB, ${entry.instances.length} instance(s)`
    );
    meshes.push({
      name: entry.name,
      file,
      bytes: bytes.length,
      triangles: s.leafTriangles,
      meshlets: data.meshletCount,
      lodLevels: s.lodLevels,
      material: materialInfo(entry.material),
      instances: entry.instances
    });
  }
  const manifest = { generator: "three-virtual-geometry", formatVersion: VG_FORMAT_VERSION, source: basename(args.input), build: args.build, meshes };
  const json = JSON.stringify(manifest, (k, v) => k === "instances" ? v.map((m) => JSON.stringify(m)) : v, 2).replace(/"(\[[^"]*\])"/g, "$1");
  writeFileSync(join(args.outDir, "manifest.json"), json);
  console.log(`wrote ${meshes.length} .vgeo files and manifest.json to ${args.outDir} in ${((performance.now() - t0) / 1e3).toFixed(1)} s`);
}
main().catch((e) => {
  console.error(e instanceof Error ? e.message : e);
  process.exit(1);
});