Commit Graph
1136 Commits
Author SHA1 Message Date
Wassim SAMADandClaude Opus 4.8 76096ffe72 fix(editor): door/window move — fix 2D+3D FPS collapse + finish modifier migration
The 3D MoveDoor/MoveWindow tools wrote useScene every frame during a move
(freeFollowAt + applyPreview alternating): the wall:move (R3F) / grid:move
(DOM) de-dup compared event.timeStamp across two event systems with different
clocks, so it never matched and the floor free-follow ran during on-wall
slides too, ping-ponging the host and churning the nodes ref → framerate
collapse in both 2D and 3D. Replace it with a single-clock wall-ownership
window (performance.now, ~4 frames): the floor follow stands down while a
wall/roof hit is fresh. On-wall slides now write no scene per frame (mesh +
useLiveTransforms only). Lower the live wall-cutout throttle 120→60ms now that
the per-frame churn is gone.

Also completes the door/window modifier-model migration (#10): Shift=cycle /
Alt=force-place, fully mode-driven snap, snapProfile:'item'; exclude
ground-line candidates from along-wall opening alignment; emit the move SFX
once per snapped step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:21:37 -04:00
Wassim SAMADandClaude Opus 4.8 636f8ed4f7 style(viewer): wrap three import in glb-interactive (formatter)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:01:13 -04:00
Wassim SAMADandClaude Opus 4.8 5938cb6805 feat(viewer): strip scans/guides from the bake, re-add from scene data
Scans (LiDAR meshes) and guides (floorplan images) are heavy reference assets
stored elsewhere, not part of the compiled building — and baking them into a
public static GLB would also bypass the per-project show_*_public flags. Strip
both from the export entirely (previously they leaked in as empty identity
nodes, timing-dependent).

The GLB viewer re-adds them at runtime from the scene graph, like lights:
GlbReferenceNodes resolves each scan/guide's registry renderer (no static nodes
import — same nodeRegistry path the viewer already uses) and portals it into its
parent level's baked node, so the node's level-local transform resolves to the
right world pose and rides level stacking. Uses the same GuideRenderer/
ScanRenderer + asset resolver as the parametric viewer, so http-backed assets
show for everyone and local asset:// ones for the owner — exact parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 08:36:02 -04:00
Wassim SAMADandClaude Opus 4.8 df901b1d4e perf(viewer): keep GLB light pool at a fixed visible count (no WebGPU recompiles)
Toggling pointLight.visible changes the active-light count, which makes the
WebGPU renderer rebuild every material's lighting node + pipeline — a multi-
hundred-ms stall. The pool reassigns on every camera move, so zooming churned
visibility and tanked FPS. Keep all 12 pool lights permanently visible and
animate only intensity (an idle light lerps to 0); the light-set never changes,
so no recompiles. Also make reassignment O(n) (score lookup map, not find).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:46:17 -04:00
Wassim SAMADandClaude Opus 4.8 a5c08b5950 feat(viewer): pool GLB item lights + stop overlay click propagation
Match the parametric ItemLightSystem instead of mounting a light per item:
a fixed pool of point lights is assigned to the nearest/most-visible lit
items each tick (camera-proximity scored, hysteresis, level factor), snapped
to each item's world position + offset, and faded on reassignment — so a large
house doesn't blow the renderer's light budget. The controls overlay already
mounts its <Html> only while the item sits in the focused zone (not hide/show);
add stopPropagation on the overlay so toggling a control no longer bubbles to
the canvas and deselects the zone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:34:41 -04:00
Wassim SAMADandClaude Opus 4.8 0514ec1858 feat(bake): bake catalog item clips (fan spin) into the GLB + play in viewer
A catalog GLB ships its own animation clips (a ceiling fan's spin), but those
clips aren't in the editor scene graph, so the bake couldn't see them. Add an
`itemClipRegistry` (core, type-only three) that the item renderer fills with the
resolved clip per node while the scene is live; the GLB export reads it and
re-emits each item's clip onto the baked subtree, rebinding tracks to the cloned
spinning node's uuid. Catalog node names repeat across instances and the glTF
roundtrip rebinds by name, so the targeted node is uniquified per item
(`<id>__lamp_018`) — every fan animates independently.

The baked viewer plays these as looping ambient motion: `<id>: loop` clips are
set to LoopRepeat (not the door/window LoopOnce) and GlbItemAnimation drives
them off each item's toggle (lit/spinning by default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:13:48 -04:00
Wassim SAMADandClaude Opus 4.8 d80ffd67d5 feat(viewer): re-light + re-control baked GLBs from the scene graph
Add a GLB interactive layer (`GlbInteractive`) that re-creates the item
interactivity the parametric viewer has — point lights and the controls
overlay — on top of a baked artifact. Effects + controls come from the
DB scene graph (joined to baked nodes by `pascalId`, no sidecar); world
transforms come from the baked Object3Ds. Lights are portaled into their
item node so they ride level stacking, and intensity tracks the shared
`useInteractive` store so overlay dimming works. Baked scenes load "lit"
(toggles default on) for a showcase viewer feel.

Extract `ControlWidget` into its own module so the parametric
`InteractiveSystem` and the GLB overlay render identical controls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:47:07 -04:00
Wassim SAMADandClaude Opus 4.8 0e55d86416 fix(viewer): keep ceiling-hosted items visible in dollhouse
Items mounted on a ceiling (lamps, fans, recessed lights) are child nodes of
the ceiling, so hiding the whole occluder node hid them too. Hide only the
ceiling/roof's own meshes (stop descending at nested identity nodes) so hosted
items stay visible when a floor is opened up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:51:00 -04:00
Wassim SAMADandClaude Opus 4.8 8a57105eec feat(editor): mode-driven shelf/column/spawn placement + cross-kind floor collision
Migrate the remaining floor-placed kinds onto the unified snapping/modifier
model and generalize floor collision so any solid floor kind blocks any other.

- shelf/column/spawn declare `snapProfile: 'item'` → contextual snapping chip,
  Shift=cycle, Ctrl=grid step during placement; their tools read the active
  mode (grid/lines/off) instead of legacy Shift/Alt bypass; spawn fresh
  placement now respects alignment ("lines") like its move.
- Resize/radial handles claim the handle-drag scope (new RESIZE_HANDLE_DRAG_LABEL)
  so the HUD shows no select-mode shortcuts mid-resize.
- Column move migrated to the generic MoveRegistryNodeTool (declare `movable`,
  drop the bespoke move-tool) — gains mode-driven snapping, alignment, R/T,
  slab lift, grid SFX, and the collision box for free. 2D move still routes
  through `floorplanMoveTarget`.
- Cross-kind floor collision: new declarative `FloorPlacedConfig.collides`
  (item/shelf/column opt in; spawn/MEP/stair stay off). `canPlaceOnFloor` now
  treats every colliding floor kind as an obstacle (was item-only), reading the
  declarative footprint; the generic move tool's red/green placement box gates
  on `collides`. Column footprint uses the visible `columnFootprintHalf` extent
  so the box/slab-lift/collision track the real (round/square) column size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:22:04 -04:00
Cocoon-BreakandGitHub af0276a0de docs: fix stale folder name editor-v2 and add missing ui package (Closes #244) (#245) 2026-06-24 16:01:04 -04:00
f69b1935a9 docs: fix editor-v2 stale folder name in apps/editor/README.md (#311)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-24 16:00:59 -04:00
Thiago LöpesandGitHub afe14a8cc7 Fix: Restore floor drag/delete functionality for imported legacy JSON… (#413)
* Fix: Restore floor drag/delete functionality for imported legacy JSON files

Summary

Fixed an issue where floors became impossible to drag or delete after importing JSON configuration files created in versions prior to 0.9.1.

Root Cause

The migration process was executing elevator parent migration before all level nodes had their children fully normalized. As a result, imported scenes could end up with inconsistent parent-child relationships, causing floor management operations such as dragging and deletion to fail.

Changes Made
Refactored migrateNodes() into a two-pass migration process.
Added normalization for level nodes:
Ensures level values are valid finite numbers.
Removes references to missing child nodes.
Preserves only valid children during migration.
Moved elevator migration logic to a dedicated second pass:
Elevator parent migration now runs only after all level.children relationships have been stabilized.
Prevents invalid hierarchy reconstruction when importing legacy JSON files.
Result

Imported layouts from versions prior to 0.9.1 now correctly preserve floor hierarchy, allowing floors to be dragged, reordered, and deleted as expected.

* Update use-scene.ts
2026-06-24 16:00:43 -04:00
088f6c50a4 fix(editor): decompress compressed textures on GLB export (#435)
three r184's GLTFExporter throws 'setTextureUtils() must be called' when a
scene contains KTX2/basis-compressed textures. Wire up WebGPUTextureUtils
(matching the app's WebGPURenderer) so compressed textures are decompressed
during GLB export.

decompress() is called without the live renderer on purpose: it resizes
whatever renderer it is given and never restores it, which would corrupt the
visible canvas. Omitting it lets three create and dispose its own throwaway
renderer for the blit.

Reworked from #435 (anton-pascal); switches WebGL->WebGPU utils and avoids the
live-renderer resize gotcha surfaced in review.

Co-authored-by: Open Pascal <open@pascal.app>
2026-06-24 15:56:30 -04:00
5fe0965297 fix(viewer): graceful fallback when WebGPU/WebGL is unavailable (#403)
Adds a capability check and an UnsupportedGpuViewerFallback so the viewer
renders an informative panel instead of crashing on environments that expose
neither WebGPU nor WebGL. Capability detection runs post-mount to stay
hydration-safe, the renderer-init failure path swaps to the fallback, and the
fallback signals scene-readiness so the host editor loader does not hang.

Reworked from #403 (anton-pascal) onto current main; resolves the import
conflict and the SSR/scene-ready gotchas surfaced in review.

Co-authored-by: Open Pascal <open@pascal.app>
2026-06-24 15:56:26 -04:00
8593c55afe fix(nodes): guard previewRef in fence tool onGridClick (Sentry MONOREPO-EDITOR-BE/CR) (#434)
Co-authored-by: openclaw-agent <agent@pascal.app>
2026-06-24 15:52:25 -04:00
05771270e3 fix(core): coerce unknown material.preset to 'custom' (Sentry MONOREPO-EDITOR-DB) (#400)
Co-authored-by: openclaw-agent <agent@pascal.app>
2026-06-24 15:52:22 -04:00
Wassim SAMADandClaude Opus 4.8 5aedc366a6 feat(viewer): GLB walkthrough in viewer, monochrome, spawn/export polish
Move the first-person walkthrough into @pascal-app/viewer (BVHEcctrl +
GlbWalkthroughController) and round out the GLB-consuming viewer:

- Walkthrough: fallback ground only on level 0 (upper floors rely on baked
  slabs), hidden spawn marker, auto pointer-lock on enter, single-Esc exit via
  pointerlockchange, force perspective on enter / restore on exit.
- GlbScene: monochrome strips baked textures and recolours meshes by surface
  role using the active theme's clay tints; spawn node hidden from render.
- glb-export: camera/label/spawn identity extras for the viewer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:50:28 -04:00
Wassim SAMADandClaude Opus 4.8 04f1b0d59e feat(editor): node-declared per-context snapping + contextual HUD + painter scope
Generify the snapping/modifier HUD off the FSM scope and node declarations
instead of wall-creation-shaped, leaking pills.

- Per-context snapping (`snappingModeByContext`, persisted): wall / item /
  polygon mode-sets with exclusive modes (grid | lines | angles | off), each
  doing exactly what its chip says. Context is node-declared via the new
  `NodeDefinition.snapProfile` ('item' | 'structural'); the resolver maps
  (profile × action) → context with no per-kind switch.
- Scope-driven HUD: helper-manager reads the interaction scope; reshaping
  (endpoint/curve/boundary) and item move get their own chip, no select-hint
  leak. Rotate R/T rounds to 45°; Alt = force-place only (hidden for
  structural kinds); Shift = cycle everywhere.
- Slab/ceiling drafting: Shift=cycle, mode-aware grid/angle, Enter finishes
  (minDraftVertices); polygon boundary vertex/edge drag begins a reshaping
  scope. Fix grid/angle being ignored on boundary edit + slab creation:
  make resolveSurfacePlanPointSnap exclusive (alignment gated on magnetic) so
  grid/angles keep the snapped fallback instead of the raw cursor.
- Painter application scope: node-derived (single/object/matching/room) from
  the hovered node, cyclable via Shift, single-source HUD chip.
- Remove the redundant GridSnapControl from view-toggles (grid step lives in
  the contextual HUD now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:23:15 -04:00
Wassim SAMADandClaude Opus 4.8 b8b3d35f26 refactor(editor): delete the movingNode legacy flag — node lives in the scope
The 7th and last of the legacy interaction flags. The node being placed/moved
now lives inside the interaction scope's `placing`/`moving` variant (carried
inline, since fresh-placement/duplicate drafts aren't in the scene yet), read via
`useMovingNode()` / `getMovingNode()` / `movingNodeOf(scope)`.

- scope.ts: `placing`/`moving` carry `node: AnyNode`; add `movingNodeOf`.
- use-interaction-scope.ts: `useMovingNode` (hook) + `getMovingNode` (imperative);
  no useRef snapshot needed — the node is set once at `begin`, stable for the gesture.
- use-editor.tsx: drop the `movingNode` field + the `set({ movingNode })` writes.
  `setMovingNode` still drives the scope and still sets `movingNodeOrigin` /
  `placementDragMode`, so cross-store subscribers keep firing. Param + ~90 call
  sites unchanged.
- migrate ~17 reader sites to `useMovingNode()` / `getMovingNode()`; drop
  `movingNode` from lib/scene.ts; export `movingNodeOf`.

Every interaction flag is now derived from the single authoritative scope; only
`movingNodeOrigin` + `placementDragMode` intentionally remain as useEditor flags
(they outlive the scope / gate companion behavior).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:24:02 -04:00
Wassim SAMADandClaude Opus 4.8 6cfc6ae01b fix(export): stamp openable only when an open clip bakes
A door/window only carries extras.openable + extras.clips when an open
animation actually bakes. Cased openings (no leaf) and fixed windows (no
operable sash) are no longer mislabelled openable, so the GLB never claims
a part opens when nothing moves. /viewer is unaffected (it already required
openable && clips). Adds a regression test for the no-clip case.

(Export barrels reordered by the formatter.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:44:36 -04:00
Wassim SAMADandClaude Opus 4.8 353b429a01 perf(floorplan): override-driven 3D wall move + granular sibling invalidation
Two fixes for the split-view FPS cliff when moving a wall or opening:

- wall/move-tool: publish the live preview to useLiveNodeOverrides instead of
  writing useScene.updateNodes every frame. The store's `nodes` ref no longer
  churns per frame (which had re-rendered every useScene(s => s.nodes) subscriber
  app-wide). Matches the existing 2D wall drag + 3D wall-system override pattern;
  the final plan still commits atomically as one undoable change.

- floorplan-registry-layer: replace the single global siblingEpoch with a
  per-node epoch bumped only for the nodes affected by the live drag (dragged
  wall -> walls at its old + new junctions + child openings; door/window -> host
  wall; gutter -> roof-peer gutters), unioned with the previous frame's live set
  so a cancelled drag reverts. Dragging one wall/opening now rebuilds a handful
  of geometries instead of all the level's walls + openings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:24:06 -04:00
Wassim SAMADandClaude Opus 4.8 0629c7d3e3 fix(export): snap levels to stacked positions before GLB export
exportSceneToGlb now calls snapLevelsToTruePositions() (the same clean stacked
presentation thumbnail capture uses) before snapshotting the scene, so the GLB
always reflects the stacked building regardless of the live levelMode
(exploded/solo) or an unsettled level lerp. Fixes baked GLBs where a level was
captured at a stray offset (e.g. ~ -100k Y), which inflated the bounding box and
made the model unframable / invisible in third-party glTF viewers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:06:20 -04:00
Wassim SAMADandClaude Opus 4.8 c8bd29e909 revert(viewer): drop distance-based LOD switching from GLB scene
Reverts the LOD switching (0469b256). Rendering lod1/lod2 on zoom-out produced
WebGPU validation errors (invalid bindGroup_object) on real-GPU browsers — not
caught earlier because headless Chromium falls back to the WebGL2 backend, so
the WebGPU path was never exercised. The viewer returns to loading the baked
KTX2 lod0 (still the 22MB->12MB win). LOD switching to revisit with a real-GPU
test loop and on-demand loading instead of preload-all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:38:40 -04:00
Wassim SAMADandClaude Opus 4.8 0469b25639 feat(viewer): distance-based LOD switching in GLB scene
GlbScene accepts an optional lodUrls list and swaps rendered detail by camera
distance to the building's world-space center: lod0 (interactive — identity,
selection, zones, clips all bind here) for normal + building-view distances,
simpler visual-only levels only past ~2.5R / ~8R. Single-URL callers are
unchanged. useGLTFKTX2 now accepts a URL array.

Manual visibility toggling rather than THREE.LOD: THREE.LOD measures distance
to its own origin (0,0,0), which mis-selected levels (blanking the scene) when
the baked geometry is offset from the origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:10:01 -04:00
Wassim SAMADandClaude Opus 4.8 cfc1fb1672 refactor(editor): delete curving + endpoint reshaping flags
Migrate `curvingWall`, `curvingFence`, `movingWallEndpoint`, `movingFenceEndpoint`
(and their setters) off `useEditor` onto the authoritative interaction scope.
The `reshaping` scope variant gains an `endpoint` discriminator; existence
checks read `useIsCurveReshape()` / `useEndpointReshape()`, and the few sites
that need the node (affordance-tool mounts, wall-vs-fence type checks) read it
from `useReshapingNode()` — a frozen drag-start snapshot, mirroring the old
flags so the tools' own per-frame writes don't feed back. `MovingWallEndpoint`
/ `MovingFenceEndpoint` move to the kind-owned tools that consume them.

`editor-api` is simpler: endpoint engagement is kind-agnostic, and the
`engageMove` reshape clears are gone (the scope is single-owner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:18:50 -04:00
Wassim SAMADandClaude Opus 4.8 2f2c3ef8f9 refactor(editor): delete editingHole + activeHandleDrag legacy flags
Migrate the two pure-mirror interaction flags off `useEditor` onto the
authoritative `useInteractionScope`. Both carried payloads byte-identical to
the scope union, so this is a zero-behaviour-change refactor: readers use the
reference-stable `useEditingHole()` / `useActiveHandleDrag()` hooks (or the
`getEditingHole()` imperative read), and producers drive the scope directly
with guarded `endIf` so clearing one interaction never stomps an unrelated
scope. Adds the `holeEditScope` builder + a no-leaked-flag invariant test.

Closes the first slice of the legacy-flag deletion; the rich-payload flags
(movingNode, curving*, *Endpoint, placementDragMode) remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:30:16 -04:00
Wassim SAMADandClaude Opus 4.8 064be98d7f feat(export): reusable exportSceneToGlb + BakeExporter for headless bake
Extract the GLB build (prepare + GLTFExporter + WebGPUTextureUtils) out of
ExportManager into exportSceneToGlb so it can run outside the editor download
flow. Add a BakeExporter R3F component that fires the export once a host viewer
signals scene-ready and hands back the ArrayBuffer. Both exported for the
headless bake route (baked-glb-export phase 3, headless de-risk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:52:14 -04:00
Wassim SAMADandClaude Opus 4.8 0a7cbd2081 feat(viewer): GLB-consuming /viewer path (baked-glb-export phase 2)
Add `GlbScene` — the viewer that renders a baked GLB artifact with no
parametric scene graph, and finish the phase-1 export contract it consumes.

Viewer (packages/viewer):
- GlbScene: loads the artifact, drives a building → level → zone → node drill
  hierarchy on useViewer.selection. Room/zone picking resolves from the floor
  plane (ray ∩ floor + point-in-polygon), node picking uses a footprint test,
  so walls/ceilings/zone-helpers never skew or block selection. Level modes
  (stacked/exploded/solo), dollhouse (hide a focused floor's ceilings + roof so
  rooms are visible and pickable), reconstructed zone floor fills + gradient
  edge borders + room labels (faded, hover-brightened), baked door/window
  open clips, click-outside / empty-space deselect, and the shared outline
  post-FX. Exported as GlbScene/GlbLevel/GlbIdentity/GlbHover.
- post-processing: composite the zone-pass tint into the base scene so rooms
  show whether or not SSGI is enabled (previously only added in the SSGI branch).

Export contract (packages/editor/src/lib/glb-export.ts):
- Convert WebGPU NodeMaterials to classic glTF-standard materials; decompress
  KTX2 maps via WebGPUTextureUtils so GLTFExporter can embed them.
- Stamp name + extras identity (pascalId/kind/label/openable/clips), bake
  door/window open clips (loop:false), strip editor overlays (off-layer +
  invisible-material hitboxes) so cutouts aren't plugged.
- Fix opaque-but-flagged-transparent materials (no spurious alphaMode=BLEND)
  and BackSide → FrontSide + winding flip (glTF has no back-face-only).
- Force levels + zones visible and stamp level display names + zone polygons so
  every floor bakes and /viewer can reconstruct rooms and label the breadcrumb.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:14:39 -04:00
Wassim SAMADandClaude Opus 4.8 f773e6b8c5 feat(editor): placement & interaction overhaul — FSM spine, bug tracks, perf
Implements plans/editor-placement-interaction-overhaul.md: an authoritative
interaction-scope state machine plus the catalogued placement/interaction
fixes, and split-view floor-plan performance.

- Interaction-scope spine (lib/interaction/* + store/use-interaction-scope),
  driven from central useEditor setters; overlay scoping (zone labels,
  context badges, floating action menu) reads resolveOverlayPolicy.
- Bug tracks A/B/D/E/F/G/H: handle/cutout raycast, footprint validity,
  auto-slab loop, ceiling hosting, B-key tool desync, 2D drop offset,
  per-frame jank.
- Snapping modes (grid/lines/angles/off) + contextual HUD chips; modifier
  model (Shift=cycle, Alt=free place, Ctrl=grid step).
- Item move now tracks the cursor 1:1 (was a laggy per-frame lerp); handle
  rig hides during a whole-node move; rotate gizmo advertises Shift=free
  rotation in the HUD and hides the move cross while rotating.
- Floor-plan perf: pause live reactivity while in 3D-only view; per-node
  geometry cache so only changed nodes rebuild on a drag; hoist wall miters
  to a once-per-pass ctx.levelData (O(N^2) -> O(N) on wall/opening drags).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:04:58 -04:00
a71de82ccb nodes: add MEP movement controls and DWV parity (#438)
* Add roof surface placement support for items

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

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

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

* fixed conflict

* feat(duct): ceiling-snap drawing + connected-joint endpoint move

Duct draw tool's ceiling mode now hangs each path point just below the
ceiling actually covering it (per-room heights tracked), with a
translucent surface highlight and a plumb line to the floor so the
in-flight point reads clearly from any angle.

Dragging a duct corner that sits on a fitting now carries the fitting's
other ducts along (port-connectivity second hop), so the joint moves
together instead of tearing apart.

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

* feat(mep): detach + vertical modifiers for duct/pipe joint editing

Alt detaches a dragged duct/pipe endpoint or fitting from its connected
joint (no elbow re-aim, no connectivity follow); Ctrl/Cmd drives vertical
riser movement on the fitting move. Behavioral parity across 2D and 3D.

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

* feat(mep): full DWV pipe parity for joint editing

Bring pipe-segment endpoint drags and pipe-fitting moves to parity with
duct: free-drag endpoints, Alt-detach, Ctrl/Cmd-vertical riser, elbow
re-aim, and connectivity follow. Generalizes the shared elbow-reaim and
auto-fitting helpers to dispatch by run kind so 2D and 3D share one path.

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

* feat(mep): wall-style arrow handles for duct fittings + segments

Add violet directional arrow affordances to duct-fitting selection (height,
move cross, rotate arc) mirroring the duct-segment rig: portaled into the
parent frame to stay out of the selection outline, rendered via the shared
HandleArrow, and carrying mated-run connectivity through the single-undo
dance. The move cross engages press-drag-release (placementDragMode) the same
way the floating drag does, so the markup hit-areas go inert and the fitting
move tool commits on pointer-up.

Also re-export the HandleArrow primitives from @pascal-app/editor and extend
the duct-segment side-move/floorplan affordances.

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

* feat(mep): click-to-latch cube handles for duct + fitting editing

Replace the hover-reveal / multi-handle selection rigs with a single
click-to-latch cube that opens a directional cluster, shared between
duct segments and fittings via a new selection-handles module
(HandleCube / MoveChevron / RotateArc, all sized to the roof pitch cube).

- Duct segment: per-vertex + run-center cubes reveal axis-locked move
  chevrons (down arrow always shown), plus a roll arc at the run center.
- Duct fitting: center cube reveals six ±XYZ move arrows and three
  per-axis rotation arcs (oriented in place), replacing the old
  height/move/rotate trio with axis-cycling.
- Rotation (fitting arcs + duct roll) snaps to 45° steps; Shift = smooth.
- thin chevron profile + press-drag-release commit retained.

* fix(mep): orient duct roll arc consistently + drop Ctrl-vertical drag

Build a fully-determined basis for the duct roll gizmo so the curved
arrow always seats at the top-outer 45° corner regardless of run
direction, instead of an arbitrary apex from a single setFromUnitVectors.
The selection-rig ±Y arrows now own vertical movement, so the redundant
Ctrl-modifier riser drag is removed from the fitting move tool.

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

* feat(mep): run-aligned duct handles, swing snapping, elbow flatten

Align the duct run-center cube + horizontal arrows to the run axis
(matching the per-vertex handles) while keeping whole-run translate.
Endpoint side / up-down swing arrows now follow grid snap points and
port-snap onto nearby collars (Shift sweeps smoothly). Relax elbow
realign + fitting schemas to flatten to a straight 0° coupling. Surface
HVAC-specific hints in the select-mode helper panel for duct / fitting.

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

* feat(mep): per-segment linesets/liquid-lines with joint-follow editing

Linesets and liquid lines now commit one independent two-point node per
drawn segment instead of folding into a single mitered polyline, so each
line selects and deletes on its own. Endpoint caps fill shared-coordinate
joints so connected segments still read as continuous pipe.

Dragging a shared endpoint carries mated segments along via port
connectivity (Alt detaches), so a run still edits as one welded piece.
Liquid-line follow mode traces the whole connected lineset run, laying a
per-segment parallel line down its full assembled length.

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

* feat(mep): vertical-offset auto-routing on duct center-cube ±Y drag

Lifting/lowering a connected run with the run-center cube now keeps each
connected end welded to its stationary partner instead of dragging the
whole network. Run-to-run ends get the classic S/Z offset (two elbows +
plumb riser, partner trimmed back one leg); elbow-connected ends form a
clean L — the existing elbow stays put and re-aims its collar vertical,
with one new top elbow + riser reconnecting to the lifted endpoint. The
offset is ghosted live and minted as a single undo step on release.

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

* Add roof accessory placement guides

Measure roof accessory placement against the active roof face using visible surface bounds and preview geometry footprints. Add dormer-local guides and special linear handling for ridge vents and gutters.

* Improve duct and placement routing

* Fix duct vertical movement routing

* Fix duct vertical offsets and roof accessory movement

* Add DWV movement parity and line endpoint controls

* Fix MEP handle review issues

* Fix chimney placement and duct offset cleanup

* Use snapped targets for roof accessory commits

* fix(nodes): repair MEP movement review issues

- auto-fitting: tee branch now follows the drawn lateral angle; update the
  stale square-tee test + doc comment that contradicted the rewrite
- duct-segment: re-enable the vertical auto-offset rewind (the disabled stub
  left mintedIds empty, so re-dragging a tagged duct stranded old elbows/risers
  and stacked duplicates); remove the dead stub
- duct-segment: strip the stale auto-offset tag on manual corner/roll commits
  so the horizontal-move path no longer trusts an out-of-date base
- chimney: resume history before mutating segment children arrays so a
  cross-segment move reparents in one tracked transaction (undo stays consistent)
- dormer: align schema test with the new windowSill=false default

* fix: address mep movement review issues

* fix: address follow-up mep review comments

* fix: address additional mep review comments

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pascal-open-bot <open@pascal.app>
2026-06-23 08:37:15 -04:00
Wassim SAMADandClaude Opus 4.8 06e4cd7748 feat(export): baked GLB export with identity, clips, cutout fix (phases 0-1)
Promote the client GLB export into the baked-artifact format from
plans/editor-baked-glb-export.md (phases 0 and 1).

- NodeMaterial -> classic MeshStandardMaterial conversion at export. The
  viewer's MeshStandard/LambertNodeMaterial set isNodeMaterial, not
  isMeshStandardMaterial, so GLTFExporter would otherwise drop every
  surface to a blank default. KTX2 (compressed) maps are decompressed via
  WebGPUTextureUtils so the exporter can embed them (PNG for now; KTX2
  re-encode is deferred to the phase-3 bake worker).
- Identity stamping from sceneRegistry: node.name = pascalId and
  extras = { pascalId, kind, label?, openable?, clips? }; all other
  userData stripped so editor/runtime ephemera never reach glTF extras.
- Door/window open clips baked from the build-once + pose-at-t primitives
  (door pascalSwingLeaf marker, window poseWindowMovingParts). Clips named
  by label ("Door 1: open"), carry extras.loop = false (consumers play
  once and hold; dumb glTF players still loop).
- Cutout fix: door/window selection hitboxes hide via material.visible,
  which onlyVisible misses, so the hitbox box plugged the wall opening.
  Non-renderable container meshes now keep their node but lose geometry;
  childless ones are removed.
- Editor-overlay stripping mirrors the thumbnail capture: emit
  thumbnail:before/after-capture so scene-layer affordances (handles,
  ceiling/site brackets) self-hide, and drop anything off SCENE_LAYER
  (gizmos, grid, zone fills).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:55:27 -04:00
Wassim SAMADandGitHub ae90da6554 Merge pull request #437 from pascalorg/feat/resize-sfx
editor: add resize SFX while dragging resize handles
2026-06-22 11:16:06 -04:00
Wassim SAMADandClaude Opus 4.8 9bfbcc9f01 editor: add resize SFX triggered while dragging resize handles
Register a `resize` sound effect (three round-robin variations with
pitch/pan jitter and an 80ms gap) and emit it from the linear/radial
resize drag handler whenever the snapped+clamped value steps, so width /
length / depth / radius / height drags tick as the size changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:09:02 -04:00
Wassim SAMADandGitHub b2f1a8432e Merge pull request #436 from pascalorg/fix/wall-selection-keeps-material
fix(selection): selected objects keep their texture (light glow, no wash)
2026-06-22 08:34:47 -04:00
Wassim SAMADandClaude Opus 4.8 8633886eef fix(selection): selected objects keep their texture (light glow, no wash)
Selecting a textured wall/slab/item replaced its surface with a flat
purple wash. Root cause: the selection highlight clones the material and
tints it, but `NodeMaterial.clone()` on the WebGPU backend drops the
texture-map node assignments, so the clone rendered flat — and a strong
albedo blend + emissive washed whatever was left.

Fix: re-attach the maps from the source material after cloning (shared by
reference) and drop the albedo tint, keeping only a gentle indigo emissive
so the real material/texture stays readable with a soft "selected" glow.
Applied to both highlight paths:
- generic editor highlight (slabs/items) in selection-manager
- wall path (walls are excluded from the generic one), built lazily +
  cached/self-healing so it survives the wall finish's async texture load

Removes the now-dead eager wall `highlightedVisible`/`selection` profile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:17:41 -04:00
Wassim SAMADandGitHub f2352616d9 Merge pull request #433 from pascalorg/chore/editor-polish
Editor polish: paint SFX, duplicate keeps slots, review-skill convention
2026-06-19 13:11:24 -04:00
Wassim SAMADandClaude Opus 4.8 08df21a96d docs(review-skill): require slots + world-scale UV convention for new nodes
Adds a Section B rule to the review-architecture skill: a new node kind or
geometry change that exposes paintable parts must carry overrides in a
schema `slots` record resolved via def.capabilities.paint (and thread it
through clone/duplicate/preset paths), and texturable def.geometry must
emit UVs in metres (1 unit = 1 m) so catalog finishes tile. Surfaced in the
skill description too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:04:42 -04:00
Wassim SAMADandGitHub affe09d3b1 Merge pull request #432 from pascalorg/feat/fence-creation
feat(fence): horizontal-board fence style with capped posts
2026-06-19 13:03:23 -04:00
Wassim SAMADandClaude Opus 4.8 346b5d8cbe feat(fence): add horizontal-board fence style with capped posts
Adds a `horizontal` fence style that builds composite-cladding panels —
horizontal boards stacked between square intermediate posts — instead of
the vertical pickets that slat/rail/privacy all produce today.

- New `style: 'horizontal'` (core schema + inspector Style control).
- Posts march along the whole span at `postSpacing`, each with a cap.
- New `postCap` control (none/flat/pyramid, default pyramid); pyramid
  caps render as a 4-sided cone fence part.
- New `slatGap` control for the board reveal — 0 collapses to one flush
  panel so the stacked-board seams don't read as lines.
- `postCap` + `slatGap` are horizontal-only (visibleIf); postCap uses a
  labelled dropdown so its options aren't a context-free switch.
- Lower the `postSpacing` inspector minimum from 0.2 to 0.05 m.
- 2D floor plan reuses the solid-panel marker for horizontal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:57:24 -04:00
Wassim SAMADandClaude Opus 4.8 115a142d83 fix(item): duplicated items keep their painted slot materials
Duplicating an item drops to the catalog placement flow, which rebuilds the
draft from the asset + transform and never carried node.slots — so the copy
lost every painted slot override. Thread slots through the draft create path:
useDraftNode.create seeds it onto the draft and commit() forwards it to the
final node, the placement coordinator passes it to its lazy wall/ceiling
draft creates, and the item move tool supplies node.slots for both the floor
(direct create) and wall/ceiling (coordinator) duplicate paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:17:30 -04:00
Wassim SAMADandClaude Opus 4.8 bf9246cd36 feat(sfx): play paint_apply sound when a material is painted
Registers a paintApply SFX (with rate/volume jitter + a 60ms gap so rapid
multi-face painting doesn't machine-gun), wires the sfx:paint-apply bus
event, and emits it from the material-paint click chokepoint in the
selection manager (fires on apply, not hover/preview). Adds the audio
asset to the editor app's public resources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:50:48 -04:00
Wassim SAMADandGitHub ee4d6de206 Merge pull request #431 from pascalorg/docs/item-authoring-guide
docs: item-authoring guide for paintable item GLBs
2026-06-19 09:08:09 -04:00
Wassim SAMADandClaude Opus 4.8 c726721ec6 docs(architecture): add item-authoring guide for paintable GLBs
Documents the content-author contract for catalog item GLBs: slot_
material naming + deriveSlotId rules, authored defaults and
pascal_material extras, the cutout reserved mesh, the ~1 UV unit/m world
scale, vertex colours for fixed detail, and the validated Blender + glTF
export recipe (Custom Properties export gotcha, gltfpack -ke extras
preservation). Indexes the page in the architecture README and links it
from materials-and-themes.md (previously a dangling "phase-6 Blender
recipe" reference).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:04:42 -04:00
Wassim SAMADandGitHub 36ad0f9933 Merge pull request #430 from pascalorg/chore/pull-authored-item-glbs
chore(items): pull authored item GLBs from prod
2026-06-18 16:40:18 -04:00
Wassim SAMADandClaude Opus 4.8 bfcb039757 chore(items): pull authored item GLBs from prod
Replace 89 bundled item model.glb files with the authored/optimized
versions currently served from prod storage (items/system/<slug>/model.glb),
so the committed source of truth matches what live projects load and a
catalog re-seed no longer clobbers authored geometry. Items with no prod
object (404) are left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:36:54 -04:00
Wassim SAMADandGitHub 628bd7f5c6 Merge pull request #429 from pascalorg/feat/paint-panel-polish
Paint panel polish: sticky controls, selection outlines, category auto-select
2026-06-18 12:53:28 -04:00
Wassim SAMADandClaude Opus 4.8 1dcaa73717 refactor(paint-slots): paint panel — three fixed bands, scroll only the grid
Layout is now: fixed eraser/reset + category tabs (no longer sticky-inside-
scroll), a single scrolling catalog grid, and an always-visible scene-material
footer. The custom-material "+" moved out of the colors-only grid cell onto the
scene-material section header, so a custom material can be added from any
category (creates a blank scene material, selects it as the brush, opens its
inline editor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:50:47 -04:00
Wassim SAMADandClaude Opus 4.8 8badd415b9 feat(paint-slots): paint panel polish — sticky controls, selection outlines, auto-select
- MaterialPaintPanel owns its scroll: the eraser/reset row stays pinned and
  the category tabs stick to the top, so only the material list scrolls.
- Selected catalog swatch + active scene-material card use the same
  `ring-1 ring-primary ring-inset` outline as item/preset tiles.
- Choosing a material category auto-selects its first material.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:45:45 -04:00
Wassim SAMADandGitHub ff2a24795c Merge pull request #428 from pascalorg/feat/paint-slots
Paintable slots: unified material painting across items, walls & procedural geometry
2026-06-18 12:24:56 -04:00
Wassim SAMAD c4b6e74d65 Merge remote-tracking branch 'origin/main' into feat/paint-slots
# Conflicts:
#	packages/core/src/store/use-scene.ts
#	packages/editor/src/components/editor/index.tsx
2026-06-18 12:22:26 -04:00