Replicates the fence Stage D recipe for slab — three drag affordances
+ one placement tool, all routed through the registry:
affordanceTools:
'boundary-edit' → thin <PolygonEditor> wrapper (vertex/edge drag)
'hole-edit' → same for a single hole polygon
move → DragAction with single-undo dance
tool: () => placement (multi-click polygon with axis/45° snap)
`PolygonEditor` + `PolygonEditorProps` exported from
`@pascal-app/editor` as Stage D transitional surface (Stage F cleanup
moves them into `@pascal-app/nodes`).
ToolManager mount sites for SlabBoundaryEditor / SlabHoleEditor now
route through `getRegistryAffordanceTool` with legacy fallback.
Slab move action does not use the live-drag exception (polygon CSG
rebuild every tick) — matches legacy behavior; optimization is a
separate task once we measure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
'use client'
|
|
|
|
import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core'
|
|
import { PolygonEditor } from '@pascal-app/editor'
|
|
import { useViewer } from '@pascal-app/viewer'
|
|
import { useCallback } from 'react'
|
|
|
|
/**
|
|
* Phase 5 Stage D — slab hole editor (registry-driven).
|
|
*
|
|
* Edits a specific hole polygon inside a slab. Mounted by ToolManager
|
|
* via `def.affordanceTools['hole-edit']` when `useEditor.editingHole`
|
|
* is set on the selected slab.
|
|
*/
|
|
export const SlabHoleEditor: React.FC<{ slabId: SlabNode['id']; holeIndex: number }> = ({
|
|
slabId,
|
|
holeIndex,
|
|
}) => {
|
|
const slabNode = useScene((s) => s.nodes[slabId])
|
|
const updateNode = useScene((s) => s.updateNode)
|
|
const setSelection = useViewer((s) => s.setSelection)
|
|
|
|
const slab = slabNode?.type === 'slab' ? (slabNode as SlabNode) : null
|
|
const holes = slab?.holes || []
|
|
const hole = holes[holeIndex]
|
|
|
|
const handlePolygonChange = useCallback(
|
|
(newPolygon: Array<[number, number]>) => {
|
|
const updatedHoles = [...holes]
|
|
updatedHoles[holeIndex] = newPolygon
|
|
updateNode(slabId, { holes: updatedHoles })
|
|
setSelection({ selectedIds: [slabId] })
|
|
},
|
|
[slabId, holeIndex, holes, updateNode, setSelection],
|
|
)
|
|
|
|
if (!(slab && hole) || hole.length < 3) return null
|
|
|
|
return (
|
|
<PolygonEditor
|
|
allowEdgeMove
|
|
allowPolygonMove
|
|
color="#ef4444"
|
|
levelId={resolveLevelId(slab, useScene.getState().nodes)}
|
|
minVertices={3}
|
|
onPolygonChange={handlePolygonChange}
|
|
polygon={hole}
|
|
surfaceHeight={slab.elevation ?? 0.05}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export default SlabHoleEditor
|