diff options
Diffstat (limited to 'apps/web/src/hooks/use-mask-handles.ts')
| -rw-r--r-- | apps/web/src/hooks/use-mask-handles.ts | 893 |
1 files changed, 633 insertions, 260 deletions
diff --git a/apps/web/src/hooks/use-mask-handles.ts b/apps/web/src/hooks/use-mask-handles.ts index d2400ee0..917a0677 100644 --- a/apps/web/src/hooks/use-mask-handles.ts +++ b/apps/web/src/hooks/use-mask-handles.ts @@ -1,260 +1,633 @@ -import { useCallback, useEffect, useRef, useState } from "react";
-import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
-import { useEditor } from "@/hooks/use-editor";
-import { useShiftKey } from "@/hooks/use-shift-key";
-import { masksRegistry } from "@/lib/masks";
-import {
- getMaskHandlePositions,
- getLineMaskLinePoints,
-} from "@/lib/masks/handle-positions";
-import { snapMaskInteraction } from "@/lib/masks/snap";
-import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds";
-import {
- SNAP_THRESHOLD_SCREEN_PIXELS,
- type SnapLine,
-} from "@/lib/preview/preview-snap";
-import type { ParamValues } from "@/lib/params";
-import type {
- BaseMaskParams,
- MaskHandlePosition,
- MaskLinePoints,
-} from "@/lib/masks/types";
-import type { MaskableElement } from "@/lib/timeline";
-import { registerCanceller } from "@/lib/cancel-interaction";
-
-interface DragState {
- trackId: string;
- elementId: string;
- handleId: string;
- startCanvasX: number;
- startCanvasY: number;
- startParams: BaseMaskParams & ParamValues;
-}
-
-export function useMaskHandles({
- onSnapLinesChange,
-}: {
- onSnapLinesChange?: (lines: SnapLine[]) => void;
-}) {
- const editor = useEditor();
- const isShiftHeldRef = useShiftKey();
- const viewport = usePreviewViewport();
- const [activeHandleId, setActiveHandleId] = useState<string | null>(null);
- const dragStateRef = useRef<DragState | null>(null);
- const captureRef = useRef<{ element: HTMLElement; pointerId: number } | null>(
- null,
- );
-
- const tracks = useEditor(
- (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks,
- );
- const currentTime = useEditor((e) => e.playback.getCurrentTime());
- const mediaAssets = useEditor((e) => e.media.getAssets());
- const canvasSize = useEditor(
- (e) => e.project.getActive().settings.canvasSize,
- );
- const selectedElements = useEditor((e) => e.selection.getSelectedElements());
-
- const elementsWithBounds = getVisibleElementsWithBounds({
- tracks,
- currentTime,
- canvasSize,
- mediaAssets,
- });
-
- const selectedWithMask =
- selectedElements.length === 1
- ? (() => {
- const sel = selectedElements[0];
- const entry = elementsWithBounds.find(
- (item) =>
- item.trackId === sel.trackId && item.elementId === sel.elementId,
- );
- if (!entry) return null;
- const element = entry.element as MaskableElement;
- if (!element.masks?.length) return null;
- return { ...entry, element, mask: element.masks[0] };
- })()
- : null;
-
- const handlePositions: MaskHandlePosition[] = selectedWithMask
- ? (() => {
- const def = masksRegistry.get(selectedWithMask.mask.type);
- const { x: scaleX, y: scaleY } = viewport.getDisplayScale();
- const displayScale = (scaleX + scaleY) / 2;
- return getMaskHandlePositions({
- overlayShape: def.overlayShape,
- features: def.features,
- params: selectedWithMask.mask.params,
- bounds: selectedWithMask.bounds,
- displayScale,
- });
- })()
- : [];
-
- const linePoints: MaskLinePoints | null =
- selectedWithMask?.mask.type === "split"
- ? getLineMaskLinePoints({
- centerX: selectedWithMask.mask.params.centerX,
- centerY: selectedWithMask.mask.params.centerY,
- rotation: selectedWithMask.mask.params.rotation,
- bounds: selectedWithMask.bounds,
- })
- : null;
-
- const clearMaskHandleState = useCallback(() => {
- dragStateRef.current = null;
- setActiveHandleId(null);
- onSnapLinesChange?.([]);
- }, [onSnapLinesChange]);
-
- const releaseCapturedPointer = useCallback(() => {
- const capture = captureRef.current;
- if (!capture) return;
-
- if (capture.element.hasPointerCapture(capture.pointerId)) {
- capture.element.releasePointerCapture(capture.pointerId);
- }
-
- captureRef.current = null;
- }, []);
-
- useEffect(() => {
- if (!activeHandleId) return;
-
- return registerCanceller({
- fn: () => {
- editor.timeline.discardPreview();
- clearMaskHandleState();
- releaseCapturedPointer();
- },
- });
- }, [
- activeHandleId,
- clearMaskHandleState,
- editor.timeline,
- releaseCapturedPointer,
- ]);
-
- const handlePointerDown = useCallback(
- ({ event, handleId }: { event: React.PointerEvent; handleId: string }) => {
- if (!selectedWithMask) return;
- event.stopPropagation();
-
- const pos = viewport.screenToCanvas({
- clientX: event.clientX,
- clientY: event.clientY,
- });
- if (!pos) return;
-
- dragStateRef.current = {
- trackId: selectedWithMask.trackId,
- elementId: selectedWithMask.elementId,
- handleId,
- startCanvasX: pos.x,
- startCanvasY: pos.y,
- startParams: { ...selectedWithMask.mask.params },
- };
- setActiveHandleId(handleId);
- const captureTarget = event.currentTarget as HTMLElement;
- captureTarget.setPointerCapture(event.pointerId);
- captureRef.current = {
- element: captureTarget,
- pointerId: event.pointerId,
- };
- },
- [selectedWithMask, viewport],
- );
-
- const handlePointerMove = useCallback(
- ({ event }: { event: React.PointerEvent }) => {
- const drag = dragStateRef.current;
- if (!drag || !selectedWithMask) return;
-
- const pos = viewport.screenToCanvas({
- clientX: event.clientX,
- clientY: event.clientY,
- });
- if (!pos) return;
-
- const deltaX = pos.x - drag.startCanvasX;
- const deltaY = pos.y - drag.startCanvasY;
- const def = masksRegistry.get(selectedWithMask.mask.type);
-
- const rawParams = def.computeParamUpdate({
- handleId: drag.handleId,
- startParams: drag.startParams,
- deltaX,
- deltaY,
- startCanvasX: drag.startCanvasX,
- startCanvasY: drag.startCanvasY,
- bounds: selectedWithMask.bounds,
- canvasSize,
- });
- const proposedParams = { ...drag.startParams, ...rawParams };
-
- const snapThreshold = viewport.screenPixelsToLogicalThreshold({
- screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS,
- });
- const { params: nextParams, activeLines } = isShiftHeldRef.current
- ? { params: proposedParams, activeLines: [] as SnapLine[] }
- : snapMaskInteraction({
- handleId: drag.handleId,
- startParams: drag.startParams,
- proposedParams,
- bounds: selectedWithMask.bounds,
- canvasSize,
- snapThreshold,
- });
-
- onSnapLinesChange?.(activeLines);
-
- const updatedMask = {
- ...selectedWithMask.mask,
- params: nextParams,
- };
- editor.timeline.previewElements({
- updates: [
- {
- trackId: drag.trackId,
- elementId: drag.elementId,
- updates: {
- masks: [
- updatedMask,
- ...(selectedWithMask.element.masks?.slice(1) ?? []),
- ],
- } as Partial<MaskableElement>,
- },
- ],
- });
- },
- [
- selectedWithMask,
- canvasSize,
- editor,
- isShiftHeldRef,
- onSnapLinesChange,
- viewport,
- ],
- );
-
- const handlePointerUp = useCallback(() => {
- if (dragStateRef.current) {
- editor.timeline.commitPreview();
- clearMaskHandleState();
- }
- releaseCapturedPointer();
- },
- [clearMaskHandleState, editor, releaseCapturedPointer],
- );
-
- return {
- selectedWithMask,
- handlePositions,
- linePoints,
- activeHandleId,
- handlePointerDown,
- handlePointerMove,
- handlePointerUp,
- };
-}
+import { useCallback, useEffect, useRef, useState } from "react"; +import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; +import { useEditor } from "@/hooks/use-editor"; +import { useShiftKey } from "@/hooks/use-shift-key"; +import { masksRegistry } from "@/lib/masks"; +import { + parseCustomMaskHandleId, + parseCustomMaskSegmentHandleId, +} from "@/lib/masks/custom-path"; +import { appendPointToCustomMask } from "@/lib/masks/definitions/custom"; +import { + getVisibleElementsWithBounds, + type ElementBounds, +} from "@/lib/preview/element-bounds"; +import { + SNAP_THRESHOLD_SCREEN_PIXELS, + type SnapLine, +} from "@/lib/preview/preview-snap"; +import type { SelectedMaskPointSelection } from "@/lib/selection/editor-selection"; +import type { Mask, MaskInteractionResult } from "@/lib/masks/types"; +import type { MaskableElement } from "@/lib/timeline"; +import { registerCanceller } from "@/lib/cancel-interaction"; + +interface DragState { + trackId: string; + elementId: string; + handleId: string; + startCanvasX: number; + startCanvasY: number; + startParams: Mask["params"]; +} + +interface PendingSegmentInsertState { + trackId: string; + elementId: string; + maskId: string; + segmentIndex: number; + startClientX: number; + startClientY: number; + startCanvasX: number; + startCanvasY: number; + startParams: Mask["params"]; + bounds: ElementBounds; +} + +const SEGMENT_CLICK_DRAG_THRESHOLD_PX = 4; + +function isMaskSelectionForElement({ + trackId, + elementId, + maskId, + selection, +}: { + trackId: string; + elementId: string; + maskId: string; + selection: SelectedMaskPointSelection | null; +}): boolean { + if (!selection) { + return false; + } + return ( + selection.trackId === trackId && + selection.elementId === elementId && + selection.maskId === maskId + ); +} + +function replaceElementMask({ + masks, + updatedMask, +}: { + masks: MaskableElement["masks"]; + updatedMask: Mask; +}): Mask[] { + return (masks ?? []).map((mask) => + mask.id === updatedMask.id ? updatedMask : mask, + ); +} + +function withUpdatedMaskParams<TMask extends Mask>({ + mask, + params, +}: { + mask: TMask; + params: TMask["params"]; +}): TMask { + return { + ...mask, + params, + }; +} + +export function useMaskHandles({ + onSnapLinesChange, +}: { + onSnapLinesChange?: (lines: SnapLine[]) => void; +}) { + const editor = useEditor(); + const isShiftHeldRef = useShiftKey(); + const viewport = usePreviewViewport(); + const [activeHandleId, setActiveHandleId] = useState<string | null>(null); + const dragStateRef = useRef<DragState | null>(null); + const pendingSegmentInsertRef = useRef<PendingSegmentInsertState | null>( + null, + ); + const captureRef = useRef<{ element: HTMLElement; pointerId: number } | null>( + null, + ); + + const tracks = useEditor( + (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks, + ); + const currentTime = useEditor((e) => e.playback.getCurrentTime()); + const mediaAssets = useEditor((e) => e.media.getAssets()); + const canvasSize = useEditor( + (e) => e.project.getActive().settings.canvasSize, + ); + const selectedElements = useEditor((e) => e.selection.getSelectedElements()); + const selectedMaskPointSelection = useEditor((e) => + e.selection.getSelectedMaskPointSelection(), + ); + + const elementsWithBounds = getVisibleElementsWithBounds({ + tracks, + currentTime, + canvasSize, + mediaAssets, + }); + + const selectedWithMask = + selectedElements.length === 1 + ? (() => { + const sel = selectedElements[0]; + const entry = elementsWithBounds.find( + (item) => + item.trackId === sel.trackId && item.elementId === sel.elementId, + ); + if (!entry) return null; + const element = entry.element as MaskableElement; + const masks = element.masks ?? []; + if (masks.length === 0) return null; + const activeMaskId = masks.some((mask) => + isMaskSelectionForElement({ + trackId: entry.trackId, + elementId: entry.elementId, + maskId: mask.id, + selection: selectedMaskPointSelection, + }), + ) + ? selectedMaskPointSelection?.maskId + : masks[0].id; + const mask = + masks.find((candidate) => candidate.id === activeMaskId) ?? + masks[0]; + return { ...entry, element, mask }; + })() + : null; + + const { handles: baseHandlePositions, overlays }: MaskInteractionResult = + selectedWithMask + ? (() => { + const def = masksRegistry.get(selectedWithMask.mask.type); + const { x: scaleX, y: scaleY } = viewport.getDisplayScale(); + const displayScale = (scaleX + scaleY) / 2; + return def.interaction.getInteraction({ + params: selectedWithMask.mask.params, + bounds: selectedWithMask.bounds, + displayScale, + scaleX, + scaleY, + }); + })() + : { handles: [], overlays: [] }; + + const selectedPointIds = new Set( + selectedWithMask && + isMaskSelectionForElement({ + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + maskId: selectedWithMask.mask.id, + selection: selectedMaskPointSelection, + }) + ? (selectedMaskPointSelection?.pointIds ?? []) + : [], + ); + + const handlePositions = baseHandlePositions.map((handle) => { + const parsedHandle = parseCustomMaskHandleId({ handleId: handle.id }); + if (!parsedHandle || parsedHandle.part !== "anchor") { + return handle; + } + return { + ...handle, + isSelected: selectedPointIds.has(parsedHandle.pointId), + }; + }); + + const customMaskPointIds = + selectedWithMask?.mask.type === "custom" + ? handlePositions + .filter((h) => h.kind === "point") + .map((h) => { + const parsedHandle = parseCustomMaskHandleId({ handleId: h.id }); + return parsedHandle?.part === "anchor" + ? parsedHandle.pointId + : null; + }) + .filter((id): id is string => id !== null) + : []; + const isCreatingCustomMask = + selectedWithMask?.mask.type === "custom" && + (!selectedWithMask.mask.params.closed || customMaskPointIds.length === 0); + + useEffect(() => { + if (!selectedMaskPointSelection) { + return; + } + if ( + !selectedWithMask || + selectedWithMask.mask.type !== "custom" || + !isMaskSelectionForElement({ + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + maskId: selectedWithMask.mask.id, + selection: selectedMaskPointSelection, + }) + ) { + editor.selection.clearMaskPointSelection(); + return; + } + + const availablePointIds = new Set(customMaskPointIds); + const nextSelectedPointIds = selectedMaskPointSelection.pointIds.filter( + (pointId) => availablePointIds.has(pointId), + ); + if ( + nextSelectedPointIds.length === selectedMaskPointSelection.pointIds.length + ) { + return; + } + if (nextSelectedPointIds.length === 0) { + editor.selection.clearMaskPointSelection(); + return; + } + + editor.selection.setSelectedMaskPoints({ + selection: { + ...selectedMaskPointSelection, + pointIds: nextSelectedPointIds, + }, + }); + }, [ + customMaskPointIds, + editor.selection, + selectedMaskPointSelection, + selectedWithMask, + ]); + + const updateCustomMaskPointSelection = useCallback( + ({ + pointId, + toggleSelection, + }: { + pointId: string; + toggleSelection: boolean; + }) => { + if (!selectedWithMask || selectedWithMask.mask.type !== "custom") { + return; + } + + const isSelectionForCurrentMask = isMaskSelectionForElement({ + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + maskId: selectedWithMask.mask.id, + selection: selectedMaskPointSelection, + }); + const currentPointIds = isSelectionForCurrentMask + ? (selectedMaskPointSelection?.pointIds ?? []) + : []; + const nextPointIds = toggleSelection + ? currentPointIds.includes(pointId) + ? currentPointIds.filter( + (currentPointId) => currentPointId !== pointId, + ) + : [...currentPointIds, pointId] + : [pointId]; + + if (nextPointIds.length === 0) { + editor.selection.clearMaskPointSelection(); + return; + } + + editor.selection.setSelectedMaskPoints({ + selection: { + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + maskId: selectedWithMask.mask.id, + pointIds: nextPointIds, + }, + }); + }, + [editor.selection, selectedMaskPointSelection, selectedWithMask], + ); + + const clearMaskHandleState = useCallback(() => { + dragStateRef.current = null; + pendingSegmentInsertRef.current = null; + setActiveHandleId(null); + onSnapLinesChange?.([]); + }, [onSnapLinesChange]); + + const releaseCapturedPointer = useCallback(() => { + const capture = captureRef.current; + if (!capture) return; + + if (capture.element.hasPointerCapture(capture.pointerId)) { + capture.element.releasePointerCapture(capture.pointerId); + } + + captureRef.current = null; + }, []); + + useEffect(() => { + if (!activeHandleId) return; + + return registerCanceller({ + fn: () => { + editor.timeline.discardPreview(); + clearMaskHandleState(); + releaseCapturedPointer(); + }, + }); + }, [ + activeHandleId, + clearMaskHandleState, + editor.timeline, + releaseCapturedPointer, + ]); + + const handlePointerDown = useCallback( + ({ event, handleId }: { event: React.PointerEvent; handleId: string }) => { + if (!selectedWithMask) return; + if (event.button !== 0) return; + event.stopPropagation(); + const parsedHandle = + selectedWithMask.mask.type === "custom" + ? parseCustomMaskHandleId({ handleId }) + : null; + const parsedSegmentHandle = + selectedWithMask.mask.type === "custom" + ? parseCustomMaskSegmentHandleId({ handleId }) + : null; + if (isCreatingCustomMask) { + const firstPointId = customMaskPointIds[0]; + if ( + firstPointId && + handleId === `point:${firstPointId}:anchor` && + customMaskPointIds.length >= 3 && + selectedWithMask.mask.type === "custom" + ) { + const updatedMask = withUpdatedMaskParams({ + mask: selectedWithMask.mask, + params: { + ...selectedWithMask.mask.params, + closed: true, + }, + }); + editor.timeline.updateElements({ + updates: [ + { + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + patch: { + masks: replaceElementMask({ + masks: selectedWithMask.element.masks, + updatedMask, + }), + } as Partial<MaskableElement>, + }, + ], + }); + } + return; + } + + const pos = viewport.screenToCanvas({ + clientX: event.clientX, + clientY: event.clientY, + }); + if (!pos) return; + + if (parsedSegmentHandle && selectedWithMask.mask.type === "custom") { + setActiveHandleId(handleId); + pendingSegmentInsertRef.current = { + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + maskId: selectedWithMask.mask.id, + segmentIndex: parsedSegmentHandle.segmentIndex, + startClientX: event.clientX, + startClientY: event.clientY, + startCanvasX: pos.x, + startCanvasY: pos.y, + startParams: { ...selectedWithMask.mask.params }, + bounds: selectedWithMask.bounds, + }; + const captureTarget = event.currentTarget as HTMLElement; + captureTarget.setPointerCapture(event.pointerId); + captureRef.current = { + element: captureTarget, + pointerId: event.pointerId, + }; + return; + } + + if (parsedHandle?.part === "anchor") { + updateCustomMaskPointSelection({ + pointId: parsedHandle.pointId, + toggleSelection: event.shiftKey, + }); + if (event.shiftKey) { + return; + } + } else if (selectedWithMask.mask.type === "custom") { + editor.selection.clearMaskPointSelection(); + } + + dragStateRef.current = { + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + handleId, + startCanvasX: pos.x, + startCanvasY: pos.y, + startParams: { ...selectedWithMask.mask.params }, + }; + setActiveHandleId(handleId); + const captureTarget = event.currentTarget as HTMLElement; + captureTarget.setPointerCapture(event.pointerId); + captureRef.current = { + element: captureTarget, + pointerId: event.pointerId, + }; + }, + [ + customMaskPointIds, + editor.selection, + editor.timeline, + isCreatingCustomMask, + selectedWithMask, + updateCustomMaskPointSelection, + viewport, + ], + ); + + const handleCanvasPointerDown = useCallback( + ({ event }: { event: React.PointerEvent }) => { + if (!selectedWithMask || !isCreatingCustomMask) { + return; + } + if (event.button !== 0) { + return; + } + if (selectedWithMask.mask.type !== "custom") { + return; + } + + event.stopPropagation(); + const pos = viewport.screenToCanvas({ + clientX: event.clientX, + clientY: event.clientY, + }); + if (!pos) { + return; + } + + const nextParams = appendPointToCustomMask({ + params: selectedWithMask.mask.params, + canvasPoint: pos, + bounds: selectedWithMask.bounds, + }); + const updatedMask = withUpdatedMaskParams({ + mask: selectedWithMask.mask, + params: nextParams, + }); + + editor.timeline.updateElements({ + updates: [ + { + trackId: selectedWithMask.trackId, + elementId: selectedWithMask.elementId, + patch: { + masks: replaceElementMask({ + masks: selectedWithMask.element.masks, + updatedMask, + }), + } as Partial<MaskableElement>, + }, + ], + }); + }, + [editor.timeline, isCreatingCustomMask, selectedWithMask, viewport], + ); + + const handlePointerMove = useCallback( + ({ event }: { event: React.PointerEvent }) => { + const pendingSegmentInsert = pendingSegmentInsertRef.current; + if (pendingSegmentInsert && !dragStateRef.current) { + const distance = Math.hypot( + event.clientX - pendingSegmentInsert.startClientX, + event.clientY - pendingSegmentInsert.startClientY, + ); + if (distance >= SEGMENT_CLICK_DRAG_THRESHOLD_PX) { + dragStateRef.current = { + trackId: pendingSegmentInsert.trackId, + elementId: pendingSegmentInsert.elementId, + handleId: "position", + startCanvasX: pendingSegmentInsert.startCanvasX, + startCanvasY: pendingSegmentInsert.startCanvasY, + startParams: pendingSegmentInsert.startParams, + }; + pendingSegmentInsertRef.current = null; + setActiveHandleId("position"); + } + } + + const drag = dragStateRef.current; + if (!drag || !selectedWithMask) return; + + const pos = viewport.screenToCanvas({ + clientX: event.clientX, + clientY: event.clientY, + }); + if (!pos) return; + + const deltaX = pos.x - drag.startCanvasX; + const deltaY = pos.y - drag.startCanvasY; + const def = masksRegistry.get(selectedWithMask.mask.type); + + const rawParams = def.computeParamUpdate({ + handleId: drag.handleId, + startParams: drag.startParams, + deltaX, + deltaY, + startCanvasX: drag.startCanvasX, + startCanvasY: drag.startCanvasY, + bounds: selectedWithMask.bounds, + canvasSize, + }); + const proposedParams = { ...drag.startParams, ...rawParams }; + + const snapThreshold = viewport.screenPixelsToLogicalThreshold({ + screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS, + }); + const { params: nextParams, activeLines } = isShiftHeldRef.current + ? { params: proposedParams, activeLines: [] as SnapLine[] } + : (def.interaction.snap?.({ + handleId: drag.handleId, + startParams: drag.startParams, + proposedParams, + bounds: selectedWithMask.bounds, + canvasSize, + snapThreshold, + }) ?? { params: proposedParams, activeLines: [] as SnapLine[] }); + + onSnapLinesChange?.(activeLines); + + const updatedMask = withUpdatedMaskParams({ + mask: selectedWithMask.mask, + params: nextParams as typeof selectedWithMask.mask.params, + }); + editor.timeline.previewElements({ + updates: [ + { + trackId: drag.trackId, + elementId: drag.elementId, + updates: { + masks: replaceElementMask({ + masks: selectedWithMask.element.masks, + updatedMask, + }), + } as Partial<MaskableElement>, + }, + ], + }); + }, + [ + selectedWithMask, + canvasSize, + editor, + isShiftHeldRef, + onSnapLinesChange, + viewport, + ], + ); + + const handlePointerUp = useCallback(() => { + const pendingSegmentInsert = pendingSegmentInsertRef.current; + if (pendingSegmentInsert && !dragStateRef.current) { + editor.timeline.insertCustomMaskPoint({ + trackId: pendingSegmentInsert.trackId, + elementId: pendingSegmentInsert.elementId, + maskId: pendingSegmentInsert.maskId, + segmentIndex: pendingSegmentInsert.segmentIndex, + canvasPoint: { + x: pendingSegmentInsert.startCanvasX, + y: pendingSegmentInsert.startCanvasY, + }, + bounds: pendingSegmentInsert.bounds, + }); + clearMaskHandleState(); + releaseCapturedPointer(); + return; + } + + if (dragStateRef.current) { + editor.timeline.commitPreview(); + clearMaskHandleState(); + } + releaseCapturedPointer(); + }, [clearMaskHandleState, editor, releaseCapturedPointer]); + + return { + selectedWithMask, + handlePositions, + overlays, + isCreatingCustomMask, + handleCanvasPointerDown, + activeHandleId, + handlePointerDown, + handlePointerMove, + handlePointerUp, + }; +} |
