import type { PartMaterialEntry, PartMaterialMap } from '../../api/cad' import type { MaterialPBR, MaterialPBRMap } from '../../api/assetLibraries' /** * Normalize a GLB mesh name by stripping suffixes added by the export pipeline: * - OCC RWGltf_CafWriter adds "_AF0", "_AF1", … for repeated assembly instances * - Blender adds ".001", ".002", … for name deduplication on re-import * * Mirrors the logic in render-worker/scripts/export_gltf.py (lines 107-114). * * Examples: * "Ring_AF3" → "Ring" * "Ring_AF0_AF1" → "Ring" (nested suffixes — loop until stable) * "Cage.001" → "Cage" * "Cage.001_AF2" → "Cage" * "KOMP_ASM_1_AF0_ASM" → "KOMP_ASM_1" (_AF0_ASM variant) * "GE360-HF_000_P_ASM_1_AF0_ASM" → "GE360-HF_000_P_ASM_1" * "PlainPart" → "PlainPart" */ export function normalizeMeshName(name: string): string { // Strip Blender dedup suffix (.001, .002, …) let n = name.replace(/\.\d{3}$/, '') // Strip OCC assembly-instance suffix — handles _AF0, _AF1, _AF0_ASM, _AF1_ASM patterns // The optional (_ASM)? group catches assembly-node variants like _AF0_ASM let prev = '' while (prev !== n) { prev = n; n = n.replace(/_AF\d+(_ASM)?$/i, '') } return n } // --------------------------------------------------------------------------- // resolvePartMaterial // --------------------------------------------------------------------------- /** * Resolve a material entry for a (already-normalized) GLB mesh name. * * OCC's GLB exporter strips certain path suffixes (_ASM_1, _1, _AF\d+_\d+) * that cadquery keeps when parsing the STEP topology. This means stored keys * from Excel-imported cad_part_materials may have extra suffixes compared to * the actual GLB mesh names. * * Strategy: * 1. Exact match: partMaterials[meshKey] * 2. Prefix match: find shortest stored key that starts with meshKey + '_' * e.g. GLB "GE360-EIN_HAELFTE" matches stored "GE360-EIN_HAELFTE_AF0_1" * * Returns undefined when no match exists. */ export function resolvePartMaterial( meshKey: string, partMaterials: PartMaterialMap, ): PartMaterialEntry | undefined { // 1. Exact match if (partMaterials[meshKey]) return partMaterials[meshKey] // 2. Shortest stored key that starts with meshKey + '_' let bestKey: string | undefined for (const key of Object.keys(partMaterials)) { if (key.startsWith(meshKey + '_')) { if (!bestKey || key.length < bestKey.length) bestKey = key } } return bestKey ? partMaterials[bestKey] : undefined } // --------------------------------------------------------------------------- // remapToPartKeys // --------------------------------------------------------------------------- /** * Remap a PartMaterialMap keyed by normalized OCC names to partKey slugs. * * When partKeyMap is available (from GLB extras), Excel-imported material keys * like "GE360-HF_000_P_ASM_1" are converted to partKey slugs like * "ge360_hf_000_p_asm_1" so they match what the viewer resolves each mesh to. * * Keys not found in partKeyMap are preserved as-is (backwards compat for old GLBs). */ export function remapToPartKeys( materials: PartMaterialMap, partKeyMap: Record, ): PartMaterialMap { if (!partKeyMap || Object.keys(partKeyMap).length === 0) return materials const mapKeys = Object.keys(partKeyMap) const result: PartMaterialMap = {} for (const [key, entry] of Object.entries(materials)) { // 1. Exact match if (partKeyMap[key]) { result[partKeyMap[key]] = entry; continue } // 2. Prefix match: cad_part_materials may have extra _1 instance suffixes // that partKeyMap doesn't (e.g. "PART_04_1" vs partKeyMap "PART_04") let matched = false for (const mk of mapKeys) { if (key.startsWith(mk + '_') || key === mk) { result[partKeyMap[mk]] = entry matched = true break } } if (!matched) result[key] = entry // preserve unmapped } return result } // --------------------------------------------------------------------------- // convertCadPartMaterials // --------------------------------------------------------------------------- /** * Convert Product.cad_part_materials (list of {part_name, material}) to * the PartMaterialMap format used by the 3D viewers. * * - Skips entries with blank part_name or material * - Detects hex colors (starting with "#") vs library material names * - Normalizes part names with normalizeMeshName() so they match GLB mesh keys */ export function convertCadPartMaterials( items: Array<{ part_name: string; material: string }>, ): PartMaterialMap { const result: PartMaterialMap = {} for (const item of items) { if (!item.part_name.trim() || !item.material.trim()) continue const key = normalizeMeshName(item.part_name.trim()) const value = item.material.trim() result[key] = { type: value.startsWith('#') ? 'hex' : 'library', value } } return result } // --------------------------------------------------------------------------- // PBR material helpers // --------------------------------------------------------------------------- /** * Apply PBR material properties from the Blender asset library to a * Three.js MeshStandardMaterial. * * The `mat` parameter is typed as `any` to avoid importing THREE in this * utility module — callers pass `THREE.MeshStandardMaterial` instances. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function applyPBRToMaterial(mat: any, pbr: MaterialPBR): void { if (!mat || !('color' in mat)) return // Use hex string via color.set() — reliable across all Three.js versions and // avoids colorSpace/gamma issues with setRGB() + ColorManagement. mat.color.set(pbrColorHex(pbr)) mat.metalness = pbr.metallic mat.roughness = pbr.roughness if (pbr.transmission && pbr.transmission > 0.1) { mat.transparent = true mat.opacity = 1 - pbr.transmission * 0.7 } mat.needsUpdate = true } /** Convert PBR base_color to hex string for UI swatches. */ export function pbrColorHex(pbr: MaterialPBR): string { const [r, g, b] = pbr.base_color return '#' + [r, g, b].map(v => Math.round(v * 255).toString(16).padStart(2, '0')).join('') } /** * Get a preview hex color for a material entry, using PBR data when available. * Replaces the old hardcoded SCHAEFFLER_COLORS lookup. */ export function previewColorForEntry( entry: PartMaterialEntry, pbrMap?: MaterialPBRMap, ): string { if (entry.type === 'hex') return entry.value if (pbrMap) { const pbr = pbrMap[entry.value] if (pbr) return pbrColorHex(pbr) } return '#888888' } // --------------------------------------------------------------------------- // MeshRegistry — O(1) access to meshes by partKey, replaces O(n) traversals // --------------------------------------------------------------------------- /** A single entry in the mesh registry, linking a Three.js mesh to its partKey. */ export interface MeshRegistryEntry { // eslint-disable-next-line @typescript-eslint/no-explicit-any mesh: any // THREE.Mesh — typed as any to avoid importing THREE partKey: string } /** * Iterate all materials on a mesh, calling `fn` for each MeshStandardMaterial. * Handles both single and array materials safely. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function forEachMeshMaterial(mesh: any, fn: (mat: any) => void): void { if (!mesh?.material) return const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material] for (const m of mats) { if (m && 'color' in m) fn(m) } }