Proves the unified (nodeId, slotId) slot contract on a procedural generator, beyond items and walls. A shelf now exposes three paintable slots — shelves / frame / back — painted through the same PaintCapability dispatch and the same node.slots: Record<slotId, MaterialRef> shape items use. Foundation (shared, reusable by future procedural kinds): - core: SlotDeclaration type + capabilities.slots(node) registry declaration; GeometryContext gains `materials` so a pure builder can resolve scene:<id> slot refs without importing useScene. - viewer GeometrySystem: threads the scene material library into every builder ctx, and re-dirties (bypassing the geometryKey skip) any geometry node that references a scene material when that material changes — so editing a custom colour propagates to every shelf using it, matching items. Shelf: - schema: slots: Record<string, MaterialRef> (mirrors ItemNode). - geometry: per-slot material resolution (slot override -> legacy whole-shelf -> declared default colour); every mesh stamped with userData.slotId; DEFAULT_SHELF_MATERIAL retired (declared default gives identical off-white). - paint.ts: PaintCapability (resolveRole from userData.slotId, scene-material commit for one-off colours, preview restricted to __fromGeometry meshes so hosted items aren't ghosted, getEffectiveMaterial incl. legacy fallback). - definition: paint + slots capabilities; slots folded into geometryKey. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
36 lines
1.4 KiB
TypeScript
36 lines
1.4 KiB
TypeScript
import type { SlotDeclaration } from '@pascal-app/core'
|
|
import type { ShelfNode } from './schema'
|
|
|
|
export type ShelfSlotId = 'shelves' | 'frame' | 'back'
|
|
|
|
// Visual parity with the retired DEFAULT_SHELF_MATERIAL (off-white).
|
|
export const SHELF_SLOT_DEFAULT_COLOR = '#ffffff'
|
|
|
|
/** Map a builder mesh name to its slot id (null = not a paintable shelf part). */
|
|
export function shelfSlotIdForMeshName(name: string): ShelfSlotId | null {
|
|
if (name.startsWith('shelf-board')) return 'shelves'
|
|
if (name === 'shelf-back') return 'back'
|
|
if (
|
|
name.startsWith('shelf-side') ||
|
|
name.startsWith('shelf-post') ||
|
|
name.startsWith('shelf-divider') ||
|
|
name.startsWith('shelf-bracket') ||
|
|
name.startsWith('shelf-brace')
|
|
) {
|
|
return 'frame'
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Which slots a given shelf actually exposes (depends on style/flags). */
|
|
export function shelfSlots(node: ShelfNode): SlotDeclaration[] {
|
|
const slots: SlotDeclaration[] = [
|
|
{ slotId: 'shelves', label: 'Shelves', default: SHELF_SLOT_DEFAULT_COLOR },
|
|
]
|
|
const hasFrame = !(node.style === 'wall-shelf' && node.bracketStyle === 'hidden')
|
|
if (hasFrame) slots.push({ slotId: 'frame', label: 'Frame', default: SHELF_SLOT_DEFAULT_COLOR })
|
|
const hasBack = node.style === 'cubby' || (node.style === 'bookshelf' && node.withBack)
|
|
if (hasBack) slots.push({ slotId: 'back', label: 'Back', default: SHELF_SLOT_DEFAULT_COLOR })
|
|
return slots
|
|
}
|