Commit Graph
1072 Commits
Author SHA1 Message Date
Wassim SAMADandClaude Opus 4.8 a80924e011 feat(editor): wire reshape scope for edit-tool chips + 2D-only wall commit
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>
2026-06-25 15:24:35 -04:00
Wassim SAMADandClaude Opus 4.8 6f37783fbd fix(editor): commit ceiling locally in 2D-only view (3D tool can't)
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>
2026-06-25 14:57:24 -04:00
Wassim SAMADandClaude Opus 4.8 0e778ab526 fix(editor): commit slab locally in 2D-only view (3D tool can't)
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>
2026-06-25 14:56:24 -04:00
Wassim SAMADandClaude Opus 4.8 ac14443fa6 fix(editor): polygon vertex/edge edit honors the active snapping mode
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>
2026-06-25 14:46:16 -04:00
Wassim SAMADandClaude Opus 4.8 167f0868ef perf+fix(editor): wall/fence/roof 2D draft to store+leaf, finish snapping-mode parity
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>
2026-06-25 14:13:58 -04:00
Wassim SAMADandClaude Opus 4.8 bc9e075b2d fix(editor): 2D slab/zone/ceiling drafting honors the active snapping mode
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>
2026-06-25 13:45:30 -04:00
Wassim SAMADandClaude Opus 4.8 a769b45756 perf(editor): extract 2D draft cursor state to store + leaves — kill per-move panel re-render
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>
2026-06-25 13:35:36 -04:00
Wassim SAMADandClaude Opus 4.8 6d5294d43f perf(editor): extract stair 2D build preview to store + leaf — kill per-move panel re-render
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>
2026-06-25 13:02:48 -04:00
Wassim SAMADandClaude Opus 4.8 bbf9291c2d feat(editor): roof/stair/elevator snapping migration + no-angle footprint draft
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>
2026-06-25 12:23:00 -04:00
Wassim SAMADandClaude Opus 4.8 8e9a45a9d5 feat(editor): MEP move-tools — mode-driven snapping (drop Shift=bypass)
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>
2026-06-25 12:06:31 -04:00
Wassim SAMADandClaude Opus 4.8 2fdbcf03c3 feat(editor): MEP placement migration — Shift=cycle / mode-driven snapping
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>
2026-06-25 10:56:55 -04:00
Wassim SAMADandClaude Opus 4.8 76096ffe72 fix(editor): door/window move — fix 2D+3D FPS collapse + finish modifier migration
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>
2026-06-25 10:21:37 -04:00
Wassim SAMADandClaude Opus 4.8 8a57105eec feat(editor): mode-driven shelf/column/spawn placement + cross-kind floor collision
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>
2026-06-24 16:22:04 -04:00
Wassim SAMADandClaude Opus 4.8 04f1b0d59e feat(editor): node-declared per-context snapping + contextual HUD + painter scope
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>
2026-06-24 14:23:15 -04:00
Wassim SAMADandClaude Opus 4.8 b8b3d35f26 refactor(editor): delete the movingNode legacy flag — node lives in the scope
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>
2026-06-23 16:24:02 -04:00
Wassim SAMADandClaude Opus 4.8 353b429a01 perf(floorplan): override-driven 3D wall move + granular sibling invalidation
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>
2026-06-23 15:24:06 -04:00
Wassim SAMADandClaude Opus 4.8 cfc1fb1672 refactor(editor): delete curving + endpoint reshaping flags
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>
2026-06-23 12:18:50 -04:00
Wassim SAMADandClaude Opus 4.8 2f2c3ef8f9 refactor(editor): delete editingHole + activeHandleDrag legacy flags
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>
2026-06-23 11:30:16 -04:00
Wassim SAMADandClaude Opus 4.8 f773e6b8c5 feat(editor): placement & interaction overhaul — FSM spine, bug tracks, perf
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>
2026-06-23 09:04:58 -04:00
Wassim SAMADandGitHub b2f1a8432e Merge pull request #436 from pascalorg/fix/wall-selection-keeps-material
fix(selection): selected objects keep their texture (light glow, no wash)
2026-06-22 08:34:47 -04:00
Wassim SAMADandClaude Opus 4.8 8633886eef fix(selection): selected objects keep their texture (light glow, no wash)
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>
2026-06-19 17:17:41 -04:00
Wassim SAMADandGitHub f2352616d9 Merge pull request #433 from pascalorg/chore/editor-polish
Editor polish: paint SFX, duplicate keeps slots, review-skill convention
2026-06-19 13:11:24 -04:00
Wassim SAMADandClaude Opus 4.8 08df21a96d docs(review-skill): require slots + world-scale UV convention for new nodes
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>
2026-06-19 13:04:42 -04:00
Wassim SAMADandGitHub affe09d3b1 Merge pull request #432 from pascalorg/feat/fence-creation
feat(fence): horizontal-board fence style with capped posts
2026-06-19 13:03:23 -04:00
Wassim SAMADandClaude Opus 4.8 346b5d8cbe feat(fence): add horizontal-board fence style with capped posts
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>
2026-06-19 12:57:24 -04:00
Wassim SAMADandClaude Opus 4.8 115a142d83 fix(item): duplicated items keep their painted slot materials
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>
2026-06-19 11:17:30 -04:00
Wassim SAMADandClaude Opus 4.8 bf9246cd36 feat(sfx): play paint_apply sound when a material is painted
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>
2026-06-19 10:50:48 -04:00
Wassim SAMADandGitHub ee4d6de206 Merge pull request #431 from pascalorg/docs/item-authoring-guide
docs: item-authoring guide for paintable item GLBs
2026-06-19 09:08:09 -04:00
Wassim SAMADandClaude Opus 4.8 c726721ec6 docs(architecture): add item-authoring guide for paintable GLBs
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>
2026-06-19 09:04:42 -04:00
Wassim SAMADandGitHub 36ad0f9933 Merge pull request #430 from pascalorg/chore/pull-authored-item-glbs
chore(items): pull authored item GLBs from prod
2026-06-18 16:40:18 -04:00
Wassim SAMADandClaude Opus 4.8 bfcb039757 chore(items): pull authored item GLBs from prod
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>
2026-06-18 16:36:54 -04:00
Wassim SAMADandGitHub 628bd7f5c6 Merge pull request #429 from pascalorg/feat/paint-panel-polish
Paint panel polish: sticky controls, selection outlines, category auto-select
2026-06-18 12:53:28 -04:00
Wassim SAMADandClaude Opus 4.8 1dcaa73717 refactor(paint-slots): paint panel — three fixed bands, scroll only the grid
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>
2026-06-18 12:50:47 -04:00
Wassim SAMADandClaude Opus 4.8 8badd415b9 feat(paint-slots): paint panel polish — sticky controls, selection outlines, auto-select
- 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>
2026-06-18 12:45:45 -04:00
Wassim SAMADandGitHub ff2a24795c Merge pull request #428 from pascalorg/feat/paint-slots
Paintable slots: unified material painting across items, walls & procedural geometry
2026-06-18 12:24:56 -04:00
Wassim SAMAD c4b6e74d65 Merge remote-tracking branch 'origin/main' into feat/paint-slots
# Conflicts:
#	packages/core/src/store/use-scene.ts
#	packages/editor/src/components/editor/index.tsx
2026-06-18 12:22:26 -04:00
Wassim SAMADandClaude Opus 4.8 d924931cee feat(paint-slots): split slab into top + side slots
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>
2026-06-18 12:05:51 -04:00
Wassim SAMADandClaude Opus 4.8 6b67ab6949 feat(paint-slots): create-in-place scene materials + paint-panel polish
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>
2026-06-18 11:46:45 -04:00
Wassim SAMADandClaude Opus 4.8 c6423e7c73 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>
2026-06-18 10:12:53 -04:00
Wassim SAMADandGitHub 9e7beb8910 Merge pull request #427 from pascalorg/fix/compass-north
fix(editor): point compass needle at true north, not camera
2026-06-18 09:16:19 -04:00
Wassim SAMADandClaude Opus 4.8 222713b26f fix(editor): point compass needle at true north, not camera
The compass needle rotation was `-floorplanUserRotationDeg`, but the
2D-floorplan SVG and 3D camera projections both place true north at
`+floorplanUserRotationDeg` (= cameraAzimuth − 90°). The negated sign
made the needle rotate opposite to north as the camera orbits, so it
appeared to track the camera instead of pointing north.

The formula was written for the 2D floorplan and reused verbatim when
the compass was portaled into the 3D viewer; both instances had the
flipped sign. "Align to north" is unaffected (userRotation 0 → up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:13:27 -04:00
Wassim SAMADandClaude Opus 4.8 faf73d66f2 feat(paint-slots): full world-space UVs for roof gable walls (match walls)
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>
2026-06-18 08:16:57 -04:00
Wassim SAMADandClaude Opus 4.8 65a7fcb74b feat(paint-slots): world-space planar UVs for walls (seamless tiling)
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>
2026-06-18 08:11:57 -04:00
Wassim SAMADandClaude Opus 4.8 c7d0cd3388 feat(paint-slots): world-reference roof gable wall UVs to align with 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>
2026-06-17 21:37:43 -04:00
Wassim SAMADandClaude Opus 4.8 e46a63d172 feat(paint-slots): fence base + rail default to greige
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:18:42 -04:00
Wassim SAMADandClaude Opus 4.8 0e8df1b946 fix(paint-slots): nudge fence vertical members 0.001m thinner than the 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>
2026-06-17 21:12:53 -04:00
Wassim SAMADandClaude Opus 4.8 f7e774485c fix(paint-slots): guard empty fence slot group before mergeGeometries
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>
2026-06-17 20:57:46 -04:00
Wassim SAMADandClaude Opus 4.8 bba0cc5f68 feat(paint-slots): prepared-drywall finish + fence 4 slots matching build 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>
2026-06-17 20:51:49 -04:00
Wassim SAMADandClaude Opus 4.8 e92ee05702 feat(paint-slots): round-2 feedback — fence 2 slots + UVs, roof defaults, 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>
2026-06-17 17:21:58 -04:00
Wassim SAMAD ad227a04a2 Merge branch 'feat/paint-slots-elevator' into feat/paint-slots 2026-06-17 15:44:14 -04:00