Commit Graph
11 Commits
Author SHA1 Message Date
Wassim SAMADandClaude Opus 4.7 56e022a093 Phase 5 Stage D wall: port endpoint move, whole-move, placement (1:1 legacy)
Three remaining wall affordances ported into nodes/src/wall/ as
direct copies of the legacy implementations. Wall D is now complete.

  - move-endpoint-tool.tsx (426 LoC) — linked-wall corner cascade +
    Alt-detach + angle label. Mounted via `affordanceTools.move-endpoint`.
  - move-tool.tsx (804 LoC, the most complex tool in the editor) —
    center-drag with axis lock, linked-wall corner cascade via
    `planWallMoveJunctions`, bridge wall ghost previews, auto-slab
    live preview via `planAutoSlabsForLevel`, R/T rotation in 45°
    steps, Shift to bypass grid snap, isNew metadata strip on first
    commit. Mounted via `affordanceTools.move`.
  - tool.tsx (332 LoC) — two-click placement with length/angle HUD,
    Shift to bypass angle snap. Mounted via `def.tool`.

Editor public surface gains:
  - createWallOnCurrentLevel, snapWallDraftPoint, WallPlanPoint
  - MovingWallEndpoint type

ToolManager dispatch for `movingWallEndpoint` routes through the
registry with the legacy fallback (same shape as the fence
move-endpoint dispatch).

Wall is now A  C  D . Stage B still pending (geometry depends on
level-batch miter data, blocked on `ctx.levelData` design decision).
Stage E pending (drop WallPanel — has slider drags + actions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:52:30 -04:00
Wassim SAMADandClaude Opus 4.7 6a1d853dd8 IconRef: add url kind, point registered kinds at palette assets
User pointed at the palette's PNG icons in the bottom toolbar — they
expected the inspector title to use the same artwork, not the iconify
lucide glyphs. Different visual style.

- IconRef gains `{ kind: 'url'; src: string }`. Plain `<img>` render
  in ParametricInspector (no next/image — the inspector is
  `'use client'`).
- All currently-registered kinds switched to URL refs matching their
  palette `iconSrc`:
    fence    → /icons/fence.png
    slab     → /icons/floor.png
    ceiling  → /icons/ceiling.png
    wall     → /icons/wall.png
    spawn    → /icons/site.png
    shelf    → /icons/column.png (placeholder, same as palette)
- Kind-owned panels (slab/ceiling) already pass URL strings to their
  own PanelWrapper; unchanged.

Door/window/item will get URL refs when they register at Stage A.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:41:45 -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 da63081f73 Phase 5 Stage D: revert curve + whole-item move affordances to legacy
User-reported regressions made it clear that my D-ports of the curve
and whole-item move tools introduced more friction than they removed.
The legacy CurveFenceTool / CurveWallTool / MoveFenceTool /
MoveSlabTool / MoveCeilingTool ship more polish than the new ports do
right now:

  - Legacy curve tools pre-snap the pointer position to the 0.5m grid
    BEFORE projecting onto the chord normal, and they Shift-toggle to
    a free-place mode. The ports skipped both — finer math, but the
    user-visible UX regressed (laggy because the cascade resolver fires
    per move, history feels broken near the no-op threshold).
  - Legacy whole-item moves use scene.update per tick which keeps
    hosted children visually aligned. The live-drag mesh.position port
    cleared the offset before the GeometrySystem could rebuild,
    producing a one-frame teleport on commit.

Drop the affordance registrations for those tools — the ToolManager /
MoveTool dispatch falls back to the legacy per-kind tools when the
registry doesn't declare the affordance. Stage D progress preserved
for: fence move-endpoint (linked cascade + alt-detach), slab/ceiling
boundary + hole editors, fence placement, slab placement, ceiling
placement.

The kind-owned files (curve-tool.tsx, move-tool.tsx, actions/curve.ts,
actions/move.ts) stay on disk for the next iteration — when they
reach parity with legacy UX, re-add the affordance entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:42:40 -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 7f2f9c685b Phase 5 Stage D wall: port CurveWallTool to DragAction (first wall D)
Direct copy of the fence curve recipe — pure
`curveWallDragAction` (chord-perpendicular projection + clamp +
normalize + single-undo dance) plus a thin wrapper feeding
`useDragAction`. Mounted via `def.affordanceTools.curve`.

Slight precision difference vs legacy: the legacy CurveWallTool snapped
the pointer position to `getWallGridStep()` before projecting onto the
chord normal; the ported action skips that pre-snap and relies on
`normalizeWallCurveOffset` to settle the final value. The user-visible
result is the same magnitude of step, just with the snap applied at
the offset level instead of the position level.

Remaining wall D affordances (endpoint move, whole-wall move,
placement) are larger and queued for future sessions — wall's move
tool alone is 804 LoC with the linked-wall corner-cascade logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:34:40 -04:00
Wassim SAMADandClaude Opus 4.7 9bcb25d0aa Phase 5 Stage C continued: wall, door, window now in registry floor plan
Three remaining kinds at Stage C this session:

wall → C
 - buildWallFloorplan: uses ctx.siblings to gather other walls in the
   level, runs calculateLevelMiters, computes plan footprint via
   getWallPlanFootprint. Same visual output as legacy.
 - getFloorplanWall thickness exaggeration inlined (~25 lines from
   editor/lib/floorplan/walls.ts) to keep nodes/wall self-contained.
 - floorplan-panel.tsx's wallPolygons short-circuits to [] when wall
   is registered.
 - Performance note: recomputes level miter data per wall (O(N²) for N
   walls in a level). Acceptable for typical scenes; ctx.levelData?.
   miters optimization deferred to Stage B's wall design pass.

door → C
 - buildDoorFloorplan: inlines getOpeningFootprint math from
   floorplan-panel.tsx (40 lines, pure math). Uses ctx.parent as the
   wall to compute direction + perpendicular for the cutout footprint.
 - Returns null when parent isn't a wall (orphaned doors during
   placement).

