import { useEffect, useState } from 'react' import { useRecoilState, useRecoilValue } from 'recoil' import { v4 as uuidv4 } from 'uuid' import { canvasSizeState, canvasState, canvasZoomState, currentMenuState, currentObjectState } from '@/store/canvasAtom' import { QPolygon } from '@/components/fabric/QPolygon' import { fontSelector } from '@/store/fontAtom' import { MENU, POLYGON_TYPE } from '@/common/common' import { useText } from '@/hooks/useText' import { usePolygon } from '@/hooks/usePolygon' // 캔버스에 필요한 이벤트 export function useCanvasEvent() { const canvas = useRecoilValue(canvasState) const [canvasForEvent, setCanvasForEvent] = useState(null) const [currentObject, setCurrentObject] = useRecoilState(currentObjectState) const canvasSize = useRecoilValue(canvasSizeState) const [canvasZoom, setCanvasZoom] = useRecoilState(canvasZoomState) const lengthTextOption = useRecoilValue(fontSelector('lengthText')) const currentMenu = useRecoilValue(currentMenuState) const { changeCorridorDimensionText } = useText() const { setPolygonLinesActualSize } = usePolygon() useEffect(() => { canvas?.setZoom(canvasZoom / 100) }, [canvasZoom]) useEffect(() => { attachDefaultEventOnCanvas() }, [currentMenu]) // 기본적인 이벤트 필요시 추가 const attachDefaultEventOnCanvas = () => { removeEventOnCanvas() canvas?.on('object:added', objectEvent.onChange) canvas?.on('object:added', objectEvent.addEvent) canvas?.on('object:modified', objectEvent.onChange) canvas?.on('object:removed', objectEvent.onChange) canvas?.on('selection:cleared', selectionEvent.cleared) canvas?.on('selection:created', selectionEvent.created) canvas?.on('selection:updated', selectionEvent.updated) /*canvas?.on('object:added', () => { document.addEventListener('keydown', handleKeyDown) })*/ canvas?.on('object:removed', objectEvent.removed) } const objectEvent = { onChange: (e) => { const target = e.target if (target) { // settleDown(target) } }, addEvent: (e) => { const target = e.target if (!target.id) { target.id = uuidv4() } if (!target.uuid) { target.uuid = uuidv4() } if (target.type === 'QPolygon' || target.type === 'QLine') { const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'lengthText') textObjs.forEach((obj) => { obj.bringToFront() }) if (target.name === POLYGON_TYPE.ROOF) { setTimeout(() => { setPolygonLinesActualSize(target) changeCorridorDimensionText() }, 300) } } if (target.name === 'cell') { target.on('mousedown', () => { if (target.get('selected')) { target.set({ selected: false }) target.set({ fill: '#BFFD9F' }) } else { target.set({ selected: true }) target.set({ fill: 'red' }) } canvas?.renderAll() }) } if (target.name === 'trestle') { target.on('mousedown', () => { if (target.defense === 'north') { alert('북쪽은 선택 불가합니다.') return } if (target.get('selected')) { target.set({ strokeWidth: 1 }) target.set({ strokeDashArray: [5, 5] }) target.set({ selected: false }) } else { target.set({ strokeWidth: 5 }) target.set({ strokeDashArray: [0, 0] }) target.set({ selected: true }) } canvas?.renderAll() }) } if (target.name === 'lengthText' && target.type.toLowerCase().includes('text') > 0) { const x = target.left const y = target.top target.lockMovementX = false target.lockMovementY = false target.fill = lengthTextOption.fontColor.value target.fontFamily = lengthTextOption.fontFamily.value target.fontSize = lengthTextOption.fontSize.value target.fontStyle = lengthTextOption.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal' target.fontWeight = lengthTextOption.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal' // Add a property to store the previous value const previousValue = target.text target.on('selected', (e) => { Object.keys(target.controls).forEach((controlKey) => { target.setControlVisible(controlKey, false) }) }) target.on('moving', (e) => { target.uuid = uuidv4() if (target.parentDirection === 'left' || target.parentDirection === 'right') { const minX = target.minX const maxX = target.maxX if (target.left <= minX) { target.set({ left: minX, top: y }) } else if (target.left >= maxX) { target.set({ left: maxX, top: y }) } else { target.set({ top: y }) } } else if (target.parentDirection === 'top' || target.parentDirection === 'bottom') { const minY = target.minY const maxY = target.maxY if (target.top <= minY) { target.set({ left: x, top: minY }) } else if (target.top >= maxY) { target.set({ left: x, top: maxY }) } else { target.set({ left: x }) } } canvas?.renderAll() }) } }, removed: (e) => { const whiteList = ['mouseLine', 'guideLine'] if (whiteList.includes(e.target.name)) { } }, } const selectionEvent = { created: (e) => { const target = e.selected[0] setCurrentObject(target) const { selected } = e if (selected?.length > 0) { selected.forEach((obj) => { // if (obj.type === 'QPolygon' && currentMenu !== MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) { if (obj.type === 'QPolygon') { const originStroke = obj.stroke obj.set({ stroke: 'red' }) if (currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) { if (obj.name === POLYGON_TYPE.MODULE) { obj.set({ strokeWidth: 3 }) } if (obj.name === POLYGON_TYPE.ROOF) { canvas.discardActiveObject() obj.set({ stroke: originStroke }) } } } }) canvas.renderAll() } }, cleared: (e) => { setCurrentObject(null) const { deselected } = e if (deselected?.length > 0) { deselected.forEach((obj) => { if (obj.type === 'QPolygon') { if (obj.name !== 'moduleSetupSurface') { obj.set({ stroke: 'black' }) } if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) { obj.set({ strokeWidth: 0.3 }) } } }) } canvas.renderAll() }, updated: (e) => { const target = e.selected[0] setCurrentObject(target) const { selected, deselected } = e if (deselected?.length > 0) { deselected.forEach((obj) => { if (obj.type === 'QPolygon') { obj.set({ stroke: 'black' }) if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) { //모듈 미선택시 라인 두께 변경 obj.set({ strokeWidth: 0.3 }) } } }) } if (selected?.length > 0) { selected.forEach((obj) => { if (obj.type === 'QPolygon') { obj.set({ stroke: 'red' }) if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) { //모듈 선택시 라인 두께 변경 obj.set({ strokeWidth: 3 }) } } }) } canvas.renderAll() }, } const removeEventOnCanvas = () => { canvas?.off('object:added') canvas?.off('object:modified') canvas?.off('object:removed') canvas?.off('selection:cleared') canvas?.off('selection:created') canvas?.off('selection:updated') } /** * 각종 키보드 이벤트 * https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values */ const handleKeyDown = (e) => { const key = e.key switch (key) { case 'Delete': case 'Backspace': handleDelete() break case 'Down': // IE/Edge에서 사용되는 값 case 'ArrowDown': // "아래 화살표" 키가 눌렸을 때의 동작입니다. moveDown() break case 'Up': // IE/Edge에서 사용되는 값 case 'ArrowUp': // "위 화살표" 키가 눌렸을 때의 동작입니다. moveUp() break case 'Left': // IE/Edge에서 사용되는 값 case 'ArrowLeft': // "왼쪽 화살표" 키가 눌렸을 때의 동작입니다. moveLeft() break case 'Right': // IE/Edge에서 사용되는 값 case 'ArrowRight': // "오른쪽 화살표" 키가 눌렸을 때의 동작입니다. moveRight() break case 'Enter': // "enter" 또는 "return" 키가 눌렸을 때의 동작입니다. break case 'Esc': // IE/Edge에서 사용되는 값 case 'Escape': break case 'z': if (!e.ctrlKey) { return } console.log('뒤로가기') break default: return // 키 이벤트를 처리하지 않는다면 종료합니다. } e.preventDefault() } const moveDown = () => { const targetObj = canvas?.getActiveObject() if (!targetObj) { return } let top = targetObj.top + 10 if (top > canvasSize.vertical) { top = canvasSize.vertical } targetObj.set({ top: top }) targetObj.fire('modified') canvas?.renderAll() } const moveUp = () => { const targetObj = canvas?.getActiveObject() if (!targetObj) { return } let top = targetObj.top - 10 if (top < 0) { top = 0 } targetObj.set({ top: top }) targetObj.fire('modified') canvas?.renderAll() } const moveLeft = () => { const targetObj = canvas?.getActiveObject() if (!targetObj) { return } let left = targetObj.left - 10 if (left < 0) { left = 0 } targetObj.set({ left: left }) targetObj.fire('modified') canvas?.renderAll() } const moveRight = () => { const targetObj = canvas?.getActiveObject() if (!targetObj) { return } let left = targetObj.left + 10 if (left > canvasSize.horizontal) { left = canvasSize.horizontal } targetObj.set({ left: left }) targetObj.fire('modified') canvas?.renderAll() } /** * 선택한 도형을 삭제한다. */ const handleDelete = () => { const targets = canvas?.getActiveObjects() if (targets?.length === 0) { alert('삭제할 대상을 선택해주세요.') return } if (!confirm('정말로 삭제하시겠습니까?')) { return } targets?.forEach((target) => { canvas?.remove(target) }) } const handleZoom = (isZoom) => { let zoom if (isZoom) { zoom = canvasZoom + 10 if (zoom > 500) { return } } else { zoom = canvasZoom - 10 if (zoom < 10) { //50%->10% return } } setCanvasZoom(zoom) } const handleZoomClear = () => { setCanvasZoom(100) zoomToAllObjects() canvas.renderAll() } const zoomToAllObjects = () => { const objects = canvas.getObjects().filter((obj) => obj.visible) if (objects.length === 0) return let minX = Infinity, minY = Infinity let maxX = -Infinity, maxY = -Infinity objects.forEach((obj) => { const bounds = obj.getBoundingRect() minX = Math.min(minX, bounds.left) minY = Math.min(minY, bounds.top) maxX = Math.max(maxX, bounds.left + bounds.width) maxY = Math.max(maxY, bounds.top + bounds.height) }) const centerX = (minX + maxX) / 2 const centerY = (minY + maxY) / 2 const centerPoint = new fabric.Point(centerX, centerY) canvas.zoomToPoint(centerPoint, 1) canvas.renderAll() } return { setCanvasForEvent, attachDefaultEventOnCanvas, handleZoomClear, handleZoom, } }