Skip to content

feat(paint-slots): paintable slots on the procedural shelf (phase 5)#412

Merged
wass08 merged 6 commits into
feat/paint-slotsfrom
feat/paint-slots-phase-5-shelf-slots
Jun 16, 2026
Merged

feat(paint-slots): paintable slots on the procedural shelf (phase 5)#412
wass08 merged 6 commits into
feat/paint-slotsfrom
feat/paint-slots-phase-5-shelf-slots

Conversation

@wass08

@wass08 wass08 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

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 PaintCapability dispatch and the same node.slots: Record<slotId, MaterialRef> shape items use (shared scene-material palette, not wall-style inline copies).

Foundation (reusable by future procedural slotted kinds)

  • core: SlotDeclaration type + capabilities.slots(node) registry declaration; GeometryContext.materials so a pure geometry 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 referencing 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 resolution (slot override → legacy whole-shelf material/materialPreset → declared default colour); every mesh stamped userData.slotId; DEFAULT_SHELF_MATERIAL retired (declared default = identical off-white).
  • paint.ts: PaintCapability cloned from item/paint.tsresolveRole 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.

floorplan-move.ts / preview.tsx carry tiny incidental cleanups (a biome optional-chain autofix; comments referencing the removed getShelfMaterial).

Decisions (with Wassim)

  • Shelf joins the unified ref model (shared scene-material palette + edit-propagation), not walls' legacy inline copies.
  • Custom colours work via the threaded scene-material store; GLB-slot world-scale was settled in phase 4a.

Verification

  • bun typecheck (turbo, incl. viewer/nodes/core builds): green.
  • Biome clean.
  • Independent review pass caught + fixed: hosted-item preview cross-contamination, and getEffectiveMaterial legacy fallback.

🤖 Generated with Claude Code

wass08 and others added 6 commits June 16, 2026 10:59
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>
@wass08 wass08 merged commit 8a0f6cf into feat/paint-slots Jun 16, 2026
2 checks passed
@wass08 wass08 deleted the feat/paint-slots-phase-5-shelf-slots branch June 16, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant