c6a70579ccfd4c28d8756f3e0c4334c2d5f00df6
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
daa1f3e99b | feat: add material browse categories (#546) | ||
|
|
f0985df887 | fix(core): preserve legacy site children during scene healing (#544) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
de13119f86 |
docs(wiki): vertical-model architecture page; drop stale SlabSystem rows (#528)
Documents the shipped vertical building model (#526): stored truth table, resolution helpers, clamp rules, pointer-decided placement, the load migration that lives in migrateNodes indefinitely, and the gotchas. The systems tables stop listing the deleted SlabSystem and point at the registry geometry path instead. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
10c9c6ad27 | fix: harden item and baked scene rendering (#522) | ||
|
|
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> |
||
|
|
9ca3eaa7fe | feat: add project plugin management (#501) | ||
|
|
4f0aa7f7b6 |
fix(core): prevent unbounded slab corner miters (#500)
* fix(core): bound slab corner miters * style(core): format slab miter fix |
||
|
|
78223d61cf | fix(core): exclude dangling walls from room polygons (#499) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
5e5b62b09e |
fix(viewer): wall-clock scene-ready cap + skip unreachable dirty nodes (#484)
Two readiness fixes surfaced by the first successful headless bake of a heavy scene (which exported with 33 items missing): - The 180-FRAME give-up cap silently assumed display-rate frames. The bake page's timer-driven loop (?disable=draw) runs those frames in ~3.6s — shorter than a cold item-model download — so the cap fired and the export raced ahead of real content. New `sceneReadyMaxWaitMs` Viewer prop replaces the frame cap with a wall-clock one on hosts whose frame cadence is decoupled from time; default frame behavior unchanged for interactive surfaces. - hasPendingSceneBuildWork now skips UNREACHABLE dirty nodes: orphans whose parent doesn't list them in `children` (or parentless non-roots) never render — every renderer enumerates the parent's children array — so no system ever builds them or clears their mark. Observed in prod scene data (two dangling windows parented to a level that doesn't list them): they held every bake of that scene to the full readiness cap. Validated: the 200+-item office scene with ?disable=draw settles organically in ~2s and exports the complete artifact; a one-shot 504'd item is now WAITED for through its retry instead of racing the cap. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aa30899090 |
fix(viewer): drive the frame loop with a timer when ?disable=draw (#483)
With no real frames submitted, Chromium's no-damage scheduler throttles requestAnimationFrame to 1Hz on Linux — measured in the headless bake worker: every useFrame system ticked once per second, so a heavy scene needed 300+ seconds of wall time just to run ~300 frames of build work (and exceeded the capture deadline on the worker's slower cores). The empty-scene clear from #482 avoids this on macOS but not on Linux. When `draw` is disabled, drive advance() with setInterval instead of rAF — plain timers are never throttled on a visible page, so the loop pacing is deterministic (fps prop) regardless of compositor heuristics. Normal rendering paths are untouched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bbe0e2b0b5 |
feat(viewer): ?disable=draw — skip scene rendering for graph-only consumers (#482)
The headless bake worker renders on SwiftShader (CPU), where per-frame vertex/draw cost dominates the whole capture — but a bake only reads the built scene graph; the pixels are thrown away. `?disable=draw` (same diagnostic-param family as postFx) renders an EMPTY scene each frame instead of the real one: useFrame systems and scene-ready keep ticking, per-frame cost collapses to a 64x64 clear. Rendering nothing at all does NOT work: with zero submitted frames Chromium's no-damage scheduler throttles rAF to exactly 1Hz (measured), starving the very systems the bake needs — hence the empty-scene draw. Measured on the office scene (200+ items) with a real GPU: capture 45.0s -> 8.4s (settle 37.6s -> 4.5s); output equivalent to a normal render (structural GLB diff clean; the ±13-accessor variance exists between normal runs too). CPU-only (worker) numbers via the prod-image container against a preview deploy. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
352954aafc |
editor: group manipulation — Photoshop-style multi-selection move, rotate, duplicate (#481)
* feat(editor): group R/T — keyboard rotate for multi-selections Extract the rigid rotate/translate math from the group gizmos into group-transform-shared (rotateGroupPatches / translateGroupPatches) and add a keyboard group-rotate path: R/T on a multi-selection steps the whole group ±45° around its bbox center, welded junctions and connected-component expansion included, committed as one updateNodes batch (one undo step). Single-selection R/T arms (reference, door/window flip, registry keyboardActions) are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): polygon participant kind — slabs/ceilings/zones join group transforms classifyParticipant learns a 'polygon' kind ([x,z] vertex arrays + optional hole rings): slab/ceiling/zone now ride along in group move and rotate — the two 3D gizmos and the keyboard group R/T all flow through the shared rotateGroupPatches/translateGroupPatches, so the wiring is the classifier plus the rotate gizmo's spread sampling. Zone's 3D renderer merges its live polygon override so it previews during the drag (slab/ceiling already rebuild via getEffectiveNode in their systems). Classifier + patch unit tests included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): 2D floorplan group drag — Photoshop-style multi-selection move Plain pointer-down on a transformable member of a multi-selection now slides the whole selection rigidly in the floor plan (the 2D parity fill for the 3D GroupMoveHandle), instead of collapsing the selection to the pressed node. A plain click (no drag) still collapses on release; modified clicks, Cmd-drag direct move, and reshape affordances keep their paths. The move-handle dot routes through the same group session when its node is part of the multi-selection. The session shares group-transform-shared verbatim: welded LinkedNeighbor endpoints, connected-component expansion, live previews via useLiveNodeOverrides.setMany, grid snapping on the delta plus Figma alignment through applyFloorplanAlignment (guides in every mode except off, magnetic pull in lines mode), interaction scope handle-drag with the shared group-move label, and a single batched updateNodes (one undo). A dashed group bbox overlay shows what rides along and tracks the live delta. clientToPlan moves to lib/floorplan/plan-coords for reuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): group manipulation polish — helper text + shortcuts dialog Multi-selection select-mode HUD now advertises the group gestures (drag any member to move the whole selection, R/T to step-rotate), and the keyboard-shortcuts dialog documents both under Selection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): restore zone renderer useLiveNodeOverrides import Formatter stripped it between the import and usage edits; nodes' tsc --build catches it where the cached turbo typecheck did not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): tag the 2D group selection box for drivability data-group-selection-box on the overlay <g> so smoke drivers and devtools can assert the multi-selection chrome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): multi-selection shows highlight only — hide per-node edit chrome While 2+ nodes are selected the group is manipulated as one rigid piece, so per-member edit affordances hide in both views: the 2D overlay pass strips handle/dimension chrome centrally (stripHandleChrome — body highlight, hit targets, and zone name text stay), and in 3D the slab / ceiling boundary editors and slab hole click-to-edit outlines mount only for a sole selection (the arrow-handle rig, wall side arrows, selection affordances, and floating menu were already single-gated). Transformable members show a move cursor in the floor plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): 3D body-drag group move replaces the move gizmo cross Pressing any transformable member of a multi-selection in 3D and dragging past the threshold now slides the whole selection on the ground plane (group-move-3d.ts, armed from the selection manager's pointer-down choke point) — same participant snapshot, welded junctions, grid + alignment snapping, live overrides, and single-undo commit as the 2D session; the 2D dashed bbox rides the same delta in split view. A plain click still collapses the selection to the pressed node. The purple GroupMoveHandle cross is deleted, and the hover cursor advertises the gesture: 'move' over any transformable member in both views (3D selection-manager cursor + 2D entry cursor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): group action menu — move / duplicate / delete the whole selection Multi-selections get a floating Move / Duplicate / Delete pill in BOTH views (GroupFloatingActionMenu anchored above the 3D group bbox, FloorplanGroupActionMenu anchored above the 2D dashed group box); the single-node menus are now sole-selection only. All three actions target the whole selection via shared group-actions: - Move starts a group pick-up: the selection rides the cursor (delta-relative, grid + alignment snapped) across BOTH surfaces — floorplan via the scene CTM, 3D via a ground-plane raycast through a three-context bridge published by the selection manager — until a click places it as one undo step; Escape / right-click cancels. - Duplicate clones the selection through the clipboard pipeline (subtrees + id remap, without touching the user's clipboard — new duplicateNodesToLevel), selects the clones, and picks them up; cancelling deletes the clones again. The pick-up scopes to the selection (no component expansion / welded links) because the clones sit exactly on the originals, and falls back to participant-data bounds because the clones' meshes haven't mounted yet. - Delete removes everything selected with the keyboard arm's bulk-confirm semantics. Verified end-to-end with the scripted smoke (16/16): chrome hiding, 2D/3D/split group drags, R/T round-trip, one-step undo, menu move / duplicate / delete, 3D move cursor. (Headless runs of the suite can deadlock in SwiftShader's GL readback on the split-view resize — environment-only; run headed.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): group transforms no longer re-create auto rooms Every group commit (2D/3D body drags, pick-up, rotate gizmo, keyboard R/T) now wraps its single updateNodes in pauseSpaceDetection / resumeSpaceDetection: the wall-driven room auto-detection rolls its baseline forward instead of treating rigidly-moved walls as a new room and duplicating its floors/ceilings. Room creation stays where it belongs — building and editing walls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): polygon hosts carry their attached items in group transforms Ceiling-mounted items are children of the ceiling, but polygon hosts (slab/ceiling) move by rewriting vertices — no group transform — so, unlike wall children which ride the wall mesh, their children were left behind by group moves/rotates. collectParticipants now snapshots a polygon participant's positioned children (level-frame coords, since the host group sits at the origin) so they ride every group path. Unit test included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): click picks up the group; the 2D dashed box is the drag handle Clicking a selected member of a multi-selection (no drag) now enters the group pick-up — the same click-to-move the sole-selection has — instead of collapsing the selection, in 2D and 3D alike; clicking outside still deselects. The 2D dashed selection box is itself the group's handle: move cursor across its whole area, press-drag anywhere inside slides the group, a plain click picks it up, and holding Cmd/Ctrl/Shift lets clicks pass through to the entries so membership toggling keeps working. HUD hint + shortcuts dialog updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): 3D dashed group selection box doubles as the drag handle The multi-selection now shows a dashed wireframe box in 3D (sibling of the 2D dashed rect): move cursor across its whole volume, press-drag anywhere on it slides the group, a plain click picks it up, and holding a selection modifier passes the press through so members inside can still be toggled. Rides the shared live-drag delta so it tracks the group mid-gesture in both panes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): mid-move R/T, 2D corner rotate, realtime opening symbols, box cursor + Esc hint - R/T while carrying a group (body drag or pick-up) now rotates the carried snapshots around the rest pivot — identical to the idle keyboard rotate — instead of leaking into the store mid-session; the session keydown owns the keys in capture phase and the group-move HUD advertises them. - The 2D dashed box grows corner rotate handles: drag a corner to spin the group in 15° steps, Shift for free rotation — the floor-plan sibling of the 3D rotate gizmo, sharing its scope, HUD hints, and single-undo + space-detection-paused commit. - Door / window floor-plan symbols now track wall drags in realtime: their defs merge the walls' live overrides (wallFloorplanSiblingOverrides) so ctx.parent is the effective wall instead of the stale store one. - The 3D selection box asserts the move cursor across its whole volume on pointer move (the old ''-guard lost to app default cursors). - Multi-select HUD + shortcuts dialog mention Esc / click-outside to clear, and the mid-move rotate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): rotate cursor + spinning box on 2D corner rotate; Delete ends a carry - The 2D corner handles show a proper curved-arrow rotate cursor (inline SVG data URI, black glyph with white halo, grab fallback). - The dashed selection box spins live with the group during a corner rotate: the rotate session publishes pivot+angle through the shared drag store and the box applies the SVG rotate transform. - Delete / Backspace during any group session (drag, rotate, pick-up) reverts the session first and then lets the global Delete arm remove the selection — no more dangling carry after deleting mid-move; the duplicate flow's cancel still discards the clones instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): per-node direct manipulation stands down for multi-selections Cmd is both the selection-toggle key and the single-node direct-move / direct-rotate trigger, so a Cmd+click that wobbled past the drag threshold over a selected member (slab or any kind) armed the legacy single-node move and yanked that one member out of the group. All four per-node direct-manipulation entry points — the 3D Cmd-drag move and Cmd+right-drag rotate in the selection manager, and their 2D siblings in the registry layer — now require the pressed node to be the SOLE selection; with a multi-selection the gesture is simply not registered (the group sessions own plain drags, Cmd+click keeps toggling). The shortcuts dialog notes the single-selection scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): group gestures own their ending pointerup — no synthesized click leak use-node-events synthesizes a selection click on EVERY canvas pointerup (deliberately more forgiving than R3F's onClick). Body drags were safe only because inputDragging suppresses it, but the pick-up flows leaked it as a race: the release that started a pick-up could collapse the multi-selection to the pressed node first (then only that node was carried), and the placement press re-selected whatever sat under the cursor after commit. The group sessions now claim their gesture-ending pointer events in the CAPTURE phase and stop propagation, so the canvas never sees them and no click is synthesized: the pick-up claims the placement press on pointerdown and commits on its pointerup; the 3D and 2D drag/rotate sessions stop their ending pointerup (split view: a 2D-started release over the 3D canvas had the same hole). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): screen-rect marquee tests projected oriented bounds, not double-inflated AABBs The 3D screen-rect box select intersected the marquee with each node's world AABB re-boxed in screen space — two nested axis-aligned inflations. Any oblique camera padded every node's claim, and rotated geometry (now common: group rotation) made it flagrant — a diagonal wall's world AABB spans a full square, so marquees selected objects visually far from the cursor. Membership now tests the marquee against the convex hull of the node's ORIENTED (local-frame) bounding box projected to screen — tight under any rotation and camera, uniform for every kind. Pure helpers (convexHull2D / rectIntersectsHull) live in marquee-geometry.ts with unit tests covering the diagonal-wall regression, containment both ways, and edge-only crossings. The plane marquee variant was already precise and is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): keep box select alive after group gestures The capture-phase stopPropagation the sessions used to silence the canvas's synthesized selection click also killed the window bubble listeners that clear the box-select pointer suppression — and the mouse pointerId is constant, so one group gesture left the marquee dead until blur. The sessions now use the designed suppression instead: they hold inputDragging raised through the release event's dispatch (lowered on a 0ms timer) so use-node-events suppresses the click itself, and every other pointerup listener runs normally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): marquee membership by plan footprint; 2D item highlight; site line steps back - The 3D screen-rect marquee now projects itself onto the active level's floor plane and tests the DATA kinds exactly there — walls/fences by their segment, slabs/ceilings/zones by their polygon — matching the plane-marquee tool's semantics. This kills the two remaining imprecision sources the hull pass couldn't: vertex-baked rotation (polygon/fence geometry lives in level coords, so after a group rotation even the local bbox is an inflated axis-aligned square) and ceiling parallax (an elevated plane projects to a screen quad offset from its room, selecting 'from far'). Mesh-transform kinds (items, columns) keep the projected oriented-bbox hull test; plan-space segment/polygon intersection helpers join marquee-geometry.ts with unit tests. - Selected items finally read as selected in the floor plan: palette stroke + heavier weight on the footprint, plus a selection ring drawn above the thumbnail (the move dot used to be the only cue, and it hides in multi-selections). - The site's dashed property line drops to 20% opacity while a multi or in-flight marquee selection exists so it stops fighting the dashed group selection box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): 2D marquee plan-footprint membership; item marquee preview; overlays re-measure after undo - The 2D screen marquee intersected each entry's getBoundingClientRect — an axis-aligned DOM box, inflated for rotated polygons and diagonal walls (the same double-inflation class as the 3D bug). It now maps the marquee through the scene CTM into a plan quad (rotated views included) and tests the data kinds exactly — walls/fences by segment, slab/ceiling/zone by polygon — reusing the shared marquee-geometry helpers; other kinds keep the DOM-rect fallback. - Items now show the marquee preview tint in 2D (highlighted viewState = the same palette stroke + thumbnail ring the selection shows, slightly lighter), instead of giving zero feedback until the drop. - Everything that measures the selection's meshes (2D/3D dashed boxes, the 3D group menu anchor, the rotate gizmo pivot) re-measures once the meshes settle after a scene change via useMeshSettleEpoch — an undo after a move no longer leaves the box at the pre-undo position (computeGroupBox reads mesh world bounds, which lag the store commit by a frame or two). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): dim the actual site boundary line during multi-selections The earlier dimming targeted registry-layer site entries, but the site lives at the scene root — outside the level DFS and the building-scoped sweep — so no entry ever matched and the dashed property line kept full opacity next to the dashed group selection box. Move the dimming to FloorplanSiteLayer (the component that actually draws the line) and drop the dead registry plumbing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
feeabf4bd3 |
fix(items): retry failed model loads, settle skipped items, keep exports clean (#480)
* fix(items): retry failed model loads, settle skipped items, keep exports clean A transient storage failure (observed: Supabase 504s under the bake page's ~200-concurrent-request burst) permanently broke an item for the whole session: drei's useGLTF caches the rejected load by URL, the per-item ErrorBoundary swallowed it, and the red debug-box fallback was BAKED into the exported GLB (observed in a prod artifact). Meanwhile ItemSystem cleared the dirty mark at group registration — before the model resolved — so scene-ready could fire while GLBs were still loading, risking exported placeholder geometry. - ModelWithRetry: bounded retries (1s/3s) that clear the useGLTF cache entry and re-mount via the boundary's new resetKey. The timer is owned by an effect keyed on the failure count, so StrictMode's synthetic unmount/remount re-arms it instead of silently discarding it (the naive onError-scheduled timer died exactly that way in dev). - Exhausted retries settle the item as SKIPPED: the debug box renders nothing during exports, and the failure lands in useViewer.itemLoadFailures (nodeId -> url) so a bake host can persist which items are missing from the artifact. - ItemSystem holds the dirty mark until the item settles (model mounted, terminally failed, or never expected) — scene-ready now genuinely waits for item content; loading placeholders also hide during exports. - ErrorBoundary: onError + resetKey props. Verified against a 200+-item prod scene locally: permanent 504 -> exactly 3 fetch attempts, bake completes without the item and without debug boxes; 504-once -> retry heals, artifact byte-equivalent to the intact run; no-failure runs unchanged (demo_1 byte-identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(items): reset retry budget + settled flag when the asset URL changes Bugbot: after a terminal load failure, swapping the item's model kept the stale failures/epoch state and the settled flag — the new URL never even attempted to load. ModelWithRetry is now keyed by asset src (clean retry budget per URL) and un-settles the item on mount so the replacement load is awaited by ItemSystem/scene-ready too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
58339c64f5 |
fix(plugin-trees): consume dirty marks so scene-ready fires for plant scenes (#476)
Instanced plant kinds (trees/grass/flowers) never cleared their dirty marks: FloorElevationSystem deliberately leaves the mark for kinds with a def.system, expecting that system to clear it after its own work, but InstancedKindSystem never participated in the dirty protocol. The marks lived forever, hasPendingSceneBuildWork() never went false, and the Viewer's scene-ready signal stalled at SCENE_READY_MAX_WAIT_FRAMES on every plant-containing scene — measured on the headless bake worker as ~190s (180 frames x ~1s SwiftShader frames) of pure cap-wait per bake. Clear the marks in a priority-2 useFrame pass: after the priority-1 floor-elevation lift in the same frame, and only for nodes whose proxy is registered (instances rebuild synchronously from the store, so a rendered node is already built). Validated by baking Wawa House locally: settle 22.5s -> 11.6s (the remainder is genuine asset loading), exported GLB byte-identical with and without the fix. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fb0b78c091 |
fix(editor): floorplan reference pass — north convention, cursors, R/T, Set Scale UX (#475)
Fix pass over 2D reference-image (guide) handling plus two adjacent repairs: - North is now world −Z (FLOORPLAN_VIEW_ROTATION_DEG 90 → 0, mirrored in world-grid-snap + mcp README; floorplan-panel de-dupes its local copy). A rotation-0 north-up scan reads upright when the compass says aligned, and "align north" maps to camera azimuth 0 instead of a 90° jump. - Guide resize cursor: custom double-arrow cursor aimed at the dragged corner in screen space — accounts for image aspect (was atan2(±1,±1), square-only), the guide's own rotation, and the floorplan view rotation the overlay group renders inside (was ignored entirely). - R/T rotate a selected reference (guide/scan) in ±45° steps; references live in selectedReferenceId, not the viewer selection, so both arms get the reference-first branch the Delete arm already had. Locked guides skip. - 2D-only mode hides the Top View button (drives the display:none 3D camera); orbit stays — it spins the synced floorplan view. - Set Scale UX: locked guides stay clickable (calibration auto-lock made them unselectable until reload); starting the flow from 3D switches to 2D; the length input pre-fills the drawn length in the pre-selected unit (imperial pre-filled meters labeled feet); Set Scale flips into Cancel while the flow runs (mirrored via referenceScaleActiveGuideId); Hide/ Clear Scale only render once calibrated; Escape cancels (global arm + dialog); Clear Scale and Replace Image drop the calibration auto-lock; discoverability: corner-hint row + panel nudge for uncalibrated guides. - healSceneNodes: strip child refs whose child's parentId names another parent (stale reparent leftovers rendered a window twice — duplicate React keys in 2D, doubled hosted geometry in 3D) and same-array dupes. - Editor accepts onLoaderChange so hosts can measure open-to-interactive time (community wires it to a PostHog timing event). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
717c2c5c0a |
fix: bug pass — legacy window crash, plant exports, zone snapping/visibility, HDR (#473)
* fix(core): apply window schema defaults on scene load Windows saved before a schema field existed (columnRatios/rowRatios/ frameThickness/…) loaded with those fields missing; the window mesh builder reads them unconditionally and threw every frame, crashing the viewer on legacy scenes. Zod-parse windows on load like doors so the schema defaults land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): mount plant geometry during client GLB/STL/OBJ export The client export never set isExporting, so instanced kinds (trees/ flowers/grass) kept their colorWrite:false raycast collider mounted and exported it as an opaque white box, while the real geometry (which only mounts while exporting) was never captured. Reuse BakeExporter's flag + frame-wait dance in ExportManager, and harden isRenderableMesh to drop colorWrite:false materials from exports entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): route zone drawing through the shared surface snap The zone tool quantized later vertices by distance along a free ray instead of snapping to the grid, and never joined the magnetic wall-corner/midpoint/crossing + alignment-guide pipeline that walls, slabs and ceilings use. Give zone the slab treatment in the 3D tool and both 2D floorplan branches (move + click + double-click commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(viewer): self-host the scene environment HDR drei's preset="sunset" fetches venice_sunset_1k.hdr from raw.githack.com, which intermittently fails ("Could not load venice_sunset_1k.hdr: Failed to fetch"). Point Environment at /hdri/venice_sunset_1k.hdr and ship the file in the app's public/ — same mirroring convention as /audios/sfx; consuming apps must carry the file too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): unmount zones entirely outside the zones layer Zone labels showed in every editing mode (visual noise), and each zone kept a drei <Html> mounted at opacity 0 — an <Html> costs per-frame matrix work and live DOM even when invisible. A new viewer presentation flag (showZones, default true) lets the editor unmount zone meshes and labels whenever the structure layer isn't 'zones' (and during snapshot capture); the registered group stays so zones keep their scene identity for selection and GLB export. Preview / first-person / viewer surfaces are untouched. Raycast-disable moved from a one-shot group flag to per-frame on the meshes, which now remount on layer toggles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
51fddc2d9c |
fix: capture-mode polish for studio & presets + pausable render loop (#469)
* fix(editor): keep snapshot pitch out of preset thumbnail capture The capture overlay's snapshot hint and 'Take snapshot' shutter label rendered in every capture variant, clashing with the save-as-preset flow's own 'Frame your item — click Capture' banner. Gate them on the existing isPreset discriminator and label the shutter 'Capture' there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): renderPaused flag suspends the frame loop Hosts that fully cover the canvas (e.g. the studio gallery overlay) can set renderPaused to stop FrameLimiter's RAF loop — a heavy animated scene otherwise keeps starving the GPU behind the overlay and makes it stutter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): drop the crop/format HUD in preset capture The pills crowded the save-as-preset banner and repeat what the fixed square frame already shows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): no contextual shortcut hints in the studio workspace Holding Shift surfaced select-mode/snapping hints over the studio compose panel and gallery, where there is no scene selection or tool to act on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f6a58489b8 |
fix(viewer): survive block-misaligned KTX2 textures on WebGPU (#467)
WebGPU rejects block-compressed textures whose base dimensions aren't multiples of 4; three's KTX2Loader only warns and transcodes anyway, so a baked GLB carrying one odd-sized user-item texture (e.g. 299x399) poisoned every render pass — endless 'Invalid BindGroup … Invalid CommandBuffer' spam. Route such payloads through a fallback transcode to uncompressed RGBA32 and repackage the pixels as a plain DataTexture (three's WebGPU upload path crashes on a CompressedTexture holding rgba8 data). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f6ffb93a39 |
fix(export): unbreak bakes and STL/OBJ exports on malformed/placeholder meshes (#466)
- roof-system: replace BoxGeometry placeholders (initial swap + empty-roof merged shell) with a group-less degenerate geometry — a Box's 6 groups against the roof's 4-material array crashed GLTFExporter on every scene with a segment-less roof node (prod: 'reading isShaderMaterial'), and count-0 groups crash MeshBVH's packed-tree build. - glb-export: sanitizeMaterialGroups pass repairs any mesh whose geometry groups don't line up with its material array before GLTFExporter runs. - export-manager: give neutralised (attribute-less) meshes an empty position attribute before STL/OBJ export — both exporters read position.count unconditionally. - bake-exporter: log the full error stack so the bake worker's console relay captures it in the job's error trail. - viewer: host-controlled disablePostFx prop (Viewer → PostProcessing) that skips building the SSGI/denoise pipeline entirely; the ?disable=postFx URL flag previously only bypassed it per-frame while still allocating it. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9b5e229770 |
feat(editor): stage overlay slot + pro capture overlay for studio (#465)
* feat(editor): stage overlay slot, preselectable snapshot crop, pro capture overlay - Editor/EditorLayoutV2 gain a stageOverlay slot rendered over the canvas but under the viewer toolbar (z-10 < z-20), so hosts can swap in a full-stage surface (e.g. studio gallery) without unmounting the WebGL canvas; FloatingLevelSelector hides while a stage overlay is active - CaptureMode 'standard' accepts an optional crop preselection (SnapshotCropMode, exported) so hosts can enter capture with standard/viewport/area already chosen - SnapshotCaptureOverlay restyle: letterboxed 16:9 frame with corner accents + rule-of-thirds for standard mode, thirds grid in viewport and area modes, top-center crop/format HUD chips, shutter button with capture/saved states — all existing crop, drag, resize and preset behaviors preserved Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): standard-capture aspect presets, capture scrim, caption fix - Standard crop gains aspect presets (16:9, 9:16, 4:3, 3:4, 1:1) — clicking the active Standard chip opens the picker; the letterbox frame, HUD and output resolution follow the choice - ThumbnailGenerateEvent carries standardSize; ThumbnailGenerator center-crops to it (default stays 1920×1080) - Subtle bottom scrim keeps shutter/caption readable on bright scenes - Fix missing space in the snapshot caption Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): standard-capture aspect preselect via CaptureMode Hosts (the studio capbar) can now pass standardAspect alongside crop when entering capture mode; the overlay initializes its aspect picker from it. SnapshotStandardAspect exported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22af31dea8 |
editor: snapping overhaul — shared-wall rooms, deterministic slab elevation, cursor-true transfers (#458)
* fix(snapping): show alignment lines in all modes, snap only in lines; north export; R/T hint; grid default - Alignment guides display in every snapping mode (grid/lines/angles/off); magnetic pull applies only in 'lines'. New isAlignmentGuideActive() predicate decouples guide display from the snap delta across all placement/move/draw producers (item, wall, fence, slab/ceiling/roof, column/shelf/spawn, door/ window, MEP, 2D floorplan drafting, surface snap). - Floorplan PDF export now rotates to north-up (FLOORPLAN_VIEW_ROTATION_DEG - building rotation), matching the on-screen aligned-to-north view. - Item placement rotate hint collapsed to a single 'R / T Rotate' row. - Default item snapping mode changed lines -> grid. * feat(walls): detect rooms across shared walls + tight connect-snap in all modes Room detection: planarize the wall graph before face-finding — split straight walls at T-junctions where another wall ends mid-span, so a room closed against the middle of an existing wall is detected, not just isolated 4-wall rooms. Auto-close: wire the wall builder's "Room (auto-close)" to the same room graph via wallClosesRoom(), so drafting stops when a segment seals a room against the existing structure — not only when the chain returns to its own start. Connectivity snap: add a tight wall-connect snap (WALL_CONNECT_SNAP_RADIUS 0.05) that also runs in grid/off/angles — within range of a wall (body or corner, uniform radius) the endpoint sticks onto it and the beacon shows, so rooms close in every mode. Lines keeps its wider magnetic radii. Gate alignment guides in non-magnetic modes to the same connect distance so a corner dot no longer magnetises the cursor from far. Tests: cover T-junction detection + wallClosesRoom; update the item-default and wall-split tests for the mode-driven behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(surfaces): gate alignment guides to connect distance in non-magnetic modes Port the wall drafting/endpoint gating to the shared slab/ceiling/roof snap (resolveSurfacePlanPointSnap): in grid/off/angles, only anchors within the connect distance are fed to the alignment resolver, so guides form to nearby points and far corner dots stop lighting up from across the plan. Filtering the candidates (local-frame, like the cursor) rather than the resolved guides avoids the floor-plan view rotation baked into the guide coords. Lines mode keeps the full-range guides; the surface wall connect-snap is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slabs): deterministic wall-slab overlap via clipped overlap length Wall elevation used ray-cast point samples that landed exactly on the slab boundary for perpendicular walls, so whether a butting wall followed the slab's elevation depended on which side of the slab it touched. Clip the wall centerline and face lines against the polygon and require >=5cm of on/inside length instead: walls along the slab edge follow it on every side, point contact never does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(grid): lift lattice 1mm off slab tops; wall-align on wall-item move start The horizontal snap lattice sat exactly at the followed surface Y, so it z-fought elevated slab tops while moving items. The visual mesh now rides 1mm above; the grid event plane keeps the true height. Moving a wall-hosted item showed a horizontal grid until the first pointer move published a wall surface — the mesh fallback assumed UP. Derive the host wall normal from the item mesh's world orientation (local +Z faces out of the wall) so the lattice is wall-aligned from the first frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): unify selection shortcuts + truthful select-mode hints Shift+click now toggles selection membership in 3D like Cmd/Ctrl (and appends on box-select release), matching the 2D floorplan. The HUD rows describe what actually works in both views: move is plain left-drag (the grip/dot), the two vague selection rows collapse into one Cmd/Shift or-group, and the modifier-held variants (freely / with guides / bypass snaps) are gone — guides follow the snapping mode now. Key pills join with + for combos and / for alternatives; Shift renders as the shift icon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): group gizmos — reliable rotate grabs, mutual hide, snapping-mode integration The group-rotate gizmo never received pointer events through the shared invisible hit-area path (EDITOR_LAYER + custom raycast) in its portalled context; its handlers now live on the visible arrow plus a plain default-layer invisible torus, so hover and drag work with a fat target. Group drags begin a handle-drag scope: each gizmo hides while its sibling drags (the frozen corner goes stale), idle hints leave the HUD, and the drag gets contextual hints — Shift free-rotation for rotate, the snapping chips for move. Group move joins the snapping-mode system via the 'item' context: Shift cycles the mode, Ctrl the grid step (both read live mid-drag), and 'lines' runs the same Figma-style alignment as single-node moves against the group's bbox anchors. Chip clicks now tick like the keyboard cycles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): group-move ticks in every snapping mode Mirror the single-node move's sfx: emit per delta change rather than only on grid crossings, so lines/off get the same rate-limited texture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(items): keep host transfers glued to the cursor Moving between hosts used to diverge from the pointer and mangle state: - Grab offsets: every surface anchor (wall / ceiling / shelf / item surface / floor) preserved the grab offset by re-seeding from the item's carried-over position on each new host, landing it far from the cursor. The grab offset now survives only on the original host and only until the item anchors anywhere else — after that every host (the original included) centers the item under the cursor. Applied uniformly to the placement coordinator and the window/door move tools. - Rotation: detaching from a rotated shelf/table back to the floor kept the HOST-local yaw as the level yaw, visibly spinning the item. The detach now re-expresses the item's world yaw in the level frame. - Elevation: the same detach wrote the level parentId to the store but not the draft ref, so the floor-elevation resolver bailed on its parent-must-be-a-level guard — the snap grid and the item stopped following slab elevations for the rest of the drag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(items): latch grab-offset forgetting across every host kind Review follow-up (PR #458): unify the per-surface grab-forget rule into one grabForgotten latch. A wall/ceiling item could not actually reach a shelf or the floor (item-surface enter rejects attachTo assets), but a wall item CAN anchor on a roof face — and returning to its original wall then restored the stale grab offset. Roof-wall transitions (and floor landings after a host visit) now trip the latch in the coordinator and the window/door move tools alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5c071ecad5 |
feat(plugins): plugin contract + first-party Nature pack (trees/flowers/grass) (#457)
* feat(plugins): trees tracer-bullet plugin + minimal host panel surface
Ship one real first-party plugin and only the host surface it forces
into existence, proving the plugin contribution paths every future
plugin (mint.gg generator, Home Assistant, environments, room volume)
will reuse.
Host surface (core + editor):
- core: Plugin.panels + observable panelRegistry; loadPlugin routes
panels (namespaced by plugin id, dup-throws like nodes).
- editor: AppSidebar merges panelRegistry into the icon rail via
useSyncExternalStore; each plugin panel lazy-loaded behind an error
boundary; host extraPanels keep precedence.
- editor: widen Tool to `KnownTool | (string & {})` so plugin tool ids
(e.g. 'trees:tree') typecheck; dispatch already registry-first.
Trees plugin (packages/plugin-trees), structurally a third-party pack
(peer-deps on @pascal-app/*):
- trees:tree node — procedural low-poly geometry (oak/pine/birch/palm),
free parametric inspector (preset/height/seed + Randomize), placement
tool + ghost preview built from public primitives only.
- presets rail panel — panel -> plugin store -> def.tool -> SceneApi ->
scene -> reactive read-back ("N planted").
Loading + docs:
- apps/editor: setPluginDiscovery([treesPlugin]) in bootstrap;
transpilePackages + dep.
- wiki/architecture/plugin-authoring.md: panels field, error-boundary
contract, styling, tool-from-panel note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugins): merge plugin panels into the v2 sidebar layout too
The community shell renders <Editor layoutVersion="v2" sidebarTabs={...}>,
a separate rail from AppSidebar. Merge registry panels into the v2
tabMap + tab bar (and thus mobile) so plugin panels show up there as
well, not only in the v1 AppSidebar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugin-trees): ez-tree geometry + instanced rendering + snapping
Replace the placeholder low-poly geometry with dgreenheck/ez-tree
(self-contained — bark/leaf textures inlined as base64, no assets to
host) and render the forest with true GPU instancing.
Rendering:
- def.system (system.tsx): one collective renderer that groups every
trees:tree node by (preset, seed) variant and draws each variant as
one InstancedMesh per ez-tree sub-mesh, composing the parent level's
world matrix into per-instance matrices. ~1 draw call per variant.
- def.renderer (proxy-renderer.tsx): an invisible, raycastable per-node
proxy so the host's existing selection / outline / zone machinery
keeps working — no instanceId bookkeeping. (Outline highlights the
proxy bbox; documented.)
- geometry per variant generated once by ez-tree and cached; seeds drawn
from a bounded pool so trees actually share variants.
Presets remapped to ez-tree built-ins (oak/pine/aspen/ash/bush).
Placement now respects the active snap mode — isGridSnapActive() +
gridSnapStep + snapPointToGrid, like the built-in item/shelf tools.
transpilePackages += @dgreenheck/ez-tree in the editor app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugin-trees): true-silhouette hover/select + richer presets panel
Selection/hover now outlines the real tree shape, not the bbox. The
proxy splits into an outer group (stable invisible box collider +
pointer handlers, the raycast target) and an inner registered group that
mounts the real ez-tree geometry (invisible, non-raycasting) only while
the node is hovered or selected. The host outline pass reads the
registered inner group, so it traces the true silhouette; picking stays
on the steady box. Geometry for the highlight reuses the cached variant.
Panel: redesigned cards with gradient swatches + selected state, a
"planted" count chip, and a height slider that seeds the next tree's
height (a per-instance scale — never touches placed trees or instancing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugin-trees): curated tree params + procedural flowers (sibling kind)
Tree params: expose foliageDensity, trunkThickness, and a leafless toggle
in the inspector and the panel brush. Each is folded into the instancing
variant key and mapped onto ez-tree options (radius scale, leaf count),
so editing one tree only re-buckets that tree — instancing degrades
gracefully, never worse than per-node.
Flowers: add a `trees:flower` sibling kind — simple procedural geometry
(stem + petals + center, merged per material), presets daisy / tulip /
lavender, instanced + selectable exactly like trees.
Refactor: extract the instanced renderer + selection proxy into a generic
`instanced.tsx` (InstancedKindSystem + KindProxy) and the snap/level/grid
placement wiring into `placement.tsx` (usePlacement). Trees and flowers
are now thin bindings — the template for future plant kinds.
Panel: a Trees / Flowers toggle, gradient preset cards, per-kind planted
count, and brush sliders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugin-trees): native inspector controls in panel + preset thumbnails + ez-tree credit
- swap the panel's custom slider/checkbox/toggle for the host's exported
SliderControl/ToggleControl/SegmentedControl so the brush matches the
right-hand inspector pixel-for-pixel
- replaceable preset thumbnails (inline SVG data URIs, no asset hosting) render
as <img> cards instead of gradient swatches
- credit footer linking @dgreenheck/ez-tree (Daniel Greenheck, MIT)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugin-trees): all ez-tree presets + type + leaf/branch colours, flower petal colour, grass kind
- tree presets now cover all of ez-tree's built-ins via species × size
(Small/Medium/Large + Bush 1/2/3 + Trellis), plus a Deciduous/Evergreen type
- edit-only leaf & branch colour tints (leaves.tint / bark.tint), all folded
into the instancing variant key
- flowers gain a per-flower petal colour (baked from preset at placement)
- new trees:grass instanced kind — procedural blade tufts (meadow/fescue/reed)
with a per-tuft blade colour, reusing the generic instanced + placement core
- panel: Trees/Flowers/Grass segmented switch, size + type controls, native
host controls throughout; credit links Daniel Greenheck's X
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(plugin-trees): tolerate nodes persisted before new fields existed
treeSpecOf/flowerPetalColor now default every geometry field, and hexToInt
guards undefined — trees/flowers placed before size/type/colour fields were
added load with neutral defaults instead of crashing the instanced renderer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugin-trees): shared always-on wind + pin credits to panel bottom
- wind.ts injects a sway into each material's begin_vertex driven by one shared
uTime uniform (advanced per frame in the instanced system); USE_INSTANCING
guards keep the same material valid for the non-instanced placement ghost.
Applied to tree, flower, and grass materials — a whole scene sways like ez-tree.
- credits footer is now sticky to the bottom of the panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugin-trees): rename rail panel Trees -> Nature (covers trees/flowers/grass)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugin-trees): rename panel header Plant -> Nature to match the rail
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: bake-exportable instanced plants + renderer-agnostic wind
Bake (GLB export): add a transient useViewer.isExporting flag. BakeExporter
flips it, waits for the commit, then exports. KindProxy watches it and, during
export, emits each plant's REAL visible geometry under scene-renderer (which the
exporter clones) instead of the invisible collider — so instanced kinds
(def.system) that live outside that subtree are captured. The collider box is
dropped during export so it doesn't bake as a phantom solid.
Wind: the previous onBeforeCompile wind was a no-op under the editor's WebGPU
renderer (WebGL-only hook). Replace it with a renderer-agnostic per-instance
base-pivot tilt animated in useFrame (which FrameLimiter drives continuously).
Reads as wind under any renderer; removes the dead wind.ts + applyWind calls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugin-trees): TSL vertex-bend wind (WebGPU), replacing CPU tilt
wind-node.ts adds a TSL positionNode that bends each plant proportional to
height above its base, phased per-instance (instanceIndex) and per-vertex, so
tips sway and roots stay planted — animated on the GPU via the renderer's time
node. ez-tree is untouched: its materials are copied into MeshStandardNodeMaterial
(toWindMaterial); flowers/grass build node materials directly (windStandardMaterial).
Instance matrices go back to static.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(plugin-trees): tree materials render black under wind (Phong not Standard)
ez-tree's bark/leaves are MeshPhongMaterial; copying them into a
MeshStandardNodeMaterial swapped the shading model (specular/shininess ->
roughness/metalness) and rendered black. Convert into the matching node
material type (Phong->Phong, Lambert->Lambert, else Standard) so map/color/
alphaTest/side are preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(plugin-trees): trees still black — build node material from map/color explicitly
Material.copy() doesn't transfer a classic material's map/color onto a node
material, so textured trees rendered black (flowers/grass were fine — they carry
no map). Re-create the ez-tree materials as MeshStandardNodeMaterial with map,
alphaMap, color, side, alphaTest passed explicitly in the constructor — the same
proven path the flower/grass materials use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(plugin-trees): tree wind — per-leaf flutter (ez-tree style), static trunk
The whole-tree height-based bend was a rigid rotation about the base ('rotating
in place'). Replicate ez-tree's actual approach: scale sway by the leaf card's
uv.y and apply it ONLY to the 'leaves' material (multi-frequency wave, phased per
instance + per leaf) so leaves flutter from their attachment while bark/branches
stay static. Flowers/grass keep the gentle whole-plant stem bend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugins): Plugin.onSceneLoad hook + viewer application; trees wind via it
Adds a broad, plugin-declared post-load scene hook so effects survive GLB export:
- core: Plugin.onSceneLoad (lazy, three-free) + observable sceneHookRegistry + loadPlugin routing.
- viewer: applyPluginSceneHooks util; PluginSceneHooksSystem re-runs it on the live
scene as nodes change; GlbScene runs it on each loaded baked GLB.
- plugin-trees: declare onSceneLoad; wind is no longer welded into the materials —
the hook re-attaches LEAF_FLUTTER/STEM_BEND by material name (leaves / flower-* /
grass-*), so it now animates in the baked /viewer too, not just live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(plugin-trees): scale-invariant leaf wind (÷ modelScale) so baked GLB matches editor
The bake's quantiser renormalises each plant mesh to [-1,1] and moves real size
into a large node scale (~3 on trees). LEAF_FLUTTER added a constant local offset,
so in the baked GLB it was multiplied by that scale → ~3x over-swing. Divide the
sway by modelScale.x to keep a constant world-space amplitude; editor (scale 1)
is unchanged, baked now matches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* revert: onSceneLoad wind approach → back to wind-in-materials
Reverts the Plugin.onSceneLoad hook + viewer application + modelScale compensation
(commits 893ca839, 6c9ee003). Superseded by the bake-policy 'replace' model
(plans/editor-plugin-trees-example.md Part D): trees render live in the viewer via
their own path, so wind is a plain positionNode on the material again — no hook,
no baked-geometry decoration, no coordinate-space fights.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(bake): per-kind bake policy (static/strip/replace)
Adds NodeDefinition.bake ('static' default | 'strip' | 'replace') + registry
helpers bakePolicyOf/kindsWithBakePolicy in core. Generalises the previously
hardcoded scan/guide handling:
- glb-export strips kinds with bake==='strip' from the artifact (was a
scan/guide type check); 'replace' kinds stay baked (portable static snapshot).
- glb-reference-nodes selects rebuild candidates by policy instead of type.
- scan/guide declare bake:'strip'; trees/flower/grass declare bake:'replace'.
Viewer-side 'replace' swap (strip baked meshes + live-rebuild) is the next slice.
Part of plans/editor-plugin-trees-example.md Part D.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(bake): viewer 'replace' swap — hide baked meshes, re-render live
Adds NodeDefinition.bakeReplaceRenderer (falls back to renderer). The baked
/viewer now, for kinds with bake:'replace' and a loaded plugin:
- hides the static baked meshes (bakePolicyOf(kind)==='replace') in GlbScene's
identity pass — capability-gated, so with no plugin the meshes stay;
- re-renders each node live via buildGlbReplaceNodes + the shared portal-into-
baked-level path (GlbReferenceNode now prefers bakeReplaceRenderer), so trees
ride level stacking automatically and wind runs in real coordinate space.
plugin-trees ships a hookless KindStatic (real geometry + wind materials, no
collider/selection) and per-kind static-renderer binds; tree/flower/grass point
bakeReplaceRenderer at them. Completes plans/editor-plugin-trees-example.md Part D.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf(viewer): memoize GlbReferenceNode(s) — skip forest re-render on camera move
GlbScene re-renders per frame during camera movement (hover raycast / walkthrough
HUD). The rebuilt nodes reconciled every frame — fine for 1-2 scans/guides, but a
bake:'replace' forest puts dozens of nodes here (profiler: KindStatic x56 6ms,
GlbReferenceNode x57 4ms per frame). memo at the node boundary skips the whole
subtree (incl. the plugin renderer) when (node, anchor) are unchanged; both props
are stable refs, so it short-circuits cleanly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf(plugin-trees): NO_RAYCAST on KindStatic meshes — cheap hover over a forest
The baked scene's <primitive> has pointer handlers, so R3F recursively raycasts
every descendant on each pointer move (incl. orbit drags). The live replace trees
had fully-raycastable dense ez-tree geometry, so hover cost ~400ms/frame over a
forest (profiler: 'JavaScript' 413ms during camera move). They carry no pascalId
(a hit resolves to the level, not the tree), so they're scenery, not pick targets
— mirror KindProxy, which already NO_RAYCASTs its real geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(bake): viewer 'replace' renders instanced per level (was per-node)
Replaces the per-node KindStatic viewer path with a collective instanced one,
mirroring the editor's system instead of diverging from it:
- extract InstancedNodes from InstancedKindSystem (shared by both) with a
localSpace flag: editor folds parent world matrix (root instances); viewer uses
level-local matrices + NO_RAYCAST, portaled into the baked level.
- bakeReplaceRenderer is now a collective renderer (BakeReplaceRenderer<N>,
receives {nodes}); GlbReplaceInstances groups replace nodes by (level, kind) and
portals each kind's instanced renderer into that baked level. GlbReferenceNode
reverts to per-node renderer (strip kinds only).
Fixes: forest hover cost (100+ tree meshes → a few instanced draws, R3F walks far
fewer objects) and per-tree wind phase (instanceIndex varies again; per-node
KindStatic gave every tree phase 0 → unison sway). Trees still ride level stacking
(portaled) and stay static in a plain glTF viewer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf(viewer): BVH the baked scene — accelerate hover/pick raycasts
The baked /viewer runs with useBvh={false}, so GlbScene's hover/pick raycasts
against the baked building were brute-force triangle intersection (profiler:
_computeIntersections + intersectTriangle ~30%). The parametric viewer wraps its
scene in <SceneBvh>; the baked one never did. SceneBvh's effect is one-shot on
mount and can't catch the async-loaded GLB, so compute a per-mesh BVH inside
GlbScene keyed on gltf.scene instead. 'replace' instances are NO_RAYCAST, so the
raycast===Mesh.prototype.raycast guard skips them (mirrors SceneBvh).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(plugin-trees): inherit ez-tree preset by default (seed/type/tints)
Our node defaults (seed:1, treeType:deciduous, colors:#ffffff) were applied as
*overrides* on top of loadPreset, discarding each preset's tuned seed, growth
model, and tints — so our trees looked nothing like eztree.dev (pine grew
deciduous, foliage washed white, every silhouette off).
Mirror the flower petalColor pattern: seed/treeType/leafColor/branchColor are now
optional; generateTree only overrides an ez-tree option when the node set it,
otherwise loadPreset's value stands. Placement stores none of them (fresh tree =
pure preset; all same-preset trees share one instancing variant); the deciduous/
evergreen brush toggle is gone (growth model comes from the preset). The inspector
keeps them as per-tree overrides, and Randomize still varies the seed.
Verified against ez-tree: fresh Oak Medium → seed 35729 + preset tints; Pine
Medium → evergreen + seed 13977; overrides apply when set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugin-trees): bundle real preset artwork (webp, in-package)
Replaces the placeholder SVG thumbnails + lucide panel icon with the real nature
art. 13 webp (~126KB total, 256px cards / 128px icon) live in src/assets and are
imported via art.ts — both consumers are Next, so transpilePackages runs them
through the image pipeline (hashed, cached /_next/static/media URLs). No CDN, no
per-app public/ mirroring; the assets travel with the package.
- art.ts: central webp imports → TREE_ART / FLOWER_ART / GRASS_ART / NATURE_ICON
- presets/flower-presets/grass-presets: thumbnail ← bundled art (was *Thumbnail())
- index.ts: Nature panel icon ← NATURE_ICON (was lucide:leaf)
- assets.d.ts: ambient *.webp decl (no next type dep)
- remove thumbnails.ts (placeholder generators, now dead)
Verified on the running dev server: webp emitted + served (HTTP 200, image/webp).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(plugin-trees): square preset cards on light-gray flattened thumbnails
Regenerate the 12 thumbnails with the transparency flattened onto gray-100
(#f3f4f6) and render the card square (aspect-square, was h-16 crop). The
nature-icon keeps its transparency for the icon rail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(plugin-trees): correct move-drag + outline behavior for instanced plants
Two selection defects, one root cause each:
1. Outline moved mirror-wise during a move drag. The host move tool drives the
registered Object3D imperatively with absolute level-local positions (and
mirrors them via useLiveTransforms) — the contract ParametricNodeRenderer
satisfies by putting position + registration on the same group. KindProxy
registered a child nested inside the transform-carrying group, so drag
positions landed in the node's rotated frame (placement gives every plant a
random Y rotation → deltas rotated up to 180°). Restructure: the registered
group now carries position/rotation (live-transform aware); the box collider
is a positioned sibling, staying out of the outline mask.
2. Outline froze while the mesh swayed. The outline mask pass renders with a
shared override material, so it can never follow the material positionNode
wind. Instead, while hovered/selected the collective system skips the node
and the proxy mounts the real geometry with static twins of the wind
materials (toStaticMaterial — explicit property transfer; node-material
clone() drops map/color). Outlined mesh == visible mesh, both still.
Bonus from the same restructure: a move drag now animates the actual plant in
realtime (the proxy is what the tool drives), not just the drag box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(plugin-trees): 2D plan symbols + trunk-sized footprint
- def.floorplan for the three kinds — the registry floor-plan layer renders any
kind that provides one, so this is all plugin nodes need to appear in 2D.
Tree: dashed canopy ring (dashed = overhead element) in the preset swatch +
solid trunk dot; ring is pointer-events:stroke so the large disc doesn't
steal clicks from what's under the canopy. Flower/grass: small colour dots.
Selected → palette stroke + move-handle; hovered → hover stroke.
- Tree floorPlaced.footprint is trunk-sized (treeTrunkRadius) instead of
canopy-sized, so the move/placement drag box hugs where the tree actually
plants instead of spanning the crown. The invisible hover collider keeps its
larger radius — only the displayed box shrinks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugin-trees): trunk-sized drag box, 45°-aligned rotation, rotate gizmo
- The move drag box ignored floorPlaced.footprint: with collides:false the tool
auto-measures the rendered mesh — which, since the selection swap-out, is the
whole canopy. Declare capabilities.dragBounds (trunk-sized, node-local) so the
box hugs the trunk and rotates with the node instead of wrapping the crown as
a world AABB.
- Placement wrote a fully random Y rotation, so the box/gizmo never sat on a 45°
step. Snap the random rotation to 45° increments (variety preserved, alignment
restored) and widen rotatable.snapAngles to 8×45° on all three kinds.
- Add the standard rotate gizmo (def.handles, shelf-style arc handle): ring
around the trunk near the ground — not the canopy, which would put the handle
meters out on a large oak.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugin-trees): live rotate preview + ground-level rotate ring
- KindProxy now folds useLiveNodeOverrides into its transform (mirrors
ParametricNodeRenderer) — the rotate/resize gizmos publish mid-drag patches
there, so the plant turns in realtime instead of snapping on commit. The
snap-on-commit (with the arc delta wrapping to ±180°) is also what made long
drags land 'the wrong way'; with live feedback the direction reads correctly
and matches the item gizmo pipeline exactly.
- Rotate ring drops from mid-trunk to 0.25m — a floor affordance like the item
gizmo, not a waist-height hoop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(registry): panelForKind — 'find in catalog' opens a plugin kind's panel
loadPlugin now associates every node kind with its plugin's first (namespaced)
panel id; panelRegistry.panelForKind(kind) exposes it. A host's find-node
handler can open the right panel for any plugin kind with zero per-plugin
knowledge.
plugin-trees uses it end-to-end: a module-level selection:find-node listener
(find-sync.ts, imported by the manifest so it's live from plugin load) points
the panel store at the found node's section + preset — panel section state
moved from panel-local useState into the store to make it addressable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(registry): plugin panels declare their workspaces (default edit-only)
Plugin panels rode into the studio rail: the host swaps its own sidebar tabs by
workspaceMode, but usePluginPanels appended registry panels unconditionally.
PluginPanel gains workspaces?: readonly ('edit'|'studio')[] — manifest metadata,
default ['edit'] (an authoring panel has no business in the clean render
workspace; a plugin shipping studio tooling opts in explicitly). usePluginPanels
filters by the current workspaceMode, covering both the v1 AppSidebar and v2 tab
bar paths. Nature declares nothing and disappears from studio via the default;
the v2 layout's existing active-tab fallback handles a panel vanishing mid-use.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: link plugin-authoring guide from README + CONTRIBUTING, fix stale wiki path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugin-trees): keep ez-tree off the eager SSR path
ez-tree loads its inlined textures at module scope (needs document), so any
eager import chain reaching geometry.ts crashed Next prerender
(ReferenceError: document is not defined on /_not-found). Move the pure
helpers (mulberry32, naturalHeight) to variant-utils.ts so the
flower/grass builders and floorplan no longer pull ez-tree, and drop the
generateTree re-export from the package barrel (no external consumers).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c51e673421 |
Merge pull request #451 from pascalorg/feat/slower-viewer-walk-speed
viewer: slow first-person walk/run/jump speeds |
||
|
|
36132bbcea |
viewer: slow first-person walk/run/jump speeds
Walk 4→2 m/s, run 5.5→5 m/s, jump 6→5 (~1.27 m peak). Applied to both the GLB walkthrough controller (viewer) and the editor first-person controls so the two surfaces stay consistent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
08731b5bde |
Merge pull request #450 from pascalorg/feat/placement-interaction-polish
fix(editor): wall-item placement & floorplan polish |
||
|
|
ba464ef47e |
fix(editor): wall-item placement & floorplan polish
Polish pass on the placement/interaction overhaul. Five fixes: - Item-on-item rotation box: the cursor box held the host-local yaw instead of world yaw, so it diverged from the item by the host's rotation when stacked on another item (fine on the floor). The box now derives world yaw from the mesh/host quaternion in both the R/T handler and the move-start sync. - 2D floorplan move now respects the snapping mode (parity with 3D): grid quantization only in grid mode, alignment guides only in lines/magnetic mode; Shift/Alt no longer hard-bypass. Item move also plays the move "tick" SFX on any resolved-position change, like the 3D move. - Wall-side item footprint side: the 2D footprint depth offset extended toward the wall (centerLocalZ -depth/2) instead of into the room, mirroring the item across the wall; flipped to +depth/2. Aligned the undefined-side anchor to the 3D convention (front +1 / else -1). - 3D placement preview side: the wall-side preview bounds + base plane used a -Z (into-wall) convention; flipped to +Z (into-room) to match the body and the fixed 2D footprint. The 2D live preview during a 3D wall placement now publishes the plan rotation (wall angle + item yaw) instead of the world cursor yaw, which was pi off on a wall face and flipped the footprint to the far side. - Cmd/Ctrl+R no longer rotates/flips the selected node (it reaches the browser reload); guard added to the global selected-node handler and the 2D move overlay. Verified: core/editor/nodes tsc, biome, editor 162/0 + nodes 292/0 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9084396a82 |
Merge pull request #449 from pascalorg/feat/floorplan-export
feat(editor): floorplan export (multi-page PDF, per level) |
||
|
|
b87b1f999d |
feat(editor): floorplan export (multi-page PDF, per level)
Add a Floorplan group to the settings Export panel alongside the 3D model exports, with "Full floorplan" and "Structure only" buttons. Export re-runs the live registry-driven floorplan pipeline (def.floorplan -> FloorplanGeometryRenderer) headlessly with a neutral viewState, fits each level to its own page, and titles each page with the level label. Every level of the active building becomes a page in one landscape A4 PDF. "Structure only" keeps category === 'structure' nodes; "Full" keeps every visible node with a floorplan builder. - new lib/floorplan/floorplan-export.tsx; jsPDF + svg2pdf dynamically imported so they only load on export - export five pure helpers from floorplan-registry-layer for reuse (buildContext, getFloorplanLevelData, floorplanLayerRank, splitFloorplanOverlay, isFloorplanNodeVisible) — no behaviour change - bake vector-effect:non-scaling-stroke widths into real units before svg2pdf (which ignores the hint and would otherwise draw door/window/ stair linework as metre-wide strokes) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a6023356b9 |
Merge pull request #444 from pascalorg/fix/bake-door-window-anims
fix(doors): bake open-animation clips for every operation door + un-flipped rest pose |
||
|
|
b0313a13dc |
chore(editor): sort imports in glb-export.test.ts after main merge
Fixes the lone biome organizeImports error so the quality gate passes on the post-#448 base. Type-check (9/9), biome, and all suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
db35af9a05 | Merge remote-tracking branch 'origin/main' into fix/bake-door-window-anims | ||
|
|
2dbe4677f4 |
Merge pull request #448 from pascalorg/feat/placement-interaction-overhaul
Placement & interaction overhaul: FSM scope spine, per-context snapping, 2D/floorplan perf |
||
|
|
43fa2b1253 |
Merge origin/main into feat/placement-interaction-overhaul
Resolve 7 conflicts keeping our snapping migration + floorplan perf work as source of truth, combined with main's MEP run-continuation / Alt-detach / latch handles. Rebuilt two import blocks the auto-merge silently truncated (node-arrow-handles.tsx, duct-fitting/move-tool.tsx). Verified: tsc clean across core/viewer/editor/nodes/mcp, 451 tests pass, biome clean. Floorplan view-transform re-render storm confirmed pre-existing (not introduced by this merge). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
15d36c12b1 |
fix(editor): zone drafting respects the snapping mode + shows its HUD chip
Two zone fixes. helper-manager: the contextual HUD only rendered for tools with `def.toolHints`, so zone (none) showed no HUD and no snapping chip even though it resolves a snap context. Render the generic RegisteredToolHelper whenever the tool has hints OR a snap/continuation context; hoist the legacy `roof` RoofHelper above it so the new fallback doesn't capture it. Any snappable hint-less draft tool now advertises Shift = cycle. zone-tool: a not-yet-migrated legacy tool — it used Shift as a snap bypass and applied `gridSnapStep` unconditionally, so Off mode still snapped to grid. Migrated to the mode-driven exclusive-modes convention (zone resolves to the 'wall' context): grid quantize gated on isGridSnapActive(), 15° ray gated on isAngleSnapActive(), Off/Lines leave the raw cursor. Dropped the Shift-bypass and its key listeners — Shift now cycles the mode globally. Recorded as migrated in the review skill's known-legacy list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a2f1ef1683 |
docs(architecture): codify live-overrides drag protocol + floorplan per-node perf
Capture the durable patterns from the placement-interaction overhaul so reviews and new devs don't regress them: - tools.md: new "Data-driven live drag" section — kinds whose geometry is recomputed from fields (wall/opening/endpoint) preview via useLiveNodeOverrides (merged by getEffectiveWall/getEffectiveNode), store written once on commit; per-tick useScene.updateNodes is the documented anti-pattern (churns the nodes ref → app-wide re-render flood). Plus "Floorplan registry: per-node subscriptions" — each entry subscribes to its own live slice, memo'd with stable props, sibling-epoch invalidation; widening to the whole Map / dropping memo is a regression. Plus a note that the HUD snapping chip renders for any snap-context tool, not only those with def.toolHints. - review SKILL.md: matching blockers in §C (data-driven drag / no per-tick store write) and §D (per-node list subscriptions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dc468a084b |
feat(editor): show placement grid for any armed grid-mode tool
The grid only appeared while a ghost was in flight (moving node / placing scope / armed GLB item), so a merely-armed draft tool (wall / slab / fence / ceiling / zone / column / MEP) showed no lattice. Gate visibility on isGridSnapActive() alone — it already derives the snap context from the interaction scope OR the armed build tool and is true only when that context resolves to grid (false for select / idle / paint / lines / off), so it is exactly the right condition. Draft tools publish no surface; the grid's horizontal branch already falls back to the active level floor, with the reveal patch following the cursor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
81c5b2d22e |
fix(nodes): match item floorplan sprite rotation to its footprint box
The footprint polygon uses `rotateVec` (R(-angle)) but the sprite was drawn via SVG `rotate(+angle)`, so image and box counter-rotated and diverged by 2x the item's rotation in the 2D floorplan. Negate the image rotation so the sprite tracks its box (and the 3D orientation). Scoped to items — the only emitter of the `image` floorplan geometry kind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
03284553aa |
perf(editor): per-node memo + own-slice subscriptions for floorplan registry
In split view, dragging any node re-rendered ALL ~150 per-node InteractiveGeometry instances every tick (profiler: InteractiveGeometry ×3192), because FloorplanRegistryLayer subscribed to the whole useLiveTransforms / useLiveNodeOverrides maps and the per-node component wasn't memoized. Extract a memoized FloorplanRegistryEntry that subscribes to ONLY its own slice (useLiveTransforms(s => s.transforms.get(id)) / overrides.get(id)). The live stores write a fresh value object for the changed node only, so unchanged nodes keep identity and don't re-render. The parent now watches just the stable node-id list; sibling invalidation (wall miters / opening cuts) moves to a store-subscribe that bumps only affected siblings' epoch. InteractiveGeometry is also memoized. Geometry cache, affected-sibling epochs, the base/overlay two-pass, hit-testing, and selection/hover are all preserved verbatim. An item move now re-renders only its own entry; a wall drag only the moving wall plus its linked-corner siblings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f603e83c4 |
fix(editor): unify wall-endpoint move activation; stop wall-move from co-firing
Two fixes to the wall-endpoint reshape interaction.
selection-manager: a node:click is synthesized on R3F pointer-up, so an endpoint
handle that sits on the wall body lets the wall mesh (raycast-hit behind it from
a 3D angle) emit its own click on the same release — selecting the wall and
arming its move tool on top of the endpoint move. Ignore the body click while an
`endpoint` reshape owns the pointer. Scoped to `endpoint` so hole-edit (which
relies on node clicks to exit) is unaffected.
move-endpoint-tool: a press-drag committed on release but a tap dismissed, and
whether the tap's release ran at all raced the window pointer-up listener
mounting a tick after the handle's pointerdown ("works once, then needs a long
press"). Unify on one rule: commit only when the endpoint actually moved,
otherwise stay armed. A tap now grabs the endpoint (it follows the cursor; the
next click after a move commits) exactly like a press-drag — both engage
identically. Drops the now-dead hasDraggedRef.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
eeadec11a7 |
perf+fix(editor): gate 2D scene in 3D mode; wall-endpoint move via live overrides
Two changes from the split-view / wall-endpoint perf + UX pass. floorplan-panel: render the heavy 2D <svg> scene only when the panel is visible (`isFloorplanOpen`, i.e. viewMode !== '3d'). The panel stays mounted in 3D (display:none) to keep the portalled compass + viewport state warm, but the registry layer / per-node InteractiveGeometry / handle layers no longer reconcile on every scene change while invisible. Renders fully in 2D and split; viewport pan/zoom is preserved across the toggle. wall move-endpoint-tool: preview via `useLiveNodeOverrides` instead of writing `useScene.updateNodes` every grid:move tick. The per-tick store write handed a fresh `nodes` ref to every `useScene(s => s.nodes)` subscriber (WallPanel, the contextual HUD, tooltips, floor plan), rebuilding them all each frame. Overrides are merged by the wall system, wall panel, and 2D floor plan, so the preview still tracks live with no store churn; the store is written once on commit, and one Ctrl-Z reverts to the original endpoint. Also swallow the click that follows every endpoint-tool release so it can't fall through to the wall body and arm the wall move tool (no-drag tap or post-commit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b384bc8178 |
perf(editor): skip floorplan viewport sync while the 2D panel is hidden
Camera-zoom hitch: the FloorplanPanel stays mounted (display:none in 3D mode), so its navigation-pose subscriber fired every camera onUpdate. During zoom the view-width changes continuously, so the epsilon guard never short-circuited and `syncFloorplanViewportToNavigationPose` ran each frame → setViewport/ setFloorplanUserRotationDeg → a full re-render of the ~10k-line floorplan SVG, even though nothing is visible (React reconciles display:none subtrees). Gate the viewport sync on `isFloorplanOpen` via a ref the per-frame subscriber reads, and re-run the mount catch-up effect when the panel reopens so the viewport snaps to the current camera. The compass is unaffected — it's portaled to the always-visible viewer area and still receives the pose; only the panel's own viewport sync is skipped while hidden. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bbe5b9a8eb |
fix(placement): wall-item facing via useFacingPose + right-click no longer cancels camera orbit
BUG 1 — wall-attached furniture facing. The coordinator drew its own inline facing triangle and published the wall grid normal from the cursor ghost's own yaw, which is the symmetric-wireframe yaw — π off the item's true facing for a wall (and a different frame for a roof-segment face). The triangle pointed into the wall and the grid normal was sign-flipped while the box still looked right. Fold the coordinator into the unified `useFacingPose` overlay (drop the inline triangle, geometry/material/constants). The per-frame surface publisher now derives the true outward facing from the draft mesh's world orientation (its local +Z faces out of the host surface) for wall/roof-wall and feeds that single yaw to BOTH the grid normal and the facing triangle. Floor/ceiling/item-surface/ shelf paths are unchanged behaviourally — same cursor yaw and Y, just routed through the overlay. Right-click cancel vs camera orbit. The right button also orbits the camera, so the old unconditional `contextmenu → onCancel` cancelled placement on every right-drag. Gate the cancel on pointerup: only when the right press moved ≤4px within ≤200ms (a quick stationary click); a longer/further press is an orbit and is left alone. contextmenu now only suppresses the OS menu. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8cc6c41b51 |
fix(editor): unstick empty-click deselect after click-to-move
Validated live by Wassim. The click-to-move branch set `clickHandledRef = true` then returned early, skipping the 50ms reset the normal select path runs at the end. The flag stayed true, so `onGridClick`'s guard silently blocked every empty-click deselect until the next normal select cleared it. Schedule the reset right after the flag is set so EVERY branch (incl. the early return) clears it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5933a00247 |
fix(editor): placement grid — wall-anchored, perf, first-frame wall seed
Validated live by Wassim. - Wall grid is anchored to the wall PLANE (its foot), not the moving ghost, so sliding a door/window only moves the reveal patch — the lattice stays a fixed snap reference instead of "following" the opening. - DoubleSide so the lattice renders when an opening is handled from the far side. - depthTest is conditional: ON for the floor (the ground occludes a sub-floor lattice) and OFF on a wall (visible through the wall from the opposite side). - Resolution change is a uniform write only — `cellSize` no longer rebuilds the uniform + material (which recompiled the shader and stalled on every step). - Y follow snaps instantly (was a lerp); `gridY` state only updates on change. - Reveal radius 5 → 12. - Door/window publish the wall surface on mount (+ claim the pointer for the wall) so the grid is vertical from the FIRST frame — no horizontal flash. - Export the active-placement-surface module so the opening tools can publish. - Drop the editor-side "Show Grid" setting: the 3D grid is now purely a placement aid (shown only while placing/moving in grid-snap mode). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
67a558a883 |
feat(editor): route door/window facing triangle through the unified overlay
Door and window are placed via preset and moved with a bespoke wall-bound `move-tool` (affordanceTools.move), not the draw tool — so the previous inline triangle never showed for the paths actually used. Migrate both: - move-tool (move + preset, the community path): publish the on-wall ghost pose to `useFacingPose` in the same building-local frame the ghost renders in, dropped to the floor under the wall (the ghost Y is the opening centre); clear on every off-wall / hide / reveal / unmount path - draw tool (standalone from-scratch path): publish the on-host pose, clear on fallback/hide; frame depth read via a ref to keep the setup effect deps clean Removes the now-dead `FacingIndicator` public export (the editor-side overlay is its only consumer, via relative import). The unified overlay now covers every placement/move path: items, column/shelf, stair, and door/window. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
68e5c6ca67 |
feat(editor): unify placement/move facing triangle into one editor-side renderer
Every placement and move path now publishes its ghost pose to a single
`useFacingPose` store, drawn by one editor-side `<FacingPoseIndicator>` overlay,
instead of each path drawing its own triangle (which left the nodes-package and
PlacementBox paths invisible):
- column/shelf presets + all moves (PlacementBox via move-registry, and
DragBoundingBox) now publish the pose, so the triangle finally shows
- stair create + move use a declarative `facingIndicator: { reversed: true }`
(new registry resolver) so the triangle sits before the entry pointing out —
resolved in one place, so create and move match automatically
- stair placement defaults to single and respects the shared `point`
continuation (C) toggle, like the other placement tools
Checkpoint on the placement-interaction epic: also carries the in-flight
continuation-profile extraction (lib/continuation), grid surface (item #8), and
HUD work. Door/window still render their own legacy inline triangle and are
migrated to the overlay next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
61f3921a83 |
Merge pull request #445 from pascalorg/polish/viewer-baking
feat(viewer): walkthrough FOV 60° + refresh ceiling-fan model |
||
|
|
70acf894d1 |
feat(viewer): widen first-person walkthrough FOV to 60°
The walkthrough rode the default 50° orbit camera, which feels cramped on foot. Both walkthrough controllers (baked GlbWalkthroughController and the parametric WalkthroughControls fallback) now set a shared WALKTHROUGH_FOV = 60 on enter and restore the prior FOV on exit, leaving orbit framing untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
69c483b5f2 |
chore(items): refresh ceiling-fan model (On clip + base paint slot)
The bundled ceiling-fan model.glb was stale (no animation clip, two slots). Replace it with the variant matching production storage: an `On` animation clip and a third `slot_base` paint slot. Same dimensions/offset, so no catalog metadata change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7530de5348 |
fix(viewer): bake folding/garage doors at an un-flipped rest pose
`poseDoorMovingParts` assigned a single euler axis (`group.rotation.y` / `.x`). The live system was fine because the group's euler stays a clean (0, y, 0). But the GLB exporter clones the door and decomposes its matrix, which re-derives a gimbal-flipped euler (x=z=π) for any rotation beyond ±90° — folding panels reach ~158°. The reset to t=0 then only zeroed `.y`, leaving the π residue on x/z and baking a 180°-flipped rest pose (panels folded out toward a wrong position even when closed). Set the full euler triple via `.set()` in every pose branch so the other two axes are always zeroed, clearing any decomposed residue. Add a regression test that exports an open folding door and asserts an identity rest pose for all panels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
76089d85ea |
docs(arch): record wall/fence Alt→chain-toggle + altKey-alignment known-legacy
interaction-scope.md + review-architecture skill: the sanctioned Alt-as-toggle (wall/fence chain mode, the one place Alt-as-force is meaningless), and a second known-legacy pattern — `event.altKey` alignment-bypass in the roof/polygon/slab previews + ceiling/slab snap paths (migrate-on-touch; wall+fence already done). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2666b07479 |
feat(editor): wall room/single + fence continuous/single chain toggles
Replace the legacy held-Alt mechanism on wall and fence drafting with a mode toggle, mirroring the snapping-mode chip: - Wall: `wallChainMode` room (auto-close on loop) / single. Room mode finishes automatically when the new endpoint lands within the join-snap radius of the chain's first vertex; single commits one wall per click. - Fence: `fenceChainMode` continuous (chain until double-click/Esc) / single. Fences are linear barriers, so continuous has no auto-close. - Both: Alt-tap cycles the active drafting tool's chain mode (clean-tap, scoped to wall/fence drafting); a clickable HUD chip shows the mode. Persisted + migrated in `useEditor`. Migrate wall and fence off held-Alt-bypass-alignment to the unified convention: alignment now follows the magnetic snap mode, which frees Alt for the toggle. 2D floorplan parity kept in sync with the 3D tools. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4c81435e3b |
fix(doors): correct folding fold direction + make open-clip names unique per node
Two issues surfaced testing the baked viewer:
- Folding door folded toward +z (into the room) — the joint rotation sign
was inverted, so the accordion opened the wrong way ("weird position").
Flip to `(prevDirection - direction) * foldAngle` so leaves fold toward
−z, matching the original inline rig. Verified panel-for-panel against the
original formula at every operationState.
- Openable clips were named by display name (`<name>: open`), but the baked
viewer drives playback by clip name (`useAnimations` maps name → action).
Several windows share the name "Window 1", so their clips collapsed to one
action and triggering any one opened the first. Key the clip name by node
id (`<id>: open`) — unique, matching the item-loop convention; the
human-readable name still lives in `extras.label`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c629171607 |
feat(doors): bake open-animation clips for sliding/garage/folding/pocket/barn doors
Only swing doors (hinged/double/french) baked an open clip into the GLB — they carry a `pascalSwingLeaf` marker the exporter reads. Every operation door type (sliding, pocket, barn, folding, garage-sectional/rollup/tiltup) baked its `operationState` straight into mesh vertex positions at build time, so the exporter had no re-poseable node to sample and the artifact never flagged them `openable`. Give operation doors the same build-once + pose-at-t split windows already use. Each builder now emits its moving parts in a named group at the CLOSED pose, and `poseDoorMovingParts` (the single source of truth, shared by the live system and the GLB exporter) drives the open motion: - sliding/pocket/barn: rigid leaf translation - garage-tiltup: rigid hinge about the lintel - folding: hinged accordion chain (nested groups, per-joint fold) - garage-sectional: per-panel groups posed along the overhead curve - garage-rollup: the one type whose live geometry changes (slats roll onto a drum, which a glTF clip can't express) keeps its full-detail live rebuild; the curtain is wrapped in a top-pivoted group the exporter scales up into the lintel as the baked approximation. The exporter samples each operation door's motion into keyframe tracks (16 segments) so the non-linear rigs (curve, accordion) stay faithful, and stamps `extras.openable` + `extras.clips` so any glTF consumer can play it. Tests: per-type kinematics (groups build, rest closed, open) + sliding/roll-up clip baking (sampled position/scale tracks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6d02ad424e |
docs(editor): correct stale Alt "free place" comment in placement coordinator
The floor grab-offset comment claimed floorStrategy.move reads localPosition "under Alt (free place)"; it reads event.position with mode-governed snapToGrid and has no Alt branch (Alt is force-place-only). Describe the real reason both frames carry the offset: it's computed local-space but the strategy consumes the world point. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f0206dc6eb |
docs(arch): codify the unified snapping/modifier convention + enforce in review
The snapping model (Shift = cycle mode, Alt = force/free, mode-driven reads via isGridSnapActive/isMagneticSnapActive/isAngleSnapActive, snapProfile-declared context) lived only in code and the plan; tools.md still preached the legacy "Shift = bypass snapping". Close the drift so the architecture review refuses tool changes that revert to the old pattern: - tools.md: replace the held-Shift-bypass manipulation policy with the unified mode-driven model + the single snap read path. - interaction-scope.md: new "Snapping mode & modifiers" section (contexts, read path, modifiers, the chip-needs-a-scope rule) + a Rules bullet + the known-legacy MEP movers (migrate-on-touch) incl. the dual-path constraint (a bespoke mover must not open a `moving` scope — it re-mounts the generic mover via useMovingNode). - review-architecture skill: add interaction-scope.md to the reads and a new "F. Interaction scope, snapping & modifiers" checklist — new shiftKey-bypass, ungated grid step, missing snapProfile, a new useEditor interaction flag, or a bespoke mover opening a moving scope are blockers; touching the legacy MEP movers forces migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1de1923e47 |
fix(nodes): raise MEP placement HUD pill clear of the cursor
The MEP run/fitting/terminal tools anchored their cursor readout pill at ~+0.35m above the placement point, so with the tall CursorSphere line (badge at +2.7m) the pill sat right on the cursor and overlapped it, especially when zoomed in. Editor main (#438) already raised the duct pill to +1.45m; this long-lived branch predates that merge. Bring every MEP tool that uses the tall cursor onto the same +1.45m anchor: duct/pipe/liquid/lineset runs, duct/pipe fittings, and duct-terminal. hvac-equipment (height-aware anchor) and pipe-trap (no cursor line) are left as-is — their HUDs already clear the ghost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a3aecf1907 |
Merge pull request #443 from pascalorg/fix/glb-export-missing-material
fix(editor): don't crash GLB export on a material-less renderable |
||
|
|
f75cffed9f |
fix(editor): don't crash GLB export on a material-less renderable
Baking some projects failed with "Cannot read properties of undefined (reading 'isShaderMaterial')". GLTFExporter reads material.isShaderMaterial unconditionally, so a renderable (Mesh / Line / Points) with no material crashes the export — and a non-Mesh renderable slips past both the isMesh prune check and material conversion. Guard it in pruneNonRenderableMeshes: a material-less renderable is dropped if it's a leaf, or neutralised (empty geometry + a hidden placeholder material) if it has children so its subtree survives. Post-FX disable now lets these scenes reach the exporter, which is why it surfaced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0fb3604586 |
perf(editor): move 2D marquee + reference-scale draft out of panel state
The last two hot per-pointer-move 2D states still lived in FloorplanPanel's useState, re-rendering the ~10k-line panel on every move: - marquee (box-select): the whole drag struct moves to a dedicated use-floorplan-marquee store; down/move/up/cancel read+write it via getState() (panel holds nothing), and a FloorplanMarqueeOverlay leaf subscribes to the moving corner and renders the rect alone. Drops the 3 bounds memos + the useState. - reference-scale: the rubber-band's moving end was always equal to the shared cursorPoint (written every move anyway), so drop the `cursor` field from the draft and read it from useFloorplanDraftPreview in a new FloorplanReferenceScaleDraftLine leaf. The draft now carries only the per-click guide + start anchor, so it no longer re-renders the panel. Closes out the 2D edition perf pass — every build/edit/select hot path now writes a store, not panel state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
84cdf4519d |
feat(nodes): mode-aware roof-segment edit + 2D rotation parity with 3D
Roof-segment edit/move ignored the active snap mode and showed no chip: - add snapProfile:'structural' so a body-move resolves the no-angle polygon context (grid/lines/off) like every other structural move; - resize uses getSegmentGridStep() (0 outside grid mode = the "smooth" resize that used to need a held Shift), dropping the captured gridSnapStep + Shift; - move drops its Shift bypass; - the affordance dispatcher opens a boundary reshape scope for the resize so the snapping chip shows and the context resolves. 2D rotation handles now match the 3D gizmo across all six rotate affordances (column / elevator / roof-segment / shelf / spawn / stair): a shared rotateAffordanceDelta snaps to the 15° step unless Shift (free), the dispatcher opens the same ROTATE_HANDLE_DRAG_LABEL handle-drag scope the 3D gizmo uses so the contextual HUD shows the "Shift = rotate freely" hint, and the live degree readout snaps to match the committed rotation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7512fccdaf |
feat(nodes): mode-aware snapping for wall/fence endpoint moves in 2D
Wall and fence endpoint-move affordances hard-snapped to the grid via a hardcoded WALL_GRID_STEP and always ran Figma line-alignment, ignoring the active snapping mode. Now: - grid step follows getSegmentGridStep() (0 outside grid mode), so lines / angles / off no longer force a grid snap the mode chip says is inactive; - Figma alignment is gated on isMagneticSnapActive() (the lines mode); - angles mode angle-locks the endpoint off the fixed corner (free length), mirroring the draft tool; - fence drops its legacy Shift-bypass to match the wall's unified model (Alt stays as linked-segment detach). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a80924e011 |
feat(editor): wire reshape scope for edit-tool chips + 2D-only wall commit
The 2D affordance dispatcher (`startAffordanceDrag`) now begins the matching reshaping interaction scope (boundary / hole / curve / endpoint) on pointer-down and tears it down on release/cancel, matched by node id. This makes the contextual snapping HUD show the right chip during polygon vertex/edge and wall endpoint/curve edits, and lets `getActiveSnapContext()` resolve the correct per-context snapping mode the affordance snap math already reads. Wall creation is owned by the 3D `WallTool`, which is dead in 2D-only view (canvas `display:none`). Mirror the slab/ceiling 2D-only committers: commit locally via `createWallOnCurrentLevel`, gated on `viewMode === '2d'`, chaining the next segment from the committed wall's resolved end. Split/3D keep their single-owner tool commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6f37783fbd |
fix(editor): commit ceiling locally in 2D-only view (3D tool can't)
Same fix as slab: ceiling is committed by its 3D registry tool, dead in 2D-only view. Commit it from the panel on both close paths, gated to viewMode==='2d'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0e778ab526 |
fix(editor): commit slab locally in 2D-only view (3D tool can't)
Slab is committed by its 3D registry tool, which accumulates the grid:click vertices the 2D panel emits and commits on close. That path is dead in 2D-only view — the 3D canvas is display:none, so the tool never commits and the slab is never created (split/3D work because the 3D side is live). Mirror the zone pattern: the panel commits the slab itself on both close paths (double-click + click-first-vertex), gated to viewMode==='2d' so split/3D keep their single-owner tool commit (no double-create). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ac14443fa6 |
fix(editor): polygon vertex/edge edit honors the active snapping mode
The shared polygon-vertex affordance snapped via snapPointToGrid(rawPoint), whose default step is the hardcoded WALL_GRID_STEP (0.5m) — so slab/zone/ceiling vertex, edge, and add-vertex edits always quantized to half-meters regardless of the active mode OR the user's grid-step setting (plan open bugs #1-2). Use the mode-aware getSegmentGridStep() (0 in non-grid modes) so grid quantizes to the live step, lines/off pass through to the wall-snap/alignment resolver. Drop the legacy shiftKey bypass from the slab/ceiling magnetic resolvers (they already gate on isMagneticSnapActive). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
167f0868ef |
perf+fix(editor): wall/fence/roof 2D draft to store+leaf, finish snapping-mode parity
Perf: move the per-move wall/fence/roof draft END points into useFloorplanDraftPreview; a new FloorplanLinearDraftLayer leaf owns the live draft polygon + fence segment + wall measurement, subscribing to the store. The shared FloorplanDraftLayer keeps only the per-click anchors. Wall/fence/roof drafts now have zero per-move panel setState — buttery smooth like slab/zone. Parity: migrate the remaining legacy Shift=bypass paths to the unified mode-driven model. roof (move + click) honored only always-grid + bypassSnap — now grid/lines/off (footprint → no angle). wall + fence click-commit still used the legacy bypass while their move-preview didn't — now consistent. Wall Alt stays 'commit single wall' (open product decision, untouched). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
bc9e075b2d |
fix(editor): 2D slab/zone/ceiling drafting honors the active snapping mode
The polygon-draft snap path used the legacy model: bypassSnap = shiftPressed and angleSnap = pointCount > 0 && !bypassSnap, so the 15deg angle lock engaged after the first vertex regardless of mode — hijacking grid/lines/off into angle-snap even though the HUD chip showed the right mode. Migrate all three placement paths (move preview, single-click vertex, double- click close) to the unified model: angleSnap = isAngleSnapActive(); grid flows through snapToHalf (step 0 in non-grid modes); wall-snap/alignment already gates on isMagneticSnapActive(). Behavior now matches the chip — grid quantizes, angles locks 15deg rays, lines snaps to walls/alignment, off is free. Drop the now-dead bypassSnap param from snapPolygonDraftPoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a769b45756 |
perf(editor): extract 2D draft cursor state to store + leaves — kill per-move panel re-render
The 2D build/edit tools republish the snapped cursor point AND the screen-space coordinate-badge position on every pointer move. Both lived in FloorplanPanel useState, so each move re-rendered the whole ~310ms panel (the badge fires on every pointermove while any build tool is active — the dominant culprit). Move both into a useFloorplanDraftPreview store written via getState() (no panel re-render); render the crosshair + live polygon-draft edge from a FloorplanDraftCursorLayer leaf and the coordinate badge from a FloorplanCursorIndicator leaf, each subscribing to the store. Same store+leaf pattern as the stair build preview. Slab/zone/ceiling drafts now have zero per-move panel setState. Wall/fence/roof draftEnd are follow-up slices. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6d5294d43f |
perf(editor): extract stair 2D build preview to store + leaf — kill per-move panel re-render
The stair tool held its 2D build preview in FloorplanPanel useState, so every grid:move re-rendered the whole ~120-220ms panel. Move the preview into a dedicated useStairBuildPreview store written via getState() (no panel re-render) and render it from a FloorplanStairBuildPreviewLayer leaf that subscribes to the store directly — the same pattern that keeps column/elevator placement smooth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d3c3c0517b |
Merge pull request #441 from pascalorg/feat/baked-glb-export
viewer: baked GLB export + GLB-consuming /viewer (lights, clips, perf) |
||
|
|
b7386f2dcb |
fix(viewer): block body for zone-shape forEach (useIterableCallbackReturn)
Main's biome config flags a callback returning a value; the ternary in the zone-shape forEach implicitly returned moveTo/lineTo. Use a block body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
708fa5278e |
Merge remote-tracking branch 'origin/main' into feat/baked-glb-export
# Conflicts: # packages/editor/src/components/editor/export-manager.tsx |
||
|
|
bbf9291c2d |
feat(editor): roof/stair/elevator snapping migration + no-angle footprint draft
Migrate roof/stair/elevator draft tools + the 2D floorplan move overlay off the legacy Shift/Alt=bypass model onto mode-driven snapping (isGridSnapActive / isMagneticSnapActive); Alt dropped (no validity gate). stair/elevator now use the live grid step. These three are placed as footprints, not directional draws, so the angle-lock mode was meaningless: add NodeDefinition.snapDraftDirectional (default true; false for roof/stair/elevator) so their draft resolves to the no-angle 'polygon' context (grid / lines / off). snapContextOf takes an injected draftDirectionalOf, like profileOf. Add toolHints to stair/elevator so they route through the contextual HUD and show the snapping chip. Fix one stale Alt-bypass comment in the item placement coordinator (#9: already force-only). +snapping-mode test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8e9a45a9d5 |
feat(editor): MEP move-tools — mode-driven snapping (drop Shift=bypass)
The 5 bespoke MEP movers (duct/pipe-segment, liquid-line, lineset, duct-fitting)
now read the active snapping mode (isGridSnapActive / isMagneticSnapActive)
instead of shiftKey=bypass. The moving scope already carries the node
(setMovingNode → begin('moving')), so the per-kind context resolves with no
extra wiring. Grid and alignment are now independent reads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
2fdbcf03c3 |
feat(editor): MEP placement migration — Shift=cycle / mode-driven snapping
Migrate all 9 MEP kinds' placement tools onto the unified snapping model:
declare snapProfile ('item' for point-placed hvac-equipment / duct-terminal /
duct-fitting / pipe-fitting / pipe-trap; 'structural' for directional runs
duct-segment / pipe-segment / liquid-line / lineset), and replace the legacy
shiftKey-bypass reads with mode-driven isGridSnapActive / isMagneticSnapActive /
isAngleSnapActive. For runs the 45° lock becomes the cyclable 'angles' mode;
Alt stays the vertical-riser modifier (run drafting has no validity gate to
force). Port mating gated on "mode != off". Dropped stale "⇧ smooth/free" hints.
The bespoke MEP move-tool/selection (endpoint) tools stay on the legacy model —
they use setMovingNode(null) so no moving-scope context resolves yet; migrating
them needs scope-wiring first (follow-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
76096ffe72 |
fix(editor): door/window move — fix 2D+3D FPS collapse + finish modifier migration
The 3D MoveDoor/MoveWindow tools wrote useScene every frame during a move (freeFollowAt + applyPreview alternating): the wall:move (R3F) / grid:move (DOM) de-dup compared event.timeStamp across two event systems with different clocks, so it never matched and the floor free-follow ran during on-wall slides too, ping-ponging the host and churning the nodes ref → framerate collapse in both 2D and 3D. Replace it with a single-clock wall-ownership window (performance.now, ~4 frames): the floor follow stands down while a wall/roof hit is fresh. On-wall slides now write no scene per frame (mesh + useLiveTransforms only). Lower the live wall-cutout throttle 120→60ms now that the per-frame churn is gone. Also completes the door/window modifier-model migration (#10): Shift=cycle / Alt=force-place, fully mode-driven snap, snapProfile:'item'; exclude ground-line candidates from along-wall opening alignment; emit the move SFX once per snapped step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
636f8ed4f7 |
style(viewer): wrap three import in glb-interactive (formatter)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5938cb6805 |
feat(viewer): strip scans/guides from the bake, re-add from scene data
Scans (LiDAR meshes) and guides (floorplan images) are heavy reference assets stored elsewhere, not part of the compiled building — and baking them into a public static GLB would also bypass the per-project show_*_public flags. Strip both from the export entirely (previously they leaked in as empty identity nodes, timing-dependent). The GLB viewer re-adds them at runtime from the scene graph, like lights: GlbReferenceNodes resolves each scan/guide's registry renderer (no static nodes import — same nodeRegistry path the viewer already uses) and portals it into its parent level's baked node, so the node's level-local transform resolves to the right world pose and rides level stacking. Uses the same GuideRenderer/ ScanRenderer + asset resolver as the parametric viewer, so http-backed assets show for everyone and local asset:// ones for the owner — exact parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
df901b1d4e |
perf(viewer): keep GLB light pool at a fixed visible count (no WebGPU recompiles)
Toggling pointLight.visible changes the active-light count, which makes the WebGPU renderer rebuild every material's lighting node + pipeline — a multi- hundred-ms stall. The pool reassigns on every camera move, so zooming churned visibility and tanked FPS. Keep all 12 pool lights permanently visible and animate only intensity (an idle light lerps to 0); the light-set never changes, so no recompiles. Also make reassignment O(n) (score lookup map, not find). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a5c08b5950 |
feat(viewer): pool GLB item lights + stop overlay click propagation
Match the parametric ItemLightSystem instead of mounting a light per item: a fixed pool of point lights is assigned to the nearest/most-visible lit items each tick (camera-proximity scored, hysteresis, level factor), snapped to each item's world position + offset, and faded on reassignment — so a large house doesn't blow the renderer's light budget. The controls overlay already mounts its <Html> only while the item sits in the focused zone (not hide/show); add stopPropagation on the overlay so toggling a control no longer bubbles to the canvas and deselects the zone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0514ec1858 |
feat(bake): bake catalog item clips (fan spin) into the GLB + play in viewer
A catalog GLB ships its own animation clips (a ceiling fan's spin), but those clips aren't in the editor scene graph, so the bake couldn't see them. Add an `itemClipRegistry` (core, type-only three) that the item renderer fills with the resolved clip per node while the scene is live; the GLB export reads it and re-emits each item's clip onto the baked subtree, rebinding tracks to the cloned spinning node's uuid. Catalog node names repeat across instances and the glTF roundtrip rebinds by name, so the targeted node is uniquified per item (`<id>__lamp_018`) — every fan animates independently. The baked viewer plays these as looping ambient motion: `<id>: loop` clips are set to LoopRepeat (not the door/window LoopOnce) and GlbItemAnimation drives them off each item's toggle (lit/spinning by default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d80ffd67d5 |
feat(viewer): re-light + re-control baked GLBs from the scene graph
Add a GLB interactive layer (`GlbInteractive`) that re-creates the item interactivity the parametric viewer has — point lights and the controls overlay — on top of a baked artifact. Effects + controls come from the DB scene graph (joined to baked nodes by `pascalId`, no sidecar); world transforms come from the baked Object3Ds. Lights are portaled into their item node so they ride level stacking, and intensity tracks the shared `useInteractive` store so overlay dimming works. Baked scenes load "lit" (toggles default on) for a showcase viewer feel. Extract `ControlWidget` into its own module so the parametric `InteractiveSystem` and the GLB overlay render identical controls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0e55d86416 |
fix(viewer): keep ceiling-hosted items visible in dollhouse
Items mounted on a ceiling (lamps, fans, recessed lights) are child nodes of the ceiling, so hiding the whole occluder node hid them too. Hide only the ceiling/roof's own meshes (stop descending at nested identity nodes) so hosted items stay visible when a floor is opened up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8a57105eec |
feat(editor): mode-driven shelf/column/spawn placement + cross-kind floor collision
Migrate the remaining floor-placed kinds onto the unified snapping/modifier
model and generalize floor collision so any solid floor kind blocks any other.
- shelf/column/spawn declare `snapProfile: 'item'` → contextual snapping chip,
Shift=cycle, Ctrl=grid step during placement; their tools read the active
mode (grid/lines/off) instead of legacy Shift/Alt bypass; spawn fresh
placement now respects alignment ("lines") like its move.
- Resize/radial handles claim the handle-drag scope (new RESIZE_HANDLE_DRAG_LABEL)
so the HUD shows no select-mode shortcuts mid-resize.
- Column move migrated to the generic MoveRegistryNodeTool (declare `movable`,
drop the bespoke move-tool) — gains mode-driven snapping, alignment, R/T,
slab lift, grid SFX, and the collision box for free. 2D move still routes
through `floorplanMoveTarget`.
- Cross-kind floor collision: new declarative `FloorPlacedConfig.collides`
(item/shelf/column opt in; spawn/MEP/stair stay off). `canPlaceOnFloor` now
treats every colliding floor kind as an obstacle (was item-only), reading the
declarative footprint; the generic move tool's red/green placement box gates
on `collides`. Column footprint uses the visible `columnFootprintHalf` extent
so the box/slab-lift/collision track the real (round/square) column size.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|