window → C
 - buildWindowFloorplan: same shape as door, glass-blue tint to
   distinguish visually.

Both share the legacy openingsPolygons gating:
 - floorplan-panel.tsx's openingsPolygons useMemo filters per kind so
   a partial migration still works (e.g., if only door registers, only
   doors get skipped). When both registered, returns [] entirely.

Item C intentionally deferred — needs parent-chain transform helpers
(buildFloorplanItemEntry / getItemFloorplanTransform from editor/lib/
floorplan/items.ts) exposed publicly or moved into core. A focused
session is the right place to design that boundary.

Stage B for door / window / wall still pending — each is a focused
session per kind (large geometry math extractions, wall needs ctx.
levelData design).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:43:53 -04:00
Wassim SAMADandClaude Opus 4.7 6a4de8cff5 Drop wall + fence feature flags: register unconditionally; remove verification logs
Parity comparison against deployed prod is now cleaner than juggling
env-var flag toggles locally. Both kinds enter builtinPlugin.nodes
unconditionally; the Phase 0 dispatch shims (<LegacySystem kind="X">
wrappers + NodeRenderer's registry-first branch) handle the cutover.

Files deleted:
 - packages/nodes/src/wall/feature-flag.ts
 - packages/nodes/src/fence/feature-flag.ts

Files changed:
 - packages/nodes/src/index.ts: drops isWallRegistryEnabled /
   isFenceRegistryEnabled gates; wallDefinition + fenceDefinition
   land directly in builtinPlugin.nodes.
 - packages/nodes/src/{wall,fence}/index.ts: drop the flag re-export.
 - packages/nodes/src/{wall,fence}/renderer.tsx: drop the one-shot
   verification console.info. Same for the system.tsx wrappers.
 - packages/viewer/src/components/renderers/{wall,fence}/{wall,fence}-
   renderer.tsx: drop the paired [X:legacy] verification logs (no
   longer comparing flag-toggled paths).

Net DX: no env var to remember when starting `bun dev:community`. To
A/B test, compare against editor.pascal.app deployed prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:59:09 -04:00
Wassim SAMADandClaude Opus 4.7 375914a07c Wall: paired verification logs for registry vs legacy dispatch
Three one-shot console.info calls so the Phase 3 milestone-B parity
check is unambiguous from the browser console alone:

- [wall:registry] system bundle mounted — fires when RegisteredSystems
  lazy-loads nodes/src/wall/system.tsx (exactly once per viewer mount
  when the flag is on).
- [wall:registry] first WallRenderer mounted — fires once when the
  first registry-driven WallRenderer mounts.
- [wall:legacy] first legacy WallRenderer mounted — fires once if the
  legacy path is active (flag off, or kind not registered).

Module-level booleans gate the renderer logs so they don't spam in
scenes with many walls. Drop all three alongside the feature flag at
Phase 3 sign-off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:18:36 -04:00
Wassim SAMADandClaude Opus 4.7 02aeca8439 Wall Phase 3 milestone B: runtime port behind feature flag
Brings the wall kind onto the registry path when
NEXT_PUBLIC_USE_REGISTRY_FOR_WALL=true; default-off keeps wall on its
legacy path unchanged.

Files added:

- nodes/src/wall/renderer.tsx — thin placeholder-mesh mount point.
  Identical pattern to the legacy WallRenderer: registers ref via
  useRegistry, marks dirty on mount, renders hosted children
  recursively via NodeRenderer. The legacy WallSystem fills geometry
  on the next frame regardless of which mount path is active.

- nodes/src/wall/system.tsx — a bundle component that renders
  <WallSystem /> + <WallCutout /> (both re-exported from viewer).
  Registered via def.system with priority 4 to mirror the legacy
  WallSystem's useFrame priority. Zero logic duplication — the
  ~970 lines of CSG/mitering/cutaway code stays in viewer.

Files changed:

- packages/viewer/src/index.ts — new exports for WallSystem, WallCutout,
  and NodeRenderer. The first two so the registry-driven system bundle
  can compose them; NodeRenderer so any parent kind (wall, slab,
  ceiling, building) can recursively render hosted children without
  reaching into viewer internals.

- nodes/src/wall/definition.ts — adds renderer + system fields. Tool
  field stays absent (wall placement / endpoint drag remain bespoke
  for now; the affordance port is a later milestone).

- nodes/src/index.ts — conditionally appends wallDefinition to
  builtinPlugin.nodes based on isWallRegistryEnabled(). With the flag
  off, the array is identical to before this commit; with it on,
  Phase 0 dispatch shims switch wall to the registry path:
    * <LegacySystem kind="wall"> around WallSystem returns null
    * <LegacySystem kind="wall"> around WallCutout returns null
    * <NodeRenderer> takes the registry-first branch and mounts the
      new renderer instead of the legacy switch case for 'wall'
    * RegisteredSystems mounts the new system bundle, which re-mounts
      the same WallSystem + WallCutout components from viewer

No behavior change with the flag off. With the flag on, behavior should
be byte-identical (same components, same priority, same geometry path).
Manual verification next: place walls, t-junctions, walls-with-doors
with the flag toggled both ways; confirm visual + interactive parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:00:15 -04:00
Wassim SAMADandClaude Opus 4.7 0a723fa1f2 Wall Phase 3 milestone A: registry skeleton (metadata only)
Lays down the wall folder under @pascal-app/nodes with everything needed
to register the kind, but intentionally without runtime wiring:

- schema.ts re-exports WallNode from core (door/window/item still type
  their parentId against WallNode.shape.id, so the schema stays canonical
  there for now).
- parametrics.ts declares thickness / height / curveOffset for the Phase 4
  inspector. Endpoints and host children are edited via affordances, not
  number inputs, so they're not in parametrics.
- definition.ts encodes capabilities (surfaces, selectable, duplicable,
  deletable — no movable since wall's move is bespoke endpoint-drag),
  relations (hosts doors/windows/items, affectsSpatial slabs/ceilings/
  zones, linkedBy endpoint-match, cascadeDelete descendants), and the
  presentation metadata for the palette. Renderer / system / tool fields
  are deliberately absent — the existing wall-renderer.tsx and
  wall-system.tsx keep serving wall until milestone B.
- feature-flag.ts gates the eventual registration via
  NEXT_PUBLIC_USE_REGISTRY_FOR_WALL (same pattern Phase 2 used for spawn).
- wallDefinition is NOT yet appended to builtinPlugin.nodes — registration
  is what flips the Phase 0 dispatch shims, and we don't want that until
  the runtime port lands. Until then this file is metadata-only.

Two type-side changes pulled forward from Phase 4 to make a metadata-only
definition compile:

- NodeDefinition.renderer becomes optional (the three-checkbox model
  documented in wiki/architecture/node-definitions.md already promises
  this). RegistryRenderer in node-renderer.tsx gains a null-guard so an
  undefined renderer cleanly falls through to the legacy switch.

No runtime behavior change. Walls render and behave exactly as before.

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