cube-parameters
Version:
A sophisticated 3D model viewer built with React, TypeScript, and Three.js, featuring advanced visualization tools, measurement capabilities, and lighting controls.
244 lines (217 loc) • 8.42 kB
text/typescript
import * as THREE from 'three';
import type { LoadedModel, SceneObject } from '../../../types/model';
// Helper function to generate consistent object IDs (same as in useObjectSelection)
const generateObjectId = (object: THREE.Object3D): string => {
// Use userData.id if available from GLB files
if (object.userData?.id) {
return `gltf_${object.userData.id}`;
}
if (object.userData.isPrimitive) {
return `primitive_${object.uuid}`;
} else if (object.userData.isPoint) {
return `point_${object.uuid}`;
} else if (object.userData.isMeasurementGroup) {
return `measurement_${object.uuid}`;
} else if (object.userData.isLoadedModel || object instanceof THREE.Group) {
return `group_${object.uuid}`;
} else if (object instanceof THREE.Mesh) {
return `mesh_${object.uuid}`;
} else if (object instanceof THREE.Light) {
return `light_${object.uuid}`;
} else if (object.type === 'GridHelper' || object.type === 'AxesHelper') {
return `env_${object.uuid}`;
}
return `object_${object.uuid}`;
};
// Helper to get display name for objects
const getObjectDisplayName = (object: THREE.Object3D, loadedModel?: LoadedModel): string => {
// Prefer loaded model name first
if (loadedModel) {
return loadedModel.name;
}
// For GLB objects, prefer userData.name (from metadata) over object.name
if (object.userData?.name) {
return object.userData.name;
}
// Then prefer user data id over object name
if (object.userData?.id) {
return object.userData.id;
}
if (object.name) {
return object.name;
}
// Fallback to type-based naming
const type = object.userData?.type || object.type;
return `${type}_${object.uuid.slice(0, 8)}`;
};
export const buildSceneObjects = (
scene: THREE.Scene | null,
loadedModels: LoadedModel[],
showPrimitives: boolean,
selectedObjects: SceneObject[] = []
): SceneObject[] => {
if (!scene) {
console.log('No scene provided to buildSceneObjects');
return [];
}
console.log('Building scene objects with:', {
sceneChildren: scene.children.length,
loadedModels: loadedModels.length,
showPrimitives
});
const sceneObjects: SceneObject[] = [];
const selectedIds = new Set(selectedObjects.map(obj => obj.id));
const processedObjects = new Set<THREE.Object3D>(); // Track processed objects to avoid duplication
// Create a map of loaded model objects for quick lookup
const loadedModelMap = new Map<THREE.Object3D, LoadedModel>();
loadedModels.forEach(model => {
loadedModelMap.set(model.object, model);
// Mark the loaded model root
model.object.userData.isLoadedModel = true;
// Also mark all children of loaded models to prevent separate processing
model.object.traverse(child => {
if (child !== model.object) {
child.userData.isPartOfLoadedModel = true;
child.userData.loadedModelRoot = model.object;
}
});
});
// Helper function to determine object type and create SceneObject
const createSceneObject = (object: THREE.Object3D, parentId?: string): SceneObject => {
let objectType: SceneObject['type'] = 'mesh';
const objectId = generateObjectId(object);
// Check if this is a loaded model root
const loadedModel = loadedModelMap.get(object);
if (loadedModel || object.userData.isLoadedModel) {
objectType = 'model';
} else if (object.userData.isPrimitive) {
objectType = 'primitive';
} else if (object.userData.isPoint) {
objectType = 'point';
} else if (object.userData.isMeasurementGroup) {
objectType = 'measurement';
} else if (object instanceof THREE.Group) {
objectType = 'group';
} else if (object instanceof THREE.Mesh) {
objectType = 'mesh';
} else if (object instanceof THREE.Light) {
objectType = 'light';
} else if (object.type === 'GridHelper' || object.type === 'AxesHelper') {
objectType = 'environment';
}
const sceneObject: SceneObject = {
id: objectId,
name: getObjectDisplayName(object, loadedModel),
type: objectType,
object: object,
children: [],
visible: object.visible,
selected: selectedIds.has(objectId)
};
// Build children recursively, but only for loaded models or non-model objects
if (object.children.length > 0) {
sceneObject.children = object.children
.map(child => createSceneObject(child, objectId))
.filter(child => {
// Filter logic based on showPrimitives and other criteria
if (!showPrimitives && child.type === 'primitive') return false;
if (child.object.userData.isHelper) return false;
return true;
});
}
return sceneObject;
};
// Traverse the scene and build the tree
scene.children.forEach((child, index) => {
console.log(`Processing scene child ${index}:`, {
name: child.name,
type: child.type,
isDetachedFromGLB: child.userData?.isDetachedFromGLB,
isHelper: child.userData?.isHelper,
hasChildren: child.children.length > 0
});
// Skip if this object has already been processed or is a helper
if (processedObjects.has(child) || child.userData.isHelper) {
console.log('Skipping child (already processed or helper)');
return;
}
// Handle all detached GLB meshes (individual model objects from different files)
if (child.userData?.isDetachedFromGLB && child instanceof THREE.Mesh) {
console.log('Processing detached GLB mesh:', child.userData?.name || child.name || child.type);
const sceneObject = createSceneObject(child);
sceneObjects.push(sceneObject);
console.log('Added detached GLB mesh to scene objects:', sceneObject.name);
processedObjects.add(child);
}
// Handle loaded model containers - include them even if empty to maintain model structure
else if (loadedModelMap.has(child) || child.userData.isLoadedModel) {
console.log('Processing loaded model container:', child.name);
const sceneObject = createSceneObject(child);
sceneObjects.push(sceneObject);
console.log('Added loaded model to scene objects:', sceneObject.name);
processedObjects.add(child);
}
// Handle all other objects (points, measurements, primitives, etc.)
else {
console.log('Processing as standalone object:', child.name || child.type);
const sceneObject = createSceneObject(child);
sceneObjects.push(sceneObject);
console.log('Added standalone object to scene objects:', sceneObject.name);
processedObjects.add(child);
}
});
console.log('Built scene objects final result:', sceneObjects.length);
return sceneObjects;
};
export const groupSceneObjects = (sceneObjects: SceneObject[]) => {
const groups = {
models: [] as SceneObject[],
meshes: [] as SceneObject[],
groups: [] as SceneObject[],
primitives: [] as SceneObject[],
points: [] as SceneObject[],
measurements: [] as SceneObject[],
lights: [] as SceneObject[],
environment: [] as SceneObject[]
};
const categorizeObject = (obj: SceneObject, isTopLevel = true) => {
// Only categorize top-level objects to avoid duplication
if (isTopLevel) {
switch (obj.type) {
case 'model':
groups.models.push(obj);
break;
case 'mesh':
// Only add meshes that are not part of loaded models
if (!obj.object.userData.isPartOfLoadedModel) {
groups.meshes.push(obj);
}
break;
case 'group':
// Only add groups that are not loaded models and not part of loaded models
if (!obj.object.userData.isLoadedModel && !obj.object.userData.isPartOfLoadedModel) {
groups.groups.push(obj);
}
break;
case 'primitive':
groups.primitives.push(obj);
break;
case 'point':
groups.points.push(obj);
break;
case 'measurement':
groups.measurements.push(obj);
break;
case 'light':
groups.lights.push(obj);
break;
case 'environment':
groups.environment.push(obj);
break;
}
}
// Don't recursively categorize children - they should be shown as part of their parent
};
sceneObjects.forEach(obj => categorizeObject(obj, true));
return groups;
};