// =====================================================================
// JS port of meshoptimizer's meshopt_buildMeshlets.
//
// Vendored from amolite/external/meshoptimizer/src/clusterizer.cpp.
// Original work by Arseny Kapoulkine, MIT licensed -- see
// amolite/external/meshoptimizer/LICENSE.md.
//
// This is a straight transliteration of the parts needed for the
// non-spatial, non-scan, non-flex variant of buildMeshlets:
//   * buildTriangleAdjacency (dense version)
//   * computeTriangleCones
//   * kd-tree (build + nearest)
//   * neighbour selection (priority + score)
//   * seed-triangle search for cluster reseeding
//   * appendMeshlet
//   * meshopt_buildMeshlets entry point
//
// Skipped: SIMD paths (no analogue in JS), the BVH-based spatial
// variant (meshopt_buildMeshletsSpatial), and the scan variant.
//
// Public API:
//   MeshOpt.buildMeshlets({
//     indices,                    // Uint32Array(3*tri_count); referenced into vertexPositions
//     vertexPositions,            // Float32Array(stride*vertex_count)
//     vertexCount,                // number of distinct vertices
//     vertexStride,               // floats between consecutive positions (3 for tightly packed)
//     maxVertices,                // <= 256
//     maxTriangles,               // <= 512
//     coneWeight,                 // 0..1
//   }) -> {
//     meshlets:          Array of { vertex_offset, triangle_offset,
//                                   vertex_count, triangle_count }
//     meshlet_vertices:  Uint32Array (vertex ids referenced by meshlets)
//     meshlet_triangles: Uint8Array  (meshlet-local 0..maxV-1 indices,
//                                     3 entries per triangle)
//   }
// =====================================================================

