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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
11015ea1ed
commit
d747d2f0ea
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
type FenceNode,
|
||||
type FloorplanAffordance,
|
||||
type FloorplanAffordanceSession,
|
||||
useScene,
|
||||
type WallNode,
|
||||
} from '@pascal-app/core'
|
||||
import { type FencePlanPoint, isWallLongEnough, snapFenceDraftPoint } from '@pascal-app/editor'
|
||||
|
||||
/**
|
||||
* Floor-plan 2D drag affordances for fence — sister to the 3D
|
||||
* `actions/move-endpoint.ts` `DragAction`. Same legacy interaction
|
||||
* (endpoint snap pipeline + linked-fence cascade via `endpoint-match`
|
||||
* with an epsilon, ALT-detach), driven from SVG pointer events instead
|
||||
* of R3F grid events.
|
||||
*
|
||||
* Why not share the `DragAction`? The 3D code goes through
|
||||
* `createDragSession` which assumes a `SceneApi`-style helper bag
|
||||
* (snapshot, restoreAll, pauseHistory, resumeHistory). The 2D registry
|
||||
* layer owns those semantics directly via the dispatcher's snapshot +
|
||||
* pause/resume dance, so the affordance only needs the pure mutation
|
||||
* logic. The shape is intentionally close to the legacy fence drag —
|
||||
* 1:1 behaviorally.
|
||||
*/
|
||||
|
||||
const LINKED_FENCE_ENDPOINT_EPSILON = 0.025
|
||||
|
||||
type FenceEndpointPayload = { fenceId: AnyNodeId; endpoint: 'start' | 'end' }
|
||||
|
||||
function pointsNearlyEqual(a: FencePlanPoint, b: FencePlanPoint): boolean {
|
||||
return (
|
||||
Math.abs(a[0] - b[0]) <= LINKED_FENCE_ENDPOINT_EPSILON &&
|
||||
Math.abs(a[1] - b[1]) <= LINKED_FENCE_ENDPOINT_EPSILON
|
||||
)
|
||||
}
|
||||
|
||||
function collectLevel(
|
||||
nodes: Record<AnyNodeId, AnyNode>,
|
||||
parentId: string | null,
|
||||
): { walls: WallNode[]; fences: FenceNode[] } {
|
||||
const walls: WallNode[] = []
|
||||
const fences: FenceNode[] = []
|
||||
for (const node of Object.values(nodes)) {
|
||||
if (!node) continue
|
||||
if ((node.parentId ?? null) !== parentId) continue
|
||||
if (node.type === 'wall') walls.push(node as WallNode)
|
||||
else if (node.type === 'fence') fences.push(node as FenceNode)
|
||||
}
|
||||
return { walls, fences }
|
||||
}
|
||||
|
||||
function collectLinkedFences(
|
||||
fences: FenceNode[],
|
||||
draggedFenceId: AnyNodeId,
|
||||
linkedPoint: FencePlanPoint,
|
||||
): Array<{ id: AnyNodeId; start: FencePlanPoint; end: FencePlanPoint }> {
|
||||
const out: Array<{ id: AnyNodeId; start: FencePlanPoint; end: FencePlanPoint }> = []
|
||||
for (const fence of fences) {
|
||||
if (fence.id === draggedFenceId) continue
|
||||
if (!pointsNearlyEqual(fence.start, linkedPoint) && !pointsNearlyEqual(fence.end, linkedPoint))
|
||||
continue
|
||||
out.push({
|
||||
id: fence.id,
|
||||
start: [fence.start[0], fence.start[1]],
|
||||
end: [fence.end[0], fence.end[1]],
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export const fenceMoveEndpointAffordance: FloorplanAffordance<FenceNode> = {
|
||||
start({ node, payload, nodes }): FloorplanAffordanceSession {
|
||||
const { endpoint } = payload as FenceEndpointPayload
|
||||
const originalStart: FencePlanPoint = [node.start[0], node.start[1]]
|
||||
const originalEnd: FencePlanPoint = [node.end[0], node.end[1]]
|
||||
const originalMovingPoint = endpoint === 'start' ? originalStart : originalEnd
|
||||
const fixedPoint: FencePlanPoint = endpoint === 'start' ? originalEnd : originalStart
|
||||
|
||||
const parentId = node.parentId ?? null
|
||||
const { walls, fences } = collectLevel(nodes, parentId)
|
||||
const linkedOriginals = collectLinkedFences(fences, node.id, originalMovingPoint)
|
||||
|
||||
const affectedIds: AnyNodeId[] = [node.id, ...linkedOriginals.map((l) => l.id)]
|
||||
|
||||
return {
|
||||
affectedIds,
|
||||
apply({ planPoint, modifiers }) {
|
||||
// Re-collect siblings each tick: the user might be dragging a
|
||||
// fence whose sibling positions changed (the dragged fence
|
||||
// itself is excluded via `ignoreFenceIds`).
|
||||
const sceneNodes = useScene.getState().nodes
|
||||
const { walls: nextWalls, fences: nextFences } = collectLevel(sceneNodes, parentId)
|
||||
const snapped = snapFenceDraftPoint({
|
||||
point: planPoint as FencePlanPoint,
|
||||
walls: nextWalls,
|
||||
fences: nextFences,
|
||||
start: fixedPoint,
|
||||
angleSnap: !modifiers.shiftKey,
|
||||
ignoreFenceIds: [node.id],
|
||||
})
|
||||
const nextStart = endpoint === 'start' ? snapped : fixedPoint
|
||||
const nextEnd = endpoint === 'end' ? snapped : fixedPoint
|
||||
|
||||
const linkedUpdates = modifiers.altKey
|
||||
? []
|
||||
: linkedOriginals.map((l) => ({
|
||||
id: l.id,
|
||||
start: pointsNearlyEqual(l.start, originalMovingPoint) ? snapped : l.start,
|
||||
end: pointsNearlyEqual(l.end, originalMovingPoint) ? snapped : l.end,
|
||||
}))
|
||||
|
||||
useScene.getState().updateNodes([
|
||||
{ id: node.id, data: { start: nextStart, end: nextEnd } },
|
||||
...linkedUpdates.map((u) => ({
|
||||
id: u.id,
|
||||
data: { start: u.start, end: u.end },
|
||||
})),
|
||||
])
|
||||
},
|
||||
canCommit() {
|
||||
const finalFence = useScene.getState().nodes[node.id] as FenceNode | undefined
|
||||
return (
|
||||
!!finalFence &&
|
||||
finalFence.type === 'fence' &&
|
||||
isWallLongEnough(finalFence.start, finalFence.end)
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user