Commit Graph
8 Commits
Author SHA1 Message Date
open-pascal 3adae8acea style: apply biome lint fixes after portforward 2026-05-20 00:54:11 +00:00
sudhirandopen-pascal 7cc11f0fd6 feat: add fence move handles and duplication functionality to the floorplan editor 2026-05-20 00:45:25 +00:00
Wassim SAMADandClaude Opus 4.7 82c9b5e1df Phase 5 Stage D moves: align useLiveTransforms with the direct mesh.position delta
User reported the slab/fence mesh jittered/teleported continuously
while dragging (not on commit — during the drag). Root cause: the
move tool wrote TWO conflicting values per grid:move tick:

 1. `mesh.position.set(deltaX, 0, deltaZ)` — relative offset, direct
    Three.js mutation.
 2. `useLiveTransforms.set(id, { position: [originalCenter + deltaX,
    0, originalCenter + deltaZ], rotation: 0 })` — absolute world
    position of the translated polygon center.

`ParametricNodeRenderer` reads `useLiveTransforms` and binds it via
React: `<group position={liveTransform.position}>`. So every Zustand
notification re-rendered the renderer and reconciled the group's
position back to "originalCenter + delta" (the absolute), overriding
the "delta" the direct mutation had just written. The two systems
fought every frame → visible jitter.

Fix: `useLiveTransforms.position` now holds the SAME delta the direct
mutation uses (`[deltaX, 0, deltaZ]`). React reconciles to the same
value the direct mutation already set — no conflict.

