Commit Graph
146 Commits
Author SHA1 Message Date
Wassim SAMADandClaude Opus 4.7 924567293a shelf: v2 — cubby default, withBottom, item hosting, paintable surface
Schema v2 adds style/rows/columns/withBack/withSides/withBottom/bracketStyle
and a `children: ItemNode[]` field for item hosting. Schema-level defaults
preserve the v1 wall-shelf visual so existing scenes load unchanged; the
placement tool spreads `shelfDefinition.defaults()` for fresh shelves
(cubby 3x2 at 1m × 0.5m × 1.8m, thickness 0.05m, back/sides/bottom on).

Four style geometries (wall-shelf / bookshelf / open-rack / cubby) share
the dimensional schema. `shelfRowSurfaceYs` exposes one host surface per
row, plus the bottom-board top when `withBottom` is on for cubby /
bookshelf.

Material is a single paintable surface (same shape walls / slabs / stairs
use); `DEFAULT_SHELF_MATERIAL` aligned with `DEFAULT_WALL_MATERIAL` so
unpainted shelves read as the canonical off-white.

Preview clones each cached material before mutating `transparent / opacity`
on the ghost — without the clone the mutation leaked into the cached
`getShelfMaterial` instance every committed shelf was using, rendering
them all see-through after the first placement preview rendered.

Store hardening: `migrateNodes` patches missing `children: []` on v1
shelves, and `updateNodesAction` reparenting tolerates a missing children
array on the new parent. `MaterialTarget` enum adds `'shelf'` so paint
mode picks up the kind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:11:13 -04:00
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 282cb22585 parametrics: add custom field kind + restore fence Length / Curve
User noticed the auto-derived fence inspector was missing the legacy
panel's Length + Curve sliders, and the section labelling was wrong
(Posts → Structure). Both Length and Curve are awkward for the
parametrics field model:

- **Length** doesn't map to a single node key — it's derived from
  `start`/`end`, and editing it moves `end` along the existing
  direction.
- **Curve** maps to `curveOffset` but the slider's min/max are
  bounded per-node by the chord length, plus updates need
  `normalizeWallCurveOffset`.

Adds a `kind: 'custom'` field with a kind-supplied
`component: ComponentType<{ node, onUpdate }>`. The inspector mounts
it and lets the kind own rendering + update logic. `key` becomes a
free-form React key/label since it no longer needs to map to a node
property.

Fence parametrics now mirrors the legacy layout 1:1:
- Style (segmented controls + showInfill toggle).
- Dimensions (Length, Curve, Height, Thickness).
- Structure (Base Height, Top Rail, Post Spacing, Post Size, Ground
  Clear, Edge Inset).

Length + Curve live in fence/inspector-editors.tsx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:27:40 -04:00
Wassim SAMADandClaude Opus 4.7 80199dc890 parametrics: add display: 'segmented' enum hint + drop fence color
User pointed out two regressions in the auto-derived fence inspector:

- A `color` field rendered for fence — but the legacy `FencePanel`
  hid it (fence's color is a leftover schema field that isn't part of
  the inspector UX). Dropped from `fenceParametrics`.

- Style + base-style enums rendered as a dropdown, but the legacy
  used the inline segmented switcher (Slat/Rail/Privacy +
  Grounded/Floating). Added a `display?: 'select' | 'segmented'` hint
  to the enum field kind. ParametricInspector renders SegmentedControl
  when set; defaults to dropdown otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:21:56 -04:00
Wassim SAMADandClaude Opus 4.7 3c37ff009b Phase 5 Stage E: fence / slab / ceiling drop their legacy panels
Three flavors of Stage E in one PR:

- **Fence** — fully auto-derived. Adds `kind: 'boolean'` to ParamField
  (rendered as ToggleControl), wires `showInfill` through
  `def.parametrics`. Legacy `FencePanel` deleted; the auto-derived
  `<ParametricInspector>` now drives fence editing entirely.

- **Slab / Ceiling** — kind-owned via `parametrics.customPanel`. The
  legacy panels have shape-specific bits (elevation/height presets,
  area display, holes list with auto-vs-manual provenance) that don't
  fit the auto-derived field model yet. `<ParametricInspector>` learns
  to lazy-load and mount `parametrics.customPanel` when present;
  legacy `SlabPanel` + `CeilingPanel` files relocate to
  `nodes/src/<kind>/panel.tsx` and the legacy copies delete.

  When `list` / `computed` / `action` field kinds eventually graduate
  to auto-derived support, these custom panels collapse back into
  `parametrics.groups`. The plan calls this out under "Custom-behavior
  escape hatch" and Foot-gun 5 of the recipe.

Public-surface additions in `@pascal-app/editor`:
- `ActionButton`, `ActionGroup`, `PanelSection`, `SegmentedControl`,
  `ToggleControl`, `PanelWrapper` — needed by the kind-owned panels.

Per-kind progress table: fence/slab/ceiling all flip to E .

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:04:11 -04: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 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 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 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 de3efa1b53 Phase 5 Stage D ceiling: port placement + move + boundary/hole editors
Replicates the slab Stage D recipe for ceiling: four affordances
routed through the registry — def.tool for the placement flow,
def.affordanceTools for boundary edit / hole edit / whole-ceiling move.

Ceiling-specific bits preserved:
- Placement tool keeps the dual-cursor + vertical TSL-gradient
  connector + ground-shadow lines (1:1 with legacy).
- Move tool wrapper renders the translucent preview fill + outline
  overlay so the user sees the destination before clicking.

ToolManager mount sites for CeilingBoundaryEditor / CeilingHoleEditor
now route through `getRegistryAffordanceTool` with legacy fallback.

Per-kind progress: ceiling A  B (n/a, def.renderer escape hatch
preserved) C  D ; E + F pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:31:40 -04:00
Wassim SAMADandClaude Opus 4.7 b2e5d84986 Phase 5 Stage D slab: port placement + move + boundary/hole editors
Replicates the fence Stage D recipe for slab — three drag affordances
+ one placement tool, all routed through the registry:

  affordanceTools:
    'boundary-edit' → thin <PolygonEditor> wrapper (vertex/edge drag)
    'hole-edit'     → same for a single hole polygon
    move            → DragAction with single-undo dance
  tool: () => placement (multi-click polygon with axis/45° snap)

