All keyframe operations now snap to the frame grid
This commit is contained in:
parent
2ec6a54060
commit
d46e0bfd2f
8 changed files with 51 additions and 17 deletions
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<IProps> = (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<DopeSheetSelection['getDragHandlers']>
|
||||
| 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,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
@ -227,6 +227,7 @@ namespace utils {
|
|||
translate: delta,
|
||||
scale: 1,
|
||||
origin: 0,
|
||||
snappingFunction: sheet.getSequence().closestGridPosition,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -198,6 +198,9 @@ function useDragKeyframe(
|
|||
...propsAtStartOfDrag.sheetObject.address,
|
||||
trackId: propsAtStartOfDrag.trackId,
|
||||
keyframes: updatedKeyframes,
|
||||
snappingFunction: val(
|
||||
propsAtStartOfDrag.layoutP.sheet,
|
||||
).getSequence().closestGridPosition,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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<SheetObjectAddress> & {
|
||||
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<SheetObjectAddress> & {
|
||||
trackId: string
|
||||
keyframes: Array<Keyframe>
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue