Commit Graph
909 Commits
Author SHA1 Message Date
Aymeric Rabot d0afde192e Merge remote-tracking branch 'origin/main' into feat/realtime-collaboration-sfx 2026-07-24 00:28:45 +02:00
7cfb88dcad viewer: snapshot capture pipeline + hero framing for thumbnails (#538)
* feat(viewer,editor): extract snapshot pipeline, add hero framing + BakeThumbnail

Move the offscreen SSGI capture pipeline out of ThumbnailGenerator into
viewer's snapshot-pipeline (adding the missing scene-referred GRADE, uniform-
driven), add hero-pose per-node-box corner framing helpers, and a BakeThumbnail
component so the community bake page can render a publish-quality hero shot
headlessly. Auto-save thumbnails now re-pose onto the same hero angle instead
of copying the user's mid-edit camera; user-driven captures keep the exact
viewport pose.

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

* feat(viewer): tune hero framing — 19° elevation, tighter fit

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

* feat(viewer,editor): ink edges in snapshot captures

Captures now run the viewport's AO -> ink -> grade order with the same
soft/strong edge derivation (edgeColorFor/edgeOpacityScaleFor), uniform-gated
so one cached pipeline serves all modes. Radius scales with render height so
supersampled captures keep the viewport's line weight. Preset/item
(transparent) captures stay ink-free.

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

* feat(viewer): lower hero elevation to 13°, exact fit — match reference framing

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

* feat(viewer): hero framing — site plate in frame, building-centered aim, facade-relative 45°

Three framing fixes from review: the intentionally-shaped site plate joins the
fit constraints (from scene data — the site Object3D carries the ±400m horizon
disc and can't be measured), the aim centers the building (plate+structure on
XZ, structure alone on Y) so outlying boxes take asymmetric margin instead of
shifting the subject, and the azimuth sits 45° to the dominant wall axis
(length-weighted fold-4 vector sum) so rotated plans still read as a proper
corner shot.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:43:00 -04:00
f6d28f6794 Fix pass: walkthrough, deck-coupled wall items, live-preview elevation, room snap modes, import validation (#537)
* fix(core): elect wall slab support from the carrying profile, not face coverage

An elevated deck drawn against a house wall covers the wall's outer face
line end-to-end (boundary contact counts), so the max-across-polylines
election handed the wall origin to the deck: every wall-hosted window/door
rode along whenever the deck height changed, and placement local-Y was
clamped above the deck top. Elect from the carrying profile instead (per
arc segment: highest support per face, min across supported faces), with
the pointer cap applied inside the profile so a capped-away deck still
falls back to the floor that carries the wall. Also pass curveOffset/
thickness/supportSlabId at the window/door tool query sites so their
cursor agrees with the rendered wall frame.

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

* fix(core): honor live node overrides in spatial-grid support queries

Group drags publish translated slab polygons and wall endpoints to
useLiveNodeOverrides only; the committed spatial index made floor items
and walls re-elect support against the pre-drag slab footprint, so
multi-selection moves and room-preset placement jumped vertically until
the validating click committed the batch. Support queries now read
live-effective slab/wall records and bypass the rendered-polygon cache
while a slab or wall on the level has an override; the committed cached
path stays the fast path otherwise.

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

* feat(editor): give the room-preset stamp tool a polygon snap context

The host app's room stamp drives placement with tool='room', which has no
registry entry, so snapContextOf resolved null: Shift never cycled the
snapping mode and the HUD chip stayed hidden during room preset placement.
A tool-level context map hands the stamp the no-angle polygon set.

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

* fix(editor): only synthesize walkthrough fallback floors for ground levels

Every slab-less visible level got a >=30x30m opening-free fallback floor
box at its elevation, so a walkthrough spawned on an upper level (the
no-spawn-node fallback ray picks the highest surface) stood on a phantom
plane it could never descend from. Match the baked-GLB viewer policy:
only the lowest level of each building (derived baseY === 0) gets the
fallback; upper levels rely on their real slabs, whose stair openings
are cut into the geometry.

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

* fix(core): fold useLiveTransforms slab deltas into support queries

The slab move tool and the room-preset stamp publish a translation DELTA
to useLiveTransforms (no polygon override), so the spatial index still
elected support against the slab's committed footprint: furniture riding
a room-preset preview (or sitting on a dragged deck) dropped to ground
under the visually-moved deck until the validating click. Effective slab
records now apply the live delta to polygon/holes/elevation, mapped
exactly once at each query's loop entry.

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

* fix(viewer): reapply floor lift every frame for nodes with live previews

FloorElevationSystem only wrote mesh Y for dirty nodes, but the React
commit that rebinds a dragged node's base-Y group position can land
between frames, after priority-2 systems consumed the dirty mark — the
lift then vanished until the next pointer tick re-dirtied the node,
blinking the Y of items dragged over elevated slabs. Nodes holding a
live override or transform now get the lift reapplied every frame.

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

* fix(editor): stop the entry camera swap from insta-cancelling walkthrough

Entering walkthrough with a persisted orthographic camera swaps it to
perspective, which recreates the interaction callbacks and re-ran the
pointer-lock effect: its cleanup called exitPointerLock, and the unlock
handler read that as "user left walkthrough" — instantly cancelling the
fresh entry and arming the browser's ~1.25s re-lock cooldown (hence
needing multiple button presses). The effect is now mount-stable (the
changing callback rides a ref, deps down to [gl]), and the entry lock
request swallows async cooldown rejections like the P-resume path.

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

