Phase 5 Stage D slab: port placement + move + boundary/hole editors
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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7b5a1c607a
commit
b2e5d84986
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import { type SlabNode, useScene } from '@pascal-app/core'
|
||||
import { CursorSphere, triggerSFX, useDragAction, useEditor } from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { moveSlabDragAction } from './actions/move'
|
||||
|
||||
/**
|
||||
* Phase 5 Stage D — thin React wrapper around `moveSlabDragAction`.
|
||||
*
|
||||
* Replaces the legacy `MoveSlabTool` (182 LoC). All math + history
|
||||
* dance lives in the action; this wrapper just renders the cursor
|
||||
* sphere following the live polygon center.
|
||||
*/
|
||||
export const SlabMoveTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
||||
const slabId = node.id
|
||||
|
||||
const initialCenter: [number, number] =
|
||||
node.polygon.length > 0
|
||||
? [
|
||||
node.polygon.reduce((s, [x]) => s + x, 0) / node.polygon.length,
|
||||
node.polygon.reduce((s, [, z]) => s + z, 0) / node.polygon.length,
|
||||
]
|
||||
: [0, 0]
|
||||
|
||||
// Live polygon center — re-derived from the scene store every tick
|
||||
// since the action writes the translated polygon onto the slab.
|
||||
const liveCenter = useScene((s) => {
|
||||
const live = s.nodes[slabId]
|
||||
if (live?.type !== 'slab') return initialCenter
|
||||
const poly = (live as SlabNode).polygon
|
||||
if (poly.length === 0) return initialCenter
|
||||
let sx = 0
|
||||
let sz = 0
|
||||
for (const [x, z] of poly) {
|
||||
sx += x
|
||||
sz += z
|
||||
}
|
||||
return [sx / poly.length, sz / poly.length] as [number, number]
|
||||
})
|
||||
|
||||
const exitMoveMode = (committed: boolean) => {
|
||||
if (committed) triggerSFX('sfx:item-place')
|
||||
useViewer.getState().setSelection({ selectedIds: [slabId] })
|
||||
useEditor.getState().setMovingNode(null)
|
||||
}
|
||||
|
||||
useDragAction({
|
||||
active: true,
|
||||
action: moveSlabDragAction,
|
||||
initial: {
|
||||
node,
|
||||
point: initialCenter,
|
||||
},
|
||||
onCommit: () => exitMoveMode(true),
|
||||
onCancel: () => exitMoveMode(false),
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
<CursorSphere position={[liveCenter[0], 0, liveCenter[1]]} showTooltip={false} />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlabMoveTool
|
||||
Reference in New Issue
Block a user