import { BoxGeometry, BufferGeometry, Color, CylinderGeometry, DoubleSide, Group, Material, Mesh, MeshPhysicalMaterial, MeshStandardMaterial, MeshStandardMaterialParameters, Object3D, SphereGeometry, TorusGeometry, Vector3 } from 'three'; import type { Vec3, ViewerPart } from '../../types/viewer'; import { markObjectAsViewerPart } from '../interaction/objectPartLookup'; export type MaterialToken = | 'castIron' | 'brushedSteel' | 'polishedSteel' | 'darkSteel' | 'aluminium' | 'brass' | 'copper' | 'rubber' | 'ceramic' | 'glass' | 'warmHighlight' | 'coolAccent' | 'fluidBlue' | 'exhaustOrange' | 'matteBlack'; interface MaterialPreset extends MeshStandardMaterialParameters { clearcoat?: number; clearcoatRoughness?: number; transmission?: number; thickness?: number; } export const MATERIAL_PRESETS: Record = { castIron: { color: '#343b46', metalness: 0.72, roughness: 0.48 }, brushedSteel: { color: '#9aa7b8', metalness: 0.86, roughness: 0.28 }, polishedSteel: { color: '#d6e0ec', metalness: 0.95, roughness: 0.16 }, darkSteel: { color: '#202733', metalness: 0.78, roughness: 0.34 }, aluminium: { color: '#b8c6d6', metalness: 0.74, roughness: 0.22 }, brass: { color: '#d3a24e', metalness: 0.82, roughness: 0.24 }, copper: { color: '#c66b42', metalness: 0.86, roughness: 0.22 }, rubber: { color: '#12151b', metalness: 0.1, roughness: 0.72 }, ceramic: { color: '#e8edf3', metalness: 0.05, roughness: 0.36 }, glass: { color: '#8fd4ff', metalness: 0.02, roughness: 0.08, transparent: true, opacity: 0.32 }, warmHighlight: { color: '#ffb04c', metalness: 0.34, roughness: 0.24, emissive: new Color('#331b08'), emissiveIntensity: 0.18 }, coolAccent: { color: '#4c8dff', metalness: 0.3, roughness: 0.2, emissive: new Color('#061a40'), emissiveIntensity: 0.12 }, fluidBlue: { color: '#3bc8ff', metalness: 0.05, roughness: 0.08, transparent: true, opacity: 0.42 }, exhaustOrange: { color: '#ff8f3d', metalness: 0.05, roughness: 0.2, transparent: true, opacity: 0.58, emissive: new Color('#3b1400'), emissiveIntensity: 0.35 }, matteBlack: { color: '#0d1118', metalness: 0.32, roughness: 0.58 } }; export function createPbrMaterial( token: MaterialToken, overrides: MeshStandardMaterialParameters = {} ): MeshStandardMaterial { const preset = MATERIAL_PRESETS[token]; const transparent = overrides.transparent ?? preset.transparent ?? (preset.opacity ?? 1) < 1; return new MeshStandardMaterial({ ...preset, ...overrides, transparent, side: overrides.side ?? preset.side ?? DoubleSide }); } export function createPhysicalGlassMaterial( color = '#8fd4ff', opacity = 0.26 ): MeshPhysicalMaterial { return new MeshPhysicalMaterial({ color, metalness: 0, roughness: 0.05, transparent: true, opacity, transmission: 0.35, thickness: 0.18, clearcoat: 0.65, clearcoatRoughness: 0.08, side: DoubleSide }); } export function createPartGroup(part: ViewerPart): Group { const group = new Group(); group.name = part.name; markObjectAsViewerPart(group, part.id, { root: true, name: part.name }); return group; } export function addMeshToPart( group: Group, name: string, geometry: BufferGeometry, material: Material | Material[], options: { position?: Vec3; rotation?: Vec3; scale?: Vec3; castShadow?: boolean; receiveShadow?: boolean; } = {} ): Mesh { const mesh = new Mesh(geometry, material); mesh.name = name; mesh.castShadow = options.castShadow ?? true; mesh.receiveShadow = options.receiveShadow ?? true; if (options.position) { mesh.position.set(...options.position); } if (options.rotation) { mesh.rotation.set(...options.rotation); } if (options.scale) { mesh.scale.set(...options.scale); } const partId = group.userData.mechanicaPartId; if (typeof partId === 'string') { markObjectAsViewerPart(mesh, partId, { name: group.name }); } group.add(mesh); return mesh; } export function createBoxPartMesh( group: Group, name: string, size: Vec3, material: Material | Material[], options: Parameters[4] = {} ): Mesh { return addMeshToPart(group, name, new BoxGeometry(size[0], size[1], size[2]), material, options); } export function createCylinderPartMesh( group: Group, name: string, radiusTop: number, radiusBottom: number, height: number, material: Material | Material[], options: Parameters[4] & { radialSegments?: number } = {} ): Mesh { return addMeshToPart( group, name, new CylinderGeometry(radiusTop, radiusBottom, height, options.radialSegments ?? 48), material, options ); } export function createSpherePartMesh( group: Group, name: string, radius: number, material: Material | Material[], options: Parameters[4] & { widthSegments?: number; heightSegments?: number } = {} ): Mesh { return addMeshToPart( group, name, new SphereGeometry(radius, options.widthSegments ?? 48, options.heightSegments ?? 24), material, options ); } export function createTorusPartMesh( group: Group, name: string, radius: number, tube: number, material: Material | Material[], options: Parameters[4] & { radialSegments?: number; tubularSegments?: number } = {} ): Mesh { return addMeshToPart( group, name, new TorusGeometry(radius, tube, options.radialSegments ?? 36, options.tubularSegments ?? 96), material, options ); } export function createCylinderBetween( group: Group, name: string, start: Vec3, end: Vec3, radius: number, material: Material | Material[], radialSegments = 24 ): Mesh { const startVector = new Vector3(...start); const endVector = new Vector3(...end); const delta = endVector.clone().sub(startVector); const length = delta.length(); const midpoint = startVector.clone().add(endVector).multiplyScalar(0.5); const mesh = addMeshToPart( group, name, new CylinderGeometry(radius, radius, length, radialSegments), material, { position: [midpoint.x, midpoint.y, midpoint.z] } ); mesh.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), delta.normalize()); return mesh; } export function disposeObjectTree(root: Object3D): void { root.traverse((object) => { const mesh = object as Mesh; if (!mesh.isMesh) { return; } mesh.geometry?.dispose(); const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; for (const material of materials) { material.dispose(); } }); } export function vec3ToVector(value: Vec3): Vector3 { return new Vector3(value[0], value[1], value[2]); } export function normaliseDirection(value: Vec3 | undefined, fallback: Vec3 = [0, 1, 0]): Vector3 { const vector = vec3ToVector(value ?? fallback); if (vector.lengthSq() < 0.0001) { return vec3ToVector(fallback).normalize(); } return vector.normalize(); }