import type { Object3D } from 'three'; import type { ViewerPart } from '../../types/viewer'; export const VIEWER_PART_ID_KEY = 'mechanicaPartId'; export const VIEWER_PART_ROOT_KEY = 'mechanicaPartRoot'; export const VIEWER_PART_NAME_KEY = 'mechanicaPartName'; export interface CollectedPartObject { partId: string; object: Object3D; } export function markObjectAsViewerPart( object: Object3D, partId: string, options: { root?: boolean; name?: string } = {} ): Object3D { object.userData[VIEWER_PART_ID_KEY] = partId; object.userData.partId = partId; if (options.root) { object.userData[VIEWER_PART_ROOT_KEY] = true; } if (options.name) { object.userData[VIEWER_PART_NAME_KEY] = options.name; } return object; } export function isViewerPartRoot(object: Object3D): boolean { return object.userData?.[VIEWER_PART_ROOT_KEY] === true; } export function findPartIdFromObject(object: Object3D | null | undefined): string | null { let current: Object3D | null | undefined = object; while (current) { const direct = current.userData?.[VIEWER_PART_ID_KEY] ?? current.userData?.partId ?? current.userData?.partID ?? current.userData?.componentId; if (typeof direct === 'string' && direct.trim()) { return direct; } current = current.parent; } return null; } export function findPartRootFromObject(object: Object3D | null | undefined): Object3D | null { let current: Object3D | null | undefined = object; while (current) { if (isViewerPartRoot(current)) { return current; } current = current.parent; } return null; } export function collectPartRoots(root: Object3D): CollectedPartObject[] { const roots: CollectedPartObject[] = []; const seen = new Set(); root.traverse((object) => { if (!isViewerPartRoot(object) || seen.has(object)) { return; } const partId = findPartIdFromObject(object); if (partId) { roots.push({ partId, object }); seen.add(object); } }); if (roots.length > 0) { return roots; } root.traverse((object) => { const partId = findPartIdFromObject(object); if (partId && !seen.has(object)) { roots.push({ partId, object }); seen.add(object); } }); return roots; } export function collectPartMeshes(root: Object3D): CollectedPartObject[] { const meshes: CollectedPartObject[] = []; root.traverse((object) => { const maybeMesh = object as Object3D & { isMesh?: boolean }; if (!maybeMesh.isMesh) { return; } const partId = findPartIdFromObject(object); if (partId) { meshes.push({ partId, object }); } }); return meshes; } export function prepareGltfPartMetadata( root: Object3D, parts: ViewerPart[], partNameMap: Record = {} ): void { const nameLookup = buildPartNameLookup(parts, partNameMap); root.traverse((object) => { const existingPartId = findPartIdFromObject(object); if (existingPartId) { markObjectAsViewerPart(object, existingPartId, { root: object.children.length > 0, name: parts.find((part) => part.id === existingPartId)?.name }); return; } const candidates = [ object.name, object.userData?.name, object.userData?.component, object.userData?.componentName ] .filter((value): value is string => typeof value === 'string') .map(normaliseObjectName); const partId = candidates .map((candidate) => nameLookup.get(candidate)) .find((candidate): candidate is string => Boolean(candidate)); if (partId) { markObjectAsViewerPart(object, partId, { root: object.children.length > 0, name: parts.find((part) => part.id === partId)?.name }); } }); } export function normaliseObjectName(value: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, ''); } function buildPartNameLookup( parts: ViewerPart[], partNameMap: Record ): Map { const lookup = new Map(); for (const [nodeName, partId] of Object.entries(partNameMap)) { lookup.set(normaliseObjectName(nodeName), partId); } for (const part of parts) { lookup.set(normaliseObjectName(part.id), part.id); lookup.set(normaliseObjectName(part.name), part.id); if (part.shortName) { lookup.set(normaliseObjectName(part.shortName), part.id); } for (const nodeName of part.nodeNames ?? []) { lookup.set(normaliseObjectName(nodeName), part.id); } } return lookup; }