`PolygonEditor` + `PolygonEditorProps` exported from
`@pascal-app/editor` as Stage D transitional surface (Stage F cleanup
moves them into `@pascal-app/nodes`).

ToolManager mount sites for SlabBoundaryEditor / SlabHoleEditor now
route through `getRegistryAffordanceTool` with legacy fallback.

Slab move action does not use the live-drag exception (polygon CSG
rebuild every tick) — matches legacy behavior; optimization is a
separate task once we measure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:27:31 -04:00
Wassim SAMADandClaude Opus 4.7 7b5a1c607a Phase 5 Stage D fence: port FenceTool placement to def.tool
Fourth and final Stage D fence affordance — the placement tool itself.

Unlike the drag affordances (curve / move / endpoint), placement is a
two-click flow with state across grid events, not a single drag-down →
drag-up lifecycle. `DragAction` doesn't fit; the component owns its
own emitter subscriptions directly. The kind exposes it via
`def.tool: () => import('./tool')` and ToolManager's existing
`getRegistryTool()` lookup mounts it (legacy `tools.structure.fence =
FenceTool` falls through when the registry entry is missing).

Adds transitional exports from `@pascal-app/editor` for the helpers
the kind-owned tool needs at module scope: `createFenceOnCurrentLevel`,
`markToolCancelConsumed`, `EDITOR_LAYER`. Stage F cleanup moves these
into `@pascal-app/nodes` once every consumer is registry-driven.

Fence Stage D is now complete. Per-kind progress: A  B  C  D 
(four affordances ported — curve, move-endpoint, move, placement).
E (drop legacy panel) and F (cleanup) pending across all kinds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:22:57 -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
Wassim SAMADandClaude Opus 4.7 5e67ffd4e8 fence curve: single-undo dance in commit — fix undo skipping past drag
The previous commit() just returned true, relying on
createDragSession.terminate() to resumeHistory. But that alone never
captures the drag in zundo's pastStates — the pause window's mutations
are skipped entirely. After commit, Ctrl-Z jumped past the curve *and*
past the prior fence creation.

