Door and window are placed via preset and moved with a bespoke wall-bound
`move-tool` (affordanceTools.move), not the draw tool — so the previous inline
triangle never showed for the paths actually used. Migrate both:
- move-tool (move + preset, the community path): publish the on-wall ghost pose
to `useFacingPose` in the same building-local frame the ghost renders in,
dropped to the floor under the wall (the ghost Y is the opening centre); clear
on every off-wall / hide / reveal / unmount path
- draw tool (standalone from-scratch path): publish the on-host pose, clear on
fallback/hide; frame depth read via a ref to keep the setup effect deps clean
Removes the now-dead `FacingIndicator` public export (the editor-side overlay is
its only consumer, via relative import). The unified overlay now covers every
placement/move path: items, column/shelf, stair, and door/window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every placement and move path now publishes its ghost pose to a single
`useFacingPose` store, drawn by one editor-side `<FacingPoseIndicator>` overlay,
instead of each path drawing its own triangle (which left the nodes-package and
PlacementBox paths invisible):
- column/shelf presets + all moves (PlacementBox via move-registry, and
DragBoundingBox) now publish the pose, so the triangle finally shows
- stair create + move use a declarative `facingIndicator: { reversed: true }`
(new registry resolver) so the triangle sits before the entry pointing out —
resolved in one place, so create and move match automatically
- stair placement defaults to single and respects the shared `point`
continuation (C) toggle, like the other placement tools
Checkpoint on the placement-interaction epic: also carries the in-flight
continuation-profile extraction (lib/continuation), grid surface (item #8), and
HUD work. Door/window still render their own legacy inline triangle and are
migrated to the overlay next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
interaction-scope.md + review-architecture skill: the sanctioned Alt-as-toggle
(wall/fence chain mode, the one place Alt-as-force is meaningless), and a second
known-legacy pattern — `event.altKey` alignment-bypass in the roof/polygon/slab
previews + ceiling/slab snap paths (migrate-on-touch; wall+fence already done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the legacy held-Alt mechanism on wall and fence drafting with a
mode toggle, mirroring the snapping-mode chip:
- Wall: `wallChainMode` room (auto-close on loop) / single. Room mode
finishes automatically when the new endpoint lands within the join-snap
radius of the chain's first vertex; single commits one wall per click.
- Fence: `fenceChainMode` continuous (chain until double-click/Esc) /
single. Fences are linear barriers, so continuous has no auto-close.
- Both: Alt-tap cycles the active drafting tool's chain mode (clean-tap,
scoped to wall/fence drafting); a clickable HUD chip shows the mode.
Persisted + migrated in `useEditor`.
Migrate wall and fence off held-Alt-bypass-alignment to the unified
convention: alignment now follows the magnetic snap mode, which frees Alt
for the toggle. 2D floorplan parity kept in sync with the 3D tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The floor grab-offset comment claimed floorStrategy.move reads localPosition
"under Alt (free place)"; it reads event.position with mode-governed snapToGrid
and has no Alt branch (Alt is force-place-only). Describe the real reason both
frames carry the offset: it's computed local-space but the strategy consumes the
world point.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The snapping model (Shift = cycle mode, Alt = force/free, mode-driven reads via
isGridSnapActive/isMagneticSnapActive/isAngleSnapActive, snapProfile-declared
context) lived only in code and the plan; tools.md still preached the legacy
"Shift = bypass snapping". Close the drift so the architecture review refuses
tool changes that revert to the old pattern:
- tools.md: replace the held-Shift-bypass manipulation policy with the unified
mode-driven model + the single snap read path.
- interaction-scope.md: new "Snapping mode & modifiers" section (contexts, read
path, modifiers, the chip-needs-a-scope rule) + a Rules bullet + the
known-legacy MEP movers (migrate-on-touch) incl. the dual-path constraint
(a bespoke mover must not open a `moving` scope — it re-mounts the generic
mover via useMovingNode).
- review-architecture skill: add interaction-scope.md to the reads and a new
"F. Interaction scope, snapping & modifiers" checklist — new shiftKey-bypass,
ungated grid step, missing snapProfile, a new useEditor interaction flag, or a
bespoke mover opening a moving scope are blockers; touching the legacy MEP
movers forces migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MEP run/fitting/terminal tools anchored their cursor readout pill at
~+0.35m above the placement point, so with the tall CursorSphere line
(badge at +2.7m) the pill sat right on the cursor and overlapped it,
especially when zoomed in. Editor main (#438) already raised the duct
pill to +1.45m; this long-lived branch predates that merge. Bring every
MEP tool that uses the tall cursor onto the same +1.45m anchor:
duct/pipe/liquid/lineset runs, duct/pipe fittings, and duct-terminal.
hvac-equipment (height-aware anchor) and pipe-trap (no cursor line) are
left as-is — their HUDs already clear the ghost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last two hot per-pointer-move 2D states still lived in FloorplanPanel's
useState, re-rendering the ~10k-line panel on every move:
- marquee (box-select): the whole drag struct moves to a dedicated
use-floorplan-marquee store; down/move/up/cancel read+write it via
getState() (panel holds nothing), and a FloorplanMarqueeOverlay leaf
subscribes to the moving corner and renders the rect alone. Drops the 3
bounds memos + the useState.
- reference-scale: the rubber-band's moving end was always equal to the
shared cursorPoint (written every move anyway), so drop the `cursor` field
from the draft and read it from useFloorplanDraftPreview in a new
FloorplanReferenceScaleDraftLine leaf. The draft now carries only the
per-click guide + start anchor, so it no longer re-renders the panel.
Closes out the 2D edition perf pass — every build/edit/select hot path now
writes a store, not panel state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Roof-segment edit/move ignored the active snap mode and showed no chip:
- add snapProfile:'structural' so a body-move resolves the no-angle polygon
context (grid/lines/off) like every other structural move;
- resize uses getSegmentGridStep() (0 outside grid mode = the "smooth" resize
that used to need a held Shift), dropping the captured gridSnapStep + Shift;
- move drops its Shift bypass;
- the affordance dispatcher opens a boundary reshape scope for the resize so
the snapping chip shows and the context resolves.
2D rotation handles now match the 3D gizmo across all six rotate affordances
(column / elevator / roof-segment / shelf / spawn / stair): a shared
rotateAffordanceDelta snaps to the 15° step unless Shift (free), the
dispatcher opens the same ROTATE_HANDLE_DRAG_LABEL handle-drag scope the 3D
gizmo uses so the contextual HUD shows the "Shift = rotate freely" hint, and
the live degree readout snaps to match the committed rotation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wall and fence endpoint-move affordances hard-snapped to the grid via a
hardcoded WALL_GRID_STEP and always ran Figma line-alignment, ignoring the
active snapping mode. Now:
- grid step follows getSegmentGridStep() (0 outside grid mode), so lines /
angles / off no longer force a grid snap the mode chip says is inactive;
- Figma alignment is gated on isMagneticSnapActive() (the lines mode);
- angles mode angle-locks the endpoint off the fixed corner (free length),
mirroring the draft tool;
- fence drops its legacy Shift-bypass to match the wall's unified model
(Alt stays as linked-segment detach).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2D affordance dispatcher (`startAffordanceDrag`) now begins the matching
reshaping interaction scope (boundary / hole / curve / endpoint) on pointer-down
and tears it down on release/cancel, matched by node id. This makes the
contextual snapping HUD show the right chip during polygon vertex/edge and wall
endpoint/curve edits, and lets `getActiveSnapContext()` resolve the correct
per-context snapping mode the affordance snap math already reads.
Wall creation is owned by the 3D `WallTool`, which is dead in 2D-only view
(canvas `display:none`). Mirror the slab/ceiling 2D-only committers: commit
locally via `createWallOnCurrentLevel`, gated on `viewMode === '2d'`, chaining
the next segment from the committed wall's resolved end. Split/3D keep their
single-owner tool commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same fix as slab: ceiling is committed by its 3D registry tool, dead in 2D-only
view. Commit it from the panel on both close paths, gated to viewMode==='2d'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slab is committed by its 3D registry tool, which accumulates the grid:click
vertices the 2D panel emits and commits on close. That path is dead in 2D-only
view — the 3D canvas is display:none, so the tool never commits and the slab is
never created (split/3D work because the 3D side is live). Mirror the zone
pattern: the panel commits the slab itself on both close paths (double-click +
click-first-vertex), gated to viewMode==='2d' so split/3D keep their single-owner
tool commit (no double-create).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shared polygon-vertex affordance snapped via snapPointToGrid(rawPoint), whose
default step is the hardcoded WALL_GRID_STEP (0.5m) — so slab/zone/ceiling vertex,
edge, and add-vertex edits always quantized to half-meters regardless of the
active mode OR the user's grid-step setting (plan open bugs #1-2). Use the
mode-aware getSegmentGridStep() (0 in non-grid modes) so grid quantizes to the
live step, lines/off pass through to the wall-snap/alignment resolver. Drop the
legacy shiftKey bypass from the slab/ceiling magnetic resolvers (they already
gate on isMagneticSnapActive).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Perf: move the per-move wall/fence/roof draft END points into
useFloorplanDraftPreview; a new FloorplanLinearDraftLayer leaf owns the live
draft polygon + fence segment + wall measurement, subscribing to the store. The
shared FloorplanDraftLayer keeps only the per-click anchors. Wall/fence/roof
drafts now have zero per-move panel setState — buttery smooth like slab/zone.
Parity: migrate the remaining legacy Shift=bypass paths to the unified
mode-driven model. roof (move + click) honored only always-grid + bypassSnap —
now grid/lines/off (footprint → no angle). wall + fence click-commit still used
the legacy bypass while their move-preview didn't — now consistent. Wall Alt
stays 'commit single wall' (open product decision, untouched).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The polygon-draft snap path used the legacy model: bypassSnap = shiftPressed and
angleSnap = pointCount > 0 && !bypassSnap, so the 15deg angle lock engaged after
the first vertex regardless of mode — hijacking grid/lines/off into angle-snap
even though the HUD chip showed the right mode.
Migrate all three placement paths (move preview, single-click vertex, double-
click close) to the unified model: angleSnap = isAngleSnapActive(); grid flows
through snapToHalf (step 0 in non-grid modes); wall-snap/alignment already gates
on isMagneticSnapActive(). Behavior now matches the chip — grid quantizes,
angles locks 15deg rays, lines snaps to walls/alignment, off is free. Drop the
now-dead bypassSnap param from snapPolygonDraftPoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2D build/edit tools republish the snapped cursor point AND the screen-space
coordinate-badge position on every pointer move. Both lived in FloorplanPanel
useState, so each move re-rendered the whole ~310ms panel (the badge fires on
every pointermove while any build tool is active — the dominant culprit).
Move both into a useFloorplanDraftPreview store written via getState() (no panel
re-render); render the crosshair + live polygon-draft edge from a
FloorplanDraftCursorLayer leaf and the coordinate badge from a
FloorplanCursorIndicator leaf, each subscribing to the store. Same store+leaf
pattern as the stair build preview. Slab/zone/ceiling drafts now have zero
per-move panel setState. Wall/fence/roof draftEnd are follow-up slices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stair tool held its 2D build preview in FloorplanPanel useState, so every
grid:move re-rendered the whole ~120-220ms panel. Move the preview into a
dedicated useStairBuildPreview store written via getState() (no panel re-render)
and render it from a FloorplanStairBuildPreviewLayer leaf that subscribes to the
store directly — the same pattern that keeps column/elevator placement smooth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate roof/stair/elevator draft tools + the 2D floorplan move overlay off the
legacy Shift/Alt=bypass model onto mode-driven snapping (isGridSnapActive /
isMagneticSnapActive); Alt dropped (no validity gate). stair/elevator now use the
live grid step.
These three are placed as footprints, not directional draws, so the angle-lock
mode was meaningless: add NodeDefinition.snapDraftDirectional (default true; false
for roof/stair/elevator) so their draft resolves to the no-angle 'polygon' context
(grid / lines / off). snapContextOf takes an injected draftDirectionalOf, like
profileOf. Add toolHints to stair/elevator so they route through the contextual
HUD and show the snapping chip. Fix one stale Alt-bypass comment in the item
placement coordinator (#9: already force-only). +snapping-mode test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 5 bespoke MEP movers (duct/pipe-segment, liquid-line, lineset, duct-fitting)
now read the active snapping mode (isGridSnapActive / isMagneticSnapActive)
instead of shiftKey=bypass. The moving scope already carries the node
(setMovingNode → begin('moving')), so the per-kind context resolves with no
extra wiring. Grid and alignment are now independent reads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate all 9 MEP kinds' placement tools onto the unified snapping model:
declare snapProfile ('item' for point-placed hvac-equipment / duct-terminal /
duct-fitting / pipe-fitting / pipe-trap; 'structural' for directional runs
duct-segment / pipe-segment / liquid-line / lineset), and replace the legacy
shiftKey-bypass reads with mode-driven isGridSnapActive / isMagneticSnapActive /
isAngleSnapActive. For runs the 45° lock becomes the cyclable 'angles' mode;
Alt stays the vertical-riser modifier (run drafting has no validity gate to
force). Port mating gated on "mode != off". Dropped stale "⇧ smooth/free" hints.
The bespoke MEP move-tool/selection (endpoint) tools stay on the legacy model —
they use setMovingNode(null) so no moving-scope context resolves yet; migrating
them needs scope-wiring first (follow-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 3D MoveDoor/MoveWindow tools wrote useScene every frame during a move
(freeFollowAt + applyPreview alternating): the wall:move (R3F) / grid:move
(DOM) de-dup compared event.timeStamp across two event systems with different
clocks, so it never matched and the floor free-follow ran during on-wall
slides too, ping-ponging the host and churning the nodes ref → framerate
collapse in both 2D and 3D. Replace it with a single-clock wall-ownership
window (performance.now, ~4 frames): the floor follow stands down while a
wall/roof hit is fresh. On-wall slides now write no scene per frame (mesh +
useLiveTransforms only). Lower the live wall-cutout throttle 120→60ms now that
the per-frame churn is gone.
Also completes the door/window modifier-model migration (#10): Shift=cycle /
Alt=force-place, fully mode-driven snap, snapProfile:'item'; exclude
ground-line candidates from along-wall opening alignment; emit the move SFX
once per snapped step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate the remaining floor-placed kinds onto the unified snapping/modifier
model and generalize floor collision so any solid floor kind blocks any other.
- shelf/column/spawn declare `snapProfile: 'item'` → contextual snapping chip,
Shift=cycle, Ctrl=grid step during placement; their tools read the active
mode (grid/lines/off) instead of legacy Shift/Alt bypass; spawn fresh
placement now respects alignment ("lines") like its move.
- Resize/radial handles claim the handle-drag scope (new RESIZE_HANDLE_DRAG_LABEL)
so the HUD shows no select-mode shortcuts mid-resize.
- Column move migrated to the generic MoveRegistryNodeTool (declare `movable`,
drop the bespoke move-tool) — gains mode-driven snapping, alignment, R/T,
slab lift, grid SFX, and the collision box for free. 2D move still routes
through `floorplanMoveTarget`.
- Cross-kind floor collision: new declarative `FloorPlacedConfig.collides`
(item/shelf/column opt in; spawn/MEP/stair stay off). `canPlaceOnFloor` now
treats every colliding floor kind as an obstacle (was item-only), reading the
declarative footprint; the generic move tool's red/green placement box gates
on `collides`. Column footprint uses the visible `columnFootprintHalf` extent
so the box/slab-lift/collision track the real (round/square) column size.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generify the snapping/modifier HUD off the FSM scope and node declarations
instead of wall-creation-shaped, leaking pills.
- Per-context snapping (`snappingModeByContext`, persisted): wall / item /
polygon mode-sets with exclusive modes (grid | lines | angles | off), each
doing exactly what its chip says. Context is node-declared via the new
`NodeDefinition.snapProfile` ('item' | 'structural'); the resolver maps
(profile × action) → context with no per-kind switch.
- Scope-driven HUD: helper-manager reads the interaction scope; reshaping
(endpoint/curve/boundary) and item move get their own chip, no select-hint
leak. Rotate R/T rounds to 45°; Alt = force-place only (hidden for
structural kinds); Shift = cycle everywhere.
- Slab/ceiling drafting: Shift=cycle, mode-aware grid/angle, Enter finishes
(minDraftVertices); polygon boundary vertex/edge drag begins a reshaping
scope. Fix grid/angle being ignored on boundary edit + slab creation:
make resolveSurfacePlanPointSnap exclusive (alignment gated on magnetic) so
grid/angles keep the snapped fallback instead of the raw cursor.
- Painter application scope: node-derived (single/object/matching/room) from
the hovered node, cyclable via Shift, single-source HUD chip.
- Remove the redundant GridSnapControl from view-toggles (grid step lives in
the contextual HUD now).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 7th and last of the legacy interaction flags. The node being placed/moved
now lives inside the interaction scope's `placing`/`moving` variant (carried
inline, since fresh-placement/duplicate drafts aren't in the scene yet), read via
`useMovingNode()` / `getMovingNode()` / `movingNodeOf(scope)`.
- scope.ts: `placing`/`moving` carry `node: AnyNode`; add `movingNodeOf`.
- use-interaction-scope.ts: `useMovingNode` (hook) + `getMovingNode` (imperative);
no useRef snapshot needed — the node is set once at `begin`, stable for the gesture.
- use-editor.tsx: drop the `movingNode` field + the `set({ movingNode })` writes.
`setMovingNode` still drives the scope and still sets `movingNodeOrigin` /
`placementDragMode`, so cross-store subscribers keep firing. Param + ~90 call
sites unchanged.
- migrate ~17 reader sites to `useMovingNode()` / `getMovingNode()`; drop
`movingNode` from lib/scene.ts; export `movingNodeOf`.
Every interaction flag is now derived from the single authoritative scope; only
`movingNodeOrigin` + `placementDragMode` intentionally remain as useEditor flags
(they outlive the scope / gate companion behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes for the split-view FPS cliff when moving a wall or opening:
- wall/move-tool: publish the live preview to useLiveNodeOverrides instead of
writing useScene.updateNodes every frame. The store's `nodes` ref no longer
churns per frame (which had re-rendered every useScene(s => s.nodes) subscriber
app-wide). Matches the existing 2D wall drag + 3D wall-system override pattern;
the final plan still commits atomically as one undoable change.
- floorplan-registry-layer: replace the single global siblingEpoch with a
per-node epoch bumped only for the nodes affected by the live drag (dragged
wall -> walls at its old + new junctions + child openings; door/window -> host
wall; gutter -> roof-peer gutters), unioned with the previous frame's live set
so a cancelled drag reverts. Dragging one wall/opening now rebuilds a handful
of geometries instead of all the level's walls + openings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate `curvingWall`, `curvingFence`, `movingWallEndpoint`, `movingFenceEndpoint`
(and their setters) off `useEditor` onto the authoritative interaction scope.
The `reshaping` scope variant gains an `endpoint` discriminator; existence
checks read `useIsCurveReshape()` / `useEndpointReshape()`, and the few sites
that need the node (affordance-tool mounts, wall-vs-fence type checks) read it
from `useReshapingNode()` — a frozen drag-start snapshot, mirroring the old
flags so the tools' own per-frame writes don't feed back. `MovingWallEndpoint`
/ `MovingFenceEndpoint` move to the kind-owned tools that consume them.
`editor-api` is simpler: endpoint engagement is kind-agnostic, and the
`engageMove` reshape clears are gone (the scope is single-owner).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the two pure-mirror interaction flags off `useEditor` onto the
authoritative `useInteractionScope`. Both carried payloads byte-identical to
the scope union, so this is a zero-behaviour-change refactor: readers use the
reference-stable `useEditingHole()` / `useActiveHandleDrag()` hooks (or the
`getEditingHole()` imperative read), and producers drive the scope directly
with guarded `endIf` so clearing one interaction never stomps an unrelated
scope. Adds the `holeEditScope` builder + a no-leaked-flag invariant test.
Closes the first slice of the legacy-flag deletion; the rich-payload flags
(movingNode, curving*, *Endpoint, placementDragMode) remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements plans/editor-placement-interaction-overhaul.md: an authoritative
interaction-scope state machine plus the catalogued placement/interaction
fixes, and split-view floor-plan performance.
- Interaction-scope spine (lib/interaction/* + store/use-interaction-scope),
driven from central useEditor setters; overlay scoping (zone labels,
context badges, floating action menu) reads resolveOverlayPolicy.
- Bug tracks A/B/D/E/F/G/H: handle/cutout raycast, footprint validity,
auto-slab loop, ceiling hosting, B-key tool desync, 2D drop offset,
per-frame jank.
- Snapping modes (grid/lines/angles/off) + contextual HUD chips; modifier
model (Shift=cycle, Alt=free place, Ctrl=grid step).
- Item move now tracks the cursor 1:1 (was a laggy per-frame lerp); handle
rig hides during a whole-node move; rotate gizmo advertises Shift=free
rotation in the HUD and hides the move cross while rotating.
- Floor-plan perf: pause live reactivity while in 3D-only view; per-node
geometry cache so only changed nodes rebuild on a drag; hoist wall miters
to a once-per-pass ctx.levelData (O(N^2) -> O(N) on wall/opening drags).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Selecting a textured wall/slab/item replaced its surface with a flat
purple wash. Root cause: the selection highlight clones the material and
tints it, but `NodeMaterial.clone()` on the WebGPU backend drops the
texture-map node assignments, so the clone rendered flat — and a strong
albedo blend + emissive washed whatever was left.
Fix: re-attach the maps from the source material after cloning (shared by
reference) and drop the albedo tint, keeping only a gentle indigo emissive
so the real material/texture stays readable with a soft "selected" glow.
Applied to both highlight paths:
- generic editor highlight (slabs/items) in selection-manager
- wall path (walls are excluded from the generic one), built lazily +
cached/self-healing so it survives the wall finish's async texture load
Removes the now-dead eager wall `highlightedVisible`/`selection` profile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a Section B rule to the review-architecture skill: a new node kind or
geometry change that exposes paintable parts must carry overrides in a
schema `slots` record resolved via def.capabilities.paint (and thread it
through clone/duplicate/preset paths), and texturable def.geometry must
emit UVs in metres (1 unit = 1 m) so catalog finishes tile. Surfaced in the
skill description too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a `horizontal` fence style that builds composite-cladding panels —
horizontal boards stacked between square intermediate posts — instead of
the vertical pickets that slat/rail/privacy all produce today.
- New `style: 'horizontal'` (core schema + inspector Style control).
- Posts march along the whole span at `postSpacing`, each with a cap.
- New `postCap` control (none/flat/pyramid, default pyramid); pyramid
caps render as a 4-sided cone fence part.
- New `slatGap` control for the board reveal — 0 collapses to one flush
panel so the stacked-board seams don't read as lines.
- `postCap` + `slatGap` are horizontal-only (visibleIf); postCap uses a
labelled dropdown so its options aren't a context-free switch.
- Lower the `postSpacing` inspector minimum from 0.2 to 0.05 m.
- 2D floor plan reuses the solid-panel marker for horizontal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Duplicating an item drops to the catalog placement flow, which rebuilds the
draft from the asset + transform and never carried node.slots — so the copy
lost every painted slot override. Thread slots through the draft create path:
useDraftNode.create seeds it onto the draft and commit() forwards it to the
final node, the placement coordinator passes it to its lazy wall/ceiling
draft creates, and the item move tool supplies node.slots for both the floor
(direct create) and wall/ceiling (coordinator) duplicate paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registers a paintApply SFX (with rate/volume jitter + a 60ms gap so rapid
multi-face painting doesn't machine-gun), wires the sfx:paint-apply bus
event, and emits it from the material-paint click chokepoint in the
selection manager (fires on apply, not hover/preview). Adds the audio
asset to the editor app's public resources.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the content-author contract for catalog item GLBs: slot_
material naming + deriveSlotId rules, authored defaults and
pascal_material extras, the cutout reserved mesh, the ~1 UV unit/m world
scale, vertex colours for fixed detail, and the validated Blender + glTF
export recipe (Custom Properties export gotcha, gltfpack -ke extras
preservation). Indexes the page in the architecture README and links it
from materials-and-themes.md (previously a dangling "phase-6 Blender
recipe" reference).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace 89 bundled item model.glb files with the authored/optimized
versions currently served from prod storage (items/system/<slug>/model.glb),
so the committed source of truth matches what live projects load and a
catalog re-seed no longer clobbers authored geometry. Items with no prod
object (404) are left as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layout is now: fixed eraser/reset + category tabs (no longer sticky-inside-
scroll), a single scrolling catalog grid, and an always-visible scene-material
footer. The custom-material "+" moved out of the colors-only grid cell onto the
scene-material section header, so a custom material can be added from any
category (creates a blank scene material, selects it as the brush, opens its
inline editor).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MaterialPaintPanel owns its scroll: the eraser/reset row stays pinned and
the category tabs stick to the top, so only the material list scrolls.
- Selected catalog swatch + active scene-material card use the same
`ring-1 ring-primary ring-inset` outline as item/preset tiles.
- Choosing a material category auto-selects its first material.
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>
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>
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>