import { useCallback, useState, type Dispatch, type SetStateAction, } from 'react'; import type { CanvasClipboard, CanvasContextMenuState, CanvasLayer, } from './ImageCanvasEditorTypes'; import { createCanvasLayerClipboard, duplicateCanvasLayers, flipCanvasLayers, getCanvasLayersByIds, groupCanvasLayers, moveCanvasLayers, removeCanvasLayers, resolveContextTargetLayerIds, toggleCanvasLayersLock, toggleCanvasLayersVisibility, ungroupCanvasLayers, updateCanvasLayersByIds, type CanvasLayerFlipAxis, type CanvasLayerMoveMode, } from './ImageCanvasLayerCommandModel'; type LayerCommandsOptions = { layers: CanvasLayer[]; contextMenu: CanvasContextMenuState | null; selectedLayerId: string | null; selectedLayerIds: string[]; setLayers: Dispatch>; setSelectedLayerId: Dispatch>; setSelectedLayerIds: Dispatch>; setHoveredLayerId: Dispatch>; setMetadataLayer: Dispatch>; setContextMenu: Dispatch>; setImageContextMenu: (menu: null) => void; setActiveTool: (tool: 'select') => void; captureCanvasHistory: () => void; selectSingleLayer: (layerId: string | null) => void; onDeleteLayerSideEffects: (targetLayerId: string) => void; exportLayerImage: (layer: CanvasLayer | null) => void; }; function createGroupId() { return `layer-group-${Date.now()}`; } export function useImageCanvasLayerCommands({ layers, contextMenu, selectedLayerId, selectedLayerIds, setLayers, setSelectedLayerId, setSelectedLayerIds, setHoveredLayerId, setMetadataLayer, setContextMenu, setImageContextMenu, setActiveTool, captureCanvasHistory, selectSingleLayer, onDeleteLayerSideEffects, exportLayerImage, }: LayerCommandsOptions) { const [canvasClipboard, setCanvasClipboard] = useState(null); const getContextTargetLayerIds = useCallback( (menu: CanvasContextMenuState | null = contextMenu) => resolveContextTargetLayerIds(menu, selectedLayerIds), [contextMenu, selectedLayerIds], ); const duplicateLayersToPoint = useCallback( ( sourceLayers: CanvasLayer[], canvasPoint?: { x: number; y: number }, options: { renameCopies?: boolean } = {}, ) => duplicateCanvasLayers({ sourceLayers, allLayers: layers, canvasPoint, renameCopies: options.renameCopies !== false, }), [layers], ); const pasteCanvasClipboard = useCallback( (canvasPoint?: { x: number; y: number }) => { if (!canvasClipboard?.layers.length) { return; } const nextLayers = duplicateLayersToPoint( canvasClipboard.layers, canvasPoint, { renameCopies: canvasClipboard.mode !== 'cut', }, ); if (!nextLayers.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => [...currentLayers, ...nextLayers]); setSelectedLayerIds(nextLayers.map((layer) => layer.id)); setSelectedLayerId(nextLayers[0]?.id ?? null); setActiveTool('select'); setContextMenu(null); }, [ canvasClipboard, captureCanvasHistory, duplicateLayersToPoint, setActiveTool, setContextMenu, setLayers, setSelectedLayerId, setSelectedLayerIds, ], ); const copyContextLayers = useCallback( (options: { cut?: boolean } = {}) => { const targetIds = getContextTargetLayerIds(); const clipboard = createCanvasLayerClipboard( layers, targetIds, options.cut ? 'cut' : 'copy', ); if (!clipboard) { return; } setCanvasClipboard(clipboard); if (options.cut) { captureCanvasHistory(); setLayers((currentLayers) => removeCanvasLayers(currentLayers, targetIds), ); selectSingleLayer(null); setMetadataLayer((currentLayer) => currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer, ); } setContextMenu(null); }, [ captureCanvasHistory, getContextTargetLayerIds, layers, selectSingleLayer, setContextMenu, setLayers, setMetadataLayer, ], ); const duplicateContextLayers = useCallback(() => { const targetIds = getContextTargetLayerIds(); const targetLayers = getCanvasLayersByIds(layers, targetIds); const nextLayers = duplicateLayersToPoint(targetLayers); if (!nextLayers.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => [...currentLayers, ...nextLayers]); setSelectedLayerIds(nextLayers.map((layer) => layer.id)); setSelectedLayerId(nextLayers[0]?.id ?? null); setContextMenu(null); }, [ captureCanvasHistory, duplicateLayersToPoint, getContextTargetLayerIds, layers, setContextMenu, setLayers, setSelectedLayerId, setSelectedLayerIds, ]); const updateContextLayers = useCallback( (updater: (layer: CanvasLayer, targetIds: string[]) => CanvasLayer) => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => updateCanvasLayersByIds(currentLayers, targetIds, updater), ); setContextMenu(null); }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers], ); const moveContextLayers = useCallback( (mode: CanvasLayerMoveMode) => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => moveCanvasLayers(currentLayers, targetIds, mode), ); setContextMenu(null); }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers], ); const groupContextLayers = useCallback(() => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => groupCanvasLayers(currentLayers, targetIds, createGroupId()), ); setContextMenu(null); }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]); const ungroupContextLayers = useCallback(() => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => ungroupCanvasLayers(currentLayers, targetIds)); setContextMenu(null); }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]); const toggleContextLayerVisibility = useCallback(() => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => toggleCanvasLayersVisibility(currentLayers, targetIds), ); setContextMenu(null); }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]); const toggleContextLayerLock = useCallback(() => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => toggleCanvasLayersLock(currentLayers, targetIds), ); setContextMenu(null); }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers]); const flipContextLayers = useCallback( (axis: CanvasLayerFlipAxis) => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => flipCanvasLayers(currentLayers, targetIds, axis), ); setContextMenu(null); }, [captureCanvasHistory, getContextTargetLayerIds, setContextMenu, setLayers], ); const deleteContextLayers = useCallback(() => { const targetIds = getContextTargetLayerIds(); if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => removeCanvasLayers(currentLayers, targetIds)); selectSingleLayer(null); setHoveredLayerId(null); setMetadataLayer((currentLayer) => currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer, ); setContextMenu(null); }, [ captureCanvasHistory, getContextTargetLayerIds, selectSingleLayer, setContextMenu, setHoveredLayerId, setLayers, setMetadataLayer, ]); const exportContextLayer = useCallback(() => { const targetIds = getContextTargetLayerIds(); const targetLayer = layers.find((layer) => targetIds.includes(layer.id)); exportLayerImage(targetLayer ?? null); setContextMenu(null); }, [exportLayerImage, getContextTargetLayerIds, layers, setContextMenu]); const deleteLayerById = useCallback( (targetLayerId: string | null) => { if (!targetLayerId) { return; } setImageContextMenu(null); setContextMenu(null); captureCanvasHistory(); setLayers((currentLayers) => { const nextLayers = currentLayers.filter( (layer) => layer.id !== targetLayerId, ); const nextSelectedLayer = nextLayers .slice() .sort((left, right) => right.zIndex - left.zIndex)[0]; selectSingleLayer(nextSelectedLayer?.id ?? null); return nextLayers; }); setHoveredLayerId(null); setMetadataLayer((currentLayer) => currentLayer?.id === targetLayerId ? null : currentLayer, ); onDeleteLayerSideEffects(targetLayerId); }, [ captureCanvasHistory, onDeleteLayerSideEffects, selectSingleLayer, setContextMenu, setHoveredLayerId, setImageContextMenu, setLayers, setMetadataLayer, ], ); const deleteSelectedLayer = useCallback(() => { const targetIds = selectedLayerIds.length ? selectedLayerIds : selectedLayerId ? [selectedLayerId] : []; if (targetIds.length <= 1) { deleteLayerById(targetIds[0] ?? null); return; } captureCanvasHistory(); setImageContextMenu(null); setContextMenu(null); setLayers((currentLayers) => { const nextLayers = currentLayers.filter( (layer) => !targetIds.includes(layer.id), ); const nextSelectedLayer = nextLayers .slice() .sort((left, right) => right.zIndex - left.zIndex)[0]; selectSingleLayer(nextSelectedLayer?.id ?? null); return nextLayers; }); setHoveredLayerId(null); setMetadataLayer((currentLayer) => currentLayer && targetIds.includes(currentLayer.id) ? null : currentLayer, ); targetIds.forEach((targetId) => onDeleteLayerSideEffects(targetId)); }, [ captureCanvasHistory, deleteLayerById, onDeleteLayerSideEffects, selectSingleLayer, selectedLayerId, selectedLayerIds, setContextMenu, setHoveredLayerId, setImageContextMenu, setLayers, setMetadataLayer, ]); const groupSelectedLayers = useCallback(() => { const targetIds = selectedLayerIds.length ? selectedLayerIds : selectedLayerId ? [selectedLayerId] : []; if (!targetIds.length) { return; } captureCanvasHistory(); setLayers((currentLayers) => groupCanvasLayers(currentLayers, targetIds, createGroupId()), ); }, [captureCanvasHistory, selectedLayerId, selectedLayerIds, setLayers]); return { canvasClipboard, getContextTargetLayerIds, pasteCanvasClipboard, copyContextLayers, duplicateContextLayers, updateContextLayers, moveContextLayers, groupContextLayers, ungroupContextLayers, toggleContextLayerVisibility, toggleContextLayerLock, flipContextLayers, deleteContextLayers, exportContextLayer, deleteLayerById, deleteSelectedLayer, groupSelectedLayers, }; }