diff --git a/src/components/floor-plan/modal/outerlinesetting/OuterLineWall.jsx b/src/components/floor-plan/modal/outerlinesetting/OuterLineWall.jsx index 74f492b1..386e98cd 100644 --- a/src/components/floor-plan/modal/outerlinesetting/OuterLineWall.jsx +++ b/src/components/floor-plan/modal/outerlinesetting/OuterLineWall.jsx @@ -5,7 +5,13 @@ import WithDraggable from '@/components/common/draggable/withDraggable' import { useRecoilState, useRecoilValue } from 'recoil' import { useMessage } from '@/hooks/useMessage' import { useEvent } from '@/hooks/useEvent' -import { canvasState, verticalHorizontalModeState } from '@/store/canvasAtom' +import { + adsorptionPointAddModeState, + adsorptionPointModeState, + canvasHistoryState, + canvasState, + verticalHorizontalModeState, +} from '@/store/canvasAtom' import { OUTER_LINE_TYPE, outerLineAngle1State, @@ -25,11 +31,18 @@ import { onlyNumberInputChange, onlyNumberWithDotInputChange } from '@/util/inpu export default function OuterLineWall(props) { const { setShowOutlineModal } = props const { getMessage } = useMessage() - const { addCanvasMouseEventListener, addDocumentEventListener, removeAllMouseEventListeners, removeAllDocumentEventListeners, removeMouseEvent } = - useEvent() + const { + addCanvasMouseEventListener, + addDocumentEventListener, + removeAllMouseEventListeners, + removeAllDocumentEventListeners, + removeMouseEvent, + getIntersectMousePoint, + } = useEvent() const { addLine, removeLine } = useLine() const { addPolygonByLines } = usePolygon() const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState) + const adsorptionPointAddMode = useRecoilValue(adsorptionPointAddModeState) const length1Ref = useRef(null) const length2Ref = useRef(null) @@ -50,13 +63,16 @@ export default function OuterLineWall(props) { const canvas = useRecoilValue(canvasState) useEffect(() => { + if (adsorptionPointAddMode) { + return + } removeMouseEvent('mouse:down', mouseDown) addCanvasMouseEventListener('mouse:down', mouseDown) clear() return () => { removeAllMouseEventListeners() } - }, [verticalHorizontalMode, points]) + }, [verticalHorizontalMode, points, adsorptionPointAddMode]) useEffect(() => { arrow1Ref.current = arrow1 @@ -83,7 +99,8 @@ export default function OuterLineWall(props) { } const mouseDown = (e) => { - const pointer = canvas.getPointer(e.e) + let pointer = getIntersectMousePoint(e) + if (points.length === 0) { setPoints((prev) => [...prev, pointer]) } else { @@ -328,6 +345,12 @@ export default function OuterLineWall(props) { if (points.length === 0) { return } + // 포커스가 length1에 있지 않으면 length1에 포커스를 줌 + const activeElem = document.activeElement + if (activeElem !== length1Ref.current) { + length1Ref.current.focus() + } + const key = e.key if (!length1Ref.current) { @@ -389,6 +412,9 @@ export default function OuterLineWall(props) { const key = e.key const activeElem = document.activeElement + if (activeElem !== length1Ref.current && activeElem !== length2Ref.current) { + length1Ref.current.focus() + } switch (key) { case 'Down': // IE/Edge에서 사용되는 값 diff --git a/src/components/floor-plan/modal/setting01/FirstOption.jsx b/src/components/floor-plan/modal/setting01/FirstOption.jsx index 3be1eb46..1c2cf20a 100644 --- a/src/components/floor-plan/modal/setting01/FirstOption.jsx +++ b/src/components/floor-plan/modal/setting01/FirstOption.jsx @@ -4,6 +4,7 @@ import { useMessage } from '@/hooks/useMessage' import React, { useEffect, useState } from 'react' import { useAxios } from '@/hooks/useAxios' import { toastUp } from '@/hooks/useToast' +import { adsorptionPointAddModeState } from '@/store/canvasAtom' export default function FirstOption() { const [objectNo, setObjectNo] = useState('test123240912001') // 이후 삭제 필요 diff --git a/src/components/floor-plan/modal/setting01/GridOption.jsx b/src/components/floor-plan/modal/setting01/GridOption.jsx index 6147f610..5e2f31db 100644 --- a/src/components/floor-plan/modal/setting01/GridOption.jsx +++ b/src/components/floor-plan/modal/setting01/GridOption.jsx @@ -2,10 +2,12 @@ import React from 'react' import { useRecoilState } from 'recoil' import { settingModalGridOptionsState } from '@/store/settingAtom' import { useMessage } from '@/hooks/useMessage' +import { adsorptionPointAddModeState } from '@/store/canvasAtom' export default function GridOption(props) { const { setShowDotLineGridModal } = props const [gridOptions, setGridOptions] = useRecoilState(settingModalGridOptionsState) + const [adsorptionPointAddMode, setAdsorptionPointAddMode] = useRecoilState(adsorptionPointAddModeState) const { getMessage } = useMessage() const onClickOption = (option) => { @@ -16,7 +18,12 @@ export default function GridOption(props) { // 점.선 그리드 setShowDotLineGridModal(true) } + + if (option.name === 'modal.canvas.setting.grid.absorption.add') { + setAdsorptionPointAddMode(!adsorptionPointAddMode) + } } + return ( <>
diff --git a/src/components/floor-plan/modal/setting01/SecondOption.jsx b/src/components/floor-plan/modal/setting01/SecondOption.jsx index 4904678d..e870cd73 100644 --- a/src/components/floor-plan/modal/setting01/SecondOption.jsx +++ b/src/components/floor-plan/modal/setting01/SecondOption.jsx @@ -1,14 +1,18 @@ -import { useRecoilState } from 'recoil' +import { useRecoilState, useSetRecoilState } from 'recoil' import { settingModalFirstOptionsState, settingModalSecondOptionsState } from '@/store/settingAtom' import { useMessage } from '@/hooks/useMessage' import React, { useEffect, useState } from 'react' import { useAxios } from '@/hooks/useAxios' import { toastUp } from '@/hooks/useToast' +import { adsorptionPointModeState, adsorptionRangeState } from '@/store/canvasAtom' export default function SecondOption() { const [objectNo, setObjectNo] = useState('test123240912001') // 이후 삭제 필요 const [settingModalFirstOptions, setSettingModalFirstOptions] = useRecoilState(settingModalFirstOptionsState) const [settingModalSecondOptions, setSettingModalSecondOptions] = useRecoilState(settingModalSecondOptionsState) + const [adsorptionPointMode, setAdsorptionPointMode] = useRecoilState(adsorptionPointModeState) + const setAdsorptionRange = useSetRecoilState(adsorptionRangeState) + const { option1, option2 } = settingModalFirstOptions const { option3, option4 } = settingModalSecondOptions const { getMessage } = useMessage() @@ -106,6 +110,7 @@ export default function SecondOption() { // HTTP POST 요청 보내기 await post({ url: `/api/canvas-management/canvas-settings`, data: patternData }).then((res) => { toastUp({ message: getMessage(res.returnMessage), type: 'success' }) + setAdsorptionRange(option.range) }) } catch (error) { toastUp({ message: getMessage(res.returnMessage), type: 'error' }) @@ -142,9 +147,14 @@ export default function SecondOption() { -
diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js index c0e74faf..052605b6 100644 --- a/src/hooks/useCanvas.js +++ b/src/hooks/useCanvas.js @@ -490,6 +490,34 @@ export function useCanvas(id) { canvas.clear() } + const getCurrentCanvas = () => { + return canvas.toJSON([ + 'selectable', + 'name', + 'parentId', + 'id', + 'length', + 'idx', + 'direction', + 'lines', + 'points', + 'lockMovementX', + 'lockMovementY', + 'lockRotation', + 'lockScalingX', + 'lockScalingY', + 'opacity', + 'cells', + 'maxX', + 'maxY', + 'minX', + 'minY', + 'x', + 'y', + 'stickeyPoint', + ]) + } + return { canvas, addShape, diff --git a/src/hooks/useEvent.js b/src/hooks/useEvent.js index c41b189f..34af6ca9 100644 --- a/src/hooks/useEvent.js +++ b/src/hooks/useEvent.js @@ -1,7 +1,15 @@ import { useEffect, useRef } from 'react' import { useRecoilState, useRecoilValue } from 'recoil' -import { canvasState, canvasZoomState, currentMenuState } from '@/store/canvasAtom' +import { + adsorptionPointAddModeState, + adsorptionPointModeState, + adsorptionRangeState, + canvasState, + canvasZoomState, + currentMenuState, +} from '@/store/canvasAtom' import { fabric } from 'fabric' +import { calculateIntersection, distanceBetweenPoints } from '@/util/canvas-util' export function useEvent() { const canvas = useRecoilValue(canvasState) @@ -9,6 +17,9 @@ export function useEvent() { const keyboardEventListeners = useRef([]) const mouseEventListeners = useRef([]) const [canvasZoom, setCanvasZoom] = useRecoilState(canvasZoomState) + const adsorptionPointAddMode = useRecoilValue(adsorptionPointAddModeState) + const adsorptionPointMode = useRecoilValue(adsorptionPointModeState) + const adsorptionRange = useRecoilValue(adsorptionRangeState) useEffect(() => { if (!canvas) { @@ -24,14 +35,42 @@ export function useEvent() { canvas?.on('mouse:wheel', wheelEvent) addDefaultEvent() - }, [currentMenu, canvas]) + }, [currentMenu, canvas, adsorptionPointAddMode, adsorptionPointMode, adsorptionRange]) const addDefaultEvent = () => { //default Event 추가 addCanvasMouseEventListener('mouse:move', defaultMouseMoveEvent) addCanvasMouseEventListener('mouse:out', defaultMouseOutEvent) - + if (adsorptionPointAddMode) { + addCanvasMouseEventListener('mouse:down', adsorptionPointAddModeStateEvent) + } addDocumentEventListener('keydown', document, defaultKeyboardEvent) + addDocumentEventListener('contextmenu', document, defaultContextMenuEvent) + } + + const defaultContextMenuEvent = (e) => { + e.preventDefault() + e.stopPropagation() + } + + const adsorptionPointAddModeStateEvent = (opt) => { + //흡착점 모드일 경우 + let pointer = getIntersectMousePoint(opt) + + const adsorptionPoint = new fabric.Circle({ + radius: 3, + fill: 'red', + left: pointer.x - 3, + top: pointer.y - 3, + x: pointer.x, + y: pointer.y, + selectable: false, + name: 'adsorptionPoint', + }) + + canvas.add(adsorptionPoint) + + canvas.renderAll() } const wheelEvent = (opt) => { @@ -62,7 +101,28 @@ export function useEvent() { removeMouseLine() // 가로선 const pointer = canvas.getPointer(e.e) - const horizontalLine = new fabric.Line([-1 * canvas.width, pointer.y, 2 * canvas.width, pointer.y], { + + const adsorptionPoints = getAdsorptionPoints() + + let arrivalPoint = { x: pointer.x, y: pointer.y } + + if (adsorptionPointMode) { + // pointer와 adsorptionPoints의 거리를 계산하여 가장 가까운 점을 찾는다. + let minDistance = adsorptionRange + let adsorptionPoint = null + adsorptionPoints.forEach((point) => { + const distance = distanceBetweenPoints(pointer, point) + if (distance < minDistance) { + minDistance = distance + adsorptionPoint = point + } + }) + if (adsorptionPoint) { + arrivalPoint = { ...adsorptionPoint } + } + } + + const horizontalLine = new fabric.Line([-1 * canvas.width, arrivalPoint.y, 2 * canvas.width, arrivalPoint.y], { stroke: 'red', strokeWidth: 1, selectable: false, @@ -70,7 +130,7 @@ export function useEvent() { }) // 세로선 - const verticalLine = new fabric.Line([pointer.x, -1 * canvas.height, pointer.x, 2 * canvas.height], { + const verticalLine = new fabric.Line([arrivalPoint.x, -1 * canvas.height, arrivalPoint.x, 2 * canvas.height], { stroke: 'red', strokeWidth: 1, selectable: false, @@ -143,11 +203,28 @@ export function useEvent() { }) } + const getAdsorptionPoints = () => { + return canvas.getObjects().filter((obj) => obj.visible && obj.name === 'adsorptionPoint') + } + + //가로선, 세로선의 교차점을 return + const getIntersectMousePoint = (e) => { + let pointer = canvas.getPointer(e.e) + const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine') + + if (mouseLines.length < 2) { + return pointer + } + + return calculateIntersection(mouseLines[0], mouseLines[1]) ?? pointer + } + return { addDocumentEventListener, addCanvasMouseEventListener, removeAllMouseEventListeners, removeAllDocumentEventListeners, removeMouseEvent, + getIntersectMousePoint, } } diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index 2d9ef2dd..7f942f59 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -1,4 +1,4 @@ -import { atom } from 'recoil' +import { atom, selector } from 'recoil' import { MENU } from '@/common/common' export const canvasState = atom({ @@ -201,3 +201,20 @@ export const verticalHorizontalModeState = atom({ key: 'verticalHorizontalMode', default: true, }) + +// 흡착점 모드 +export const adsorptionPointModeState = atom({ + key: 'adsorptionPointModeState', + default: false, +}) +// 흡착점 추가모드 +export const adsorptionPointAddModeState = atom({ + key: 'adsorptionPointAddModeState', + default: false, +}) + +// 흡착점 범위 +export const adsorptionRangeState = atom({ + key: 'adsorptionRangeState', + default: 50, +}) diff --git a/src/store/settingAtom.js b/src/store/settingAtom.js index c00cc667..93cec544 100644 --- a/src/store/settingAtom.js +++ b/src/store/settingAtom.js @@ -40,10 +40,10 @@ export const settingModalSecondOptionsState = atom({ { id: 4, name: 'modal.canvas.setting.font.plan.edit.circuit.num' }, ], option4: [ - { id: 1, column: 'adsorpRangeSmall', name: 'modal.canvas.setting.font.plan.absorption.small', selected: true }, - { id: 2, column: 'adsorpRangeSmallSemi', name: 'modal.canvas.setting.font.plan.absorption.small.semi', selected: false }, - { id: 3, column: 'adsorpRangeMedium', name: 'modal.canvas.setting.font.plan.absorption.medium', selected: false }, - { id: 4, column: 'adsorpRangeLarge', name: 'modal.canvas.setting.font.plan.absorption.large', selected: false }, + { id: 1, column: 'adsorpRangeSmall', name: 'modal.canvas.setting.font.plan.absorption.small', selected: true, range: 10 }, + { id: 2, column: 'adsorpRangeSmallSemi', name: 'modal.canvas.setting.font.plan.absorption.small.semi', selected: false, range: 30 }, + { id: 3, column: 'adsorpRangeMedium', name: 'modal.canvas.setting.font.plan.absorption.medium', selected: false, range: 50 }, + { id: 4, column: 'adsorpRangeLarge', name: 'modal.canvas.setting.font.plan.absorption.large', selected: false, range: 80 }, ], }, dangerouslyAllowMutability: true,