Files
HartOMat/frontend/src/components/cad/cadUtils.ts
T
Hartmut 6c5873d51f feat: performance optimizations + part-materials validation
- @timed_step decorator with wall-clock + RSS tracking (pipeline_logger)
- Blender timing laps for sharp edges and material assignment
- MeshRegistry pattern: eliminate 13 scene.traverse() calls across viewers
- Lazy material cloning (clone-on-first-write in both viewers)
- _pipeline_session context manager: 7 create_engine() → 2 in render_thumbnail
- KD-tree spatial pre-filter for sharp edge marking (bbox-based pruning)
- Batch material library append: N bpy.ops.wm.append → single bpy.data.libraries.load
- GMSH single-session batching: compound all solids into one tessellation call
- Validate part-materials save endpoints against parsed_objects (prevents bogus keys)
- ROADMAP updated with completion status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:53:14 +01:00

199 lines
7.5 KiB
TypeScript

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<string, string>,
): 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)
}
}