All files / src/cad mesh-consolidator.ts

90.9% Statements 50/55
66.66% Branches 8/12
100% Functions 1/1
90.9% Lines 50/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87          1x 1x                       1x 21x 21x 21x 21x       21x 21x     21x 21x   21x 1137x 692x 692x   692x 692x 692x 123x 123x     692x 692x 692x 692x 21x     21x         21x     21x 123x 123x 123x 123x 123x 123x 123x 123x 123x 123x 123x 123x 123x 123x   123x   123x 692x 692x 123x 123x   21x 21x  
// src/cad/mesh-consolidator.ts
// Performance optimizer for LEGO models:
// Consolidates hundreds of individual part meshes into a few merged meshes grouped by material,
// reducing draw calls by 95-99% while preserving authentic visuals and shadows.
 
import * as THREE from 'three';
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
 
export interface ConsolidationOptions {
  castShadow?: boolean;
  receiveShadow?: boolean;
  clusterId?: string;
}
 
/**
 * Consolidates all static Mesh descendants of a Three.js Group into merged meshes
 * grouped by material. Replaces hundreds of individual draw calls with 1-4 draw calls.
 */
export function consolidateGroupMeshes(
  rootGroup: THREE.Group,
  options: ConsolidationOptions = {}
): THREE.Group {
  if (rootGroup.children.length <= 1) {
    return rootGroup;
  }
 
  rootGroup.updateMatrixWorld(true);
  const invRoot = rootGroup.matrixWorld.clone().invert();
 
  // Bucket geometries by material instance
  const byMaterial = new Map<THREE.Material, THREE.BufferGeometry[]>();
  const originalMeshes: THREE.Mesh[] = [];
 
  rootGroup.traverse((child) => {
    if ((child as THREE.Mesh).isMesh && !(child as THREE.InstancedMesh).isInstancedMesh) {
      const mesh = child as THREE.Mesh;
      if (!mesh.geometry) return;
 
      originalMeshes.push(mesh);
      const mat = mesh.material as THREE.Material;
      if (!byMaterial.has(mat)) {
        byMaterial.set(mat, []);
      }
 
      // Transform geometry into rootGroup's local coordinate space
      const localMat = mesh.matrixWorld.clone().premultiply(invRoot);
      const clonedGeom = mesh.geometry.clone().applyMatrix4(localMat);
      byMaterial.get(mat)!.push(clonedGeom);
    }
  });
 
  // If there are no meshes to consolidate (or just 1), keep as is
  if (originalMeshes.length <= 1) {
    return rootGroup;
  }
 
  // Clear existing unmerged children
  rootGroup.clear();
 
  // Create merged meshes for each unique material
  for (const [mat, geometries] of byMaterial.entries()) {
    try {
      const mergedGeom = BufferGeometryUtils.mergeGeometries(geometries, false);
      if (mergedGeom) {
        const mergedMesh = new THREE.Mesh(mergedGeom, mat);
        mergedMesh.castShadow = options.castShadow ?? true;
        mergedMesh.receiveShadow = options.receiveShadow ?? true;
        mergedMesh.userData = {
          isConsolidated: true,
          clusterId: options.clusterId ?? rootGroup.userData?.clusterId,
          partCount: geometries.length,
        };
        rootGroup.add(mergedMesh);
      }
    } catch (err) {
      console.warn('[mesh-consolidator] Failed to merge geometries for material:', err);
    } finally {
      // Clean up temporary intermediate geometries
      for (const g of geometries) {
        g.dispose();
      }
    }
  }
 
  return rootGroup;
}