Adds the same dance now used by move-endpoint: restoreAll →
resumeHistory → re-apply the final draft. Zundo records one undo step
for the whole drag. cancel() becomes a no-op (orchestrator's
restoreAll covers it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:57:51 -04:00
Wassim SAMADandClaude Opus 4.7 05efa25edb Phase 5 Stage D fence: port MoveFenceEndpointTool to DragAction
Second Stage D affordance port (425 LoC legacy → ~430 split across
action + wrapper). All math (snap, linked-fence cascade, alt-detach,
min-length gate, single-undo dance) lives in the pure
`moveFenceEndpointDragAction`. The React wrapper handles the UI
overlays (cursor sphere, Drag/Detach badge, angle label) and the live
state subscriptions.

The action introduces the single-undo dance pattern for multi-write
commits: `commit()` calls `scene.restoreAll()` → `resumeHistory()` →
re-applies the final draft so zundo records the entire drag as one
undo step. Reusable shape for slab/wall/door endpoint ports.

Transitional exports added to `@pascal-app/editor`'s public surface
(`snapFenceDraftPoint`, `isWallLongEnough`, the segment-angle helpers,
`MovingFenceEndpoint`). Stage F cleanup moves these into
`@pascal-app/nodes` once every consumer is registry-driven.

`getRegistryAffordanceTool` is now generic (`ComponentType<any>`) so
affordances with different prop shapes (`{ node }`, `{ target }`, …)
all dispatch through the same helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:51:04 -04:00
Wassim SAMADandClaude Opus 4.7 1082b62552 useDragAction: activation-click grace + curve fence commit sfx
The legacy CurveFenceTool ignored grid:click for 150ms after mount —
otherwise the very click that activates a tool (e.g. the floating menu
"curve" button) cascades through the R3F drei <Html> portal into the
grid, fires grid:click on the just-mounted tool, and commits the drag
before any preview move runs. The new useDragAction was missing this
guard, so the Stage D fence curve port "click → place sfx → exit"
without ever letting the user adjust.

Adds `activationGraceMs` (default 150) on useDragAction; ports the
sfx:item-place commit emission into FenceCurveTool so the kind-owned
tool matches legacy UX. Same guard will cover the upcoming Stage D
ports (endpoint move, whole-fence move, placement, plus
slab/ceiling/wall D).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:29:04 -04:00
Wassim SAMADandClaude Opus 4.7 8ca9686b27 Phase 5 Stage D: fence curve affordance → registry DragAction
First Stage D port — `CurveFenceTool` (178 LoC legacy) split into a pure
`DragAction` primitive (`packages/nodes/src/fence/actions/curve.ts`) plus
a thin React wrapper (`packages/nodes/src/fence/curve-tool.tsx`) that
feeds it through `useDragAction`. The kind declares the affordance via
`def.affordanceTools.curve`; ToolManager lazy-loads it at runtime when
`useEditor.curvingFence` activates. Falls back to the legacy
`CurveFenceTool` for any kind that hasn't been ported.

The lazy-load dispatch dodges the editor→nodes circular dep (nodes
already depends on editor for `useDragAction` + `CursorSphere`).

Establishes the pattern for the remaining fence affordances
(`MoveFenceEndpoint`, `MoveFence`, placement) and for slab/ceiling/wall
D ports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:18:30 -04:00
Wassim SAMADandClaude Opus 4.7 9aec943740 Phase 5 Stage C: item floor plan — all 9 kinds now at Stage C
Item is the last Stage A-only kind to gain def.floorplan. Closes Stage C
across every registered kind.

Files added:
 - nodes/src/item/floorplan.ts: buildItemFloorplan inlines a self-
   contained parent-chain transform walker using `ctx.resolve`. Mirrors
   the legacy `getItemFloorplanTransform` math from editor/lib/floorplan/
   items.ts:
     * Wall parent: rotate item.position by wall's angle, anchor at
       wall.start, handle wall-side attachTo via wall.thickness offset.
     * Item parent (nested): recurse for parent's transform.
     * Level / slab / ceiling parent: item.position is level-local.
   Returns a rotated width × depth rectangle. asset.floorPlanUrl image
   overlay deferred for Phase 5 follow-up.

Files changed:
 - nodes/src/item/definition.ts: wires `floorplan: buildItemFloorplan`.
 - floorplan-panel.tsx: floorplanItemEntries useMemo short-circuits to
   [] when nodeRegistry.has('item'). Phase 6 deletes the entire useMemo.

Stage C coverage (all 9 registered kinds):
 shelf  spawn  fence  slab  ceiling  wall  door  window  item 

Next sessions: Stage B for door / window / wall (each large geometry
extraction), Stage D per kind (DragAction affordance ports), Stage E
(drop legacy panels), Phase 6 Stage F cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:50:30 -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 969b154b08 Phase 5 depth-first: spawn C, fence B+C, slab B+C, ceiling C
Depth-first session: drive registered kinds through Stage B (pure
def.geometry, drop system re-export) and Stage C (def.floorplan,
short-circuit legacy inline rendering in floorplan-panel.tsx).

spawn → C
 - buildSpawnFloorplan wired on definition (was written but deferred
   to avoid double-render).
 - floorplan-panel.tsx's floorplanSpawnEntries useMemo short-circuits
   to [] when nodeRegistry.has('spawn').

fence → B
 - generateFenceGeometry exported from viewer; buildFenceGeometry
   wraps it in a Group+Mesh with DEFAULT_STAIR_MATERIAL.
 - def.geometry set; renderer + system fields dropped.
 - Deleted nodes/src/fence/{renderer.tsx,system.tsx}.

fence → C
 - buildFenceFloorplan: polyline along centerline (sampled for curved
   fences via sampleWallCenterline from core). Stroke width = node.thickness.
 - floorplan-panel.tsx's floorplanFenceEntries short-circuits.

slab → B
 - generateSlabGeometry exported from viewer; buildSlabGeometry wraps
   it in a Group+Mesh + cached material (preset / custom / default
   pattern preserved from legacy renderer).
 - def.geometry set; renderer + system fields dropped.
 - Deleted nodes/src/slab/{renderer.tsx,system.tsx}.

slab → C
 - buildSlabFloorplan: SVG path with outer polygon + hole subpaths
   (uses getRenderableSlabPolygon from core for wall-clipping parity).
 - floorplan-panel.tsx's slabPolygons short-circuits.

ceiling → B INTENTIONALLY SKIPPED
 - Ceiling renderer renders React children (hosted items) + uses TSL
   shader materials + named meshes that other systems poke
   (getObjectByName('ceiling-grid')). Pure def.geometry can't preserve
   that. Ceiling keeps def.renderer (the custom escape hatch) — same
   pattern item uses. Documented in ceiling/definition.ts.

ceiling → C
 - buildCeilingFloorplan: dashed-outline path with hole subpaths
   (visually distinct from slab since ceilings are above).
 - floorplan-panel.tsx's ceilingPolygons short-circuits.

Per-kind progress after this session:
 - shelf: B  C  (Stage E since brand-new)
 - spawn: A  C 
 - wall: A  (B blocked on ctx.levelData design)
 - fence: A  B  C 
 - slab: A  B  C 
 - ceiling: A  C  (B intentionally not applicable)
 - door / window / item: A  (B+C pending in future sessions)

Known test issue: `bun test` in packages/nodes fails to load
`three-bvh-csg` through the viewer's transitive imports (UMD/ESM
mismatch in Bun's test runner). The Next.js editor build works fine
because it bundles differently. Fix requires either dynamic imports
(breaks sync def.geometry contract) or test env config — deferred.
Other tests (schema, geometry, parity) pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:14:22 -04:00
Wassim SAMADandClaude Opus 4.7 8d65be17fa Phase 5 batch kind: item migrates to registry (always-on)
Item is the first kind to use the `def.renderer` escape hatch (custom
React component with `useGLTF` + drei + interactive widgets) — not
expressible as a pure `def.geometry`. Catalog-backed + multi-host
(free / wall / wall-side / ceiling).

Files added (packages/nodes/src/item/):
 - schema.ts: re-exports ItemNode from core.
 - parametrics.ts: empty groups[]. Item parametrics come from the
   asset's catalog-defined interactive controls (toggles / sliders /
   temperature) — too dynamic for the auto-inspector at Stage A.
   Legacy ItemPanel renders the catalog-driven controls; Phase 5
   Stage E will likely use parametrics.customPanel.
 - definition.ts: capabilities (no `movable` — item move is bespoke
   MoveItemContent that handles attachTo transitions floor↔wall↔
   ceiling mid-drag; capability-driven dispatch keeps legacy mover),
   parametrics, renderer (wrap-export of ItemRenderer), system
   bundling ItemSystem + ItemLightSystem, toolHints matching the
   user's screenshot (Place item / R rotate ccw / T rotate cw /
   Shift free place / Esc cancel). defaults() casts an object
   literal with a stub asset since asset is required by the schema;
   createNode re-parses through ItemNode at runtime.
 - renderer.tsx: wrap-export of legacy ItemRenderer (~280 lines with
   useGLTF + interactive widgets — too much to duplicate at Stage A).
 - system.tsx: bundles ItemSystem + ItemLightSystem.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for ItemRenderer,
   ItemSystem, ItemLightSystem.
 - packages/nodes/src/index.ts: appends itemDefinition.
 - packages/editor/src/components/ui/panels/item-panel.tsx: panel
   slider-drag fix recipe applied (nodeRef pattern, drop subscribed
   updateNode dep, drop node from useCallback deps). Item panel has
   scale + position + rotation sliders all subject to the cascade.

Item is the registry's stress test for `def.renderer` escape hatch.
GLB loading via useGLTF + drei works as-is; nothing in the registry
forces a pure-geometry shape on kinds that don't fit.

