diff options
Diffstat (limited to 'apps/web/src/hooks/use-mask-handles.ts')
| -rw-r--r-- | apps/web/src/hooks/use-mask-handles.ts | 633 |
1 files changed, 0 insertions, 633 deletions
diff --git a/apps/web/src/hooks/use-mask-handles.ts b/apps/web/src/hooks/use-mask-handles.ts deleted file mode 100644 index 917a0677..00000000 --- a/apps/web/src/hooks/use-mask-handles.ts +++ /dev/null @@ -1,633 +0,0 @@ -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, - }; -} |
