Commit Graph
132 Commits
Author SHA1 Message Date
Wassim SAMADandClaude Opus 4.8 967a905e3b feat(paint-slots): unified slot defaults + paint for slab, ceiling, wall (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>
2026-06-17 07:51:37 -04:00
Wassim SAMADandClaude Opus 4.8 e079615196 fix(shelf): floor-anchor cubby no-bottom divider + embed bookshelf divider 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>
2026-06-16 13:05:47 -04:00
Wassim SAMADandClaude Opus 4.8 c68dfa2095 fix(shelf): match cubby divider depth to the boards (no front overflow, 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>
2026-06-16 12:57:00 -04:00
Wassim SAMADandClaude Opus 4.8 c717c0c13d fix(shelf): tuck cubby cell dividers into the boards they meet
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>
2026-06-16 12:43:13 -04:00
Wassim SAMADandClaude Opus 4.8 b602e04b70 fix(shelf): stop top-face z-fighting + avoid plate/side gaps
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>
2026-06-16 12:19:17 -04:00
Wassim SAMADandClaude Opus 4.8 e17c298284 fix(shelf): recess plates 1mm to stop z-fighting with the frame
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>
2026-06-16 11:10:34 -04:00
Wassim SAMADandClaude Opus 4.8 101341d98d feat(paint-slots): paintable slots on the procedural shelf (phase 5)
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>
2026-06-16 10:59:59 -04:00
Wassim SAMADandClaude Fable 5 3d5a36c73d feat(render-modes): items show real materials in colored mode (not flat 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>
2026-06-15 17:17:44 -04:00
Wassim SAMADandClaude Fable 5 d52cbe6757 feat(paint-slots): read pascal_material glTF extras as curated slot default
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>
2026-06-15 11:32:05 -04:00
Wassim SAMADandClaude Fable 5 7afb286e47 feat(paint-slots): authored item materials + unified slot painting
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>
2026-06-15 10:01:08 -04:00
cf24b62c44 fix(viewer): UI flicker on camera move from interactive overlays + dirty-mark leaks (#401)
* chore: sync bun.lock with 0.9.1 workspace versions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(viewer): stop interactive overlays from starving frames and flickering the UI

Every interactive item mounted a drei <Html occlude> overlay
unconditionally — invisible (opacity 0) when no zone was selected, but
still alive. With `occlude` as a bare boolean, drei raycasts the entire
scene per overlay on every camera-move frame, and rewrites each
element's z-index while toggling display when the occlusion flips. On
scenes with hundreds of interactive items (recessed lights, ceiling
fans) this starved the frame budget and made the whole DOM UI blink
during camera moves while the WebGPU canvas stayed healthy.

Overlays now mount only while a zone is selected and the item sits
inside its polygon, fade in/out over 300ms (the child components stay
rendered so the exit transition can play before the <Html> unmounts),
and drop `occlude` entirely. eps=-1 works around a drei mount bug: its
mount path writes the element transform without the distanceFactor
scale, and with a static camera the eps guard never re-applies it, so
freshly mounted overlays stayed mis-scaled until the camera moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(viewer): break down dirty nodes by kind in the perf overlay

DIRTY now reads e.g. "29 (12 wall, 9 ceiling, 8 item)" — sorted by
count, only non-zero kinds, with a "missing" bucket for dirty ids whose
node no longer exists. Makes dirty-mark leaks attributable at a glance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(core): skip dirty marks for kinds with no dirty consumer

dirtyNodes is consumed by GeometrySystem (def.geometry),
FloorElevationSystem (capabilities.floorPlaced), and the legacy
per-kind viewer systems. Site, building, level, zone, and guide match
none of those, so their marks were never cleared: they accumulated for
the whole session (every child create/delete dirties its parent),
permanently defeated every consumer's empty-set early exit each frame,
and polluted the perf overlay's DIRTY readout.

NodeDefinition gains an explicit dirtyTracking?: boolean opt-out
(default tracked — no derivable predicate exists since wall's dirty
consumption lives in the viewer while zone/guide/level declare
def.system for unrelated per-frame work). markDirty consults the
registry; the five structural kinds opt out.

Also fixes a second leak: deleteNodesAction never removed deleted ids
from the dirty set, and every consumer skips missing nodes without
clearing them, so marks on deleted nodes lived forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: replace concise-arrow forEach with for...of in deleteNodesAction

biome's useIterableCallbackReturn rejects forEach callbacks that
implicitly return a value (Set.add / clearDirty).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:05:06 -04:00
Aymeric RabotandGitHub 5411f5abc8 fix(editor): improve guided manipulation and snap affordances 2026-06-11 23:59:15 -04:00
Aymeric RabotandGitHub aab48e053f fix(editor): harden editor interactions and WebGPU rendering
Fix editor bug sweep regressions, WebGPU CSG/material crashes, Shift snap bypass behavior, arrow handle drag projection, and the wall preview null guard covered by the Sentry follow-up PRs.
2026-06-11 13:03:34 -04:00
Aymeric RabotandClaude Fable 5 ee7bb39605 style: biome format/import-order for refactored tool files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:36:19 -04:00
Aymeric RabotandClaude Fable 5 aa3b0ef758 refactor: release-review cleanup for roof wall openings
Dual review pass (Claude multi-angle + Codex release-quality). One
correctness fix and the agreed do-now cleanups:

- fix: clone-scene-graph remaps roofSegmentId like wallId in both
  clone paths — duplicated scenes/levels kept pointing roof-hosted
  children at the original segments.
- extract the settled, stateless roof target/cursor math shared by the
  four door/window tools into shared/roof-wall-opening-placement.ts
  (resolveRoofWallOpeningTarget + getRoofWallOpeningCursorPose +
  worldToSelectedBuildingLocal); tools keep the stateful lifecycle
  (drafts, undo/temporal, commit field lists). −199 net lines.
- rename host-generic state: currentWallId→currentHostId,
  markWallDirty→markHostDirty (they hold segment ids too); capability
  cascadesViaHostSegment→dirtyHandledByOwnSystem (behavior-facing,
  before the public API hardens).
- drop getRoofAccessoryKinds from core's public API — its only caller
  was the standalone Build tab, which now enumerates the registry
  inline with its app-specific filter.
- window move-tool uses the shared stripPlacementMetadataFlags; stale
  "segment-local" comment fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:33:13 -04:00
Aymeric Rabot b8dfa90762 Merge remote-tracking branch 'origin/main' into feat/editor-ux-rendering-placement 2026-06-10 14:21:35 -04:00
Aymeric RabotandClaude Fable 5 7879b83df3 feat: face-frame hosting for roof wall children + items on roof walls
Wall-mounted items join doors/windows on roof-segment wall faces, and
the storage model moves to FACE-LOCAL coordinates so hosted children
track segment edits live.

- Children store roofFace + position [u, v, z-from-mid-plane] with
  rotation 0 in-frame — the exact wall-child conventions (the wall
  volume's mid-plane lands on the nominal footprint). A shared
  <RoofFaceHostFrame> derives segment pose + face frame from the
  live-override-merged segment: children follow resize handle drags in
  real time and never jump on commit. No re-anchor cascade needed —
  position is authoritative, the frame is derived. migrateNodes
  converts branch-era segment-local data.
- Items: roofWallStrategy + roof:* handlers in the placement
  coordinator (surface 'roof-wall'), Shift free-place normalized with
  walls, ItemSystem wall-side push extended to segment hosts, correct
  2D plan glyphs via face→segment→roof pose composition. The roof
  hit resolver + overlap guard moved to @pascal-app/editor (the
  coordinator lives there; nodes already depends on editor).
- Cuts: subtractAccessoryCuts extracted and applied in BOTH the
  merged-shell and per-segment CSG paths (full edit mode / painted
  segments used to lose every hole), built from the CURRENT host
  geometry and live-effective children so holes follow segment and
  opening drags.
- Handle rig: the grandparent portal now maps the node's world pose
  into the portal frame instead of composing parent+node registry
  poses — correct for any nesting (the face-frame group broke the
  old assumption), identical for walls.
- Host-field hygiene: useDraftNode.commit/adopt and the window panel
  duplicate forward roofSegmentId/roofFace/wallId; every roof↔wall
  re-anchor clears and every revert restores them.

Codex-reviewed (design consultation, adversarial rounds on the
replaced cascade and on this refactor); frame conventions locked by
unit tests. Record: private-editor plans/editor-roof-wall-openings.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:21:14 -04:00
Aymeric Rabot 5fd9be05df Fix spawn floorplan move handle 2026-06-10 14:11:50 -04:00
d36e8fcc78 fix: zone grid snapping, shelf position panel, deselect on item placement (#394)
* fix: zone grid snapping, shelf position panel, deselect on item placement

- Zone tool reads the editor's gridSnapStep (0.5/0.25/0.1/0.05) instead
  of a hardcoded 0.5 for both cursor move and click snapping.
- Shelf inspector gains a Position group (vec3 X/Y/Z sliders), matching
  the item panel.
- Item catalog clears the viewer selection before arming placement so
  shortcuts (rotate & co) don't hit both the ghost and the selected node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: move level display naming into @pascal-app/core

getDefaultLevelName / getLevelDisplayName ("Ground Floor" / "Floor N" /
"Basement N") lived in packages/editor's internal lib, so viewer-only
surfaces couldn't reach them and fell back to hand-rolled "Level N"
labels. The helpers are pure domain logic, so they move to core and
export from its barrel; the editor package's seven call sites now import
them from there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:56:24 -04:00
MateoSaettoneandGitHub 11135b608b fix: respect imperial units in wall panel (#321) 2026-06-10 07:48:34 -04:00
Aymeric RabotandClaude Fable 5 bdfee058bd feat: doors and windows on roof-segment wall faces
Openings now host on the walls a roof segment generates — the base
walls under the roof and the coplanar gable/shed/gambrel end faces, so
a window can sit in a gable pediment.

- core: roof-segment-walls.ts models the four vertical faces as 2D
  frames (u along face, v height) with convex profile polygons that
  mirror the wall volume getRoofSegmentBrushes builds; rect-in-profile
  clamping and anchored resize limits via half-plane algebra.
- schemas: optional roofSegmentId on door/window; position is the
  segment-local wall mid-plane center, rotation[1] the face yaw.
- cut: reuses capabilities.roofAccessory.buildCut; new cutScope: 'wall'
  subtracts from the wall brush only. cascadesViaHostSegment keeps the
  roof-merge loop from consuming door/window dirty marks (their own
  systems cascade via parentId).
- tools: roof:* handlers in door/window tool + move-tool (the Build-tab
  preset path), with roofSegmentId cleared/restored across every
  roof<->wall re-anchor and revert; shared hit resolver normalizes
  normals through world space (merged mesh vs painted segment frames).
- fix: RoofSystem no longer rebuilds per-segment CSG in accessory-reveal
  mode — the uncut rebuild used to draw over the merged shell's fresh
  opening until deselect.
- fix: the walkthrough collider world now prunes by renderer-effective
  visibility; stale uncut segment CSG inside the hidden segments-wrapper
  blocked the player at openings the merged shell had cut through.

Known gap: painted segments render per-segment CSG without accessory
cuts (pre-existing, also affects skylight/dormer). Twice Codex-reviewed;
details in private-editor plans/editor-roof-wall-openings.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:39:13 -04:00
Aymeric RabotandClaude Fable 5 9bfd75c2c7 chore(release): align core/viewer/editor to 0.9.0 ahead of v0.9.1
The v0.9.0 GitHub release declared the trio at 0.9.0 but the npm publish
never ran (npm still serves 0.8.0). Aligning manifests so the release
workflow's bump=patch lands everything at the announced-next 0.9.1
(mcp 0.3.0 -> 0.3.1, ifc-converter 0.1.0 -> 0.1.1 keep their own lines,
matching the per-package version table style of previous releases).
Inter-package peer/dev refs move to ^0.9.0 so the workspace stays
self-linked during the release run; the workflow re-syncs them to the
final versions after bumping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:54:42 -04:00
Aymeric Rabot e81dc63b28 feat: sync surface polygon editing 2026-06-09 19:54:35 -04:00
Aymeric Rabot 8d8052297c Merge remote-tracking branch 'origin/main' into chore/deps-2026-06 2026-06-09 18:52:42 -04:00
Aymeric Rabot 92078741cd feat: add ceiling wall snap feedback 2026-06-09 17:50:01 -04:00
Wassim SAMADandGitHub 50304d7d10 Support template scans and room preset workflows (#390)
* Allow template forks to preserve scans

* Add room preset save trigger

* Show room preset action for selected zones

* Use standard inspector for selected zones

* Support room preset editor workflows

* Preview room clear-underneath items
2026-06-09 16:36:31 -04:00
Aymeric Rabot f3c1407cae fix: sync ceiling handles during polygon edits 2026-06-09 15:58:43 -04:00
Aymeric Rabot 265acdb2be fix: improve ceiling handle feedback and WebGPU placeholders 2026-06-09 14:49:42 -04:00
Aymeric RabotandClaude Fable 5 9d24e260c3 chore(deps): June 2026 dependency refresh
Range edits:
- next 16.2.1 -> 16.2.7 in apps/editor + apps/ifc-converter. 16.2.5/16.2.6
  fixed six high-severity advisories (middleware bypass, DoS, SSRF, XSS,
  RSC cache poisoning); 16.2.7 is bugfix backports. No API/config changes.
- typescript 6.0.2 -> 6.0.3 everywhere, including aligning the 5.9.3
  stragglers (@pascal-app/mcp, @repo/ui, @repo/eslint-config) that date
  from the original scaffold and were never intentionally held back.
- @types/node in @pascal-app/mcp: ^25.5.0 -> ^22.19.20 — mcp was the only
  workspace typed against non-LTS node 25; 22 matches the release
  workflow's node and every other workspace.
- @number-flow/react ^0.5.14 -> ^0.6.0 (packages/editor, apps/editor);
  the only breaking change (removed --number-flow-char-height CSS var)
  is unused here.
- agentation ^2.3.2 -> ^3.0.2 (apps/editor devtool; v3 is a drop-in for
  the props-less <Agentation /> usage, additions are opt-in).
- Root overrides: @types/react 19.2.14 -> 19.2.17,
  @types/three 0.184.0 -> 0.184.1 (types-only fixes).

Lockfile refresh within existing ranges picks up react 19.2.7 (pairs with
next 16.2.7 — 19.2.6 had a server-action FormData regression), motion
12.40.0, three-mesh-bvh 0.9.10, @react-three/uikit-lucide 1.0.73, the
June radix wave, lucide-react 1.17.0, zustand 5.0.14, tailwind-merge
3.6.0, geist 1.7.2 (fixes Geist Mono ligature regression), react-grab
0.1.44, biome 2.4.16 + ultracite 7.8.2, turbo 2.9.17.

Held: three stays 0.184.0 (npm latest; single-instance constraint),
tailwindcss 4.3.0 + lightningcss 1.32.0 already match the pinned
optionalDependencies native binaries, eslint 10 major not taken
(@repo/ui is unconsumed legacy scaffold).

Verified: turbo build, check-types, biome check, and 931 package tests
green; bun.lock stays lockfileVersion 1 (CI bun 1.3.0 compatible);
single next/three resolution confirmed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 14:48:40 -04:00
555cf06c12 editor: building rotation, alt-click single wall, world-grid alignment, tap-to-engage move (#388)
* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* Snapshot: ridge vent locks to segment ridge during placement, free sliders after

Placement tool clamps the cursor to the segment's ridge line for all roof
types (gable, hip, shed, gambrel, dutch, mansard; flat rejected). Renderer
re-derives Y from the live surface plus a small lift, and treats stored
position[1] / position[2] as user offsets so the inspector sliders move
the vent after placement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(roof): contribute outer silhouette to alignment-guide candidates

Roof has no centred-box footprint (it's the union of its `roof-segment`
children) so the capability bridge needs the resolved AABB directly. We
build it from the children's corners in roof-local space, then transform
to world coords. Roofs only contribute as static candidates — the
move-roof tool drives them by origin, so the relocatable-box path never
applies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(floorplan): upright text geometry that stays horizontal under scene rotation

Floor-plan text labels were rotating with the 90° default scene rotation,
making zone names read sideways. Add an `upright` flag to text geometry:
when set, the registry layer counter-rotates the label by sceneRotationDeg
around its anchor so it reads horizontally on screen. Zone labels opt in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): redesign material-paint cursor badge and gate floor-plan paint mode

Paint cursor swaps the inline icon+label chip for a stacked badge — a
glowing accent stem pointing down to the hit point with the paint icon
above — so the cue reads as "pointer" rather than "tooltip". Switches
the accent to indigo. The 2D floor-plan now also shows the paint-icon
overlay and routes pointer interactions to the painter when in
material-paint mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): 90° rotate action on the floating building menu

Adds a Rotate button to the building action menu (and a generic `onRotate`
slot to NodeActionMenu). Each click rotates the building 90° CW around
its world-bbox center, mirroring the offset compensation
`MoveBuildingContent` uses during R/T-rotate drags — so the building
spins in place instead of orbiting its (often off-centre) origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(column,shelf): show cursor sphere during placement preview

Matches the placement affordance other floor-placed tools already have
so users see the snap point as a discrete dot alongside the ghost mesh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ifc-converter): update next-env types path to .next/dev/types

Next.js moved generated route types under `.next/dev/types/` — regenerate
the reference so the type-check resolves them again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ridge-vent): track parent segment's live overrides for real-time follow

While dragging a roof-segment resize handle (width / depth / pitch /
wallHeight / rotation), the new dimensions stream through
useLiveNodeOverrides and only flush to the store on release — so the
ridge vent was snapping to the new ridge only after the drag ended.
Merge segment overrides into the renderer's segment view so the vent
rides the resize in real time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): step building rotation in 15° increments

Cuts the floating building menu's rotate-step from 90° to 15° so each
click nudges the building rather than slamming it to the next cardinal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): world-frame alignment guides and grid snap under building rotation

Brings the 2D floor-plan alignment + grid behaviour in line with 3D so
guides and snap follow the world XZ grid regardless of how the active
building is rotated.

Core: adds resolveAlignmentInBuildingWorld + snapWorldXZToBuildingLocal,
plus a shared BuildingPose type, so callers can resolve in world space
while staying in building-local coords downstream.

Editor: introduces packages/editor/src/lib/world-grid-snap.ts as the
single entry point for tools that need world-frame alignment / grid
snap (resolveAlignmentForActiveBuilding, getActiveBuildingPose,
snapWorldXZForActiveBuilding, snapBuildingLocalToWorldGrid). Honours
useLiveTransforms and resolves the active building via level.parentId
first so the helper stays in sync with the floor-plan panel.

Alignment store unification: applyFloorplanAlignment and the registry
move overlay now publish guides in world XZ — the same frame the 3D
tools already use — so the shared useAlignmentGuides store carries one
consistent coordinate system. The 2D layer renders world XZ via the
fixed world → SVG transform (a 90° rotation around the building's
world position; independent of building rotation), so guides come out
parallel to the world-axis-aligned floor-plan grid in every case.

Drafting + move sessions across walls, fences, slabs, ceilings,
columns, items, shelves, roofs, stairs, zones and elevators are
re-routed through the new helpers so cursor placement, grid snap,
draft endpoints, and move commits all land on the world XZ grid
even when the building has been rotated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(editor): drop building drag-rotate and grid building-XZ chase

Two behaviours added recently were causing the world grid to slide
around during building edits. Reverts both, keeping the rest of the
world-frame alignment work intact.

- floating-building-action-menu / node-action-menu: roll the rotate
  button back to a single click that increments rotation by 15°
  around the bbox center (state at 3eb02fde). The drag-rotate from
  b50c0d5a streamed live position updates per frame, which the
  floor plan and grid both followed.
- grid: stop chasing the active building's world XZ each frame
  (pre-c00a2469 behaviour). The grid stays anchored at world (0, 0)
  so it doesn't visibly sweep when the building origin moves —
  during a drag, an R/T rotation in the move tool, or a click
  rotate that re-anchors the origin to the bbox-pivot. Y still
  lerps to the active level's floor height.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): consistent building rotation pivot + 2D position sync

Several connected fixes to building rotate/move so the 3D mesh, the
2D floor plan, and alignment guides all stay visually consistent
across rotations and drags.

Rotation-invariant pivot
- New `getBuildingLocalBboxCenter` helper walks descendant mesh
  geometry bounds in the building's LOCAL frame, returning a
  centroid that doesn't depend on the building's current world
  rotation. The previous approach derived the pivot from the world
  AABB at the current rotation, which made the local pivot subtly
  drift each time the building was rotated.
- Floating menu click-rotate and move-tool R/T rotation now seed
  their pivot from this helper, so successive rotations stay
  pinned at the same world point.

Move-tool stability
- R/T rotation always pivots around the current world bbox center,
  even before the cursor has moved onto the grid (previously it
  fell back to rotating around the origin in that case).
- Commit (`onGridClick`) now writes the mesh's actual on-screen
  pose instead of recomputing it from the click event's grid
  coords, removing a one-cell snap on release.

Alignment resolver
- Tie-break is now perp-first, primary-second. The previous order
  was reliable when wall faces shared an exact axis-aligned coord
  (pre-rotation), but post-rotation float deltas meant primary
  never tied and the resolver could lock onto the far corner of a
  candidate. Perp-first picks the visually closest point.

2D ↔ 3D sync
- The floor-plan SVG scene group now applies a live `translate(...)`
  derived from `(committedBuildingPosition → live buildingPosition)`,
  in addition to the existing live rotation. Building-local content
  slides in lockstep with the 3D mesh during a drag.
- The alignment-guide layer projects from world XZ using the
  COMMITTED building position, so guides stay locked to their world
  coordinates while the building slides past them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Merge remote-tracking branch 'origin/main' into fix/fir-5-june

* feat(wall): alt-click commits a single wall instead of chaining

Holding Alt on the click that completes a segment now stops drafting
instead of starting a new segment from the just-placed endpoint. Same
behaviour wired in both the 3D wall tool and the 2D floor-plan
placement hook (which already passed `singleWall: event.altKey` —
the 2D handler was ignoring the flag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(editor): drop 15° rotate from floating building action menu

Removes the rotate button on the floating building menu and the
`getBuildingLocalBboxCenter` pivot helper that only existed to support
it. The selected-group rotate handle remains the way to rotate a
building.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ifc-converter): point next-env at .next/types instead of .next/dev/types

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): unify 3D move grip with floating-menu Move via tap-to-engage

The translate gizmo on the 3D bounding box now hands the node to the
registry move tool (same path as the floating action menu's Move
button) instead of running its own plane-drag. One move flow — green
bounding box, cursor follow, alignment guides, R/T rotation,
click-to-commit — drives both entry points.

Adds a `dragBounds` capability on the node registry so kinds whose
rendered mesh contains extras (per-level landing assemblies on an
elevator, etc.) can declare the box the user thinks of as "the thing
being dragged." Elevator and stair opt in; everything else still
auto-measures. `MoveRoofTool` now shows the green box for whole-stair
and whole-roof moves, and aligns roofs by their footprint corners
(previously only stairs did). Slab floor-plan moves clear
`autoFromWalls` on commit so space-detection doesn't snap the slab
back, via a new `extraCommitFields` hook on the polygon-centroid
move target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): 2D alignment guides in building-local frame + world-grid wall snap

The 2D floor-plan alignment layer now mounts inside the rotated scene
<g> and reads from an editor-local `useAlignmentGuides` store —
guides are stored in building-local meters and follow the SVG
transform that already carries the rest of the floor-plan geometry.
Pill labels are counter-rotated by the scene rotation so they stay
upright under building rotation. The 3D pipeline keeps its own
world-frame store; the two surfaces no longer share frames.

Wall drafting picks up an optional `gridSnap` override so the 2D
floor-plan can snap drafts onto the world-XZ grid even when the
active building is rotated — without it, a rotated building's
drafted wall would chase the local grid and miss the visible lines.
`snapPointTo45Degrees` and `snapWallDraftPointDetailed` route through
the override when supplied; otherwise the prior local-axis grid snap
is unchanged. The floor-plan panel wires `snapBuildingLocalToWorldGrid`
through the placement hook and exports `WALL_GRID_STEP` to support it.

Also fixes the minor/major grid stroke palette swap so minor lines
render with the minor palette and majors with the major palette.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(editor): drop dead onRotate slot, memoize dragBounds snapshot

Removes the unreachable `onRotate` prop, `RotateCw` import, and
rotate button from `NodeActionMenu` — its only caller was the
floating building menu's 15° rotate, which was removed in d65fd5fe.

Memoizes `MoveRegistryNodeTool`'s `dragBounds` lookup so it captures
the scene snapshot once per node instead of re-reading
`useScene.getState().nodes` on every render. Bounds depend only on
`node` (locked for the tool's lifetime) and start-time sibling state
(elevator shaft height from the level set), so a one-shot snapshot is
the right semantics — and it avoids implying a live subscription that
isn't there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: biome check --write (import order, formatting, unused import)

Auto-fixes from `bun check:fix`:
- import-order normalization in editor/nodes/core
- unused `snapToGrid` / `useLayoutEffect` imports dropped
- minor whitespace/wrapping tweaks

No behavioural change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 11:05:27 -04:00
Aymeric Rabot b33059ef81 feat: add persistent site boundary handles 2026-06-08 17:42:20 -04:00
ce6f999310 arch: enforce layer boundaries — ceiling dispatch, store relocation, shared helper (#382)
* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* editor: per-door-type floor-plan symbols

Render a distinct, static plan symbol for each door type in the
registry floor-plan builder (`packages/nodes/src/door/floorplan.ts`),
independent of the door's live open/close animation:

- single / hinged: fixed 90° swing with a dashed quarter-circle arc
- double / french: two mirrored half-width leaves + dashed arcs
- folding / bifold: static zigzag accordion (~80% span) on the wall face
- sliding: bypass — two overlapping panels on parallel tracks + arrow
- pocket: thin white leaf, ~60% closed, sliding into the solid wall
- barn: surface-mounted panel parked over the wall, dashed closed-ghost
  + slide arrow

The swing arc is dashed in screen-pixel units (the renderer uses
non-scaling-stroke). Symbols are oriented by hingesSide / swingDirection
/ slideDirection as appropriate.

Also includes pre-existing working-tree changes unrelated to the door
symbols: group move/rotate transform and box-select tweaks, and a
regenerated ifc-converter next-env.d.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix recessed ceiling fixtures and draw safety

* feat(editor): magnetic wall-snap with per-kind beacon (2D + 3D)

Snap the wall draft / endpoint-move point onto existing wall geometry —
corners, midpoints, wall–wall intersections, and along-wall edges — and
show a beacon at the snap point whose glyph encodes what it caught
(square = corner, triangle = midpoint, ✕ = intersection, circle = edge).

- Pure snap geometry extracted to wall-snap-geometry.ts (unit-tested).
- Ephemeral useWallSnapIndicator store drives a 3D pillar+glyph beacon
  and a 2D SVG glyph beacon, both indigo to match the alignment guides.
- Gated by a new persisted "Magnetic snap" toggle in the Display menu
  (useEditor); honored by draw + commit + endpoint-move in both views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* editor: garage and open-doorway floor-plan symbols

Extend the per-door-type plan symbols in the registry floor-plan
builder (packages/nodes/src/door/floorplan.ts):

- open doorway (openingKind === 'opening'): bare gap, no leaf/arc/panel
  (mirrors the 3D system, which renders only the cutout for openings)
- garage sectional: closed leaf + side tracks into the garage + dashed
  parked ghost at the inner end
- garage roll-up: closed leaf + coil barrel (capsule) with a coil hint
- garage tilt-up: closed leaf + dashed parked panel + dashed curved
  up-and-over swing path
- gate the swing arc to actual swing doors (hinged/double/french) so
  other types fall back to the plain footprint

Garage mechanisms sit on the interior (door-local -z) side to match the
3D garage builders, independent of swingDirection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* arch: enforce layer boundaries — registry dispatch, store relocation, shared helper

Three architectural fixes to bring the branch into full compliance:

1. **ceiling-system kind check → CeilingCutCapability**
   Replace `child.type === 'item'` branch in `ceiling-system` with registry
   dispatch. Add `CeilingCutCapability` type to `packages/core` registry types,
   implement `buildCeilingHole` on `itemDefinition`, and rewrite
   `collectRecessedItemHoles` → `collectCeilingHoles` to dispatch through
   `nodeRegistry` — viewer never again inspects a node's kind directly.

2. **useAlignmentGuides + useWallSnapIndicator → packages/editor**
   These stores are editor-only UI (snap beacons, alignment guides). Move them
   from `packages/core/src/store/` to `packages/editor/src/store/`, re-export
   from `packages/editor`, and update all 34 consumer files across
   `packages/editor` and `packages/nodes` to import from `@pascal-app/editor`.

3. **findLevelAncestorId extracted to core**
   `item-light-system` had a private `resolveNodeLevelId` that duplicated
   level-ancestor traversal logic. Extract it as `findLevelAncestorId` in
   `packages/core` (spatial-grid-sync), export it, and replace the local copy.

All four packages typecheck cleanly (zero errors).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: fix lint, untrack .claude/launch.json

Run bun check --write to clear 8 Biome errors (formatting + import order
+ one unused import). Untrack .claude/launch.json and add it plus
.claude/settings.local.json to .gitignore so local IDE/agent configs
stop landing in commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 13:14:39 -04:00
Aymeric Rabot 73cd6e8614 fix(editor): strip placement metadata on commit 2026-06-08 02:12:01 -04:00
Aymeric Rabot 8dc602caa9 Improve editor manipulation flows 2026-06-08 01:07:53 -04:00
Aymeric Rabot 6b8dc33b62 fix: restore walkthrough collisions and spawn controls 2026-06-07 23:58:49 -04:00
Aymeric Rabot be0f491bbd Fix relative move drag offsets 2026-06-07 18:18:46 -04:00
Aymeric Rabot 03f57b1f4f Fix stair opening previews and slab cutouts 2026-06-07 00:46:51 -04:00
Aymeric Rabot 21b79c0d62 Snap roof segment resize handles to grid 2026-06-07 00:44:58 -04:00
Aymeric Rabot f84910848b Improve roof node editing 2026-06-07 00:24:15 -04:00
Aymeric Rabot 3750679d48 Add wall placement axis angle guides 2026-06-07 00:05:36 -04:00
Aymeric Rabot 16e09d778d Fix auto ceiling height sync 2026-06-06 23:34:33 -04:00
1419709131 editor: structure build-start/end cues + menu hover/click SFX (#378)
* wip sfx

* wip audios

* feat(editor): structure build-start/end cues + menu hover/click SFX

Sound design pass for the build/items experience:

- Split structure-build into a start cue (draft begins) and end cue
  (commit). Wall/fence fire start on the first click; slab/ceiling/zone
  tick start on every non-closing vertex; roof on the first corner.
- Doors and windows now use the structure-end cue on place + move;
  shelves use item-place instead.
- New menu_hover (round-robin retired in favour of a single sample) and
  menu_click cues wired to the Build/Items main categories, item-catalog
  tiles, the icon rail / tab bar, and the bottom action menu (via the
  shared ActionButton).
- Refresh item_pick / item_place / item_rotate samples; drop the
  now-unused numbered menu_hover variations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(editor): silence forEach return lint in updateSFXVolumes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(editor): convert SFX wavs to mp3 to shrink assets

Re-encode all SFX samples as 128kbps mp3 (~94% smaller, transparent for
short UI cues) and point the player at the .mp3 sources. Drops the .wav
originals; the freshly converted item_pick/place/rotate.mp3 override the
older committed mp3s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:15:24 -04:00
8f2e5678e8 ci: add lint and typecheck workflow (closes #147) (#369)
* fix: resolve lint and type errors to enable CI

- Remove stale biome-ignore suppressions in bootstrap.ts, r3f.d.ts,
  and parametric-node-renderer.tsx (rules no longer fire).
- Replace forEach callbacks that return values with for...of loops
  in node-actions.ts and build-collider-world.ts
  (lint/suspicious/useIterableCallbackReturn).
- Rewrite assign-in-expression guards in ceiling/tool.tsx to explicit
  if-statements (lint/suspicious/noAssignInExpressions).
- Hoist useMemo/useCallback above early return in chimney/panel.tsx
  (lint/correctness/useHookAtTopLevel).
- Add `^build` to turbo check-types dependsOn so packages/core dist is
  up-to-date before type checking — this resolves all 32 type errors
  which were caused by stale dist, not missing symbols.

* ci: add lint and typecheck workflow on push/PR

Adds a quality gate that runs `bun run check` (Biome) and
`bun run check-types` (TypeScript) on every push and PR to main.
Uses concurrency groups to cancel stale runs. Closes #147.

---------

Co-authored-by: Pascal <open@pascal.app>
2026-06-06 09:05:16 -04:00
Aymeric RabotandGitHub 0b338cf647 feat(editor): live floor-stacking, unified handle system, slab-hole editing + interaction polish (#375)
- Live slab-stacking Y previews for all floor-placed kinds (item/shelf/spawn/column/stair) during placement + both move pathways, via a shared core resolver; canonical positions unchanged.
- Unified 3D handle system (one drag pipeline + one visual primitive) with forgiving invisible hit-areas on every handle, kept on EDITOR_LAYER so they don't poison the MRT scene pass.
- Hover + click-to-edit slab holes in 3D (manual hole -> hole editor; stair/elevator hole -> select owner); generic cross-arrow polygon-move grip; normalized handle interaction colors.
- NaN-safe node mutations + non-finite shadow-light bounds guard.
- Built on #373 (level-scoped alignment / registry slab tool); #373 owns X/Z alignment, this owns Y floor-stacking.
2026-06-05 16:24:48 -04:00
d1b40aa98d editor: level-scoped alignment, reference-floor symbols & registry slab tool (#373)
* feat(editor): level-scoped alignment, reference-floor registry symbols, registry slab tool

- Scope alignment candidates to the active level so a node directly below
  on another floor no longer snaps (alignment is XZ-only); building-scoped
  nodes like elevator shafts stay in the pool across floors.
- Derive the elevator alignment footprint from its outer shaft (not the
  inset cab), so its guide actually surfaces within the snap threshold.
- Extract shared stair footprint geometry (rotateXZ, segment transforms,
  stairFootprintAABB) so opening-sync and alignment anchors derive the
  chain identically; stairs now contribute plan bbox anchors.
- Render reference-floor stairs/roofs/elevators/shelves/spawns through
  their registry floorplan builders for pixel-identical symbols.
- Move slab creation to the registry-driven slab tool (parity with
  ceiling); 2D handlers only maintain draft state.
- Lift the 3D alignment guide ribbon to the active level's Y each frame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): registry-drive elevator/stair alignment footprints

Address architecture review of the level-scoped alignment work: the core
anchor bridge (`alignment-anchors.ts`) hardcoded `if (node.type === 'elevator')`
and `if (node.type === 'stair')` branches to derive their plan footprints.
Move that onto the kinds themselves via a new `alignmentFootprint` capability
so the bridge dispatches generically — matching the registry composition model.

- Add `Capabilities.alignmentFootprint`: returns a `box` (rotatable rect
  centred on position, relocatable for movable kinds) or an `aabb` (already
  resolved, for non-rectangular plan shapes). Elevator uses `box` (its outer
  shaft, and it's movable); stair uses `aabb` (segment chain / annular sector,
  moves by origin).
- Drop both hardcoded branches; the bridge now consults the capability via
  `floorFootprint` (box) and a unified `alignmentAABB` (box ∪ aabb).
- Export `stairFootprintAABB` from core so the stair definition consumes it.
- Tests register synthetic defs carrying the capability (the bridge no longer
  knows elevator/stair by name), reproducing the production glue from the same
  core helpers.
- Document why `REFERENCE_REGISTRY_KINDS` is a deliberate editor-local
  curation, not an auto-derived set (most floorplan-builder kinds shouldn't
  appear as standalone reference symbols, and "reference floor" is an editor
  concept core must not know).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): hide node arrow handles during thumbnail capture

The arrow-handle rig lives on SCENE_LAYER (so its chevrons read as proper
3D plates), but the thumbnail camera only filters EDITOR_LAYER + GRID_LAYER —
so a node selected at capture time would leak its arrows into the snapshot.
Hide the rig on `thumbnail:before-capture` and restore it on
`thumbnail:after-capture`, the same emitter handshake SelectionManager uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:04:14 -04:00
46f94b97b3 editor: alignment guides + floor-plan move/placement parity (#372)
* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* feat(editor): 3D alignment guides for item/wall/fence move + placement

Bring Figma-style alignment guides into the 3D editor, reusing the shared
pure resolver (`resolveAlignment`) and ephemeral guide store
(`useAlignmentGuides`) that previously only drove the 2D floor plan.

Core:
- `alignment-anchors.ts`: node→anchor adapters (footprint AABBs, corner
  anchors, wall/fence segment anchors) + `refineGuidesToGap` so a guide's
  line and distance read to the candidate's nearest edge, not the far side.
- `bboxCornerAnchors` + corner-only footprint anchors so alignment locks to
  item edges, never centrelines.
- `resolvePointSnap` (point-coincidence variant; kept for future use).

Editor:
- `Alignment3DGuideLayer`: dashed ribbon + flat floor dots + distance pill,
  in the project's indigo accent, mounted inside ToolManager's building-local
  group so guides render in the cursor's frame.
- Producers wired in the item move tool, item placement coordinator, and the
  wall + fence endpoint tools; walls and fences cross-align.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): align guides snap to nearest real anchor, drop bbox re-span

The 3D alignment guide could place its end dot in empty space: a diagonal
wall (or any rotated / non-rectangular object) has bounding-box corners that
don't lie on the object, and `refineGuidesToGap` re-spanned the guide to
exactly those AABB edges — so the dot floated "along the coordinate" rather
than on the item.

- `resolveAlignment` now tie-breaks to the candidate anchor NEAREST on the
  perpendicular axis (after the tightest axis match). Anchors are real points
  (corners / endpoints / midpoints), so the guide always connects to the
  closest actual point — which also yields the facing-edge gap distance.
- All four producers (item move, item placement, wall + fence endpoints) now
  publish the raw resolver guides; the AABB nearest-edge re-span is gone.
- Removed the now-dead `refineGuidesToGap` and `resolvePointSnap` helpers
  (and their tests / exports).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(editor): group move handle + shared group-transform core, move-tool polish

Add a group-move gizmo alongside the existing group-rotate handle, both
driven by a new shared `group-transform-shared` module (participant
classification, group-box + corner math, connected wall/fence component
expansion so attached structure transforms rigidly as one piece).

- core: refactor alignment-anchors collection + tests, extend handle registry
- editor: group-move-handle, group-transform-shared; rotate handle reuses them;
  node-arrow-handles gains click-swallow guard; box-select + placement tweaks
- nodes: move-tool updates across ceiling/column/slab/roof/registry; door math
  and panel adjustments; item definition cleanup
- nodes(fence): play `sfx:grid-snap` ticker on endpoint move, matching the
  wall endpoint tool (fixes missing audio feedback on fence side drag)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): keep autosave alive across page unload

The autosave debounces writes by 1s and relies on a `beforeunload` flush
for anything still pending. That flush fired a plain `fetch` PUT, which
the browser cancels the instant the page unloads — so refreshing right
after an edit (e.g. painting a roof material) silently dropped the change
and the reload showed the last persisted scene.

Thread a `{ keepalive }` option through the save callback and set it on
the unload flush so the request survives the unload. Also listen for
`pagehide` (fires where `beforeunload` does not, e.g. mobile Safari /
bfcache) and clear the dirty flag up front so the two listeners don't
double-send. Normal debounced saves omit `keepalive` (its 64KB body cap
only constrains the best-effort unload flush, not regular saves).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(editor): paint eraser + reset-all, drop roof cross-role bleed

Material paint gains an eraser (clear a surface back to its default) and a
"Reset all" action that defaults every painted surface on a node — for a
roof that includes each child segment — via a generic
`buildResetSurfaceMaterialUpdates` that nulls catch-all and role-specific
material fields without per-kind knowledge.

Also stop a single painted roof surface from bleeding onto the others:
`getEffectiveRoofSurfaceMaterial`, `getRoofMaterialArray`, and the segment
renderer no longer cross-fall-back between top/edge/wall. An unset role
resolves only to the legacy catch-all (back-compat) or the theme default,
so painting just the shingle, trim, or soffit stays on that surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(editor): alignment guides + drag bounding box across tools

Extend the Figma-style 3D alignment guides to the placement and move
tools for columns, elevators, roofs, stairs, ceilings, slabs, fences,
walls, doors, and windows: each collects alignment anchors from the
scene, resolves a snap within the shared threshold, and drives the
`useAlignmentGuides` overlay. Wall openings (doors/windows) only snap
along their host wall via the new `wall-opening-alignment` helper.

Add a shared `DragBoundingBox` overlay (exported from the editor barrel)
that renders the dragged object's bounds during a move, wired into the
column move tool alongside the alignment snap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): axis-stable resize-arrow drag plane + slimmer gizmo handles

Build the linear resize-arrow's drag plane so it always contains the
handle's axis (view direction minus its along-axis component) instead of
a plane that merely faces the camera. The old camera-facing normal
collapsed when the axis pointed toward the viewer — screen motion barely
changed the axis component, so the resize crawled or stopped tracking the
cursor. Also slim the extruded arrow/handle geometry (shared by the node
arrows, wall side handles, and polygon editor) for a lighter gizmo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): fill-block wall opening highlight + selected frameless openings

Draw the selected-wall opening highlight as a translucent block filling
the cutout volume (front-side culled) instead of a single vertical pane,
so it reads as an occupied slot from any angle — including a top-down
floorplan view where an edge-on pane was invisible. Also highlight a
directly-selected frameless opening (a `door` with openingKind
`'opening'`), which otherwise renders no geometry of its own, and reflect
live drag overrides via `useLiveNodeOverrides`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(viewer): double-side slab hole side-walls

Build the slab in 3D rather than via ExtrudeGeometry so each hole-wall
quad is emitted twice with opposite winding. The slab material is forced
to FrontSide (DoubleSide poisons the MRT scene pass), under which
ExtrudeGeometry's single-sided hole walls get back-face culled and you
see straight through the cut. The doubled quads keep the cut's inner
thickness visible from any angle without z-fighting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): box-select building-scoped nodes like elevators

Building-scoped selectable nodes (e.g. elevators) are children of the
building, not the active level, so the level walk never reached them.
Also walk the level's building children and box-test any registry-
selectable kind by its rendered bounds, matching the column/stair/shelf
path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(editor): shelf placement alignment + column placement ghost

Shelf placement now snaps to Figma-style alignment guides by its
footprint edges (layered on grid snap, Alt bypasses), matching the
existing 3D move tool. Guides refresh after each drop and clear on
teardown.

Column placement migrates to the registry `def.tool` path so it can
render a translucent column ghost at the cursor (like the shelf build
tool) instead of a bare cursor sphere — the editor package can't import
the column geometry, so the tool now lives in packages/nodes:
- extract `ColumnBody` from the renderer and add a `ColumnPreview`
  (cloned translucent material, raycast disabled, origin-positioned)
- new `column/tool.tsx` registry placement tool with the same
  footprint-edge alignment as shelf / column move
- wire `def.tool` + tool hints; drop the now-unreachable legacy
  editor-side `ColumnTool` and its dead file

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(editor): floor-plan alignment, pivot moves & placement ghosts

Bring the 3D editor's Figma-style alignment experience to the 2D floor
plan across every node kind, fix pivot semantics on move, and add 2D
placement ghosts.

Alignment
- Wall anchors now include ±thickness/2 face corners so columns/items/etc.
  snap flush to wall faces (fixes pillar↔wall); shared by 2D and 3D.
- Shared apply-alignment helper (applyFloorplanAlignment /
  alignFloorplanDraftPoint, with excludeIds) used by move sessions,
  structural drafting (wall/fence/slab/zone/ceiling/roof), and wall/fence
  endpoint drags.
- Door/window/wall-item moves get along-wall edge-to-edge snapping.
- Generic free-translate move path aligns by edges (corner anchors).

Pivot moves (2D)
- Polygon kinds (slab/ceiling/zone) move by centroid→cursor via a shared
  polygon-centroid mover; stair moves by origin→cursor; matching 3D.
- Shelf/column move targets write position directly (single source of
  truth) so the 3D group no longer sticks on commit.

Placement ghosts (2D)
- usePlacementPreview store + FloorplanPlacementPreviewLayer render a
  kind's def.floorplan footprint following the cursor; wired for column
  and elevator.

Fixes
- Elevator placement no longer deselects the active floor plan
  (preserve levelId through setSelection's hierarchy guard).
- Guides clear on every commit/cancel/unmount path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(editor): address architecture review for floor-plan work

- Move usePlacementPreview store from core to editor: placement ghosts
  are an editor/tool concern the read-only viewer never needs. Rewire the
  column tool (via the @pascal-app/editor public surface) and the
  editor-internal elevator tool + preview layer (relative imports).
- FloorplanPlacementPreviewLayer: read scene lazily in ctx.resolve instead
  of bulk-reading the nodes map during render.
- wiki/architecture/tools.md: refresh the stale useLiveTransforms-per-kind
  note to reflect item/shelf/column (world-plan) + slab/ceiling/zone (delta).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(nodes): recreate window draft on wall:move when null after placement

After a successful click-to-place, the click handler deletes the
transient draft and relies on the wall-rebuild → R3F pointer-enter
cascade to create a fresh draft for the next placement. If that cascade
doesn't fire synchronously (e.g. async geometry rebuild) the next
wall:move receives a null draftRef and bails — requiring leave/re-enter
to place again.

Fix: in onWallMove, when draftRef.current is null but we're hovering a
valid wall, recreate the draft immediately (same WindowNode.parse +
createNode path as onWallEnter). This is idempotent: if wall:enter does
fire first, destroyDraft() in onWallEnter cleans up cleanly.

Preserves parity with door multi-place behaviour, matching #367's intent.

* fix(nodes): recreate door draft on wall:move when null after placement

Mirror of the window fix one commit back: after click-to-place the
DoorTool deletes its transient draft and relies on the wall-rebuild \u2192
R3F pointer-enter cascade to spawn a fresh draft for the next placement.
When that cascade doesn't fire synchronously, the next wall:move sees a
null draftRef and bails \u2014 forcing a leave/re-enter to place again.

Recreate the draft in onWallMove when null and over a valid wall on the
current level. Idempotent with onWallEnter (destroyDraft cleans up if
both fire).

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Pascal <open@pascal.app>
2026-06-04 14:03:00 -04:00
ee98c55b65 Polish: window preset preview + smooth shelf item placement + Shelf build tool (#367)
* fix(nodes,viewer): window preset live wall preview + shaped-cutout move tracking

MoveWindowTool marked the moving window `isTransient` unconditionally, but
WindowSystem only rebuilds the host wall's cutout for non-transient windows —
so a window *preset* (isNew) showed no live hole on the wall and couldn't be
placed consecutively without leaving/re-entering. Guard the transient mark on
`!isNew`, matching MoveDoorTool.

Separately, shaped openings (arch / rounded / `opening`) rebuild their cutout
brush from `node.position`, which a same-wall move doesn't write (it mutates the
mesh directly and publishes to `useLiveTransforms`). The wall-system's
`getEffectiveNode` only merges `useLiveNodeOverrides` (resize arrows), so shaped
cutouts lagged the move while rectangular ones (rebuilt from the live mesh
matrixWorld) tracked. Fold `useLiveTransforms` into door/window children before
collecting cutouts so shaped holes follow the live move too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(editor,viewer,nodes): smooth item placement on shelves

Three issues made hosting an item on a shelf janky (item-on-item was already
smooth because plain items have no `def.geometry` to rebuild):

- Jitter: the draft mesh intercepted the cursor ray, so the shelf-row hit was
  re-derived from the moving item each frame. Disable raycasting on the draft
  during placement (incl. async GLB children, reconciled per frame) so the ray
  passes through to the surface beneath — mirrors MoveRegistryNodeTool.

- Reparent/vanish at the edges: `onShelfLeave` flipped state to floor without
  reparenting the draft off the shelf, so the floor strategy's level-local
  position rendered compounded with the shelf transform. The grid handler now
  owns the shelf→floor transition.

- In/out oscillation: reparenting the draft onto the shelf dirtied the shelf,
  and GeometrySystem disposed+rebuilt its boards, making r3f fire a spurious
  shelf:leave→enter that thrashed placement between the row and the floor. Add
  an opt-in `def.geometryKey` so GeometrySystem skips the rebuild when geometry
  inputs are unchanged (shelf boards don't depend on hosted children). Keep the
  item sticky by testing the cursor ray against the shelf's bounding box: a ray
  that slips through a gap and lands on the floor behind the shelf still counts
  as "on the shelf"; only a ray that misses the shelf detaches to the floor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(editor-app): add Shelf to the standalone editor Build tab

The shelf kind is fully wired (def.tool, presentation icon, StructureTool id)
but was absent from the standalone editor's Build palette. Add it between
Column and Spawn Point. Community has its own build-tab and is left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(mcp): biome-format coordinate-conventions-demo.json

The committed example violated Biome formatting (expanded polygon arrays),
which broke the `mcp-ci` Biome check on main (#356) and every branch since.
Format it so CI is green. Pure formatting — no content change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:10:22 -04:00
AntonandGitHub cefcb013d9 fix(editor): null-safe cursor-group access in placement coordinator (#323)
Guards every `cursorGroupRef.current` dereference in the placement coordinator against the null window where mitt listeners are live but the `<group>` is unmounted (mount/teardown race) — the EDITOR-BC/BD crash family.

`getContext()` falls back to the draft's rotation, so the validation/revalidate path (Shift keys, onKeyUp) is safe; only the cursor *writes* are guarded, so Escape/right-click cancel, Shift reset, leave-state cleanup and transition state still run unconditionally. Also guards `wallPreviewRef` inside `WallTool.stopDrafting()` (the double-click/cancel path that the earlier diff missed).

Reimplemented against current main (the original branch conflicted with #366 and used over-broad handler guards that silently dropped cancel/Shift). Verified: `tsc -p apps/editor` clean, biome clean on touched files.
2026-06-03 15:54:21 -04:00
AntonandGitHub 98eeb1da90 fix(nodes): coalesce node.children to [] in container renderers (#333)
Guards building/ceiling/site/wall renderers with `(node.children ?? [])` so a node whose `children` array is missing (legacy/unparsed scene data) no longer crashes the renderer with "Cannot read properties of undefined (reading 'map')" (EDITOR-C0). Matches the existing guard in roof/renderer.tsx and the Array.isArray check in the parametric renderer.

Note: the schema declares `children: z.array(...).default([])`, so this can only be hit by data that bypasses Zod normalization on load. This is a defense-in-depth crash-stopper; the deeper fix is to normalize/parse legacy nodes in migrateNodes (use-scene.ts) so missing arrays are repaired before render — tracked as a follow-up.
2026-06-03 14:30:12 -04:00
c3199e7540 editor: green/red placement box for shelf + tool sync for presets (#366)
* feat(editor): shelf placement validity box + sync tool for positioned presets

Give the generic move tool a green/red footprint box for shelf placement,
matching the GLB item cursor, and drop the vertical-arrow CursorSphere for
shelves. Box colour comes from canPlaceOnFloor; an invalid (red) drop is
refused unless Shift forces it, and R/T play the rotate sfx. Re-sync the box
transform on node change so a re-armed clone isn't left at the previous
rotation/position.

Extract the box wireframe geometry helpers into a shared
placement-box-geometry module reused by the item coordinator and the new
declarative PlacementBox component. Export the Tool type so host apps can set
the active tool for a positioned preset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: apply biome formatting + ignore generated next-env.d.ts

Format files that predate the current biome config (lineWidth 100, single
quotes, semicolons as-needed) so they stop showing as dirty on every checkout.
Formatting only — no behavior change.

Also exclude **/next-env.d.ts from biome: Next regenerates it (double quotes +
semicolon) on every build, so biome kept reformatting it into a perpetual dirty
diff. Ignoring it lets Next own the file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:52:57 -04:00