Phase 5 progress: shelf  spawn  wall  fence  slab  ceiling 
door  window  item . Nine kinds on the registry. Stair / roof /
zone / containers remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:23:08 -04:00
Wassim SAMADandClaude Opus 4.7 9eced06f32 Phase 5 batch: door + window migrate to registry (always-on)
Both kinds share traits — hosted on walls, cuttable, animated open/
close state via a geometry system + animation system. Stage A
migration: register + wrap-export the legacy renderer + bundle both
per-kind systems. Pure geometry + floor-plan ports are later
milestones.

Files added (packages/nodes/src/door/, packages/nodes/src/window/):
 - schema.ts: re-export from core.
 - parametrics.ts: minimal — dimensions only. Door has 29 sliders +
   segmented controls + presets in its legacy panel; window has 15+
   sliders. Auto-inspector can't cover them at Stage A — legacy
   panel keeps rendering via panel-manager.tsx case fall-through.
   Stage E may extend parametrics or use parametrics.customPanel
   escape hatch.
 - definition.ts: capabilities (no `movable` — wall-bound drag is
   bespoke; capability-driven dispatch keeps legacy MoveDoorTool /
   MoveWindowTool), parametrics, renderer, system. defaults() uses
   `DoorNode.parse({...stub})` to leverage zod's schema-level
   `.default()` annotations — door has 40+ fields, window has 20+;
   listing them inline duplicates the schema.
 - renderer.tsx: wrap-export of legacy DoorRenderer / WindowRenderer
   (thin 33-36 lines each).
 - system.tsx: bundles each kind's TWO systems — DoorSystem +
   DoorAnimationSystem, WindowSystem + WindowAnimationSystem. Both
   per-kind systems mount via RegisteredSystems when the kind is
   registry-driven; `<LegacySystem kind="door|window">` wrappers
   around each individual system short-circuit.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for DoorRenderer,
   DoorSystem, DoorAnimationSystem, WindowRenderer, WindowSystem,
   WindowAnimationSystem.
 - packages/nodes/src/index.ts: appends doorDefinition + windowDefinition.
 - packages/editor/src/components/ui/panels/door-panel.tsx + window-
   panel.tsx: panel slider-drag fix recipe applied. Drop the
   subscribed `updateNode` action, drop the `node` dep from
   handleUpdate / previewDoorUpdate / commitDoorPreview useCallbacks.
   Use useScene.getState() inside. Door panel has 29 SliderControls,
   window 15+ — both at high risk of the Maximum update depth
   cascade without the fix.

Phase 5 progress: shelf , spawn , wall , fence , slab , ceiling ,
door , window . Eight kinds on the registry. Item / stair / roof /
zone / containers remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:10:17 -04:00
Wassim SAMADandClaude Opus 4.7 2dd50fa5be Phase 5 batch kind: ceiling migrates to registry (always-on)
Structurally identical to slab. Stage A migration: registers the kind,
wraps the legacy renderer + system, applies the panel slider-drag fix
recipe.

Files added (packages/nodes/src/ceiling/):
 - schema.ts: re-exports CeilingNode from core.
 - parametrics.ts: height slider only. Polygon + holes via floor-plan
   editors.
 - definition.ts: capabilities (no `movable`, `surfaces.top` mapped to
   `height`), relations (hosts: ['item'] for ceiling-mounted lights /
   fans, cascadeDelete: 'descendants'), toolHints, parametrics.
 - renderer.tsx: wrap-export of legacy CeilingRenderer. The legacy
   renderer uses TSL shader code for grid-line patterns (~100 lines);
   not worth duplicating at Stage A. Per-stage migration plan in
   plans/editor-node-registry.md moves the renderer body into this
   folder at Stage B/F.
 - system.tsx: re-exports legacy CeilingSystem.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for CeilingRenderer
   + CeilingSystem.
 - packages/nodes/src/index.ts: appends ceilingDefinition.
 - packages/editor/src/components/ui/panels/ceiling-panel.tsx: panel
   slider-drag fix recipe applied (nodeRef pattern, useScene.getState()
   inside handler, drop subscribed updateNode dep) so the height
   slider doesn't trigger the same cascade fence + wall + slab fixed.

Phase 5 progress: shelf , spawn , wall , fence , slab , ceiling .
Six kinds on the registry. Door / window / item / stair / roof / zone
follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:32:01 -04:00
Wassim SAMADandClaude Opus 4.7 4891f681f3 Phase 5 batch kind: slab migrates to registry (always-on)
Same shape as fence — thin renderer + system re-export, capabilities
declared, panel slider-drag fix recipe applied. Pure geometry +
floor-plan ports are later milestones.

Files added (packages/nodes/src/slab/):
 - schema.ts: re-exports SlabNode from core.
 - parametrics.ts: elevation slider only. Polygon + holes edited
   via floor-plan boundary / hole editors, not number inputs.
 - definition.ts: capabilities (no `movable` — slab move is bespoke
   whole-translation through MoveSlabTool that integrates with the
   boundary editor; capability-driven dispatch keeps the legacy mover),
   surfaces.top with elevation-as-height for stacked items, relations
   (hosts: ['item'], cascadeDelete: 'descendants'), toolHints (trace /
   finish / cancel for the placement tool).
 - renderer.tsx: thin placeholder mesh + markDirty on mount + node
   events + cached material via the same getSlabMaterial pattern as
   the legacy renderer (preset apply on shared material instance).
 - system.tsx: re-exports the legacy SlabSystem from viewer.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: exports SlabSystem (already had
   DEFAULT_SLAB_MATERIAL, applyMaterialPresetToMaterials, createMaterial
   from earlier exports).
 - packages/nodes/src/index.ts: appends slabDefinition unconditionally
   to builtinPlugin.nodes.
 - packages/editor/src/components/ui/panels/slab-panel.tsx: applied
   the panel slider-drag fix recipe from plans/editor-node-registry.md
   prophylactically (nodeRef pattern, useScene.getState().updateNode
   inside handler, drop subscribed updateNode dep). Slab's elevation
   slider is the only drag-driven control in the panel — would have
   triggered the same Maximum update depth cascade as wall/fence.

