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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
36c48b7fc9
commit
c16878c876
@@ -17,6 +17,7 @@ import type {
|
||||
WindowNode,
|
||||
} from '@pascal-app/core'
|
||||
import { nodeRegistry } from '@pascal-app/core'
|
||||
import { Suspense } from 'react'
|
||||
import { Vector3 } from 'three'
|
||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
@@ -28,6 +29,7 @@ import { MoveElevatorTool } from '../elevator/move-elevator-tool'
|
||||
import { MoveFenceTool } from '../fence/move-fence-tool'
|
||||
import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool'
|
||||
import { MoveRoofTool } from '../roof/move-roof-tool'
|
||||
import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
||||
import { MoveSlabTool } from '../slab/move-slab-tool'
|
||||
import { MoveSpawnTool } from '../spawn/move-spawn-tool'
|
||||
import { MoveWallTool } from '../wall/move-wall-tool'
|
||||
@@ -122,6 +124,18 @@ export const MoveTool: React.FC<{
|
||||
return <MoveRegistryNodeTool node={movingNode} />
|
||||
}
|
||||
|
||||
// Phase 5 Stage D: registry-driven move affordance (kind-owned
|
||||
// `DragAction` with bespoke semantics). Falls through to the legacy
|
||||
// per-kind chain below when the kind hasn't ported its move tool.
|
||||
const RegistryMove = getRegistryAffordanceTool(movingNode.type, 'move')
|
||||
if (RegistryMove) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<RegistryMove node={movingNode} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
if (movingNode.type === 'building')
|
||||
return <MoveBuildingContent node={movingNode as BuildingNode} />
|
||||
if (movingNode.type === 'door') return <MoveDoorTool node={movingNode as DoorNode} />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { nodeRegistry } from '@pascal-app/core'
|
||||
import { type ComponentType, lazy } from 'react'
|
||||
|
||||
/**
|
||||
* Phase 5 Stage D — runtime lazy-load of a kind's affordance tool.
|
||||
*
|
||||
* The editor can't statically import from `@pascal-app/nodes` (the
|
||||
* nodes package depends on editor — static imports would cycle). The
|
||||
* kind declares its drag-affordance components in
|
||||
* `def.affordanceTools[<key>]: () => import('./<name>-tool')`; this
|
||||
* helper resolves that to a `React.lazy` component at the call site.
|
||||
*
|
||||
* Returns null when the kind doesn't declare the affordance — callers
|
||||
* mount the legacy fallback in that case.
|
||||
*/
|
||||
const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType>()
|
||||
|
||||
export function getRegistryAffordanceTool(
|
||||
kind: string,
|
||||
affordance: string,
|
||||
): ComponentType<any> | null {
|
||||
const def = nodeRegistry.get(kind)
|
||||
const loader = def?.affordanceTools?.[affordance]
|
||||
if (!loader) return null
|
||||
const cached = lazyToolCache.get(loader)
|
||||
if (cached) return cached
|
||||
const Comp = lazy(loader as () => Promise<{ default: ComponentType<any> }>)
|
||||
lazyToolCache.set(loader, Comp as unknown as ComponentType)
|
||||
return Comp as unknown as ComponentType<any>
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { MoveFenceEndpointTool } from './fence/move-fence-endpoint-tool'
|
||||
import { ItemTool } from './item/item-tool'
|
||||
import { MoveTool } from './item/move-tool'
|
||||
import { RoofTool } from './roof/roof-tool'
|
||||
import { getRegistryAffordanceTool } from './shared/affordance-dispatch'
|
||||
import { SiteBoundaryEditor } from './site/site-boundary-editor'
|
||||
import { SlabBoundaryEditor } from './slab/slab-boundary-editor'
|
||||
import { SlabHoleEditor } from './slab/slab-hole-editor'
|
||||
@@ -49,26 +50,6 @@ function getRegistryTool(tool: Tool | null): ComponentType | null {
|
||||
return Comp
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy-loads the kind's drag-affordance component from
|
||||
* `def.affordanceTools[name]`. Lets editor consume kind-owned tools
|
||||
* from `@pascal-app/nodes` without a static import (which would create
|
||||
* a circular dep: nodes already depends on editor).
|
||||
*
|
||||
* Returns null when the kind doesn't declare the affordance — caller
|
||||
* renders the legacy fallback in that case.
|
||||
*/
|
||||
function getRegistryAffordanceTool(kind: string, affordance: string): ComponentType<any> | null {
|
||||
const def = nodeRegistry.get(kind)
|
||||
const loader = def?.affordanceTools?.[affordance]
|
||||
if (!loader) return null
|
||||
const cached = lazyToolCache.get(loader)
|
||||
if (cached) return cached
|
||||
const Comp = lazy(loader as () => Promise<{ default: ComponentType<any> }>)
|
||||
lazyToolCache.set(loader, Comp as unknown as ComponentType)
|
||||
return Comp as unknown as ComponentType<any>
|
||||
}
|
||||
|
||||
const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
|
||||
site: {
|
||||
'property-line': SiteBoundaryEditor,
|
||||
|
||||
Reference in New Issue
Block a user