import getStudio from '@theatre/studio/getStudio' ; import type { CommitOrDiscard } from '@theatre/studio/StudioStore/StudioStore' ; import useDrag from '@theatre/studio/uiComponents/useDrag' ; import useKeyDown from '@theatre/studio/uiComponents/useKeyDown' ; import useValToAtom from '@theatre/studio/uiComponents/useValToAtom' ; import mutableSetDeep from '@theatre/shared/utils/mutableSetDeep' ; import useRefAndState from '@theatre/studio/utils/useRefAndState' ; import { usePrism } from '@theatre/react' ; import type { $IntentionalAny } from '@theatre/shared/utils/types' ; import type { Pointer } from '@theatre/dataverse' ; import { val } from '@theatre/dataverse' ; import React , { useMemo , useRef } from 'react' ; import styled from 'styled-components' ; import type { DopeSheetSelection , SequenceEditorPanelLayout , } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' ; import type { SequenceEditorTree_AllRowTypes } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' ; const Container = styled . div < { isShiftDown : boolean ; } > `
  cursor: ${ ( props ) => ( props . isShiftDown ? 'cell' : 'default' ) };
` ; const DopeSheetSelectionView = ( { layoutP , children } ) => { const [ containerRef , containerNode ] = useRefAndState < HTMLDivElement | null > ( null ) ; const isShiftDown = useKeyDown ( 'Shift' ) ; const selectionBounds = useCaptureSelection ( layoutP , containerNode ) ; const selectionBoundsRef = useRef < typeof selectionBounds > ( selectionBounds ) ; selectionBoundsRef . current = selectionBounds ; return ( < Container ref = { containerRef } isShiftDown = { isShiftDown } > { selectionBounds && ( < SelectionRectangle state = { selectionBounds } layoutP = { layoutP } / > ) } { children } < /Container>); } ; type SelectionBounds = { positions : [ from : number , to : number ] ; ys : [ from : number , to : number ] ; } ; function useCaptureSelection ( layoutP , containerNode ) { const [ ref , state ] = useRefAndState < SelectionBounds | null > ( null ) ; useDrag ( containerNode , useMemo ( ( ) : Parameters < typeof useDrag > [ 1 ] => { return { dontBlockMouseDown : true , lockCursorTo : 'cell' , onDragStart ( event ) { if ( ! event . shiftKey || event . target instanceof HTMLInputElement ) { return false ; } const rect = containerNode ! . getBoundingClientRect ( ) ; const posInScaledSpace = event . clientX - rect . left ; const posInUnitSpace = val ( layoutP . scaledSpace . toUnitSpace ) ( posInScaledSpace ) ; ref . current = { positions : [ posInUnitSpace , posInUnitSpace ] , ys : [ event . clientY - rect . top , event . clientY - rect . top ] , } ; val ( layoutP . selectionAtom ) . setState ( { current : undefined } ) ; } , onDrag ( dx , dy , event ) { const state = ref . current ! ; const rect = containerNode ! . getBoundingClientRect ( ) ; const posInScaledSpace = event . clientX - rect . left ; const posInUnitSpace = val ( layoutP . scaledSpace . toUnitSpace ) ( posInScaledSpace ) ; ref . current = { positions : [ ref . current ! . positions [ 0 ] , posInUnitSpace ] , ys : [ ref . current ! . ys [ 0 ] , event . clientY - rect . top ] , } ; const selection = utils . boundsToSelection ( layoutP , ref . current ) ; val ( layoutP . selectionAtom ) . setState ( { current : selection } ) ; } , onDragEnd ( dragHappened ) { ref . current = null ; } , } ; } , [ layoutP , containerNode , ref ] ) ) ; return state ; } namespace utils { const collectorByLeafType = { primitiveProp ( layoutP , leaf , bounds , selection ) { const { sheetObject , trackId } = leaf ; const trackData = val ( getStudio ( ) ! . atomP . historic . coreByProject [ sheetObject . address . projectId ] . sheetsById [ sheetObject . address . sheetId ] . sequence . tracksByObject [ sheetObject . address . objectKey ] . trackData [ trackId ] ) ! ; const toCollect = trackData ! . keyframes . filter ( ( kf ) => kf . position >= bounds . positions [ 0 ] && kf . position <= bounds . positions [ 1 ] ) ; for ( const kf of trackData . keyframes ) { if ( kf . position <= bounds . positions [ 0 ] ) continue ; if ( kf . position >= bounds . positions [ 1 ] ) break ; mutableSetDeep ( selection , ( p ) => p . byObjectKey [ sheetObject . address . objectKey ] . byTrackId [ trackId ] . byKeyframeId [ kf . id ] , true ) ; } } , } ; const collectChildren = ( layoutP , leaf , bounds , selection ) => { if ( 'children' in leaf ) { for ( const sub of leaf . children ) { collectFromAnyLeaf ( layoutP , sub , bounds , selection ) ; } } } ; function collectFromAnyLeaf ( layoutP , leaf , bounds , selection ) { if ( bounds . ys [ 0 ] > leaf . top + leaf . heightIncludingChildren || leaf . top > bounds . ys [ 1 ] ) { return ; } const collector = collectorByLeafType [ leaf . type ] ; if ( collector ) { collector ( layoutP , leaf as $IntentionalAny , bounds , selection ) ; } else { collectChildren ( layoutP , leaf , bounds , selection ) ; } } export function boundsToSelection ( layoutP , bounds ) { const sheet = val ( layoutP . tree . sheet ) ; const selection = { type : 'DopeSheetSelection' , byObjectKey : { } , getDragHandlers ( origin ) { let tempTransaction ; let toUnitSpace ; return { onDragStart ( ) { toUnitSpace = val ( layoutP . scaledSpace . toUnitSpace ) ; } , onDrag ( dx , _ , event ) { let delta = toUnitSpace ( dx ) ; if ( tempTransaction ) { tempTransaction . discard ( ) ; tempTransaction = undefined ; } const snapTarget = event . composedPath ( ) . find ( ( el ) : el is Element => el instanceof Element && el !== origin . domNode && el . hasAttribute ( 'data-pos' ) ) ; if ( snapTarget ) { const snapPos = parseFloat ( snapTarget . getAttribute ( 'data-pos' ) ! ) ; if ( isFinite ( snapPos ) ) { delta = snapPos - origin . positionAtStartOfDrag ; } } tempTransaction = getStudio ( ) ! . tempTransaction ( ( { stateEditors } ) => { const transformKeyframes = stateEditors . coreByProject . historic . sheetsById . sequence . transformKeyframes ; for ( const objectKey of Object . keys ( selection . byObjectKey ) ) { const { byTrackId } = selection . byObjectKey [ objectKey ] ! ; for ( const trackId of Object . keys ( byTrackId ) ) { const { byKeyframeId } = byTrackId [ trackId ] ! ; transformKeyframes ( { trackId , keyframeIds : Object . keys ( byKeyframeId ) , translate : delta , scale : 1 , origin : 0 , snappingFunction : sheet . getSequence ( ) . closestGridPosition , objectKey , projectId : origin . projectId , sheetId : origin . sheetId , } ) ; } } } ) ; } , onDragEnd ( dragHappened ) { if ( dragHappened ) { if ( tempTransaction ) { tempTransaction . commit ( ) ; } } else { if ( tempTransaction ) { tempTransaction . discard ( ) ; } } tempTransaction = undefined ; } , } ; } , delete ( ) { getStudio ( ) ! . transaction ( ( { stateEditors } ) => { const deleteKeyframes = stateEditors . coreByProject . historic . sheetsById . sequence . deleteKeyframes ; for ( const objectKey of Object . keys ( selection . byObjectKey ) ) { const { byTrackId } = selection . byObjectKey [ objectKey ] ! ; for ( const trackId of Object . keys ( byTrackId ) ) { const { byKeyframeId } = byTrackId [ trackId ] ! ; deleteKeyframes ( { ... sheet . address , objectKey , trackId , keyframeIds : Object . keys ( byKeyframeId ) , } ) ; } } } ) ; } , } ; bounds = sortBounds ( bounds ) ; const tree = val ( layoutP . tree ) ; collectFromAnyLeaf ( layoutP , tree , bounds , selection ) ; return selection ; } } const SelectionRectangleDiv = styled . div `
  position: absolute;
  background: rgba(255, 255, 255, 0.1);
  border: 1px dashed rgba(255, 255, 255, 0.4);
  box-size: border-box;
` ; const sortBounds = ( b ) : SelectionBounds => { return { positions : [ ... b . positions ] . sort ( ( a , b ) => a - b ) as SelectionBounds [ 'positions' ] , ys : [ ... b . ys ] . sort ( ( a , b ) => a - b ) as SelectionBounds [ 'ys' ] , } ; } ; const SelectionRectangle = ( { state , layoutP } ) => { const atom = useValToAtom ( state ) ; return usePrism ( ( ) => { const state = val ( atom . pointer ) ; const sorted = sortBounds ( state ) ; const unitSpaceToScaledSpace = val ( layoutP . scaledSpace . fromUnitSpace ) ; const positionsInScaledSpace = sorted . positions . map ( unitSpaceToScaledSpace ) ; const top = sorted . ys [ 0 ] ; const height = sorted . ys [ 1 ] - sorted . ys [ 0 ] ; const left = positionsInScaledSpace [ 0 ] ; const width = positionsInScaledSpace [ 1 ] - positionsInScaledSpace [ 0 ] ; return ( < SelectionRectangleDiv style = { { top : top + 'px' , height : height + 'px' , left : left + 'px' , width : width + 'px' , } } > < /SelectionRectangleDiv>); } , [ layoutP , atom ] ) ; } ; export default DopeSheetSelectionView ;