No behavior change. Slab now mounts via the registry path, but the
legacy SlabSystem still does the actual polygon triangulation + hole
CSG work (re-exported, not duplicated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:12:45 -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 9883f1cdc1 Phase 5 first batch kind: fence migrates to registry behind feature flag
Same shape as wall milestone B — thin renderer + system re-export, no
geometry / floor-plan / tool ports yet (later milestones). Feature flag
NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE gates the dispatch flip.

Files added (packages/nodes/src/fence/):
 - schema.ts: re-exports FenceNode from core.
 - parametrics.ts: dimensions / posts / style fields for the auto-
   inspector. Endpoints + curveOffset edited via tools, not in
   parametrics.
 - feature-flag.ts: mirrors the wall flag pattern.
 - definition.ts: capabilities (snappable + surfaces sides +
   selectable + duplicable + deletable), relations (linkedBy
   endpoint-match, no hosts, no affectsSpatial — matches legacy),
   parametrics, renderer, system, toolHints (Left click / Shift /
   Esc — fence has no helper file today so this adds a panel where
   there wasn't one). Tool field absent: fence has 4 tools (build,
   curve, move, move-endpoint) wired through editor state, not the
   registry dispatch — they keep running unchanged.
 - renderer.tsx: thin placeholder mesh + markDirty on mount + node
   events + DEFAULT_STAIR_MATERIAL (matches legacy material reuse).
   Verification log fires once on first mount.
 - system.tsx: re-exports the legacy FenceSystem from viewer.
   Verification log on mount/unmount confirms the bundle activates.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new exports for FenceSystem and
   DEFAULT_STAIR_MATERIAL so the @pascal-app/nodes bundle can
   compose them without reaching into viewer internals.
 - packages/viewer/src/components/renderers/fence/fence-renderer.tsx:
   paired one-shot legacy verification log so the dispatch path is
   unambiguous from the browser console.
 - packages/nodes/src/index.ts: conditional fenceEntries appended to
   builtinPlugin.nodes based on isFenceRegistryEnabled. With the flag
   off (default), behavior is unchanged; with it on, Phase 0 shims
   switch fence to the registry path — legacy <FenceRenderer> and
   <LegacySystem kind="fence"><FenceSystem /></LegacySystem> short-
   circuit, the bundled system.tsx re-mounts FenceSystem via
   RegisteredSystems, and the new renderer takes over the dispatch.

No behavior change with the flag off. With it on, behavior should be
byte-identical (same FenceSystem code, same priority, same geometry
path).

Phase 5 batch order continues with slab / ceiling / door / window /
item / etc. as flagged migrations after fence parity signs off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:50:41 -04:00
Wassim SAMADandClaude Opus 4.7 d10d7f8deb Phase 4 follow-on: floor-plan interaction + def.toolHints + spawn floorplan builder
Three additions on top of the floor-plan registry contract:

1. def.toolHints + RegisteredToolHelper (registry contract for the
   shortcut hint panel)

   - New ToolHint type in core: { key, label } static array.
   - Added `def.toolHints?: ToolHint[]` to NodeDefinition.
   - New <RegisteredToolHelper hints={...}> in editor — same visual
     styling as WallHelper / ItemHelper but data-driven.
   - HelperManager: registry-first check before falling through to the
     hand-written per-tool switch. Per-tool helper files get deleted
     as their kind migrates `toolHints` in.
   - Shelf + spawn definitions ship toolHints today; wall ports in
     Phase 3 Milestone C alongside its tool/affordance port.

2. Floor-plan interaction layer (selection + drag-to-move)

   - <FloorplanRegistryLayer> now wraps each entry in an interactive
     <g>:
       * Click → useViewer.setSelection({ selectedIds: [id] }).
         Selection visual is a thicker accent-colored stroke applied
         via withSelectionStyle() recursion through the FloorplanGeometry
         tree — kinds don't author selection decoration.
       * Drag → imperative SVG transform during the gesture, single
         updateNode commit on pointerup. Same "smooth move" pattern as
         MoveRegistryNodeTool for 3D drag: no per-tick store update,
         no React re-render storm, no zundo bloat. Coordinate
         conversion via svg.getScreenCTM().inverse().
       * useScene.temporal.pause/resume brackets the gesture so one
         drag = one undo step.
   - Global pointermove / pointerup listeners so the gesture survives
     the cursor leaving the entry's bounding box (matches the legacy
     elevator-resize-drag and item-drag patterns in floorplan-panel).

3. Spawn floor-plan builder (deferred wiring)

   - buildSpawnFloorplan written but NOT wired on the definition —
     spawn already renders in the legacy floorplan-panel.tsx via
     `floorplanSpawnEntries`, and wiring def.floorplan now would
     double-render. The pure builder lives in nodes/src/spawn/
     floorplan.ts ready to wire when the legacy inline branch is
     removed (Phase 5 spawn-floorplan migration PR — same shape as
     wall's feature flag, but per kind inside the legacy panel).

Plan updated: floor-plan interaction section locks the click/drag
contract in, wall-floor-plan-as-legacy note flags everything advanced
the user sees today as legacy that ports alongside Milestone C.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:51:22 -04:00
Wassim SAMADandClaude Opus 4.7 ac36297e7e Phase 4 follow-on: registry-driven floor-plan rendering + shelf port
Adds the floor-plan side of the three-checkbox composition model
documented in wiki/architecture/node-definitions.md. Mirrors the 3D
side (def.geometry → <GeometrySystem> → <ParametricNodeRenderer>) but
emits SVG primitives instead of three.js Object3Ds, and runs inside
the floor-plan panel.

Type-side (packages/core/src/registry/types.ts):
 - New FloorplanGeometry tagged union covering path / polygon /
   polyline / rect / circle / line / group. FloorplanStyle props map
   straight to SVG attributes. Coordinates are level-local meters;
   rotations are radians (three.js convention).
 - New `def.floorplan?: (node, ctx) => FloorplanGeometry | null` field
   on NodeDefinition, independent of `geometry` and `renderer`. Re-
   exported via packages/core/src/registry/index.ts.

Runtime (packages/editor):
 - <FloorplanGeometryRenderer> walks the FloorplanGeometry tree and
   emits the matching React-SVG elements. Pure data → DOM; no per-kind
   logic.
 - <FloorplanRegistryLayer> reads the active levelId, walks the level
   subtree, looks up each node's def.floorplan, builds a
   GeometryContext, calls the builder, and renders the output via
   FloorplanGeometryRenderer.
 - Mounted in floorplan-panel.tsx just before <FloorplanMarqueeLayer>
   so registry-driven kinds layer above legacy inline content.

Shelf migration (proof port):
 - New nodes/src/shelf/floorplan.ts — buildShelfFloorplan(node) emits
   a group with the rotation/translation transform and a width × depth
   rectangle in the shelf's color. Brackets omitted (hidden under top
   board from above).
 - Wired to shelfDefinition.floorplan. Shelf now appears in the floor
   plan view for the first time (was missing from the legacy panel's
   inline switch).

