Two new files in packages/editor/src/components/editor-2d/ keep the
work out of the 18k-line floorplan-panel.tsx monolith. The panel
itself gets only four tiny additions (two imports, two component
mounts, one data attribute).
<FloorplanRegistryActionMenu>
- Reads useViewer.selection — when a registered kind is selected and
we're not in a move state, queries the rendered [data-node-id] <g>
for its bounding rect (polled via rAF for pan/zoom/move reactivity).
- Portals an HTML overlay above the bounding box with the existing
<NodeActionMenu>. Buttons gated by def.capabilities:
* Move → setMovingNode(node)
* Duplicate → structuredClone + schema.parse + createNode + set
movingNode (placement cursor) — matches 3D duplicate UX.
* Delete → deleteNode(id) + clear selection.
- Same visual styling as the legacy <FloorplanActionMenuLayer> per
kind, but driven by registry data.
<FloorplanRegistryMoveOverlay>
- Activates when useEditor.movingNode is a kind with def.floorplan.
- Listens on window for pointermove (to track cursor in floor plan
meters via the scene <g>'s getScreenCTM — matches the legacy
getSvgPointFromClientPoint coordinate path so cursor → meters
accounts for pan/zoom/building rotation).
- Renders a 50%-opacity ghost via portal into the floor-plan scene
<g>. Builder reused from def.floorplan — no per-kind ghost code.
- Click commits via updateNode({ position: [cx, oldY, cz] }) and
clears movingNode. Clears `isNew` metadata on duplicates so they
don't loop. Esc cancels.
floorplan-panel.tsx touches:
- Two imports (action menu + move overlay).
- data-floorplan-scene="" attribute on the floorplanSceneRef <g>
so the overlay can find the scene without sharing a ref.
- <FloorplanRegistryActionMenu /> mounted alongside the legacy
action menu layer.
- <FloorplanRegistryMoveOverlay /> mounted inside the SVG tree
alongside the registry render layer.
FloorplanRegistryLayer: also stopPropagation on click events so the
outer SVG's onClick={handleBackgroundClick} doesn't deselect right
after our pointerDown sets selection. Fixes "click-in-2D doesn't
select" bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The drag-with-grab-cursor model was wrong design — 3D doesn't drag,
it uses select → inspector "Move" button → click-to-place. Floor
plan should match. Drag also had a coord-conversion bug (used outer
<svg> CTM instead of the floorplanSceneRef <g>, so screen→meter
conversion didn't account for the floor plan's pan/zoom/building-
rotation transforms).
Strips the drag pointerdown/move/up handlers, the imperative
transform override, the temporal pause bracketing, and the global
window listeners. Cursor goes back to 'pointer'. Only click-to-
select remains.
The right pattern (move via inspector / action menu + cursor-driven
placement) needs:
- Registry-aware FloorplanActionMenuLayer path
- Generic movingNode handler in floor-plan-panel for any registered
kind with capabilities.movable
- Shared floorplanSceneRef for accurate coord conversion
Both flagged in the plan as Phase 4 follow-on gaps with their
acceptance criteria. 3D-realtime-sync-while-moving is documented as
deferred (legacy doesn't do it for any kind either; design + ship
in a dedicated PR later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.
Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
@types/react, @types/react-dom, @types/three to prevent JSX namespace
fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
match the new versions on every bump (so viewer/editor/mcp tarballs always
reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts
Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
what private-editor currently reports
Open PRs against editor-v2 will need rebasing/conflict resolution.