Commit Graph
1044 Commits
Author SHA1 Message Date
Wassim SAMADandClaude Opus 4.7 11004d90ad review-architecture: absorb node-registry infrastructure
Phase 6 of the node-registry plan landed the @pascal-app/nodes package,
the three-checkbox composition model (def.geometry / def.renderer /
def.system), capability-driven move dispatch, and the deletion of
per-kind dispatch / per-kind files across viewer + editor. Update the
review skill so it catches reintroductions of those legacy patterns
and reviews registry-driven additions against the new contract.

- Add packages/nodes as the 4th layer in the package-boundary pass.
- Add a "Node registry & composition" checklist section.
- Require reviewers to read node-definitions.md and plugin-authoring.md.
- Flag new `case '<kind>':` clauses, kind-specific files in legacy
  locations, framework imports of @pascal-app/nodes, force-routing
  bespoke-move kinds through MoveRegistryNodeTool, useLiveTransforms-
  driven drag motion, builders that import useScene, missing
  `__fromGeometry` markers, and v1-plugin-surface overreach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:35:55 -04:00
Wassim SAMADandGitHub e2d9ffbee8 Merge pull request #318 from pascalorg/fix/fence-tool-handlers
fix: restore fence tool handlers dropped by null-guard commit
2026-05-20 08:17:46 -04:00
Wassim SAMADandClaude Opus 4.7 d218cb7f26 fix: restore fence tool handlers dropped by null-guard commit
251af2c added a `?.` on cursorRef in onGridClick but inadvertently
deleted the closing braces, the trailing reset block, and the
onKeyDown/onKeyUp/onCancel handlers — leaving onGridClick syntactically
unterminated and three undefined references at the emitter.on/off sites.
Restores the deleted block verbatim, keeping the optional-chaining null
guard on cursorRef.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:15:26 -04:00
Wassim SAMADandGitHub 820c5d9e3e Merge pull request #315 from pascalorg/portforward/pr-309
portforward: StairOpeningSystem, material library, UI refinements (from #309)
2026-05-20 08:03:47 -04:00
Wassim SAMADandGitHub 70100d5a2d Merge pull request #317 from pascalorg/feat/client-bootstrap-provider
apps/editor: mount node-registry bootstrap at the root layout
2026-05-20 07:18:49 -04:00
Wassim SAMADandClaude Opus 4.7 c4001a656f apps/editor: mount node-registry bootstrap at the root layout
The standalone editor app loaded `lib/bootstrap.ts` as a side-effect
import only from `components/scene-loader.tsx`. That worked for the
`/edit/[sceneId]` route but left every other page (homepage, settings,
viewer-only routes) hitting `<Viewer>` with an empty client-side
registry — node materials resolved to `null` and React surfaced a
`<html>`-level hydration mismatch on first paint.

Fix mirrors the community-app side that landed in pascalorg/private-
editor#27:

 - New `app/client-bootstrap.tsx` — thin client wrapper that imports
   `../lib/bootstrap` and renders children.
 - `app/layout.tsx` mounts `<ClientBootstrap>` around `{children}` so
   every page in the standalone editor gets the registry populated
   before its first `<Viewer>` / `<Editor>` mounts.
 - `lib/bootstrap.ts` switched to **synchronous** built-in registration
   via `registerNode(def)` per kind instead of `await loadPlugin(...)`.
   The previous async kick-off only resolved in a microtask, letting
   the first SSR / hydration pass see an empty registry. External
   plugin discovery (`discoverPlugins()`) stays async and runs via its
   own `loadExternalPlugins()` path, gated by `externalsKickedOff` so
   HMR doesn't re-fetch.
 - `components/scene-loader.tsx` drops the per-page side-effect import
   — the root provider handles it now.

`bun.lock` syncs `@pascal-app/editor` into `@pascal-app/nodes`'s
peerDependencies + devDependencies (already declared in
`packages/nodes/package.json`; only the lockfile lagged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:17:30 -04:00
open-pascal 251af2ce1d fix: add null guards for cursorRef in fence and wall tool click handlers 2026-05-20 00:57:15 +00:00
open-pascal 3adae8acea style: apply biome lint fixes after portforward 2026-05-20 00:54:11 +00:00
sudhirandopen-pascal d48be77f5d Reduce wall rebuilds during window and door placement 2026-05-20 00:53:10 +00:00
sudhirandopen-pascal fe68dbeebd Constrain wall moves along wall normals 2026-05-20 00:53:10 +00:00
sudhirandopen-pascal c31c08fd90 feat: implement StairOpeningSystem to handle automated stair opening updates and adjust camera distance constraints 2026-05-20 00:53:10 +00:00
sudhirandopen-pascal 8b73d7986b feat: expand material library with new wood and flooring textures while cleaning up legacy assets 2026-05-20 00:52:08 +00:00
sudhirandopen-pascal a9d26239b7 Refactor material catalog for roofing and flooring 2026-05-20 00:52:01 +00:00
sudhirandopen-pascal a3378a666d Preserve elevator stop order and handle viewport resize 2026-05-20 00:52:01 +00:00
sudhirandopen-pascal d7a1ce89f3 Scope first-person overlay and unify elevator colors 2026-05-20 00:51:43 +00:00
sudhirandopen-pascal 5bd2f51d15 Make fence drawing continue between segments 2026-05-20 00:51:43 +00:00
sudhirandopen-pascal 5d53be26e5 Refine wall chaining and door arch clipping 2026-05-20 00:51:01 +00:00
sudhirandopen-pascal 023cd511e9 Remove material strips from roof, stair, and fence panels 2026-05-20 00:50:47 +00:00
sudhirandopen-pascal 4fc57a6fd9 Enhance elevator placement preview 2026-05-20 00:49:48 +00:00
sudhirandopen-pascal 89c62800a3 Refine window panel by family 2026-05-20 00:49:41 +00:00
sudhirandopen-pascal 2ee69e24e2 Organize door panel controls by door family 2026-05-20 00:49:41 +00:00
sudhirandopen-pascal 4a402756fc Fix floorplan labels and door/fence geometry 2026-05-20 00:45:33 +00:00
sudhirandopen-pascal cdde88f9c0 Clamp spiral stair openings at full sweep 2026-05-20 00:45:28 +00:00
sudhirandopen-pascal 3800bd7fc8 fix: increase stair opening buffer constraints and set default offset to 0.15 2026-05-20 00:45:25 +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
sudhirandopen-pascal 5f17172638 refactor: make onMove optional in FloorplanActionMenuEntry and remove unused wall move handler 2026-05-20 00:41:28 +00:00
sudhirandopen-pascal 2b9bb769f3 feat: add camera-aware wall move handles and UI layer for floorplan editor, and remove unused elevator icon. 2026-05-20 00:41:26 +00:00
sudhirandopen-pascal db5c743e1c Render curved wall measurements along the wall 2026-05-20 00:41:18 +00:00
sudhirandopen-pascal 6af3a0aa82 Add draft angle arcs for walls and fences 2026-05-20 00:41:11 +00:00
sudhirandopen-pascal e009f3658b Use wall face mitering for draft angle arcs 2026-05-20 00:38:21 +00:00
Wassim SAMADandGitHub 0bcec8e6ba Merge pull request #314 from pascalorg/feat/node-registry-primitives
Node registry & plugin-ready architecture (Phase 0-5)
2026-05-19 20:10:28 -04:00
Wassim SAMADandClaude Opus 4.7 2bf824cc91 floor-elevation: restore stripped import + fix column rotation type
Two follow-up fixes after the FloorElevationSystem landed:

 - `viewer/src/components/viewer/index.tsx`: the auto-format hook
   stripped the `FloorElevationSystem` import a second time, leaving
   the JSX mount unresolved.
 - `packages/nodes/src/column/definition.ts`: column stores Y rotation
   as a scalar `number`, but `floorPlaced.footprint` types the rotation
   field as the full Euler tuple. Wrap as `[0, column.rotation, 0]`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:54:35 -04:00
Wassim SAMAD e3f2343456 nodes: restore floorPlaced imports stripped by the formatter
The previous commit's `floorPlaced` capability used `getScaledDimensions`
in `item/definition.ts` and `ColumnNodeType` in `column/definition.ts`,
but the editor's auto-format hook ran between the import edit and the
body edit and removed both as "unused" — breaking `bun dev`.

Re-add the imports.
2026-05-19 19:49:09 -04:00
Wassim SAMADandClaude Opus 4.7 9a481f2511 registry: generic FloorElevationSystem driven by a floorPlaced capability
Symptom: a shelf placed on a level with a raised slab underneath
visually clipped through it — `ItemSystem` lifted items onto slabs,
but shelves (and other floor-placed registry kinds) had no equivalent
path.

Fix: lift the slab-elevation logic out of `ItemSystem` into a generic
`<FloorElevationSystem>` keyed off a new `capabilities.floorPlaced`
config. Any kind that opts in declares a `footprint(node)` (dimensions
+ rotation used to query overlapping slabs) and an optional `applies`
predicate (skips items whose `asset.attachTo` is wall / ceiling).

The new system runs at frame priority 1 so its `mesh.position.y`
override lands before `ItemSystem` / `GeometrySystem` (priority 2)
clear the dirty mark. The spatial-grid sync's `markNodesOverlappingSlab`
also dropped its hardcoded `item` branch in favour of an iteration over
every registered kind that declares `floorPlaced` — so any new
floor-placed kind picks up slab-driven re-elevation automatically.

Tagged kinds:
 - `item`  — `footprint = getScaledDimensions`, `applies = !asset.attachTo`
 - `shelf` — `footprint = (w, h, d)`
 - `column` — `footprint = (w, h, d)`
 - `spawn` — `footprint = (0.6, 1.8, 0.6)` (marker)

`ItemSystem` retains only the wall-side z-offset block (`mesh.position.z =
wallThickness / 2`). The elevation block + its `getScaledDimensions` /
`resolveLevelId` / `spatialGridManager` imports moved to the generic
system.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:44:21 -04:00
Wassim SAMADandClaude Opus 4.7 78e3ed13d8 floorplan-panel: prune dead constants, drag-state types, and unused helpers
Final sweep after the layers, handlers, and state were dismantled —
pruning the remaining orphan types and constants that only the dead
code referenced.

Removed:
 - 27 dead constants: `FLOORPLAN_*_HOVER_*_STROKE_WIDTH` for wall /
   item / endpoint (consumed by the deleted legacy SVG layers),
   `FLOORPLAN_WALL_OUTER_MEASUREMENT_*` + `FLOORPLAN_WALL_INNER_
   MEASUREMENT_*` + `FLOORPLAN_OPENING_MEASUREMENT_*` palette
   constants (no live measurement consumer), `FLOORPLAN_ITEM_
   CLEARANCE_*` thresholds, `FLOORPLAN_MEASUREMENT_LABEL_*` / `LINE_
   OUTLINE_*` + `FLOORPLAN_ACTION_MENU_OFFSET_Y` / `FLOORPLAN_NODE_
   FOOTPRINT_*` / `FLOORPLAN_SPAWN_*` / `FLOORPLAN_TRACE_*_FILL_
   OPACITY` + several `FLOORPLAN_WALL_*_STROKE_WIDTH` variants.
 - Dead drag-state types: `SlabBoundaryDraft`, `SlabHoleBoundaryDraft`,
   `SlabVertexDragState`, `SlabHoleVertexDragState`, `SlabHoleMoveDraft`,
   `CeilingBoundaryDraft`, `CeilingVertexDragState`,
   `CeilingHoleBoundaryDraft`, `CeilingHoleVertexDragState`,
   `CeilingHoleMoveDraft`, `ZoneBoundaryDraft`, `ZoneVertexDragState`,
   `WallFaceLine`.
 - Dead helpers: `getWallMeasurementOverlay` + `getLinearMeasurementOverlay`
   (both defined but never called now that the measurement layers are
   gone).

Floor-plan panel: 9,006 → 8,651 lines (-355). Cumulative reduction
vs `main`: 17,913 → 8,651 (-9,262 lines, ~52%).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:26:15 -04:00
Wassim SAMADandClaude Opus 4.7 a3dc48a503 floorplan-panel: prune dead drag state, useEffects, and measurement helpers
After the legacy SVG layers and handler callbacks were dismantled, the
backing drag-state useStates, mirror useRefs, and watch-effects became
orphans, along with a handful of top-level math helpers whose callers
were already gone.

Removed:
 - 12 dead `useState` declarations: `slabBoundaryDraft` /
   `slabVertexDragState` / `slabHoleBoundaryDraft` /
   `slabHoleVertexDragState` / `slabHoleMoveDraft` / `ceilingBoundary
   Draft` / `ceilingVertexDragState` / `ceilingHoleBoundaryDraft` /
   `ceilingHoleVertexDragState` / `ceilingHoleMoveDraft` /
   `zoneBoundaryDraft` / `zoneVertexDragState`. All written only by
   the dead vertex-drag handlers and read only by the dead
   `clear*BoundaryInteraction` callbacks and `transientFloorplanFit`
   checks (which collapse to `false` once their inputs are gone).
 - 5 dead `useRef` mirrors of the above (`slabBoundaryDraftRef` etc.)
   plus their mirror `useEffect` writes.
 - 5 dead `clear*BoundaryInteraction` callbacks (slab, slabHole,
   ceiling, ceilingHole, zone) — only called by deleted useEffects
   and by `clearDraft`, where the call became a no-op.
 - 7 dead `useEffect` watchers for `slabVertexDragState`,
   `ceilingVertexDragState`, `slabHoleVertexDragState`,
   `slabHoleMoveDraft`, `ceilingHoleVertexDragState`,
   `ceilingHoleMoveDraft`, `zoneVertexDragState` (each early-returns
   because state is always null, so all listener wiring + commit /
   cancel paths inside were unreachable).
 - 5 dead `shouldShow*BoundaryHandles` flags + the 5 `useEffect`s
   that called `clear*BoundaryInteraction` when they flipped.
 - `selectedSlabEditingHoleIndex` / `selectedSlabEditingHole` /
   `selectedCeilingEditingHoleIndex` / `selectedCeilingEditingHole`
   — derived from now-`null` selectedX entries.
 - Dead drag-state guards in `handlePointerMove`, `handleSvgPointer
   Move`, and the floor-plan fit `useMemo`.
 - Dead top-level helpers: `getRaySegmentIntersection`,
   `getSlabHandlePolygon`, `getSlabVisualOffsets`,
   `getDraftSlabVisualPolygon`, `WallMeasurementFaceContext` type,
   `getWallMeasurementFaceContext`, `getAdjacentOpeningBounds`,
   `getSelectedWallMeasurementOverlays`,
   `getItemDimensionMeasurementOverlays`, `polygonCentroid`.

Floor-plan panel: 10,466 → 9,006 lines (-1,460). Cumulative reduction
vs `main`: 17,913 → 9,006 (-8,907 lines, **~50%**).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:19:34 -04:00
Wassim SAMADandClaude Opus 4.7 ff147626bb floorplan-panel: remove dead action menu, handle layers, selected* state and handlers
Continuing the post-migration cleanup. After registry-driven kinds
absorbed selection chrome, action menus, and boundary editing through
`def.floorplanAffordances`, the legacy plumbing in `floorplan-panel.tsx`
was left mounted with empty inputs — another ~4k LoC of dead code.

Removed:
 - `Editor2dFloorplanActionMenuLayer` mount + its 10 `selectedX
   ActionMenuPosition` useMemos. All ten action menus computed positions
   from empty entry arrays / null `selectedXEntry`, so the layer never
   rendered anything. `FloorplanRegistryActionMenu` is the only mount left.
 - Legacy handle layers: `FloorplanWallEndpointLayer`,
   `FloorplanFenceEndpointLayer`, `FloorplanWallCurveHandleLayer`, and
   the four `FloorplanPolygonHandleLayer` mounts for slab / slab-hole /
   ceiling / ceiling-hole — all rendered from `wallEndpointHandles` /
   `slabVertexHandles` / etc. which were empty after the registry took
   over endpoint, curve, and polygon affordances.
 - `FloorplanZoneLabelLayer` mount + component. Zone labels are now
   emitted as `kind: 'text'` from `def.floorplan` on the zone kind.
 - Dead handlers: every `handle*Select`, `handle*PointerDown`,
   `handle*DoubleClick`, `handle*HoverChange`, `handleFloorplan*Hover
   Enter`, `handleSelected*` (Move/Delete/Duplicate/AddHole/HoleMove/
   HoleDelete/Curve for all 10 legacy kinds), `duplicateSelected*`,
   `handleSelectedWallCurve`, `handleSlabVertex*`/`Midpoint*`/`Edge*`/
   `Hole*` (same for ceiling and zone), `handleWallEndpointPointerDown`,
   `handleFenceEndpointPointerDown`, `handleWallCurvePointerDown`,
   `emitFloorplanNodeClick`, `syncDeleteHoveredId`,
   `handleZoneLabelClick`, `hasDuplicatableFloorplanSelection`,
   `handleDuplicateFloorplanSelection`, `FloorplanDuplicateHotkey` mount.
 - Dead handle data: `wallEndpointHandles`, `fenceEndpointHandles`,
   `wallCurveHandles`, `canCurveSelectedWall`, and all the
   slab/slab-hole/ceiling/ceiling-hole/zone vertex+midpoint+edge handle
   useMemos.
 - Dead selection / measurement useMemos: `selectedItemEntry`,
   `selectedOpeningEntry`, `selectedSpawnEntry`, `selectedFenceEntry`,
   `selectedStairEntry`, `selectedRoofEntry`, `selectedElevatorEntry`,
   `selectedWallEntry`, plus `selectedItemClearanceMeasurements` and
   `movingOpeningPlacementMeasurements` (both 100-200 lines of math
   that consumed the empty entries).
 - Two dead `<FloorplanMeasurementsLayer>` mounts (clearance / opening
   placement) — both fed by useMemos that returned `[]`.
 - `<FloorplanZoneLayer>` mount — `visibleZonePolygons` is always empty.

Kept:
 - `siteVertexHandles` / `siteMidpointHandles` + `<FloorplanPolygonHandle
   Layer>` mount for site — site is the only kind not registry-driven.
 - `FloorplanStairLayer` mount (preview-only, hover/click props swapped
   for noop helpers since the preview isn't interactive).
 - Stub `selectedSlabEntry` / `selectedCeilingEntry` / `selectedZoneEntry`
   as `null` typed values so the remaining hole-editing fallback code
   compiles. Those fallbacks themselves run as no-ops now and can be
   torn down in a follow-up.

Floor-plan panel: 14,395 → 10,466 lines (-3,929). Cumulative reduction
vs `main`: 17,913 → 10,466 (-7,447 lines, ~42%).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:07:59 -04:00
Wassim SAMADandClaude Opus 4.7 c6bef1ec2f floorplan-panel: dismantle legacy SVG layers replaced by the registry
The Phase 5 / 6 migration moved 7 kinds (fence, column, spawn, item,
elevator, stair, roof) to the registry's `def.floorplan` path, but the
legacy SVG layers in `floorplan-panel.tsx` were left mounted with empty
entry arrays — dead code carrying ~3k lines of useless cost in the diff.

Removed:
 - `FloorplanGeometryLayer` (~1.7k lines): inline component rendering
   walls / slabs / ceilings / openings from `wallPolygons` /
   `slabPolygons` / `ceilingPolygons` / `openingsPolygons` — all
   permanently empty stubs after the migration. Wall / slab / ceiling
   / door / window now render via `FloorplanRegistryLayer`.
 - `FloorplanFenceLayer` (~265 lines): fence entries always empty
   post-migration; fence renders via the registry.
 - `FloorplanElevatorLayer` (~420 lines): elevator entries always
   empty post-migration.
 - `FloorplanNodeLayer` (~415 lines): rendered items / spawns / stairs.
   Items + spawns are registry-driven; stair only needed the in-flight
   preview, which is now mounted directly via `FloorplanStairLayer`
   (preserved as a sibling of the registry layer).
 - `FloorplanItemImage` (~40 lines): internal helper used only by
   `FloorplanNodeLayer`.
 - `floorplan-roof-layer.tsx` (113 lines): roof / roof-segment now
   registry-driven.

Net: -2,989 lines from `floorplan-panel.tsx`, plus the deleted roof
layer file. The remaining 14k-line monolith still owns the orchestration
state (selection lookups, marquee, helper lifecycles, hit-test plumbing)
— that's a separate teardown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:53:57 -04:00
Wassim SAMADandClaude Opus 4.7 1ec65acc5f registry: post-migration polish — ceiling 3D selection, ceiling item commit, floorplan move drift
Three bugs surfaced after the Stage E node-registry migration:

1. Ceiling intercepts 3D hover/click selection
   Selecting via the floor-plan helper or the boundary-editor handles
   is the intended flow; a direct 3D click on the ceiling should fall
   through to whatever's underneath. `SelectionManager` now early-returns
   on `ceiling` in onEnter/onLeave/onClick, so `event.stopPropagation`
   is skipped and the ray reaches the item/wall/floor below.

2. Ceiling item placement: final click does nothing
   When a ceiling-attached draft hangs in front of the ceiling-grid
   mesh, the click ray hits the draft first and fires `item:click`,
   not `ceiling:click`. `onItemClick` already forwards self-clicks to
   shelf-surface / item-surface hosts; this PR adds the matching
   ceiling branch so the commit lands on the ceiling under the cursor.

3. Floor-plan item move drift after the commit click
   Two contributing causes, both fixed:

   - `usePlacementCoordinator`'s `useFrame` lerped the draft mesh
     toward `gridPosition.current` (the item's pre-move spot) every
     frame, fighting React's render from `scene.position` while the
     2D `FloorplanRegistryMoveOverlay` drove the move. Gated the lerp
     on a `has3DPointerDrivenMoveRef` flag set on first 3D pointer
     event — pure 3D drags are unchanged.
   - The overlay's pointer-up handler skipped a final `session.apply`
     and committed at the last pointermove position. Browsers don't
     guarantee a pointermove right before pointerup, so a quick click
     after a drag could land a few pixels off. Re-apply at pointer-up
     coords so commit matches where the user actually released.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:32:11 -04:00
Wassim SAMADandClaude Opus 4.7 d747d2f0ea Phase 5 Stage E: full kind migration into packages/nodes
Wholesale move of every remaining kind into its own subdirectory under
`packages/nodes/src/`, finishing the registry-driven migration. Each
kind now ships its definition, schema (re-exported from core), and any
of `geometry` / `renderer` / `system` / `floorplan` / `tool` /
`move-tool` / `panel` / `floorplan-move` / `floorplan-affordances` /
`parametrics` / `preview` it needs — no per-kind code remains under
`packages/editor/src/components/tools/` or
`packages/viewer/src/components/renderers/`.

Deleted (replaced by registry-driven equivalents):
- `tools/{ceiling,column,door,fence,item,slab,spawn,wall,window}/...`
  (boundary editors, hole editors, placement tools, move tools,
  endpoint movers, curve tools, helpers, math libs)
- `ui/helpers/{ceiling,slab,wall}-helper.tsx`
- `ui/panels/{column,door,elevator,item,roof,roof-segment,spawn,
  stair,stair-segment,wall,window}-panel.tsx`
- `viewer/src/components/renderers/{building,ceiling,column,door,
  elevator,fence,guide,item,level,roof,roof-segment,scan,site,slab,
  spawn,stair,stair-segment,wall,window,zone}-renderer.tsx`
- `viewer/src/components/viewer/legacy-system.tsx`

Added under `packages/nodes/src/`:
- `building/`, `column/`, `elevator/`, `guide/`, `level/`, `roof/`,
  `roof-segment/`, `scan/`, `shared/`, `site/`, `stair/`,
  `stair-segment/` packages with definition + schema + renderer / system
  / floorplan / panel as appropriate.
- New `floorplan-move.ts` for every kind that supports 2D moves
  (ceiling, door, item, shelf, slab, window) — single registry-driven
  dispatch path via `def.floorplanMoveTarget`.
- New `floorplan-affordances.ts` for kinds with polygon / endpoint
  drags (ceiling, fence, slab, wall) — using the shared
  `polygon-vertex-affordance` factories.
- New per-kind `panel.tsx` for kinds with custom inspector content
  (door, item, shelf, spawn, wall, window).
- New per-kind `tool.tsx` for placement (door, item, shelf, window).
- New per-kind `move-tool.tsx` for kinds with custom 3D move flows
  (door, item, slab, window).

Coordinator + manager updates in `packages/editor/`:
- `tool-manager.tsx` resolves tools from the registry only — no
  hardcoded type→component map.
- `panel-manager.tsx` resolves inspector panels the same way.
- `placement-{coordinator,strategies,types}.ts` extended with
  shelf-surface placement.
- `selection-manager.tsx` adds the registry-selectable fallback.
- `floorplan-panel.tsx`, `floorplan-background-placement.ts`,
  `floorplan-render-context.tsx` updated for the registry layer's new
  contract (props, affordance dispatch, render context).

Viewer updates:
- `viewer/index.tsx` drops legacy renderer mounts.
- `node-renderer.tsx` resolves by registry only.
- `scene-bvh.tsx`, `use-node-events.ts`, `level-system.tsx`,
  `wall-cutout.tsx`, `zone-system.tsx`, `materials.ts` adjusted for
  the registry-only world.

Sidebar tree nodes for ceiling / fence / slab / shelf / tree-node
updated to read from the registered nodes instead of the deleted
legacy renderer trees.

Wiki: new `plugin-authoring.md` page, README index updated.

Tests in `packages/nodes/src/index.test.ts` validate every registered
kind has the required shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:14:12 -04:00
Wassim SAMADandClaude Opus 4.7 11015ea1ed wiki: document parametric-node + move-tool pitfalls
Captures the gotchas surfaced while building the shelf so the next
contributor adding a registry-driven kind doesn't rediscover them.

`node-definitions.md` — new Pitfalls section + a rule that builders
must emit local-space children. Covers:
- `<GeometrySystem>` must NOT mutate `group.position` / `group.rotation`
  after rebuild (the renderer binds them via JSX prop).
- Tag geometry-built children with `userData.__fromGeometry` so
  rebuilds don't dispose React-mounted hosted children (the
  item-disappears-on-shelf bug).
- Previews must clone materials before mutating them when the kind's
  builder caches at module scope.
- Host kinds need a `children: z.array(...).default([])` field on their
  schema (and a migration patch for older scenes).

`tools.md` — three new move/placement pitfalls:
- Disable raycast on the moved mesh during drag, otherwise it captures
  the ray and starves `grid:move` → commits land at the stale start.
- Commit handlers listen to `grid:click` AND every `${kind}:click` to
  catch clicks that land on neighbouring 3D geometry first.
- Move tools must preserve the node's actual `rotation[1]` in
  `useLiveTransforms` — hardcoding 0 makes the node un-rotate mid-drag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:12:27 -04:00
Wassim SAMADandClaude Opus 4.7 0540692bd5 item: fix 2D move — floor items stay parented to the level
Two related bugs caused floor items to disappear mid-2D-drag:

1. `startLevelId` did `parent.parentId` unconditionally for non-wall
   parents — so for a floor item parented directly to the level (the
   canonical convention from `use-placement-coordinator`), it returned
   `level.parentId = building.id`. `findContainingSurface` then iterated
   the building's children (levels, not slabs) and the fallback
   `parentId: startLevelId` reparented the item to the building. Now we
   walk the parent chain until we hit a `level` and short-circuit on
   `parent.type === 'level'`.

2. `buildSurfaceItemSession` reparented floor items to a slab when the
   cursor was over one. Slabs don't carry a `children` field on their
   schema (only ceilings + level do), so `updateNodesAction`'s reparent
   logic operated on `undefined.children` — the item dropped out of the
   level→children DFS the floor-plan layer walks, and the polygon
   stopped rendering mid-drag. Split into `buildFloorItemSession`
   (always parents to the level, just updates position) and a
   ceiling-only `buildSurfaceItemSession`. `findContainingSurface`
   narrows to `'ceiling'` as the only valid target.

Matches the 3D `detachItemSurfaceToFloor` convention: floor items live
as level children, not slab children.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:12:15 -04:00
Wassim SAMADandClaude Opus 4.7 0dee7747d2 floorplan/zone: name label inside polygon + polygon editor on select
`buildZoneFloorplan` now emits a centered name label at the polygon's
area-weighted centroid (Shoelace formula, with bbox-center fallback for
degenerate rings). Label uses the legacy `FloorplanZoneLabel` styling:
`fontSize: 0.2`, white fill, zone-color stroke, `paintOrder: 'stroke'`
for the "outlined text" look that stays legible above any fill.

When the zone is selected the builder also emits the polygon editor —
edge-handle per edge, midpoint-handle per midpoint, endpoint-handle per
vertex — driven by the shared `createPolygonVertexAffordance` /
`createPolygonAddVertexAffordance` / `createPolygonMoveEdgeAffordance`
factories slabs and ceilings already use. Zones have no `holes` field
so the factory's optional `holeIndex` stays undefined and the operations
target `node.polygon` directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:12:02 -04:00
Wassim SAMADandClaude Opus 4.7 3419cf8587 floorplan/slab: lower fill opacity so zones show through
The slab body covered the underlying zone fill almost completely —
`opacity: 0.85` (gray) and `opacity: 0.95` (white-on-select) drowned out
any zone color sitting beneath. Switched to independent `fillOpacity`
(0.6 unselected / 0.45 selected) and `strokeOpacity` (0.85 / 0.96) so
the outline stays crisp while the zone color reads through the fill.
On the selected state the hatch overlay carries the visual weight; no
need for an opaque white background underneath.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:11:52 -04:00
Wassim SAMADandClaude Opus 4.7 7a92641baa floorplan/registry-layer: z-order buckets + overlay pass + click guard
Three layered improvements on the 2D registry layer:

- Z-order buckets so the SVG document order reflects intent: zones (0)
  paint first, slabs/ceilings (1) next, every other kind (walls / items /
  shelves / columns / stairs / …) on top. Stable sort preserves DFS order
  within a bucket.

- Base / overlay split: each entry's `FloorplanGeometry` tree is walked
  through `splitFloorplanOverlay`, partitioning into a base group
  (polygons, paths, fills, hatches) and an overlay group (interactive
  handles + labels — `text` / `endpoint-handle` / `midpoint-handle` /
  `edge-handle` / `move-handle` / `dimension` / `dimension-label`). Base
  renders rank-sorted; overlays paint after every base entry so polygon-
  editor chrome on a selected slab and zone name labels stay legible
  above the structural fills sitting on top of them.

- Click guard on the outer layer `<g>`. The base/overlay split means
  pointer-down lands on base and pointer-up lands on overlay (selection
  mounts the overlay on top mid-gesture). The browser then dispatches
  `click` to the lowest common ancestor, ABOVE the entry's
  `onClick={handleClickStop}`. Without a higher-level stop, the click
  reached the SVG's `handleBackgroundClick` → `clear-elements` and the
  selection set on pointer-down vanished a frame later. Scoped to
  `onClick` only so pointer / hover / drag still propagate inside the
  registry tree.

Also extends the `text` FloorplanGeometry with stroke / strokeWidth /
paintOrder / fontFamily so kinds can match the legacy "white fill +
colored outline" label look (used by the new zone name label).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:11:44 -04:00
Wassim SAMADandClaude Opus 4.7 586cec8ad7 registry/move-tool: fix 3D drag — keep rotation + stop transform reset
Two bugs caused dragged shelves to snap to a weird position on commit:

1. `MoveRegistryNodeTool` wrote `useLiveTransforms.set(id, { ..., rotation: 0 })`,
   so during the drag `ParametricNodeRenderer` applied `<group rotation={[0,0,0]}>`
   and the shelf visually un-rotated. On commit the live transform cleared
   and the renderer re-read the node's true rotation — the snap-back read
   as "reverts to a weird position." Now we capture `originalRotationY`
   from the node at mount time and forward it on every set.

2. `<GeometrySystem>` reset `group.position.set(0,0,0)` /
   `group.rotation.set(0,0,0)` after every rebuild. That was carry-over
   from legacy per-kind systems that didn't bind `position` on the group.
   `ParametricNodeRenderer` now drives the transform via JSX prop, and
   the reset clobbered it — React doesn't necessarily re-render on a
   rebuild tick, so R3F never re-applied the prop and the registered
   `<group>` stayed at the origin. Removed the reset; builders are
   expected to emit local-space children.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:11:26 -04:00
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 8c2b03f99b parametric-inspector: render def.presentation.icon before the title
User pointed out the auto-derived panel was title-only; the legacy
panels rendered a small icon in front. Legacy used a URL path (next
Image). The registry-driven path has structured `IconRef` values
(iconify / svg / lazy component) declared on `def.presentation.icon`.

- PanelWrapper's `icon` prop now accepts `string | React.ReactNode`.
  String → next/image (legacy URL behavior). Node → rendered as-is.
- ParametricInspector resolves `def.presentation.icon` to a node:
  iconify → `<Icon icon="lucide:fence" />`, svg → inline svg, component
  → Suspense + lazy.

Kind-owned custom panels (slab/ceiling) keep their existing legacy
URL icons since they pass `<PanelWrapper>` themselves — no change
needed there.

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