Pattern proven; every future kind migrating in Phase 5 follows the
same shape: a pure (node, ctx) => FloorplanGeometry function. As kinds
register their floor-plan builders, the corresponding inline branches
in floorplan-panel.tsx become dead code and can be deleted in the
same PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:34:42 -04:00
Wassim SAMADandClaude Opus 4.7 3f3818f3b0 Phase 4: generic GeometrySystem + ParametricNodeRenderer; shelf ports off renderer/system files
Lands the three-checkbox composition runtime documented in
wiki/architecture/node-definitions.md. A kind with only a pure
geometry function now needs zero per-kind React or system code.

Type-side additions (packages/core/src/registry/types.ts):
 - New `GeometryContext` (resolve / children / siblings / parent) — read-
   only scene access for builders that reference other nodes by ID
   (wall miters, door cutouts). Most kinds ignore it.
 - New `geometry?: (node, ctx) => Object3D` field on NodeDefinition,
   independent of renderer/system. Three orthogonal opt-ins replace the
   v0 RendererSource union.
 - Re-exported via packages/core/src/registry/index.ts (consumed by
   nodes packages through `export * from './registry'`).

Runtime (packages/viewer):
 - New <GeometrySystem> (systems/geometry/geometry-system.tsx) walks
   dirtyNodes, builds a GeometryContext per dirty node, calls
   def.geometry, disposes old children, attaches new ones, clearDirty.
   Frame priority 2 (matches the priority shelf's per-kind system had).
   Mounted in viewer/index.tsx alongside <RegisteredSystems>.
 - New <ParametricNodeRenderer> (components/renderers/parametric-node-
   renderer.tsx) — empty <group> + useRegistry + useNodeEvents +
   markDirty-on-mount + useLiveTransforms. Mounts hosted children via
   <NodeRenderer> recursively. The default renderer for any registered
   kind without a custom def.renderer.
 - <NodeRenderer> dispatch updated: custom renderer wins, else
   geometry-only kinds fall through to ParametricNodeRenderer, else
   null (legacy switch fallback). Documented inline.

Shelf migration (proof of the boilerplate collapse):
 - Deleted nodes/src/shelf/renderer.tsx (was 45 lines of registry +
   handler boilerplate).
 - Deleted nodes/src/shelf/system.tsx (was 60 lines of dirty-loop +
   dispose plumbing).
 - shelfDefinition now: `geometry: buildShelfGeometry`. One line.
   buildShelfGeometry is the pure function from geometry.ts that already
   existed.

End-to-end effect: registry-driven shelf now mounts via the framework's
generic renderer + system. Parametric edits flow through the same
dirty-driven rebuild path, but the kind ships ~100 fewer lines of
boilerplate. Every future kind that fits the same shape (item, fence
segment, column, etc. as they migrate in Phase 5) follows the same
"one line, one pure function" pattern.

Wall stays on its dedicated def.renderer + def.system — its mitering
needs level-batch context (`ctx.levelData?.miters`, future extension)
that the generic system doesn't yet provide. Decided at Phase 3+, not
blocking Phase 4 acceptance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:03:51 -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
Wassim SAMADandClaude Opus 4.7 7f24593041 Shelf: move geometry build into a system, slim renderer
Follows the renderer/system split documented in wiki/architecture/
renderers.md and systems.md: the renderer must not run geometry
generation. Mirrors the door-renderer/door-system pattern.

- New ShelfSystem reads dirtyNodes in useFrame, retrieves the shelf's
  registered Group from sceneRegistry, swaps its children with the
  output of buildShelfGeometry(node), then clears the dirty flag.
  Geometry rebuild is fully imperative — no React work involved.
- ShelfRenderer is now a thin empty <group> that registers with
  sceneRegistry, marks the node dirty on mount, and carries the
  pointer-event handlers + live transform overrides at the root.
- Wired system into shelfDefinition so RegisteredSystems mounts it
  alongside the renderer.

Net effect: dragging shelf parametric sliders no longer re-renders
the renderer per tick — the system rebuilds meshes at frame cadence
based on dirtyNodes, the inspector's per-field subscription only
re-renders the dragged field, and the rest of the React tree stays
quiet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:51:36 -04:00
Wassim SAMADandClaude Opus 4.7 166e860bfe Move: pure imperative via sceneRegistry (smooth, zero re-renders); drop dev logs
User feedback: updating the store per tick caused tons of React
re-renders → laggy drag. Switched to pure imperative.

MoveRegistryNodeTool now:
- Mutates `sceneRegistry.nodes.get(id).position` directly per
  grid:move tick. No useScene.updateNode during drag. No store
  change → no renderer re-render → R3F doesn't reapply
  `position={node.position}` → the imperative mutation sticks.
- On commit: single tracked `useScene.updateNode(id, { position })`.
  Undo replays one step (original → final), no per-tick spam.
- On cancel / unmount: imperatively snap the mesh back to original.
  Store was never touched so no data revert needed.

Trade-off vs the items pattern (which does update the store per tick
and re-renders per tick): our approach is faster but assumes the
renderer doesn't re-render mid-drag. Items get away with constant
re-renders because their renderer is heavily optimized; for parametric
shelves (and future kinds) the imperative path is simpler and faster.

Cleanup: removed the dev `[shelf] rendered` and `[shelf] placed`
console.info logs from the shelf renderer and tool. They were Phase 2
verification scaffolding — no longer needed now that everything works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:19:37 -04:00
Wassim SAMADandClaude Opus 4.7 e84b1ec8bc Add preview slot to NodeDefinition; show translucent shape during move
User feedback: the move tool's CursorSphere is just a vertical line,
hard to tell what you're moving. Placement gets a translucent shape
that follows the cursor; move should too.

NodeDefinition.preview?: () => Promise<{ default: ComponentType<{ node }> }>
opt-in lazy component that renders a translucent ghost of the node.
Used by:
- The placement tool (ShelfTool) — renders the preview at the cursor
  position so the user sees the shape they're placing.
- The move tool (MoveRegistryNodeTool) — renders the preview at the
  drag target alongside the CursorSphere. Plus the original node is
  also dragged via live transforms, so the user sees both: the actual
  node moving + a translucent ghost at the same spot.

Implementation:
- New nodes/shelf/preview.tsx: ShelfPreview component. Renders the
  same shape as ShelfRenderer but `transparent: true, opacity: 0.5`.
- shelfDefinition.preview = () => import('./preview').
- ShelfTool's placement preview now uses <ShelfPreview node={defaults} />
  instead of an inline copy of the box geometry.
- MoveRegistryNodeTool lazy-loads `def.preview` (cached by loader,
  Suspense-wrapped). If a kind doesn't define `preview`, only the
  CursorSphere shows — matches today's behavior.

Phase 4 may merge `preview` with `renderer` behind an `opacity` prop
so kinds don't duplicate JSX between the solid and translucent
versions; until then defining `preview` is opt-in and one extra file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:01:32 -04:00
Wassim SAMADandClaude Opus 4.7 6d97a87547 Selection: registry-driven, drop spawn flag, restore green color
Two concerns from the spike:

1) Selection / floating-action-menu had hardcoded kind lists scattered
   across 4 files. Adding 'shelf' to each one per migration was the
   wrong abstraction — the user's question "did you make it generic
   from the noderegistry?" was the right one. Done now.

   Added to @pascal-app/core/registry:
   - getSelectableKinds(): string[] — returns all registered kinds
     whose definition declares `capabilities.selectable`.
   - isRegistrySelectable(kind): boolean — predicate for OR-chains.

   Refactored hardcoded sites to merge registry kinds at runtime,
   keeping legacy hardcoded lists intact so existing kinds keep
   working unchanged:
   - editor SelectionManager: 4 subscription loops (enter/leave/click)
     + structure.isValid + getSelectionTarget — all augment with
     registry kinds. Phase 6 deletes the hardcoded lists.
   - viewer SelectionManager: subscription loop + SelectableNodeType
     broadened with `(string & {})` to accept registry kinds.
   - floating-action-menu: ALLOWED_TYPES OR'd with isRegistrySelectable.
   - Removed the manually-added 'shelf' entries from previous commit
     857ddd4; they were redundant once the registry-driven path landed.

   Future built-in nodes that declare `capabilities.selectable` get
   click-selection + hover + the floating action menu (move/delete
   icons) for free, no editing of these 4 files.

