feat: add range selection functionality to element interaction
This commit is contained in:
parent
6fdb1559aa
commit
97db7aa21c
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue