import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, globalPitchState, pitchTextSelector } from '@/store/canvasAtom' import { useRecoilValue } from 'recoil' import { fabric } from 'fabric' import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, isPointOnLine } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' import { isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils' import { basicSettingState, 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 globalPitch = useRecoilValue(globalPitchState) const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet const { setActualSize } = useLine() 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( +roofSizeSet === 1 ? (actualSize ? actualSize.toString() : length.toString()) : 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 + acc.y1 > cur.y2 + cur.y1 ? 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 + acc.y1 < cur.y2 + cur.y1 ? 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 + acc.x1 < cur.x2 + cur.x1 ? 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 + acc.x1 > cur.x2 + cur.x1 ? 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 값으로 방향 글자 설정 필요 // moduleCompass 각도와 direction(지붕면 방향)에 따라 한자 방위 텍스트 매핑 const drawDirectionStringToArrow2 = (polygon, showDirectionText) => { let { 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 = '' // moduleCompass 각도와 direction에 따른 한자 방위 매핑 // direction: south(↓), west(←), north(↑), east(→) // 각도 범위별 매핑 테이블 (사진 기준) const getDirectionText = (angle, dir) => { // 각도를 정규화 (-180 ~ 180 범위로) let normalizedAngle = Number(angle) while (normalizedAngle > 180) normalizedAngle -= 360 while (normalizedAngle < -180) normalizedAngle += 360 // 매핑 테이블: { south(↓), west(←), north(↑), east(→) } // 각도 0: 남, 서, 북, 동 // 각도 45: 남서, 북서, 북동, 남동 // 각도 90: 서, 북, 동, 남 // 각도 135: 북서, 북동, 남동, 남서 // 각도 180: 북, 동, 남, 서 // 각도 -45: 남동, 남서, 북서, 북동 // 각도 -90: 동, 남, 서, 북 // 각도 -135: 북동, 남동, 남서, 북서 let mapping // 정확한 각도 먼저 체크 if (normalizedAngle === 0) { mapping = { south: '南', west: '西', north: '北', east: '東' } } else if (normalizedAngle === 45) { mapping = { south: '南西', west: '北西', north: '北東', east: '南東' } } else if (normalizedAngle === 90) { mapping = { south: '西', west: '北', north: '東', east: '南' } } else if (normalizedAngle === 135) { mapping = { south: '北西', west: '北東', north: '南東', east: '南西' } } else if (normalizedAngle === 180 || normalizedAngle === -180) { mapping = { south: '北', west: '東', north: '南', east: '西' } } else if (normalizedAngle === -45) { mapping = { south: '南東', west: '南西', north: '北西', east: '北東' } } else if (normalizedAngle === -90) { mapping = { south: '東', west: '南', north: '西', east: '北' } } else if (normalizedAngle === -135) { mapping = { south: '北東', west: '南東', north: '南西', east: '北西' } } // 범위 각도 체크 else if (normalizedAngle >= 1 && normalizedAngle <= 44) { // 1~44: 남남서, 서북서, 북북동, 동남동 mapping = { south: '南南西', west: '西北西', north: '北北東', east: '東南東' } } else if (normalizedAngle >= 46 && normalizedAngle <= 89) { // 46~89: 서남서, 북북서, 동북동, 남남동 mapping = { south: '西南西', west: '北北西', north: '東北東', east: '南南東' } } else if (normalizedAngle >= 91 && normalizedAngle <= 134) { // 91~134: 서북서, 북북동, 동남동, 남남서 mapping = { south: '西北西', west: '北北東', north: '東南東', east: '南南西' } } else if (normalizedAngle >= 136 && normalizedAngle <= 179) { // 136~179: 북북서, 동북동, 남남동, 서남서 mapping = { south: '北北西', west: '東北東', north: '南南東', east: '西南西' } } else if (normalizedAngle >= -44 && normalizedAngle <= -1) { // -1~-44: 남남동, 서남서, 북북서, 동북동 mapping = { south: '南南東', west: '西南西', north: '北北西', east: '東北東' } } else if (normalizedAngle >= -89 && normalizedAngle <= -46) { // -46~-89: 동남동, 남남서, 서북서, 북북동 mapping = { south: '東南東', west: '南南西', north: '西北西', east: '北北東' } } else if (normalizedAngle >= -134 && normalizedAngle <= -91) { // -91~-134: 동북동, 남남동, 서남서, 북북서 mapping = { south: '東北東', west: '南南東', north: '西南西', east: '北北西' } } else if (normalizedAngle >= -179 && normalizedAngle <= -136) { // -136~-179: 북북동, 동남동, 남남서, 서북서 mapping = { south: '北北東', west: '東南東', north: '南南西', east: '西北西' } } else { // 기본값: 0도 mapping = { south: '南', west: '西', north: '北', east: '東' } } return mapping[dir] || '南' } text = getDirectionText(moduleCompass, direction) // surfaceCompass가 있으면 text를 덮어쓰기 (기존 로직 유지) if (surfaceCompass !== null && surfaceCompass !== undefined) { if ([0].includes(surfaceCompass)) { text = '南' } else if ([15, 30].includes(surfaceCompass)) { text = '南南東' } else if ([45].includes(surfaceCompass)) { text = '南東' } else if ([60, 75].includes(surfaceCompass)) { text = '東南東' } else if ([90].includes(surfaceCompass)) { text = '東' } else if ([105, 120].includes(surfaceCompass)) { text = '東北東' } else if ([135].includes(surfaceCompass)) { text = '北東' } else if ([150, 165].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 }) const auxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine') 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] // polygonLines와 innerLines의 겹침을 확인하고 type을 변경하는 함수 const checkLineOverlap = (line1, line2) => { // 두 선분이 같은 직선 위에 있는지 확인 const isOnSameLine = (l1, l2) => { // 수직선인 경우 (x1 == x2) if (Math.abs(l1.x1 - l1.x2) < 1 && Math.abs(l2.x1 - l2.x2) < 1) { return Math.abs(l1.x1 - l2.x1) < 1 } // 수평선인 경우 (y1 == y2) if (Math.abs(l1.y1 - l1.y2) < 1 && Math.abs(l2.y1 - l2.y2) < 1) { return Math.abs(l1.y1 - l2.y1) < 1 } // 대각선인 경우는 기울기가 같은지 확인 const slope1 = (l1.y2 - l1.y1) / (l1.x2 - l1.x1) const slope2 = (l2.y2 - l2.y1) / (l2.x2 - l2.x1) const intercept1 = l1.y1 - slope1 * l1.x1 const intercept2 = l2.y1 - slope2 * l2.x1 return Math.abs(slope1 - slope2) < 0.01 && Math.abs(intercept1 - intercept2) < 1 } if (!isOnSameLine(line1, line2)) { return false } // 선분들이 같은 직선 위에 있다면 겹치는 부분이 있는지 확인 const getLineRange = (line) => { if (Math.abs(line.x1 - line.x2) < 1) { // 수직선: y 범위 확인 return { min: Math.min(line.y1, line.y2), max: Math.max(line.y1, line.y2), } } else { // 수평선 또는 대각선: x 범위 확인 return { min: Math.min(line.x1, line.x2), max: Math.max(line.x1, line.x2), } } } const range1 = getLineRange(line1) const range2 = getLineRange(line2) // 겹치는 부분이 있는지 확인 return !(range1.max < range2.min || range2.max < range1.min) } polygonLines.forEach((line) => { line.need = true }) // 순서에 의존하지 않도록 모든 조합을 먼저 확인한 후 처리 const innerLineMapping = new Map() // innerLine -> polygonLine 매핑 저장 // innerLines와 polygonLines의 겹침을 확인하고 type 변경 innerLines.forEach((innerLine) => { polygonLines.forEach((polygonLine) => { if (polygonLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { return } if (checkLineOverlap(innerLine, polygonLine)) { // innerLine의 type을 polygonLine의 type으로 변경 if (innerLine.attributes && polygonLine.attributes.type) { // innerLine이 polygonLine보다 긴 경우 polygonLine.need를 false로 변경 if (polygonLine.length < innerLine.length) { if (polygonLine.lineName !== 'eaveHelpLine' || polygonLine.lineName !== 'eaveHelpLine') { polygonLine.need = false } } // innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize // innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize // innerLine.attributes.type = polygonLine.attributes.type // innerLine.direction = polygonLine.direction // innerLine.attributes.isStart = true // innerLine.parentLine = polygonLine // 매핑된 innerLine의 attributes를 변경 (교차점 계산 전에 적용) innerLineMapping.forEach((polygonLine, innerLine) => { innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize innerLine.attributes.type = polygonLine.attributes.type innerLine.direction = polygonLine.direction innerLine.attributes.isStart = true innerLine.parentLine = polygonLine }) } } }) }) const roofs = [] polygonLines = polygonLines.filter((line) => line.need) //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과 innerLines에서 startPoint, endPoint가 같은 라인을 innerLines에서 제거하고 canvas에서도 제거 const linesToRemove = [] innerLines = innerLines.filter((innerLine) => { const shouldRemove = polygonLines.some((polygonLine) => { return ( (isSamePoint(innerLine.startPoint, polygonLine.startPoint) && isSamePoint(innerLine.endPoint, polygonLine.endPoint)) || (isSamePoint(innerLine.startPoint, polygonLine.endPoint) && isSamePoint(innerLine.endPoint, polygonLine.startPoint)) ) }) if (shouldRemove) { linesToRemove.push(innerLine) } return !shouldRemove }) // 중복된 라인들을 canvas에서 제거 linesToRemove.forEach((line) => { canvas.remove(line) }) // innerLines가 합쳐졌을 때 polygonLine과 같은 경우 그 polygonLine의 need를 false로 변경 const mergeOverlappingInnerLines = (lines) => { const mergedLines = [] const processed = new Set() lines.forEach((line, index) => { if (processed.has(index)) return let currentLine = { ...line } processed.add(index) // 현재 라인과 겹치는 다른 라인들을 찾아서 합치기 for (let i = index + 1; i < lines.length; i++) { if (processed.has(i)) continue const otherLine = lines[i] if (checkLineOverlap(currentLine, otherLine)) { // 두 라인을 합치기 - 가장 긴 범위로 확장 const isVertical = Math.abs(currentLine.x1 - currentLine.x2) < 1 if (isVertical) { const allYPoints = [currentLine.y1, currentLine.y2, otherLine.y1, otherLine.y2] currentLine.y1 = Math.min(...allYPoints) currentLine.y2 = Math.max(...allYPoints) currentLine.x1 = currentLine.x2 = (currentLine.x1 + otherLine.x1) / 2 } else { const allXPoints = [currentLine.x1, currentLine.x2, otherLine.x1, otherLine.x2] currentLine.x1 = Math.min(...allXPoints) currentLine.x2 = Math.max(...allXPoints) currentLine.y1 = currentLine.y2 = (currentLine.y1 + otherLine.y1) / 2 } processed.add(i) } } mergedLines.push(currentLine) }) return mergedLines } const mergedInnerLines = mergeOverlappingInnerLines(innerLines) // 합쳐진 innerLine과 동일한 polygonLine의 need를 false로 설정 polygonLines.forEach((polygonLine) => { mergedInnerLines.forEach((mergedInnerLine) => { const isSameLine = (isSamePoint(polygonLine.startPoint, mergedInnerLine.startPoint) && isSamePoint(polygonLine.endPoint, mergedInnerLine.endPoint)) || (isSamePoint(polygonLine.startPoint, mergedInnerLine.endPoint) && isSamePoint(polygonLine.endPoint, mergedInnerLine.startPoint)) if (isSameLine) { polygonLine.need = false } }) }) canvas.renderAll() /*polygonLines.forEach((line) => { line.set({ strokeWidth: 10 }) canvas.add(line) }) canvas.renderAll()*/ polygonLines = polygonLines.filter((line) => line.need) 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 (checkLineOverlap(line, innerLine)) { return } 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', }) // 두 라인 중 큰 길이로 통일 const length1 = Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10 const length2 = Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10 const maxLength = Math.max(length1, length2) const unifiedPlaneSize = line.attributes.planeSize ?? maxLength const unifiedActualSize = line.attributes.actualSize ?? maxLength newLine1.attributes = { ...line.attributes, planeSize: unifiedPlaneSize, actualSize: unifiedActualSize, } newLine1.length = maxLength newLine2.attributes = { ...line.attributes, planeSize: unifiedPlaneSize, actualSize: unifiedActualSize, } newLine2.length = maxLength 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', }) const calcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 newLine.attributes = { ...line.attributes, planeSize: line.attributes.planeSize ?? calcLength, actualSize: line.attributes.actualSize ?? calcLength, } newLine.length = line.attributes.planeSize ?? calcLength 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', }) const lastCalcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 newLine.attributes = { ...line.attributes, planeSize: line.attributes.planeSize ?? lastCalcLength, actualSize: line.attributes.actualSize ?? lastCalcLength, } newLine.length = line.attributes.planeSize ?? lastCalcLength 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] polygonLines.forEach((polygonLine) => { polygonLine.attributes = { ...polygonLine.attributes, isStart: true } }) let allLines = [...polygonLines, ...innerLines] // allLines를 전부 돌면서 교차점이 있는 경우 그 line을 잘라서 allLines에 추가 const processIntersections = () => { const linesToProcess = canvas.getObjects().filter((obj) => obj.type === 'QLine' && obj.name === 'auxiliaryLine' && obj.visible) const newDividedLines = [] const processedLines = new Set() for (let i = 0; i < linesToProcess.length; i++) { for (let j = i + 1; j < linesToProcess.length; j++) { const line1 = linesToProcess[i] const line2 = linesToProcess[j] // 이미 처리된 line들은 건너뛰기 if (processedLines.has(line1) || processedLines.has(line2)) continue // 같은 line이거나 이미 연결된 line은 건너뛰기 if (line1 === line2) continue if ( isSamePoint({ x: line1.x1, y: line1.y1 }, { x: line2.x1, y: line2.y1 }) || isSamePoint({ x: line1.x1, y: line1.y1 }, { x: line2.x2, y: line2.y2 }) || isSamePoint({ x: line1.x2, y: line1.y2 }, { x: line2.x1, y: line2.y1 }) || isSamePoint({ x: line1.x2, y: line1.y2 }, { x: line2.x2, y: line2.y2 }) ) { continue } const intersectionPoint = calculateIntersection(line1, line2) if (intersectionPoint) { // line1에 교차점 추가 if (!line1.intersectionPoints) line1.intersectionPoints = [] line1.intersectionPoints.push(intersectionPoint) // line2에 교차점 추가 if (!line2.intersectionPoints) line2.intersectionPoints = [] line2.intersectionPoints.push(intersectionPoint) } } } // 교차점이 있는 line들을 분할 linesToProcess.forEach((line) => { if (line.intersectionPoints && line.intersectionPoints.length > 0) { // 교차점들을 line의 시작점에서부터의 거리순으로 정렬 const sortedPoints = line.intersectionPoints.sort((a, b) => { const distA = Math.hypot(a.x - line.x1, a.y - line.y1) const distB = Math.hypot(b.x - line.x1, b.y - line.y1) return distA - distB }) let currentPoint = { x: line.x1, y: line.y1 } // 각 교차점까지의 line segment 생성 sortedPoints.forEach((intersectionPoint) => { if (!isSamePoint(currentPoint, intersectionPoint)) { const newLine = new QLine([currentPoint.x, currentPoint.y, intersectionPoint.x, intersectionPoint.y], { stroke: line.stroke, strokeWidth: line.strokeWidth, fontSize: line.fontSize, attributes: { ...line.attributes }, name: line.name, visible: line.visible, }) // startPoint와 endPoint 설정 if (newLine.x1 < newLine.x2) { newLine.startPoint = { x: newLine.x1, y: newLine.y1 } newLine.endPoint = { x: newLine.x2, y: newLine.y2 } } else if (newLine.x1 > newLine.x2) { newLine.startPoint = { x: newLine.x2, y: newLine.y2 } newLine.endPoint = { x: newLine.x1, y: newLine.y1 } } else { if (newLine.y1 < newLine.y2) { newLine.startPoint = { x: newLine.x1, y: newLine.y1 } newLine.endPoint = { x: newLine.x2, y: newLine.y2 } } else { newLine.startPoint = { x: newLine.x2, y: newLine.y2 } newLine.endPoint = { x: newLine.x1, y: newLine.y1 } } } newDividedLines.push(newLine) } currentPoint = intersectionPoint }) // 마지막 교차점에서 line 끝점까지의 segment const endPoint = { x: line.x2, y: line.y2 } if (!isSamePoint(currentPoint, endPoint)) { const newLine = new QLine([currentPoint.x, currentPoint.y, endPoint.x, endPoint.y], { stroke: line.stroke, strokeWidth: line.strokeWidth, fontSize: line.fontSize, attributes: { ...line.attributes }, name: line.name, visible: line.visible, }) // startPoint와 endPoint 설정 if (newLine.x1 < newLine.x2) { newLine.startPoint = { x: newLine.x1, y: newLine.y1 } newLine.endPoint = { x: newLine.x2, y: newLine.y2 } } else if (newLine.x1 > newLine.x2) { newLine.startPoint = { x: newLine.x2, y: newLine.y2 } newLine.endPoint = { x: newLine.x1, y: newLine.y1 } } else { if (newLine.y1 < newLine.y2) { newLine.startPoint = { x: newLine.x1, y: newLine.y1 } newLine.endPoint = { x: newLine.x2, y: newLine.y2 } } else { newLine.startPoint = { x: newLine.x2, y: newLine.y2 } newLine.endPoint = { x: newLine.x1, y: newLine.y1 } } } newDividedLines.push(newLine) } processedLines.add(line) } }) // allLines 업데이트: 분할된 line들 제거하고 새 line들 추가 allLines = allLines.filter((line) => !processedLines.has(line)) allLines = [...allLines, ...newDividedLines] } // 교차점 처리 실행 processIntersections() /*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) }) // 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)) || (isSamePoint(l.startPoint, line.endPoint) && isSamePoint(l.endPoint, line.startPoint)) ) }) ) }) allLines = allLines.filter((line) => { return Math.abs(line.startPoint.x - line.endPoint.x) > 2 || Math.abs(line.startPoint.y - line.endPoint.y) > 2 }) // 나눠서 중복 제거된 roof return let newRoofs = getSplitRoofsPoints(allLines) const createdRoofs = [] newRoofs = newRoofs.filter((roof) => roof.length !== 0) newRoofs.forEach((roofPoint, index) => { let defense, pitch let representLines = [] let representLine // 지붕을 그리면서 기존 polygon의 line중 연결된 line을 찾는다. ;[...polygonLines, ...innerLines].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 || line.attributes.type === LINE_TYPE.WALLLINE.EAVE_HELP_LINE) ) { representLines.push(line) } else if (!representLines.includes(line) && line.attributes.type === LINE_TYPE.WALLLINE.HIPANDGABLE) { representLines.push(line) } } }) // representLines가 없다면 A,B타입중 하나임 if (representLines.length === 0) { // 1. roofPoint로 폴리곤의 라인들을 생성 const roofPolygonLines = [] for (let i = 0; i < roofPoint.length; i++) { const nextIndex = (i + 1) % roofPoint.length const startPt = roofPoint[i] const endPt = roofPoint[nextIndex] roofPolygonLines.push({ x1: startPt.x, y1: startPt.y, x2: endPt.x, y2: endPt.y, startPoint: startPt, endPoint: endPt, }) } // 3. 평행 여부 확인 함수 const checkParallel = (line1, line2) => { const v1x = line1.x2 - line1.x1 const v1y = line1.y2 - line1.y1 const v2x = line2.x2 - line2.x1 const v2y = line2.y2 - line2.y1 const length1 = Math.sqrt(v1x ** 2 + v1y ** 2) const length2 = Math.sqrt(v2x ** 2 + v2y ** 2) if (length1 === 0 || length2 === 0) return false const norm1x = v1x / length1 const norm1y = v1y / length1 const norm2x = v2x / length2 const norm2y = v2y / length2 const EPSILON = 0.01 const crossProduct = Math.abs(norm1x * norm2y - norm1y * norm2x) const dotProduct = norm1x * norm2x + norm1y * norm2y return crossProduct < EPSILON || Math.abs(Math.abs(dotProduct) - 1) < EPSILON } // 4. 점에서 라인까지의 거리 계산 함수 const getDistanceFromPointToLine = (point, lineP1, lineP2) => { const A = point.x - lineP1.x const B = point.y - lineP1.y const C = lineP2.x - lineP1.x const D = lineP2.y - lineP1.y const dot = A * C + B * D const lenSq = C * C + D * D let param = -1 if (lenSq !== 0) { param = dot / lenSq } let xx, yy if (param < 0) { xx = lineP1.x yy = lineP1.y } else if (param > 1) { xx = lineP2.x yy = lineP2.y } else { xx = lineP1.x + param * C yy = lineP1.y + param * D } const dx = point.x - xx const dy = point.y - yy return Math.sqrt(dx * dx + dy * dy) } // 5. 두 평행한 라인 사이의 거리 계산 (한 라인의 중점에서 다른 라인까지의 거리) const getDistanceBetweenParallelLines = (line1, line2) => { const midPoint = { x: (line1.x1 + line1.x2) / 2, y: (line1.y1 + line1.y2) / 2, } return getDistanceFromPointToLine(midPoint, { x: line2.x1, y: line2.y1 }, { x: line2.x2, y: line2.y2 }) } // 6. roofPolygonLines의 모든 라인에서 평행하면서 가장 가까운 EAVES 라인 찾기 let closestLine = null let minDistance = Infinity roofPolygonLines.forEach((roofLine) => { ;[...polygonLines, ...innerLines].forEach((line) => { // EAVES 타입만 필터링 if (line.attributes?.type !== LINE_TYPE.WALLLINE.EAVES && line.attributes?.type !== LINE_TYPE.WALLLINE.EAVE_HELP_LINE) { return } const lineObj = { x1: line.startPoint.x, y1: line.startPoint.y, x2: line.endPoint.x, y2: line.endPoint.y, } if (checkParallel(roofLine, lineObj)) { const distance = getDistanceBetweenParallelLines(roofLine, lineObj) if (distance < minDistance && distance > 0) { minDistance = distance closestLine = line } } }) }) if (closestLine) { representLines.push(closestLine) } } // representLines중 가장 긴 line을 찾는다. representLines.forEach((line) => { if (!representLine) { representLine = line } else { if (getLengthByLine(representLine) < getLengthByLine(line)) { representLine = line } } }) if (!representLine) { 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 ?? representLine?.attributes?.pitch ?? globalPitch 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, from: 'roofCover', 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) => { //console.log("::::::::::",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) createdRoofs.push(roof) canvas.remove(polygon) canvas.renderAll() }) //지붕 완료 후 보조선을 전부 제거한다. auxiliaryLines.forEach((line) => { canvas.remove(line) }) createdRoofs.forEach((roof) => { canvas.add(roof) }) canvas.renderAll() canvas.discardActiveObject() } const getSplitRoofsPoints = (allLines) => { // 모든 좌표점들을 수집 const allPoints = [] allLines.forEach((line, lineIndex) => { allPoints.push({ point: line.startPoint, lineIndex, pointType: 'start' }) allPoints.push({ point: line.endPoint, lineIndex, pointType: 'end' }) }) // X 좌표 통일 for (let i = 0; i < allPoints.length; i++) { for (let j = i + 1; j < allPoints.length; j++) { const point1 = allPoints[i].point const point2 = allPoints[j].point if (Math.abs(point1.x - point2.x) < 1) { const maxX = Math.max(point1.x, point2.x) point1.x = maxX point2.x = maxX } } } // Y 좌표 통일 for (let i = 0; i < allPoints.length; i++) { for (let j = i + 1; j < allPoints.length; j++) { const point1 = allPoints[i].point const point2 = allPoints[j].point if (Math.abs(point1.y - point2.y) < 1) { const maxY = Math.max(point1.y, point2.y) point1.y = maxY point2.y = maxY } } } // ==== Utility functions ==== function isSamePoint(p1, p2, epsilon = 1) { return Math.abs(p1.x - p2.x) <= epsilon && Math.abs(p1.y - p2.y) <= epsilon } 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 findShortestPath(start, end, graph, epsilon = 1) { const startKey = pointToKey(start, epsilon) const endKey = pointToKey(end, epsilon) // 거리와 이전 노드 추적 const distances = { [startKey]: 0 } const previous = {} const visited = new Set() // 우선순위 큐 (거리가 짧은 순으로 정렬) const queue = [{ key: startKey, dist: 0 }] // 모든 노드 초기화 for (const key in graph) { if (key !== startKey) { distances[key] = Infinity } } // 우선순위 큐에서 다음 노드 선택 const getNextNode = () => { if (queue.length === 0) return null queue.sort((a, b) => a.dist - b.dist) return queue.shift() } let current while ((current = getNextNode())) { const currentKey = current.key // 목적지에 도달하면 종료 if (currentKey === endKey) break // 이미 방문한 노드는 건너뜀 if (visited.has(currentKey)) continue visited.add(currentKey) // 인접 노드 탐색 for (const neighbor of graph[currentKey] || []) { const neighborKey = pointToKey(neighbor.point, epsilon) if (visited.has(neighborKey)) continue const alt = distances[currentKey] + neighbor.distance // 더 짧은 경로를 찾은 경우 업데이트 if (alt < (distances[neighborKey] || Infinity)) { distances[neighborKey] = alt previous[neighborKey] = currentKey // 우선순위 큐에 추가 queue.push({ key: neighborKey, dist: alt }) } } } // 경로 재구성 const path = [] let currentKey = endKey // 시작점에 도달할 때까지 역추적 while (previous[currentKey] !== undefined) { const [x, y] = currentKey.split(',').map(Number) path.unshift({ x, y }) currentKey = previous[currentKey] } // 시작점 추가 if (path.length > 0) { const [sx, sy] = startKey.split(',').map(Number) path.unshift({ x: sx, y: sy }) } return path.length > 0 ? path : null } // 최종 함수 function getPath(start, end, graph, epsilon = 1) { // startPoint와 arrivalPoint가 될 수 있는 점은 line.attributes.type이 'default' 혹은 null이 아닌 line인 경우에만 가능 const isValidPoint = (point) => { return allLines.some((line) => { const isOnLine = isSamePoint(line.startPoint, point, epsilon) || isSamePoint(line.endPoint, point, epsilon) const hasValidType = line.attributes?.type && line.attributes.type !== 'default' return isOnLine && hasValidType }) } if (!isValidPoint(start) || !isValidPoint(end)) { console.log('시작점 또는 도착점이 유효하지 않음. 무시.') return [] } 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 [] } // 사용된 노드들을 graph에서 제거 if (path.length > 0) { path.forEach((point) => { const pointKey = pointToKey(point, epsilon) delete graph[pointKey] // 다른 노드들의 연결에서도 이 노드를 제거 Object.keys(graph).forEach((key) => { graph[key] = graph[key].filter((neighbor) => !isSamePoint(neighbor.point, point, epsilon)) }) }) } return path } const roofs = [] const remainingLines = [...allLines] // 사용 가능한 line들의 복사본 // isStart가 true인 line들만 시작점으로 사용 const startLines = remainingLines.filter((line) => line.attributes?.isStart === true) startLines.forEach((startLine) => { // 현재 남아있는 line들로 그래프 생성 const graph = {} for (const line of remainingLines.filter((line2) => line2 !== startLine)) { const p1 = line.startPoint const p2 = line.endPoint const key1 = pointToKey(p1) const key2 = pointToKey(p2) const distance = calcDistance(p1, p2) const isStartLine = line.attributes?.isStart === true if (!graph[key1]) graph[key1] = [] if (!graph[key2]) graph[key2] = [] // isStart가 아닌 line을 우선하도록 distance 조정 const adjustedDistance = isStartLine ? distance + 1000 : distance graph[key1].push({ point: p2, distance: adjustedDistance, line }) graph[key2].push({ point: p1, distance: adjustedDistance, line }) } const startPoint = { ...startLine.startPoint } // 시작점 let arrivalPoint = { ...startLine.endPoint } // 도착점 const roof = getPath(startPoint, arrivalPoint, graph) if (roof.length > 0) { roofs.push(roof) // 사용된 startLine을 remainingLines에서 제거 const startLineIndex = remainingLines.findIndex((line) => line === startLine) if (startLineIndex !== -1) { remainingLines.splice(startLineIndex, 1) } } }) return removeDuplicatePolygons( roofs.filter((roof) => roof.length < 100), allLines.some((line) => line.name === 'auxiliaryLine'), ) } 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() } /** * 폴리곤의 라인 속성을 복도치수, 실제치수에 따라 actualSize 설정 * @param polygon */ const setPolygonLinesActualSize = (polygon) => { if (!polygon.lines || polygon.lines.length === 0 || !polygon.roofMaterial) { return } // createdRoofs들의 모든 lines를 확인해서 length값이 1이하인 차이가 있으면 통일 시킨다. const allRoofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) const allRoofLines = allRoofs.flatMap((roof) => roof.lines) for (let i = 0; i < allRoofLines.length; i++) { for (let j = i + 1; j < allRoofLines.length; j++) { const line1 = allRoofLines[i] const line2 = allRoofLines[j] const diff = Math.abs(line1.length - line2.length) if (diff > 0 && diff <= 2) { const maxLength = Math.max(line1.length, line2.length) line1.setLengthByValue(maxLength * 10) line2.setLengthByValue(maxLength * 10) // attributes도 통일 const maxPlaneSize = Math.max(line1.attributes.planeSize || 0, line2.attributes.planeSize || 0) const maxActualSize = Math.max(line1.attributes.actualSize || 0, line2.attributes.actualSize || 0) line1.attributes.planeSize = maxPlaneSize line1.attributes.actualSize = maxActualSize line2.attributes.planeSize = maxPlaneSize line2.attributes.actualSize = maxActualSize } } } polygon.lines.forEach((line, index) => { if (line.attributes.isCalculated) { return } //text 와 planSize 및 actualSize가 안맞는 문제 /*const nextText = polygon?.texts?.[index]?.text const nextPlaneSize = Number(nextText) if (nextText != null && nextText !== '' && Number.isFinite(nextPlaneSize)) { if (line.attributes.actualSize !== nextPlaneSize && line.attributes.planeSize !== nextPlaneSize) { line.attributes.planeSize = nextPlaneSize } }*/ setActualSize(line, polygon.direction, +polygon.roofMaterial?.pitch) }) addLengthText(polygon) } return { addPolygon, addPolygonByLines, removePolygon, drawDirectionArrow, addLengthText, splitPolygonWithLines, splitPolygonWithSeparate, setPolygonLinesActualSize, } }