feat: add range selection functionality to element interaction

This commit is contained in:
oguzhancttnky 2026-05-17 14:55:47 +03:00
parent 6fdb1559aa
commit 97db7aa21c
3 changed files with 99 additions and 21 deletions

View File

@ -55,6 +55,10 @@ export interface ElementSelectionApi {
getSelected: () => readonly ElementRef[]; getSelected: () => readonly ElementRef[];
isSelected: (ref: ElementRef) => boolean; isSelected: (ref: ElementRef) => boolean;
select: (ref: ElementRef) => void; select: (ref: ElementRef) => void;
selectRange: (args: {
anchor?: ElementRef | null;
target: ElementRef;
}) => readonly ElementRef[];
handleClick: (args: ElementRef & { isMultiKey: boolean }) => void; handleClick: (args: ElementRef & { isMultiKey: boolean }) => void;
clearKeyframeSelection: () => void; clearKeyframeSelection: () => void;
} }
@ -295,6 +299,7 @@ export class ElementInteractionController {
// has already returned to idle, so the "was this a drag?" answer must // has already returned to idle, so the "was this a drag?" answer must
// outlive the session. Reset on the next mousedown. // outlive the session. Reset on the next mousedown.
private lastGestureWasDrag = false; private lastGestureWasDrag = false;
private elementSelectionAnchor: ElementRef | null = null;
private readonly subscribers = new Set<() => void>(); private readonly subscribers = new Set<() => void>();
private readonly depsRef: ElementInteractionDepsRef; private readonly depsRef: ElementInteractionDepsRef;
@ -372,13 +377,21 @@ export class ElementInteractionController {
const ref = { trackId: track.id, elementId: element.id }; const ref = { trackId: track.id, elementId: element.id };
if (event.metaKey || event.ctrlKey || event.shiftKey) { let rangeSelection: readonly ElementRef[] | null = null;
if (event.shiftKey) {
rangeSelection = this.deps.selection.selectRange({
anchor: this.elementSelectionAnchor,
target: ref,
});
} else if (event.metaKey || event.ctrlKey) {
this.deps.selection.handleClick({ ...ref, isMultiKey: true }); this.deps.selection.handleClick({ ...ref, isMultiKey: true });
} }
const selectedElements = this.deps.selection.isSelected(ref) const selectedElements =
? this.deps.selection.getSelected() rangeSelection ??
: [ref]; (this.deps.selection.isSelected(ref)
? this.deps.selection.getSelected()
: [ref]);
this.session = { this.session = {
kind: "pending", kind: "pending",
@ -423,9 +436,11 @@ export class ElementInteractionController {
this.deps.selection.getSelected().length > 1 this.deps.selection.getSelected().length > 1
) { ) {
this.deps.selection.select(ref); this.deps.selection.select(ref);
this.elementSelectionAnchor = ref;
return; return;
} }
this.elementSelectionAnchor = ref;
this.deps.selection.clearKeyframeSelection(); this.deps.selection.clearKeyframeSelection();
}; };

View File

@ -50,6 +50,7 @@ export function useElementInteraction({
getSelected: () => selection.selectedElements, getSelected: () => selection.selectedElements,
isSelected: selection.isElementSelected, isSelected: selection.isElementSelected,
select: selection.selectElement, select: selection.selectElement,
selectRange: selection.selectElementRange,
handleClick: selection.handleElementClick, handleClick: selection.handleElementClick,
clearKeyframeSelection: () => editor.selection.clearKeyframeSelection(), clearKeyframeSelection: () => editor.selection.clearKeyframeSelection(),
}, },

View File

@ -83,10 +83,10 @@ export function useElementSelection() {
* Merges elements into the current selection, deduplicating by identity. * Merges elements into the current selection, deduplicating by identity.
* Used for additive box-select where the pre-drag selection is preserved. * Used for additive box-select where the pre-drag selection is preserved.
*/ */
const mergeElementsIntoSelection = useCallback( const mergeElementsIntoSelection = useCallback(
({ elements }: { elements: ElementRef[] }) => { ({ elements }: { elements: ElementRef[] }) => {
const merged = [ const merged = [
...selectedElements.filter( ...selectedElements.filter(
(selectedElement) => (selectedElement) =>
!elements.some( !elements.some(
(element) => (element) =>
@ -98,13 +98,74 @@ export function useElementSelection() {
]; ];
editor.selection.setSelectedElements({ elements: merged }); editor.selection.setSelectedElements({ elements: merged });
}, },
[selectedElements, editor], [selectedElements, editor],
); );
const selectElementRange = useCallback(
/** ({
* Handles click interaction on an element. anchor,
* - Regular click: select only this element target,
}: {
anchor?: ElementRef | null;
target: ElementRef;
}) => {
const tracks = editor.scenes.getActiveScene().tracks;
const track = [...tracks.overlay, tracks.main, ...tracks.audio].find(
(candidate) => candidate.id === target.trackId,
);
const rangeAnchor =
anchor?.trackId === target.trackId
? anchor
: selectedElements
.slice()
.reverse()
.find((element) => element.trackId === target.trackId);
if (!track || !rangeAnchor) {
const elements = [target];
editor.selection.setSelectedElements({ elements });
return elements;
}
const orderedElements = track.elements
.map((element, index) => ({ element, index }))
.sort((a, b) => {
if (a.element.startTime !== b.element.startTime) {
return a.element.startTime - b.element.startTime;
}
return a.index - b.index;
});
const anchorIndex = orderedElements.findIndex(
({ element }) => element.id === rangeAnchor.elementId,
);
const targetIndex = orderedElements.findIndex(
({ element }) => element.id === target.elementId,
);
if (anchorIndex === -1 || targetIndex === -1) {
const elements = [target];
editor.selection.setSelectedElements({ elements });
return elements;
}
const start = Math.min(anchorIndex, targetIndex);
const end = Math.max(anchorIndex, targetIndex);
const elements = orderedElements
.slice(start, end + 1)
.map(({ element }) => ({
trackId: target.trackId,
elementId: element.id,
}));
editor.selection.setSelectedElements({ elements });
return elements;
},
[selectedElements, editor],
);
/**
* Handles click interaction on an element.
* - Regular click: select only this element
* - Multi-key click (Ctrl/Cmd): toggle this element in selection * - Multi-key click (Ctrl/Cmd): toggle this element in selection
*/ */
const handleElementClick = useCallback( const handleElementClick = useCallback(
@ -125,12 +186,13 @@ export function useElementSelection() {
return { return {
selectedElements, selectedElements,
isElementSelected, isElementSelected,
selectElement, selectElement,
setElementSelection, setElementSelection,
mergeElementsIntoSelection, mergeElementsIntoSelection,
addElementToSelection, selectElementRange,
removeElementFromSelection, addElementToSelection,
toggleElementSelection, removeElementFromSelection,
toggleElementSelection,
clearElementSelection, clearElementSelection,
handleElementClick, handleElementClick,
}; };