Commit Graph
1194 Commits
Author SHA1 Message Date
16ea186f77 fix(viewer): guard texture.repeat against non-array values (Sentry MONOREPO-EDITOR-EK) (#504)
* fix(viewer): guard texture.repeat against non-array values (Sentry MONOREPO-EDITOR-EK)

* fix(viewer): normalize legacy texture repeats

---------

Co-authored-by: openclaw-agent <agent@pascal.app>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
2026-07-19 17:40:18 +02:00
d0ac9fb739 fix(editor): guard GLB export against undefined array-material slots (Sentry MONOREPO-EDITOR-79) (#459)
Co-authored-by: Anton Pascal <anton-pascal@users.noreply.github.com>
2026-07-19 17:40:01 +02:00
6cc10c929e editor: improve cabinet resizing and wall alignment (#503)
* 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

* fix wall treatment miter geometry

* fix cabinet group resizing and corner alignment

* fix cabinet corner depth resizing

* fix(cabinet): stabilize modular preset changes

* fix(cabinet): stabilize corner resizing and wall alignment

* fix(nodes): stabilize wall cabinet depth resizing

* fix(editor): hide cabinet arrows for module selection

* feat(cabinet): add individual width resize handles

* feat(cabinet): improve wall cabinet editing

* fix(cabinet): harden wall cabinet resizing

* fix(cabinet): correct wall drag and depth handles

* fix(cabinet): refine individual depth resizing

* fix(cabinet): preserve context-aware corner depth behavior

* fix(editor): respect snapping modes for resize handles

* fix(editor): harden cabinet resize interactions

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
2026-07-19 17:33:57 +02:00
Aymeric RabotandGitHub c0a5db935e Merge pull request #514 from pascalorg/feat/host-integration-primitives
Expose generic host integration primitives
2026-07-19 17:19:11 +02:00
Aymeric Rabot 8f1c834e56 feat(core): expose generic host integration primitives 2026-07-19 17:11:43 +02:00
Aymeric RabotandGitHub 2084892383 fix: derive zone quantities from room topology (#512) 2026-07-18 14:35:12 +02:00
Aymeric RabotandGitHub cddb73b232 fix: harden measurement geometry (#507) 2026-07-18 09:44:34 +02:00
github-actions[bot] 99cc537e25 release: @pascal-app/core@0.9.2 @pascal-app/viewer@0.9.2 @pascal-app/editor@0.9.2 @pascal-app/nodes@0.1.1 @pascal-app/mcp@0.3.2 @pascal-app/ifc-converter@0.1.2 2026-07-17 17:11:30 +00:00
Aymeric RabotandGitHub cdf026bb92 fix: publish built-in nodes package (#506) 2026-07-17 19:07:12 +02:00
ae87ca5475 feat: add production measurement tools (#505)
* feat: add persistent measurement tools

* feat: make measurements associative

* fix: finish measurements with Escape

* feat: improve measurement snapping guides

* feat: clarify measurement axis feedback

* feat: smart measure lens, zone reports, and direct measurement editing

- Smart measurement lens: registry-owned wall/slab/zone hover reports with a
  single top-center HUD, click-to-pin, latest-event back pressure, and no
  scene writes
- Conservative derived zone quantities (footprint, perimeter, proven
  enclosure, gross wall/floor surface, flat-room volume) with the
  selected-zone blueprint panel
- Direct editing of committed measurements via selected-only 2D/3D vertex
  affordances with midpoint insertion, cancellation, and one-write history
- Shared measurement surface-query session; 2D tracing joins the
  slab/ceiling magnetic pipeline with registered-corner snapping
- Angle arcs on the smaller angle, indigo active/black resting hierarchy,
  screen-sized normal-aligned contact rings

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

* fix: make measurement snapping always magnetic

Measurement drafting and committed-edit paths gated wall, semantic, and
axis magnetism on isMagneticSnapActive(), which is only true in the
'lines' snapping mode — in the default 'grid' mode corners and wall
intersections barely attracted (3D association fell to the 0.012 m
verify tolerance, 2D wall radii to the 0.05 m connect stick).

Measurement is an analysis tool whose anchors exist to bind real
geometry, so its snapping no longer consults the construction
snapping-mode chip: 2D/3D drafting and committed vertex edits are always
magnetic, Alt is the temporary bypass in both views (releasing the axis
pull, wall magnetism, and the 2D projected-geometry pull, and shrinking
association to contact tolerance). A discrete 2D wall snap (endpoint /
midpoint / crossing) now outranks the locked axis pull, and committed 2D
edits route the fallback through the raw pointer so free drags no longer
quantize to the construction grid. Volume extrusion height keeps its
mode-driven grid quantize.

Codex adversarial review confirmed the diagnosis and plumbing; its 2D
Alt-depth and grid-quantize findings are applied. New
surface-plan-snap tests pin the magnetic override seam.

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

* fix(measurement): stabilize area surface intent

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:01:01 +02:00
22c9472066 Adjustments pass: shadow bias, polygon editor UX, space-detection load fix (#502)
* fix(viewer): tune shadow biases to stop acne without detaching shadows

normalBias 0.02 was too small a texel offset for the building-fit 1024
shadow map and brought back self-shadowing acne. Settle on normalBias 0.08
(0.07 and below acnes, 0.1 reads detached) plus depth bias -0.0005 to
suppress the residual acne that a normal bias alone couldn't clear.

Also adds a `?debug=shadowcamera` diagnostic that draws a CameraHelper for
each shadow camera so the fitted frustum can be inspected while tuning.

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

* feat(editor): polygon editor handle UX — focus, cursors, visibility

- vertex/midpoint cylinders ignore scene depth like the edge arrows so
  they stay visible through walls and slabs; vertex radius 0.1 -> 0.08,
  midpoint 0.06 -> 0.05, midpoints use the brighter arrow shade at rest
- during any drag only the active handle stays mounted (other arrows,
  vertices, edge bars and the cross disappear) so the gesture reads clearly
- handles set pointer cursors: move for vertices/midpoints/cross, and a
  screen-space direction-aware resize cursor for the edge arrows

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

* fix(editor): keep ceiling affordances quiet during any interaction

The ceiling corner brackets' hit boxes caught drag-time hover (spatial
events keep firing during host drags by design), set hoveredId to the
ceiling and flashed the ceiling grid mid-gesture — e.g. while dragging a
slab polygon vertex. Unmount the brackets while ANY interaction scope is
active, and gate CeilingSystem's hover-driven grid reveal on idle scope +
no inputDragging as a second layer.

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

* fix(core): stop room detection resurrecting deleted slabs on load

Two holes in the wall-driven auto slab sync:

- initSpaceDetectionSync started with an empty baseline, so scene
  hydration (one atomic setScene) read as 'every wall changed' and ran a
  full detection pass on every load, recreating auto slabs the user had
  deleted. Seed the baseline from the store at init and treat a level's
  first snapshot as baseline — detection now only reacts to in-session
  wall edits.

- matchesManualFootprint required mutual coverage, so a single manual
  slab spanning multiple rooms never suppressed those rooms' auto slabs
  (only a fraction of it lies inside each room). Suppression now only
  asks whether the ROOM is substantially covered by the union of manual
  slabs.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:55:05 -04:00
Wassim SAMADandGitHub 9ca3eaa7fe feat: add project plugin management (#501) 2026-07-16 10:26:59 -04:00
Wassim SAMADandGitHub 4f0aa7f7b6 fix(core): prevent unbounded slab corner miters (#500)
* fix(core): bound slab corner miters

* style(core): format slab miter fix
2026-07-15 20:10:50 -04:00
Wassim SAMADandGitHub 78223d61cf fix(core): exclude dangling walls from room polygons (#499) 2026-07-15 17:53:51 -04:00
a524da1574 Fix room surfaces, wall openings, and paint scope (#498)
* chore(core): point material catalog at KTX2 tiers for wood/flooring/roofing finishes

All 48 remaining webp/jpg/png finish entries now reference _512.ktx2 maps
and 256px _thumb.webp previews, matching the fabric/leather/concrete/metal
convention. flipY set to false on the converted entries — compressed
textures can't be flipped at upload.

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

* fix: stabilize room surfaces and wall openings

* style(core): format KTX2 material catalog

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:35:04 -04:00
4fca38a3ef fix: make room walls and slabs join cleanly (#497)
* fix(editor): wall endpoint move — detach/attach modifier sync and zero-move drop

- restore linked walls to original positions the moment alt-detach engages
- re-run the endpoint preview on alt keydown/keyup so re-attach doesn't wait for a mousemove; preview, HUD badge, and commit share one alt source
- second click at an unmoved position cleanly drops the endpoint (no history entry) instead of leaving the interaction stuck
- 2D floorplan parity: clear stale linked overrides on detach, re-apply drag sessions on modifier changes

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

* fix(core): demote orphaned auto slabs/ceilings to manual instead of deleting

Deleting a wall that encloses a room no longer destroys the room's
slab/ceiling (paint, holes, elevation). Unmatched auto surfaces are only
deleted when >=60% of their footprint is still covered by a detected room
(rooms-merged case); otherwise they are demoted to manual nodes.

- demoted slab polygons are baked (inset by SLAB_OUTSET + AUTO_SLAB_INSET)
  so the rendered footprint doesn't jump between the auto and manual paths
- auto-creation suppression now also matches manual nodes by mutual
  footprint coverage, so re-closing the room doesn't stack a duplicate
  auto surface on the demoted one

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

* fix(editor): hide alt-to-detach hint when the moving endpoint has no linked walls

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

* fix(editor): split host walls in every snap mode, stop wall chain on existing walls

Splitting a room with a wall now splits the walls it lands on and migrates
hosted doors/windows/items — matching what already happened to the slab and
ceiling. The split machinery existed but was gated on magnetic snapping
('lines' mode) while the wall tool defaults to 'grid'; split resolution now
always runs, with the join radius scoped to the active mode (0.35 magnetic,
0.05 connect snap otherwise), and the whole commit lands as one undo step.

The drawing chain now terminates when a committed segment ends on a wall
outside the current chain (T-junction), like the room auto-close — users
don't draw overlapping walls. Applied in 3D and 2D, and the 2D-only path
gains the previously missing wallClosesRoom parity check.

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

* feat(core): per-edge build-time slab offsets — slabs tile at wall centerlines

Replaces the stored-polygon render offsets (auto: centroid inset 0.02,
manual: flat outset 0.05) with one uniform per-edge rule computed at
geometry build time, for auto and manual slabs alike:

- edge shared with a sibling slab (collinear-overlap test, T-junction
  sub-segments included) → small relief inset; adjacent rooms tile
  exactly and can never overlap
- edge on a wall centerline with no slab neighbor → expand outward by
  that wall's thickness/2, flush with the facade
- free edge → rendered exactly as drawn

Slab demotion no longer bakes polygons (offsets never live in node
data); no stored-data migration. New slab system marks level slabs
dirty when wall geometry/thickness or sibling slab footprints change.

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

* fix(viewer): import KTX2Loader from its deep path, not the jsm aggregate

The Addons.js aggregate re-exports LottieLoader/TTFLoader whose CDN URL
imports (lottie-web, opentype.js) abort bun test in every package that
transitively imports the viewer.

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

* feat(editor): split host wall when an endpoint move lands on its interior

Moving a wall endpoint onto another wall's mid-span now splits that wall
and migrates its doors/windows/items, matching the draw tool — previously
the room closed (detection planarizes internally) but the wall node stayed
whole. New resolveEndpointWallSplit reuses the draw path's split pipeline;
endpoint write + split commit as one undo step. Applied in 3D and 2D.
Corner drops still join without splitting; straddling openings skip the
split; alt-detached commits split the stationary former sibling correctly.

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

* feat(core): slab-wall adoption — absolute edge projection, band snapping, rendered-footprint wall elevation

Slab edges near a wall now render projected onto the wall's exact
geometry instead of getting relative offsets, healing legacy scenes
(face-aligned edges, old baked outsets, hand-drag slop) with no data
migration:

- adoption band: an edge collinear with a wall centerline within
  thickness/2 + 6cm (both sides) is wall-backed; nearest centerline
  wins, a sibling slab in the same band forces the interior seam
- wall-backed exterior edges project to the outer face; interior edges
  to the centerline minus the relief gap; free edges render as drawn
- slab edge resize: wall snapping is now edge-based, not cursor-based —
  fixes the drop landing short by the grab offset (0.34m arrow gap in
  3D, hit-stroke slop in 2D); the snap translates the edge onto the
  wall centerline (canonical stored position), beacon/preview/commit
  agree, full band in lines mode, 5cm stick otherwise; 2D shows a
  dashed stored-boundary skeleton when it diverges from the fill
- wall elevation tests the slab's rendered footprint instead of the
  stored polygon with a 0.1mm epsilon, so walls sit on legacy slabs
  and re-elevate when slabs are reshaped

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

* fix(core): subcut slab edges per backing span

A slab edge backed differently along its length (offset rooms sharing a
wall over part of the span, a wall shorter than the edge, collinear
walls of different thickness) is now subdivided at the backing-span
breakpoints; each sub-span classifies and projects independently, with
a perpendicular step connector at intra-edge transitions that lands
inside the crossing wall's footprint. Breakpoints closer than 5cm merge
so no sliver geometry reaches the ring, and same-target spans re-fuse
(curved-wall sampling doesn't balloon vertex counts; whole-edge cases
render bit-identically to before).

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

* fix(core): wall elevation picks the slab that supports it, not the highest graze

A wall touching a raised slab only at an endpoint no longer lifts
entirely to that slab. Elevation selection is now coverage-based:
per-slab support is the wall's centerline+face length covered by the
slab's rendered footprint minus holes; slabs within 0.1mm of elevation
pool their support (party walls spanning two rooms still lift); the
wall sits on the highest elevation covering >=50% of its length, else
the best-covered group (ties prefer higher), else 0. Sub-5cm grazes
are ignored outright.

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

* fix(core): interior slab edges meet exactly at the wall centerline

Removes the 2cm interior relief inset: both rooms' seam edges project
onto the same centerline (or the symmetric sibling midline when no wall
backs the seam), so adjacent slabs tile with a shared edge — the 4cm
slit under shared walls, visible at its open end on the facade, is
gone. Safe against z-fighting because slab side quads are single-sided
(FrontSide is enforced repo-wide for the MRT scene pass) and the
coincident seam faces have opposite normals; junction step pockets grow
to 5cm but remain strictly inside the intersecting wall footprints.

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

* fix(core): elevation-aware interior slab seams — close the vertical band slit

With unequal room elevations, both slabs stopping at the shared wall's
centerline left the lower room's half of the wall band open between the
lower slab top and the wall base (the wall seats on the higher slab).
Interior seams across a wall are now elevation-aware: equal elevations
keep the exact centerline seam; unequal elevations project BOTH slabs
to the wall face on the lower side — the higher slab runs through the
band under the wall, the lower butts the same plane. Wall-less unequal
seams keep the sibling midline (a visible step face is correct there).
Slab elevation joins the level dependency signature so height edits
rebuild sibling slabs live.

Verified against real local scenes: all wall bands solid where the
seam rule applies (remaining pockets are the known flat-wall-base
limitation, tracked separately).

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

* fix(plugin-trees): run wind displacement before the instance transform

The r184->r185 upgrade broke the plant wind: r185 fixed TSL's statement
emission order so a material's positionNode now runs *after* the
instancing transform (r184 emitted it before — verified in the compiled
WGSL of both versions). The wind nodes were tuned against the r184
order, reading positionLocal as geometry-local coordinates, so on r185
the displacement moved into level space: sway no longer scaled with the
per-instance scale (scaled-down trees thrashed like a storm, leaf cards
visibly detaching from branches), leaf phase followed world placement,
and STEM_BEND's height term read the floor elevation, so grass/flowers
on upper levels slid around rigidly.

Restore the r184 semantics explicitly: WindNodeMaterial assigns the wind
node to positionLocal inside setupPosition() before super applies the
instance transform, instead of using positionNode. The emitted WGSL is
statement-for-statement identical to r184's.

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

* fix(core): resolve slab joins across floor elevations

* fix(editor): keep slab resize arrows visible

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 13:28:29 -04:00
a590747748 Adjustments pass: three 0.185, undo/cancel semantics, guide & panel fixes (#496)
* fix(editor): only start handle drags on primary button

Right-click over a rotation/move/resize handle started the gesture and
stopPropagation()'d, fighting the camera orbit. Guard every gesture
starter (shared useHandleDrag, group rotate gizmo, wall endpoint/height/
move, fence move, roof trim) with event.button !== 0 before it swallows
the event.

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

* feat(viewer): default units from timezone/locale until user picks

Derive the metric/imperial default from the IANA timezone (US, Liberia,
Myanmar zones -> imperial; anything else -> metric), falling back to an
explicit locale region subtag only when no timezone resolves. Timezone
tracks actual location, unlike navigator.language where en-US is a common
default far outside the US. The unit is only persisted once the user
explicitly sets it, so an untouched preference keeps tracking location;
existing persisted values are treated as explicit and left alone.

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

* fix(viewer): toggle shadows via renderer.shadowMap.enabled, not castShadow

Flipping a light's castShadow at runtime crashes three r184's WebGPU
renderer: toggling off disposes the shadow map's GPU texture, but the
node builder cache evicts with the post-toggle key, so the shadows-on
entry survives still referencing the destroyed texture. Re-enabling
reuses that stale state and every frame submit fails with
GPUValidationError ("Invalid CommandBuffer from CommandEncoder").

Keep castShadow static and drive the user-facing toggle through the
Canvas shadows prop (renderer.shadowMap.enabled), which rebuilds
materials without disposing shadow resources.

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

* fix(core,editor): floor undo history at scene load

Undo could step back past the scene load into the pre-load (empty)
state, wiping the whole project — which autosave would then persist.
Two defects: clearSceneHistory() had zero call sites, so every load left
the empty pre-load state in zundo's pastStates; and setScene wrote the
store twice, recording a half-normalized intermediate as a second undo
target.

- applySceneGraphToEditor, JSON import, and reset-to-default now clear
  history so the loaded scene is the undo floor
- setScene collapses to a single tracked write (final state identical)
- clearSceneHistory also resumes tracking so a load landing inside a
  pause window can't strand undo recording off

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

* build(three): upgrade runtime to 0.185.1, pin @types/three at 0.184

r185 renames directionToColor/colorToDirection to packNormalToRGB/
unpackRGBToNormal and splits SSGI's packed rgba output into separate AO
(getAONode, single channel) and GI (getGINode) textures; wind-node's
positionLocal reads become positionGeometry.

@types/three stays at 0.184.1: the 0.185 typings send tsgo's inference
into unbounded allocation (microsoft/typescript-go#2125 class — it ate
~70GB/90s and OOM-killed the machine). viewer/lib/tsl-compat.ts bridges
the two renamed TSL exports with 0.184-typed signatures; drop it and the
pin together once tsgo copes.

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

* fix(viewer): gate .ktx2 preset-texture loads on detectSupport

KTX2Loader.load throws before detectSupport has run, and materials
created while a standalone capture canvas's renderer was still
initializing cached themselves permanently texture-less — fabric slots
rendered white in item thumbnails. .ktx2 loads now await whenKtx2Ready()
(resolved by the first successful ensureKtx2Support), which is exported
so hosts with standalone canvases can arm it.

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

* feat(viewer): accept host-supplied country for the unit default

applyCountryUnitDefault lets the host app feed an authoritative
IP-derived country (e.g. Vercel's x-vercel-ip-country) into the unit
default. Stronger signal than the timezone heuristic applied at store
creation, still never overrides an explicit user choice.

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

* fix(editor): box select starts over locked guide images

A locked guide's hit-rect swallowed pointer-down via stopPropagation, so
marquee selection couldn't start on top of it. Locked guides now let the
event bubble to the svg root; click-to-select and the unlock affordance
still work because a non-drag release fires onClick as before.

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

* feat(editor): live rotation readout while rotating a guide image

Rotating a guide with the 2D handles gave no angle feedback. Reuse the
registry layer's RotationAngleOverlay (wedge + degree chip) for guide
rotate drags: sweeps from the grabbed corner's bearing at grab to its
current snapped bearing, suppressed under ~0.5deg so a fresh grab doesn't
flash a sliver.

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

* feat(editor): lingo free-text input in the guide set-scale dialog

The real-length field accepts natural measurement text via
@pascal-app/lingo — 5'11", 180cm, 1m80, 12ft — parsed in the dropdown's
unit (a bare number still means that unit, a typed unit wins). A faint
'= 1.80 m' hint previews non-trivial input, unparseable text gets a
clear error, and the odd onBlur force-reset to 0.0001 is gone.

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

* feat(editor): cmd+z mid-interaction cancels the gesture instead of undoing

Undo pressed while the mouse is mid-action (moving, drawing, dragging a
handle) used to history-jump under the live pointer — stale carry, half
gestures committing against a rewound scene. Now it reads as 'abort this
action', exactly like Escape:

- the global undo/redo arms first route through the tool:cancel path
  (covers build drafts, placement ghosts, move tools) and skip the
  history jump when anything was in flight (consumed, scope-active, or
  inputDragging);
- pointer drags that only knew pointercancel (generic handle drags,
  group rotate, wall side/height handles, roof trim) gain the same
  capture-phase Escape/cmd+z keydown the group-move drags already had —
  fixing Escape for them too;
- the existing capture-phase handlers (3D/2D group move, 2D registry
  move overlay) additionally accept cmd+z as cancel.

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

* fix(editor): box select arms over any guide image that won't drag

Follow-up to cd830279, which only let LOCKED guides bubble pointer-down.
An unlocked, unselected guide also swallowed the event for nothing (no
translate drag starts), so marquee selection could never start on top of
it. Now only the one case that uses the event consumes it — selected +
unlocked → translate drag — and everything else bubbles to the svg root.
Click-to-select still works: a non-drag release never crosses the
box-select threshold, so the trailing click fires the guide's onClick.

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

* fix(editor): cmd+z during draft placement cancels instead of undoing

The preset/item placement flow (useDraftNode + placement coordinator)
registers no interaction scope and holds no pointer, so the cmd+z cancel
guard from c699d74e saw it as idle and history-jumped mid-placement.
Paused scene history is the universal tell — the draft cycle (and every
adopted-move session) keeps temporal paused for the whole gesture, and
an undo against a paused store lands on a stale baseline anyway. Treat
!isTracking as in-flight.

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

* fix(editor): cmd+z mid-placement completes the cancel, not just skips undo

4ffda723 stopped the history jump during preset/item placement but left
the draft alive: the item tool passes no coordinator onCancel — it is
Escape's fall-through (switch to select, unmount the tool) that actually
destroys the draft. Extract that fall-through and run it from the cmd+z
path too whenever a gesture is live and nothing consumed tool:cancel,
so cmd+z now behaves exactly like Escape end to end.

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

* fix(editor): node selection clears a lingering reference selection

Selecting a guide clears the node selection (handleGuideSelect), but the
reverse was never wired: clicking a wall with a floorplan reference
selected left selectedReferenceId set, and the panel manager's
reference-first priority kept showing the floorplan panel until it was
closed by hand. PanelManager now drops the stale reference the moment a
scene selection (nodes or zone) appears.

Also: the inspector's expanded state is shared across panel swaps by
design, but it survived close/reopen too — deselecting everything now
resets it, so a fresh selection opens the panel collapsed again.

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

* fix(plugin-trees): wind displacement reads positionLocal, not positionGeometry

The three-0.185 migration renamed positionLocal to positionGeometry in
the wind nodes, but positionLocal was never removed in r185 — and the
two are not interchangeable here. NodeMaterial.setupPosition applies the
instance transform by mutating positionLocal, then overwrites it with
positionNode's output; reading raw positionGeometry therefore discarded
every instance matrix — leaf cards rendered unscaled at tree-local
coordinates (a giant canopy filling the sky) and grass/flower instances
collapsed invisibly. Reading positionLocal (instance transform included)
restores r184 behavior exactly.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:26:35 -04:00
3625d17bfc fix(editor): default sidebar to build tab, drop to select on panel close (#495)
The AI/chat tab was the default panel when opening a project; build is
the expected starting point. Also, collapsing the sidebar (rail click or
drag-collapse on desktop, sheet close on mobile) left the last build
tool armed — it now resets to select mode.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:48:25 -04:00
47313263fb feat(viewer): rendering & lighting pass — sun-dominant look, sky backdrop, grounded horizon (#493)
* feat(editor): dev-only window hook for deterministic camera poses

Exposes a getter for the CameraControls impl in development so
screenshot/automation tooling can set exact camera poses.

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

* feat(viewer): rendering pass — sun-dominant lighting, grade, gradient-sky IBL, albedo clamp, SSGI tune

- Shadow intensity clamp 0.55 → 0.9 (sun no longer leaks into shadow),
  2048 shadow maps, PCFSoft filtering
- Scene-referred contrast/saturation grade before ACES output
  (GRADE_PARAMS in post-processing)
- Procedural gradient-sky IBL (cool zenith / warm horizon / ground
  bounce) replaces the venice_sunset HDR fetch; env-only, background
  unchanged
- Near-white albedos clamped to ~0.83 linear (defaults, white palette,
  catalog preset-white/softwhite, schema presets)
- SSGI: 2 slices / 6 steps, radius 1.6, aoIntensity 1.7, giIntensity 2
  (bounce on); studio hemi 0.6→0.45, fill 0.75→0.6

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

* feat(viewer): shadow-caster-only cutaway, glass fresnel, ground fade, specular-map unwiring

- SHADOW_ONLY_LAYER (4) + lib/shadow-only.ts: hidden roofs/levels in
  level-solo (editor) and dollhouse (GLB viewer) stay in the shadow map,
  so interiors keep sun shadows + window light patches; only the sun's
  shadow camera enables the layer
- Glass: fresnel-driven opacity + envMapIntensity on transparent
  standard materials (catalog glass, scene glass, window default)
- Site ground: radial fade into the theme background at the lot
  boundary (TSL colorNode); dead ground-occluder.tsx removed
- Catalog: 22 bogus *specular*→metalnessMap wirings removed (specular
  level maps are not metalness; they darkened/metallized dielectrics)

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

* feat(viewer): PCSS contact-hardening sun shadows via LightShadow.filterNode

Custom TSL filter: Vogel-disk blocker search (textureLoad — the sampled
path would inherit the comparison sampler, which WGSL rejects) →
receiver-blocker penumbra estimate → variable-radius rotated PCF.
shadow.radius scales max penumbra (now 4).

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

* feat(viewer): dark-theme recalibration for the new lighting pipeline

Night/twilight: lifted ambient/hemi beds (the 0.9 shadow intensity
crushed them), brightened theme grounds to a lit mid-tone, and dimmed
the daylight gradient-sky IBL to 0.2 for dark appearances.

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

* feat(viewer): rendering-pass follow-ups — GI denoise, lighter interactive SSGI, sky gradients, scoped solo shadows

- Denoise the SSGI GI bounce (it composited raw — the visible grain) and
  drop giIntensity to 1
- Interactive SSGI back to 1 slice (×6 steps); SSGI_BAKE_PARAMS (2×6)
  for the thumbnail/bake pipeline
- Per-theme backgroundSky: vertical zenith→horizon backdrop gradient in
  the post pipeline; makes the lot-edge ground fade read in every theme
- Level solo: only levels above the soloed floor stay shadow-caster-only;
  below-levels plain-hide (they can't block the sun)

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

* feat(viewer): kill SSGI grain, real horizon — infinite ground disc fading into the sky

- SSGI back to 2 slices × 6 steps: three's own minimum preset without
  temporal filtering — 1×6 was below the floor and the grain showed on
  flat walls; denoise radius 5 on both AO and GI, aoIntensity 1.5;
  bake preset raised to 3×8 (single-frame renders)
- Site renderer: presentation horizon disc (8× lot radius, min 400 m)
  under the lot in the theme ground colour, fading radially into the
  theme background; lot fill back to plain ground colour (the disc
  carries the fade); never pickable (noop raycast)
- Backdrop sky gradient compressed to the upper half of the screen so
  it meets the disc's far fade at exactly the horizon colour — no seam

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

* feat(viewer): laptop-budget SSGI + seamless world-space horizon

- SSGI interactive: 1 slice × 6 steps, GI bounce OFF (denoised AO only)
  — 2 slices was a thermal problem on laptops; the bounce moves to
  SSGI_BAKE_PARAMS (3×8, gi 1) for one-shot renders
- Horizon disc dissolve: albedo fades to black while emissive fades to
  the background colour, so the far end IS the backdrop (no lit-vs-flat
  seam); backdrop gradient graded with the same transform as the scene
- Sky gradient is now world-space: per-pixel view ray reconstructed from
  the scene camera matrices, sky above the true horizon (dir.y 0→0.35),
  pure background below — aligns with the disc at any camera angle
- Studio theme: ground #e9e7e2 / horizon #fbfbfa / sky #dde7ef so the
  fade is actually visible against the white void

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

* fix(viewer): kill shadow grain (PCSS opt-in only) and the inked horizon line

- The eye-level ground grain was measured (high-pass σ on a flat patch:
  2.95 baseline → 1.52 shadows-off → 2.49 ao-off): PCSS's per-pixel IGN
  dither was the dominant source, and it can't be fixed within a laptop
  budget without TAA. Interactive shadows revert to the renderer's
  PCFSoft (clean, cheap); PCSS stays wired behind ?enable=pcss for
  experiments and future bake-time use
- The horizon 'line' was the ink pass edge-detecting the ground disc's
  depth silhouette against the backdrop. Ink now fades with raw depth
  (full below ~150 m, gone past ~350 m) — near silhouettes keep their
  SketchUp line, the horizon dissolves cleanly

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

* feat(viewer): soft blue skies for light presets, slab coplanarity epsilon

- backgroundSky retuned: studio/paper/blueprint/verdant get a soft blue
  zenith (overcast keeps a bluish gray — it's overcast; mediterranean/
  sunset were already blue; dark themes untouched)
- Slabs duplicated at the exact same position z-fight and no camera
  near/far tuning can separate identical depths; each slab mesh now gets
  a deterministic sub-3mm lift hashed from its node id. Render-only —
  node data, snapping and measurements untouched. (Long-term fix is
  reversedDepthBuffer; parked in plans with the depth-consumer audit.)

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

* fix(viewer): walk back perf-costly rendering-pass pieces, soften dark-theme ink

Review feedback on the rendering pass:
- SSGI back to main's params (1 slice / 4 steps / r1, AO-only) everywhere,
  including thumbnails — SSGI_BAKE_PARAMS removed. Kills the added AO grain
  and the extra per-frame cost; bake workers also stop paying for heavier GI.
- Shadows stay visible via plain knobs only (intensity 0.9, radius 4): the
  custom TSL PCSS filter is gone, shadow map back to 1024, filter back to PCF
  (r184's Vogel-disk PCF respects radius, so edges stay soft).
- Night/twilight ink edges: colour now derived from the theme background
  (lifted toward white) instead of a near-white constant, and dark scenes run
  the ink at 70% alpha — no more glowing wireframe on dark backdrops.
- Snapshots: the `transparent` capture flag is now honored — preset/item
  captures keep their alpha, while studio renders and project thumbnails
  composite the theme background + sky gradient (same world-ray math as the
  viewport backdrop, uniform-driven so the cached pipeline serves both).

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

* feat(viewer,nodes): ground the scene — contact vignette + horizon haze, shadow tune

Follow-ups from review:
- Shadow intensity 0.9 → 0.75 (read too heavy) and PCF radius 4 → 2: the
  filter's per-pixel dither spreads with radius, which showed as dots across
  wide penumbras. Both are free knobs.
- New shared backdrop formula (viewer lib/backdrop.ts): background below,
  theme-derived haze band hugging the horizon (background lifted toward
  white — faint glow on dark themes), sky above. Used by the post pipeline,
  the thumbnail pipeline, and the site horizon disc, whose far-field
  dissolve now evaluates the same gradient per fragment view direction —
  ground and backdrop converge to identical colours, so no horizon seam
  from any camera pose.
- Contact vignette on the horizon disc: a soft albedo darkening hugging the
  lot (15%, fading out by ~2.6 lot radii) so the parcel sits on the field
  instead of floating on it. Albedo-only — never tints the dissolve.

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

* fix(nodes): make the contact vignette read on bright themes

A fixed 15% albedo cut disappears into the tone mapper's shoulder on themes
with strong key lights (studio runs intensity 4), and the dissolve's bright
emissive diluted what was left. Scale the vignette with the theme's strongest
light (0.13×, clamped at 0.45) and apply the halo to the in-band emissive as
well — it zeroes out by 2.6R while the dissolve completes at 5R, so the far
field stays the pure backdrop and the horizon seam guarantee holds.

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

* fix(viewer): no more horizon line, sky gradient reaches the horizon

Two things drew a visible line where the infinite ground met the sky:
- SSGI AO grows a band along the geometry↔sky depth cliff (same disease the
  ink pass had). Fade AO to 1 with raw depth over the ink's ≈150→350 m
  window, in both the viewport and thumbnail pipelines — AO is a near-field
  cue, it has no business shading the horizon.
- The backdrop's flat haze plateau sat between two ramps, which the eye
  amplifies into Mach lines. The gradient is now one smooth background→sky
  ramp crossing the horizon, with the haze applied as an exponential glow
  peaking exactly at dir.y = 0 — C¹-smooth on both sides, edge-free.

The sky ramp also starts at the horizon instead of ~11° up, so the theme's
backgroundSky blue actually reads at eye level instead of hiding at the top
of the frame.

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

* feat(viewer): three-stop sky — derived deep zenith, warm horizon haze

The single pale backgroundSky stop read as a white void with blue hiding at
the top of the frame. The backdrop is now three derived stops, inZOI-style:
- pale theme sky arrives fast (full by ≈8° elevation),
- then deepens toward a zenith colour derived per theme in HSL (saturate
  ×1.5, darken ×0.72 — hue stays the theme's own: blue studio, lavender
  sunset, near-black night),
- horizon haze now lifts toward a warm white (#fff4de) instead of pure
  white, giving the junction the slight yellow of sun-scattered atmosphere.

All derived from the existing backgroundSky/background fields — no theme
data changes, and the shared-formula seam guarantee (viewport = captures =
horizon disc) carries over.

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

* fix(viewer): horizon haze derives from the sky, hugs the horizon tighter

White-based haze read as a tall white stripe between ground and sky. Aerial
perspective is sky-coloured light with a little sun scatter, so the haze now
pulls the theme's backgroundSky toward the warm sun tint (50% light themes,
25% dark) and the glow decay tightens (exp −7→−11, weight 0.9→0.8) — the
merge band is skyish, sunish, and half the height, so blue starts right
above the ground line.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:24:58 -04:00
c45808840c nodes: add modular cabinets and wall trim controls (#461)
* 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

* Add modular cabinet node

* Add modular cabinet run and tall cabinet workflows

* Add cabinet paint slots and material groups

* Improve modular cabinet presets and wall snapping

* Improve cabinet wall snapping

* Fix cabinet wall snapping and countertop joins

* Improve cabinet appliance compartments

* Improve fridge visuals and smooth cabinet animations

Fridge exterior is now brushed silver with brass accents and the
interior is all white. Door/drawer animation no longer rebuilds the
full cabinet geometry per frame — animated parts carry pose metadata
that a new per-frame cabinet system applies directly to transforms.

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

* Improve gas hob flames and cooktop knob controls

Port the curved-flame look from reference gas-burner photos: each burner
gets a ring of vertex-coloured tube flames (blue body, orange-yellow tips)
whose spines breathe and flicker per frame at ~30fps, plus a flat ignition
glow and a faded heat halo. Fix the knob pointer notch to rotate with the
knob instead of drifting sideways. Also includes cooktop compartment
presets/panels and cabinet selection/move affordance work.

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

* Fix cabinet UVs and registry actions

* Fix registry move snap typecheck

* Fix cabinet knob animation concurrency

* Fix cabinet material disposal, undo flood, and 2D move parity

- Flag shared cabinet appliance materials (and the viewer's cached material
  factories) as cached so geometry rebuilds no longer dispose materials
  still referenced by other nodes, forcing scene-wide shader recompiles.
- Route the door/drawer open animation through useLiveNodeOverrides with a
  single final commit, so one play is one undo step instead of ~20.
- Add a cabinet-module floorplanMoveTarget: 2D drags now convert through
  planToLocal + magneticSnap like the 3D path, instead of writing plan
  coords into the run-local position (teleporting modules on rotated runs).
- Re-key sibling runs (cabinetAdjacencyRevision) when a run's
  neighbor-affecting inputs change, so countertop overhang joins re-trim
  when a neighbor moves/resizes/deletes.
- Narrow the cabinet panel's scene subscriptions (useShallow module
  selector) so it stops re-rendering on every scene mutation.

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

* Restructure cabinet schema and centralize run math/mutations

Prepares the cabinet node for the queued specialty units (corner L-shape,
sink base, appliance gap, open shelving):

- Add a `moduleKind` discriminator (default 'standard') on CabinetModuleNode
  so new unit types extend an enum instead of overloading the stack, and
  split CabinetCompartment into a z.discriminatedUnion so invalid field
  combos (drawer with rack style, fridge with burner state) are
  unrepresentable. Shared box fields now come from one `cabinetBoxFields`
  object so run/module schemas can't drift.
- Extract the straight-line run assumption (sort-by-x, edges, adjacency,
  spans, side-insert, reflow, frame transforms) into run-layout.ts —
  previously spread across definition/geometry/quick-actions/stack/
  move-frame, so corner support would have meant five parallel edits.
- Consolidate the run mutations (add module, wall cabinet/hood above,
  base↔tall switch, layout-revision bump) into run-ops.ts on SceneApi.
  Panel and quick-actions had drifted copies: the panel's add-module
  skipped gap checks, revision-bump scope differed per surface.
- Remove dead code: node-level doorStyle (with a migrateNodes entry —
  geometry only ever read per-compartment doorType), the unreachable
  slot-handle mesh block, the no-op handlePosition 'edge' enum value,
  wallLocalX, and duplicate totalCabinetHeight definitions.

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

* Add cabinet snap tests and split the cabinet panel

- New tests: cabinetModuleParentFrame.magneticSnap (flush snap, threshold,
  Z alignment, nearest-edge) and wall-snap's resolveCabinetWallFaceOffset /
  collectCabinetWallSnapNeighbors (straight face, miter taper, ray-miss
  fallback, yaw/face/parent filtering) — 87 cabinet tests, up from 71.
- De-brittle geometry test lookups: coordinate-encoded mesh names are now
  matched by pattern, so dimension-default changes don't break them.
- Split panel.tsx (1,541 → 679 lines): CompartmentCard + option constants
  into compartment-card.tsx, CabinetRunPanel + reflow wiring into
  run-panel.tsx, and the compartment type-transition tables into a pure
  resolveCompartmentTransition in stack-transitions.ts.

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

* Split cabinet geometry.ts into per-appliance modules

Pure code motion: geometry.ts (4,728 lines) becomes a 336-line orchestrator
dispatching into geometry/ — shared helpers + cached appliance materials
(shared.ts), CSG fronts/handles/doors/drawers (fronts.ts), run spans +
countertop (run.ts), and self-contained hood/fridge/cooktop/dishwasher/
oven-microwave/pantry builders. No mesh names, userData stamps, materials,
or math changed; the './geometry' import path is unchanged.

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

* Add undermount sink compartment with CSG-cut countertops

New 'sink' compartment type (single / double / 60-40 bowl layouts)
following the cooktop pattern: zero stack height, countertop-plane
geometry, panel card with a bowl-layout selector, and a Sink Base
preset. Bowl openings are subtracted from both module- and run-owned
countertop slabs via three-bvh-csg; the basin shells, drain plumbing,
and a spec-sheet-proportioned gooseneck faucet (Grohe Minta-style pin
lever) render beneath the cut. Sink modules skip the carcass top panel
and the deck under the sink row so the basin hangs through.

Also scope the viewer mock in roof-surface-placement-guides.test.ts to
only stub useViewer — the bare module stub leaked into later suites and
broke real CSG imports.

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

* Update ifc-converter next-env reference to build-mode route types

Regenerated by `next typegen` during check-types; points at
.next/types instead of the dev-mode .next/dev/types path.

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

* Draw cabinet floor plans with kitchen drafting symbols

Replace the per-module box soup with NKBA/architectural-convention
symbols: the run draws one heavier countertop outline per span
(extended by the overhang), base modules draw their front edge plus
compartment symbols — rounded bowl rects + faucet dot for sinks,
burner/zone rings for cooktops (reusing the 3D layout tables so 2D and
3D always agree) — and appliance modules carry standard upright labels
(DW / REF / OV / MW / PAN). Nested wall cabinets and hood-only modules
draw as dashed open outlines per the above-cut-plane convention.

* Play cabinet open/close animation on the E interaction key

Add an `e` slot to registry KeyboardActions and dispatch it in the
keyboard hook ahead of the legacy door/window arms, so kinds opt into
the E interaction on their NodeDefinition. Cabinets register it: E on a
module eases its doors/drawers open or closed, E on a run swings every
child module together, and hood-only modules fall through. The panel's
rAF animator moves into cabinet/interaction.ts (live-override frames,
single undo commit) so the Play button and E share one animation, and
the shortcuts dialog now documents E.

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

* Add kitchen island and bar counter support to cabinet runs

Islands: run-level countertopBackOverhang (seating side) and
withFinishedBack (decorative back panel), plus an I-key island placement
mode in the cabinet tool that skips wall snap. Bar counters: optional
barLedge {edge, height, depth} drawing a knee wall and raised slab along
the back or either run end, superseding the seating overhang on its
edge. withWaterfall drops slab-material panels to the floor on exposed
run ends. 2D floorplan outlines, selection bounds, geometry keys, and
run panel controls updated in parity; schemaVersion 3 -> 6.

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

* Merge quick-action arrow and plus into one directional add glyph

The add-left/add-right buttons showed two unrelated icons; a single
plus-on-tail arrow reads as "add on this side".

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

* Extend plugin discovery instead of replacing the host source

Also track the ifc-converter next-env routes reference update.

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

* Add L-corner runs to modular cabinets with run-scoped grouping

An "L Left/Right" quick action spawns a perpendicular base leg, wall leg,
and wall bridge as metadata-linked runs parented to the SOURCE RUN, so the
run stays the single group: selecting a base module selects only it, and
the clicked module never becomes a container. Corner runs re-anchor when
the source module resizes or moves (new MovableParentFrame.onCommit hook,
mirrored in the 2D floorplan commit), run bounds fold in child leg runs,
and deleting one corner member removes only that node while unlink patches
keep the survivors' link metadata consistent.

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

* Delete emptied cabinet run groups in the same delete gesture

Removing a run's last child (module or derived corner leg) left an
empty group node in the scene graph and persisted data. onDeleteCascade
now receives the gesture's pending delete ids so multi-select deletes
count siblings as gone, and both cabinet kinds cascade the orphaned
parent run away in one undo step.

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

* Remove filler front gap treatment

* Apply architecture review fixes across cabinet, plugin-trees, and framework

Move cabinet semantics out of packages/editor behind registry capabilities
(def.tree, selectionProxy.bypassDirectPick, IconRef quick-action icons,
catalog tool field), route cabinet selection through a core selection-proxy
helper, and make PanelWorkspace an opaque host tag. Fix the cabinet
placement undo flood, dirtyNodes.add bypasses, normalized UVs on paintable
slots, plugin-trees ghost layers/stale level transforms/untyped find-sync
event, and cross-file store-mock pollution plus orphaned dist tests that
broke combined test runs.

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

* Propagate front styling to L-corner runs even when re-layout bails

Group style changes rode entirely on syncDerivedCornerRun, which bails
silently when a later-drawn wall blocks the corner layout or a leg run
gained modules outside the derived-run spec — leaving the L legs styled
stale. Apply the style patch directly to every linked corner run before
attempting the geometric re-layout.

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

* Improve editor viewer integration

* Refactor plugin panels and floorplan invalidation

* UV-unwrap and material-assign roof-segment trim planes

* Fix cabinet placement snap feedback

* Add cabinet wall snap bypass

* Fix cabinet continuous placement flow

* Implement wall band material slots

* Add corner addition preview functionality and related tests

* Fix cabinet and wall band behavior

* Fix architecture review blockers

* Fix post-merge quality check

* fix cursor review feedback

* Fix cabinet placement type and quick action history

* Improve cabinet placement feedback

* Make wall bands configurable

* Add wall trim profiles and default materials

* Fix architecture review issues

* Fix cabinet rotation and wall band visibility

* Fix sink appliance paint persistence

* Bake cabinet animations and preserve wall band material

* Fix cabinet group duplication and module snapping

* Fix cabinet duplication architecture

* Fix cabinet floorplan test and make placement-type hint a live chip

- floorplan.test.ts: pass the required liveTransforms map to
  cabinetFloorplanSiblingOverrides (the runtime callers always supply it)
- ToolHint gains an optional generic `chip` contract (subscribe/value/
  cycle/labels) so a kind can render a tool hint as a live mode chip —
  like the snapping/continuation chips — without the editor knowing the
  kind's store
- cabinet: the I hint now shows the current value (Type: Cabinet /
  Type: Island) and is clickable to toggle

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Wassim SAMAD <wass08@gmail.com>
2026-07-12 18:33:48 -04:00
Aymeric RabotandGitHub 7d6879c416 Merge pull request #487 from pascalorg/fix/compass-follow-hidden-align-north
fix(editor): compass needle follows align-north in 3D-only view
2026-07-10 10:22:26 +02:00
Aymeric RabotandClaude Fable 5 cbcc705173 fix(editor): animate compass needle on align-north in 3D-only view
When the 2D panel is hidden, align-north publishes a single '2d' pose that
the camera applies through the echo-suppressed pending-pose path, so no
'3d' poses ever reach the compass subscription and the needle stayed
frozen. Drive the needle with a local rAF exponential decay (same 90ms
time constant as the 2D view animation and the camera's effective
smoothTime); a live '3d' pose cancels it and takes back ownership.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:15:48 +02:00
5e5b62b09e fix(viewer): wall-clock scene-ready cap + skip unreachable dirty nodes (#484)
Two readiness fixes surfaced by the first successful headless bake of a
heavy scene (which exported with 33 items missing):

- The 180-FRAME give-up cap silently assumed display-rate frames. The
  bake page's timer-driven loop (?disable=draw) runs those frames in
  ~3.6s — shorter than a cold item-model download — so the cap fired
  and the export raced ahead of real content. New `sceneReadyMaxWaitMs`
  Viewer prop replaces the frame cap with a wall-clock one on hosts
  whose frame cadence is decoupled from time; default frame behavior
  unchanged for interactive surfaces.

- hasPendingSceneBuildWork now skips UNREACHABLE dirty nodes: orphans
  whose parent doesn't list them in `children` (or parentless non-roots)
  never render — every renderer enumerates the parent's children array —
  so no system ever builds them or clears their mark. Observed in prod
  scene data (two dangling windows parented to a level that doesn't list
  them): they held every bake of that scene to the full readiness cap.

Validated: the 200+-item office scene with ?disable=draw settles
organically in ~2s and exports the complete artifact; a one-shot 504'd
item is now WAITED for through its retry instead of racing the cap.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:01:23 -04:00
aa30899090 fix(viewer): drive the frame loop with a timer when ?disable=draw (#483)
With no real frames submitted, Chromium's no-damage scheduler throttles
requestAnimationFrame to 1Hz on Linux — measured in the headless bake
worker: every useFrame system ticked once per second, so a heavy scene
needed 300+ seconds of wall time just to run ~300 frames of build work
(and exceeded the capture deadline on the worker's slower cores). The
empty-scene clear from #482 avoids this on macOS but not on Linux.

When `draw` is disabled, drive advance() with setInterval instead of
rAF — plain timers are never throttled on a visible page, so the loop
pacing is deterministic (fps prop) regardless of compositor heuristics.
Normal rendering paths are untouched.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:25:09 -04:00
bbe0e2b0b5 feat(viewer): ?disable=draw — skip scene rendering for graph-only consumers (#482)
The headless bake worker renders on SwiftShader (CPU), where per-frame
vertex/draw cost dominates the whole capture — but a bake only reads the
built scene graph; the pixels are thrown away. `?disable=draw` (same
diagnostic-param family as postFx) renders an EMPTY scene each frame
instead of the real one: useFrame systems and scene-ready keep ticking,
per-frame cost collapses to a 64x64 clear.

Rendering nothing at all does NOT work: with zero submitted frames
Chromium's no-damage scheduler throttles rAF to exactly 1Hz (measured),
starving the very systems the bake needs — hence the empty-scene draw.

Measured on the office scene (200+ items) with a real GPU: capture
45.0s -> 8.4s (settle 37.6s -> 4.5s); output equivalent to a normal
render (structural GLB diff clean; the ±13-accessor variance exists
between normal runs too). CPU-only (worker) numbers via the prod-image
container against a preview deploy.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:30:08 -04:00
352954aafc editor: group manipulation — Photoshop-style multi-selection move, rotate, duplicate (#481)
* feat(editor): group R/T — keyboard rotate for multi-selections

Extract the rigid rotate/translate math from the group gizmos into
group-transform-shared (rotateGroupPatches / translateGroupPatches) and
add a keyboard group-rotate path: R/T on a multi-selection steps the
whole group ±45° around its bbox center, welded junctions and
connected-component expansion included, committed as one updateNodes
batch (one undo step). Single-selection R/T arms (reference, door/window
flip, registry keyboardActions) are untouched.

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

* feat(editor): polygon participant kind — slabs/ceilings/zones join group transforms

classifyParticipant learns a 'polygon' kind ([x,z] vertex arrays + optional
hole rings): slab/ceiling/zone now ride along in group move and rotate —
the two 3D gizmos and the keyboard group R/T all flow through the shared
rotateGroupPatches/translateGroupPatches, so the wiring is the classifier
plus the rotate gizmo's spread sampling. Zone's 3D renderer merges its live
polygon override so it previews during the drag (slab/ceiling already
rebuild via getEffectiveNode in their systems). Classifier + patch unit
tests included.

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

* feat(editor): 2D floorplan group drag — Photoshop-style multi-selection move

Plain pointer-down on a transformable member of a multi-selection now
slides the whole selection rigidly in the floor plan (the 2D parity fill
for the 3D GroupMoveHandle), instead of collapsing the selection to the
pressed node. A plain click (no drag) still collapses on release; modified
clicks, Cmd-drag direct move, and reshape affordances keep their paths.
The move-handle dot routes through the same group session when its node
is part of the multi-selection.

The session shares group-transform-shared verbatim: welded LinkedNeighbor
endpoints, connected-component expansion, live previews via
useLiveNodeOverrides.setMany, grid snapping on the delta plus Figma
alignment through applyFloorplanAlignment (guides in every mode except
off, magnetic pull in lines mode), interaction scope handle-drag with the
shared group-move label, and a single batched updateNodes (one undo).
A dashed group bbox overlay shows what rides along and tracks the live
delta. clientToPlan moves to lib/floorplan/plan-coords for reuse.

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

* feat(editor): group manipulation polish — helper text + shortcuts dialog

Multi-selection select-mode HUD now advertises the group gestures (drag
any member to move the whole selection, R/T to step-rotate), and the
keyboard-shortcuts dialog documents both under Selection.

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

* fix(editor): restore zone renderer useLiveNodeOverrides import

Formatter stripped it between the import and usage edits; nodes'
tsc --build catches it where the cached turbo typecheck did not.

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

* feat(editor): tag the 2D group selection box for drivability

data-group-selection-box on the overlay <g> so smoke drivers and devtools
can assert the multi-selection chrome.

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

* feat(editor): multi-selection shows highlight only — hide per-node edit chrome

While 2+ nodes are selected the group is manipulated as one rigid piece,
so per-member edit affordances hide in both views: the 2D overlay pass
strips handle/dimension chrome centrally (stripHandleChrome — body
highlight, hit targets, and zone name text stay), and in 3D the slab /
ceiling boundary editors and slab hole click-to-edit outlines mount only
for a sole selection (the arrow-handle rig, wall side arrows, selection
affordances, and floating menu were already single-gated). Transformable
members show a move cursor in the floor plan.

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

* feat(editor): 3D body-drag group move replaces the move gizmo cross

Pressing any transformable member of a multi-selection in 3D and dragging
past the threshold now slides the whole selection on the ground plane
(group-move-3d.ts, armed from the selection manager's pointer-down choke
point) — same participant snapshot, welded junctions, grid + alignment
snapping, live overrides, and single-undo commit as the 2D session; the
2D dashed bbox rides the same delta in split view. A plain click still
collapses the selection to the pressed node. The purple GroupMoveHandle
cross is deleted, and the hover cursor advertises the gesture: 'move'
over any transformable member in both views (3D selection-manager cursor
+ 2D entry cursor).

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

* feat(editor): group action menu — move / duplicate / delete the whole selection

Multi-selections get a floating Move / Duplicate / Delete pill in BOTH
views (GroupFloatingActionMenu anchored above the 3D group bbox,
FloorplanGroupActionMenu anchored above the 2D dashed group box); the
single-node menus are now sole-selection only. All three actions target
the whole selection via shared group-actions:

- Move starts a group pick-up: the selection rides the cursor
  (delta-relative, grid + alignment snapped) across BOTH surfaces —
  floorplan via the scene CTM, 3D via a ground-plane raycast through a
  three-context bridge published by the selection manager — until a
  click places it as one undo step; Escape / right-click cancels.
- Duplicate clones the selection through the clipboard pipeline
  (subtrees + id remap, without touching the user's clipboard — new
  duplicateNodesToLevel), selects the clones, and picks them up;
  cancelling deletes the clones again. The pick-up scopes to the
  selection (no component expansion / welded links) because the clones
  sit exactly on the originals, and falls back to participant-data
  bounds because the clones' meshes haven't mounted yet.
- Delete removes everything selected with the keyboard arm's
  bulk-confirm semantics.

Verified end-to-end with the scripted smoke (16/16): chrome hiding,
2D/3D/split group drags, R/T round-trip, one-step undo, menu move /
duplicate / delete, 3D move cursor. (Headless runs of the suite can
deadlock in SwiftShader's GL readback on the split-view resize —
environment-only; run headed.)

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

* fix(editor): group transforms no longer re-create auto rooms

Every group commit (2D/3D body drags, pick-up, rotate gizmo, keyboard
R/T) now wraps its single updateNodes in pauseSpaceDetection /
resumeSpaceDetection: the wall-driven room auto-detection rolls its
baseline forward instead of treating rigidly-moved walls as a new room
and duplicating its floors/ceilings. Room creation stays where it
belongs — building and editing walls.

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

* fix(editor): polygon hosts carry their attached items in group transforms

Ceiling-mounted items are children of the ceiling, but polygon hosts
(slab/ceiling) move by rewriting vertices — no group transform — so,
unlike wall children which ride the wall mesh, their children were left
behind by group moves/rotates. collectParticipants now snapshots a
polygon participant's positioned children (level-frame coords, since the
host group sits at the origin) so they ride every group path. Unit test
included.

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

* feat(editor): click picks up the group; the 2D dashed box is the drag handle

Clicking a selected member of a multi-selection (no drag) now enters the
group pick-up — the same click-to-move the sole-selection has — instead
of collapsing the selection, in 2D and 3D alike; clicking outside still
deselects. The 2D dashed selection box is itself the group's handle:
move cursor across its whole area, press-drag anywhere inside slides the
group, a plain click picks it up, and holding Cmd/Ctrl/Shift lets clicks
pass through to the entries so membership toggling keeps working. HUD
hint + shortcuts dialog updated.

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

* feat(editor): 3D dashed group selection box doubles as the drag handle

The multi-selection now shows a dashed wireframe box in 3D (sibling of
the 2D dashed rect): move cursor across its whole volume, press-drag
anywhere on it slides the group, a plain click picks it up, and holding
a selection modifier passes the press through so members inside can
still be toggled. Rides the shared live-drag delta so it tracks the
group mid-gesture in both panes.

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

* feat(editor): mid-move R/T, 2D corner rotate, realtime opening symbols, box cursor + Esc hint

- R/T while carrying a group (body drag or pick-up) now rotates the
  carried snapshots around the rest pivot — identical to the idle
  keyboard rotate — instead of leaking into the store mid-session; the
  session keydown owns the keys in capture phase and the group-move HUD
  advertises them.
- The 2D dashed box grows corner rotate handles: drag a corner to spin
  the group in 15° steps, Shift for free rotation — the floor-plan
  sibling of the 3D rotate gizmo, sharing its scope, HUD hints, and
  single-undo + space-detection-paused commit.
- Door / window floor-plan symbols now track wall drags in realtime:
  their defs merge the walls' live overrides (wallFloorplanSiblingOverrides)
  so ctx.parent is the effective wall instead of the stale store one.
- The 3D selection box asserts the move cursor across its whole volume
  on pointer move (the old ''-guard lost to app default cursors).
- Multi-select HUD + shortcuts dialog mention Esc / click-outside to
  clear, and the mid-move rotate.

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

* feat(editor): rotate cursor + spinning box on 2D corner rotate; Delete ends a carry

- The 2D corner handles show a proper curved-arrow rotate cursor (inline
  SVG data URI, black glyph with white halo, grab fallback).
- The dashed selection box spins live with the group during a corner
  rotate: the rotate session publishes pivot+angle through the shared
  drag store and the box applies the SVG rotate transform.
- Delete / Backspace during any group session (drag, rotate, pick-up)
  reverts the session first and then lets the global Delete arm remove
  the selection — no more dangling carry after deleting mid-move; the
  duplicate flow's cancel still discards the clones instead.

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

* fix(editor): per-node direct manipulation stands down for multi-selections

Cmd is both the selection-toggle key and the single-node direct-move /
direct-rotate trigger, so a Cmd+click that wobbled past the drag
threshold over a selected member (slab or any kind) armed the legacy
single-node move and yanked that one member out of the group. All four
per-node direct-manipulation entry points — the 3D Cmd-drag move and
Cmd+right-drag rotate in the selection manager, and their 2D siblings in
the registry layer — now require the pressed node to be the SOLE
selection; with a multi-selection the gesture is simply not registered
(the group sessions own plain drags, Cmd+click keeps toggling). The
shortcuts dialog notes the single-selection scope.

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

* fix(editor): group gestures own their ending pointerup — no synthesized click leak

use-node-events synthesizes a selection click on EVERY canvas pointerup
(deliberately more forgiving than R3F's onClick). Body drags were safe
only because inputDragging suppresses it, but the pick-up flows leaked
it as a race: the release that started a pick-up could collapse the
multi-selection to the pressed node first (then only that node was
carried), and the placement press re-selected whatever sat under the
cursor after commit.

The group sessions now claim their gesture-ending pointer events in the
CAPTURE phase and stop propagation, so the canvas never sees them and no
click is synthesized: the pick-up claims the placement press on
pointerdown and commits on its pointerup; the 3D and 2D drag/rotate
sessions stop their ending pointerup (split view: a 2D-started release
over the 3D canvas had the same hole).

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

* fix(editor): screen-rect marquee tests projected oriented bounds, not double-inflated AABBs

The 3D screen-rect box select intersected the marquee with each node's
world AABB re-boxed in screen space — two nested axis-aligned inflations.
Any oblique camera padded every node's claim, and rotated geometry (now
common: group rotation) made it flagrant — a diagonal wall's world AABB
spans a full square, so marquees selected objects visually far from the
cursor.

Membership now tests the marquee against the convex hull of the node's
ORIENTED (local-frame) bounding box projected to screen — tight under any
rotation and camera, uniform for every kind. Pure helpers
(convexHull2D / rectIntersectsHull) live in marquee-geometry.ts with unit
tests covering the diagonal-wall regression, containment both ways, and
edge-only crossings. The plane marquee variant was already precise and is
untouched.

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

* fix(editor): keep box select alive after group gestures

The capture-phase stopPropagation the sessions used to silence the
canvas's synthesized selection click also killed the window bubble
listeners that clear the box-select pointer suppression — and the mouse
pointerId is constant, so one group gesture left the marquee dead until
blur. The sessions now use the designed suppression instead: they hold
inputDragging raised through the release event's dispatch (lowered on a
0ms timer) so use-node-events suppresses the click itself, and every
other pointerup listener runs normally.

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

* fix(editor): marquee membership by plan footprint; 2D item highlight; site line steps back

- The 3D screen-rect marquee now projects itself onto the active level's
  floor plane and tests the DATA kinds exactly there — walls/fences by
  their segment, slabs/ceilings/zones by their polygon — matching the
  plane-marquee tool's semantics. This kills the two remaining
  imprecision sources the hull pass couldn't: vertex-baked rotation
  (polygon/fence geometry lives in level coords, so after a group
  rotation even the local bbox is an inflated axis-aligned square) and
  ceiling parallax (an elevated plane projects to a screen quad offset
  from its room, selecting 'from far'). Mesh-transform kinds (items,
  columns) keep the projected oriented-bbox hull test; plan-space
  segment/polygon intersection helpers join marquee-geometry.ts with
  unit tests.
- Selected items finally read as selected in the floor plan: palette
  stroke + heavier weight on the footprint, plus a selection ring drawn
  above the thumbnail (the move dot used to be the only cue, and it
  hides in multi-selections).
- The site's dashed property line drops to 20% opacity while a multi or
  in-flight marquee selection exists so it stops fighting the dashed
  group selection box.

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

* fix(editor): 2D marquee plan-footprint membership; item marquee preview; overlays re-measure after undo

- The 2D screen marquee intersected each entry's getBoundingClientRect —
  an axis-aligned DOM box, inflated for rotated polygons and diagonal
  walls (the same double-inflation class as the 3D bug). It now maps the
  marquee through the scene CTM into a plan quad (rotated views included)
  and tests the data kinds exactly — walls/fences by segment,
  slab/ceiling/zone by polygon — reusing the shared marquee-geometry
  helpers; other kinds keep the DOM-rect fallback.
- Items now show the marquee preview tint in 2D (highlighted viewState =
  the same palette stroke + thumbnail ring the selection shows, slightly
  lighter), instead of giving zero feedback until the drop.
- Everything that measures the selection's meshes (2D/3D dashed boxes,
  the 3D group menu anchor, the rotate gizmo pivot) re-measures once the
  meshes settle after a scene change via useMeshSettleEpoch — an undo
  after a move no longer leaves the box at the pre-undo position
  (computeGroupBox reads mesh world bounds, which lag the store commit by
  a frame or two).

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

* fix(editor): dim the actual site boundary line during multi-selections

The earlier dimming targeted registry-layer site entries, but the site
lives at the scene root — outside the level DFS and the building-scoped
sweep — so no entry ever matched and the dashed property line kept full
opacity next to the dashed group selection box. Move the dimming to
FloorplanSiteLayer (the component that actually draws the line) and
drop the dead registry plumbing.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:10:35 -04:00
feeabf4bd3 fix(items): retry failed model loads, settle skipped items, keep exports clean (#480)
* fix(items): retry failed model loads, settle skipped items, keep exports clean

A transient storage failure (observed: Supabase 504s under the bake page's
~200-concurrent-request burst) permanently broke an item for the whole
session: drei's useGLTF caches the rejected load by URL, the per-item
ErrorBoundary swallowed it, and the red debug-box fallback was BAKED into
the exported GLB (observed in a prod artifact). Meanwhile ItemSystem
cleared the dirty mark at group registration — before the model resolved —
so scene-ready could fire while GLBs were still loading, risking exported
placeholder geometry.

- ModelWithRetry: bounded retries (1s/3s) that clear the useGLTF cache
  entry and re-mount via the boundary's new resetKey. The timer is owned
  by an effect keyed on the failure count, so StrictMode's synthetic
  unmount/remount re-arms it instead of silently discarding it (the
  naive onError-scheduled timer died exactly that way in dev).
- Exhausted retries settle the item as SKIPPED: the debug box renders
  nothing during exports, and the failure lands in
  useViewer.itemLoadFailures (nodeId -> url) so a bake host can persist
  which items are missing from the artifact.
- ItemSystem holds the dirty mark until the item settles (model mounted,
  terminally failed, or never expected) — scene-ready now genuinely waits
  for item content; loading placeholders also hide during exports.
- ErrorBoundary: onError + resetKey props.

Verified against a 200+-item prod scene locally: permanent 504 -> exactly
3 fetch attempts, bake completes without the item and without debug
boxes; 504-once -> retry heals, artifact byte-equivalent to the intact
run; no-failure runs unchanged (demo_1 byte-identical).

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

* fix(items): reset retry budget + settled flag when the asset URL changes

Bugbot: after a terminal load failure, swapping the item's model kept the
stale failures/epoch state and the settled flag — the new URL never even
attempted to load. ModelWithRetry is now keyed by asset src (clean retry
budget per URL) and un-settles the item on mount so the replacement load
is awaited by ItemSystem/scene-ready too.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:50:20 -04:00
Aymeric RabotandGitHub 5831b7b234 Merge pull request #479 from pascalorg/feat/smart-unit-suffix
editor: smart unit suffix — dim on focus, yield to explicit units
2026-07-09 16:20:44 +02:00
Aymeric RabotandGitHub 2d4e7be709 Merge pull request #478 from pascalorg/fix/compass-sync
editor: compass sync + fast align-to-north
2026-07-09 16:20:40 +02:00
Aymeric RabotandClaude Fable 5 5b4b267a40 feat(editor): smart unit suffix — dim on focus, yield to explicit units
While editing a numeric field, the unit suffix now visually responds to
the draft content:
- Plain number / empty: suffix dims to 40% opacity (signals "default unit
  if you commit a bare number")
- Explicit unit typed (hint showing): suffix fades to opacity-0 over
  150ms (the conversion hint provides unit feedback instead)
- Unparseable text: suffix stays dimmed (field will revert on commit)
- At rest: unchanged full-opacity suffix

Uses opacity-0 rather than unmounting so the flex layout doesn't jump.
The predicate is simply `hint !== null` — it aligns exactly with
measurementHint's internal PLAIN_NUMBER guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:07:46 +02:00
58339c64f5 fix(plugin-trees): consume dirty marks so scene-ready fires for plant scenes (#476)
Instanced plant kinds (trees/grass/flowers) never cleared their dirty
marks: FloorElevationSystem deliberately leaves the mark for kinds with
a def.system, expecting that system to clear it after its own work, but
InstancedKindSystem never participated in the dirty protocol. The marks
lived forever, hasPendingSceneBuildWork() never went false, and the
Viewer's scene-ready signal stalled at SCENE_READY_MAX_WAIT_FRAMES on
every plant-containing scene — measured on the headless bake worker as
~190s (180 frames x ~1s SwiftShader frames) of pure cap-wait per bake.

Clear the marks in a priority-2 useFrame pass: after the priority-1
floor-elevation lift in the same frame, and only for nodes whose proxy
is registered (instances rebuild synchronously from the store, so a
rendered node is already built). Validated by baking Wawa House
locally: settle 22.5s -> 11.6s (the remainder is genuine asset
loading), exported GLB byte-identical with and without the fix.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 07:39:36 -04:00
Aymeric RabotandClaude Fable 5 d4f1053528 fix(editor): compass sync review fixes
Route all pending-pose cancellations through clearPendingFloorplanNavigationPose()
so savedSmoothTime is always restored. Add a controlstart listener to catch
right-click orbit and other pointer drags that weren't handled. Guard the
rotation-degree ref mirror to prevent React re-renders from clobbering the
imperative 3D-owned value when the panel is hidden, and add a useLayoutEffect
to keep the needle DOM in sync. Make the catch-up effect re-run directly on
panel reopen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:23:09 +02:00
Aymeric RabotandClaude Fable 5 cb5cfc7e40 fix(editor): compass sync + fast align-to-north
- Remove CSS transition on compass needle SVG so rotation tracks
  exactly without rubber-band lag (was transition-transform 150ms)
- Drive compass needle imperatively via ref when floorplan panel is
  hidden (3D-only mode), avoiding per-frame React re-renders of the
  full SVG while keeping the needle live
- Align-to-north in 3D-only mode derives from latestNavigationSyncPose
  instead of stale viewport, publishes directly to the 3D camera
- Temporarily lower camera-controls smoothTime from 0.25s to 0.18s
  during 2D-origin transitions (SmoothDamp τ≈90ms matches the 2D
  exponential decay), restoring on completion or cancellation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:09:31 +02:00
Aymeric RabotandGitHub a477c4ade3 Merge pull request #477 from pascalorg/fix/unit-labels-lingo-0.2
fix(editor): area readouts + generated names follow the unit toggle; bump lingo to 0.2.0
2026-07-08 22:51:49 +02:00
Aymeric RabotandClaude Opus 4.8 8476a991f4 chore(deps): bump @pascal-app/lingo to 0.2.0
0.2.0 is API-compatible with our usage (parseQuantity, quantity, Kind);
it adds i18n/locale entrypoints and does not change the symbols we import.
Verified: @pascal-app/editor and @pascal-app/mcp typecheck, and all 31
measurement tests pass, including the AMBIGUOUS_NUMBER escalate guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:34:31 +02:00
Aymeric RabotandClaude Opus 4.8 682c023be1 fix(editor): area readouts and generated names follow the unit toggle
The metric/imperial preference was already honored by every length input,
but two area surfaces were still hardcoded to m²:

- the "Load build" import dialog's Floor area stat
- the auto-generated default names for zones, slabs, and ceilings
  (e.g. "Zone (12.3m²)"), which are live display fallbacks

Both now convert value + label to the active unit. Adds shared
squareMetersToAreaUnit / getAreaUnitLabel / formatAreaLabel helpers to
lib/measurements (with tests) and routes the site-panel Area readout
through them, replacing an inline 10.7639 magic constant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:34:30 +02:00
fb0b78c091 fix(editor): floorplan reference pass — north convention, cursors, R/T, Set Scale UX (#475)
Fix pass over 2D reference-image (guide) handling plus two adjacent repairs:

- North is now world −Z (FLOORPLAN_VIEW_ROTATION_DEG 90 → 0, mirrored in
  world-grid-snap + mcp README; floorplan-panel de-dupes its local copy).
  A rotation-0 north-up scan reads upright when the compass says aligned,
  and "align north" maps to camera azimuth 0 instead of a 90° jump.
- Guide resize cursor: custom double-arrow cursor aimed at the dragged
  corner in screen space — accounts for image aspect (was atan2(±1,±1),
  square-only), the guide's own rotation, and the floorplan view rotation
  the overlay group renders inside (was ignored entirely).
- R/T rotate a selected reference (guide/scan) in ±45° steps; references
  live in selectedReferenceId, not the viewer selection, so both arms get
  the reference-first branch the Delete arm already had. Locked guides skip.
- 2D-only mode hides the Top View button (drives the display:none 3D
  camera); orbit stays — it spins the synced floorplan view.
- Set Scale UX: locked guides stay clickable (calibration auto-lock made
  them unselectable until reload); starting the flow from 3D switches to
  2D; the length input pre-fills the drawn length in the pre-selected unit
  (imperial pre-filled meters labeled feet); Set Scale flips into Cancel
  while the flow runs (mirrored via referenceScaleActiveGuideId); Hide/
  Clear Scale only render once calibrated; Escape cancels (global arm +
  dialog); Clear Scale and Replace Image drop the calibration auto-lock;
  discoverability: corner-hint row + panel nudge for uncalibrated guides.
- healSceneNodes: strip child refs whose child's parentId names another
  parent (stale reparent leftovers rendered a window twice — duplicate
  React keys in 2D, doubled hosted geometry in 3D) and same-array dupes.
- Editor accepts onLoaderChange so hosts can measure open-to-interactive
  time (community wires it to a PostHog timing event).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:12:49 -04:00
Aymeric RabotandGitHub 2cfce3f6b5 Merge pull request #472 from pascalorg/feat/lingo-natural-inputs
feat: natural-language measurement inputs (UI + MCP tools) via @pascal-app/lingo
2026-07-08 17:08:43 +02:00
Aymeric RabotandClaude Opus 4.8 5ffde5f66a fix(editor): site-panel property-line coords + area/perimeter follow the unit toggle
The property-line vertex X/Z inputs and the Area/Perimeter readout were
hardcoded to meters. Convert them to feet / ft² when the viewer preference is
imperial, matching every other length input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:00:47 +02:00
Aymeric RabotandClaude Opus 4.8 69813a6e62 feat(editor): honor the metric/imperial toggle in every length input
SliderControl (which drives the auto-inspector's ~144 fields and most custom
panels) previously ignored the viewer unit preference and always rendered raw
meters with a static "m" label, while MetricControl converted to feet — so
toggling imperial changed some length inputs and not others. Extract the
conversion into a shared `useLinearDisplay(unit, precision)` hook and route
BOTH controls through it, so every `unit="m"` length field displays and edits
in feet when imperial and meters when metric — consistently.

Values are always stored in meters; only display, the text field, and drag/
wheel/arrow deltas move to the display unit. For metric and non-length units
(°, %, in, …) the conversions are the identity, so those paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:00:47 +02:00
717c2c5c0a fix: bug pass — legacy window crash, plant exports, zone snapping/visibility, HDR (#473)
* fix(core): apply window schema defaults on scene load

Windows saved before a schema field existed (columnRatios/rowRatios/
frameThickness/…) loaded with those fields missing; the window mesh
builder reads them unconditionally and threw every frame, crashing the
viewer on legacy scenes. Zod-parse windows on load like doors so the
schema defaults land.

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

* fix(editor): mount plant geometry during client GLB/STL/OBJ export

The client export never set isExporting, so instanced kinds (trees/
flowers/grass) kept their colorWrite:false raycast collider mounted and
exported it as an opaque white box, while the real geometry (which only
mounts while exporting) was never captured. Reuse BakeExporter's
flag + frame-wait dance in ExportManager, and harden isRenderableMesh
to drop colorWrite:false materials from exports entirely.

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

* fix(editor): route zone drawing through the shared surface snap

The zone tool quantized later vertices by distance along a free ray
instead of snapping to the grid, and never joined the magnetic
wall-corner/midpoint/crossing + alignment-guide pipeline that walls,
slabs and ceilings use. Give zone the slab treatment in the 3D tool and
both 2D floorplan branches (move + click + double-click commit).

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

* fix(viewer): self-host the scene environment HDR

drei's preset="sunset" fetches venice_sunset_1k.hdr from
raw.githack.com, which intermittently fails ("Could not load
venice_sunset_1k.hdr: Failed to fetch"). Point Environment at
/hdri/venice_sunset_1k.hdr and ship the file in the app's public/ —
same mirroring convention as /audios/sfx; consuming apps must carry
the file too.

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

* feat(editor): unmount zones entirely outside the zones layer

Zone labels showed in every editing mode (visual noise), and each zone
kept a drei <Html> mounted at opacity 0 — an <Html> costs per-frame
matrix work and live DOM even when invisible. A new viewer presentation
flag (showZones, default true) lets the editor unmount zone meshes and
labels whenever the structure layer isn't 'zones' (and during snapshot
capture); the registered group stays so zones keep their scene identity
for selection and GLB export. Preview / first-person / viewer surfaces
are untouched. Raycast-disable moved from a one-shot group flag to
per-frame on the meshes, which now remount on layer toggles.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:19:27 -04:00
Aymeric RabotandClaude Opus 4.8 af1fe6a34c chore(deps): add @pascal-app/lingo to the lockfile; note duplicated helper
Records @pascal-app/lingo@^0.1.0 in bun.lock (added to @pascal-app/editor and
@pascal-app/mcp), so `bun install --frozen-lockfile` in CI resolves it. Also
documents that measurement.ts is intentionally duplicated with the AI-chat copy
across the submodule boundary and must be kept in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:32:36 +02:00
Aymeric RabotandClaude Opus 4.8 3273aac551 fix(mcp): restore >0 validation and reject ambiguous numbers in measurement()
Adversarial-review follow-ups to the lingo measurement() field:
- Add a `positive` option (strict > 0) and use it for the non-zero dimension
  params. Swapping z.number().positive() → measurement(..,{min:0}) had started
  admitting 0 (the core node schemas have no positivity backstop), so a zero-size
  wall/opening/roof could be created. Inclusive-0 fields (overhang, sill height,
  knee-wall height, opening offset, roof pitch) keep min:0.
- Escalate AMBIGUOUS_NUMBER to error so "1,234" fails instead of silently reading
  as 1234 — a 1000x hazard for European decimals. Matches lingo's own /ai fields.
- roofLevelElevation reverted to z.number() (it's a level ordinal, not meters);
  radians fields now advertise radian-appropriate examples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:09:59 +02:00
Aymeric RabotandClaude Opus 4.8 f66dcd438b feat(mcp): natural-language measurements for tool inputs via @pascal-app/lingo
Measurement/angle parameters on the scene tools now accept a bare number OR a
natural-language string ("6 in", "180cm", "45°", "1.57rad"), canonicalized to
the unit the handler already expects (meters for length, radians/degrees for
angles) with min/max bounds and model-readable errors. Makes LLM tool calls
safer — "6 in" no longer has to be pre-converted to 0.1524, and a bad value is
rejected with a message the model can self-correct from.

A shared `measurement(kind, unit)` zod field wraps lingo's parseQuantity as a
`number | string` union+transform; numbers pass through unchanged (backward
compatible), so no handler changes were needed. Covers create_wall, cut_opening,
place_item, create_level, create_story_shell, create_roof,
create_stair_between_levels, create_room, add_door, add_window, photo_to_scene.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:51:08 +02:00
Aymeric RabotandClaude Opus 4.8 a5963560b1 feat(editor): natural-language measurement inputs via @pascal-app/lingo
Type any unit into SliderControl / MetricControl fields — "6ft", "180cm",
"1m80", "5'11\"", "72in", "45°", "1.57rad" — and it canonicalizes to the
unit the field already stores (meters / degrees / radians / inches),
independent of the metric/imperial display toggle. A live "= 1.83 m" hint
previews the parsed value while typing.

Only the typed-text commit path changes: drag-scrub, wheel, and arrow-key
editing are untouched, and any unit the adapter doesn't recognise (%, unitless,
rad/s) falls back to Number.parseFloat, so nothing regresses. The shared
lib/measurement-parser.ts maps each field's `unit` prop to a lingo kind +
canonical unit, covering the auto-inspector and every custom panel at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:04:14 +02:00
51fddc2d9c fix: capture-mode polish for studio & presets + pausable render loop (#469)
* fix(editor): keep snapshot pitch out of preset thumbnail capture

The capture overlay's snapshot hint and 'Take snapshot' shutter label
rendered in every capture variant, clashing with the save-as-preset
flow's own 'Frame your item — click Capture' banner. Gate them on the
existing isPreset discriminator and label the shutter 'Capture' there.

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

* feat(viewer): renderPaused flag suspends the frame loop

Hosts that fully cover the canvas (e.g. the studio gallery overlay) can
set renderPaused to stop FrameLimiter's RAF loop — a heavy animated
scene otherwise keeps starving the GPU behind the overlay and makes it
stutter.

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

* fix(editor): drop the crop/format HUD in preset capture

The pills crowded the save-as-preset banner and repeat what the fixed
square frame already shows.

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

* fix(editor): no contextual shortcut hints in the studio workspace

Holding Shift surfaced select-mode/snapping hints over the studio
compose panel and gallery, where there is no scene selection or tool to
act on.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:10:53 -04:00
f6a58489b8 fix(viewer): survive block-misaligned KTX2 textures on WebGPU (#467)
WebGPU rejects block-compressed textures whose base dimensions aren't
multiples of 4; three's KTX2Loader only warns and transcodes anyway, so a
baked GLB carrying one odd-sized user-item texture (e.g. 299x399) poisoned
every render pass — endless 'Invalid BindGroup … Invalid CommandBuffer'
spam. Route such payloads through a fallback transcode to uncompressed
RGBA32 and repackage the pixels as a plain DataTexture (three's WebGPU
upload path crashes on a CompressedTexture holding rgba8 data).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:39:52 -04:00
f6ffb93a39 fix(export): unbreak bakes and STL/OBJ exports on malformed/placeholder meshes (#466)
- roof-system: replace BoxGeometry placeholders (initial swap + empty-roof
  merged shell) with a group-less degenerate geometry — a Box's 6 groups
  against the roof's 4-material array crashed GLTFExporter on every scene
  with a segment-less roof node (prod: 'reading isShaderMaterial'), and
  count-0 groups crash MeshBVH's packed-tree build.
- glb-export: sanitizeMaterialGroups pass repairs any mesh whose geometry
  groups don't line up with its material array before GLTFExporter runs.
- export-manager: give neutralised (attribute-less) meshes an empty position
  attribute before STL/OBJ export — both exporters read position.count
  unconditionally.
- bake-exporter: log the full error stack so the bake worker's console relay
  captures it in the job's error trail.
- viewer: host-controlled disablePostFx prop (Viewer → PostProcessing) that
  skips building the SSGI/denoise pipeline entirely; the ?disable=postFx URL
  flag previously only bypassed it per-frame while still allocating it.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:07:08 -04:00
9b5e229770 feat(editor): stage overlay slot + pro capture overlay for studio (#465)
* feat(editor): stage overlay slot, preselectable snapshot crop, pro capture overlay

- Editor/EditorLayoutV2 gain a stageOverlay slot rendered over the canvas
  but under the viewer toolbar (z-10 < z-20), so hosts can swap in a
  full-stage surface (e.g. studio gallery) without unmounting the WebGL
  canvas; FloatingLevelSelector hides while a stage overlay is active
- CaptureMode 'standard' accepts an optional crop preselection
  (SnapshotCropMode, exported) so hosts can enter capture with
  standard/viewport/area already chosen
- SnapshotCaptureOverlay restyle: letterboxed 16:9 frame with corner
  accents + rule-of-thirds for standard mode, thirds grid in viewport
  and area modes, top-center crop/format HUD chips, shutter button with
  capture/saved states — all existing crop, drag, resize and preset
  behaviors preserved

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

* feat(editor): standard-capture aspect presets, capture scrim, caption fix

- Standard crop gains aspect presets (16:9, 9:16, 4:3, 3:4, 1:1) — clicking
  the active Standard chip opens the picker; the letterbox frame, HUD and
  output resolution follow the choice
- ThumbnailGenerateEvent carries standardSize; ThumbnailGenerator
  center-crops to it (default stays 1920×1080)
- Subtle bottom scrim keeps shutter/caption readable on bright scenes
- Fix missing space in the snapshot caption

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

* feat(editor): standard-capture aspect preselect via CaptureMode

Hosts (the studio capbar) can now pass standardAspect alongside crop when
entering capture mode; the overlay initializes its aspect picker from it.
SnapshotStandardAspect exported.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:09:56 -04:00
22af31dea8 editor: snapping overhaul — shared-wall rooms, deterministic slab elevation, cursor-true transfers (#458)
* fix(snapping): show alignment lines in all modes, snap only in lines; north export; R/T hint; grid default

- Alignment guides display in every snapping mode (grid/lines/angles/off);
  magnetic pull applies only in 'lines'. New isAlignmentGuideActive() predicate
  decouples guide display from the snap delta across all placement/move/draw
  producers (item, wall, fence, slab/ceiling/roof, column/shelf/spawn, door/
  window, MEP, 2D floorplan drafting, surface snap).
- Floorplan PDF export now rotates to north-up (FLOORPLAN_VIEW_ROTATION_DEG -
  building rotation), matching the on-screen aligned-to-north view.
- Item placement rotate hint collapsed to a single 'R / T  Rotate' row.
- Default item snapping mode changed lines -> grid.

* feat(walls): detect rooms across shared walls + tight connect-snap in all modes

Room detection: planarize the wall graph before face-finding — split straight
walls at T-junctions where another wall ends mid-span, so a room closed against
the middle of an existing wall is detected, not just isolated 4-wall rooms.

Auto-close: wire the wall builder's "Room (auto-close)" to the same room graph
via wallClosesRoom(), so drafting stops when a segment seals a room against the
existing structure — not only when the chain returns to its own start.

Connectivity snap: add a tight wall-connect snap (WALL_CONNECT_SNAP_RADIUS 0.05)
that also runs in grid/off/angles — within range of a wall (body or corner,
uniform radius) the endpoint sticks onto it and the beacon shows, so rooms close
in every mode. Lines keeps its wider magnetic radii. Gate alignment guides in
non-magnetic modes to the same connect distance so a corner dot no longer
magnetises the cursor from far.

Tests: cover T-junction detection + wallClosesRoom; update the item-default and
wall-split tests for the mode-driven behaviour.

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

* feat(surfaces): gate alignment guides to connect distance in non-magnetic modes

Port the wall drafting/endpoint gating to the shared slab/ceiling/roof snap
(resolveSurfacePlanPointSnap): in grid/off/angles, only anchors within the
connect distance are fed to the alignment resolver, so guides form to nearby
points and far corner dots stop lighting up from across the plan. Filtering the
candidates (local-frame, like the cursor) rather than the resolved guides avoids
the floor-plan view rotation baked into the guide coords. Lines mode keeps the
full-range guides; the surface wall connect-snap is unaffected.

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

* fix(slabs): deterministic wall-slab overlap via clipped overlap length

Wall elevation used ray-cast point samples that landed exactly on the
slab boundary for perpendicular walls, so whether a butting wall
followed the slab's elevation depended on which side of the slab it
touched. Clip the wall centerline and face lines against the polygon
and require >=5cm of on/inside length instead: walls along the slab
edge follow it on every side, point contact never does.

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

* fix(grid): lift lattice 1mm off slab tops; wall-align on wall-item move start

The horizontal snap lattice sat exactly at the followed surface Y, so it
z-fought elevated slab tops while moving items. The visual mesh now rides
1mm above; the grid event plane keeps the true height.

Moving a wall-hosted item showed a horizontal grid until the first
pointer move published a wall surface — the mesh fallback assumed UP.
Derive the host wall normal from the item mesh's world orientation
(local +Z faces out of the wall) so the lattice is wall-aligned from
the first frame.

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

* feat(editor): unify selection shortcuts + truthful select-mode hints

Shift+click now toggles selection membership in 3D like Cmd/Ctrl (and
appends on box-select release), matching the 2D floorplan. The HUD rows
describe what actually works in both views: move is plain left-drag (the
grip/dot), the two vague selection rows collapse into one Cmd/Shift
or-group, and the modifier-held variants (freely / with guides / bypass
snaps) are gone — guides follow the snapping mode now. Key pills join
with + for combos and / for alternatives; Shift renders as the shift
icon.

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

* fix(editor): group gizmos — reliable rotate grabs, mutual hide, snapping-mode integration

The group-rotate gizmo never received pointer events through the shared
invisible hit-area path (EDITOR_LAYER + custom raycast) in its portalled
context; its handlers now live on the visible arrow plus a plain
default-layer invisible torus, so hover and drag work with a fat target.

Group drags begin a handle-drag scope: each gizmo hides while its
sibling drags (the frozen corner goes stale), idle hints leave the HUD,
and the drag gets contextual hints — Shift free-rotation for rotate, the
snapping chips for move.

Group move joins the snapping-mode system via the 'item' context: Shift
cycles the mode, Ctrl the grid step (both read live mid-drag), and
'lines' runs the same Figma-style alignment as single-node moves against
the group's bbox anchors. Chip clicks now tick like the keyboard cycles.

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

* fix(editor): group-move ticks in every snapping mode

Mirror the single-node move's sfx: emit per delta change rather than
only on grid crossings, so lines/off get the same rate-limited texture.

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

* fix(items): keep host transfers glued to the cursor

Moving between hosts used to diverge from the pointer and mangle state:

- Grab offsets: every surface anchor (wall / ceiling / shelf / item
  surface / floor) preserved the grab offset by re-seeding from the
  item's carried-over position on each new host, landing it far from
  the cursor. The grab offset now survives only on the original host
  and only until the item anchors anywhere else — after that every
  host (the original included) centers the item under the cursor.
  Applied uniformly to the placement coordinator and the window/door
  move tools.

- Rotation: detaching from a rotated shelf/table back to the floor kept
  the HOST-local yaw as the level yaw, visibly spinning the item. The
  detach now re-expresses the item's world yaw in the level frame.

- Elevation: the same detach wrote the level parentId to the store but
  not the draft ref, so the floor-elevation resolver bailed on its
  parent-must-be-a-level guard — the snap grid and the item stopped
  following slab elevations for the rest of the drag.

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

* fix(items): latch grab-offset forgetting across every host kind

Review follow-up (PR #458): unify the per-surface grab-forget rule into
one grabForgotten latch. A wall/ceiling item could not actually reach a
shelf or the floor (item-surface enter rejects attachTo assets), but a
wall item CAN anchor on a roof face — and returning to its original
wall then restored the stale grab offset. Roof-wall transitions (and
floor landings after a host visit) now trip the latch in the
coordinator and the window/door move tools alike.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:18:37 -04:00