2) Spawn parity is signed off. Drop the
   NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag entirely; spawn registers
   unconditionally in builtinPlugin.nodes. Restored SPAWN_COLOR to
   the original #22c55e green (was #ef4444 red as a Phase 2
   verification marker).

Pre-existing typecheck errors in editor (ceiling/fence/slab tree-node,
scene.ts buildingId) are unchanged.

630 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:22:51 -04:00
Wassim SAMADandClaude Opus 4.7 d17e083c77 Add sfx:grid-snap on cursor cell-cross for shelf + spawn
Matches the wall / slab / curve-wall tool pattern: emit
sfx:grid-snap only when the snapped position changes (cursor crosses
a grid cell), not every frame of mouse movement within the same cell.
Tracked via a `previousSnapRef` per tool, reset when the tool
re-activates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:01:56 -04:00
Wassim SAMADandClaude Opus 4.7 76794ceb7d Wire shelf + spawn placement polish: SFX, cursor, sidebar, selection
User-visible follow-ups after first running the Phase 2 spike.

Spawn tool now matches legacy UX:
- CursorSphere from @pascal-app/editor for the placement indicator
  (ring + line + tool-icon tooltip) — was a plain sphere mesh.
- Emits sfx:structure-build on commit + setTool(null) + setMode
  ('select') to exit build mode, matching legacy spawn-tool.

Shelf tool placement:
- Emits sfx:structure-build on commit.
- Cursor preview now shows top board + brackets (was just the top),
  matching what gets placed.

Shelf selectable from the 3D canvas:
- ShelfEvent type added to @pascal-app/core/events/bus.
- 'shelf' added to NodeConfig in useNodeEvents.
- ShelfRenderer wires `useNodeEvents(node, 'shelf')` handlers onto
  every mesh. Clicks/hovers now bubble through the editor's selection
  manager and update useViewer.selection.

Shelf appears in the sidebar:
- ShelfTreeNode component (mirrors spawn-tree-node's shape +
  selection/hover/rename wiring; lucide Layers icon).
- TreeNode dispatcher adds a `case 'shelf':` arm.

Framework changes:
- @pascal-app/editor exports CursorSphere alongside triggerSFX.
- @pascal-app/nodes now declares @pascal-app/editor as peer/dev dep.

Pre-existing typecheck errors in @pascal-app/editor (ceiling-tree-node,
fence-tree-node, slab-tree-node, scene.ts) are unchanged — present on
main and not introduced by this commit.

630 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:55:03 -04:00
Wassim SAMADandClaude Opus 4.7 a89a1efccf Fix spawn flag inlining + shelf cursor frame + simpler renderer
Three concrete bugs surfaced when first-running the spike in community:

1) NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag never detected:
   The previous readEnvFlag used dynamic bracket access
   (`env?.[name]`), which Next.js / Turbopack does NOT substitute at
   build time. Only literal `process.env.NEXT_PUBLIC_FOO` references
   get inlined into the client bundle. Switched to literal access
   plus a `typeof process` guard. Spawn now toggles via the flag as
   designed.

