diff --git a/theatre/core/src/sequences/Sequence.ts b/theatre/core/src/sequences/Sequence.ts index 686425f..aa389cc 100644 --- a/theatre/core/src/sequences/Sequence.ts +++ b/theatre/core/src/sequences/Sequence.ts @@ -97,11 +97,13 @@ export default class Sequence { return this.closestGridPosition(this.position) } - closestGridPosition(posInUnitSpace: number): number { + closestGridPosition = (posInUnitSpace: number): number => { const subUnitsPerUnit = this.subUnitsPerUnit const gridLength = 1 / subUnitsPerUnit - return Math.round(posInUnitSpace / gridLength) * gridLength + return parseFloat( + (Math.round(posInUnitSpace / gridLength) * gridLength).toFixed(3), + ) } set position(requestedPosition: number) { diff --git a/theatre/studio/src/StudioStore/StudioStore.ts b/theatre/studio/src/StudioStore/StudioStore.ts index 8bdea33..3881a0c 100644 --- a/theatre/studio/src/StudioStore/StudioStore.ts +++ b/theatre/studio/src/StudioStore/StudioStore.ts @@ -144,12 +144,14 @@ export default class StudioStore { ) as $FixMe as SequenceTrackId | undefined if (typeof trackId === 'string') { + const seq = root.sheet.getSequence() stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition( { ...propAddress, trackId, - position: root.sheet.getSequence().position, + position: seq.position, value: v as $FixMe, + snappingFunction: seq.closestGridPosition, }, ) } else { @@ -196,7 +198,8 @@ export default class StudioStore { { ...propAddress, trackId, - position: root.sheet.getSequence().position, + position: + root.sheet.getSequence().positionSnappedToGrid, }, ) } else { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx index 3db3bbb..5aef1aa 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Connector.tsx @@ -14,6 +14,7 @@ import type { } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import {dotSize} from './Dot' import type KeyframeEditor from './KeyframeEditor' +import type Sequence from '@theatre/core/sequences/Sequence' const connectorHeight = dotSize / 2 + 1 const connectorWidthUnscaled = 1000 @@ -108,6 +109,8 @@ const Connector: React.FC = (props) => { replaceKeyframes({ ...props.leaf.sheetObject.address, + snappingFunction: val(props.layoutP.sheet).getSequence() + .closestGridPosition, trackId: props.leaf.trackId, keyframes: [ { @@ -157,6 +160,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) { let selectionDragHandlers: | ReturnType | undefined + let sequence: Sequence return { lockCursorTo: 'ew-resize', onDragStart(event) { @@ -174,6 +178,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) { } propsAtStartOfDrag = propsRef.current + sequence = val(propsAtStartOfDrag.layoutP.sheet).getSequence() toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) }, @@ -201,6 +206,7 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) { translate: delta, scale: 1, origin: 0, + snappingFunction: sequence.closestGridPosition, }, ) }) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx index c4b977c..a835567 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx @@ -209,6 +209,9 @@ function useDragKeyframe( ...propsAtStartOfDrag.leaf.sheetObject.address, trackId: propsAtStartOfDrag.leaf.trackId, keyframes: [{...original, position: newPosition}], + snappingFunction: val( + propsAtStartOfDrag.layoutP.sheet, + ).getSequence().closestGridPosition, }, ) }) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx index 2a7269f..1282af0 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx @@ -227,6 +227,7 @@ namespace utils { translate: delta, scale: 1, origin: 0, + snappingFunction: sheet.getSequence().closestGridPosition, }) } } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx index 37d6900..b7bd2b6 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx @@ -175,6 +175,9 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void { stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( { ...propsAtStartOfDrag.sheetObject.address, + snappingFunction: val( + propsAtStartOfDrag.layoutP.sheet, + ).getSequence().closestGridPosition, trackId: propsAtStartOfDrag.trackId, keyframes: [ { @@ -194,6 +197,9 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void { { ...propsAtStartOfDrag.sheetObject.address, trackId: propsAtStartOfDrag.trackId, + snappingFunction: val( + propsAtStartOfDrag.layoutP.sheet, + ).getSequence().closestGridPosition, keyframes: [ { ...next, diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Dot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Dot.tsx index 7aa606f..2efa355 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Dot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Dot.tsx @@ -198,6 +198,9 @@ function useDragKeyframe( ...propsAtStartOfDrag.sheetObject.address, trackId: propsAtStartOfDrag.trackId, keyframes: updatedKeyframes, + snappingFunction: val( + propsAtStartOfDrag.layoutP.sheet, + ).getSequence().closestGridPosition, }, ) }) diff --git a/theatre/studio/src/store/stateEditors.ts b/theatre/studio/src/store/stateEditors.ts index 9807157..e4a342b 100644 --- a/theatre/studio/src/store/stateEditors.ts +++ b/theatre/studio/src/store/stateEditors.ts @@ -458,11 +458,13 @@ namespace stateEditors { trackId: string position: number value: number + snappingFunction: SnappingFunction }, ) { + const position = p.snappingFunction(p.position) const {keyframes} = _getTrack(p) const existingKeyframeIndex = keyframes.findIndex( - (kf) => kf.position === p.position, + (kf) => kf.position === position, ) if (existingKeyframeIndex !== -1) { const kf = keyframes[existingKeyframeIndex] @@ -471,12 +473,12 @@ namespace stateEditors { } const indexOfLeftKeyframe = findLastIndex( keyframes, - (kf) => kf.position < p.position, + (kf) => kf.position < position, ) if (indexOfLeftKeyframe === -1) { keyframes.unshift({ id: generateKeyframeId(), - position: p.position, + position, connectedRight: true, handles: [0.5, 1, 0.5, 0], value: p.value, @@ -486,7 +488,7 @@ namespace stateEditors { const leftKeyframe = keyframes[indexOfLeftKeyframe] keyframes.splice(indexOfLeftKeyframe + 1, 0, { id: generateKeyframeId(), - position: p.position, + position, connectedRight: leftKeyframe.connectedRight, handles: [0.5, 1, 0.5, 0], value: p.value, @@ -508,6 +510,8 @@ namespace stateEditors { keyframes.splice(index, 1) } + type SnappingFunction = (p: number) => number + export function transformKeyframes( p: WithoutSheetInstance & { trackId: string @@ -515,6 +519,7 @@ namespace stateEditors { translate: number scale: number origin: number + snappingFunction: SnappingFunction }, ) { const track = _getTrack(p) @@ -527,7 +532,9 @@ namespace stateEditors { const transformed = selectedKeyframes.map((untransformedKf) => { const oldPosition = untransformedKf.position - const newPosition = transformNumber(oldPosition, p) + const newPosition = p.snappingFunction( + transformNumber(oldPosition, p), + ) return {...untransformedKf, position: newPosition} }) @@ -551,17 +558,20 @@ namespace stateEditors { p: WithoutSheetInstance & { trackId: string keyframes: Array + snappingFunction: SnappingFunction }, ) { const track = _getTrack(p) const initialKeyframes = current(track.keyframes) - const sanitizedKeyframes = p.keyframes.filter((kf) => { - if (!isFinite(kf.value)) return false - if (!kf.handles.every((handleValue) => isFinite(handleValue))) - return false + const sanitizedKeyframes = p.keyframes + .filter((kf) => { + if (!isFinite(kf.value)) return false + if (!kf.handles.every((handleValue) => isFinite(handleValue))) + return false - return true - }) + return true + }) + .map((kf) => ({...kf, position: p.snappingFunction(kf.position)})) const newKeyframesById = keyBy(sanitizedKeyframes, 'id') @@ -569,13 +579,13 @@ namespace stateEditors { (kf) => !newKeyframesById[kf.id], ) - const unselectedByPositino = keyBy(unselected, 'position') + const unselectedByPosition = keyBy(unselected, 'position') // If the new transformed keyframes overlap with any existing keyframes, // we remove the overlapped keyframes sanitizedKeyframes.forEach(({position}) => { const existingKeyframeAtThisPosition = - unselectedByPositino[position] + unselectedByPosition[position] if (existingKeyframeAtThisPosition) { pullFromArray(unselected, existingKeyframeAtThisPosition) }