import { useRef, useState } from 'react' import QLine from '@/components/fabric/QLine' import QRect from '@/components/fabric/QRect' import QPolygon from '@/components/fabric/QPolygon' import { getStartIndex, rearrangeArray } from '@/util/canvas-util' import { useRecoilState } from 'recoil' import { fontSizeState } from '@/store/canvasAtom' export const Mode = { DRAW_LINE: 'drawLine', // 기준선 긋기모드 EDIT: 'edit', TEMPLATE: 'template', TEXTBOX: 'textbox', DRAW_RECT: 'drawRect', DEFAULT: 'default', } export function useMode() { const [mode, setMode] = useState() const points = useRef([]) const historyPoints = useRef([]) const historyLines = useRef([]) const [canvas, setCanvas] = useState(null) const [zoom, setZoom] = useState(100) const [fontSize] = useRecoilState(fontSizeState) const addEvent = (mode) => { switch (mode) { case 'default': canvas?.off('mouse:down') break case 'drawLine': drawLineMode() break case 'edit': editMode() break case 'template': templateMode() break case 'textbox': textboxMode() break case 'drawRect': drawRectMode() break } } const changeMode = (canvas, mode) => { setMode(mode) // mode변경 시 이전 이벤트 제거 setCanvas(canvas) canvas?.off('mouse:down') addEvent(mode) } const editMode = () => { let distanceText = null // 거리를 표시하는 텍스트 객체를 저장할 변수 canvas?.on('mouse:move', function (options) { const pointer = canvas?.getPointer(options.e) if (historyLines.current.length === 0) return const direction = getDirection(historyLines.current[0], pointer) // 각 선과 마우스 위치 사이의 거리를 계산합니다. const dx = historyLines.current[0].x1 - pointer.x const dy = 0 const minDistance = Math.sqrt(dx * dx + dy * dy) // 거리를 표시하는 텍스트 객체를 생성하거나 업데이트합니다. if (distanceText) { distanceText.set({ left: pointer.x, top: pointer.y, text: `${minDistance.toFixed(2)}`, }) } else { distanceText = new fabric.Text(`${minDistance.toFixed(2)}`, { left: pointer.x, top: pointer.y, fontSize: fontSize, }) canvas?.add(distanceText) } // 캔버스를 다시 그립니다. canvas?.renderAll() }) canvas?.on('mouse:down', function (options) { const pointer = canvas?.getPointer(options.e) const circle = new fabric.Circle({ radius: 1, fill: 'transparent', // 원 안을 비웁니다. stroke: 'black', // 원 테두리 색상을 검은색으로 설정합니다. left: pointer.x, top: pointer.y, originX: 'center', originY: 'center', selectable: false, }) historyPoints.current.push(circle) points.current.push(circle) canvas?.add(circle) if (points.current.length === 2) { const length = Number(prompt('길이를 입력하세요:')) // length 값이 숫자가 아닌 경우 if (isNaN(length) || length === 0) { //마지막 추가 된 points 제거합니다. const lastPoint = historyPoints.current[historyPoints.current.length - 1] canvas?.remove(lastPoint) historyPoints.current.pop() points.current.pop() return } 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 line = new QLine( [ points.current[0].left, points.current[0].top, points.current[0].left + scaledVector.x, points.current[0].top + scaledVector.y, ], { stroke: 'black', strokeWidth: 2, selectable: false, viewLengthText: true, direction: getDirection(points.current[0], points.current[1]), fontSize: fontSize, }, ) historyLines.current.push(line) // 라인의 끝에 점을 추가합니다. const endPointCircle = new fabric.Circle({ radius: 1, fill: 'transparent', // 원 안을 비웁니다. stroke: 'black', // 원 테두리 색상을 검은색으로 설정합니다. left: points.current[0].left + scaledVector.x, top: points.current[0].top + scaledVector.y, originX: 'center', originY: 'center', selectable: false, }) canvas?.add(line) canvas?.add(endPointCircle) historyPoints.current.push(endPointCircle) points.current.forEach((point) => { canvas?.remove(point) }) points.current = [endPointCircle] } } canvas?.renderAll() }) } const templateMode = () => { changeMode(canvas, Mode.EDIT) if (historyPoints.current.length >= 4) { const firstPoint = historyPoints.current[0] const lastPoint = historyPoints.current[historyPoints.current.length - 1] historyPoints.current.forEach((point) => { canvas?.remove(point) }) drawLineWithLength(lastPoint, firstPoint) points.current = [] historyPoints.current = [] handleOuterlines() makePolygon() } } const textboxMode = () => { canvas?.on('mouse:down', function (options) { if (canvas?.getActiveObject()?.type === 'textbox') return const pointer = canvas?.getPointer(options.e) const textbox = new fabric.Textbox('텍스트를 입력하세요', { left: pointer.x, top: pointer.y, width: 150, // 텍스트박스의 너비를 설정합니다. fontSize: fontSize, // 텍스트의 크기를 설정합니다. }) canvas?.add(textbox) canvas?.setActiveObject(textbox) // 생성된 텍스트박스를 활성 객체로 설정합니다. canvas?.renderAll() // textbox가 active가 풀린 경우 editing mode로 변경 textbox?.on('editing:exited', function () { changeMode(canvas, Mode.EDIT) }) }) } const drawLineMode = () => { canvas?.on('mouse:down', function (options) { const pointer = canvas?.getPointer(options.e) const line = new QLine( [pointer.x, 0, pointer.x, canvas.height], // y축에 1자 선을 그립니다. { stroke: 'black', strokeWidth: 2, viewLengthText: true, selectable: false, fontSize: fontSize, }, ) canvas?.add(line) canvas?.renderAll() }) } const drawRectMode = () => { let rect, isDown, origX, origY canvas.on('mouse:down', function (o) { isDown = true const pointer = canvas.getPointer(o.e) origX = pointer.x origY = pointer.y rect = new fabric.Rect({ left: origX, top: origY, originX: 'left', originY: 'top', width: pointer.x - origX, height: pointer.y - origY, angle: 0, fill: 'transparent', stroke: 'black', transparentCorners: false, }) canvas.add(rect) }) canvas.on('mouse:move', function (o) { if (!isDown) return const pointer = canvas.getPointer(o.e) if (origX > pointer.x) { rect.set({ left: Math.abs(pointer.x) }) } if (origY > pointer.y) { rect.set({ top: Math.abs(pointer.y) }) } rect.set({ width: Math.abs(origX - pointer.x) }) rect.set({ height: Math.abs(origY - pointer.y) }) }) canvas.on('mouse:up', function (o) { const pointer = canvas.getPointer(o.e) const qRect = new QRect({ left: origX, top: origY, originX: 'left', originY: 'top', width: pointer.x - origX, height: pointer.y - origY, angle: 0, viewLengthText: true, fill: 'transparent', stroke: 'black', transparentCorners: false, fontSize: fontSize, }) canvas.remove(rect) canvas.add(qRect) isDown = false }) } /** * 두 점 사이의 방향을 반환합니다. */ const getDirection = (a, b) => { const vector = { x: b.left - a.left, y: b.top - a.top, } if (Math.abs(vector.x) > Math.abs(vector.y)) { // x축 방향으로 더 많이 이동 return vector.x > 0 ? 'right' : 'left' } else { // y축 방향으로 더 많이 이동 return vector.y > 0 ? 'bottom' : 'top' } } /** * 두 점을 연결하는 선과 길이를 그립니다. * a : 시작점, b : 끝점 */ const drawLineWithLength = (a, b) => { const vector = { x: b.left - a.left, y: b.top - a.top, } const line = new QLine([a.left, a.top, b.left, b.top], { stroke: 'black', strokeWidth: 2, selectable: false, viewLengthText: true, direction: getDirection(a, b), fontSize: fontSize, }) historyLines.current.push(line) canvas?.add(line) canvas?.renderAll() } const makePolygon = (otherLines) => { // 캔버스에서 모든 라인 객체를 찾습니다. const lines = otherLines || historyLines.current if (!otherLines) { const sortedIndex = getStartIndex(lines) const tmpArraySorted = rearrangeArray(lines, sortedIndex) function findTopTwoIndexesByDistance(objArr) { if (objArr.length < 2) { return [] // 배열의 길이가 2보다 작으면 빈 배열 반환 } let firstIndex = -1 let secondIndex = -1 let firstDistance = -Infinity let secondDistance = -Infinity for (let i = 0; i < objArr.length; i++) { const distance = objArr[i].length if (distance > firstDistance) { secondDistance = firstDistance secondIndex = firstIndex firstDistance = distance firstIndex = i } else if (distance > secondDistance) { secondDistance = distance secondIndex = i } } return [firstIndex, secondIndex] } const topIndex = findTopTwoIndexesByDistance(tmpArraySorted) let shape = 0 if (topIndex[0] === 2) { if (topIndex[1] === 3) shape = 1 } else if (topIndex === 1) { if (topIndex[1] === 2) shape = 4 } historyLines.current = [] } // 각 라인의 시작점과 끝점을 사용하여 다각형의 점 배열을 생성합니다. const points = lines.map((line) => ({ x: line.x1, y: line.y1 })) // 모든 라인 객체를 캔버스에서 제거합니다. lines.forEach((line) => { canvas?.remove(line) }) // 점 배열을 사용하여 새로운 다각형 객체를 생성합니다. const polygon = new QPolygon(points, { stroke: 'black', fill: 'transparent', viewLengthText: true, selectable: true, fontSize: fontSize, }) // 새로운 다각형 객체를 캔버스에 추가합니다. canvas.add(polygon) // 캔버스를 다시 그립니다. if (!otherLines) { polygon.fillCell() canvas.renderAll() polygon.setViewLengthText(false) } } /** * 해당 캔버스를 비운다. */ const handleClear = () => { canvas?.clear() points.current = [] historyPoints.current = [] historyLines.current = [] } const zoomIn = () => { canvas?.setZoom(canvas.getZoom() + 0.1) setZoom(Math.round(zoom + 10)) } const zoomOut = () => { canvas?.setZoom(canvas.getZoom() - 0.1) setZoom(Math.ceil(zoom - 10)) } const handleOuterlines = () => { const newOuterlines = [] for (let i = 0; i < historyLines.current.length; i++) { const next = historyLines.current[i + 1] const prev = historyLines.current[i - 1] ?? historyLines.current[historyLines.current.length - 1] if (next) { if (next.direction === 'right') { // 다름 라인이 오른쪽으로 이동 if (historyLines.current[i].direction === 'top') { if (prev.direction !== 'right') { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 + 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 + 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 + 50, }) } } else { // bottom if (prev?.direction !== 'right') { newOuterlines.push({ x1: historyLines.current[i].x1 - 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 - 50, y2: historyLines.current[i].y2 + 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 - 50, y1: historyLines.current[i].y1 + 50, x2: historyLines.current[i].x2 - 50, y2: historyLines.current[i].y2 + 50, }) } } } else if (next.direction === 'left') { if (historyLines.current[i].direction === 'top') { if (prev?.direction !== 'left') { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 + 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 - 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 - 50, }) } } else { // bottom if (prev?.direction !== 'left') { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 + 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 - 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 - 50, y2: historyLines.current[i].y2 - 50, }) } } } else if (next.direction === 'top') { if (historyLines.current[i].direction === 'right') { if (prev?.direction !== 'top') { newOuterlines.push({ x1: historyLines.current[i].x1 - 50, y1: historyLines.current[i].y1 + 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 + 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 + 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 + 50, }) } } else { // left if (prev?.direction !== 'top') { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 - 50, y2: historyLines.current[i].y2 - 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 - 50, }) } } } else if (next.direction === 'bottom') { if (historyLines.current[i].direction === 'right') { if (prev?.direction !== 'bottom') { newOuterlines.push({ x1: historyLines.current[i].x1 - 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 - 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 - 50, y1: historyLines.current[i].y1 + 50, x2: historyLines.current[i].x2 - 50, y2: historyLines.current[i].y2 + 50, }) } } else { // left if (prev.direction !== 'bottom') { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 - 50, y2: historyLines.current[i].y2 - 50, }) } else { newOuterlines.push({ x1: historyLines.current[i].x1 + 50, y1: historyLines.current[i].y1 - 50, x2: historyLines.current[i].x2 + 50, y2: historyLines.current[i].y2 - 50, }) } } } } else { const tmp = newOuterlines[newOuterlines.length - 1] newOuterlines.push({ x1: tmp.x2, y1: tmp.y2, x2: newOuterlines[0].x1, y2: newOuterlines[0].y1, }) } } makePolygon(newOuterlines) } return { mode, changeMode, setCanvas, handleClear, zoomIn, zoomOut, zoom, } }