| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { useSceneData, useSceneSelector } from '@/lib/contexts/scene-context'; |
| import { |
| useCanvasStore, |
| type SpotlightOptions, |
| type HighlightOverlayOptions, |
| } from '@/lib/store/canvas'; |
| import type { SlideContent } from '@/lib/types/stage'; |
| import type { PPTElement, Slide } from '@/lib/types/slides'; |
| import { useCallback, useMemo } from 'react'; |
| import { useHistorySnapshot } from '@/lib/hooks/use-history-snapshot'; |
| import { toast } from 'sonner'; |
| import { ElementAlignCommands, ElementOrderCommands } from '@/lib/types/edit'; |
| import { getElementListRange } from '@/lib/utils/element'; |
| import { useOrderElement } from './use-order-element'; |
| import { nanoid } from 'nanoid'; |
|
|
| type PPTElementKey = keyof PPTElement; |
|
|
| interface RemovePropData { |
| id: string; |
| propName: PPTElementKey | PPTElementKey[]; |
| } |
|
|
| interface UpdateElementData { |
| id: string | string[]; |
| props: Partial<PPTElement>; |
| slideId?: string; |
| } |
|
|
| export function useCanvasOperations() { |
| const { updateSceneData } = useSceneData<SlideContent>(); |
| const currentSlide = useSceneSelector<SlideContent, Slide>((content) => content.canvas); |
|
|
| const activeElementIdList = useCanvasStore.use.activeElementIdList(); |
| const activeElementList = useMemo( |
| () => currentSlide.elements.filter((el) => activeElementIdList.includes(el.id)), |
| [currentSlide.elements, activeElementIdList], |
| ); |
| const activeGroupElementId = useCanvasStore.use.activeGroupElementId(); |
| const setActiveElementIdList = useCanvasStore.use.setActiveElementIdList(); |
| const handleElementId = useCanvasStore.use.handleElementId(); |
| const hiddenElementIdList = useCanvasStore.use.hiddenElementIdList(); |
|
|
| const viewportSize = useCanvasStore.use.viewportSize(); |
| const viewportRatio = useCanvasStore.use.viewportRatio(); |
|
|
| const _setEditorareaFocus = useCanvasStore.use.setEditorAreaFocus(); |
|
|
| const { addHistorySnapshot } = useHistorySnapshot(); |
| const { moveUpElement, moveDownElement, moveTopElement, moveBottomElement } = useOrderElement(); |
|
|
| |
| |
| |
| |
| |
| const addElement = useCallback( |
| (element: PPTElement | PPTElement[], autoSelect = true) => { |
| const elements = Array.isArray(element) ? element : [element]; |
|
|
| updateSceneData((draft) => { |
| draft.canvas.elements.push(...elements); |
| }); |
|
|
| |
| if (autoSelect) { |
| const newIds = elements.map((el) => el.id); |
| setActiveElementIdList(newIds); |
| } |
| }, |
| [updateSceneData, setActiveElementIdList], |
| ); |
|
|
| |
| |
| |
| const deleteElement = (elementId?: string) => { |
| let newElementList: PPTElement[] = []; |
|
|
| if (elementId) { |
| |
| newElementList = currentSlide.elements.filter((el) => el.id !== elementId); |
| setActiveElementIdList(activeElementIdList.filter((id) => id !== elementId)); |
| } else { |
| |
| if (!activeElementIdList.length) return; |
|
|
| if (activeGroupElementId) { |
| newElementList = currentSlide.elements.filter((el) => el.id !== activeGroupElementId); |
| } else { |
| newElementList = currentSlide.elements.filter((el) => !activeElementIdList.includes(el.id)); |
| } |
| setActiveElementIdList([]); |
| } |
|
|
| updateSlide({ elements: newElementList }); |
| addHistorySnapshot(); |
| }; |
|
|
| |
| const deleteAllElements = () => { |
| if (!currentSlide.elements.length) return; |
| setActiveElementIdList([]); |
| updateSlide({ elements: [] }); |
| addHistorySnapshot(); |
| }; |
|
|
| |
| |
| |
| |
| const updateElement = useCallback( |
| (data: UpdateElementData) => { |
| const { id, props } = data; |
| const elementIds = Array.isArray(id) ? id : [id]; |
|
|
| updateSceneData((draft) => { |
| draft.canvas.elements.forEach((el) => { |
| if (elementIds.includes(el.id)) { |
| Object.assign(el, props); |
| } |
| }); |
| }); |
| }, |
| [updateSceneData], |
| ); |
|
|
| |
| |
| |
| const updateSlide = useCallback( |
| (props: Partial<Slide>) => { |
| updateSceneData((draft) => { |
| Object.assign(draft.canvas, props); |
| }); |
| }, |
| [updateSceneData], |
| ); |
|
|
| |
| |
| |
| const removeElementProps = useCallback( |
| (data: RemovePropData) => { |
| const { id, propName } = data; |
| const elementIds = Array.isArray(id) ? id : [id]; |
| const propNames = Array.isArray(propName) ? propName : [propName]; |
|
|
| updateSceneData((draft) => { |
| draft.canvas.elements.forEach((el) => { |
| if (elementIds.includes(el.id)) { |
| propNames.forEach((name) => { |
| delete el[name]; |
| }); |
| } |
| }); |
| }); |
| }, |
| [updateSceneData], |
| ); |
|
|
| |
| const copyElement = () => { |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| toast.warning('Not implemented'); |
| }; |
|
|
| |
| const cutElement = () => { |
| |
| |
| toast.warning('Not implemented'); |
| }; |
|
|
| |
| const pasteElement = () => { |
| |
| |
| |
| toast.warning('Not implemented'); |
| }; |
|
|
| |
| const _quickCopyElement = () => { |
| copyElement(); |
| pasteElement(); |
| }; |
|
|
| |
| const lockElement = () => { |
| const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.elements)); |
|
|
| for (const element of newElementList) { |
| if (activeElementIdList.includes(element.id)) element.lock = true; |
| } |
| updateSlide({ elements: newElementList }); |
| setActiveElementIdList([]); |
| addHistorySnapshot(); |
| }; |
|
|
| |
| |
| |
| |
| const unlockElement = (handleElement: PPTElement) => { |
| const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.elements)); |
|
|
| if (handleElement.groupId) { |
| const groupElementIdList = []; |
| for (const element of newElementList) { |
| if (element.groupId === handleElement.groupId) { |
| element.lock = false; |
| groupElementIdList.push(element.id); |
| } |
| } |
| updateSlide({ elements: newElementList }); |
| setActiveElementIdList(groupElementIdList); |
| } else { |
| for (const element of newElementList) { |
| if (element.id === handleElement.id) { |
| element.lock = false; |
| break; |
| } |
| } |
| updateSlide({ elements: newElementList }); |
| setActiveElementIdList([handleElement.id]); |
| } |
| addHistorySnapshot(); |
| }; |
|
|
| |
| const selectAllElements = () => { |
| const unlockedElements = currentSlide.elements.filter( |
| (el) => !el.lock && !hiddenElementIdList.includes(el.id), |
| ); |
| const newActiveElementIdList = unlockedElements.map((el) => el.id); |
| setActiveElementIdList(newActiveElementIdList); |
| }; |
|
|
| |
| const selectElement = (id: string) => { |
| if (handleElementId === id) return; |
| if (hiddenElementIdList.includes(id)) return; |
|
|
| const lockedElements = currentSlide.elements.filter((el) => el.lock); |
| if (lockedElements.some((el) => el.id === id)) return; |
|
|
| setActiveElementIdList([id]); |
| }; |
|
|
| |
| |
| |
| |
| const alignElementToCanvas = (command: ElementAlignCommands) => { |
| const viewportWidth = viewportSize; |
| const viewportHeight = viewportSize * viewportRatio; |
| const { minX, maxX, minY, maxY } = getElementListRange(activeElementList); |
|
|
| const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.elements)); |
| for (const element of newElementList) { |
| if (!activeElementIdList.includes(element.id)) continue; |
|
|
| |
| if (command === ElementAlignCommands.CENTER) { |
| const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2; |
| const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2; |
| element.top = element.top - offsetY; |
| element.left = element.left - offsetX; |
| } |
|
|
| |
| if (command === ElementAlignCommands.TOP) { |
| const offsetY = minY - 0; |
| element.top = element.top - offsetY; |
| } |
|
|
| |
| else if (command === ElementAlignCommands.VERTICAL) { |
| const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2; |
| element.top = element.top - offsetY; |
| } |
|
|
| |
| else if (command === ElementAlignCommands.BOTTOM) { |
| const offsetY = maxY - viewportHeight; |
| element.top = element.top - offsetY; |
| } |
|
|
| |
| else if (command === ElementAlignCommands.LEFT) { |
| const offsetX = minX - 0; |
| element.left = element.left - offsetX; |
| } |
|
|
| |
| else if (command === ElementAlignCommands.HORIZONTAL) { |
| const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2; |
| element.left = element.left - offsetX; |
| } |
|
|
| |
| else if (command === ElementAlignCommands.RIGHT) { |
| const offsetX = maxX - viewportWidth; |
| element.left = element.left - offsetX; |
| } |
| } |
|
|
| updateSlide({ elements: newElementList }); |
| addHistorySnapshot(); |
| }; |
|
|
| |
| |
| |
| |
| |
| const orderElement = (element: PPTElement, command: ElementOrderCommands) => { |
| let newElementList; |
|
|
| if (command === ElementOrderCommands.UP) |
| newElementList = moveUpElement(currentSlide.elements, element); |
| else if (command === ElementOrderCommands.DOWN) |
| newElementList = moveDownElement(currentSlide.elements, element); |
| else if (command === ElementOrderCommands.TOP) |
| newElementList = moveTopElement(currentSlide.elements, element); |
| else if (command === ElementOrderCommands.BOTTOM) |
| newElementList = moveBottomElement(currentSlide.elements, element); |
|
|
| if (!newElementList) return; |
|
|
| updateSlide({ elements: newElementList }); |
| addHistorySnapshot(); |
| }; |
|
|
| |
| |
| |
| const _canCombine = useMemo(() => { |
| if (activeElementList.length < 2) return false; |
|
|
| const firstGroupId = activeElementList[0].groupId; |
| if (!firstGroupId) return true; |
|
|
| const inSameGroup = activeElementList.every((el) => el.groupId && el.groupId === firstGroupId); |
| return !inSameGroup; |
| }, [activeElementList]); |
|
|
| |
| |
| |
| const combineElements = () => { |
| if (!activeElementList.length) return; |
|
|
| |
| let newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.elements)); |
|
|
| |
| const groupId = nanoid(10); |
|
|
| |
| const combineElementList: PPTElement[] = []; |
| for (const element of newElementList) { |
| if (activeElementIdList.includes(element.id)) { |
| element.groupId = groupId; |
| combineElementList.push(element); |
| } |
| } |
|
|
| |
| |
| |
| const combineElementMaxLevel = newElementList.findIndex( |
| (_element) => _element.id === combineElementList[combineElementList.length - 1].id, |
| ); |
| const combineElementIdList = combineElementList.map((_element) => _element.id); |
| newElementList = newElementList.filter( |
| (_element) => !combineElementIdList.includes(_element.id), |
| ); |
|
|
| const insertLevel = combineElementMaxLevel - combineElementList.length + 1; |
| newElementList.splice(insertLevel, 0, ...combineElementList); |
|
|
| updateSlide({ elements: newElementList }); |
| addHistorySnapshot(); |
| }; |
|
|
| |
| |
| |
| const uncombineElements = () => { |
| if (!activeElementList.length) return; |
| const hasElementInGroup = activeElementList.some((item) => item.groupId); |
| if (!hasElementInGroup) return; |
|
|
| const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.elements)); |
| for (const element of newElementList) { |
| if (activeElementIdList.includes(element.id) && element.groupId) delete element.groupId; |
| } |
| updateSlide({ elements: newElementList }); |
|
|
| |
| |
| const handleElementIdList = handleElementId ? [handleElementId] : []; |
| setActiveElementIdList(handleElementIdList); |
|
|
| addHistorySnapshot(); |
| }; |
|
|
| |
| |
| |
| |
| const updateBackground = useCallback( |
| (background: SlideContent['canvas']['background']) => { |
| updateSceneData((draft) => { |
| draft.canvas.background = background; |
| }); |
| }, |
| [updateSceneData], |
| ); |
|
|
| |
| |
| |
| |
| const updateTheme = useCallback( |
| (theme: Partial<SlideContent['canvas']['theme']>) => { |
| updateSceneData((draft) => { |
| draft.canvas.theme = { |
| ...draft.canvas.theme, |
| ...theme, |
| }; |
| }); |
| }, |
| [updateSceneData], |
| ); |
|
|
| |
| |
| |
| |
| |
| const spotlightElement = useCallback((elementId: string, options?: SpotlightOptions) => { |
| useCanvasStore.getState().setSpotlight(elementId, options); |
| }, []); |
|
|
| |
| |
| |
| const clearSpotlight = useCallback(() => { |
| useCanvasStore.getState().clearSpotlight(); |
| }, []); |
|
|
| |
| |
| |
| |
| |
| const highlightElements = useCallback( |
| (elementIds: string[], options?: HighlightOverlayOptions) => { |
| useCanvasStore.getState().setHighlight(elementIds, options); |
| }, |
| [], |
| ); |
|
|
| |
| |
| |
| const clearHighlight = useCallback(() => { |
| useCanvasStore.getState().clearHighlight(); |
| }, []); |
|
|
| |
| |
| |
| |
| |
| const laserElement = useCallback( |
| (elementId: string, options?: { color?: string; duration?: number }) => { |
| useCanvasStore.getState().setLaser(elementId, options); |
| }, |
| [], |
| ); |
|
|
| |
| |
| |
| const clearLaser = useCallback(() => { |
| useCanvasStore.getState().clearLaser(); |
| }, []); |
|
|
| |
| |
| |
| |
| |
| const zoomElement = useCallback((elementId: string, scale: number) => { |
| useCanvasStore.getState().setZoom(elementId, scale); |
| }, []); |
|
|
| |
| |
| |
| const clearZoom = useCallback(() => { |
| useCanvasStore.getState().clearZoom(); |
| }, []); |
|
|
| |
| |
| |
| const clearAllEffects = useCallback(() => { |
| useCanvasStore.getState().clearSpotlight(); |
| useCanvasStore.getState().clearHighlight(); |
| useCanvasStore.getState().clearLaser(); |
| useCanvasStore.getState().clearZoom(); |
| }, []); |
|
|
| return { |
| |
| addElement, |
| deleteElement, |
| deleteAllElements, |
| updateElement, |
| updateSlide, |
| removeElementProps, |
| copyElement, |
| pasteElement, |
| cutElement, |
|
|
| |
| lockElement, |
| unlockElement, |
| selectAllElements, |
| selectElement, |
| alignElementToCanvas, |
| orderElement, |
| combineElements, |
| uncombineElements, |
|
|
| |
| updateBackground, |
| updateTheme, |
|
|
| |
| spotlightElement, |
| clearSpotlight, |
| highlightElements, |
| clearHighlight, |
| laserElement, |
| clearLaser, |
| zoomElement, |
| clearZoom, |
| clearAllEffects, |
| }; |
| } |
|
|
| |
| export type CanvasOperations = ReturnType<typeof useCanvasOperations>; |
|
|