feat(paint-slots): migrate wall + procedural kinds onto node.slots

Retire the inline material fields on every slot-model kind, moving them
onto the unified node.slots model on load so painting, edit-propagation,
and the picker behave uniformly.

Walls: slots {interior,exterior}; slot-first viewer resolution threading
sceneMaterials (content folded into the wall material hash); WallRenderer
subscribes to the scene-material palette so a scene-material edit
re-renders live; wallPaint rebuilt on createSlotPaintCapability.

Load migration generalizes legacy -> slots across slab/ceiling (surface),
fence (posts/infill/base/rail), column (shaft/base/capital/frame), shelf
(shelves/frame/back), and stair (per-role tread/side/railing). Library/
scene refs pass through; inline customs mint a deduped scene material;
legacy fields cleared. No visual change (renderers already fell back to
the legacy fields). Roof/chimney/dormer/vents intentionally stay on their
role system.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-18 10:12:53 -04:00
co-authored by Claude Opus 4.8
parent faf73d66f2
commit c6423e7c73
11 changed files with 659 additions and 162 deletions
+7
View File
@@ -20,6 +20,13 @@ export const WallNode = BaseNode.extend({
interiorMaterialPreset: z.string().optional(),
exteriorMaterial: MaterialSchema.optional(),
exteriorMaterialPreset: z.string().optional(),
// Per-slot material overrides on the unified slot model, mirroring
// `SlabNode.slots`. Key = slot id (`interior` / `exterior`), value = a
// `MaterialRef` (`library:<id>` / `scene:<id>`). Absent = the declared slot
// default (`WALL_SLOT_DEFAULT`). The legacy `*Material*` fields above are
// read only by the load migration that moves them into `slots`; delete them
// in a follow-up once migrated scenes are the norm.
slots: z.record(z.string(), z.string()).optional(),
thickness: z.number().optional(),
height: z.number().optional(),
curveOffset: z.number().optional(),