The cursor sphere position stays as the translated polygon center
(it's tracked separately via React state, not `useLiveTransforms`).

Ceiling aligned for consistency, though CeilingRenderer doesn't read
`useLiveTransforms` so the value there has no rendering effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:46:23 -04:00
Wassim SAMADandClaude Opus 4.7 f4ea07e05b Phase 5 Stage D moves: live-drag mesh.position for slab / ceiling / fence
User-reported regressions on the 1:1 legacy ports: slab/ceiling moves
were slow (polygon CSG rebuilds per scene.update tick), fence moves
teleported briefly on commit (residual mesh.position offset survived
the geometry rebuild). All three now use the same live-drag pattern
the legacy fence move was designed for:

- During drag, write only to `sceneRegistry.nodes.get(id).position`
  + `useLiveTransforms`. No `scene.update`, no polygon rebuild, no
  React re-render of geometry.
- History stays UNPAUSED — scene state isn't changing.
- On commit, a single `scene.update` writes the translated
  polygon (or fence start/end + linked-fence cascade). Recorded as one
  natural undo step.
- Tools leave `mesh.position` at the drag delta on commit;
  GeometrySystem / CeilingSystem reset it to (0,0,0) when they
  rebuild the geometry on the next frame. By the time position
  clears, the new geometry is in place — no teleport.

Two framework changes enable this:
- `GeometrySystem` (viewer/systems/geometry) now resets
  `group.position` + `group.rotation` after every rebuild, matching
  the legacy `FenceSystem.updateFenceGeometry` behavior. Tools that
  translate the group during live-drag can rely on the reset.
- Legacy `CeilingSystem.updateCeilingGeometry` extends its existing
  `position.y` reset to cover X/Z too — previously it left X/Z at the
  drag delta after rebuild, double-translating the visual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:12:15 -04:00
Wassim SAMADandClaude Opus 4.7 1e15e10185 Phase 5 Stage D: re-port curve + whole-item move tools 1:1 from legacy
Each tool is now a direct copy of the legacy implementation, relocated
under @pascal-app/nodes/<kind>/ and dispatched via the registry's
def.affordanceTools. No DragAction abstraction, no clever live-drag
exception, no novel snap pipeline — same code, same UX, same
performance, same history dance.

Ports:
- fence/curve-tool.tsx    (legacy CurveFenceTool, 1:1)
- fence/move-tool.tsx     (legacy MoveFenceTool, 1:1 — including
                           the mesh.position + useLiveTransforms
                           exception that the legacy uses for fence
                           specifically)
- wall/curve-tool.tsx     (legacy CurveWallTool, 1:1)
- slab/move-tool.tsx      (legacy MoveSlabTool, 1:1)
- ceiling/move-tool.tsx   (legacy MoveCeilingTool, 1:1 — preview
                           fill + outline overlay preserved)

Drops the obsolete DragAction-based action files
(packages/nodes/src/{fence,wall,slab,ceiling}/actions/{curve,move}.ts)
and their now-empty actions/ directories where applicable. Fence
keeps actions/move-endpoint.ts since that port works.

Editor public surface gains `getWallGridStep` + `snapScalarToGrid`
(transitional exports — Stage F moves them into @pascal-app/nodes).

ToolManager + MoveTool dispatch unchanged: the same legacy-fallback
branches now mount the registry component because the affordances are
declared, but the rendered behavior matches the legacy because the
implementations are copies.

Per-kind progress: fence D  (curve / move-endpoint / move / placement
all kind-owned), slab D , ceiling D , wall D 🟡 (curve only,
endpoint/move/placement still legacy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:53:51 -04:00
Wassim SAMADandClaude Opus 4.7 d1231b4901 Phase 5 Stage D moves: live-drag for slab/ceiling + cursor follows pointer
User-reported regressions after the previous Stage D move ports:

1. Slab/ceiling moves were sluggish because the actions wrote
   scene.update(polygon) every grid:move tick → React re-render +
   CSG-with-holes geometry rebuild per frame.

   Apply the live-drag exception (same recipe fence move already used):
   visual translation via `sceneRegistry.nodes.get(id).position` +
   `useLiveTransforms`; scene.polygon is only written on commit. Polygon
   center precomputed in ctx, mesh-offset clears on commit/cancel.

2. Cursor sphere sat at the polygon center (offset from the user's
   actual cursor by `originalCenter - first_cursor`). Move wrappers
   now subscribe to `grid:move` and set `cursorRef.current.position`
   directly — no React state, no per-tick reconcile. Cursor lands on
   the user's pointer.

3. Fence move had the same React-reconcile-per-tick cost via its
   `useLiveTransforms` subscription. Switched to the same direct
   ref-mutation pattern.

Adds a "REAL bend" test pinning that one Ctrl-Z after a real curve
drag undoes only the bend, not the create. The previously reported
"first undo does nothing" outcome reproduces only for no-op bends
(drags within `normalizeWallCurveOffset`'s straight-snap threshold,
≈1.5cm on a 3m fence). For visible bends the dance pushes a real
pastState entry and one undo step rolls back the bend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:27:27 -04:00
Wassim SAMADandClaude Opus 4.7 c2c2f66427 Phase 5 Stage D: fix bend history + move-tool loop + grid-snap sfx
Three user-reported regressions, all in the Stage D move/curve ports:

1. **Fence/wall bend cancelled the fence creation on Ctrl-Z.** The
   action.commit's `return false` shortcut on "no offset change"
   bypassed the dance entirely — pastStates wasn't touched, so the
   next Ctrl-Z fell through to whatever preceded activation
   (typically the create step). Fix: always run the dance, even on
   no-op commits. First Ctrl-Z then absorbs a silent no-op entry,
   subsequent presses roll back real prior actions. Same fix applied
   to fence move-endpoint, fence move, slab move, ceiling move.

2. **Slab/ceiling move 'maximum update depth exceeded' loop.** The
   `useScene` selector in `SlabMoveTool` returned a freshly-allocated
   `[sx, sz]` tuple on every call. Zustand's `Object.is` equality
   failed each comparison → re-subscribe → re-render → loop. Fix:
   subscribe to the stable live-node reference and derive the center
   via `useMemo`. Same recipe for fence/ceiling move-tools, including
   memoizing the `originalCenter` fallback that was getting a new
   array per render.

3. **No grid-snap sfx during move drag.** Action `preview` now tracks
   the last snapped pointer on a mutable `lastSnapped` ctx field and
   emits `sfx:grid-snap` when it changes between ticks. Matches the
   legacy MoveFenceTool's per-tick sound.

Locking test for foot-gun 1 lives in
`packages/core/src/services/single-undo-dance.test.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:12:07 -04:00
Wassim SAMADandClaude Opus 4.7 c16878c876 Phase 5 Stage D fence: port MoveFenceTool to DragAction (whole-fence move)
Third Stage D affordance port (302 LoC legacy → action + thin wrapper).

The action (`actions/move.ts`) uses the live-drag exception documented
in editor/wiki/architecture/tools.md: visual-only updates via
`sceneRegistry.nodes.get(id).position` + `useLiveTransforms` during
the drag, no scene mutations. Avoids re-rebuilding the fence geometry
(many posts + infill panels) every pointer tick. Commit performs the
single-undo dance — writes final start/end to scene, geometry rebuilds
once, Ctrl-Z reverses the whole drag.

Linked-fence cascade follows the same shape as MoveFenceEndpoint —
any fence in the same parent that shared an endpoint at activation
moves with the drag.

`getRegistryAffordanceTool` extracted to
`tools/shared/affordance-dispatch.ts` so move-tool.tsx and
tool-manager.tsx share the lazy-load helper (no duplicate caches).

MoveTool dispatch gains a generic `affordanceTools.move` check after
the capability-driven `movable` shortcut — fence routes through here;
wall / slab / etc. fall through to their legacy per-kind chain until
their D ports land.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:19:51 -04:00