feat(paint-slots): paintable slots on the procedural shelf (phase 5)#412
Merged
Merged
Conversation
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>
Open-rack / no-sides shelves span their boards across the full footprint, so a board's end / front / back faces land exactly on the corner posts' outer faces (and, with a back panel, on its rear face). Coplanar surfaces the depth buffer can't order flicker as z-fighting (visible as seams where plates meet posts). Recess every board 1mm on width + depth via a `boardGeometry` helper so each plate sits just inside the frame — the meshes still overlap (no visible gap), but no two faces are coplanar. Frame parts (posts/sides/back/dividers/brackets) keep their full size as the silhouette. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups to the plate-recess fix: - Top surface: full-height frame members that pass UNDER the top board (back panel, bookshelf column dividers, corner posts) had their top face at unitHeight — coplanar with the top board's top — so they z-fought through it (the seam on the top). Drop their top 1mm (cappedFrameY); bottoms stay on the floor so nothing floats. - Plate width: the previous pass recessed every board's width, which opened a 1mm gap where boards ABUT the side panels (cubby / bookshelf-with-sides). Width is now recessed only where boards span OVER posts (open-rack / no-sides bookshelf); abutting boards keep full width and meet the sides flush. Depth stays recessed everywhere (gap-free, fixes the back-panel fight). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cubby per-cell dividers landed exactly flush on the board below and under the board above — coplanar faces that shimmer and read as merged into the shelf. Extend each divider 1mm into both boards (centre unchanged) so it tucks under the top board and onto the bottom board with a solid, seam-free join. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w, no back seam) The cubby column divider was full-depth while the boards are depth-recessed, so it (a) sat proud of the shelf fronts — the earlier Y-tuck then made it poke past the top/bottom boards — and (b) shared the back panel's rear plane, z-fighting down the centre of the back. Give the divider the same depth recess as the boards (via boardGeometry) and drop the Y-tuck: it now sits flush with the shelf fronts and its back tucks inside the back panel. Flush top/bottom is fine — those board faces are back-to-back, not co-facing, so they don't fight. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vider depth - Cubby without a bottom board: the lowest cell opens onto the floor, but its column divider still started at the (missing) bottom board's top, leaving it floating ~thickness above the floor. Anchor it at y=0 when there's no bottom. - Bookshelf full-height divider: it crosses the continuous shelves, so it can't share their depth plane (proud at the front, coplanar with the back panel — z-fighting down the centre back). Recess its depth to sit fully INSIDE the boards' depth: embedded at each shelf crossing (board occludes it) and tucked inside the back panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Paint-slots phase 5 — proves the unified
(nodeId, slotId)slot contract on a procedural generator (the shelf), beyond items and walls.A shelf now exposes three paintable slots — shelves / frame / back — painted through the same
PaintCapabilitydispatch and the samenode.slots: Record<slotId, MaterialRef>shape items use (shared scene-material palette, not wall-style inline copies).Foundation (reusable by future procedural slotted kinds)
SlotDeclarationtype +capabilities.slots(node)registry declaration;GeometryContext.materialsso a pure geometry builder can resolvescene:<id>slot refs without importinguseScene.GeometrySystem: threads the scene material library into every builder ctx, and re-dirties (bypassing thegeometryKeyskip) any geometry node referencing a scene material when that material changes — so editing a custom colour propagates to every shelf using it, matching items.Shelf
slots: Record<string, MaterialRef>(mirrorsItemNode).material/materialPreset→ declared default colour); every mesh stampeduserData.slotId;DEFAULT_SHELF_MATERIALretired (declared default = identical off-white).PaintCapabilitycloned fromitem/paint.ts—resolveRolefromuserData.slotId, scene-material commit for one-off colours, preview restricted to__fromGeometrymeshes (so hosted items aren't ghosted),getEffectiveMaterialincl. legacy fallback.paint+slotscapabilities;slotsfolded intogeometryKey.floorplan-move.ts/preview.tsxcarry tiny incidental cleanups (a biome optional-chain autofix; comments referencing the removedgetShelfMaterial).Decisions (with Wassim)
Verification
bun typecheck(turbo, incl. viewer/nodes/core builds): green.getEffectiveMateriallegacy fallback.🤖 Generated with Claude Code