Paintable slots: unified material painting across items, walls & procedural geometry#428
Merged
Conversation
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>
Phase 3 paint UI. - Extract reusable MaterialPropertiesEditor (shared by the custom active- paint editor and the scene-material editor). - Curated Colors swatch row in the picker; catalog presets fork-on-tweak by seeding a custom material from the entry's previewColor. - Scene materials section in MaterialPaintPanel (shown once any exist): list with swatch, inline rename, 'used by N parts', paint-with, edit (live-propagates to every referencing part via the renderer's sceneMaterials dep), duplicate, delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The editor autosave/load is the single source of truth for what travels with a scene. Include document-level state (collections, materials) so it survives reload in every embedder (community cloud save + standalone localStorage): - SceneGraph carries optional collections + materials. - useAutoSave triggers on collections/materials reference changes (not just nodes) and writes them into the saved graph + the unload flush. - applySceneGraphToEditor restores them via setScene's extras arg on load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paint slots: phase 1 + paint unification
…efault Insert the plan's middle resolution step for item slots: a curated default baked into the GLB as a per-material 'pascal_material' extra (three's GLTFLoader copies material extras to userData). Resolution per slot is now: node.slots[slotId] override -> pascal_material curated default -> authored GLB material. Bare ids resolve as library:<id>; unresolved refs fall through. Runtime reads only the GLB (no DB). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paint slots 2a: render pascal_material curated defaults
…at clay) In colored mode (textures on), every item now renders its authored material — textures, vertex colours, and default colours — instead of being stripped to the uniform off-white baseMaterial. AI-generated and vertex-coloured items show their real appearance; slot resolution (override -> curated -> authored) is unchanged. Monochrome (textures off) still collapses to the furnishing clay role colour (the escape hatch). Drops the now-unused isAuthored marker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The textures axis had no UI control. Add Colored (textures on — show item materials, textures, vertex colours) / Monochrome (textures off — flat clay by surface role) options to the Render dropdown, wired to useViewer.textures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colored/Monochrome: items show real materials + Render-menu toggle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re UV contract Pool slabs (generatePoolGeometry) were the one procedural surface emitting normalized [0..1] floor UVs, so a textured finish stretched to fit the pool instead of tiling at real-world scale like every other surface. Switch the floor to shape-space metres (x, -z) — the same mapping generatePositiveSlab- Geometry already uses for its caps. Pool walls were already in metres. Also document the contract in wiki/architecture/materials-and-themes.md: every procedural surface generates UVs in metres (1 UV unit = 1 m), GLB slots follow the same ~1 unit/m authoring convention, and a catalog material's `repeat` is therefore a per-material world-scale setting (tiles per metre), identical for every surface that uses it — never per-item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rldscale feat(paint-slots): world-scale pool slab floor UVs + metre UV contract
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>
…-slots feat(paint-slots): paintable slots on the procedural shelf (phase 5)
…olors - Replace location-based categories (wood/flooring/roof/other) with material families: colors, wood, stone, brick, tile, concrete, metal, fabric, leather, roofing, ground, glass - Add MaterialSurface tags (floor/wall/ceiling/roof/furniture/outdoor); retag all 65 existing finishes; ids unchanged so library refs keep resolving - Expand curated colors 15 -> 45, ordered by hue; unify on catalog library items and retire CURATED_COLORS (picker reads the colors family) - Paint picker: wrapping rounded category chips (was horizontal scroll), empty families auto-hide, preset-style swatch cards (name label, selection-only ring, hover bg + SFX) Phase 4 (finish-library content) of editor-paint-slots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(paint-slots): material catalog families + 45-color palette + preset-style picker
…l (phase 4) Wire KTX2 into the catalog finish path and add 15 textured finishes across four new families, all as GPU-compressed KTX2 (512px) + a webp picker thumbnail. - Shared `ktx2-loader.ts`: one KTX2Loader (Basis transcoder) used by both the GLB loader and catalog textures; `ensureKtx2Support(renderer)` runs once at viewer init (GPUDeviceWatcher) so `.ktx2` finishes load even with no GLB in the scene. `use-gltf-ktx2` now reuses the shared instance. - `materials.ts`: texture loaders pick the KTX2 loader for `.ktx2` urls, the image loader otherwise (all three load paths). - `material-library.ts`: 15 entries (fabric ×6, leather ×2, concrete ×4, metal ×3). Neutral albedo for tinting, `flipY: false` (compressed textures can't flip), `repeat` per real-world tile size, metals `metalness: 1`. Normals encoded UASTC, data maps ETC1S/linear. Assets generated from raw sources via the new community `scripts/build-material-textures.ts`. 1024/256 tiers + raws kept out of git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h-library feat(paint-slots): KTX2 finish library — fabric/leather/concrete/metal (phase 4)
…all (phase 5) Brings slab, ceiling, and wall onto the unified slot contract the shelf established, so each declares its paintable slots with a declarative default and (slab/ceiling) is painted through the registry capabilities.paint dispatch. - Shared helper packages/nodes/src/shared/slot-paint.ts: a node.slots-based PaintCapability factory (commit/resolve/effective generic; preview injected). Distinct from surface-paint.ts, which writes the legacy inline node.material. - slab: schema slots; def.geometry resolves node.slots.surface -> legacy material -> declared default, tags the mesh userData.slotId; slabPaint + capabilities.slots. Retires DEFAULT_SLAB_MATERIAL in the slab path. - ceiling: schema slots; material builders extracted to ceiling/materials.ts (shared by renderer + paint preview, built BackSide so the hover preview is visible from below); renderer resolves the slot; ceilingPaint + slots. - wall: WALL_SLOT_DEFAULT in core; the viewer's getMaterialsForWall renders an unpainted face with its declared default instead of the themed wall role; capabilities.slots (interior/exterior). wallPaint's inline interior/exterior fields are unchanged (node.slots migration is a later step). - selection-manager + material-paint: drop slab/ceiling from the legacy single-surface arms (now registry-driven). Behavior change (intended, matches the shelf precedent + the phase-5 plan): colored-mode UNPAINTED slab/ceiling/wall surfaces now render their fixed slot default (#e5e5e5 / #f5f5dc / #ffffff) instead of the theme role colour. The textures-off (monochrome) role collapse is unchanged — the escape hatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e, slab=wood plank 48, ceiling=soft white) - material-library: add 'concrete-plate' KTX2 finish (512, fabric/leather-style pipeline) + editor-app texture mirror. - viewer: shared resolveSlotDefaultMaterial(colour|library ref) so a kind's slot default can be a catalog finish, not just a flat colour. - wall default -> library:concrete-plate (interior + exterior). - slab default -> library:wood-woodplank48 (slab geometry resolves it via the shared helper). - ceiling default -> soft white #f2eee6 (ceiling renders flat-tinted). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…panel/glass) Windows and doors build all visuals in their viewer systems from module-global materials, so this threads per-node slot materials + userData.slotId tags through those builders without restructuring them: - window: 'frame' + 'glass' slots. door: 'panel' (body = casing + leaf) + 'glass'; the opening reveal keeps its own material. - Each system captures per-frame viewer state, then updateWindow/DoorMesh points the builder-facing base/glass materials at the node's resolved slot override (recomputed per node, so the next node resets without a restore). Meshes are auto-tagged in the shared addBox/addShape helpers by which material they got. - Textures-off still collapses to the role material (escape hatch); a slot override only applies in colored mode. - Editing a referenced scene material re-dirties the window/door (these systems aren't covered by GeometrySystem's scene-material re-dirty). - New paint capabilities (resolve role from userData.slotId, preview by userData.slotId) + capabilities.slots; window/door dropped from the paint disabled list. Shared previewSlotByUserData helper. Defaults unchanged: unpainted windows/doors render exactly as before (the slot fallback is the existing frame/glass material), so no visual regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dren The window/door root is an invisible hitbox the system gives full-depth BoxGeometry; its front face intercepted every paint/hover ray, so the hit resolved to the hitbox (no slotId) → role null → paint silently disabled. Disable the hitbox's own raycast in the visual path so R3F's recursive intersect returns the tagged frame/glass (panel/glass) children instead; selection still works because those child hits bubble to the root's event handlers. Restored to the default raycast each build, and kept for 'opening' windows/doors (no visuals to paint, still need a selectable hitbox). The 'cutout' child is visible=false so the raycaster already skips it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt debug logs - post-processing: hoverHighlightMode was a dependency of the pipeline-build effect, so every hover rebuilt the entire pipeline. The hover style is already pushed to uniforms in a separate effect, so the rebuild was pure waste — removed it from the deps (and the build log). - selection-manager: temporary [paint-debug] logs for window/door hover to trace why their paint dispatch drops (to be removed once diagnosed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt hits the slots The real interceptor was the 'cutout' mesh, not the hitbox: it's a 1m-deep CSG helper whose front face sits 0.5m proud of the glass, and current three.js raycasts invisible meshes — so every paint/hover ray resolved to 'cutout' (no slotId → role null). Disable its raycast; combined with the hitbox noop, paint/hover rays now land on the tagged frame/glass (panel/glass) children. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…world-scale UVs Builds on the explicit per-mesh slot tagging (currentDoorSlot/currentWindowSlot): - Per-part painting: door = panel/frame/glass/hardware, window = frame/glass, each independently paintable. The recessed door/window body sits behind the wall, so the proud invisible cutout wins the scene raycast over the wall and the shared resolveSlotByReRaycast() re-raycasts the kind's own subtree to pick the exact part under the cursor (panel↔frame↔glass↔hardware). Hover tracks the cursor via a re-eval (idempotent, no flicker). - Door frame is its own slot (separate frameMaterial); hardware = new flat 'metal-chrome'. - Library defaults (generic): panel/frame -> library:preset-softwhite, glass -> library:preset-glass (flipped preset-glass to FrontSide — DoubleSide poisons the WebGPU MRT pass; it's the only glass we use). - Catalog: add flat (non-PBR) 'metal-chrome' + 'metal-brass'; drop metal metalness 1 -> 0.6 so metals are lit by existing lights (no env needed). - World-scale UVs (1 unit = 1m) on door/window box meshes via shared box-uv.ts, so finishes tile at real-world scale instead of stretching. - PaintResolveArgs gains an optional for subtree re-raycasting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inject an EditorEnvironment wrapper (drei prefiltered sunset HDRI at environmentIntensity 0.6) as a child of the editor Viewer, not baked into the Viewer component, so read-only/embed viewers stay lightweight. This gives PBR metals their reflections and lifts lighting on vertical walls that flat directional + hemisphere lights cannot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the editor-only EditorEnvironment wrapper with a SceneEnvironment component exported from @pascal-app/viewer, mounted as an opt-in <Viewer> child (still not baked into the Viewer component). One source of truth the editor and the community public viewer both inject; embed/thumbnail surfaces simply don't mount it. Sunset preset at environmentIntensity 0.6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(paint-slots): unified slot defaults + paint for slab, ceiling, wall (phase 5)
Remove height/displacement/mask/mortar/joint texture files the material schema never samples (it reads only albedo/normal/roughness/ao/metalness). Computed as present-on-disk minus catalog-referenced in material-library.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r schemas Pre-adds the unified slot-ref field (Record<slotId, MaterialRef>) to the four kinds being migrated onto the paint-slot model, so per-kind work can proceed in parallel worktrees without cross-package core edits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat #e5e5e5 placeholder on the roof shingle surface (material index 3) with the catalog roof-weatheredshingles finish via resolveSlotDefaultMaterial. Wall/trim, deck, and interior surfaces and the textures-off role array are unchanged. Roof keeps its existing per-segment role material system (no node.slots migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fence joins the node.slots paint model with one paintable 'surface' slot (merged post/base/rail geometry is a single mesh). Resolution order matches slab: textures-off joinery role -> node.slots.surface ref -> legacy inline material/preset -> declared default (library:wood-finewood27). Tags the mesh userData.slotId='surface' and wires capabilities.slots + paint. Stops borrowing DEFAULT_STAIR_MATERIAL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stair exposes three paintable slots — treads / body / railing (railing only when railingMode != none) — on node.slots. The 2-material-index body mesh maps materialIndex 0->treads, 1->body via userData.slotIds; railing meshes tag userData.slotId='railing'. Per-slot resolution layers over the viewer's base body/railing materials only in textures-on mode: node.slots ref -> legacy per-part field (preserved) -> declared default (treads wood-woodplank48, body wood-woodfine2, railing metal-steel). A custom preview swaps the targeted body material-array index and whole-mesh railing materials. Monochrome unchanged. Stops using DEFAULT_STAIR_MATERIAL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Column exposes shaft / base / capital / frame slots on node.slots (base when baseStyle!=none, capital when capitalStyle!=none, frame only for non-vertical support styles). The single material context is widened to a per-slot map plus a ColumnSlotContext; each part subtree is wrapped in <ColumnSlot> so every mesh primitive resolves its material AND stamps userData.slotId from the same context. Decorative shaft parts inherit shaft, base/capital carvings inherit their parent slot. Resolution: textures-off wall role -> node.slots ref -> legacy material/preset -> declared default (shaft/base/capital concrete-plaster, frame metal-steel). Monochrome unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Elevator exposes four finish slots — cab / doors / shaft / glass (glass only when shaft or door style is glass) — on node.slots. The finish materials now resolve through a per-node material set (context provider) keyed on node.slots + scene materials: textures-off joinery role -> node.slots ref -> declared default (cab preset-softwhite, doors metal-steel, shaft preset-lightgrey, glass preset-glass), with glass transparency flags re-applied. Cab/door/shaft/ glass meshes tag userData.slotId; buttons, indicators, control panels, and queue strips stay untagged (functional UI, not paintable). Interaction system untouched. Monochrome unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lts, stair body - fence: split into two paint slots — panel (posts/base/infill, default charcoal) and rail (cap, default wood-finewood27) — as separate meshes with userData.slotId. Fix applyFenceUVs to continuous world-space 1 UV unit = 1 m (drop the per-part min origin that broke tiling across parts). New generateFenceSlotGeometries. - roof: real catalog defaults for the segment surfaces via getRoofMaterialArray (the actual default path): wall/trim concrete-plate (matches walls), deck + soffit soft-white, shingle terracotta; textures-off role escape hatch kept. Align nodes getRoofMaterials no-parent fallback to match. - stair: body slot default -> preset-lightgrey. - (biome formatting normalization of the round-1 merged renderer files rides along.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uild options - Add 'concrete-drywall' (Prepared Drywall) catalog finish + 512 KTX2/webp runtime assets; default walls (WALL_SLOT_DEFAULT interior+exterior) and the roof wall/trim band to it so roofs read continuous with walls. - fence: replace the 2-slot panel/rail split with 4 slots that match the panel's build options — posts / infill (showInfill) / base (grounded only) / rail — each its own mesh with userData.slotId; conditional slots track the build state. Defaults: posts/infill/base charcoal, rail wood-finewood27. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mergeGeometries throws on an empty array, so an absent slot group (infill with showInfill off, base on a floating fence) crashed the geometry build. Return an empty BufferGeometry for an empty group; the renderer already skips meshes with no position attribute. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e rails The posts/infill and the accent rail shared the same panelDepth*0.35 depth, so their faces were coplanar where they cross -> z-fighting. Make the verticals 0.001m shy so the rail wins the depth test cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…walls The gable wall band tiled its V in segment-local space, so its texture phase didn't line up with the wall below (whose ExtrudeGeometry UVs map V = 1 - height-from-base). Make vertical roof faces tile V in world space (V = 1 - worldY) via the segment's resolved world Y (parent roof + segment position), so the band matches a ground-floor wall's vertical tiling at the eave seam. U is unchanged (stays in the face's local run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Walls used THREE's ExtrudeGeometry UVs, which restart at each wall's local start/end — so finishes seamed at wall joins, showed a mid-face axis-switch stripe, and didn't line up with the roof gable. Re-project the render mesh's UVs in WORLD space (1 unit = 1 m), matching roof-system's pushRoofUv exactly: vertical faces U = ±worldX/Z, V = 1 - worldY. De-indexes so each triangle uses its own face normal (no edge seams). Applied only to the render mesh; collision and floorplan geometry keep the original UVs. Now walls tile continuously across segments and meet the gable seamlessly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alls) The earlier gable fix world-referenced only V, leaving U in segment-local space — so gable and wall lined up vertically but slid horizontally. Replace the V-only scalar offset with the segment's full world matrix (roof group composed with segment transform) and project vertical faces exactly like the wall kind: U = ±worldX/Z, V = 1 - worldY. Gable now tiles continuously into the walls in both axes. Sloped shingle UVs are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Custom-create now pre-creates a scene material and opens its inline editor in the build pane (no separate right-side PaintPanel, which is removed); the brush + "Paint with" use a scene: ref so painting stores the ref and edits propagate everywhere. Slot preview (shared + item) resolves scene refs so hover shows the real material. Material properties editor uses the shared SliderControl for roughness/ metalness/opacity; row action buttons use Tooltip instead of title; the color input renders as a clean filled swatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slabs now expose two paintable faces instead of one: `surface` (top, keeps the wood floor default and the prior slot id so painted slabs are unaffected) and `side` (vertical walls + underside, default light grey). The merged slab buffer is split by per-triangle face normal into a top mesh and a side mesh, each tagged with its slot id for paint resolve/preview; legacy whole-slab material maps onto the top only. Textures-off still collapses to the floor role. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
# Conflicts: # packages/core/src/store/use-scene.ts # packages/editor/src/components/editor/index.tsx
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.
What does this PR do?
Ships the paintable slots system (plan:
editor-paint-slots). One unifiedpaint tool recolors/retextures named parts ("slots") of anything — GLB item
parts, wall faces, and procedural geometry — through the same
(nodeId, slotId)code path, data shape, and undo path.
Highlights:
slot_-prefixedglTF materials become paintable slots; unmarked materials render authored data
untouched. Colored mode shows real materials for every item; Monochrome
collapses to clay surface-role colours (the textures-off escape hatch).
node.slotsmodel across every paintable kind — items, walls,slab, ceiling, fence, column, shelf, stair, window, door — with the legacy
inline
material*fields retired (a load migration moves old scenes over).library:<id>(catalog)or
scene:<id>(user-created, auto-named, renameable, "used by N parts")refs, never inline copies. Editing a scene material updates every part using
it. Custom-create pre-creates a scene material edited inline in the build pane.
with a texels-per-metre world-scale UV contract; finishes tile at identical
world scale on walls, slabs, procedural parts, and GLB slots.
continuously (Sims/Paralives-style).
renderedshading.This is the editor-package side; the hosted curation UI, scene-material
portability for catalog presets/rooms, and item-editor fixes live in the
pascalorg/private-editorconsumer (separate PR).Not in this PR (deferred, prod-gated): the phase-6 catalog re-export sweep
(handmade, run against the live catalog post-deploy) and the item-author docs.
The renderer keeps authored/legacy materials, so existing catalog items render
correctly until they're re-exported.
How to test
bun dev, open the editor; enter paint mode.resolves to its own slot; picks persist and survive reload.
to two parts and edit it → both update live.
Colored → authored/painted materials return.
slotsand render identically.
Screenshots / screen recording
Validated visually throughout by @wass08 (WebGPU can't be verified headless).
Checklist
bun devbun checkto verify)mainbranch🤖 Generated with Claude Code