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
+1
View File
@@ -158,6 +158,7 @@ export {
} from './nodes/wall'
export { WindowNode, WindowType } from './nodes/window'
export { ZoneNode } from './nodes/zone'
export { generateSceneMaterialId, SceneMaterial, type SceneMaterialId } from './scene-material'
export type { AnyNodeId, AnyNodeType } from './types'
// Union types
export { AnyNode } from './types'
+5
View File
@@ -146,6 +146,11 @@ export const ItemNode = BaseNode.extend({
// Denormalized references to collections this node belongs to
collectionIds: z.array(z.custom<CollectionId>()).optional(),
// Per-slot material overrides. Key = slot id (see deriveSlotId), value = a
// MaterialRef string ('library:<id>' or 'scene:<id>'). Absent = authored /
// registry default. A dangling ref renders the default (never blocks).
slots: z.record(z.string(), z.string()).optional(),
asset: assetSchema,
}).describe(dedent`Item node - used to represent a item in the building
- position: position in level coordinate system (or parent coordinate system if attached)
@@ -0,0 +1,13 @@
import { z } from 'zod'
import { generateId } from './base'
import { MaterialSchema } from './material'
export type SceneMaterialId = `mat_${string}`
export const generateSceneMaterialId = (): SceneMaterialId => generateId('mat')
export const SceneMaterial = z.object({
id: z.string(),
name: z.string(),
material: MaterialSchema,
})
export type SceneMaterial = z.infer<typeof SceneMaterial>