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>
133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
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)
|
|
)
|
|
},
|
|
}
|
|
},
|
|
}
|