import { useEffect, 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, getInterSectionLineNotOverCoordinate, } from '@/util/canvas-util' import { useAdsorptionPoint } from '@/hooks/useAdsorptionPoint' import { useDotLineGrid } from '@/hooks/useDotLineGrid' import { useTempGrid } from '@/hooks/useTempGrid' import { gridColorState } from '@/store/gridAtom' import { gridDisplaySelector } from '@/store/settingAtom' import { MENU, POLYGON_TYPE } from '@/common/common' import { useMouse } from '@/hooks/useMouse' export function useEvent() { const canvas = useRecoilValue(canvasState) const currentMenu = useRecoilValue(currentMenuState) const documentEventListeners = useRef([]) const mouseEventListeners = useRef([]) const setCanvasZoom = useSetRecoilState(canvasZoomState) const gridColor = useRecoilValue(gridColorState) const isGridDisplay = useRecoilValue(gridDisplaySelector) const zoom = useRecoilValue(canvasZoomState) const { adsorptionPointAddMode, adsorptionPointMode, adsorptionRange, getAdsorptionPoints, adsorptionPointAddModeStateEvent } = useAdsorptionPoint() const { dotLineGridSetting, interval, getClosestLineGrid } = useDotLineGrid() const { tempGridModeStateLeftClickEvent, tempGridMode } = useTempGrid() const roofAdsorptionPoints = useRef([]) const intersectionPoints = useRef([]) const textMode = useRecoilValue(textModeState) const { getIntersectMousePoint } = useMouse() // 이벤트 초기화 위치 수정 -> 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() } useEffect(() => { const whiteMenus = [MENU.BATCH_CANVAS.SURFACE_SHAPE_BATCH, MENU.BATCH_CANVAS.OBJECT_BATCH, MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING] if (canvas && !whiteMenus.includes(currentMenu)) { addCanvasMouseEventListener('mouse:move', defaultMouseMoveEvent) } }, [zoom]) 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() // 현재 줌 값 // console.log('zoom', zoom, 'delta', delta) zoom += delta > 0 ? -0.1 : 0.1 // 줌 값 제한 (최소 0.5배, 최대 5배) if (zoom > 5) zoom = 5 if (zoom < 0.1) zoom = 0.1 setCanvasZoom(Number((zoom * 100).toFixed(0))) // 마우스 위치 기준으로 확대/축소 canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom) canvas.getObjects().forEach((obj) => { obj.setCoords() }) canvas.renderAll() // 이벤트의 기본 동작 방지 (스크롤 방지) opt.e.preventDefault() opt.e.stopPropagation() } const defaultMouseOutEvent = (e) => { removeMouseLine() } const defaultMouseMoveEvent = (e) => { removeMouseLine() const roofs = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) // 가로선 const pointer = canvas.getPointer(e.e) let arrivalPoint = { x: pointer.x, y: pointer.y } if (adsorptionPointMode) { const roofsPoints = roofs.map((roof) => roof.getCurrentPoints()).flat() roofAdsorptionPoints.current = [...roofsPoints] const auxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine') const otherAdsorptionPoints = [] auxiliaryLines.forEach((line1) => { auxiliaryLines.forEach((line2) => { if (line1 === line2) { return } const intersectionPoint = calculateIntersection(line1, line2) // 보조선끼리 만나는 점 const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') // 외벽선 outerLines.forEach((outerLine) => { const outerIntersectionPoint = calculateIntersection(outerLine, line1) // 외벽선과 보조선의 교차점 if (outerIntersectionPoint) { intersectionPoints.current.push(outerIntersectionPoint) } }) if (!intersectionPoint || intersectionPoints.current.some((point) => point.x === intersectionPoint.x && point.y === intersectionPoint.y)) { return } otherAdsorptionPoints.push(intersectionPoint) }) }) let innerLinePoints = [] let outerLinePoints = [] canvas .getObjects() .filter((obj) => obj.innerLines) .forEach((polygon) => { polygon.innerLines.forEach((line) => { innerLinePoints.push({ x: line.x1, y: line.y1 }) innerLinePoints.push({ x: line.x2, y: line.y2 }) }) }) const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') outerLines.forEach((line) => { outerLinePoints.push({ x: line.x2, y: line.y2 }) outerLinePoints.push({ x: line.x1, y: line.y1 }) }) const allAuxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine') allAuxiliaryLines.forEach((aux) => { const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) roofs.forEach((roof) => { //지붕과 보조선이 만나는 지점 roof.lines.forEach((line) => { const intersectionPoint = calculateIntersection(aux, line) if (intersectionPoint) { intersectionPoints.current.push(intersectionPoint) } }) //innerLines와 보조선이 만나는 지점 roof.innerLines.forEach((line) => { const intersectionPoint = calculateIntersection(aux, line) if (intersectionPoint) { intersectionPoints.current.push(intersectionPoint) } }) }) // outerLines와의 교점 const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') outerLines.forEach((outerLine) => { const intersectionPoint = calculateIntersection(aux, outerLine) if (intersectionPoint) { intersectionPoints.current.push(intersectionPoint) } }) }) const modulePoints = [] const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE) modules.forEach((module) => { module.points.forEach((point) => { modulePoints.push({ x: point.x, y: point.y }) }) }) let adsorptionPoints = [ ...getAdsorptionPoints(), ...roofAdsorptionPoints.current, ...otherAdsorptionPoints, ...intersectionPoints.current, ...innerLinePoints, ...outerLinePoints, ...allAuxiliaryLines.map((line) => { return { x: line.x1, y: line.y1, } }), ...allAuxiliaryLines.map((line) => { return { x: line.x2, y: line.y2, } }), ...modulePoints, ] adsorptionPoints = removeDuplicatePoints(adsorptionPoints) if (dotLineGridSetting.LINE || canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name)).length > 1) { const closestLine = getClosestLineGrid(pointer) const horizonLines = canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name) && obj.direction === 'horizontal') const verticalLines = canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name) && obj.direction === 'vertical') if (!horizonLines || !verticalLines) { drawMouseLine(pointer) return } let closestHorizontalLine = null let closestVerticalLine = null if (horizonLines && horizonLines.length > 0) { closestHorizontalLine = horizonLines.reduce((prev, curr) => { const prevDistance = calculateDistance(pointer, prev) const currDistance = calculateDistance(pointer, curr) return prevDistance < currDistance ? prev : curr }) } if (verticalLines && verticalLines.length > 0) { closestVerticalLine = verticalLines.reduce((prev, curr) => { const prevDistance = calculateDistance(pointer, prev) const currDistance = calculateDistance(pointer, curr) return prevDistance < currDistance ? prev : curr }) } if (!closestVerticalLine || !closestHorizontalLine) { drawMouseLine(pointer) return } 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) { console.error(e) } drawMouseLine(arrivalPoint) // 캔버스를 다시 그립니다. canvas?.renderAll() } const drawMouseLine = (pointer) => { // 캔버스의 실제 보이는 영역 계산 (zoom과 pan 고려) const canvasWidth = canvas.getWidth() const canvasHeight = canvas.getHeight() const currentZoom = canvas.getZoom() const viewportTransform = canvas.viewportTransform const visibleLeft = -viewportTransform[4] / currentZoom const visibleTop = -viewportTransform[5] / currentZoom const visibleRight = visibleLeft + canvasWidth / currentZoom const visibleBottom = visibleTop + canvasHeight / currentZoom // 여유 공간 추가 const padding = 200 const lineLeft = visibleLeft - padding const lineTop = visibleTop - padding const lineRight = visibleRight + padding const lineBottom = visibleBottom + padding // 가로선 (수평선) const horizontalLine = new fabric.Line([lineLeft, pointer.y, lineRight, pointer.y], { stroke: 'red', strokeWidth: 1, selectable: false, direction: 'horizontal', name: 'mouseLine', }) // 세로선 (수직선) const verticalLine = new fabric.Line([pointer.x, lineTop, pointer.x, lineBottom], { stroke: 'red', strokeWidth: 1, selectable: false, direction: 'vertical', name: 'mouseLine', }) // 선들을 캔버스에 추가합니다. canvas?.add(horizontalLine, verticalLine) } const removeMouseLine = () => { // 캔버스에서 마우스 선을 찾아 제거합니다. canvas ?.getObjects() .filter((obj) => obj.name === 'mouseLine') .forEach((line) => { canvas?.remove(line) }) } const tempGridRightClickEvent = (e) => { e.preventDefault() e.stopPropagation() //임의 그리드 모드일 경우 let originPointer = { x: e.offsetX, y: e.offsetY } const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine') let pointer = getInterSectionLineNotOverCoordinate(mouseLines[0], mouseLines[1]) || { x: Math.round(originPointer.x), y: Math.round(originPointer.y), } const tempGrid = new fabric.Line([-1500, pointer.y, 2500, pointer.y], { stroke: gridColor, strokeWidth: 1, selectable: true, lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, strokeDashArray: [5, 2], opacity: 0.3, padding: 5, name: 'tempGrid', visible: isGridDisplay, direction: 'horizontal', }) canvas.add(tempGrid) canvas.renderAll() } const addCanvasMouseEventListener = (eventType, handler) => { canvas.off(eventType) canvas.on(eventType, handler) canvas.on('mouse:move', defaultMouseMoveEvent) // default mouse:move 이벤트는 항상 등록 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 }) } const removeDuplicatePoints = (points) => { const map = new Map() points.forEach((point) => { const key = `${point.x},${point.y}` if (!map.has(key)) { map.set(key, point) } }) return Array.from(map.values()) } return { addDocumentEventListener, addCanvasMouseEventListener, addTargetMouseEventListener, removeAllMouseEventListeners, removeAllDocumentEventListeners, removeDocumentEvent, removeMouseEvent, removeMouseLine, defaultMouseMoveEvent, initEvent, } }