133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
'use client'
|
|
|
|
import {
|
|
type AnyNodeId,
|
|
emitter,
|
|
type FenceNode,
|
|
type GridEvent,
|
|
pauseSceneHistory,
|
|
resumeSceneHistory,
|
|
useLiveNodeOverrides,
|
|
useScene,
|
|
} from '@pascal-app/core'
|
|
import {
|
|
CursorSphere,
|
|
getSegmentGridStep,
|
|
isGridSnapActive,
|
|
markToolCancelConsumed,
|
|
snapScalarToGrid,
|
|
triggerSFX,
|
|
useInteractionScope,
|
|
} from '@pascal-app/editor'
|
|
import { useViewer } from '@pascal-app/viewer'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
const TANGENT_HANDLE_ARM_SCALE = 3
|
|
|
|
export const MoveFenceTangentTool: React.FC<{
|
|
target: { fence: FenceNode; index: number; side: 'in' | 'out' }
|
|
}> = ({ target }) => {
|
|
const fenceId = target.fence.id as AnyNodeId
|
|
const { index, side } = target
|
|
const anchor = target.fence.path?.[index] ?? target.fence.start
|
|
|
|
const [cursor, setCursor] = useState<[number, number, number]>([anchor[0], 0, anchor[1]])
|
|
|
|
useEffect(() => {
|
|
pauseSceneHistory(useScene)
|
|
let committed = false
|
|
const originalTangents: Array<[number, number] | null> = (target.fence.tangents ?? []).map(
|
|
(t) => (t ? [t[0], t[1]] : null),
|
|
)
|
|
let lastTangents = originalTangents
|
|
|
|
const writeTangent = (vector: [number, number]) => {
|
|
const pathLength = target.fence.path?.length ?? originalTangents.length
|
|
const next: Array<[number, number] | null> = Array.from(
|
|
{ length: pathLength },
|
|
(_, tangentIndex) => lastTangents[tangentIndex] ?? null,
|
|
)
|
|
next[index] = vector
|
|
lastTangents = next
|
|
useLiveNodeOverrides.getState().set(fenceId, { tangents: next })
|
|
useScene.getState().markDirty(fenceId)
|
|
}
|
|
|
|
const restore = () => {
|
|
useLiveNodeOverrides.getState().clear(fenceId)
|
|
useScene.getState().markDirty(fenceId)
|
|
lastTangents = originalTangents
|
|
}
|
|
|
|
const exit = (didCommit: boolean) => {
|
|
if (didCommit) triggerSFX('sfx:item-place')
|
|
useViewer.getState().setSelection({ selectedIds: [fenceId] })
|
|
useInteractionScope
|
|
.getState()
|
|
.endIf(
|
|
(scope) =>
|
|
scope.kind === 'reshaping' &&
|
|
scope.reshape === 'tangent' &&
|
|
scope.nodeId === fenceId &&
|
|
scope.index === index &&
|
|
scope.side === side,
|
|
)
|
|
}
|
|
|
|
const onGridMove = (event: GridEvent) => {
|
|
const step = isGridSnapActive() ? getSegmentGridStep() : 0
|
|
const px = step > 0 ? snapScalarToGrid(event.localPosition[0], step) : event.localPosition[0]
|
|
const pz = step > 0 ? snapScalarToGrid(event.localPosition[2], step) : event.localPosition[2]
|
|
setCursor([px, 0, pz])
|
|
let armX = px - anchor[0]
|
|
let armZ = pz - anchor[1]
|
|
if (side === 'in') {
|
|
armX = -armX
|
|
armZ = -armZ
|
|
}
|
|
writeTangent([armX / TANGENT_HANDLE_ARM_SCALE, armZ / TANGENT_HANDLE_ARM_SCALE])
|
|
}
|
|
|
|
const onGridClick = (event: GridEvent) => {
|
|
committed = true
|
|
const finalTangents = lastTangents
|
|
resumeSceneHistory(useScene)
|
|
useScene.getState().updateNode(fenceId, { tangents: finalTangents })
|
|
useLiveNodeOverrides.getState().clear(fenceId)
|
|
useScene.getState().markDirty(fenceId)
|
|
lastTangents = finalTangents
|
|
exit(true)
|
|
event.nativeEvent?.stopPropagation?.()
|
|
}
|
|
|
|
const onCancel = () => {
|
|
restore()
|
|
resumeSceneHistory(useScene)
|
|
markToolCancelConsumed()
|
|
exit(false)
|
|
}
|
|
|
|
emitter.on('grid:move', onGridMove)
|
|
emitter.on('grid:click', onGridClick)
|
|
emitter.on('tool:cancel', onCancel)
|
|
|
|
return () => {
|
|
if (!committed) {
|
|
restore()
|
|
resumeSceneHistory(useScene)
|
|
}
|
|
emitter.off('grid:move', onGridMove)
|
|
emitter.off('grid:click', onGridClick)
|
|
emitter.off('tool:cancel', onCancel)
|
|
}
|
|
}, [anchor[0], anchor[1], fenceId, index, side, target.fence])
|
|
|
|
return (
|
|
<group>
|
|
<CursorSphere position={cursor} showTooltip={false} />
|
|
</group>
|
|
)
|
|
}
|
|
|
|
export default MoveFenceTangentTool
|