diff --git a/src/components/SettingsModal.jsx b/src/components/SettingsModal.jsx index 3cf9997b..dc7dc5ba 100644 --- a/src/components/SettingsModal.jsx +++ b/src/components/SettingsModal.jsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input } from '@nextui-org/react' import { useRecoilState, useRecoilValue } from 'recoil' import { modalContent, modalState } from '@/store/modalAtom' -import { guideLineState } from '@/store/canvasAtom' +import { guideLineState, horiGuideLinesState, vertGuideLinesState } from '@/store/canvasAtom' import { fabric } from 'fabric' export default function SettingsModal(props) { @@ -16,6 +16,8 @@ export default function SettingsModal(props) { const [open, setOpen] = useRecoilState(modalState) const [guideLine, setGuideLine] = useRecoilState(guideLineState) + const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState) + const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState) const gridSettingArray = [] @@ -63,6 +65,7 @@ export default function SettingsModal(props) { lockScalingX: true, lockScalingY: true, name: 'guideLine', + direction: 'horizontal', }, ) canvasProps.add(horizontalLine) @@ -82,6 +85,7 @@ export default function SettingsModal(props) { lockScalingX: true, lockScalingY: true, name: 'guideLine', + direction: 'vertical', }, ) canvasProps.add(verticalLine) @@ -99,6 +103,16 @@ export default function SettingsModal(props) { moduleHoriLength: moduleHoriLength, } gridSettingArray.push(recoilObj) + const newHoriGuideLines = [...horiGuideLines] + horizontalLineArray.forEach((line) => { + newHoriGuideLines.push(line) + }) + const newVertGuideLines = [...vertGuideLines] + verticalLineArray.forEach((line) => { + newVertGuideLines.push(line) + }) + setHoriGuideLines(newHoriGuideLines) + setVertGuideLines(newVertGuideLines) } if (gridCheckedValue.includes('dot')) { @@ -170,6 +184,8 @@ export default function SettingsModal(props) { guideLines?.forEach((item) => canvasProps.remove(item)) canvasProps.renderAll() setGuideLine([]) + setHoriGuideLines([]) + setVertGuideLines([]) } else { alert('그리드가 없습니다.') return diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index a481fec9..4d6dc238 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -25,6 +25,8 @@ import { templateTypeState, wallState, guideLineState, + horiGuideLinesState, + vertGuideLinesState, } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' import { fabric } from 'fabric' @@ -69,8 +71,8 @@ export function useMode() { const [guideLineMode, setGuideLineMode] = useState(false) const [guideDotMode, setGuideDotMode] = useState(false) - const [horiGuideLines, setHoriGuideLines] = useState([]) - const [vertGuideLines, setVertGuideLines] = useState([]) + const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState) + const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState) useEffect(() => { // if (!canvas) { @@ -99,10 +101,10 @@ export function useMode() { }, [endPoint]) useEffect(() => { + changeMode(canvas, mode) canvas?.off('mouse:move') canvas?.on('mouse:move', drawMouseLines) - changeMode(canvas, mode) - }, [mode]) + }, [mode, horiGuideLines, vertGuideLines]) useEffect(() => { setGuideLineMode(false) @@ -111,9 +113,6 @@ export function useMode() { const guideLineState = guideLineInfo.filter((item) => item.guideMode === 'guideLine') const guideDotState = guideLineInfo.filter((item) => item.guideMode === 'guideDot') - setHoriGuideLines(guideLineState[0].horizontalLineArray) - setVertGuideLines(guideLineState[0].verticalLineArray) - setGuideLineMode(guideLineState.length > 0) setGuideDotMode(guideDotState.length > 0) } @@ -160,12 +159,8 @@ export function useMode() { if (mode === Mode.EDIT || mode === Mode.ADSORPTION_POINT) { let adsorptionPoint = adsorptionPointList.length > 0 ? findClosestPoint(pointer, adsorptionPointList) : null - if (isGuideLineMode && isGuideDotMode) { - const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray) - const closetVerticalLine = getClosestVerticalLine(pointer, verticalLineArray) - const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) - const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) - + if ((horiGuideLines.length > 0 || vertGuideLines.length > 0) && guideDotMode) { + } else if (guideDotMode) { const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) @@ -177,35 +172,28 @@ export function useMode() { if (isAttachX && isAttachY) { newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 - } else { - if (Math.min(xDiff, yDiff) <= 20) { - if (xDiff < yDiff) { - newX = closetVerticalLine.x1 - newY = pointer.y - } else { - newX = pointer.x - newY = closestHorizontalLine.y1 - } + } + } else if (horiGuideLines.length > 0 || vertGuideLines.length > 0) { + const closestHorizontalLine = getClosestHorizontalLine(pointer, horiGuideLines) + const closetVerticalLine = getClosestVerticalLine(pointer, vertGuideLines) + let intersection = null + let intersectionDistance = Infinity + + if (closestHorizontalLine && closetVerticalLine) { + intersection = calculateIntersection(closestHorizontalLine, closetVerticalLine) + if (intersection) { + intersectionDistance = distanceBetweenPoints(pointer, intersection) } } - } else if (isGuideDotMode) { - const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) - const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) - const xRate = x / guideLineLengthHori - const yRate = y / guideLineLengthVert - const isAttachX = xRate >= 0.4 && xRate <= 0.7 - const isAttachY = yRate >= 0.4 && yRate <= 0.7 + let xDiff, yDiff - if (isAttachX && isAttachY) { - newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 - newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 + if (closetVerticalLine) { + xDiff = Math.abs(pointer.x - closetVerticalLine.x1) + } + if (closestHorizontalLine) { + yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) } - } else if (isGuideLineMode) { - const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray) - const closetVerticalLine = getClosestVerticalLine(pointer, verticalLineArray) - const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) - const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert) @@ -214,23 +202,26 @@ export function useMode() { const yRate = y / guideLineLengthVert const isAttachX = xRate >= 0.4 && xRate <= 0.7 const isAttachY = yRate >= 0.4 && yRate <= 0.7 - if (isAttachX && isAttachY) { newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2 } else { - if (Math.min(xDiff, yDiff) <= 20) { - if (xDiff < yDiff) { - newX = closetVerticalLine.x1 - newY = pointer.y - } else { - newX = pointer.x - newY = closestHorizontalLine.y1 + if (intersection && intersectionDistance < 20) { + newX = intersection.x + newY = intersection.y + } else { + if (Math.min(xDiff, yDiff) <= 20) { + if (xDiff < yDiff) { + newX = closetVerticalLine.x1 + newY = pointer.y + } else { + newX = pointer.x + newY = closestHorizontalLine.y1 + } } } } } - if (adsorptionPoint && distanceBetweenPoints(pointer, adsorptionPoint) < 20) { newX = adsorptionPoint.left newY = adsorptionPoint.top @@ -358,8 +349,6 @@ export function useMode() { // 모드에 따른 마우스 이벤트 변경 const changeMouseEvent = (mode) => { - document.removeEventListener('contextmenu', mouseEvent.drawLineModeRightClick) - switch (mode) { case 'drawLine': canvas?.on('mouse:down', mouseEvent.drawLineModeLeftClick) @@ -367,7 +356,6 @@ export function useMode() { break case 'edit': canvas?.on('mouse:down', mouseEvent.editMode) - break case 'textbox': canvas?.on('mouse:down', mouseEvent.textboxMode) @@ -546,11 +534,14 @@ export function useMode() { } const mouseAndkeyboardEventClear = () => { + canvas?.off('mouse:down') Object.keys(mouseEvent).forEach((key) => { canvas?.off('mouse:down', mouseEvent[key]) + document.removeEventListener('contextmenu', mouseEvent[key]) }) + Object.keys(keyboardEvent).forEach((key) => { - window.removeEventListener('keydown', keyboardEvent[key]) + document.removeEventListener('keydown', keyboardEvent[key]) }) } @@ -558,6 +549,7 @@ export function useMode() { // rerendering을 막기 위해 useCallback 사용 editMode: useCallback( (e) => { + e.preventDefault() switch (e.key) { case 'ArrowDown': { if (!keyValid()) { @@ -627,11 +619,12 @@ export function useMode() { } const changeMode = (canvas, mode) => { + mouseAndkeyboardEventClear() setMode(mode) setCanvas(canvas) // mode별 이벤트 변경 - mouseAndkeyboardEventClear() + changeMouseEvent(mode) changeKeyboardEvent(mode) @@ -670,42 +663,49 @@ export function useMode() { if (mode === Mode.EDIT) { switch (mode) { case 'edit': - window.addEventListener('keydown', keyboardEvent.editMode) + document.addEventListener('keydown', keyboardEvent.editMode) break } } } const mouseEvent = { - drawLineModeLeftClick: useCallback( - (options) => { - const pointer = canvas?.getPointer(options.e) + drawLineModeLeftClick: (options) => { + if (mode !== Mode.DRAW_LINE) { + return + } + const pointer = canvas?.getPointer(options.e) - const line = new QLine( - [pointer.x, 0, pointer.x, canvas.height], // y축에 1자 선을 그립니다. - { - stroke: 'gray', - strokeWidth: 1, - selectable: true, - lockMovementX: true, - lockMovementY: true, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - name: 'guideLine', - }, - ) + const line = new QLine( + [pointer.x, 0, pointer.x, canvasSize.vertical], // y축에 1자 선을 그립니다. + { + stroke: 'gray', + strokeWidth: 1, + selectable: true, + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + name: 'guideLine', + direction: 'vertical', + }, + ) - canvas?.add(line) - canvas?.renderAll() + canvas?.add(line) + canvas?.renderAll() - const newVerticalLineArray = [...vertGuideLines, line] - setVertGuideLines(newVerticalLineArray) - }, - [canvas, vertGuideLines], - ), + const newVerticalLineArray = [...vertGuideLines] + newVerticalLineArray.push(line) + + setVertGuideLines(newVerticalLineArray) + }, drawLineModeRightClick: useCallback( (options) => { + document.removeEventListener('contextmenu', mouseEvent.drawLineModeRightClick) + if (mode !== Mode.DRAW_LINE) { + return + } const line = new fabric.Line( [0, options.offsetY, canvasSize.horizontal, options.offsetY], // y축에 1자 선을 그립니다. { @@ -718,132 +718,136 @@ export function useMode() { lockScalingX: true, lockScalingY: true, name: 'guideLine', + direction: 'horizontal', }, ) canvas?.add(line) canvas?.renderAll() - const newHorizontalLineArray = [...horiGuideLines, line] + const newHorizontalLineArray = [...horiGuideLines] + newHorizontalLineArray.push(line) setHoriGuideLines(newHorizontalLineArray) }, - [canvas, horiGuideLines], + [canvas, mode, horiGuideLines], ), - editMode: useCallback( - (options) => { - let pointer = canvas?.getPointer(options.e) + editMode: (options) => { + if (mode !== Mode.EDIT) { + return + } + let pointer = canvas?.getPointer(options.e) - if (getInterSectPointByMouseLine()) { - pointer = getInterSectPointByMouseLine() - } + if (getInterSectPointByMouseLine()) { + pointer = getInterSectPointByMouseLine() + } - const circle = new fabric.Circle({ - radius: 5, - fill: 'transparent', // 원 안을 비웁니다. - stroke: 'red', // 원 테두리 색상을 검은색으로 설정합니다. - left: pointer.x, - top: pointer.y, - x: pointer.x, - y: pointer.y, - originX: 'center', - originY: 'center', - selectable: false, - }) - if (!startPoint.current) { - startPoint.current = circle - pointCount.current = pointCount.current + 1 - } - let prevEndPoint + const circle = new fabric.Circle({ + radius: 5, + fill: 'transparent', // 원 안을 비웁니다. + stroke: 'red', // 원 테두리 색상을 검은색으로 설정합니다. + left: pointer.x, + top: pointer.y, + x: pointer.x, + y: pointer.y, + originX: 'center', + originY: 'center', + selectable: false, + }) + if (!startPoint.current) { + startPoint.current = circle + pointCount.current = pointCount.current + 1 + } + let prevEndPoint - setEndPoint((prev) => { - prevEndPoint = prev - return circle - }) + setEndPoint((prev) => { + prevEndPoint = prev + return circle + }) - historyPoints.current.push(circle) - points.current.push(circle) - canvas?.add(circle) + historyPoints.current.push(circle) + points.current.push(circle) + canvas?.add(circle) - if (points.current.length === 2) { - if (guideLineMode || guideDotMode) { - const vector = { - x: points.current[1].left - points.current[0].left, - y: points.current[1].top - points.current[0].top, - } - const slope = Math.abs(vector.y / vector.x) // 기울기 계산 - - let scaledVector - - if (slope >= 1) { - // 기울기가 1 이상이면 x축 방향으로 그림 - scaledVector = { - x: 0, - y: pointer.y - prevEndPoint?.top, - } - } else { - // 기울기가 1 미만이면 y축 방향으로 그림 - scaledVector = { - x: pointer.x - prevEndPoint?.left, - y: 0, - } - } - - const verticalLength = scaledVector.y - const horizontalLength = scaledVector.x - - drawCircleAndLine(verticalLength, horizontalLength) - canvas?.renderAll() - return + if (points.current.length === 2) { + if (guideLineMode || guideDotMode) { + const vector = { + x: points.current[1].left - points.current[0].left, + y: points.current[1].top - points.current[0].top, } - const length = Number(prompt('길이를 입력하세요:')) + const slope = Math.abs(vector.y / vector.x) // 기울기 계산 - // length 값이 숫자가 아닌 경우 - if (isNaN(length) || length === 0) { - //마지막 추가 된 points 제거합니다. + let scaledVector - const lastPoint = historyPoints.current[historyPoints.current.length - 1] - - canvas?.remove(lastPoint) - setEndPoint(prevEndPoint) - historyPoints.current.pop() - points.current.pop() - return + if (slope >= 1) { + // 기울기가 1 이상이면 x축 방향으로 그림 + scaledVector = { + x: 0, + y: pointer.y - prevEndPoint?.top, + } + } else { + // 기울기가 1 미만이면 y축 방향으로 그림 + scaledVector = { + x: pointer.x - prevEndPoint?.left, + y: 0, + } } - if (length) { - const vector = { - x: points.current[1].left - points.current[0].left, - y: points.current[1].top - points.current[0].top, - } - const slope = Math.abs(vector.y / vector.x) // 기울기 계산 + const verticalLength = scaledVector.y + const horizontalLength = scaledVector.x - let scaledVector - if (slope >= 1) { - // 기울기가 1 이상이면 x축 방향으로 그림 - scaledVector = { - x: 0, - y: vector.y >= 0 ? Number(length) : -Number(length), - } - } else { - // 기울기가 1 미만이면 y축 방향으로 그림 - scaledVector = { - x: vector.x >= 0 ? Number(length) : -Number(length), - y: 0, - } - } + drawCircleAndLine(verticalLength, horizontalLength) + canvas?.renderAll() + return + } + const length = Number(prompt('길이를 입력하세요:')) - const verticalLength = scaledVector.y - const horizontalLength = scaledVector.x + // length 값이 숫자가 아닌 경우 + if (isNaN(length) || length === 0) { + //마지막 추가 된 points 제거합니다. - drawCircleAndLine(verticalLength, horizontalLength) - } + const lastPoint = historyPoints.current[historyPoints.current.length - 1] + + canvas?.remove(lastPoint) + setEndPoint(prevEndPoint) + historyPoints.current.pop() + points.current.pop() + return } - canvas?.renderAll() - }, - [canvas], - ), - textboxMode: useCallback((options) => { + if (length) { + const vector = { + x: points.current[1].left - points.current[0].left, + y: points.current[1].top - points.current[0].top, + } + const slope = Math.abs(vector.y / vector.x) // 기울기 계산 + + let scaledVector + if (slope >= 1) { + // 기울기가 1 이상이면 x축 방향으로 그림 + scaledVector = { + x: 0, + y: vector.y >= 0 ? Number(length) : -Number(length), + } + } else { + // 기울기가 1 미만이면 y축 방향으로 그림 + scaledVector = { + x: vector.x >= 0 ? Number(length) : -Number(length), + y: 0, + } + } + + const verticalLength = scaledVector.y + const horizontalLength = scaledVector.x + + drawCircleAndLine(verticalLength, horizontalLength) + } + } + + canvas?.renderAll() + }, + + textboxMode: (options) => { + if (mode !== Mode.TEXTBOX) return if (canvas?.getActiveObject()?.type === 'textbox') return const pointer = canvas?.getPointer(options.e) @@ -861,8 +865,9 @@ export function useMode() { textbox?.on('editing:exited', function () { changeMode(canvas, Mode.EDIT) }) - }, []), - drawRectMode: useCallback((o) => { + }, + drawRectMode: (o) => { + if (mode !== Mode.DRAW_RECT) return let rect, isDown, origX, origY isDown = true const pointer = canvas.getPointer(o.e) @@ -902,39 +907,37 @@ export function useMode() { canvas.off('mouse:up') setMode(Mode.DEFAULT) }) - }, []), + }, // 흡착점 추가 - adsorptionPoint: useCallback( - (o) => { - const pointer = canvas.getPointer(o.e) - let newX = pointer.x - let newY = pointer.y + adsorptionPoint: (o) => { + if (mode !== Mode.ADSORPTION_POINT) return + const pointer = canvas.getPointer(o.e) + let newX = pointer.x + let newY = pointer.y - if (getInterSectPointByMouseLine()) { - const interSectPoint = getInterSectPointByMouseLine() - newX = interSectPoint.x - newY = interSectPoint.y - } + if (getInterSectPointByMouseLine()) { + const interSectPoint = getInterSectPointByMouseLine() + newX = interSectPoint.x + newY = interSectPoint.y + } - const circle = new fabric.Circle({ - radius: 5, - fill: 'transparent', // 원 안을 비웁니다. - stroke: 'black', // 원 테두리 색상을 검은색으로 설정합니다. - left: newX, - top: newY, - originX: 'center', - originY: 'center', - x: newX - 5, - y: newY - 5, - selectable: false, - name: 'adsorptionPoint', - }) + const circle = new fabric.Circle({ + radius: 5, + fill: 'transparent', // 원 안을 비웁니다. + stroke: 'black', // 원 테두리 색상을 검은색으로 설정합니다. + left: newX, + top: newY, + originX: 'center', + originY: 'center', + x: newX - 5, + y: newY - 5, + selectable: false, + name: 'adsorptionPoint', + }) - canvas.add(circle) - canvas.renderAll() - }, - [canvas], - ), + canvas.add(circle) + canvas.renderAll() + }, } const getInterSectPointByMouseLine = () => { diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index c2b245d7..8d16b98f 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -95,3 +95,15 @@ export const currentObjectState = atom({ default: null, dangerouslyAllowMutability: true, }) + +export const horiGuideLinesState = atom({ + key: 'horiGuideLines', + default: [], + dangerouslyAllowMutability: true, +}) + +export const vertGuideLinesState = atom({ + key: 'vertGuideLines', + default: [], + dangerouslyAllowMutability: true, +})