* fix(core): don't block build-JSON import over plugin-node children

Exports from projects with plugins carry nodes like trees:tree whose ids
sit in level.children; the import validator parsed parents against the
static children id union, so one tree id hard-failed the level schema and
blocked the whole import — while the same data loads fine from the DB
(setScene never runs this gate). Parents are now validated against a copy
with non-static-schema child ids filtered out; those nodes keep surfacing
through the unknown-types warning and the imported payload is unchanged.

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

* feat(core): treat registered plugin kinds as first-class in import validation

Nodes of runtime-registered plugin kinds (trees:tree, trees:grass) were
lumped into the unknown-types warning even when the plugin is loaded.
The validator now consults the node registry: registered kinds validate
against their own registered schema (corrupt plugin nodes still block),
count under stats.pluginTypes, and raise no warning — only genuinely
unregistered types do, which stays correct for hosts without the plugin.

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

* style: appease biome (format + forEach block body)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:31:09 -04:00
Aymeric Rabot 8b8ef23a59 Merge remote-tracking branch 'origin/main' into feat/realtime-collaboration-sfx
# Conflicts:
#	packages/core/src/schema/nodes/level.test.ts
#	packages/core/src/schema/nodes/level.ts
#	packages/core/src/store/use-scene.ts
2026-07-23 17:43:41 +02:00
Aymeric Rabot 8715d481ba test(core): avoid duplicate compiled test runs 2026-07-23 17:38:47 +02:00
c3659acdf8 fix(editor): don't flag camera as dragging on ACTION.NONE controlstart (#536)
camera-controls fires controlstart for every pointerdown — including
buttons mapped to ACTION.NONE (plain left click in edit mode). Since
#535 that set cameraDragging=true with no rest/sleep ever following to
clear it, so every canvas click (selection, wall placement) was
suppressed once the camera was at rest.

