diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-27 00:55:15 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:56:48 +0200 |
| commit | 4d127a038bf768eded68aae3cfa5c1b12241bdac (patch) | |
| tree | 16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/hooks/timeline/element | |
| parent | 82817cb50cb0487350573d6129130aec705f924b (diff) | |
| download | opencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz | |
chore: normalize line endings
Diffstat (limited to 'apps/web/src/hooks/timeline/element')
| -rw-r--r-- | apps/web/src/hooks/timeline/element/use-element-selection.ts | 274 | ||||
| -rw-r--r-- | apps/web/src/hooks/timeline/element/use-keyframe-selection.ts | 458 |
2 files changed, 366 insertions, 366 deletions
diff --git a/apps/web/src/hooks/timeline/element/use-element-selection.ts b/apps/web/src/hooks/timeline/element/use-element-selection.ts index dcfb5720..42187b7f 100644 --- a/apps/web/src/hooks/timeline/element/use-element-selection.ts +++ b/apps/web/src/hooks/timeline/element/use-element-selection.ts @@ -1,137 +1,137 @@ -import { useCallback } from "react"; -import { useEditor } from "@/hooks/use-editor"; -import type { ElementRef } from "@/lib/timeline/types"; - -export function useElementSelection() { - const editor = useEditor(); - const selectedElements = useEditor((e) => e.selection.getSelectedElements()); - - const isElementSelected = useCallback( - ({ trackId, elementId }: ElementRef) => - selectedElements.some( - (element) => - element.trackId === trackId && element.elementId === elementId, - ), - [selectedElements], - ); - - const selectElement = useCallback( - ({ trackId, elementId }: ElementRef) => { - editor.selection.setSelectedElements({ - elements: [{ trackId, elementId }], - }); - }, - [editor], - ); - - const addElementToSelection = useCallback( - ({ trackId, elementId }: ElementRef) => { - const alreadySelected = selectedElements.some( - (element) => - element.trackId === trackId && element.elementId === elementId, - ); - if (alreadySelected) return; - - editor.selection.setSelectedElements({ - elements: [...selectedElements, { trackId, elementId }], - }); - }, - [selectedElements, editor], - ); - - const removeElementFromSelection = useCallback( - ({ trackId, elementId }: ElementRef) => { - editor.selection.setSelectedElements({ - elements: selectedElements.filter( - (element) => - !(element.trackId === trackId && element.elementId === elementId), - ), - }); - }, - [selectedElements, editor], - ); - - const toggleElementSelection = useCallback( - ({ trackId, elementId }: ElementRef) => { - const alreadySelected = selectedElements.some( - (element) => - element.trackId === trackId && element.elementId === elementId, - ); - - if (alreadySelected) { - removeElementFromSelection({ trackId, elementId }); - } else { - addElementToSelection({ trackId, elementId }); - } - }, - [selectedElements, addElementToSelection, removeElementFromSelection], - ); - - const clearElementSelection = useCallback(() => { - editor.selection.clearSelection(); - }, [editor]); - - const setElementSelection = useCallback( - ({ elements }: { elements: ElementRef[] }) => { - editor.selection.setSelectedElements({ elements }); - }, - [editor], - ); - - - /** - * Merges elements into the current selection, deduplicating by identity. - * Used for additive box-select where the pre-drag selection is preserved. - */ - const mergeElementsIntoSelection = useCallback( - ({ elements }: { elements: ElementRef[] }) => { - const merged = [ - ...selectedElements.filter( - (selectedElement) => - !elements.some( - (element) => - element.trackId === selectedElement.trackId && - element.elementId === selectedElement.elementId, - ), - ), - ...elements, - ]; - editor.selection.setSelectedElements({ elements: merged }); - }, - [selectedElements, editor], - ); - - - /** - * Handles click interaction on an element. - * - Regular click: select only this element - * - Multi-key click (Ctrl/Cmd): toggle this element in selection - */ - const handleElementClick = useCallback( - ({ - trackId, - elementId, - isMultiKey, - }: ElementRef & { isMultiKey: boolean }) => { - if (isMultiKey) { - toggleElementSelection({ trackId, elementId }); - } else { - selectElement({ trackId, elementId }); - } - }, - [toggleElementSelection, selectElement], - ); - - return { - selectedElements, - isElementSelected, - selectElement, - setElementSelection, - mergeElementsIntoSelection, - addElementToSelection, - removeElementFromSelection, - toggleElementSelection, - clearElementSelection, - handleElementClick, - }; -} +import { useCallback } from "react";
+import { useEditor } from "@/hooks/use-editor";
+import type { ElementRef } from "@/lib/timeline/types";
+
+export function useElementSelection() {
+ const editor = useEditor();
+ const selectedElements = useEditor((e) => e.selection.getSelectedElements());
+
+ const isElementSelected = useCallback(
+ ({ trackId, elementId }: ElementRef) =>
+ selectedElements.some(
+ (element) =>
+ element.trackId === trackId && element.elementId === elementId,
+ ),
+ [selectedElements],
+ );
+
+ const selectElement = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ editor.selection.setSelectedElements({
+ elements: [{ trackId, elementId }],
+ });
+ },
+ [editor],
+ );
+
+ const addElementToSelection = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ const alreadySelected = selectedElements.some(
+ (element) =>
+ element.trackId === trackId && element.elementId === elementId,
+ );
+ if (alreadySelected) return;
+
+ editor.selection.setSelectedElements({
+ elements: [...selectedElements, { trackId, elementId }],
+ });
+ },
+ [selectedElements, editor],
+ );
+
+ const removeElementFromSelection = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ editor.selection.setSelectedElements({
+ elements: selectedElements.filter(
+ (element) =>
+ !(element.trackId === trackId && element.elementId === elementId),
+ ),
+ });
+ },
+ [selectedElements, editor],
+ );
+
+ const toggleElementSelection = useCallback(
+ ({ trackId, elementId }: ElementRef) => {
+ const alreadySelected = selectedElements.some(
+ (element) =>
+ element.trackId === trackId && element.elementId === elementId,
+ );
+
+ if (alreadySelected) {
+ removeElementFromSelection({ trackId, elementId });
+ } else {
+ addElementToSelection({ trackId, elementId });
+ }
+ },
+ [selectedElements, addElementToSelection, removeElementFromSelection],
+ );
+
+ const clearElementSelection = useCallback(() => {
+ editor.selection.clearSelection();
+ }, [editor]);
+
+ const setElementSelection = useCallback(
+ ({ elements }: { elements: ElementRef[] }) => {
+ editor.selection.setSelectedElements({ elements });
+ },
+ [editor],
+ );
+
+
+ /**
+ * Merges elements into the current selection, deduplicating by identity.
+ * Used for additive box-select where the pre-drag selection is preserved.
+ */
+ const mergeElementsIntoSelection = useCallback(
+ ({ elements }: { elements: ElementRef[] }) => {
+ const merged = [
+ ...selectedElements.filter(
+ (selectedElement) =>
+ !elements.some(
+ (element) =>
+ element.trackId === selectedElement.trackId &&
+ element.elementId === selectedElement.elementId,
+ ),
+ ),
+ ...elements,
+ ];
+ editor.selection.setSelectedElements({ elements: merged });
+ },
+ [selectedElements, editor],
+ );
+
+
+ /**
+ * Handles click interaction on an element.
+ * - Regular click: select only this element
+ * - Multi-key click (Ctrl/Cmd): toggle this element in selection
+ */
+ const handleElementClick = useCallback(
+ ({
+ trackId,
+ elementId,
+ isMultiKey,
+ }: ElementRef & { isMultiKey: boolean }) => {
+ if (isMultiKey) {
+ toggleElementSelection({ trackId, elementId });
+ } else {
+ selectElement({ trackId, elementId });
+ }
+ },
+ [toggleElementSelection, selectElement],
+ );
+
+ return {
+ selectedElements,
+ isElementSelected,
+ selectElement,
+ setElementSelection,
+ mergeElementsIntoSelection,
+ addElementToSelection,
+ removeElementFromSelection,
+ toggleElementSelection,
+ clearElementSelection,
+ handleElementClick,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts b/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts index 8634cb48..1a219d1f 100644 --- a/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts +++ b/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts @@ -1,229 +1,229 @@ -import { useCallback, useSyncExternalStore } from "react"; -import { useEditor } from "@/hooks/use-editor"; -import type { SelectedKeyframeRef } from "@/lib/animation/types"; - -function getSelectedKeyframeId({ - keyframe, -}: { - keyframe: SelectedKeyframeRef; -}): string { - return `${keyframe.trackId}:${keyframe.elementId}:${keyframe.propertyPath}:${keyframe.keyframeId}`; -} - -function mergeUniqueKeyframes({ - keyframes, -}: { - keyframes: SelectedKeyframeRef[]; -}): SelectedKeyframeRef[] { - const keyframesById = new Map<string, SelectedKeyframeRef>(); - for (const keyframe of keyframes) { - keyframesById.set(getSelectedKeyframeId({ keyframe }), keyframe); - } - return [...keyframesById.values()]; -} - -export function useKeyframeSelection() { - const editor = useEditor(); - const selectedKeyframes = useSyncExternalStore( - (listener) => editor.selection.subscribe(listener), - () => editor.selection.getSelectedKeyframes(), - ); - const keyframeSelectionAnchor = useSyncExternalStore( - (listener) => editor.selection.subscribe(listener), - () => editor.selection.getKeyframeSelectionAnchor(), - ); - - const isKeyframeSelected = useCallback( - ({ keyframe }: { keyframe: SelectedKeyframeRef }) => { - const keyframeId = getSelectedKeyframeId({ keyframe }); - return selectedKeyframes.some( - (selectedKeyframe) => - getSelectedKeyframeId({ keyframe: selectedKeyframe }) === keyframeId, - ); - }, - [selectedKeyframes], - ); - - const setKeyframeSelection = useCallback( - ({ - keyframes, - anchorKeyframe, - }: { - keyframes: SelectedKeyframeRef[]; - anchorKeyframe?: SelectedKeyframeRef; - }) => { - const uniqueKeyframes = mergeUniqueKeyframes({ keyframes }); - editor.selection.setSelectedKeyframes({ - keyframes: uniqueKeyframes, - anchorKeyframe: - anchorKeyframe ?? uniqueKeyframes[uniqueKeyframes.length - 1] ?? null, - }); - }, - [editor], - ); - - const addKeyframesToSelection = useCallback( - ({ - keyframes, - anchorKeyframe, - }: { - keyframes: SelectedKeyframeRef[]; - anchorKeyframe?: SelectedKeyframeRef; - }) => { - const mergedKeyframes = mergeUniqueKeyframes({ - keyframes: [...selectedKeyframes, ...keyframes], - }); - editor.selection.setSelectedKeyframes({ - keyframes: mergedKeyframes, - anchorKeyframe: - anchorKeyframe ?? mergedKeyframes[mergedKeyframes.length - 1] ?? null, - }); - }, - [selectedKeyframes, editor], - ); - - const removeKeyframesFromSelection = useCallback( - ({ - keyframes, - anchorKeyframe, - }: { - keyframes: SelectedKeyframeRef[]; - anchorKeyframe?: SelectedKeyframeRef; - }) => { - const keyframeIdsToRemove = new Set( - keyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })), - ); - const nextKeyframes = selectedKeyframes.filter( - (selectedKeyframe) => - !keyframeIdsToRemove.has( - getSelectedKeyframeId({ keyframe: selectedKeyframe }), - ), - ); - editor.selection.setSelectedKeyframes({ - keyframes: nextKeyframes, - anchorKeyframe: - anchorKeyframe ?? nextKeyframes[nextKeyframes.length - 1] ?? null, - }); - }, - [selectedKeyframes, editor], - ); - - const clearKeyframeSelection = useCallback(() => { - editor.selection.clearKeyframeSelection(); - }, [editor]); - - const toggleKeyframeSelection = useCallback( - ({ - keyframes, - isMultiKey, - }: { - keyframes: SelectedKeyframeRef[]; - isMultiKey: boolean; - }) => { - const anchorKeyframe = keyframes[0]; - const areAllKeyframesSelected = keyframes.every((keyframe) => - isKeyframeSelected({ keyframe }), - ); - if (!isMultiKey) { - setKeyframeSelection({ keyframes, anchorKeyframe }); - return; - } - - if (areAllKeyframesSelected) { - removeKeyframesFromSelection({ keyframes, anchorKeyframe }); - return; - } - - addKeyframesToSelection({ keyframes, anchorKeyframe }); - }, - [ - setKeyframeSelection, - isKeyframeSelected, - removeKeyframesFromSelection, - addKeyframesToSelection, - ], - ); - - const selectKeyframeRange = useCallback( - ({ - orderedKeyframes, - targetKeyframes, - isAdditive, - }: { - orderedKeyframes: SelectedKeyframeRef[]; - targetKeyframes: SelectedKeyframeRef[]; - isAdditive: boolean; - }) => { - if (orderedKeyframes.length === 0 || targetKeyframes.length === 0) { - return; - } - - const anchorKeyframe = - keyframeSelectionAnchor ?? - selectedKeyframes[selectedKeyframes.length - 1] ?? - targetKeyframes[0]; - if (!anchorKeyframe) { - return; - } - - const targetKeyframeIds = new Set( - targetKeyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })), - ); - const anchorId = getSelectedKeyframeId({ keyframe: anchorKeyframe }); - const anchorIndex = orderedKeyframes.findIndex( - (keyframe) => getSelectedKeyframeId({ keyframe }) === anchorId, - ); - if (anchorIndex === -1) { - if (isAdditive) { - addKeyframesToSelection({ - keyframes: targetKeyframes, - anchorKeyframe, - }); - return; - } - setKeyframeSelection({ keyframes: targetKeyframes, anchorKeyframe }); - return; - } - - const targetIndexes = orderedKeyframes - .map((keyframe, index) => ({ - keyframeId: getSelectedKeyframeId({ keyframe }), - index, - })) - .filter(({ keyframeId }) => targetKeyframeIds.has(keyframeId)) - .map(({ index }) => index); - if (targetIndexes.length === 0) { - return; - } - - const rangeStart = Math.min(anchorIndex, ...targetIndexes); - const rangeEnd = Math.max(anchorIndex, ...targetIndexes); - const rangeKeyframes = orderedKeyframes.slice(rangeStart, rangeEnd + 1); - - if (isAdditive) { - addKeyframesToSelection({ keyframes: rangeKeyframes, anchorKeyframe }); - return; - } - - setKeyframeSelection({ keyframes: rangeKeyframes, anchorKeyframe }); - }, - [ - keyframeSelectionAnchor, - selectedKeyframes, - addKeyframesToSelection, - setKeyframeSelection, - ], - ); - - return { - selectedKeyframes, - keyframeSelectionAnchor, - isKeyframeSelected, - setKeyframeSelection, - addKeyframesToSelection, - removeKeyframesFromSelection, - clearKeyframeSelection, - toggleKeyframeSelection, - selectKeyframeRange, - }; -} +import { useCallback, useSyncExternalStore } from "react";
+import { useEditor } from "@/hooks/use-editor";
+import type { SelectedKeyframeRef } from "@/lib/animation/types";
+
+function getSelectedKeyframeId({
+ keyframe,
+}: {
+ keyframe: SelectedKeyframeRef;
+}): string {
+ return `${keyframe.trackId}:${keyframe.elementId}:${keyframe.propertyPath}:${keyframe.keyframeId}`;
+}
+
+function mergeUniqueKeyframes({
+ keyframes,
+}: {
+ keyframes: SelectedKeyframeRef[];
+}): SelectedKeyframeRef[] {
+ const keyframesById = new Map<string, SelectedKeyframeRef>();
+ for (const keyframe of keyframes) {
+ keyframesById.set(getSelectedKeyframeId({ keyframe }), keyframe);
+ }
+ return [...keyframesById.values()];
+}
+
+export function useKeyframeSelection() {
+ const editor = useEditor();
+ const selectedKeyframes = useSyncExternalStore(
+ (listener) => editor.selection.subscribe(listener),
+ () => editor.selection.getSelectedKeyframes(),
+ );
+ const keyframeSelectionAnchor = useSyncExternalStore(
+ (listener) => editor.selection.subscribe(listener),
+ () => editor.selection.getKeyframeSelectionAnchor(),
+ );
+
+ const isKeyframeSelected = useCallback(
+ ({ keyframe }: { keyframe: SelectedKeyframeRef }) => {
+ const keyframeId = getSelectedKeyframeId({ keyframe });
+ return selectedKeyframes.some(
+ (selectedKeyframe) =>
+ getSelectedKeyframeId({ keyframe: selectedKeyframe }) === keyframeId,
+ );
+ },
+ [selectedKeyframes],
+ );
+
+ const setKeyframeSelection = useCallback(
+ ({
+ keyframes,
+ anchorKeyframe,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ anchorKeyframe?: SelectedKeyframeRef;
+ }) => {
+ const uniqueKeyframes = mergeUniqueKeyframes({ keyframes });
+ editor.selection.setSelectedKeyframes({
+ keyframes: uniqueKeyframes,
+ anchorKeyframe:
+ anchorKeyframe ?? uniqueKeyframes[uniqueKeyframes.length - 1] ?? null,
+ });
+ },
+ [editor],
+ );
+
+ const addKeyframesToSelection = useCallback(
+ ({
+ keyframes,
+ anchorKeyframe,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ anchorKeyframe?: SelectedKeyframeRef;
+ }) => {
+ const mergedKeyframes = mergeUniqueKeyframes({
+ keyframes: [...selectedKeyframes, ...keyframes],
+ });
+ editor.selection.setSelectedKeyframes({
+ keyframes: mergedKeyframes,
+ anchorKeyframe:
+ anchorKeyframe ?? mergedKeyframes[mergedKeyframes.length - 1] ?? null,
+ });
+ },
+ [selectedKeyframes, editor],
+ );
+
+ const removeKeyframesFromSelection = useCallback(
+ ({
+ keyframes,
+ anchorKeyframe,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ anchorKeyframe?: SelectedKeyframeRef;
+ }) => {
+ const keyframeIdsToRemove = new Set(
+ keyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })),
+ );
+ const nextKeyframes = selectedKeyframes.filter(
+ (selectedKeyframe) =>
+ !keyframeIdsToRemove.has(
+ getSelectedKeyframeId({ keyframe: selectedKeyframe }),
+ ),
+ );
+ editor.selection.setSelectedKeyframes({
+ keyframes: nextKeyframes,
+ anchorKeyframe:
+ anchorKeyframe ?? nextKeyframes[nextKeyframes.length - 1] ?? null,
+ });
+ },
+ [selectedKeyframes, editor],
+ );
+
+ const clearKeyframeSelection = useCallback(() => {
+ editor.selection.clearKeyframeSelection();
+ }, [editor]);
+
+ const toggleKeyframeSelection = useCallback(
+ ({
+ keyframes,
+ isMultiKey,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ isMultiKey: boolean;
+ }) => {
+ const anchorKeyframe = keyframes[0];
+ const areAllKeyframesSelected = keyframes.every((keyframe) =>
+ isKeyframeSelected({ keyframe }),
+ );
+ if (!isMultiKey) {
+ setKeyframeSelection({ keyframes, anchorKeyframe });
+ return;
+ }
+
+ if (areAllKeyframesSelected) {
+ removeKeyframesFromSelection({ keyframes, anchorKeyframe });
+ return;
+ }
+
+ addKeyframesToSelection({ keyframes, anchorKeyframe });
+ },
+ [
+ setKeyframeSelection,
+ isKeyframeSelected,
+ removeKeyframesFromSelection,
+ addKeyframesToSelection,
+ ],
+ );
+
+ const selectKeyframeRange = useCallback(
+ ({
+ orderedKeyframes,
+ targetKeyframes,
+ isAdditive,
+ }: {
+ orderedKeyframes: SelectedKeyframeRef[];
+ targetKeyframes: SelectedKeyframeRef[];
+ isAdditive: boolean;
+ }) => {
+ if (orderedKeyframes.length === 0 || targetKeyframes.length === 0) {
+ return;
+ }
+
+ const anchorKeyframe =
+ keyframeSelectionAnchor ??
+ selectedKeyframes[selectedKeyframes.length - 1] ??
+ targetKeyframes[0];
+ if (!anchorKeyframe) {
+ return;
+ }
+
+ const targetKeyframeIds = new Set(
+ targetKeyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })),
+ );
+ const anchorId = getSelectedKeyframeId({ keyframe: anchorKeyframe });
+ const anchorIndex = orderedKeyframes.findIndex(
+ (keyframe) => getSelectedKeyframeId({ keyframe }) === anchorId,
+ );
+ if (anchorIndex === -1) {
+ if (isAdditive) {
+ addKeyframesToSelection({
+ keyframes: targetKeyframes,
+ anchorKeyframe,
+ });
+ return;
+ }
+ setKeyframeSelection({ keyframes: targetKeyframes, anchorKeyframe });
+ return;
+ }
+
+ const targetIndexes = orderedKeyframes
+ .map((keyframe, index) => ({
+ keyframeId: getSelectedKeyframeId({ keyframe }),
+ index,
+ }))
+ .filter(({ keyframeId }) => targetKeyframeIds.has(keyframeId))
+ .map(({ index }) => index);
+ if (targetIndexes.length === 0) {
+ return;
+ }
+
+ const rangeStart = Math.min(anchorIndex, ...targetIndexes);
+ const rangeEnd = Math.max(anchorIndex, ...targetIndexes);
+ const rangeKeyframes = orderedKeyframes.slice(rangeStart, rangeEnd + 1);
+
+ if (isAdditive) {
+ addKeyframesToSelection({ keyframes: rangeKeyframes, anchorKeyframe });
+ return;
+ }
+
+ setKeyframeSelection({ keyframes: rangeKeyframes, anchorKeyframe });
+ },
+ [
+ keyframeSelectionAnchor,
+ selectedKeyframes,
+ addKeyframesToSelection,
+ setKeyframeSelection,
+ ],
+ );
+
+ return {
+ selectedKeyframes,
+ keyframeSelectionAnchor,
+ isKeyframeSelected,
+ setKeyframeSelection,
+ addKeyframesToSelection,
+ removeKeyframesFromSelection,
+ clearKeyframeSelection,
+ toggleKeyframeSelection,
+ selectKeyframeRange,
+ };
+}
|
