4e11b333bb4190b949ca7b540c0d9d1369415a7e
916
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
c45808840c |
nodes: add modular cabinets and wall trim controls (#461)
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * Add modular cabinet node * Add modular cabinet run and tall cabinet workflows * Add cabinet paint slots and material groups * Improve modular cabinet presets and wall snapping * Improve cabinet wall snapping * Fix cabinet wall snapping and countertop joins * Improve cabinet appliance compartments * Improve fridge visuals and smooth cabinet animations Fridge exterior is now brushed silver with brass accents and the interior is all white. Door/drawer animation no longer rebuilds the full cabinet geometry per frame — animated parts carry pose metadata that a new per-frame cabinet system applies directly to transforms. Co-Authored-By: Claude <noreply@anthropic.com> * Improve gas hob flames and cooktop knob controls Port the curved-flame look from reference gas-burner photos: each burner gets a ring of vertex-coloured tube flames (blue body, orange-yellow tips) whose spines breathe and flicker per frame at ~30fps, plus a flat ignition glow and a faded heat halo. Fix the knob pointer notch to rotate with the knob instead of drifting sideways. Also includes cooktop compartment presets/panels and cabinet selection/move affordance work. Co-Authored-By: Claude <noreply@anthropic.com> * Fix cabinet UVs and registry actions * Fix registry move snap typecheck * Fix cabinet knob animation concurrency * Fix cabinet material disposal, undo flood, and 2D move parity - Flag shared cabinet appliance materials (and the viewer's cached material factories) as cached so geometry rebuilds no longer dispose materials still referenced by other nodes, forcing scene-wide shader recompiles. - Route the door/drawer open animation through useLiveNodeOverrides with a single final commit, so one play is one undo step instead of ~20. - Add a cabinet-module floorplanMoveTarget: 2D drags now convert through planToLocal + magneticSnap like the 3D path, instead of writing plan coords into the run-local position (teleporting modules on rotated runs). - Re-key sibling runs (cabinetAdjacencyRevision) when a run's neighbor-affecting inputs change, so countertop overhang joins re-trim when a neighbor moves/resizes/deletes. - Narrow the cabinet panel's scene subscriptions (useShallow module selector) so it stops re-rendering on every scene mutation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restructure cabinet schema and centralize run math/mutations Prepares the cabinet node for the queued specialty units (corner L-shape, sink base, appliance gap, open shelving): - Add a `moduleKind` discriminator (default 'standard') on CabinetModuleNode so new unit types extend an enum instead of overloading the stack, and split CabinetCompartment into a z.discriminatedUnion so invalid field combos (drawer with rack style, fridge with burner state) are unrepresentable. Shared box fields now come from one `cabinetBoxFields` object so run/module schemas can't drift. - Extract the straight-line run assumption (sort-by-x, edges, adjacency, spans, side-insert, reflow, frame transforms) into run-layout.ts — previously spread across definition/geometry/quick-actions/stack/ move-frame, so corner support would have meant five parallel edits. - Consolidate the run mutations (add module, wall cabinet/hood above, base↔tall switch, layout-revision bump) into run-ops.ts on SceneApi. Panel and quick-actions had drifted copies: the panel's add-module skipped gap checks, revision-bump scope differed per surface. - Remove dead code: node-level doorStyle (with a migrateNodes entry — geometry only ever read per-compartment doorType), the unreachable slot-handle mesh block, the no-op handlePosition 'edge' enum value, wallLocalX, and duplicate totalCabinetHeight definitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add cabinet snap tests and split the cabinet panel - New tests: cabinetModuleParentFrame.magneticSnap (flush snap, threshold, Z alignment, nearest-edge) and wall-snap's resolveCabinetWallFaceOffset / collectCabinetWallSnapNeighbors (straight face, miter taper, ray-miss fallback, yaw/face/parent filtering) — 87 cabinet tests, up from 71. - De-brittle geometry test lookups: coordinate-encoded mesh names are now matched by pattern, so dimension-default changes don't break them. - Split panel.tsx (1,541 → 679 lines): CompartmentCard + option constants into compartment-card.tsx, CabinetRunPanel + reflow wiring into run-panel.tsx, and the compartment type-transition tables into a pure resolveCompartmentTransition in stack-transitions.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split cabinet geometry.ts into per-appliance modules Pure code motion: geometry.ts (4,728 lines) becomes a 336-line orchestrator dispatching into geometry/ — shared helpers + cached appliance materials (shared.ts), CSG fronts/handles/doors/drawers (fronts.ts), run spans + countertop (run.ts), and self-contained hood/fridge/cooktop/dishwasher/ oven-microwave/pantry builders. No mesh names, userData stamps, materials, or math changed; the './geometry' import path is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add undermount sink compartment with CSG-cut countertops New 'sink' compartment type (single / double / 60-40 bowl layouts) following the cooktop pattern: zero stack height, countertop-plane geometry, panel card with a bowl-layout selector, and a Sink Base preset. Bowl openings are subtracted from both module- and run-owned countertop slabs via three-bvh-csg; the basin shells, drain plumbing, and a spec-sheet-proportioned gooseneck faucet (Grohe Minta-style pin lever) render beneath the cut. Sink modules skip the carcass top panel and the deck under the sink row so the basin hangs through. Also scope the viewer mock in roof-surface-placement-guides.test.ts to only stub useViewer — the bare module stub leaked into later suites and broke real CSG imports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Update ifc-converter next-env reference to build-mode route types Regenerated by `next typegen` during check-types; points at .next/types instead of the dev-mode .next/dev/types path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Draw cabinet floor plans with kitchen drafting symbols Replace the per-module box soup with NKBA/architectural-convention symbols: the run draws one heavier countertop outline per span (extended by the overhang), base modules draw their front edge plus compartment symbols — rounded bowl rects + faucet dot for sinks, burner/zone rings for cooktops (reusing the 3D layout tables so 2D and 3D always agree) — and appliance modules carry standard upright labels (DW / REF / OV / MW / PAN). Nested wall cabinets and hood-only modules draw as dashed open outlines per the above-cut-plane convention. * Play cabinet open/close animation on the E interaction key Add an `e` slot to registry KeyboardActions and dispatch it in the keyboard hook ahead of the legacy door/window arms, so kinds opt into the E interaction on their NodeDefinition. Cabinets register it: E on a module eases its doors/drawers open or closed, E on a run swings every child module together, and hood-only modules fall through. The panel's rAF animator moves into cabinet/interaction.ts (live-override frames, single undo commit) so the Play button and E share one animation, and the shortcuts dialog now documents E. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add kitchen island and bar counter support to cabinet runs Islands: run-level countertopBackOverhang (seating side) and withFinishedBack (decorative back panel), plus an I-key island placement mode in the cabinet tool that skips wall snap. Bar counters: optional barLedge {edge, height, depth} drawing a knee wall and raised slab along the back or either run end, superseding the seating overhang on its edge. withWaterfall drops slab-material panels to the floor on exposed run ends. 2D floorplan outlines, selection bounds, geometry keys, and run panel controls updated in parity; schemaVersion 3 -> 6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Merge quick-action arrow and plus into one directional add glyph The add-left/add-right buttons showed two unrelated icons; a single plus-on-tail arrow reads as "add on this side". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Extend plugin discovery instead of replacing the host source Also track the ifc-converter next-env routes reference update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add L-corner runs to modular cabinets with run-scoped grouping An "L Left/Right" quick action spawns a perpendicular base leg, wall leg, and wall bridge as metadata-linked runs parented to the SOURCE RUN, so the run stays the single group: selecting a base module selects only it, and the clicked module never becomes a container. Corner runs re-anchor when the source module resizes or moves (new MovableParentFrame.onCommit hook, mirrored in the 2D floorplan commit), run bounds fold in child leg runs, and deleting one corner member removes only that node while unlink patches keep the survivors' link metadata consistent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Delete emptied cabinet run groups in the same delete gesture Removing a run's last child (module or derived corner leg) left an empty group node in the scene graph and persisted data. onDeleteCascade now receives the gesture's pending delete ids so multi-select deletes count siblings as gone, and both cabinet kinds cascade the orphaned parent run away in one undo step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove filler front gap treatment * Apply architecture review fixes across cabinet, plugin-trees, and framework Move cabinet semantics out of packages/editor behind registry capabilities (def.tree, selectionProxy.bypassDirectPick, IconRef quick-action icons, catalog tool field), route cabinet selection through a core selection-proxy helper, and make PanelWorkspace an opaque host tag. Fix the cabinet placement undo flood, dirtyNodes.add bypasses, normalized UVs on paintable slots, plugin-trees ghost layers/stale level transforms/untyped find-sync event, and cross-file store-mock pollution plus orphaned dist tests that broke combined test runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Propagate front styling to L-corner runs even when re-layout bails Group style changes rode entirely on syncDerivedCornerRun, which bails silently when a later-drawn wall blocks the corner layout or a leg run gained modules outside the derived-run spec — leaving the L legs styled stale. Apply the style patch directly to every linked corner run before attempting the geometric re-layout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Improve editor viewer integration * Refactor plugin panels and floorplan invalidation * UV-unwrap and material-assign roof-segment trim planes * Fix cabinet placement snap feedback * Add cabinet wall snap bypass * Fix cabinet continuous placement flow * Implement wall band material slots * Add corner addition preview functionality and related tests * Fix cabinet and wall band behavior * Fix architecture review blockers * Fix post-merge quality check * fix cursor review feedback * Fix cabinet placement type and quick action history * Improve cabinet placement feedback * Make wall bands configurable * Add wall trim profiles and default materials * Fix architecture review issues * Fix cabinet rotation and wall band visibility * Fix sink appliance paint persistence * Bake cabinet animations and preserve wall band material * Fix cabinet group duplication and module snapping * Fix cabinet duplication architecture * Fix cabinet floorplan test and make placement-type hint a live chip - floorplan.test.ts: pass the required liveTransforms map to cabinetFloorplanSiblingOverrides (the runtime callers always supply it) - ToolHint gains an optional generic `chip` contract (subscribe/value/ cycle/labels) so a kind can render a tool hint as a live mode chip — like the snapping/continuation chips — without the editor knowing the kind's store - cabinet: the I hint now shows the current value (Type: Cabinet / Type: Island) and is clickable to toggle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Wassim SAMAD <wass08@gmail.com> |
||
|
|
7d6879c416 |
Merge pull request #487 from pascalorg/fix/compass-follow-hidden-align-north
fix(editor): compass needle follows align-north in 3D-only view |
||
|
|
cbcc705173 |
fix(editor): animate compass needle on align-north in 3D-only view
When the 2D panel is hidden, align-north publishes a single '2d' pose that the camera applies through the echo-suppressed pending-pose path, so no '3d' poses ever reach the compass subscription and the needle stayed frozen. Drive the needle with a local rAF exponential decay (same 90ms time constant as the 2D view animation and the camera's effective smoothTime); a live '3d' pose cancels it and takes back ownership. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
5831b7b234 |
Merge pull request #479 from pascalorg/feat/smart-unit-suffix
editor: smart unit suffix — dim on focus, yield to explicit units |
||
|
|
2d4e7be709 |
Merge pull request #478 from pascalorg/fix/compass-sync
editor: compass sync + fast align-to-north |
||
|
|
5b4b267a40 |
feat(editor): smart unit suffix — dim on focus, yield to explicit units
While editing a numeric field, the unit suffix now visually responds to the draft content: - Plain number / empty: suffix dims to 40% opacity (signals "default unit if you commit a bare number") - Explicit unit typed (hint showing): suffix fades to opacity-0 over 150ms (the conversion hint provides unit feedback instead) - Unparseable text: suffix stays dimmed (field will revert on commit) - At rest: unchanged full-opacity suffix Uses opacity-0 rather than unmounting so the flex layout doesn't jump. The predicate is simply `hint !== null` — it aligns exactly with measurementHint's internal PLAIN_NUMBER guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
d4f1053528 |
fix(editor): compass sync review fixes
Route all pending-pose cancellations through clearPendingFloorplanNavigationPose() so savedSmoothTime is always restored. Add a controlstart listener to catch right-click orbit and other pointer drags that weren't handled. Guard the rotation-degree ref mirror to prevent React re-renders from clobbering the imperative 3D-owned value when the panel is hidden, and add a useLayoutEffect to keep the needle DOM in sync. Make the catch-up effect re-run directly on panel reopen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cb5cfc7e40 |
fix(editor): compass sync + fast align-to-north
- Remove CSS transition on compass needle SVG so rotation tracks exactly without rubber-band lag (was transition-transform 150ms) - Drive compass needle imperatively via ref when floorplan panel is hidden (3D-only mode), avoiding per-frame React re-renders of the full SVG while keeping the needle live - Align-to-north in 3D-only mode derives from latestNavigationSyncPose instead of stale viewport, publishes directly to the 3D camera - Temporarily lower camera-controls smoothTime from 0.25s to 0.18s during 2D-origin transitions (SmoothDamp τ≈90ms matches the 2D exponential decay), restoring on completion or cancellation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a477c4ade3 |
Merge pull request #477 from pascalorg/fix/unit-labels-lingo-0.2
fix(editor): area readouts + generated names follow the unit toggle; bump lingo to 0.2.0 |
||
|
|
8476a991f4 |
chore(deps): bump @pascal-app/lingo to 0.2.0
0.2.0 is API-compatible with our usage (parseQuantity, quantity, Kind); it adds i18n/locale entrypoints and does not change the symbols we import. Verified: @pascal-app/editor and @pascal-app/mcp typecheck, and all 31 measurement tests pass, including the AMBIGUOUS_NUMBER escalate guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
682c023be1 |
fix(editor): area readouts and generated names follow the unit toggle
The metric/imperial preference was already honored by every length input, but two area surfaces were still hardcoded to m²: - the "Load build" import dialog's Floor area stat - the auto-generated default names for zones, slabs, and ceilings (e.g. "Zone (12.3m²)"), which are live display fallbacks Both now convert value + label to the active unit. Adds shared squareMetersToAreaUnit / getAreaUnitLabel / formatAreaLabel helpers to lib/measurements (with tests) and routes the site-panel Area readout through them, replacing an inline 10.7639 magic constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
2cfce3f6b5 |
Merge pull request #472 from pascalorg/feat/lingo-natural-inputs
feat: natural-language measurement inputs (UI + MCP tools) via @pascal-app/lingo |
||
|
|
5ffde5f66a |
fix(editor): site-panel property-line coords + area/perimeter follow the unit toggle
The property-line vertex X/Z inputs and the Area/Perimeter readout were hardcoded to meters. Convert them to feet / ft² when the viewer preference is imperial, matching every other length input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
69813a6e62 |
feat(editor): honor the metric/imperial toggle in every length input
SliderControl (which drives the auto-inspector's ~144 fields and most custom panels) previously ignored the viewer unit preference and always rendered raw meters with a static "m" label, while MetricControl converted to feet — so toggling imperial changed some length inputs and not others. Extract the conversion into a shared `useLinearDisplay(unit, precision)` hook and route BOTH controls through it, so every `unit="m"` length field displays and edits in feet when imperial and meters when metric — consistently. Values are always stored in meters; only display, the text field, and drag/ wheel/arrow deltas move to the display unit. For metric and non-length units (°, %, in, …) the conversions are the identity, so those paths are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
af1fe6a34c |
chore(deps): add @pascal-app/lingo to the lockfile; note duplicated helper
Records @pascal-app/lingo@^0.1.0 in bun.lock (added to @pascal-app/editor and @pascal-app/mcp), so `bun install --frozen-lockfile` in CI resolves it. Also documents that measurement.ts is intentionally duplicated with the AI-chat copy across the submodule boundary and must be kept in sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3273aac551 |
fix(mcp): restore >0 validation and reject ambiguous numbers in measurement()
Adversarial-review follow-ups to the lingo measurement() field:
- Add a `positive` option (strict > 0) and use it for the non-zero dimension
params. Swapping z.number().positive() → measurement(..,{min:0}) had started
admitting 0 (the core node schemas have no positivity backstop), so a zero-size
wall/opening/roof could be created. Inclusive-0 fields (overhang, sill height,
knee-wall height, opening offset, roof pitch) keep min:0.
- Escalate AMBIGUOUS_NUMBER to error so "1,234" fails instead of silently reading
as 1234 — a 1000x hazard for European decimals. Matches lingo's own /ai fields.
- roofLevelElevation reverted to z.number() (it's a level ordinal, not meters);
radians fields now advertise radian-appropriate examples.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f66dcd438b |
feat(mcp): natural-language measurements for tool inputs via @pascal-app/lingo
Measurement/angle parameters on the scene tools now accept a bare number OR a
natural-language string ("6 in", "180cm", "45°", "1.57rad"), canonicalized to
the unit the handler already expects (meters for length, radians/degrees for
angles) with min/max bounds and model-readable errors. Makes LLM tool calls
safer — "6 in" no longer has to be pre-converted to 0.1524, and a bad value is
rejected with a message the model can self-correct from.
A shared `measurement(kind, unit)` zod field wraps lingo's parseQuantity as a
`number | string` union+transform; numbers pass through unchanged (backward
compatible), so no handler changes were needed. Covers create_wall, cut_opening,
place_item, create_level, create_story_shell, create_roof,
create_stair_between_levels, create_room, add_door, add_window, photo_to_scene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a5963560b1 |
feat(editor): natural-language measurement inputs via @pascal-app/lingo
Type any unit into SliderControl / MetricControl fields — "6ft", "180cm", "1m80", "5'11\"", "72in", "45°", "1.57rad" — and it canonicalizes to the unit the field already stores (meters / degrees / radians / inches), independent of the metric/imperial display toggle. A live "= 1.83 m" hint previews the parsed value while typing. Only the typed-text commit path changes: drag-scrub, wheel, and arrow-key editing are untouched, and any unit the adapter doesn't recognise (%, unitless, rad/s) falls back to Number.parseFloat, so nothing regresses. The shared lib/measurement-parser.ts maps each field's `unit` prop to a lingo kind + canonical unit, covering the auto-inspector and every custom panel at once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
bf25af6add |
viewer: Fix Dutch roof trim artifacts (#452)
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * Fix spiral stair openings and fence handle arrows * Implement roof trim planes and ridge vent clipping * Fix mansard roof and ridge vent placement * Fix mansard merged roof cutouts * Fix Dutch roof gable overhang * Refactor roof segment, ridge vent, and surface geometry Remove Dutch ridge axis abstraction and rework roof edit system, ridge vent clipping geometry, and roof surface placement. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Simplify Dutch roof shape * Add Dutch roof gable top geometry controls * Fix Dutch roof slope material slots * Render dutch roof tops as double-sided faces * Add auto ridge vent toggle to roof segments Track ridge vent auto-generation via an `autoRidgeVent` metadata flag so geometry changes only regenerate default vents when enabled, treating legacy segments with generated vents as auto-enabled for back-compat. Expose a panel toggle to opt in/out per segment. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Snap new walls to the floor below Feed the walls of the level directly beneath the active one into the draft snap pipeline as extra references, so a new wall can align with the floor below. They share the same local XZ origin, and the list is kept separate from the current-level walls so the measurement HUD and wall splitting only act on the active level. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Set Dutch roof shape defaults on type switch Seed the Dutch shape parameters (waist width/height/length, top rake thickness/length) with sensible defaults whenever a segment is created as or switched to Dutch, so the gablet is well-formed regardless of leftover values from the previous roof type. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use green accent for corner and endpoint snap markers Color the corner/endpoint snap markers and the vertical cursor pillar green across the 2D floorplan beacon, the 3D alignment guide dots, and the wall snap beacon so snap targets read as a consistent accent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add magnetic wall snapping to the roof tool Snap roof draft corners onto wall corners, midpoints, crossings, and bodies on the active level and the floor below, reusing the wall tool's snap pipeline so the beacon and coloring match. The cursor's ground dot/ring is hidden while a wall snap is active to avoid overlapping the beacon glyph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Update auto-generated Next.js route types path Regenerated next-env.d.ts now references ./.next/dev/types/routes.d.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Show cutaway outline while dragging roof trim Slice an untrimmed segment volume generated from the live node instead of the registry mesh, whose CSG rebuild lags a few frames behind the drag and may still hold placeholder geometry — so the section outline now renders deterministically. Use LineBasicNodeMaterial so the outline draws under the WebGPU pipeline, and export generateRoofSegmentGeometry for the slice source. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fill and clip roof trim cutaway, gate it to active drag Add a violet silhouette fill behind the cutaway outline, extend the section slicing to angled diagonal/corner trims via a generic vertical cut plane, and clip each slice to its footprint span so the infinite plane no longer sprouts stray lines across the rest of the roof. The cutaway now renders only while a trim handle is being dragged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Separate and extend Dutch roof end slopes Pull the Dutch hip end slopes out of the watertight shingle shell into their own slab wedge so they can be reshaped independently, and extend each end slope inward up its own hip plane until the top edge meets the gablet's inner triangle. Refactor roof-segment shape geometry into a shared roof-segment-shape module. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Render roof trim cutaway as a material-only section cut Replace the triangle-mesh slicer with a CSG intersection of a thin slab against the untrimmed roof shell, so the cutaway shows red only on real material (wall + deck bands) and leaves the hollow attic empty. Add an analytic surface-edge outline, style both solid red like a SketchUp section, and make the cutaway persist whenever a segment is trimmed. Keep the merged roof shell visible during trim editing (re-trimmed live from each segment's drag override) instead of swapping in the per-segment meshes, whose abutting end-cap faces showed as stray white planes the commit never had. Extend each slab past free cut-line ends only — trimmed ends clamp to the cut line — so the red section stays inside the trim box. Re-export INTERSECTION from the viewer CSG surface for the editor. * Outline roof cutaway by fill silhouette, restyle to destructive red Derive the section-cut outline from the fill geometry's edges (EdgesGeometry) so it traces the real cut shape — wall/deck band boundaries and the hollow-attic edge — instead of just the top surface line. Drop the fill to 85% opacity and recolor both fill and outline to the app's destructive red, matching the delete/destructive UI. * Include roof accessories in trim clipping and red cutaway Roof accessories (chimney, vents, skylight, dormer, gutter, downspout, solar-panel, cupola) now slice at the trim plane like the roof shell and appear in the red section-cut while dragging a trim handle: - Export clipGeometryBySegmentTrim from the viewer as a reusable segment-local trim-clip primitive. - Add a shared useSegmentTrimClippedGeometry hook + TrimClippedMesh wrapper (nodes) that slice accessory geometry by the host segment's live trim override, so the cut tracks the drag. - Wire the clip into all 11 accessory renderers, including skylight glass panes and dormer window glass/frame/sill. - Feed every hosted accessory mesh into the editor's red cutaway, welding triangle-soup geometry (e.g. ridge vent) so CSG INTERSECTION yields a cross-section. - Register skylight in the scene-graph tree-node map so it shows in the outliner when placed on a roof. Co-Authored-By: Claude <noreply@anthropic.com> * Add smooth spline fences with editable curve handles Fences can now be drawn as one continuous Catmull-Rom/Bezier curve via an optional `path` (+ per-point `tangents`), selectable in a Straight/Curved mode toggle. Selected spline fences expose draggable control-point dots (hexagon) and symmetric tangent handles (circle) joined by a violet line, editable in both 2D plan and 3D. Side-move arrows are dropped for splines. Co-Authored-By: Claude <noreply@anthropic.com> * Fix dutch roof ridge vent handling * Fix Dutch ridge vent placement and support * Fix Dutch roof trim artifacts * Fix Dutch roof trim preview geometry * Tag roof trim overlay meshes with EDITOR_LAYER Child meshes relied on a parent group's layer, which three.js does not propagate, so the trim section/rail/plane overlays rendered on the scene layer — getting inked/SSGI-darkened and leaking into thumbnail exports. Co-Authored-By: Claude <noreply@anthropic.com> * Apply Biome cleanup * fix(core): address Dutch roof review feedback * chore: apply biome check cleanup * fix(core): relax Dutch roof surface helper input * fix * Fix biome checks and dev verification * fixes * Remove unsupported Biome noShadow override * Improve roof interactions and fence editing * Fix fence drag and ridge vent default handling * editor: drop wall-snap debug log, gate curved-fence finish hint on draft start Remove the leftover TEMP DIAGNOSTIC console.log in the wall tool's onMove hot path. Curved fences commit on a closing gesture (double-click / Enter) rather than per-click, so surface a 'Finish curve' hint in the fence HUD — but only once a point has been placed and a curve is actually in flight. The draft point count is published from SplineFenceDraft into a small ephemeral editor store (useFenceCurveDraft) that the contextual helper reads, mirroring the existing useSegmentDraftChain pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Wassim SAMAD <wass08@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 | ||
|
|
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> |
||
|
|
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> |