feat(PBR): extract Blender PBR properties and apply in 3D viewer

Extract Base Color, Metallic, Roughness, Transmission, IOR from Blender
asset library materials via catalog_assets.py. Store in catalog JSON and
serve via /api/asset-libraries/pbr-map endpoint. Frontend viewers apply
PBR properties to Three.js MeshStandardMaterial using hex color strings
(avoiding Three.js ColorManagement sRGB/linear issues).

Key fixes:
- RLS bypass for material alias lookup in pbr-map endpoint
- pbrMap empty guard prevents premature grey fallback in viewers
- Cache-Control: no-cache on pbr-map requests to avoid stale data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 10:37:23 +01:00
parent 577dd1ca7e
commit d843162e5f
12 changed files with 764 additions and 351 deletions
+87
View File
@@ -1,4 +1,5 @@
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:
@@ -61,6 +62,44 @@ export function resolvePartMaterial(
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
// ---------------------------------------------------------------------------
@@ -85,3 +124,51 @@ export function convertCadPartMaterials(
}
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'
}