2) Shelf cursor appeared offset from the mouse:
   The cursor mesh lives inside the ToolManager's building-local
   group, but the tool was setting `cursorRef.current.position` to
   level-local coordinates (computed via `worldToLocal(level)`).
   Result: cursor shifted by (building-pos − level-pos) in worst
   case. Switched cursor display to use `event.localPosition`
   (already building-local) with grid snap — matches the legacy
   spawn-tool pattern. The commit path keeps the worldToLocal(level)
   conversion since the shelf node's `position` field is stored
   relative to its level parent.

3) Shelf rendered invisibly after click (suspected):
   The renderer used a useEffect-swap pattern where it mounted an
   empty <group> and imperatively added Three.js children from a
   buildShelfGeometry() Group. Plausibly fragile under StrictMode
   double-invoke or fast HMR. Switched to inline R3F JSX — top
   board + brackets as plain <mesh> primitives. The pure geometry
   function still exists in geometry.ts for tests and AI-authored
   consumers; renderer just doesn't go through it.

Diagnostics added (dev-only; removed once spawn parity ships):
- `[shelf] placed <id> level-local <pos> parent <levelId>` on click
- `[shelf] rendered <id> at <pos>` on mount

Also: types: ["node"] in nodes/tsconfig.json so the typeof process
guard typechecks cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:27:08 -04:00
Wassim SAMADandClaude Opus 4.7 ac71c1a83b Fix shelf cursor tracking + bigger snap step; tint registry spawn red
Three small follow-ups after the first user-visible spike test.

shelf cursor stuck at origin:
  The placeholder cursor is a translucent slab inside a <group ref>.
  The tool was calling setCursor(state) on every grid:move, which
  triggered a React re-render. R3F re-applies props on each render,
  and since the <group> had no `position` prop, the implicit default
  [0,0,0] clobbered the imperative `position.set` from the previous
  tick. Result: cursor stuck at level origin instead of following
  the mouse.

  Fix: drop the unused useState entirely. Pure imperative position
  updates via the ref. No re-renders, no clobbering. (The legacy
  spawn tool gets away with the same pattern because CursorSphere
  buffers its position prop differently — but for the spike, the
  simpler model is fine.)

shelf snap step:
  Was 0.1 (10cm) — much finer than the editor's default 0.5 grid.
  Bumped to 0.5 (matches the toolbar grid setting and the legacy
  half-meter snap pattern used by spawn/column).

registry-driven spawn renderer paints red:
  Temporary verification marker. With NEXT_PUBLIC_USE_REGISTRY_FOR_
  SPAWN=1, spawns rendered via the new path now appear in #ef4444
  red. Legacy renderer stays in #22c55e green. Easy visual check
  for "which dispatch path is this spawn on?" Reverted in the PR
  that signs off spawn parity (alongside legacy file deletion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:59:49 -04:00
Wassim SAMADandClaude Opus 4.7 b6d77206b4 Phase 2 spike: spawn migration (flagged) + new shelf node
The first time registry-driven nodes actually run in the editor.

Spawn migration (under NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag):
- New packages/nodes/src/spawn/ folder with renderer, tool, schema
  (re-exported from core), parametrics, definition, index.
- Spawn definition appended to builtinPlugin.nodes only when the flag
  is set. With the flag off, the Phase 0 shims fall through and the
  legacy SpawnRenderer / SpawnTool keep ownership.
- New no-props SpawnTool reads activeLevelId from useViewer directly,
  matches legacy placement behavior (half-meter snap, singleton-per-
  level, replace-on-reclick).
- Structural parity test (9 cases) validates definition shape +
  schema identity. Pixel-diff defers to Phase 4 alongside more nodes.

New shelf node (no legacy — registered unconditionally):
- ShelfNode schema in core/schema/nodes/shelf.ts (hand-maintained
  AnyNode union for now; Phase 6 derives the union from the registry
  and moves the schema fully into nodes/shelf/).
- packages/nodes/src/shelf/ folder: pure geometry builder
  (buildShelfGeometry returns a Three.js Group of top board +
  brackets), R3F renderer that mounts the built group, no-props
  placement tool, parametrics descriptor (width/depth/thickness/
  height/bracketStyle/color), definition with surfaces.top stackable
  surface for future stacking, and presentation metadata for the
  palette.
- 13 unit tests across schema bounds and geometry behavior.
- Palette wiring: 'shelf' added to StructureTool union + an entry in
  the structure-tools array (placeholder icon, replaced in Phase 4
  when palette is registry-driven).

Framework changes:
- @pascal-app/viewer now exports useNodeEvents from its public barrel
  so node bundles in @pascal-app/nodes can subscribe to node-specific
  pointer events. (Used by spawn renderer; shelf renderer skips it
  for now since useNodeEvents has a hardcoded kind list — Phase 4
  generalizes it via the registry.)
- @pascal-app/nodes gains @pascal-app/viewer as a peer + dev dep so
  node bundles can import from it.

630 tests pass across 76 files (22 new this phase). Editor app
continues to ship green with both legacy spawn and the new shelf
node co-existing through the Phase 0 dispatch shims.

To validate end-to-end in dev:
- bun dev:community → open editor → click 'Shelf' in structure
  toolbar → click to place. Confirms full registry path
  (NodeRenderer dispatch + ToolManager dispatch + sceneRegistry
  byType Proxy).
- Set NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN=1, restart dev, place spawn
  → visually identical to legacy. Confirms parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:07:38 -04:00
Wassim SAMADandClaude Opus 4.7 fc36b5b600 Add @pascal-app/nodes package skeleton with empty builtinPlugin
New workspace package that owns built-in node bundles, one folder per
kind. Today the plugin is empty — no behavior change. Future commits
will add column/, shelf/, wall/, etc. and append each definition to
builtinPlugin.nodes.

The package depends on core (registry types) at v1; viewer, editor,
react, three are declared as peer deps so future node bundles can use
them without bumping their own version on every monorepo bump.

External plugins land as separate packages with the exact same shape
— this package is the dogfooded reference.

Tests: builtinPlugin shape + loadPlugin succeeds with zero kinds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:24:59 -04:00