import { useEffect, useRef, useState } from 'react' import QRect from '@/components/fabric/QRect' import QPolygon from '@/components/fabric/QPolygon' import { getStartIndex, rearrangeArray, findTopTwoIndexesByDistance, } from '@/util/canvas-util' import { useRecoilState } from 'recoil' import { fontSizeState, sortedPolygonArray } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' 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 [shape, setShape] = useState(0) const [sortedArray, setSortedArray] = useRecoilState(sortedPolygonArray) 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 = () => { 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, }, ) pushHistoryLine(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(endPointCircle) historyPoints.current.push(endPointCircle) points.current.forEach((point) => { canvas?.remove(point) }) points.current = [endPointCircle] } } canvas?.renderAll() }) } const pushHistoryLine = (line) => { if ( historyLines.current.length > 0 && historyLines.current[historyLines.current.length - 1].direction === line.direction ) { // 같은 방향의 선이 두 번 연속으로 그려지면 이전 선을 제거하고, 새로운 선과 merge한다. const lastLine = historyLines.current.pop() canvas?.remove(lastLine) const mergedLine = new QLine( [lastLine.x1, lastLine.y1, line.x2, line.y2], { stroke: 'black', strokeWidth: 2, selectable: false, viewLengthText: true, direction: lastLine.direction, fontSize: fontSize, }, ) historyLines.current.push(mergedLine) canvas?.add(mergedLine) } else { historyLines.current.push(line) canvas?.add(line) } } 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() handleOuterlinesTest() //외곽선 그리기 테스트 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, }) pushHistoryLine(line) canvas?.renderAll() } const makePolygon = (otherLines) => { // 캔버스에서 모든 라인 객체를 찾습니다. const lines = otherLines || historyLines.current if (!otherLines) { //외각선 기준 const sortedIndex = getStartIndex(lines) let tmpArraySorted = rearrangeArray(lines, sortedIndex) if (tmpArraySorted[0].direction === 'right') { //시계방향 tmpArraySorted = tmpArraySorted.reverse() //그럼 배열을 거꾸로 만들어서 무조건 반시계방향으로 배열 보정 } setSortedArray(tmpArraySorted) //recoil에 넣음 const topIndex = findTopTwoIndexesByDistance(tmpArraySorted) //배열중에 큰 2값을 가져옴 TODO: 나중에는 인자로 받아서 다각으로 수정 해야됨 //일단 배열 6개 짜리 기준의 선 번호 if (topIndex[0] === 4) { if (topIndex[1] === 5) { //1번 setShape(1) } } else if (topIndex[0] === 1) { //4번 if (topIndex[1] === 2) { setShape(4) } } else if (topIndex[0] === 0) { if (topIndex[1] === 1) { //2번 setShape(2) } else if (topIndex[1] === 5) { setShape(3) } } 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, ) // 새로운 다각형 객체를 캔버스에 추가합니다. canvas.add(polygon) // 캔버스를 다시 그립니다. if (!otherLines) { polygon.fillCell() canvas.renderAll() polygon.setViewLengthText(false) setMode(Mode.DEFAULT) } } /** * 해당 캔버스를 비운다. */ 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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') { if (historyLines.current.length === 4) { 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 (historyLines.current.length === 6) { 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 (historyLines.current.length === 8) { 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, }) } } console.log(newOuterlines) makePolygon(newOuterlines) } const handleOuterlinesTest = () => { var offsetPoints = [] let offset = 71 // == 100 - 29 const sortedIndex = getStartIndex(historyLines.current) let tmpArraySorted = rearrangeArray(historyLines.current, sortedIndex) if (tmpArraySorted[0].direction === 'right') { //시계방향 tmpArraySorted = tmpArraySorted.reverse() //그럼 배열을 거꾸로 만들어서 무조건 반시계방향으로 배열 보정 } const points = tmpArraySorted.map((line) => ({ x: line.x1, y: line.y1, })) for (var i = 0; i < points.length; i++) { var prev = points[(i - 1 + points.length) % points.length] var current = points[i] var next = points[(i + 1) % points.length] // 두 벡터 계산 (prev -> current, current -> next) var vector1 = { x: current.x - prev.x, y: current.y - prev.y } var vector2 = { x: next.x - current.x, y: next.y - current.y } // 벡터의 길이 계산 var length1 = Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y) var length2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y) // 벡터를 단위 벡터로 정규화 var unitVector1 = { x: vector1.x / length1, y: vector1.y / length1 } var unitVector2 = { x: vector2.x / length2, y: vector2.y / length2 } // 법선 벡터 계산 (왼쪽 방향) var normal1 = { x: -unitVector1.y, y: unitVector1.x } var normal2 = { x: -unitVector2.y, y: unitVector2.x } // 법선 벡터 평균 계산 var averageNormal = { x: (normal1.x + normal2.x) / 2, y: (normal1.y + normal2.y) / 2, } // 평균 법선 벡터를 단위 벡터로 정규화 var lengthNormal = Math.sqrt( averageNormal.x * averageNormal.x + averageNormal.y * averageNormal.y, ) var unitNormal = { x: averageNormal.x / lengthNormal, y: averageNormal.y / lengthNormal, } // 오프셋 적용 var offsetPoint = { x1: current.x + unitNormal.x * offset, y1: current.y + unitNormal.y * offset, } offsetPoints.push(offsetPoint) } makePolygon(offsetPoints) } const togglePolygonLine = (obj) => { const rtnLines = [] if (obj.type === 'QPolygon') { const points = obj.getCurrentPoints() points.forEach((point, index) => { const nextPoint = points[(index + 1) % points.length] // 마지막 점이면 첫 번째 점으로 연결 const line = new QLine([point.x, point.y, nextPoint.x, nextPoint.y], { stroke: 'black', strokeWidth: 2, selectable: false, fontSize: fontSize, // fontSize는 필요에 따라 조정 parent: obj, }) obj.visible = false canvas.add(line) rtnLines.push(line) }) canvas.renderAll() } if (obj.type === 'QLine') { const parent = obj.parent canvas ?.getObjects() .filter((obj) => obj.parent === parent) .forEach((obj) => { rtnLines.push(obj) canvas.remove(obj) }) parent.visible = true canvas.renderAll() } return rtnLines } return { mode, changeMode, setCanvas, handleClear, zoomIn, zoomOut, zoom, togglePolygonLine, } }