541 lines
18 KiB
JavaScript
541 lines
18 KiB
JavaScript
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,
|
|
}
|
|
}
|