const MeshOpt = (() => {
  const FLT_MAX = Number.MAX_VALUE;
  const SENT    = 0xFFFFFFFF;            // ~0u

  const kMeshletMaxSeeds      = 256;
  const kMeshletAddSeeds      = 4;
  const kMeshletMaxTreeDepth  = 50;

  // ------------------------------------------------------------------
  // Triangle adjacency (dense). For each vertex, a contiguous list of
  // triangle ids that touch it. counts[v] is the live count, offsets[v]
  // is where v's neighbours begin in data[]. As triangles are emitted,
  // counts is decremented (and the slot at the end is swapped in) to
  // keep neighbour scans linear in remaining work.
  // ------------------------------------------------------------------
  function buildTriangleAdjacency(indices, indexCount, vertexCount) {
    const counts  = new Uint32Array(vertexCount);
    const offsets = new Uint32Array(vertexCount);
    const data    = new Uint32Array(indexCount);

    for (let i = 0; i < indexCount; i++) counts[indices[i]]++;

    let offset = 0;
    for (let i = 0; i < vertexCount; i++) {
      offsets[i] = offset;
      offset += counts[i];
    }

    const writeCursor = new Uint32Array(offsets);
    const faceCount = indexCount / 3;
    for (let i = 0; i < faceCount; i++) {
      const a = indices[i*3 + 0], b = indices[i*3 + 1], c = indices[i*3 + 2];
      data[writeCursor[a]++] = i;
      data[writeCursor[b]++] = i;
      data[writeCursor[c]++] = i;
    }

    return { counts, offsets, data };
  }

  function clearUsed(used, vertexCount, indices, indexCount) {
    if (vertexCount <= indexCount) {
      used.fill(-1);
    } else {
      for (let i = 0; i < indexCount; i++) used[indices[i]] = -1;
    }
  }

  // ------------------------------------------------------------------
  // Per-triangle cone (centroid + face normal). Stored flat as
  // [px,py,pz,nx,ny,nz] per triangle to mirror the C++ layout (which
  // matters for the kd-tree, which reads the same memory with a
  // float-stride).
  // ------------------------------------------------------------------
  function computeTriangleCones(indices, indexCount, vertexPositions, vertexStride) {
    const faceCount = indexCount / 3;
    const triangles = new Float32Array(faceCount * 6);
    let meshArea = 0;
    for (let i = 0; i < faceCount; i++) {
      const a = indices[i*3 + 0], b = indices[i*3 + 1], c = indices[i*3 + 2];
      const ax = vertexPositions[a*vertexStride + 0];
      const ay = vertexPositions[a*vertexStride + 1];
      const az = vertexPositions[a*vertexStride + 2];
      const bx = vertexPositions[b*vertexStride + 0];
      const by = vertexPositions[b*vertexStride + 1];
      const bz = vertexPositions[b*vertexStride + 2];
      const cx = vertexPositions[c*vertexStride + 0];
      const cy = vertexPositions[c*vertexStride + 1];
      const cz = vertexPositions[c*vertexStride + 2];
      const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
      const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
      const nx = e1y*e2z - e1z*e2y;
      const ny = e1z*e2x - e1x*e2z;
      const nz = e1x*e2y - e1y*e2x;
      const area = Math.sqrt(nx*nx + ny*ny + nz*nz);
      const invArea = area === 0 ? 0 : 1 / area;
      triangles[i*6 + 0] = (ax + bx + cx) / 3;
      triangles[i*6 + 1] = (ay + by + cy) / 3;
      triangles[i*6 + 2] = (az + bz + cz) / 3;
      triangles[i*6 + 3] = nx * invArea;
      triangles[i*6 + 4] = ny * invArea;
      triangles[i*6 + 5] = nz * invArea;
      meshArea += area;
    }
    return { triangles, meshArea };
  }

  function getMeshletScore(distance, spread, coneWeight, expectedRadius) {
    let cone = 1 - spread * coneWeight;
    if (cone < 1e-3) cone = 1e-3;
    return (1 + distance / expectedRadius * (1 - coneWeight)) * cone;
  }

  // Average + axis-normalize the accumulated cone. acc is { px, py, pz, nx, ny, nz }.
  function getMeshletCone(acc, triangleCount) {
    const result = { px: acc.px, py: acc.py, pz: acc.pz, nx: acc.nx, ny: acc.ny, nz: acc.nz };
    const cs = triangleCount === 0 ? 0 : 1 / triangleCount;
    result.px *= cs; result.py *= cs; result.pz *= cs;
    const len = result.nx*result.nx + result.ny*result.ny + result.nz*result.nz;
    const as = len === 0 ? 0 : 1 / Math.sqrt(len);
    result.nx *= as; result.ny *= as; result.nz *= as;
    return result;
  }

  // ------------------------------------------------------------------
  // Append a triangle (a,b,c) to the current meshlet. Returns true if
  // the meshlet was full and a new one had to be started before
  // appending. Mutates `meshlet` (vertex_offset/triangle_offset shift
  // when a meshlet finalises) and writes directly into
  // meshletVertices / meshletTriangles. used[] tracks the meshlet-local
  // index of each vertex (or -1 if unused).
  // ------------------------------------------------------------------
  function appendMeshlet(meshlet, a, b, c, used, meshlets, meshletVertices, meshletTriangles, meshletOffset, maxVertices, maxTriangles, split) {
    const av = used[a], bv = used[b], cv = used[c];
    let usedExtra = 0;
    if (av < 0) usedExtra++;
    if (bv < 0) usedExtra++;
    if (cv < 0) usedExtra++;
    let result = false;

    if (meshlet.vertex_count + usedExtra > maxVertices || meshlet.triangle_count >= maxTriangles || split) {
      meshlets[meshletOffset] = {
        vertex_offset:   meshlet.vertex_offset,
        triangle_offset: meshlet.triangle_offset,
        vertex_count:    meshlet.vertex_count,
        triangle_count:  meshlet.triangle_count,
      };
      for (let j = 0; j < meshlet.vertex_count; j++) {
        used[meshletVertices[meshlet.vertex_offset + j]] = -1;
      }
      meshlet.vertex_offset   += meshlet.vertex_count;
      meshlet.triangle_offset += meshlet.triangle_count * 3;
      meshlet.vertex_count = 0;
      meshlet.triangle_count = 0;
      result = true;
    }

    let aLocal = used[a];
    if (aLocal < 0) {
      aLocal = meshlet.vertex_count;
      used[a] = aLocal;
      meshletVertices[meshlet.vertex_offset + meshlet.vertex_count++] = a;
    }
    let bLocal = used[b];
    if (bLocal < 0) {
      bLocal = meshlet.vertex_count;
      used[b] = bLocal;
      meshletVertices[meshlet.vertex_offset + meshlet.vertex_count++] = b;
    }
    let cLocal = used[c];
    if (cLocal < 0) {
      cLocal = meshlet.vertex_count;
      used[c] = cLocal;
      meshletVertices[meshlet.vertex_offset + meshlet.vertex_count++] = c;
    }

    const t = meshlet.triangle_offset + meshlet.triangle_count * 3;
    meshletTriangles[t + 0] = aLocal;
    meshletTriangles[t + 1] = bLocal;
    meshletTriangles[t + 2] = cLocal;
    meshlet.triangle_count++;
    return result;
  }

  // ------------------------------------------------------------------
  // Score-based neighbour selection. For each vertex in the current
  // meshlet, scan its remaining adjacency and pick the best candidate
  // by (priority, score). Priority encodes topology preferences (don't
  // strand triangles); score is the spatial+cone metric.
  // ------------------------------------------------------------------
  function getNeighborTriangle(meshlet, meshletCone, meshletVertices, indices, adjacency, triangles, liveTriangles, used, meshletExpectedRadius, coneWeight) {
    let bestTriangle = SENT;
    let bestPriority = 5;
    let bestScore = FLT_MAX;

    for (let i = 0; i < meshlet.vertex_count; i++) {
      const idx = meshletVertices[meshlet.vertex_offset + i];
      const off = adjacency.offsets[idx];
      const cnt = adjacency.counts[idx];
      for (let j = 0; j < cnt; j++) {
        const triangle = adjacency.data[off + j];
        const a = indices[triangle*3 + 0];
        const b = indices[triangle*3 + 1];
        const c = indices[triangle*3 + 2];
        let extra = 0;
        if (used[a] < 0) extra++;
        if (used[b] < 0) extra++;
        if (used[c] < 0) extra++;

        let priority;
        if (extra === 0) priority = 0;
        else if (liveTriangles[a] === 1 || liveTriangles[b] === 1 || liveTriangles[c] === 1) priority = 1;
        else if (((liveTriangles[a] === 2 ? 1 : 0) + (liveTriangles[b] === 2 ? 1 : 0) + (liveTriangles[c] === 2 ? 1 : 0)) >= 2) priority = 1 + extra;
        else priority = 2 + extra;

        if (priority > bestPriority) continue;

        const tcx = triangles[triangle*6 + 0];
        const tcy = triangles[triangle*6 + 1];
        const tcz = triangles[triangle*6 + 2];
        const tnx = triangles[triangle*6 + 3];
        const tny = triangles[triangle*6 + 4];
        const tnz = triangles[triangle*6 + 5];
        const dx = tcx - meshletCone.px, dy = tcy - meshletCone.py, dz = tcz - meshletCone.pz;
        const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
        const spread = tnx*meshletCone.nx + tny*meshletCone.ny + tnz*meshletCone.nz;
        const score = getMeshletScore(distance, spread, coneWeight, meshletExpectedRadius);

        if (priority < bestPriority || score < bestScore) {
          bestTriangle = triangle;
          bestPriority = priority;
          bestScore = score;
        }
      }
    }
    return bestTriangle;
  }

  // ------------------------------------------------------------------
  // Seed-triangle helpers. When a meshlet finishes, we look one step
  // out from each meshlet vertex for "starve-resistant" continuation
  // candidates and bank them in a global seed pool.
  // ------------------------------------------------------------------
  function appendSeedTriangles(seeds, seedStart, meshlet, meshletVertices, indices, adjacency, triangles, liveTriangles, cornerx, cornery, cornerz) {
    const bestSeeds = new Array(kMeshletAddSeeds).fill(SENT);
    const bestLive  = new Array(kMeshletAddSeeds).fill(SENT);
    const bestScore = new Array(kMeshletAddSeeds).fill(FLT_MAX);

    for (let i = 0; i < meshlet.vertex_count; i++) {
      const idx = meshletVertices[meshlet.vertex_offset + i];
      let bestNeighbor = SENT;
      let bestNeighborLive = SENT;

      const off = adjacency.offsets[idx];
      const cnt = adjacency.counts[idx];
      for (let j = 0; j < cnt; j++) {
        const triangle = adjacency.data[off + j];
        const a = indices[triangle*3 + 0];
        const b = indices[triangle*3 + 1];
        const c = indices[triangle*3 + 2];
        const live = liveTriangles[a] + liveTriangles[b] + liveTriangles[c];
        if (live < bestNeighborLive) {
          bestNeighbor = triangle;
          bestNeighborLive = live;
        }
      }
      if (bestNeighbor === SENT) continue;

      const tcx = triangles[bestNeighbor*6 + 0];
      const tcy = triangles[bestNeighbor*6 + 1];
      const tcz = triangles[bestNeighbor*6 + 2];
      const dx = tcx - cornerx, dy = tcy - cornery, dz = tcz - cornerz;
      const bestNeighborScore = Math.sqrt(dx*dx + dy*dy + dz*dz);

      for (let j = 0; j < kMeshletAddSeeds; j++) {
        if (bestNeighborLive < bestLive[j] ||
            (bestNeighborLive === bestLive[j] && bestNeighborScore <= bestScore[j])) {
          bestSeeds[j] = bestNeighbor;
          bestLive[j]  = bestNeighborLive;
          bestScore[j] = bestNeighborScore;
          break;
        }
      }
    }

    let seedCount = 0;
    for (let i = 0; i < kMeshletAddSeeds; i++) {
      if (bestSeeds[i] !== SENT) seeds[seedStart + seedCount++] = bestSeeds[i];
    }
    return seedCount;
  }

  function pruneSeedTriangles(seeds, seedCount, emittedFlags) {
    let result = 0;
    for (let i = 0; i < seedCount; i++) {
      const idx = seeds[i];
      seeds[result] = idx;
      if (emittedFlags[idx] === 0) result++;
    }
    return result;
  }

  function selectSeedTriangle(seeds, seedCount, indices, triangles, liveTriangles, cornerx, cornery, cornerz) {
    let bestSeed  = SENT;
    let bestLive  = SENT;
    let bestScore = FLT_MAX;
    for (let i = 0; i < seedCount; i++) {
      const idx = seeds[i];
      const a = indices[idx*3 + 0];
      const b = indices[idx*3 + 1];
      const c = indices[idx*3 + 2];
      const live = liveTriangles[a] + liveTriangles[b] + liveTriangles[c];
      const tcx = triangles[idx*6 + 0];
      const tcy = triangles[idx*6 + 1];
      const tcz = triangles[idx*6 + 2];
      const dx = tcx - cornerx, dy = tcy - cornery, dz = tcz - cornerz;
      const score = Math.sqrt(dx*dx + dy*dy + dz*dz);
      if (live < bestLive || (live === bestLive && score < bestScore)) {
        bestSeed = idx;
        bestLive = live;
        bestScore = score;
      }
    }
    return bestSeed;
  }

  // ------------------------------------------------------------------
  // KD-tree over triangle centroids for fallback nearest-neighbour
  // lookup when the meshlet runs out of adjacency. C++'s union of
  // {float split | unsigned int index} becomes two named fields here;
  // bit fields {axis:2; children:30} become plain numbers.
  //
  // The tree is laid out implicitly: a branch's left subtree is at
  // root+1, and `children` is the offset to the right subtree.
  // ------------------------------------------------------------------
  function makeKDNodes(n) {
    const arr = new Array(n);
    for (let i = 0; i < n; i++) {
      arr[i] = { split: 0, index: 0, axis: 0, children: 0 };
    }
    return arr;
  }

  function kdtreePartition(indices, count, points, stride, axis, pivot) {
    let m = 0;
    for (let i = 0; i < count; i++) {
      const v = points[indices[i] * stride + axis];
      const t = indices[m];
      indices[m] = indices[i];
      indices[i] = t;
      if (v < pivot) m++;
    }
    return m;
  }

  function kdtreeBuildLeaf(offset, nodes, indices, indicesStart, count) {
    const head = nodes[offset];
    head.index    = indices[indicesStart];
    head.axis     = 3;
    head.children = count;
    for (let i = 1; i < count; i++) {
      const tail = nodes[offset + i];
      tail.index    = indices[indicesStart + i];
      tail.axis     = 3;
      tail.children = 0x3FFFFFFF;   // bogus marker (matches ~0u >> 2)
    }
    return offset + count;
  }

  function kdtreeBuild(offset, nodes, points, stride, indices, indicesStart, count, leafSize, depth) {
    if (count <= leafSize) return kdtreeBuildLeaf(offset, nodes, indices, indicesStart, count);

    const mean = [0, 0, 0], vars = [0, 0, 0];
    let runc = 1, runs = 1;
    for (let i = 0; i < count; i++, runc += 1, runs = 1 / runc) {
      const base = indices[indicesStart + i] * stride;
      for (let k = 0; k < 3; k++) {
        const v = points[base + k];
        const delta = v - mean[k];
        mean[k] += delta * runs;
        vars[k] += delta * (v - mean[k]);
      }
    }

    const axis = (vars[0] >= vars[1] && vars[0] >= vars[2]) ? 0 : (vars[1] >= vars[2] ? 1 : 2);
    const split = mean[axis];
    // partition operates on the slice indices[indicesStart .. indicesStart+count)
    const slice = indices.subarray(indicesStart, indicesStart + count);
    const middle = kdtreePartition(slice, count, points, stride, axis, split);

    if (middle <= (leafSize >> 1) || middle >= count - (leafSize >> 1) || depth >= kMeshletMaxTreeDepth) {
      return kdtreeBuildLeaf(offset, nodes, indices, indicesStart, count);
    }

    const head = nodes[offset];
    head.split = split;
    head.axis  = axis;
    const nextOffset = kdtreeBuild(offset + 1, nodes, points, stride, indices, indicesStart, middle, leafSize, depth + 1);
    head.children = nextOffset - offset - 1;
    return kdtreeBuild(nextOffset, nodes, points, stride, indices, indicesStart + middle, count - middle, leafSize, depth + 1);
  }

  // result is { result: SENT, limit: FLT_MAX } -- mutated by the recursion.
  function kdtreeNearest(nodes, root, points, stride, emittedFlags, position, result) {
    const node = nodes[root];
    if (node.children === 0) return;

    if (node.axis === 3) {
      let inactive = true;
      for (let i = 0; i < node.children; i++) {
        const idx = nodes[root + i].index;
        if (emittedFlags[idx]) continue;
        inactive = false;
        const base = idx * stride;
        const dx = points[base + 0] - position[0];
        const dy = points[base + 1] - position[1];
        const dz = points[base + 2] - position[2];
        const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
        if (distance < result.limit) {
          result.result = idx;
          result.limit  = distance;
        }
      }
      if (inactive) nodes[root].children = 0;
    } else {
      const delta = position[node.axis] - node.split;
      const first  = (delta <= 0) ? 0 : node.children;
      const second = first ^ node.children;
      if ((nodes[root + 1 + first].children | nodes[root + 1 + second].children) === 0) {
        nodes[root].children = 0;
      }
      kdtreeNearest(nodes, root + 1 + first, points, stride, emittedFlags, position, result);
      if (Math.abs(delta) <= result.limit) {
        kdtreeNearest(nodes, root + 1 + second, points, stride, emittedFlags, position, result);
      }
    }
  }

  // ------------------------------------------------------------------
  // Bound on the worst-case meshlet count.
  // ------------------------------------------------------------------
  function buildMeshletsBound(indexCount, maxVertices, maxTriangles) {
    const maxVerticesConservative = maxVertices - 2;
    const maxMeshletsVertices  = Math.floor((indexCount + maxVerticesConservative * 3 - 1) / (maxVerticesConservative * 3));
    const maxMeshletsTriangles = Math.floor((indexCount / 3 + maxTriangles - 1) / maxTriangles);
    return maxMeshletsVertices > maxMeshletsTriangles ? maxMeshletsVertices : maxMeshletsTriangles;
  }

  // ------------------------------------------------------------------
  // Top-level entry. Mirrors meshopt_buildMeshletsFlex with
  // min_triangles == max_triangles and split_factor = 0 (i.e. the
  // simple meshopt_buildMeshlets behaviour).
  // ------------------------------------------------------------------
  function buildMeshlets(args) {
    const { indices, vertexPositions, vertexCount, vertexStride, maxVertices, maxTriangles, coneWeight } = args;
    const indexCount = indices.length;

    if (indexCount % 3 !== 0)        throw new Error(`indices length ${indexCount} not divisible by 3`);
    if (maxVertices < 3 || maxVertices > 256) throw new Error(`maxVertices ${maxVertices} out of [3,256]`);
    if (maxTriangles < 1 || maxTriangles > 512) throw new Error(`maxTriangles ${maxTriangles} out of [1,512]`);
    if (coneWeight < 0 || coneWeight > 1) throw new Error(`coneWeight ${coneWeight} out of [0,1]`);
    if (indexCount === 0) {
      return { meshlets: [], meshlet_vertices: new Uint32Array(0), meshlet_triangles: new Uint8Array(0) };
    }

    const bound = buildMeshletsBound(indexCount, maxVertices, maxTriangles);
    const meshlets          = new Array(bound);
    const meshletVertices   = new Uint32Array(indexCount);  // worst case
    const meshletTriangles  = new Uint8Array(indexCount);

    const adjacency = buildTriangleAdjacency(indices, indexCount, vertexCount);
    const liveTriangles = adjacency.counts;   // alias; decremented as triangles emit

    const faceCount = indexCount / 3;
    const emittedFlags = new Uint8Array(faceCount);

    const { triangles, meshArea } = computeTriangleCones(indices, indexCount, vertexPositions, vertexStride);
    const triangleAreaAvg = faceCount === 0 ? 0 : (meshArea / faceCount) * 0.5;
    const meshletExpectedRadius = Math.sqrt(triangleAreaAvg * maxTriangles) * 0.5;

    // kd-tree over centroids. Stride = 6 (each Cone is 6 floats).
    const kdIndices = new Uint32Array(faceCount);
    for (let i = 0; i < faceCount; i++) kdIndices[i] = i;
    const nodes = makeKDNodes(faceCount * 2);
    kdtreeBuild(0, nodes, triangles, 6, kdIndices, 0, faceCount, 8, 0);

    // Find a corner of the AABB of the centroids and pick the centroid closest to it as the seed.
    let cornerx = FLT_MAX, cornery = FLT_MAX, cornerz = FLT_MAX;
    for (let i = 0; i < faceCount; i++) {
      const px = triangles[i*6 + 0], py = triangles[i*6 + 1], pz = triangles[i*6 + 2];
      if (px < cornerx) cornerx = px;
      if (py < cornery) cornery = py;
      if (pz < cornerz) cornerz = pz;
    }

    const used = new Int16Array(vertexCount);
    clearUsed(used, vertexCount, indices, indexCount);

    let initialSeed = SENT;
    let initialScore = FLT_MAX;
    for (let i = 0; i < faceCount; i++) {
      const dx = triangles[i*6 + 0] - cornerx;
      const dy = triangles[i*6 + 1] - cornery;
      const dz = triangles[i*6 + 2] - cornerz;
      const score = Math.sqrt(dx*dx + dy*dy + dz*dz);
      if (initialSeed === SENT || score < initialScore) {
        initialSeed = i;
        initialScore = score;
      }
    }

    const seeds = new Uint32Array(kMeshletMaxSeeds);
    let seedCount = 0;

    const meshlet = { vertex_offset: 0, triangle_offset: 0, vertex_count: 0, triangle_count: 0 };
    let meshletOffset = 0;
    const meshletConeAcc = { px: 0, py: 0, pz: 0, nx: 0, ny: 0, nz: 0 };

    while (true) {
      const meshletCone = getMeshletCone(meshletConeAcc, meshlet.triangle_count);

      let bestTriangle;
      if (meshletOffset === 0 && meshlet.triangle_count === 0) {
        bestTriangle = initialSeed;
      } else {
        bestTriangle = getNeighborTriangle(meshlet, meshletCone, meshletVertices, indices, adjacency, triangles, liveTriangles, used, meshletExpectedRadius, coneWeight);
      }

      if (bestTriangle === SENT) {
        const position = [meshletCone.px, meshletCone.py, meshletCone.pz];
        const out = { result: SENT, limit: FLT_MAX };
        kdtreeNearest(nodes, 0, triangles, 6, emittedFlags, position, out);
        bestTriangle = out.result;
      }
      if (bestTriangle === SENT) break;

      const ba = indices[bestTriangle*3 + 0];
      const bb = indices[bestTriangle*3 + 1];
      const bc = indices[bestTriangle*3 + 2];
      let bestExtra = 0;
      if (used[ba] < 0) bestExtra++;
      if (used[bb] < 0) bestExtra++;
      if (used[bc] < 0) bestExtra++;

      // If the candidate doesn't fit, switch to a stored seed.
      if (meshlet.vertex_count + bestExtra > maxVertices || meshlet.triangle_count >= maxTriangles) {
        seedCount = pruneSeedTriangles(seeds, seedCount, emittedFlags);
        seedCount = (seedCount + kMeshletAddSeeds <= kMeshletMaxSeeds) ? seedCount : (kMeshletMaxSeeds - kMeshletAddSeeds);
        seedCount += appendSeedTriangles(seeds, seedCount, meshlet, meshletVertices, indices, adjacency, triangles, liveTriangles, cornerx, cornery, cornerz);
        const bestSeed = selectSeedTriangle(seeds, seedCount, indices, triangles, liveTriangles, cornerx, cornery, cornerz);
        if (bestSeed !== SENT) bestTriangle = bestSeed;
      }

      const a = indices[bestTriangle*3 + 0];
      const b = indices[bestTriangle*3 + 1];
      const c = indices[bestTriangle*3 + 2];

      if (appendMeshlet(meshlet, a, b, c, used, meshlets, meshletVertices, meshletTriangles, meshletOffset, maxVertices, maxTriangles, false)) {
        meshletOffset++;
        meshletConeAcc.px = 0; meshletConeAcc.py = 0; meshletConeAcc.pz = 0;
        meshletConeAcc.nx = 0; meshletConeAcc.ny = 0; meshletConeAcc.nz = 0;
      }

      // Remove emitted triangle from each of its vertices' adjacency lists.
      for (let k = 0; k < 3; k++) {
        const idx = indices[bestTriangle*3 + k];
        const off = adjacency.offsets[idx];
        const cnt = adjacency.counts[idx];
        for (let i = 0; i < cnt; i++) {
          if (adjacency.data[off + i] === bestTriangle) {
            adjacency.data[off + i] = adjacency.data[off + cnt - 1];
            adjacency.counts[idx]--;
            break;
          }
        }
      }

      meshletConeAcc.px += triangles[bestTriangle*6 + 0];
      meshletConeAcc.py += triangles[bestTriangle*6 + 1];
      meshletConeAcc.pz += triangles[bestTriangle*6 + 2];
      meshletConeAcc.nx += triangles[bestTriangle*6 + 3];
      meshletConeAcc.ny += triangles[bestTriangle*6 + 4];
      meshletConeAcc.nz += triangles[bestTriangle*6 + 5];

      emittedFlags[bestTriangle] = 1;
    }

    if (meshlet.triangle_count) {
      meshlets[meshletOffset++] = {
        vertex_offset:   meshlet.vertex_offset,
        triangle_offset: meshlet.triangle_offset,
        vertex_count:    meshlet.vertex_count,
        triangle_count:  meshlet.triangle_count,
      };
    }

    meshlets.length = meshletOffset;
    return { meshlets, meshlet_vertices: meshletVertices, meshlet_triangles: meshletTriangles };
  }

  return { buildMeshlets, buildMeshletsBound };
})();
