6c5873d51f
- @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>
199 lines
7.5 KiB
TypeScript
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)
|
|
}
|
|
}
|