feat(paint-slots): authored item materials + unified slot painting

Phase 1 + paint unification of the paint-slots plan.

- core: scene-material data layer (materials map mirroring collections,
  undo/partialize/setScene full-graph support), SceneMaterial schema,
  scene:/library: MaterialRef helpers + parseMaterialRef, slot id helpers
  (deriveSlotId/slotLabelFromId), slots map on ItemNode, hitObject on
  PaintResolveArgs, optional PaintCapability.commit.
- viewer: resolveMaterialRef (library:/scene: -> three material, null on
  dangling).
- nodes(item): renderer keeps authored GLB materials for slot-authored
  assets and applies per-slot overrides per-instance (never mutates the
  shared cached GLB); textures-off still collapses to furnishing role;
  non-authored items unchanged. Item paint capability + registration.
- editor: item joins the unified (nodeId, slotId) paint dispatch; item
  paint target + slot reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-15 10:01:08 -04:00
co-authored by Claude Fable 5
parent cf24b62c44
commit 7afb286e47
16 changed files with 627 additions and 78 deletions
+22
View File
@@ -2423,17 +2423,39 @@ export function getCatalogMaterialById(id?: string): MaterialCatalogItem | undef
}
export const LIBRARY_MATERIAL_REF_PREFIX = 'library:'
export const SCENE_MATERIAL_REF_PREFIX = 'scene:'
export function toLibraryMaterialRef(id: string) {
return `${LIBRARY_MATERIAL_REF_PREFIX}${id}`
}
export function toSceneMaterialRef(id: string) {
return `${SCENE_MATERIAL_REF_PREFIX}${id}`
}
export function getLibraryMaterialIdFromRef(materialRef?: string | null) {
if (!materialRef) return null
if (!materialRef.startsWith(LIBRARY_MATERIAL_REF_PREFIX)) return null
return materialRef.slice(LIBRARY_MATERIAL_REF_PREFIX.length)
}
export function getSceneMaterialIdFromRef(materialRef?: string | null): string | null {
if (!materialRef || !materialRef.startsWith(SCENE_MATERIAL_REF_PREFIX)) return null
return materialRef.slice(SCENE_MATERIAL_REF_PREFIX.length)
}
export type MaterialRef = string
export type ParsedMaterialRef = { kind: 'library'; id: string } | { kind: 'scene'; id: string }
export function parseMaterialRef(ref?: string | null): ParsedMaterialRef | null {
const lib = getLibraryMaterialIdFromRef(ref)
if (lib) return { kind: 'library', id: lib }
const scene = getSceneMaterialIdFromRef(ref)
if (scene) return { kind: 'scene', id: scene }
return null
}
export function getMaterialPresetByRef(materialRef?: string | null): MaterialPresetPayload | null {
const materialId = getLibraryMaterialIdFromRef(materialRef)
if (!materialId) return null