import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, pitchTextSelector } from '@/store/canvasAtom' import { useRecoilValue } from 'recoil' import { fabric } from 'fabric' import { distanceBetweenPoints, findAndRemoveClosestPoint, getDegreeByChon, getDegreeInOrientation, isPointOnLine, toFixedWithoutRounding, } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' import { isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils' import { flowDisplaySelector } from '@/store/settingAtom' import { fontSelector } from '@/store/fontAtom' import { QLine } from '@/components/fabric/QLine' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import { useLine } from '@/hooks/useLine' export const usePolygon = () => { const canvas = useRecoilValue(canvasState) const isFlowDisplay = useRecoilValue(flowDisplaySelector) const flowFontOptions = useRecoilValue(fontSelector('flowText')) const lengthTextFontOptions = useRecoilValue(fontSelector('lengthText')) const currentAngleType = useRecoilValue(currentAngleTypeSelector) const pitchText = useRecoilValue(pitchTextSelector) const { getLengthByLine } = useLine() const addPolygon = (points, options, isAddCanvas = true) => { const polygon = new QPolygon(points, { ...options, fontSize: lengthTextFontOptions.fontSize.value, fill: options.fill || 'transparent', stroke: options.stroke || '#000000', // selectable: true, }) if (isAddCanvas) canvas?.add(polygon) addLengthText(polygon) return polygon } const addPolygonByLines = (lines, options) => { //lines의 idx를 정렬한다. lines.sort((a, b) => a.idx - b.idx) const points = createPolygonPointsFromOuterLines(lines) return addPolygon(points, { ...options, }) } const addLengthText = (polygon) => { const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText' && obj.parentId === polygon.id) lengthTexts.forEach((text) => { canvas.remove(text) }) const lines = polygon.lines polygon.texts = [] lines.forEach((line, i) => { const length = line.getLength() const { planeSize, actualSize } = line.attributes const scaleX = line.scaleX const scaleY = line.scaleY const x1 = line.left const y1 = line.top const x2 = line.left + line.width * scaleX const y2 = line.top + line.height * scaleY let left, top if (line.direction === 'right') { left = (x1 + x2) / 2 top = (y1 + y2) / 2 + 10 } else if (line.direction === 'top') { left = (x1 + x2) / 2 + 10 top = (y1 + y2) / 2 } else if (line.direction === 'left') { left = (x1 + x2) / 2 top = (y1 + y2) / 2 - 30 } else if (line.direction === 'bottom') { left = (x1 + x2) / 2 - 50 top = (y1 + y2) / 2 } const minX = line.left const maxX = line.left + line.width const minY = line.top const maxY = line.top + line.length const degree = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI const text = new fabric.Textbox(planeSize ? planeSize.toString() : length.toString(), { left: left, top: top, fontSize: lengthTextFontOptions.fontSize.value, minX, maxX, minY, maxY, parentDirection: line.direction, parentDegree: degree, parentId: polygon.id, planeSize: planeSize ?? length, actualSize: actualSize ?? length, editable: false, selectable: true, lockRotation: true, lockScalingX: true, lockScalingY: true, parent: polygon, name: 'lengthText', }) polygon.texts.push(text) canvas.add(text) }) /*const points = polygon.get('points') points.forEach((start, i) => { const end = points[(i + 1) % points.length] const dx = end.x - start.x const dy = end.y - start.y const length = Number(Math.sqrt(dx * dx + dy * dy).toFixed(1)) * 10 const midPoint = new fabric.Point((start.x + end.x) / 2, (start.y + end.y) / 2) const degree = (Math.atan2(dy, dx) * 180) / Math.PI // Create new text object if it doesn't exist const text = new fabric.Text(length.toString(), { left: midPoint.x, top: midPoint.y, parentId: polygon.id, fontSize: lengthTextFontOptions.fontSize.value, minX: Math.min(start.x, end.x), maxX: Math.max(start.x, end.x), minY: Math.min(start.y, end.y), maxY: Math.max(start.y, end.y), parentDirection: getDirectionByPoint(start, end), parentDegree: degree, dirty: true, editable: true, selectable: true, lockRotation: true, lockScalingX: true, lockScalingY: true, idx: i, name: 'lengthText', parent: polygon, }) // this.texts.push(text) canvas.add(text) })*/ canvas.renderAll() } const createPolygonPointsFromOuterLines = (outerLines) => { if (!outerLines || outerLines.length === 0) { return [] } // Extract points from outerLines return outerLines.map((line) => ({ x: line.x1, y: line.y1, })) } const removePolygon = (polygon) => { const texts = canvas.getObjects().filter((obj) => obj.parentId === polygon.id) texts.forEach((text) => { canvas.remove(text) }) canvas.remove(polygon) canvas.renderAll() } /** * poolygon의 방향에 따라 화살표를 추가한다. * @param polygon * @param showDirectionText */ const drawDirectionArrow = (polygon, showDirectionText = true) => { if (!polygon) { return } if (polygon.points.length < 3) { return } // 모듈있으면 화살표 이미 그려져 있으므로 수행 안함 const hasModules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE).length > 0 if (hasModules) { return } const direction = polygon.direction if (!direction) { return } //동일 아이디가 있으면 일단 지우고 다시 그린다 const existArrow = polygon.canvas.getObjects().filter((obj) => obj.name === 'arrow' && obj.parentId === polygon.id) if (existArrow.length > 0) { polygon.canvas.remove(...existArrow) } polygon.canvas .getObjects() .filter((obj) => obj.name === 'flowText' && obj.parentId === polygon.arrow?.id) .forEach((obj) => polygon.canvas.remove(obj)) let arrow = null let points = [] if (polygon.arrow) { polygon.canvas.remove(polygon.arrow) } let centerPoint = { x: polygon.left, y: polygon.top } const { width, height } = polygon let stickeyPoint const polygonMaxX = Math.max(...polygon.getCurrentPoints().map((point) => point.x)) const polygonMinX = Math.min(...polygon.getCurrentPoints().map((point) => point.x)) const polygonMaxY = Math.max(...polygon.getCurrentPoints().map((point) => point.y)) const polygonMinY = Math.min(...polygon.getCurrentPoints().map((point) => point.y)) const lines = polygon.lines let centerPoints switch (direction) { case 'south': // lines중 가장 아래에 있는 라인을 찾는다. const line = lines.reduce((acc, cur) => { return acc.y2 > cur.y2 ? acc : cur }, lines[0]) centerPoint = { x: (line.x2 + line.x1) / 2, y: Math.max(line.y1, line.y2) } break case 'north': // lines중 가장 위에 있는 라인을 찾는다. const line2 = lines.reduce((acc, cur) => { return acc.y2 < cur.y2 ? acc : cur }, lines[0]) centerPoint = { x: (line2.x2 + line2.x1) / 2, y: Math.min(line2.y1, line2.y2) } break case 'west': // lines중 가장 왼쪽에 있는 라인을 찾는다. const line3 = lines.reduce((acc, cur) => { return acc.x2 < cur.x2 ? acc : cur }, lines[0]) centerPoint = { x: Math.min(line3.x1, line3.x2), y: (line3.y1 + line3.y2) / 2 } break case 'east': // lines중 가장 오른쪽에 있는 라인을 찾는다. const line4 = lines.reduce((acc, cur) => { return acc.x2 > cur.x2 ? acc : cur }, lines[0]) centerPoint = { x: Math.max(line4.x1, line4.x2), y: (line4.y1 + line4.y2) / 2 } break } switch (direction) { case 'north': points = [ { x: centerPoint.x, y: polygonMinY - 50 }, { x: centerPoint.x + 20, y: polygonMinY - 50 }, { x: centerPoint.x + 20, y: polygonMinY - 80 }, { x: centerPoint.x + 50, y: polygonMinY - 80 }, { x: centerPoint.x, y: polygonMinY - 110 }, { x: centerPoint.x - 50, y: polygonMinY - 80 }, { x: centerPoint.x - 20, y: polygonMinY - 80 }, { x: centerPoint.x - 20, y: polygonMinY - 50 }, ] stickeyPoint = { x: centerPoint.x, y: polygonMinY - 110 } break case 'south': points = [ { x: centerPoint.x, y: polygonMaxY + 50 }, { x: centerPoint.x + 20, y: polygonMaxY + 50 }, { x: centerPoint.x + 20, y: polygonMaxY + 80 }, { x: centerPoint.x + 50, y: polygonMaxY + 80 }, { x: centerPoint.x, y: polygonMaxY + 110 }, { x: centerPoint.x - 50, y: polygonMaxY + 80 }, { x: centerPoint.x - 20, y: polygonMaxY + 80 }, { x: centerPoint.x - 20, y: polygonMaxY + 50 }, ] stickeyPoint = { x: centerPoint.x, y: polygonMaxY + 110 } break case 'west': points = [ { x: polygonMinX - 50, y: centerPoint.y }, { x: polygonMinX - 50, y: centerPoint.y + 20 }, { x: polygonMinX - 80, y: centerPoint.y + 20 }, { x: polygonMinX - 80, y: centerPoint.y + 50 }, { x: polygonMinX - 110, y: centerPoint.y }, { x: polygonMinX - 80, y: centerPoint.y - 50 }, { x: polygonMinX - 80, y: centerPoint.y - 20 }, { x: polygonMinX - 50, y: centerPoint.y - 20 }, ] stickeyPoint = { x: polygonMinX - 110, y: centerPoint.y } break case 'east': points = [ { x: polygonMaxX + 50, y: centerPoint.y }, { x: polygonMaxX + 50, y: centerPoint.y + 20 }, { x: polygonMaxX + 80, y: centerPoint.y + 20 }, { x: polygonMaxX + 80, y: centerPoint.y + 50 }, { x: polygonMaxX + 110, y: centerPoint.y }, { x: polygonMaxX + 80, y: centerPoint.y - 50 }, { x: polygonMaxX + 80, y: centerPoint.y - 20 }, { x: polygonMaxX + 50, y: centerPoint.y - 20 }, ] stickeyPoint = { x: polygonMaxX + 110, y: centerPoint.y } break } arrow = new QPolygon(points, { selectable: false, name: 'arrow', fill: 'transparent', stroke: 'black', direction: direction, parent: polygon, stickeyPoint: stickeyPoint, surfaceCompass: polygon.surfaceCompass, moduleCompass: polygon.moduleCompass, visible: isFlowDisplay, pitch: polygon.roofMaterial?.pitch ?? 4, parentId: polygon.id, }) arrow.setViewLengthText(false) polygon.arrow = arrow polygon.canvas.add(arrow) polygon.canvas.renderAll() drawDirectionStringToArrow2(polygon, showDirectionText) // drawDirectionStringToArrow() } //arrow의 compass 값으로 방향 글자 설정 필요 const drawDirectionStringToArrow2 = (polygon, showDirectionText) => { const { direction, surfaceCompass, moduleCompass, arrow } = polygon if (moduleCompass === null || moduleCompass === undefined) { const textObj = new fabric.Text(`${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText}`, { fontFamily: flowFontOptions.fontFamily.value, fontWeight: flowFontOptions.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', fontStyle: flowFontOptions.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', fontSize: flowFontOptions.fontSize.value, fill: flowFontOptions.fontColor.value, originX: 'center', originY: 'center', pitch: arrow.pitch, name: 'flowText', selectable: false, left: arrow.stickeyPoint.x, top: arrow.stickeyPoint.y, parent: arrow, parentId: arrow.id, visible: isFlowDisplay, }) polygon.canvas.add(textObj) return } let text = '' let compassType = (375 - getDegreeInOrientation(moduleCompass)) / 15 if (moduleCompass === 0 || (moduleCompass < 0 && moduleCompass >= -6)) { compassType = 1 } else if (moduleCompass < 0 && moduleCompass >= -21) { compassType = 2 } else if (moduleCompass < 0 && moduleCompass >= -36) { compassType = 3 } else if (moduleCompass < 0 && moduleCompass >= -51) { compassType = 4 } else if (moduleCompass < 0 && moduleCompass >= -66) { compassType = 5 } else if (moduleCompass < 0 && moduleCompass >= -81) { compassType = 6 } else if (moduleCompass < 0 && moduleCompass >= -96) { compassType = 7 } else if (moduleCompass < 0 && moduleCompass >= -111) { compassType = 8 } else if (moduleCompass < 0 && moduleCompass >= -126) { compassType = 9 } else if (moduleCompass < 0 && moduleCompass >= -141) { compassType = 10 } else if (moduleCompass < 0 && moduleCompass >= -156) { compassType = 11 } else if (moduleCompass < 0 && moduleCompass >= -171) { compassType = 12 } else if (moduleCompass === 180) { compassType = 13 } if ([1, 25].includes(compassType)) { direction === 'north' ? (text = '北') : direction === 'south' ? (text = '南') : direction === 'west' ? (text = '西') : (text = '東') } else if ([2, 3].includes(compassType)) { direction === 'north' ? (text = '北北東') : direction === 'south' ? (text = '南南西') : direction === 'west' ? (text = '西北西') : (text = '東南東') } else if ([4].includes(compassType)) { direction === 'north' ? (text = '北東') : direction === 'south' ? (text = '南西') : direction === 'west' ? (text = '北西') : (text = '南東') } else if ([5, 6].includes(compassType)) { direction === 'north' ? (text = '東北東') : direction === 'south' ? (text = '西南西') : direction === 'west' ? (text = '北北西') : (text = '南南東') } else if ([7].includes(compassType)) { direction === 'north' ? (text = '東') : direction === 'south' ? (text = '西') : direction === 'west' ? (text = '北') : (text = '南') } else if ([8, 9].includes(compassType)) { direction === 'north' ? (text = '東南東') : direction === 'south' ? (text = '西北西') : direction === 'west' ? (text = '北北東') : (text = '南南西') } else if ([10].includes(compassType)) { direction === 'north' ? (text = '南東') : direction === 'south' ? (text = '北西') : direction === 'west' ? (text = '南西') : (text = '北東') } else if ([11, 12].includes(compassType)) { direction === 'north' ? (text = '南南東') : direction === 'south' ? (text = '北北西') : direction === 'west' ? (text = '東北東') : (text = '西南西') } else if ([13].includes(compassType)) { direction === 'north' ? (text = '南') : direction === 'south' ? (text = '北') : direction === 'west' ? (text = '東') : (text = '西') } else if ([14, 15].includes(compassType)) { direction === 'north' ? (text = '南南西') : direction === 'south' ? (text = '北北東') : direction === 'west' ? (text = '東南東') : (text = '西北西') } else if ([16].includes(compassType)) { direction === 'north' ? (text = '南西') : direction === 'south' ? (text = '北東') : direction === 'west' ? (text = '南東') : (text = '北西') } else if ([17, 18].includes(compassType)) { direction === 'north' ? (text = '西南西') : direction === 'south' ? (text = '東北東') : direction === 'west' ? (text = '南南東') : (text = '北北西') } else if ([19].includes(compassType)) { direction === 'north' ? (text = '西') : direction === 'south' ? (text = '東') : direction === 'west' ? (text = '南') : (text = '北') } else if ([20, 21].includes(compassType)) { direction === 'north' ? (text = '西北西') : direction === 'south' ? (text = '東南東') : direction === 'west' ? (text = '南南西') : (text = '北北東') } else if ([22].includes(compassType)) { direction === 'north' ? (text = '北西') : direction === 'south' ? (text = '南東') : direction === 'west' ? (text = '南西') : (text = '北東') } else if ([23, 24].includes(compassType)) { direction === 'north' ? (text = '北北西') : direction === 'south' ? (text = '南南東') : direction === 'west' ? (text = '西南西') : (text = '東北東') } // 東,西,南,北 if ([360].includes(surfaceCompass)) { text = '南' } else if ([345, 330].includes(surfaceCompass)) { text = '南南東' } else if ([315].includes(surfaceCompass)) { text = '南東' } else if ([300, 285].includes(surfaceCompass)) { text = '東南東' } else if ([270].includes(surfaceCompass)) { text = '東' } else if ([255, 240].includes(surfaceCompass)) { text = '東北東' } else if ([225].includes(surfaceCompass)) { text = '北東' } else if ([210, 195].includes(surfaceCompass)) { text = '北北東' } else if ([180].includes(surfaceCompass)) { text = '北' } else if ([165, 150].includes(surfaceCompass)) { text = '北北西' } else if ([135].includes(surfaceCompass)) { text = '北西' } else if ([120, 105].includes(surfaceCompass)) { text = '西北西' } else if ([90].includes(surfaceCompass)) { text = '西' } else if ([75, 60].includes(surfaceCompass)) { text = '西南西' } else if ([45].includes(surfaceCompass)) { text = '西南' } else if ([30, 15].includes(surfaceCompass)) { text = '西西南' } const sameDirectionCnt = canvas.getObjects().filter((obj) => { const onlyStrDirection = obj.directionText?.replace(/[0-9]/g, '') return obj.name === POLYGON_TYPE.ROOF && obj !== polygon && onlyStrDirection === text }) text = text + (sameDirectionCnt.length + 1) polygon.set('directionText', text) const textObj = new fabric.Text( `${showDirectionText && text} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText})`, { fontFamily: flowFontOptions.fontFamily.value, fontWeight: flowFontOptions.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', fontStyle: flowFontOptions.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', fontSize: flowFontOptions.fontSize.value, fill: flowFontOptions.fontColor.value, pitch: arrow.pitch, originX: 'center', originY: 'center', name: 'flowText', originText: text, selectable: false, left: arrow.stickeyPoint.x, top: arrow.stickeyPoint.y, parent: arrow, parentId: arrow.id, visible: isFlowDisplay, }, ) polygon.canvas.add(textObj) } /** * 방향을 나타낸 화살표에 각도에 따라 글씨 추가 * @param canvas * @param compass */ const drawDirectionStringToArrow = (canvas, compass = 0) => { const arrows = canvas?.getObjects().filter((obj) => obj.name === 'arrow') if (arrows.length === 0) { return } const eastArrows = arrows.filter((arrow) => arrow.direction === 'east') const westArrows = arrows.filter((arrow) => arrow.direction === 'west') const northArrows = arrows.filter((arrow) => arrow.direction === 'north') const southArrows = arrows.filter((arrow) => arrow.direction === 'south') let southText = '南' let eastText = '東' let westText = '西' let northText = '北' if (compass === 0 || compass === 360) { // 남,동,서 가능 // 그대로 } else if (compass < 45) { //남(남남동),동(동북동),서(서남서) 가능 //북(북북서) southText = '南南東' eastText = '東北東' westText = '西南西' northText = '北北西' } else if (compass === 45) { // 남, 서 가능 // 남(남동) // 서(남서) // 북(북서) // 동(북동) southText = '南東' westText = '南西' northText = '北西' eastText = '北東' } else if (compass < 90) { // 북(서북서) // 동 (북북동) // 남(동남동) // 서(남남서) northText = '北西北' eastText = '北北東' southText = '東南東' westText = '南南西' } else if (compass === 90) { // 동(북) // 서(남) // 남(동) // 북(서) eastText = '北' westText = '南' southText = '東' northText = '西' } else if (compass < 135) { // 남,서,북 가능 // 동(북북서) // 서(남남동) // 남(동북동) // 북(서남서) eastText = '北北西' westText = '南南東' southText = '東北東' northText = '西南西' } else if (compass === 135) { // 서,북 가능 // 서(남동) // 북(남서) // 남(북동) // 동(북서) westText = '南東' northText = '南西' southText = '北東' eastText = '北西' } else if (compass < 180) { // 북,동,서 가능 // 북(남남서) // 동(서북서) // 남(북북동) // 서(동남동) northText = '南南西' eastText = '西北西' southText = '北北東' westText = '東南東' } else if (compass === 180) { // 북,동,서 가능 // 북(남) // 동(서) // 남(북) // 서(동) northText = '南' eastText = '西' southText = '北' westText = '東' } else if (compass < 225) { // 서,북,동 가능 // 북(남남동) // 동(서남서) // 남(북북서) // 서(동남동) northText = '南南東' eastText = '西南西' southText = '北北西' westText = '東南東' } else if (compass === 225) { // 북,동 가능 // 북(남동) // 동(남서) // 남(북서) // 서(북동) northText = '南東' eastText = '南西' southText = '北西' westText = '北東' } else if (compass < 270) { // 북동남 가능 // 북(동남동) // 동(남남서) // 남(서북서) // 서(북북동) northText = '東南東' eastText = '南南西' southText = '西北西' westText = '北北東' } else if (compass === 270) { // 북동남 가능 // 북(동) // 동(남) // 남(서) // 서(북) northText = '東' eastText = '南' southText = '西' westText = '北' } else if (compass < 315) { // 북,동,남 가능 // 북(동북동) // 동(남남동) // 남(서남서) // 서(북북서) northText = '東北東' eastText = '南南東' southText = '西南西' westText = '北北西' } else if (compass === 315) { // 동,남 가능 // 북(북동) // 동(남동) // 남(남서) // 서(북서) northText = '北東' eastText = '南東' southText = '南西' westText = '北西' } else if (compass < 360) { // 남,동,서 가능 // 북(북북동) // 동(동남동) // 남(남남서) // 서(서북서) northText = '北北東' eastText = '東南東' southText = '南南西' westText = '西北西' } clearFlowText(canvas) addTextByArrows(eastArrows, eastText, canvas) addTextByArrows(westArrows, westText, canvas) addTextByArrows(northArrows, northText, canvas) addTextByArrows(southArrows, southText, canvas) } const clearFlowText = (canvas) => { const texts = canvas.getObjects().filter((obj) => obj.name === 'flowText') texts.forEach((text) => { canvas.remove(text) }) } const addTextByArrows = (arrows, txt, canvas) => { arrows.forEach((arrow, index) => { // const textStr = `${txt}${index + 1} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText})` const textStr = `${txt} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText})` const text = new fabric.Text(`${textStr}`, { fontFamily: flowFontOptions.fontFamily.value, fontWeight: flowFontOptions.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', fontStyle: flowFontOptions.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', fontSize: flowFontOptions.fontSize.value, fill: flowFontOptions.fontColor.value, pitch: arrow.pitch, originX: 'center', originY: 'center', name: 'flowText', originText: `${txt}${index + 1}`, selectable: false, left: arrow.stickeyPoint.x, top: arrow.stickeyPoint.y, parent: arrow, parentId: arrow.id, visible: isFlowDisplay, }) canvas.add(text) }) } const splitPolygonWithLines = (polygon) => { polygon.set({ visible: false }) let innerLines = [...polygon.innerLines].filter((line) => line.visible) /*// innerLine이 세팅이 안되어있는경우 찾아서 세팅한다. if (!innerLines || innerLines.length === 0) { let innerLineTypes = Object.keys(LINE_TYPE.SUBLINE).map((key, value) => LINE_TYPE.SUBLINE[key]) polygon.innerLines = canvas .getObjects() .filter( (obj) => obj.type === 'QLine' && obj.attributes?.type !== 'pitchSizeLine' && obj.attributes?.roofId === polygon.id && innerLineTypes.includes(obj.name), ) innerLines = [...polygon.innerLines] }*/ canvas.renderAll() let polygonLines = [...polygon.lines] const roofs = [] //polygonLines를 순회하며 innerLines와 교차하는 점을 line의 속성에 배열로 저장한다. polygonLines.forEach((line) => { let startPoint // 시작점 let endPoint // 끝점 if (line.x1 < line.x2) { startPoint = { x: line.x1, y: line.y1 } endPoint = { x: line.x2, y: line.y2 } } else if (line.x1 > line.x2) { startPoint = { x: line.x2, y: line.y2 } endPoint = { x: line.x1, y: line.y1 } } else { if (line.y1 < line.y2) { startPoint = { x: line.x1, y: line.y1 } endPoint = { x: line.x2, y: line.y2 } } else { startPoint = { x: line.x2, y: line.y2 } endPoint = { x: line.x1, y: line.y1 } } } line.startPoint = startPoint line.endPoint = endPoint }) innerLines.forEach((line) => { let startPoint // 시작점 let endPoint // 끝점 if (line.x1 < line.x2) { startPoint = { x: line.x1, y: line.y1 } endPoint = { x: line.x2, y: line.y2 } } else if (line.x1 > line.x2) { startPoint = { x: line.x2, y: line.y2 } endPoint = { x: line.x1, y: line.y1 } } else { if (line.y1 < line.y2) { startPoint = { x: line.x1, y: line.y1 } endPoint = { x: line.x2, y: line.y2 } } else { startPoint = { x: line.x2, y: line.y2 } endPoint = { x: line.x1, y: line.y1 } } } line.startPoint = startPoint line.endPoint = endPoint }) /*polygonLines.forEach((line) => { line.set({ strokeWidth: 10 }) canvas.add(line) }) canvas.renderAll()*/ polygonLines.forEach((line) => { /*const originStroke = line.stroke line.set({ stroke: 'red' }) canvas.renderAll()*/ const intersections = [] innerLines.forEach((innerLine) => { /*const originInnerStroke = innerLine.stroke innerLine.set({ stroke: 'red' }) canvas.renderAll()*/ if (isPointOnLine(line, innerLine.startPoint)) { canvas.renderAll() if (isSamePoint(line.startPoint, innerLine.startPoint) || isSamePoint(line.endPoint, innerLine.startPoint)) { return } intersections.push(innerLine.startPoint) } if (isPointOnLine(line, innerLine.endPoint)) { canvas.renderAll() if (isSamePoint(line.startPoint, innerLine.endPoint) || isSamePoint(line.endPoint, innerLine.endPoint)) { return } intersections.push(innerLine.endPoint) } /*innerLine.set({ stroke: originInnerStroke }) canvas.renderAll()*/ }) line.set({ intersections }) /*line.set({ stroke: originStroke }) canvas.renderAll()*/ }) const divideLines = polygonLines.filter((line) => line.intersections?.length > 0) let newLines = [] polygonLines = polygonLines.filter((line) => !line.intersections || line.intersections.length === 0) for (let i = divideLines.length - 1; i >= 0; i--) { const line = divideLines[i] const { intersections, startPoint, endPoint } = line if (intersections.length === 1) { const newLinePoint1 = [line.x1, line.y1, intersections[0].x, intersections[0].y] const newLinePoint2 = [intersections[0].x, intersections[0].y, line.x2, line.y2] const newLine1 = new QLine(newLinePoint1, { stroke: 'blue', strokeWidth: 3, fontSize: polygon.fontSize, attributes: line.attributes, name: 'newLine', }) const newLine2 = new QLine(newLinePoint2, { stroke: 'blue', strokeWidth: 3, fontSize: polygon.fontSize, attributes: line.attributes, name: 'newLine', }) newLine1.attributes = { ...line.attributes, planeSize: Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10, actualSize: Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10, } newLine2.attributes = { ...line.attributes, planeSize: Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10, actualSize: Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10, } newLines.push(newLine1, newLine2) divideLines.splice(i, 1) // 기존 line 제거 } else { let currentPoint = startPoint while (intersections.length !== 0) { const minDistancePoint = findAndRemoveClosestPoint(currentPoint, intersections) const newLinePoint = [currentPoint.x, currentPoint.y, minDistancePoint.x, minDistancePoint.y] const newLine = new QLine(newLinePoint, { stroke: 'blue', strokeWidth: 3, fontSize: polygon.fontSize, attributes: line.attributes, name: 'newLine', }) newLine.attributes = { ...line.attributes, planeSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, actualSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, } newLines.push(newLine) currentPoint = minDistancePoint } const newLinePoint = [currentPoint.x, currentPoint.y, endPoint.x, endPoint.y] const newLine = new QLine(newLinePoint, { stroke: 'blue', strokeWidth: 3, fontSize: polygon.fontSize, attributes: line.attributes, name: 'newLine', }) newLine.attributes = { ...line.attributes, planeSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, actualSize: Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10, } newLines.push(newLine) divideLines.splice(i, 1) // 기존 line 제거 } } //polygonLines에서 divideLines를 제거하고 newLines를 추가한다. newLines = newLines.filter((line) => !(Math.abs(line.startPoint.x - line.endPoint.x) < 1 && Math.abs(line.startPoint.y - line.endPoint.y) < 1)) polygonLines = [...polygonLines, ...newLines] let allLines = [...polygonLines, ...innerLines] /*allLines.forEach((line) => { const originColor = line.stroke line.set('stroke', 'red') canvas.renderAll() line.set('stroke', originColor) canvas.renderAll() })*/ const allPoints = [] // test용 좌표 const polygonLinesPoints = polygonLines.map((line) => { return { startPoint: line.startPoint, endPoint: line.endPoint } }) const innerLinesPoints = innerLines.map((line) => { return { startPoint: line.startPoint, endPoint: line.endPoint } }) polygonLinesPoints.forEach(({ startPoint, endPoint }) => { allPoints.push(startPoint) allPoints.push(endPoint) }) innerLinesPoints.forEach(({ startPoint, endPoint }) => { allPoints.push(startPoint) allPoints.push(endPoint) }) // allPoints에서 중복을 제거한다. const uniquePoints = allPoints.filter((point, index, self) => { return ( index === self.findIndex((p) => { return isSamePoint(p, point) }) ) }) // 2025-02-19 대각선은 케라바, 직선은 용마루로 세팅 innerLines.forEach((innerLine) => { const startPoint = innerLine.startPoint const endPoint = innerLine.endPoint // startPoint와 endPoint의 각도가 0,90,180,270이면 직선으로 판단 if (Math.abs(startPoint.x - endPoint.x) < 2 || Math.abs(startPoint.y - endPoint.y) < 2) { if (!innerLine.attributes || !innerLine.attributes.type || innerLine.attributes.type === 'default') { innerLine.attributes = { ...innerLine.attributes, type: LINE_TYPE.SUBLINE.RIDGE, } } } else { if (!innerLine.attributes || !innerLine.attributes.type || innerLine.attributes.type === 'default') { innerLine.attributes = { ...innerLine.attributes, type: LINE_TYPE.SUBLINE.GABLE, } } } }) /*innerLines.forEach((line) => { const startPoint = line.startPoint const endPoint = line.endPoint /!*canvas.add(new fabric.Circle({ left: startPoint.x, top: startPoint.y + 10, radius: 5, fill: 'red' })) canvas.add(new fabric.Circle({ left: endPoint.x, top: endPoint.y - 10, radius: 5, fill: 'blue' }))*!/ })*/ /** * 왼쪽 상단을 startPoint로 전부 변경 */ allLines.forEach((line) => { let startPoint // 시작점 let endPoint // 끝점 if (line.x1 < line.x2) { startPoint = { x: line.x1, y: line.y1 } endPoint = { x: line.x2, y: line.y2 } } else if (line.x1 > line.x2) { startPoint = { x: line.x2, y: line.y2 } endPoint = { x: line.x1, y: line.y1 } } else { if (line.y1 < line.y2) { startPoint = { x: line.x1, y: line.y1 } endPoint = { x: line.x2, y: line.y2 } } else { startPoint = { x: line.x2, y: line.y2 } endPoint = { x: line.x1, y: line.y1 } } } line.startPoint = startPoint line.endPoint = endPoint }) //allLines에서 중복을 제거한다. allLines = allLines.filter((line, index, self) => { return ( index === self.findIndex((l) => { return isSamePoint(l.startPoint, line.startPoint) && isSamePoint(l.endPoint, line.endPoint) }) ) }) // 나눠서 중복 제거된 roof return const newRoofs = getSplitRoofsPoints(allLines) newRoofs.forEach((roofPoint, index) => { let defense, pitch let representLines = [] let representLine // 지붕을 그리면서 기존 polygon의 line중 연결된 line을 찾는다. polygonLines.forEach((line) => { let startFlag = false let endFlag = false const startPoint = line.startPoint const endPoint = line.endPoint roofPoint.forEach((point, index) => { if (isSamePoint(point, startPoint)) { startFlag = true } if (isSamePoint(point, endPoint)) { endFlag = true } }) if (startFlag && endFlag) { if (!representLines.includes(line) && line.attributes.type === LINE_TYPE.WALLLINE.EAVES) { representLines.push(line) } } }) // representLines중 가장 긴 line을 찾는다. representLines.forEach((line) => { if (!representLine) { representLine = line } else { if (getLengthByLine(representLine) < getLengthByLine(line)) { representLine = line } } }) if (!representLine) { representLines = representLines.filter((line) => line.stroke !== 'blue') representLines.forEach((line) => { if (!representLine) { representLine = line } else { if (representLine.length < line.length) { representLine = line } } }) } const direction = polygon.direction ?? representLine?.direction ?? '' const polygonDirection = polygon.direction switch (direction) { case 'top': defense = 'east' break case 'right': defense = 'south' break case 'bottom': defense = 'west' break case 'left': defense = 'north' break default: defense = 'south' break } pitch = polygon.lines[index]?.attributes?.pitch ?? 0 const roof = new QPolygon(roofPoint, { fontSize: polygon.fontSize, stroke: 'black', fill: 'transparent', strokeWidth: 3, name: POLYGON_TYPE.ROOF, originX: 'center', originY: 'center', selectable: true, defense: defense, direction: polygonDirection ?? defense, pitch: pitch, }) //allLines중 생성된 roof와 관련있는 line을 찾는다. const roofLines = [...polygonLines, ...polygon.innerLines].filter((line) => { let startFlag = false let endFlag = false const startPoint = line.startPoint const endPoint = line.endPoint roofPoint.forEach((point, index) => { if (isSamePoint(point, startPoint)) { startFlag = true } if (isSamePoint(point, endPoint)) { endFlag = true } }) return startFlag && endFlag }) roofLines.forEach((line) => { roof.lines.forEach((roofLine) => { if ( (isSamePoint(line.startPoint, roofLine.startPoint) && isSamePoint(line.endPoint, roofLine.endPoint)) || (isSamePoint(line.startPoint, roofLine.endPoint) && isSamePoint(line.endPoint, roofLine.startPoint)) ) { roofLine.attributes = { ...line.attributes } } }) }) canvas.add(roof) addLengthText(roof) canvas.remove(polygon) canvas.renderAll() }) //지붕 완료 후 보조선을 전부 제거한다. const auxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine') auxiliaryLines.forEach((line) => { canvas.remove(line) }) canvas.renderAll() canvas.discardActiveObject() } const getSplitRoofsPoints = (allLines) => { // ==== Utility functions ==== function isSamePoint(p1, p2, epsilon = 1) { return Math.abs(p1.x - p2.x) <= 2 && Math.abs(p1.y - p2.y) <= 2 } function normalizePoint(p, epsilon = 1) { return { x: Math.round(p.x / epsilon) * epsilon, y: Math.round(p.y / epsilon) * epsilon, } } function pointToKey(p, epsilon = 1) { const norm = normalizePoint(p, epsilon) return `${norm.x},${norm.y}` } // 거리 계산 function calcDistance(p1, p2) { return Math.hypot(p2.x - p1.x, p2.y - p1.y) } // ==== Direct edge check ==== function isDirectlyConnected(start, end, graph, epsilon = 1) { const startKey = pointToKey(start, epsilon) return (graph[startKey] || []).some((neighbor) => isSamePoint(neighbor.point, end, epsilon)) } // ==== Dijkstra pathfinding ==== function findShortestPath(start, end, graph, epsilon = 1) { const startKey = pointToKey(start, epsilon) const endKey = pointToKey(end, epsilon) const distances = {} const previous = {} const visited = new Set() const queue = [{ key: startKey, dist: 0 }] for (const key in graph) distances[key] = Infinity distances[startKey] = 0 while (queue.length > 0) { queue.sort((a, b) => a.dist - b.dist) const { key } = queue.shift() if (visited.has(key)) continue visited.add(key) for (const neighbor of graph[key] || []) { const neighborKey = pointToKey(neighbor.point, epsilon) const alt = distances[key] + neighbor.distance if (alt < distances[neighborKey]) { distances[neighborKey] = alt previous[neighborKey] = key queue.push({ key: neighborKey, dist: alt }) } } } const path = [] let currentKey = endKey if (!previous[currentKey]) return null while (currentKey !== startKey) { const [x, y] = currentKey.split(',').map(Number) path.unshift({ x, y }) currentKey = previous[currentKey] } const [sx, sy] = startKey.split(',').map(Number) path.unshift({ x: sx, y: sy }) return path } // 최종 함수 function getPath(start, end, graph, epsilon = 1) { if (isDirectlyConnected(start, end, graph, epsilon)) { console.log('직선 연결 있음. 무시.') return [] } const path = findShortestPath(start, end, graph, epsilon) if (!path || path.length < 3) { console.log('경로 존재하나 3개 미만 좌표. 무시.') return [] } return path } const roofs = [] allLines.forEach((line) => { // 그래프 생성 const graph = {} const edges = allLines .filter((line2) => line !== line2) .map((line) => { return [line.startPoint, line.endPoint] }) for (const [p1, p2] of edges) { const key1 = pointToKey(p1) const key2 = pointToKey(p2) const distance = calcDistance(p1, p2) if (!graph[key1]) graph[key1] = [] if (!graph[key2]) graph[key2] = [] graph[key1].push({ point: p2, distance }) graph[key2].push({ point: p1, distance }) } const startPoint = { ...line.startPoint } // 시작점 let arrivalPoint = { ...line.endPoint } // 도착점 roofs.push(getPath(startPoint, arrivalPoint, graph)) }) return removeDuplicatePolygons(roofs.filter((roof) => roof.length < 100)) } const splitPolygonWithSeparate = (separates) => { separates.forEach((separate) => { const points = separate.lines.map((line) => { return { x: line.x1, y: line.y1 } }) let defense = '' switch (separate.attributes.direction) { case 'top': defense = 'east' break case 'right': defense = 'south' break case 'bottom': defense = 'west' break case 'left': defense = 'north' break } const roof = new QPolygon(points, { fontSize: separate.fontSize, stroke: 'black', fill: 'transparent', strokeWidth: 3, name: POLYGON_TYPE.ROOF, originX: 'center', originY: 'center', selectable: true, defense: defense, pitch: separate.attributes.pitch, direction: defense, }) canvas.add(roof) }) canvas.renderAll() } return { addPolygon, addPolygonByLines, removePolygon, drawDirectionArrow, addLengthText, splitPolygonWithLines, splitPolygonWithSeparate, } }