Only flag dragging when currentAction actually drives the camera, and
clear the flag on controlend for mapped-button taps with zero movement
(no wake -> no rest/sleep).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 09:58:28 -04:00
Aymeric Rabot c42b3f01c9 fix(core): preserve remote patch interaction state 2026-07-23 14:14:24 +02:00
fcac55ca30 editor: improve floorplan navigation performance (#535)
* 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(editor): defer floorplan annotation collision layout

* fix(viewer): suspend outlines during camera movement

* fix(editor): keep floorplan zoom off the render hot path

* fix(editor): keep floorplan pan off render hot path

* fix(editor): keep floorplan rotation off render hot path

* fix(editor): keep rotation backdrop white

* fix(editor): finalize floorplan navigation safely

* fix(editor): keep navigation interaction state current

* fix(editor): stop floorplan animation before pan

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-23 07:53:01 -04:00
Aymeric Rabot 4b27966f9a fix(core): preserve plugin create fields 2026-07-23 12:32:48 +02:00
Aymeric Rabot 08d1bbf017 fix(core): preserve level child id types 2026-07-23 12:22:03 +02:00
Aymeric Rabot 3126b0b430 fix(core): allow plugin nodes under levels 2026-07-23 12:16:02 +02:00
Aymeric Rabot 4be0e55836 fix(core): apply exact remote scene patches 2026-07-23 12:00:39 +02:00
Wassim SAMADandGitHub 26c9e17fed feat(nodes): draft axis guides at the moving endpoint + 2d parity (#534)
Wall/fence drafting already drew an X/Z axis cross at the segment start.
Now the moving endpoint gets a single long guide line perpendicular to the
draft segment (a second cross would collide with the start cross on
axis-aligned segments), fences gain the same guides they lacked entirely,
and the 2D floor plan renders the equivalent SVG guides (cross at start,
perpendicular line at end) fed by the floorplan draft preview store.

The guide components are extracted from wall/tool.tsx into
nodes/shared/draft-axis-guides.tsx so both tools (and the fence's formerly
duplicated arc/label helpers) share one implementation.
2026-07-22 14:51:34 -04:00
77442861d9 editor: complete floorplan construction documentation (#531)
* Add roof surface placement support for items

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

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

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

* fixed conflict

* feat(floorplan): add construction dimension strings

* feat(floorplan): coordinate opening dimensions

* feat(floorplan): add opening documentation

* feat(floorplan): add construction dimensions and notes

* feat(floorplan): add interior dimensions and curved note leaders

* feat(floorplan): improve construction dimensions and document plan

* feat(floorplan): harden construction document output

* feat(floorplan): add annotation collision diagnostics

* feat(floorplan): size export annotations in paper space

* feat(floorplan): automatically separate overlapping labels

* fix(floorplan): resolve dense label overlaps

* fix(floorplan): remove stale collision warning overlays

* fix(floorplan): treat mark pills as collision obstacles

* feat(floorplan): place short dimension values outside

* fix(floorplan): preserve dimension string order

* fix(floorplan): avoid architectural geometry in label layout

* feat(floorplan): add dimension side fallback leaders

* docs(floorplan): update chapter 17 implementation status

* fix(floorplan): dimension subdivided interior walls

* feat: add associative floor plan dimensions

* feat: add continuous construction dimension strings

* feat: add structural floor plan grids

* feat: coordinate columns with structural grids

Snap column placement and movement to structural axes and intersections, derive associative grid references, and preserve floor-plan rotation by allowing secondary-button pointer moves through the grid drafting layer.

* feat: add architectural room documentation

Add room-role metadata, editable documentation fields, centered room labels, and persisted live/PDF visibility while preserving generic zone behavior.

* feat: generate architectural room schedules

Add registry-driven room schedule rows with unit-aware areas and heights, natural room ordering, enclosure resolution, and document-quality warnings.

* feat: add reliable room clear dimensions

Derive unit-aware clear dimensions from proven modeled inside wall faces for straight rectangular rooms, including rotated and split-wall enclosures, while suppressing unproven datums.

* feat: add architectural stair documentation

Add level-aware UP/DN graphics, derived flight and rail notes, plan break and overhead conventions, linked destination-level projection, and persisted live/PDF visibility.

* feat: add typed specialty construction notes

Add schema-validated specialty payloads, standardized plan notation, contract-scope metadata, configurable overhead outlines, and editor authoring controls.

* feat: add curved and circular dimensions

Add associative radius, diameter, center, chord, arc-length, angular, and coordinate modes with unit-aware notation, repeated-feature labels, 2D authoring, and document controls.

* feat: coordinate floor plan drawing types

Add persistent floor, foundation, reflected-ceiling, roof, and site plan views with per-dimension show, omit, reference, and foundation-controller behavior across live and PDF output.

* feat: add associative curved wall dimensions

Bind radius, center, chord, arc-length, and angular construction dimensions directly to curved wall geometry so annotations update when the host curve changes.

* fix: render automatic curved wall dimensions

The wall floor-plan builder explicitly skipped curved walls, leaving the associative authoring workflow as the only dimension path. Render a concentric arc-length dimension automatically and keep it governed by automatic-dimension visibility.

* fix: use radius callout for curved walls

Replace the automatic arc-length annotation with the source-standard radius method: computed center mark, radial leader, curve arrow, and R value. Keep adjacent linear strings responsible for locating the curve tangencies and depth.

* Implement construction dimension string editing

* Add construction dimension standards controls

* Apply drawing standards to automatic dimensions

* Add floorplan overhead and reference visibility controls

* Add view-specific dimension segment suppression

* Add persistent drawing sheet model

* Plot floorplan exports at fixed scale

* Apply paper-space annotation profiles

* Compose floorplan PDF sheets

* Support sheet paper sizes and preflight

* Persist pinned annotation layout overrides

* Expand annotation collision obstacles

* Add floorplan annotation preflight surface

* Add reusable drawing sheet general notes

* Add drawing sheet keyed note instances

* Add drawing sheet document markers

* Expand construction note leader terminators

* Add wall assembly layer model

* Resolve wall assembly datum references

* Add wall assembly floorplan graphics

* Add opening documentation dimension policies

* Add finish-face room clear dimensions

* Extend room clear dimensions to rectilinear rooms

* Add construction module advisories

* Add clearance advisory profiles

* Add dimension completeness audit

* Expand dimension completeness audit

* Include preflight issues in completeness audit

* feat: complete floorplan construction documentation

* refactor: remove construction note node

* feat: refine floorplan documentation and unit display

* fix(editor): improve floorplan PDF dimensions

* fix(floorplan): refresh annotation collision layout

* fix(floorplan): keep annotations clear and restore registry boundaries

* feat(floorplan): refine construction dimension references

* fix(floorplan): align documentation tools with architecture

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-22 14:02:17 -04:00
2adb50a340 editor: stop wall endpoint drag snapping back to the stale junction corner (#533)
* fix(wall): stop endpoint drag snapping back to the stale junction corner

Walls attached to the moving corner cascade with the drag, but the snap
pipeline reads the scene store, which keeps their pre-drag coordinates
until commit. Their stale corners recreated the old junction as a snap /
alignment target, so inside the connect radius (5cm, 70cm magnetic) the
endpoint could never land closer than that to where it started — sub-5cm
corrections (e.g. squaring a scan-imported 91° junction) were impossible
in every snapping mode.

Both endpoint-move paths (3D tool + 2D floorplan affordance) now exclude
the walls linked at the moving corner from snap candidates and alignment
anchors while attached; Alt-detach keeps them (they stay put, so they're
live geometry). The 2D path previously over-excluded: walls linked at the
FIXED corner don't move, and their anchors are exactly what lets the
dragged corner align back onto a true axis.

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

* fix(wall): re-run full snap pipeline on Alt toggle during endpoint drag

The snap/alignment candidate set now depends on Alt (stale-junction
exclusion), so a keyboard detach/re-attach must re-resolve from the raw
cursor point instead of re-applying the previously snapped one — matching
what the 2D dispatcher already does by re-invoking apply() with the raw
planPoint on modifier changes. Flagged by Bugbot on #533.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 09:46:06 -04:00
0fba611a05 feat(export): texture references — stamp sources at load, reference mode in GLB export (#532)
Bake v2 exporter side: library/preset, legacy scene-material, and item-GLB
textures get a validated pascalTextureRef stamped at load (origin-checked,
env-derived). Material-ish sources resolve to 'library-material' (storage
bucket) or 'app-material' (static catalog on the assets CDN) by URL shape.
exportSceneToGlb grows a textures: 'embed' | 'reference' option — reference
mode swaps stamped textures for 1x1 canvas-backed placeholders (skipping the
WebGPU blit) and writes the ref to both texture and image extras via a
GLTFExporter writeTexture plugin. Download GLB stays embed; the bake page
passes reference.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 08:33:38 -04:00
cb6fadbc28 editor: unify walkthrough + viewer UI, crouch, screenshot pause (#529)
* feat(walkthrough): unify first-person and baked walkthrough UI into a shared HUD

The builder first-person overlay (crosshair, Exit Street View button, hints
card) and the baked-GLB walkthrough HUD were two divergent UIs. Extract the
GLB-style HUD (reticle, floor/room labels, Esc pill, interact prompt) into a
shared WalkthroughHud in packages/editor, feed it from FirstPersonControls via
a small useFirstPersonHud store (interact target each frame, floor/zone labels
sampled from the camera), and align FOV/projection handling with the baked
controller. The now-unused WalkthroughControls glide controller is removed
from packages/viewer (WALKTHROUGH_FOV moves to the GLB controller module).

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

* feat(viewer-ui): shared ViewerControlsBar + ViewerSceneHeader for preview and embedders

Preview mode's ViewerOverlay was an older copy of the community viewer UI
(separate scan/guide/camera buttons, own render/theme/edges menus, 4-state
wall mode). Extract the community design into shared prop-driven components:
ViewerControlsBar (visibility, level/wall modes, display menu, walkthrough,
orbit/top view) and ViewerSceneHeader (back, project info, optional stats
slot, breadcrumb, levels card). ViewerOverlay is now a thin composition of
them. The display menu gains the edges submenu everywhere; the vestigial
translucent wall mode is dropped (a stale value renders as cutaway).

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

* feat(walkthrough): hold-Ctrl crouch + P screenshot pause in both controllers

Crouch swaps the capsule for a short one (shrinks around the centre, so a
mid-jump crouch lowers the head and raises the feet — enough to thread window
openings), lowers the eye with a short lerp, and slows movement; standing back
up is gated on headroom via an upward raycast against the collider world.
Tuning constants live in the GLB controller module and are shared with the
editor first-person controller.

P releases the pointer lock without leaving the walkthrough so the cursor is
free for an OS screenshot (macOS region capture needs a movable pointer);
clicking the canvas re-locks and resumes.

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

* fix(walkthrough): crouched profile fits ~1 m openings

The float gap counts toward the effective obstacle height — the capsule rides
floatHeight (0.5 m) above the ground, so the old crouch spanned 0.5–1.3 m and
a 1.14 m opening still blocked it. Crouching now also lowers the float gap
(0.25 m) and uses a shorter capsule (0.7 m), for an effective 0.25–0.95 m
span; the stand-up headroom check grows to match.

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

* feat(walkthrough): seamless screenshot pause — auto-release pointer lock on ⌘

Replace the P shortcut: macOS swallows the full ⇧⌘4 but the ⌘-down keystroke
still reaches the page, so the moment ⌘ (or PrintScreen) goes down while
locked the cursor is released without leaving the walkthrough — the native
screenshot flow just works, no user education. The HUD pill flips to "Click
to resume" (click-through, so the resuming click lands on the canvas) via a
new walkthroughSuspended flag on the viewer store, reset on lock/exit/unmount.

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

* fix(walkthrough): screenshot pause is P again, advertised in the HUD

The ⌘ auto-release fired on every command combo and felt broken. Back to an
explicit P toggle, now discoverable: the HUD bottom shows a "P free cursor"
pill next to "Esc to exit", and while paused it flips to "Click or P to
resume · Esc to exit" (click-through so the resuming click lands on the
canvas). P re-locks as well as releasing.

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

* fix(walkthrough): freeze crouch during cursor pause; no editor hints in first person

While the P pause is active, Ctrl no longer toggles crouch — ⌃⇧⌘4 (clipboard
screenshot) was crouching the player mid-capture; the held state stays frozen
until resume. HelperManager now renders nothing in first-person mode, so the
Ctrl multi-select hint no longer pops over the walkthrough HUD (Ctrl is
crouch there).

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

* chore: biome formatting + pre-existing useOptionalChain fix in wall panel

The wall-panel lint error predates this branch (#526); fixed here to unblock
the quality gate.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:04:00 -04:00
adc1ec89f4 fix(viewer): guard TextureNode updates against null textures (#527)
* fix(viewer): guard TextureNode updates against null textures

three's override-material passes (shadow, prepasses) copy per-object texture
slots onto shared materials whose cached per-mesh node graphs can disagree
about a slot's presence; when a graph with a TextureNode pulls a null slot,
the exception kills the whole render pass and the scene goes black. Patch
TextureNode.prototype.update to skip null values (with a throttled warning)
so the slot just renders textureless for a frame and recovers.

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

* revert(viewer): drop the always-on neutral displacement texture

Giving every standard material a displacement texture put displacement
TextureNodes into the shared shadow/prepass override graphs; in scenes mixing
preset materials with GLB item materials (no displacementMap), an item's
shadow render can hit a cached graph that expects the texture and pull null
— build-order dependent, which is why it broke many-but-not-all projects at
open right after the release. The TextureNode fallback guard covers the
original paint-hover crash (black texel = zero displacement), so the broad
neutral-texture approach is retired.

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

* style: sort guard imports

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:34:07 -04:00
603f5d2242 feat(materials): dynamic library registry, picker source tabs, Other category (#525)
* feat(materials): dynamic library registry, picker source tabs, create-material entry point

Core gains a runtime material registry (registerLibraryMaterials/
unregisterLibraryMaterials/subscribeLibraryMaterials) so embedders can
feed user/community materials; library: refs to registered materials
resolve in the viewer unchanged. MaterialCatalogItem carries an optional
source (pascal|community|mine|workspace). MaterialPicker gets a source
filter row and an optional onCreateMaterialRequest '+ New material'
tile, threaded through MaterialPaintPanel.

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

* fix(materials): underline source tabs in picker, matching catalog browse surfaces

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

* feat(materials): add 'other' category for uncategorized library materials

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

* fix(viewer): rebuild node material when a texture slot is cleared

Reused cached WebGPU materials keep a compiled TextureNode per slot; nulling
the slot for a cold texture load (or a preset without the map) without
needsUpdate leaves the node's per-frame material reference pulling null,
crashing the render pass in TextureNode.update. Cold loads are the norm for
freshly generated library materials, whose maps aren't in the texture cache.

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

* fix(viewer): standard materials always carry a displacement texture

three's WebGPU shadow pass copies each object's displacementMap onto one
shared per-light shadow material while caching per-mesh shadow node graphs
against that shared override. A mesh whose shadow graph was built with a
displacement TextureNode (painted with a generated material — the only
presets carrying height maps) crashes the render pass with "null (reading
'matrix')" the moment a material without a displacement texture is swapped
onto it, which is exactly what the paint hover preview does. Give every
MeshStandardNodeMaterial a shared 1x1 black displacement texel (zero offset)
when it has no real height map, so the copied slot is never null in either
direction.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:00:48 -04:00
bf89b5bcf2 feat: vertical building model — stored level heights, wall inversion, decks (#526)
* feat(core): stored storey heights groundwork — pure slab-support module + level height schema

Extract pointInPolygon/computeWallSlabSupport and friends into a cycle-free
packages/core/src/systems/slab/slab-support.ts (severs level-height ->
spatial-grid-manager -> use-scene), add deriveLegacyLevelHeight as the pure
mesh-free equivalent of the viewer's stacked level height, and add the
optional LevelNode.height field plus the storey service (getStoredLevelHeight,
getLevelElevations per-building prefix sums). No behavior change.

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

* feat(editor): storey height badge + edit popover on level rows

Each floating-level-selector row shows its storey height; clicking opens a
popover with 2.5/3.0/3.5 presets and a free slider writing level.height.

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

* feat(core): vertical-model load migration — stored heights, ordinal compaction, wall-top classification

Pass 3 in migrateNodes: derive and store each legacy level's exact stacked
height (never snapped), compact ordinals per building anchored at zero so
basements stay basements, classify wall tops against the derived plane
(|plane - top| < 0.20 strictly -> plane-bound, else explicit height
materialized), and drop the blind totalRise 2.5 stair default on legacy
scenes only. Epsilon and strictness validated by a prod census.

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

* feat: pin wall tops to the storey plane; stored heights become the only vertical truth

Wall-top inversion: a wall without an explicit height now tops out at its
storey plane (resolveWallTop); slabs lift only the base. Window/door caps
resolve the real top through the same slab election instead of Infinity.
All level stacking (viewer, elevator, first-person, stair openings, MCP
scene queries) reads stored LevelNode.height; the four divergent live
derivations and level.metadata.height are deleted. Stair totalRise becomes
optional and derives from the storey height when absent. MCP create-level
stops writing its elevation param into the ordinal. Level creation sites
write explicit heights; templates carry their true derived heights.

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

* feat: clamp slabs/ceilings under the storey plane; sweep wall-height fallbacks; wall Top control

Slab elevation writes clamp to plane − MIN_WALL_HEIGHT when plane-bound
walls elect the slab (pure clampSlabElevationForWalls + registry handle
bounds + shared panels for 2D/3D parity); ceiling heights clamp under the
plane and auto-ceilings derive from resolved wall tops. Every remaining
wall.height ?? 2.5 fallback resolves through resolveWallTop /
resolveWallEffectiveHeight (panels, overlays, measurements, quantities,
spatial grid, MCP reports); template walls matching their storey become
plane-bound. Wall panel gains a Top control (Follows storey / Custom
height) derived purely from height presence; the store update path now
deletes keys passed as explicit undefined so plane-binding round-trips.

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

* feat(core): persisted support hosts — schema, host-preferring election, rendered-polygon unification

Floor-placed nodes and walls gain a nullable supportSlabId. Elections
surface the winning slab (getSlabSupportForItem, candidates query) and
prefer a still-valid persisted host, falling back silently when the host
is gone or reshaped away; deleting the host strips references in the same
undo commit. Item-side support now tests the rendered slab polygon (like
walls) through a per-level cache invalidated by the spatial-grid sync.
Also adds the resolveStairTotalRise unit tests from the stage-1 gates.

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

* feat: persist support hosts at commit; thread wall host preference everywhere

Floor-placed commits (draft pipeline, per-kind creation tools, registry
move tool) and wall create/move/endpoint commits persist supportSlabId
via shared resolveSupportSlabPatch helpers — only when overlapping
supports disagree on elevation, clearing it otherwise or when the node
leaves the floor. WallSlabSupport surfaces electedSlabId; every wall
support read site passes wall.supportSlabId as the preferred host.

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

* feat: split slab into placement + thickness; pools become explicit recess intent

slab.elevation stays the walking surface; new thickness grows downward so
the solid occupies [elevation − thickness, elevation]. Migration writes
thickness := elevation for solids (byte-identical intervals, including
degenerate zero) and recessed: true for legacy negative pools. Geometry
branches on recessed instead of the elevation sign; presets keep today's
intervals; free elevation edits move the body without coupling thickness
(the deck semantic). Dead viewer SlabSystem component deleted.

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

* feat: clamp ceilings to covering-slab undersides across levels

getLevelAbove + getCoveringSlabUndersideAt give the first cross-level
query; ceiling writes clamp to min(storey plane, lowest covering
underside) − 0.01, and the space-detection reconcile now clamps manual
ceilings down (never up) when a deck above intrudes — a flush deck
reactively lowers the ceiling below it.

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

* feat: mezzanine and balcony build-tab tools

One-gesture composites over the kernel: draw a deck footprint and commit
deck slab + railings + stair (mezzanine) or deck + railings (balcony) in
a single undo step. Fences gain supportSlabId and lift onto their host
deck; railing runs split around the stair mouth; edges near wall
centerlines are treated as closed. Stairs target the deck via explicit
totalRise with no level-to-level opening sync.

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

* feat: adaptive slab vertical editing; level vocabulary in UI copy

Dragging a grounded slab's top stretches it (elevation and thickness move
together — gaps impossible); floating decks move with thickness preserved
and land grounded at zero; pools keep the drag-through-zero gesture.
User-facing copy says level, not storey (Follows level, Level height).

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

* feat: clamp plane-bound wall tops to covering-slab undersides; fix vertical reactivity

getWallPlaneTop samples the wall span against the level above's slabs, so
a thick or flush upper floor shortens the walls below it instead of
colliding (automatic attach, no dialog). Level-height edits now dirty the
level's walls, stairs, ceilings, and fences; covering-slab changes dirty
the level below. The effective-height helper triplicated across editor
overlays moved to core.

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

* feat: ceilings follow the level top by default

ceiling.height becomes optional — absent means the ceiling tracks
min(level top, covering slab underside) − 0.01 live, so level-height
edits no longer require ceiling fixes. Ceiling panel gains the same
Follows level / Custom height control as walls; auto-from-walls ceilings
are created height-less and their height-derivation machinery is deleted;
migration drops stored heights within 0.20 of the bound on legacy scenes.

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

* fix: wall plane clamp missed max-side boundary walls

Auto slabs derive their polygon from wall centerlines, so covering-clamp
samples sat exactly on the boundary where ray-cast point-in-polygon is
side-dependent (min edges in, max edges out) — walls clamped or not by
orientation. getWallPlaneTop now clips the wall's thickness band against
the covering polygon (boundary-inclusive, arc-aware) and the ceiling
bound's point sampling gained an explicit on-boundary test. Verified
against the reported repro scene.

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

* feat: stairs attach to decks; pointer-decided placement surface

Stairs gain deckSlabId — rise follows the deck's elevation live (straight
flights re-converge via a write-sync mirroring auto-openings), the panel
shows a unified To destination with Follows deck / Custom rise, and the
mezzanine tool attaches instead of baking a stale rise. Item placement
under an elevated deck no longer flickers: grid events fed a feedback
loop (the grid plane rode the ghost's elected height), so the support
election is now capped at the surface the pointer ray actually hits,
with a ground sentinel keeping under-deck commits deterministic.

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

* chore: remove the mezzanine/balcony composite tools

Decks ship as catalog presets instead; the kernel the presets rely on
(fence deck-hosting, stair deck attachment, clone remaps, pointer-decided
placement) all stays. The tool code lives at e30042db for reference.

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

* feat: gate wall adoption to grounded slabs; panel moves, drag stretches then unsticks

Floating decks keep their drawn polygon (and stop being seam candidates
for grounded neighbors) instead of growing into nearby walls. Panel
elevation edits are pure placement; the viewport drag stretches a
grounded slab up to 0.4 m then unsticks it into a thin deck.

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

* fix: deck-attached stairs land flush with the deck surface

The rise now subtracts the stair's own elected base (same election the
visual lift uses), so base + rise always equals the deck walking surface;
the auto-sync defers a microtask so it reads a settled spatial grid and
re-converges on both deck and base-slab moves.

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

* feat: draw walls and fences on elevated decks

Wall and fence draw tools now publish the pointed surface, so the draw
plane rides the deck top (no more perspective-skewed floor hits) and
previews sit on the deck. Fences gain real support election: a pure
resolveFenceSupportSlabPatch persists the deck host at draw and reshape
commits; wall commits thread the pointer cap so aiming under a deck
elects the floor.

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

* feat: unit-aware height presets

Level, ceiling, and slab preset buttons show clean values per display
system (8/9/10 ft storeys, 8'-9' ceilings, whole-inch slab steps) instead
of converted metric labels; metric presets unchanged.

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

* feat: stairs converge to their resolved rise; deck attachment disables the cutout

syncStairRises now converges every follows-mode straight stair (level or
deck) plus deck-attached custom rises — detaching a stair from a deck
re-derives its height, and ordinary stairs finally track level-height
changes. Attaching via the panel writes slabOpeningMode none and hides
the cutout controls; detaching restores the destination cutout and
clears the stale explicit rise.

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

* fix: stacked-slab move hopping — one ray, one surface, one XZ

The hop was hysteresis: the election consumed the riding grid plane's
perspective-skewed hit, giving two self-consistent fixed points for one
pointer ray. getPointedSupportSurface now returns the ray's crossing of
the pointed surface and both the support cap and the cursor XZ derive
from that single computation, so items stay on the surface the pointer
aims at and sit exactly under the cursor across storeys.

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

* fix: single stair per click; tools restore select mode on exit

The stair tool subscribed to both node clicks (synthesized on pointerup)
and the native-click grid event with none of the guards sibling tools
carry — one physical click over any node surface committed twice. A
commit gate + follow-up click swallow fix the double dispatch, and the
stair and column tools now restore select mode on exit instead of
leaving the dead build-mode-without-tool state that ignored every click.

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

* feat: compact multi-selection panel with host footer slot

Selecting multiple nodes now docks the collapsed-by-default panel on the
right: N selected header, kind breakdown, and Duplicate/Delete mirroring
the floating pill. A new multiSelectionFooter slot lets the host app dock
actions below it, exactly like inspectorFooter.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 09:57:30 -04:00
Aymeric Rabot e1a4dba740 feat: improve collaboration feedback and previews 2026-07-20 20:25:30 +02:00
Wassim SAMADandGitHub 10c9c6ad27 fix: harden item and baked scene rendering (#522) 2026-07-20 12:15:08 -04:00
Aymeric RabotandGitHub 1d43a39f9a docs(viewer): document npm registry bootstrap (#521) 2026-07-19 18:43:50 +02:00
Aymeric RabotandGitHub 494150dc1f fix(viewer): stack levels per building (#519)
* fix(viewer): stack levels per building

* fix(viewer): recover legacy level ownership
2026-07-19 18:29:00 +02:00
87f3952e4d fix(viewer): harden WebGPU slab recovery (#518)
Co-authored-by: Connor Soohoo <connorsoohoo@users.noreply.github.com>
2026-07-19 17:58:53 +02:00
Aymeric RabotandGitHub a7734b8ffe test(nodes): isolate cabinet regression suites (#517) 2026-07-19 17:54:38 +02:00
263b4ab6d0 fix(mcp): keep store receiver bound in scene event operations (#489)
* fix(mcp): keep store receiver bound in scene event operations

appendSceneEvent and listSceneEvents detached the store methods before
invoking them (const append = this.requireStore().appendSceneEvent;
append(options)), so `this` was undefined inside store implementations
that rely on it — SqliteSceneStore.withWriteTransaction in particular.
Every save_scene, create_from_template, and live-sync mutation routed
through the facade failed with "undefined is not an object (evaluating
'this.withWriteTransaction')" even though the SQLite write succeeded.

Invoke the methods on the store instance instead, and add facade-level
regression tests covering both the bound-receiver path and the
stores-without-scene-events fallback.

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

* test(mcp): close scene store after facade tests

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
2026-07-19 17:49:41 +02:00
e484e17f82 fix(viewer): kick render on resume (#516)
Co-authored-by: mvanhorn <mvanhorn@users.noreply.github.com>
2026-07-19 17:46:17 +02:00
f67787077e fix(core): guard material-ref parsers against non-string values (Sentry MONOREPO-EDITOR-EM) (#486)
* fix(core): guard material-ref parsers against non-string values (Sentry MONOREPO-EDITOR-EM)

getLibraryMaterialIdFromRef and getSceneMaterialIdFromRef only guarded
against null/undefined, then called .startsWith(). When a non-string
material ref reaches them (legacy/malformed wall material slot ref),
.startsWith is undefined -> TypeError: e.startsWith is not a function.

Narrow with typeof !== 'string' -> return null, so a bad ref degrades to
'no library/scene material' instead of throwing during wall material
resolution (packages/viewer wall-materials.ts -> parseMaterialRef).

* test(core): cover malformed material refs

---------

Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
2026-07-19 17:41:54 +02:00
Aymeric RabotandGitHub 5c7f58f295 Merge pull request #515 from pascalorg/fix/preload-tool-inspector
Preload the selected tool inspector
2026-07-19 17:40:32 +02:00
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
Aymeric Rabot 2129eed0c2 fix(editor): preload selected tool inspector 2026-07-19 17:35:45 +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 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
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