Phase 5 Stage D fence: port MoveFenceEndpointTool to DragAction
Second Stage D affordance port (425 LoC legacy → ~430 split across
action + wrapper). All math (snap, linked-fence cascade, alt-detach,
min-length gate, single-undo dance) lives in the pure
`moveFenceEndpointDragAction`. The React wrapper handles the UI
overlays (cursor sphere, Drag/Detach badge, angle label) and the live
state subscriptions.
The action introduces the single-undo dance pattern for multi-write
commits: `commit()` calls `scene.restoreAll()` → `resumeHistory()` →
re-applies the final draft so zundo records the entire drag as one
undo step. Reusable shape for slab/wall/door endpoint ports.
Transitional exports added to `@pascal-app/editor`'s public surface
(`snapFenceDraftPoint`, `isWallLongEnough`, the segment-angle helpers,
`MovingFenceEndpoint`). Stage F cleanup moves these into
`@pascal-app/nodes` once every consumer is registry-driven.
`getRegistryAffordanceTool` is now generic (`ComponentType<any>`) so
affordances with different prop shapes (`{ node }`, `{ target }`, …)
all dispatch through the same helper.
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
ee309ece6f
commit
05efa25edb
@@ -58,18 +58,15 @@ function getRegistryTool(tool: Tool | null): ComponentType | null {
|
||||
* 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<{ node: any }> | null {
|
||||
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 as ComponentType<{ node: any }>
|
||||
const Comp = lazy(loader as () => Promise<{ default: ComponentType<{ node: any }> }>)
|
||||
if (cached) return cached
|
||||
const Comp = lazy(loader as () => Promise<{ default: ComponentType<any> }>)
|
||||
lazyToolCache.set(loader, Comp as unknown as ComponentType)
|
||||
return Comp
|
||||
return Comp as unknown as ComponentType<any>
|
||||
}
|
||||
|
||||
const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
|
||||
@@ -212,7 +209,20 @@ export const ToolManager: React.FC = () => {
|
||||
<CeilingHoleEditor ceilingId={selectedCeilingId} holeIndex={editingHole.holeIndex} />
|
||||
)}
|
||||
{movingWallEndpoint && <MoveWallEndpointTool target={movingWallEndpoint} />}
|
||||
{movingFenceEndpoint && <MoveFenceEndpointTool target={movingFenceEndpoint} />}
|
||||
{movingFenceEndpoint &&
|
||||
(() => {
|
||||
const RegistryAffordance = getRegistryAffordanceTool(
|
||||
movingFenceEndpoint.fence.type,
|
||||
'move-endpoint',
|
||||
)
|
||||
return RegistryAffordance ? (
|
||||
<Suspense fallback={null}>
|
||||
<RegistryAffordance target={movingFenceEndpoint} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<MoveFenceEndpointTool target={movingFenceEndpoint} />
|
||||
)
|
||||
})()}
|
||||
{curvingWall && <CurveWallTool node={curvingWall} />}
|
||||
{curvingFence &&
|
||||
(() => {
|
||||
|
||||
@@ -4,7 +4,18 @@ export {
|
||||
type SnapshotCameraData,
|
||||
ThumbnailGenerator,
|
||||
} from './components/editor/thumbnail-generator'
|
||||
// Phase 5 Stage D transitional exports — pure drafting / angle helpers
|
||||
// consumed by kind-owned drag actions in @pascal-app/nodes. Stage F
|
||||
// cleanup moves these into @pascal-app/nodes (fence/drafting.ts +
|
||||
// shared/segment-angle.ts) once every Stage D port is in.
|
||||
export { type FencePlanPoint, snapFenceDraftPoint } from './components/tools/fence/fence-drafting'
|
||||
export { CursorSphere } from './components/tools/shared/cursor-sphere'
|
||||
export {
|
||||
formatAngleRadians,
|
||||
getAngleToSegmentReference,
|
||||
getSegmentAngleReferenceAtPoint,
|
||||
} from './components/tools/shared/segment-angle'
|
||||
export { isWallLongEnough } from './components/tools/wall/wall-drafting'
|
||||
export { CameraActions as ViewerToolbarRight } from './components/ui/action-menu/camera-actions'
|
||||
export { ViewToggles as ViewerToolbarLeft } from './components/ui/action-menu/view-toggles'
|
||||
export { useCommandPalette } from './components/ui/command-palette'
|
||||
@@ -36,7 +47,12 @@ export { applySceneGraphToEditor } from './lib/scene'
|
||||
export { triggerSFX } from './lib/sfx-bus'
|
||||
export { default as useAudio } from './store/use-audio'
|
||||
export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
|
||||
export type { FloorplanSelectionTool, SplitOrientation, ViewMode } from './store/use-editor'
|
||||
export type {
|
||||
FloorplanSelectionTool,
|
||||
MovingFenceEndpoint,
|
||||
SplitOrientation,
|
||||
ViewMode,
|
||||
} from './store/use-editor'
|
||||
export { default as useEditor } from './store/use-editor'
|
||||
export {
|
||||
type PaletteView,
|
||||
|
||||
Reference in New Issue
Block a user