import { useRef } from 'react' import { useRecoilValue, useSetRecoilState } from 'recoil' import { canvasState, canvasZoomState, currentMenuState, textModeState } from '@/store/canvasAtom' import { fabric } from 'fabric' import { calculateDistance, calculateDistancePoint, calculateIntersection, distanceBetweenPoints, findClosestPoint } from '@/util/canvas-util' import { useAdsorptionPoint } from '@/hooks/useAdsorptionPoint' import { useDotLineGrid } from '@/hooks/useDotLineGrid' import { useTempGrid } from '@/hooks/useTempGrid' export function useEvent() { const canvas = useRecoilValue(canvasState) const currentMenu = useRecoilValue(currentMenuState) const documentEventListeners = useRef([]) const mouseEventListeners = useRef([]) const setCanvasZoom = useSetRecoilState(canvasZoomState) const { adsorptionPointAddMode, adsorptionPointMode, adsorptionRange, getAdsorptionPoints, adsorptionPointAddModeStateEvent } = useAdsorptionPoint() const { dotLineGridSetting, interval, getClosestLineGrid } = useDotLineGrid() const { tempGridModeStateLeftClickEvent, tempGridMode, tempGridRightClickEvent } = useTempGrid() const textMode = useRecoilValue(textModeState) // 이벤트 초기화 위치 수정 -> useCanvasSetting에서 세팅값 불러오고 나서 초기화 함수 호출 // useEffect(() => { // initEvent() // }, [currentMenu, canvas, adsorptionPointAddMode, adsorptionPointMode, adsorptionRange, dotLineGridSetting]) // 임시 그리드 모드 변경 시 이벤트 초기화 호출 위치 수정 -> GridOption 컴포넌트에서 임시 그리드 모드 변경 시 이벤트 초기화 함수 호출 // useEffect(() => { // initEvent() // }, [tempGridMode]) const initEvent = () => { if (!canvas) { return } removeAllDocumentEventListeners() removeAllMouseEventListeners() /** * wheelEvent */ canvas?.off('mouse:wheel') canvas?.on('mouse:wheel', wheelEvent) addDefaultEvent() } const addDefaultEvent = () => { //default Event 추가 addCanvasMouseEventListener('mouse:move', defaultMouseMoveEvent) addCanvasMouseEventListener('mouse:out', defaultMouseOutEvent) addDocumentEventListener('contextmenu', document, defaultContextMenuEvent) if (adsorptionPointAddMode) { addCanvasMouseEventListener('mouse:down', adsorptionPointAddModeStateEvent) } if (tempGridMode) { addCanvasMouseEventListener('mouse:down', tempGridModeStateLeftClickEvent) addDocumentEventListener('contextmenu', document, tempGridRightClickEvent) } } const defaultContextMenuEvent = (e) => { e.preventDefault() e.stopPropagation() } const wheelEvent = (opt) => { const delta = opt.e.deltaY // 휠 이동 값 (양수면 축소, 음수면 확대) let zoom = canvas.getZoom() // 현재 줌 값 zoom += delta > 0 ? -0.1 : 0.1 // 줌 값 제한 (최소 0.5배, 최대 5배) if (zoom > 5) zoom = 5 if (zoom < 0.5) zoom = 0.5 setCanvasZoom(Number((zoom * 100).toFixed(0))) // 마우스 위치 기준으로 확대/축소 canvas.zoomToPoint(new fabric.Point(opt.e.offsetX, opt.e.offsetY), zoom) canvas.calcOffset() canvas.setViewportTransform(canvas.viewportTransform) canvas.requestRenderAll() // 이벤트의 기본 동작 방지 (스크롤 방지) opt.e.preventDefault() opt.e.stopPropagation() } const defaultMouseOutEvent = (e) => { removeMouseLine() } const defaultMouseMoveEvent = (e) => { removeMouseLine() // 가로선 const pointer = canvas.getPointer(e.e) const adsorptionPoints = getAdsorptionPoints() let arrivalPoint = { x: pointer.x, y: pointer.y } if (adsorptionPointMode) { if (dotLineGridSetting.LINE || canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name)).length > 0) { const closestLine = getClosestLineGrid(pointer) const horizonLines = canvas.getObjects().filter((obj) => obj.name === 'lineGrid' && obj.direction === 'horizontal') const verticalLines = canvas.getObjects().filter((obj) => obj.name === 'lineGrid' && obj.direction === 'vertical') if (!horizonLines || !verticalLines) { return } const closestHorizontalLine = horizonLines.reduce((prev, curr) => { const prevDistance = calculateDistance(pointer, prev) const currDistance = calculateDistance(pointer, curr) return prevDistance < currDistance ? prev : curr }) const closestVerticalLine = verticalLines.reduce((prev, curr) => { const prevDistance = calculateDistance(pointer, prev) const currDistance = calculateDistance(pointer, curr) return prevDistance < currDistance ? prev : curr }) const closestIntersectionPoint = calculateIntersection(closestHorizontalLine, closestVerticalLine) if (closestLine) { const distanceClosestLine = calculateDistance(pointer, closestLine) let distanceClosestPoint = Infinity if (closestIntersectionPoint) { distanceClosestPoint = calculateDistancePoint(pointer, closestIntersectionPoint) } if (distanceClosestLine < adsorptionRange) { arrivalPoint = closestLine.direction === 'vertical' ? { x: closestLine.x1, y: pointer.y } : { x: pointer.x, y: closestLine.y1, } if (distanceClosestPoint * 2 < adsorptionRange) { arrivalPoint = { ...closestIntersectionPoint } } } } } if (dotLineGridSetting.DOT) { const horizontalInterval = interval.horizontalInterval const verticalInterval = interval.verticalInterval const x = pointer.x - horizontalInterval * Math.floor(pointer.x / horizontalInterval) const y = pointer.y - verticalInterval * Math.floor(pointer.y / verticalInterval) const xRate = (x / horizontalInterval) * 100 const yRate = (y / verticalInterval) * 100 let tempPoint if (xRate <= adsorptionRange && yRate <= adsorptionRange) { tempPoint = { x: Math.round(pointer.x / horizontalInterval) * horizontalInterval + horizontalInterval / 2, y: Math.round(pointer.y / verticalInterval) * verticalInterval + horizontalInterval / 2, } } else if (xRate <= adsorptionRange && yRate >= adsorptionRange) { tempPoint = { x: Math.round(pointer.x / horizontalInterval) * horizontalInterval + horizontalInterval / 2, y: Math.round(pointer.y / verticalInterval) * verticalInterval - horizontalInterval / 2, } } else if (xRate >= adsorptionRange && yRate <= adsorptionRange) { tempPoint = { x: Math.round(pointer.x / horizontalInterval) * horizontalInterval - horizontalInterval / 2, y: Math.round(pointer.y / verticalInterval) * verticalInterval + horizontalInterval / 2, } } else if (xRate >= adsorptionRange && yRate >= adsorptionRange) { tempPoint = { x: Math.round(pointer.x / horizontalInterval) * horizontalInterval - horizontalInterval / 2, y: Math.round(pointer.y / verticalInterval) * verticalInterval - horizontalInterval / 2, } } if (distanceBetweenPoints(pointer, tempPoint) <= adsorptionRange) { arrivalPoint = tempPoint } } // pointer와 adsorptionPoints의 거리를 계산하여 가장 가까운 점을 찾는다. let adsorptionPoint = findClosestPoint(pointer, adsorptionPoints) if (adsorptionPoint && distanceBetweenPoints(pointer, adsorptionPoint) <= adsorptionRange) { arrivalPoint = { ...adsorptionPoint } } } try { const helpGuideLines = canvas.getObjects().filter((obj) => obj.name === 'helpGuideLine') if (helpGuideLines.length === 2) { const guideIntersectionPoint = calculateIntersection(helpGuideLines[0], helpGuideLines[1]) if (guideIntersectionPoint && distanceBetweenPoints(guideIntersectionPoint, pointer) <= adsorptionRange) { arrivalPoint = guideIntersectionPoint } } } catch (e) {} const horizontalLine = new fabric.Line([-4 * canvas.width, arrivalPoint.y, 4 * canvas.width, arrivalPoint.y], { stroke: 'red', strokeWidth: 1, selectable: false, name: 'mouseLine', }) // 세로선 const verticalLine = new fabric.Line([arrivalPoint.x, -4 * canvas.height, arrivalPoint.x, 4 * canvas.height], { stroke: 'red', strokeWidth: 1, selectable: false, name: 'mouseLine', }) // 선들을 캔버스에 추가합니다. canvas?.add(horizontalLine, verticalLine) // 캔버스를 다시 그립니다. canvas?.renderAll() } const removeMouseLine = () => { // 캔버스에서 마우스 선을 찾아 제거합니다. canvas ?.getObjects() .filter((obj) => obj.name === 'mouseLine') .forEach((line) => { canvas?.remove(line) }) } const defaultKeyboardEvent = (e) => { if (e.key === 'Escape') { console.log('defaultKeyboardEvent') } } const addCanvasMouseEventListener = (eventType, handler) => { canvas.off(eventType) canvas.on(eventType, handler) mouseEventListeners.current.push({ eventType, handler }) } const removeAllMouseEventListeners = () => { mouseEventListeners.current.forEach(({ eventType, handler }) => { canvas.off(eventType) }) mouseEventListeners.current.length = 0 // 배열 초기화 } const addTargetMouseEventListener = (eventType, target, handler) => { target.off(eventType) target.on(eventType, handler) mouseEventListeners.current.push({ eventType, handler }) } /** * document 이벤트의 경우 이 함수를 통해서만 등록 * @param eventType * @param element * @param handler */ const addDocumentEventListener = (eventType, element, handler) => { removeDocumentEvent(eventType) element.addEventListener(eventType, handler) documentEventListeners.current.push({ eventType, element, handler }) } /** * document에 등록되는 event 제거 */ const removeAllDocumentEventListeners = () => { documentEventListeners.current.forEach(({ eventType, element, handler }) => { element.removeEventListener(eventType, handler) }) documentEventListeners.current.length = 0 // 배열 초기화 } const removeMouseEvent = (type) => { mouseEventListeners.current = mouseEventListeners.current.filter((event) => { if (event.eventType === type) { canvas.off(type, event.handler) return false } return true }) } const removeDocumentEvent = (type) => { documentEventListeners.current = documentEventListeners.current.filter((event) => { if (event.eventType === type) { document.removeEventListener(event.eventType, event.handler) return false } return true }) } return { addDocumentEventListener, addCanvasMouseEventListener, addTargetMouseEventListener, removeAllMouseEventListeners, removeAllDocumentEventListeners, removeDocumentEvent, removeMouseEvent, removeMouseLine, initEvent, } }