diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index bfc283c7..d712b3f1 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -2,7 +2,7 @@ import { fabric } from 'fabric' import { v4 as uuidv4 } from 'uuid' import { QLine } from '@/components/fabric/QLine' import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util' -import { calculateAngle, drawRidgeRoof, drawShedRoof, toGeoJSON } from '@/util/qpolygon-utils' +import { calculateAngle, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON } from '@/util/qpolygon-utils' import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' @@ -255,6 +255,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { // && obj.name !== 'outerLinePoint', ) .forEach((obj) => this.canvas.remove(obj)) + this.innerLines = [] this.canvas.renderAll() let textMode = 'plane' @@ -274,50 +275,75 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { break } - const types = [] - this.lines.forEach((line) => types.push(line.attributes.type)) + const types = this.lines.map((line) => line.attributes.type) - const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] + const isGableRoof = function (types) { + if (!types.includes(LINE_TYPE.WALLLINE.GABLE)) { + return false + } + const gableTypes = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] + const oddTypes = types.filter((type, i) => i % 2 === 0) + const evenTypes = types.filter((type, i) => i % 2 === 1) - const hasShed = types.includes(LINE_TYPE.WALLLINE.SHED) + const oddAllEaves = oddTypes.every((type) => type === LINE_TYPE.WALLLINE.EAVES) + const evenAllGable = evenTypes.every((type) => gableTypes.includes(type)) + const evenAllEaves = evenTypes.every((type) => type === LINE_TYPE.WALLLINE.EAVES) + const oddAllGable = oddTypes.every((type) => gableTypes.includes(type)) - if (hasShed) { - const sheds = this.lines.filter((line) => line.attributes !== undefined && line.attributes.type === LINE_TYPE.WALLLINE.SHED) - const areLinesParallel = function (line1, line2) { - const angle1 = calculateAngle(line1.startPoint, line1.endPoint) - const angle2 = calculateAngle(line2.startPoint, line2.endPoint) - return angle1 === angle2 + return (oddAllEaves && evenAllGable) || (evenAllEaves && oddAllGable) + } + + const isShedRoof = function (types, lines) { + const gableTypes = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] + if (!types.includes(LINE_TYPE.WALLLINE.SHED)) { + return false + } + const shedLines = lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.SHED) + const areShedLinesParallel = function (shedLines) { + return shedLines.every((shed, i) => { + const nextShed = shedLines[(i + 1) % shedLines.length] + const angle1 = calculateAngle(shed.startPoint, shed.endPoint) + const angle2 = calculateAngle(nextShed.startPoint, nextShed.endPoint) + return angle1 === angle2 + }) + } + if (!areShedLinesParallel(shedLines)) { + return false } - let isShedRoof = true - sheds.forEach((shed, i) => { - isShedRoof = areLinesParallel(shed, sheds[(i + 1) % sheds.length]) - }) - if (isShedRoof) { - const eaves = this.lines - .filter((line) => line.attributes !== undefined && line.attributes.type === LINE_TYPE.WALLLINE.EAVES) - .filter((line) => { - const angle1 = calculateAngle(sheds[0].startPoint, sheds[0].endPoint) - const angle2 = calculateAngle(line.startPoint, line.endPoint) - if (Math.abs(angle1 - angle2) === 180) { - return line - } - }) - if (eaves.length > 0) { - const gables = this.lines.filter((line) => sheds.includes(line) === false && eaves.includes(line) === false) - const isGable = gables.every((line) => gableType.includes(line.attributes.type)) - if (isGable) { - drawShedRoof(this.id, this.canvas, textMode) - } else { - drawRidgeRoof(this.id, this.canvas, textMode) - } - } else { - drawRidgeRoof(this.id, this.canvas, textMode) - } - } else { - drawRidgeRoof(this.id, this.canvas, textMode) + const getParallelEavesLines = function (shedLines, lines) { + const eavesLines = lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.EAVES) + + const referenceAngle = calculateAngle(shedLines[0].startPoint, shedLines[0].endPoint) + + return eavesLines.filter((line) => { + const eavesAngle = calculateAngle(line.startPoint, line.endPoint) + return Math.abs(referenceAngle - eavesAngle) === 180 + }) } + + const parallelEaves = getParallelEavesLines(shedLines, lines) + if (parallelEaves.length === 0) { + return false + } + + const remainingLines = lines.filter((line) => !shedLines.includes(line) && !parallelEaves.includes(line)) + return remainingLines.every((line) => gableTypes.includes(line.attributes.type)) + } + + if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) { + // 용마루 -- straight-skeleton + console.log('용마루 지붕') + drawRidgeRoof(this.id, this.canvas, textMode) + } else if (isGableRoof(types)) { + // A형, B형 박공 지붕 + console.log('패턴 지붕') + drawGableRoof(this.id, this.canvas, textMode) + } else if (isShedRoof(types, this.lines)) { + console.log('한쪽흐름 지붕') + drawShedRoof(this.id, this.canvas, textMode) } else { + console.log('변별로 설정') drawRidgeRoof(this.id, this.canvas, textMode) } }, diff --git a/src/components/simulator/Simulator.jsx b/src/components/simulator/Simulator.jsx index d59eabe1..5d6a7e3d 100644 --- a/src/components/simulator/Simulator.jsx +++ b/src/components/simulator/Simulator.jsx @@ -49,34 +49,25 @@ export default function Simulator() { return isNaN(num) ? 0 : num }), - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)', - 'rgba(0, 99, 132, 0.2)', - 'rgba(0, 162, 235, 0.2)', - 'rgba(0, 206, 86, 0.2)', - 'rgba(0, 192, 192, 0.2)', - 'rgba(0, 102, 255, 0.2)', - 'rgba(0, 159, 64, 0.2)', - ], - borderColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)', - 'rgba(0, 99, 132, 0.2)', - 'rgba(0, 162, 235, 0.2)', - 'rgba(0, 206, 86, 0.2)', - 'rgba(0, 192, 192, 0.2)', - 'rgba(0, 102, 255, 0.2)', - 'rgba(0, 159, 64, 0.2)', - ], + backgroundColor: (context) => { + const chart = context.chart + const { ctx, chartArea } = chart + + if (!chartArea) { + // This case happens on initial chart load + return null + } + + const gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top) + gradient.addColorStop(0, '#4FC3F7') // Light blue at bottom + gradient.addColorStop(0.3, '#2FA8E0') // Original blue + gradient.addColorStop(0.7, '#1976D2') // Medium blue + gradient.addColorStop(1, '#0D47A1') // Dark blue at top + + + return gradient + }, + borderColor: '#2FA8E0' , borderWidth: 1, }, ], diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index e1fa2650..7d1d326a 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -411,6 +411,35 @@ export function useMovementSetting(id) { targetBaseLines.sort((a, b) => a.distance - b.distance) targetBaseLines = targetBaseLines.filter((line) => line.distance === targetBaseLines[0].distance) + + if (isGableRoof) { + const zeroLengthLines = targetBaseLines.filter( + (line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) < 1, + ) + if (zeroLengthLines.length > 0) { + zeroLengthLines.forEach((line) => { + const findLine = line.line + const findCoords = [ + { x: findLine.x1, y: findLine.y1 }, + { x: findLine.x2, y: findLine.y2 }, + ] + wall.baseLines + .filter((baseLine) => { + return findCoords.some( + (coord) => + (Math.abs(coord.x - baseLine.x1) < 0.1 && Math.abs(coord.y - baseLine.y1) < 0.1) || + (Math.abs(coord.x - baseLine.x2) < 0.1 && Math.abs(coord.y - baseLine.y2) < 0.1), + ) + }) + .forEach((baseLine) => { + const isAlready = targetBaseLines.find((target) => target.line === baseLine) + if (isAlready) return + targetBaseLines.push({ line: baseLine, distance: targetBaseLines[0].distance }) + }) + }) + } + } + let value if (typeRef.current === TYPE.FLOW_LINE) { value = @@ -445,58 +474,56 @@ export function useMovementSetting(id) { } } value = value.div(10) - targetBaseLines.forEach((target) => { - const currentLine = target.line - const index = baseLines.findIndex((line) => line === currentLine) - const nextLine = baseLines[(index + 1) % baseLines.length] - const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] - let deltaX = 0 - let deltaY = 0 - if (currentLine.y1 === currentLine.y2) { - deltaY = value.toNumber() - } else { - deltaX = value.toNumber() - } + targetBaseLines + .filter((line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) >= 1) + .forEach((target) => { + const currentLine = target.line + const index = baseLines.findIndex((line) => line === currentLine) + const nextLine = baseLines[(index + 1) % baseLines.length] + const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] + let deltaX = 0 + let deltaY = 0 + if (currentLine.y1 === currentLine.y2) { + deltaY = value.toNumber() + } else { + deltaX = value.toNumber() + } - currentLine.set({ - x1: currentLine.x1 + deltaX, - y1: currentLine.y1 + deltaY, - x2: currentLine.x2 + deltaX, - y2: currentLine.y2 + deltaY, - startPoint: { x: currentLine.x1 + deltaX, y: currentLine.y1 + deltaY }, - endPoint: { x: currentLine.x2 + deltaX, y: currentLine.y2 + deltaY }, - }) - const currentSize = calcLinePlaneSize({ - x1: currentLine.x1, - y1: currentLine.y1, - x2: currentLine.x2, - y2: currentLine.y2, - }) - currentLine.attributes.planeSize = currentSize - currentLine.attributes.actualSize = currentSize + currentLine.set({ + x1: currentLine.x1 + deltaX, + y1: currentLine.y1 + deltaY, + x2: currentLine.x2 + deltaX, + y2: currentLine.y2 + deltaY, + startPoint: { x: currentLine.x1 + deltaX, y: currentLine.y1 + deltaY }, + endPoint: { x: currentLine.x2 + deltaX, y: currentLine.y2 + deltaY }, + }) + const currentSize = calcLinePlaneSize({ + x1: currentLine.x1, + y1: currentLine.y1, + x2: currentLine.x2, + y2: currentLine.y2, + }) + currentLine.attributes.planeSize = currentSize + currentLine.attributes.actualSize = currentSize - const nextOldActualSize = nextLine.attributes.planeSize - nextLine.set({ - x1: nextLine.x1 + deltaX, - y1: nextLine.y1 + deltaY, - startPoint: { x: nextLine.x1 + deltaX, y: nextLine.y1 + deltaY }, - }) - const nextSize = calcLinePlaneSize({ x1: nextLine.x1, y1: nextLine.y1, x2: nextLine.x2, y2: nextLine.y2 }) - nextLine.attributes.planeSize = nextSize - nextLine.attributes.actualSize = nextSize + nextLine.set({ + x1: currentLine.x2, + y1: currentLine.y2, + startPoint: { x: currentLine.x2, y: currentLine.y2 }, + }) + const nextSize = calcLinePlaneSize({ x1: nextLine.x1, y1: nextLine.y1, x2: nextLine.x2, y2: nextLine.y2 }) + nextLine.attributes.planeSize = nextSize + nextLine.attributes.actualSize = nextSize - const prevOldActualSize = prevLine.attributes.planeSize - prevLine.set({ - x2: prevLine.x2 + deltaX, - y2: prevLine.y2 + deltaY, - endPoint: { x: prevLine.x2 + deltaX, y: prevLine.y2 + deltaY }, + prevLine.set({ + x2: currentLine.x1, + y2: currentLine.y1, + endPoint: { x: currentLine.x1, y: currentLine.y1 }, + }) + const prevSize = calcLinePlaneSize({ x1: prevLine.x1, y1: prevLine.y1, x2: prevLine.x2, y2: prevLine.y2 }) + prevLine.attributes.planeSize = prevSize + prevLine.attributes.actualSize = prevSize }) - const prevSize = calcLinePlaneSize({ x1: prevLine.x1, y1: prevLine.y1, x2: prevLine.x2, y2: prevLine.y2 }) - prevLine.attributes.planeSize = prevSize - prevLine.attributes.actualSize = prevSize - - canvas.renderAll() - }) roof.drawHelpLine() initEvent() diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 482693dc..15b9e569 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -6,6 +6,7 @@ import { QPolygon } from '@/components/fabric/QPolygon' import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' +import { canvas } from 'framer-motion/m' const TWO_PI = Math.PI * 2 @@ -489,6 +490,921 @@ export const isSamePoint = (a, b) => { return Math.abs(Math.round(a.x) - Math.round(b.x)) <= 2 && Math.abs(Math.round(a.y) - Math.round(b.y)) <= 2 } +/** + * 용마루 지붕 + * @param roofId + * @param canvas + * @param textMode + */ +export const drawEavesRoof = (roofId, canvas, textMode) => {} + +/** + * 박공지붕(A,B 패턴) + * @param roofId + * @param canvas + * @param textMode + */ +export const drawGableRoof = (roofId, canvas, textMode) => { + const roof = canvas?.getObjects().find((object) => object.id === roofId) + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + + const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) + const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) + + const eavesLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES) + + const ridgeLines = [] + const innerLines = [] + + /** + * 처마 라인 속성 판단 + * @param line + * @returns {{startPoint: {x, y}, endPoint: {x, y}, length: number, angleDegree: number, normalizedAngle: number, isHorizontal: boolean, isVertical: boolean, isDiagonal: boolean, directionVector: {x: number, y: number}, roofLine}} + */ + const analyzeEavesLine = (line) => { + const tolerance = 1 + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const length = Math.sqrt(dx * dx + dy * dy) + const angleDegree = (Math.atan2(dy, dx) * 180) / Math.PI + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + const directionVector = { x: dx / length, y: dy / length } + let isHorizontal = false, + isVertical = false, + isDiagonal = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } else { + isDiagonal = true + } + + const originPoint = line.attributes.originPoint + const midX = (originPoint.x1 + originPoint.x2) / 2 + const midY = (originPoint.y1 + originPoint.y2) / 2 + const offset = line.attributes.offset + + const checkRoofLines = roof.lines.filter((roof) => { + const roofDx = Big(roof.x2).minus(Big(roof.x1)).toNumber() + const roofDy = Big(roof.y2).minus(Big(roof.y1)).toNumber() + const roofLength = Math.sqrt(roofDx * roofDx + roofDy * roofDy) + const roofVector = { x: roofDx / roofLength, y: roofDy / roofLength } + return directionVector.x === roofVector.x && directionVector.y === roofVector.y + }) + + let roofVector = { x: 0, y: 0 } + if (isHorizontal) { + const checkPoint = { x: midX, y: midY + offset } + if (wall.inPolygon(checkPoint)) { + roofVector = { x: 0, y: -1 } + } else { + roofVector = { x: 0, y: 1 } + } + } + if (isVertical) { + const checkPoint = { x: midX + offset, y: midY } + if (wall.inPolygon(checkPoint)) { + roofVector = { x: -1, y: 0 } + } else { + roofVector = { x: 1, y: 0 } + } + } + + const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofVector.x * offset, y: midY + roofVector.y * offset } } + const edgeDx = + Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 + ? 0 + : Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() + const edgeDy = + Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 + ? 0 + : Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).toNumber() + const edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy) + const edgeVector = { x: edgeDx / edgeLength, y: edgeDy / edgeLength } + + const intersectRoofLines = [] + checkRoofLines.forEach((roofLine) => { + const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } + const intersect = edgesIntersection(lineEdge, findEdge) + if (intersect) { + const intersectDx = + Big(intersect.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 ? 0 : Big(intersect.x).minus(Big(findEdge.vertex1.x)).toNumber() + const intersectDy = + Big(intersect.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 ? 0 : Big(intersect.y).minus(Big(findEdge.vertex1.y)).toNumber() + const intersectLength = Math.sqrt(intersectDx * intersectDx + intersectDy * intersectDy) + const intersectVector = { x: intersectDx / intersectLength, y: intersectDy / intersectLength } + if (edgeVector.x === intersectVector.x && edgeVector.y === intersectVector.y) { + intersectRoofLines.push({ roofLine, intersect, length: intersectLength }) + } + } + }) + + intersectRoofLines.sort((a, b) => a.length - b.length) + let currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect) && roof.length - offset < 0.1) + if (!currentRoof) { + currentRoof = intersectRoofLines[0] + } + + let startPoint, endPoint + if (isHorizontal) { + startPoint = { x: Math.min(line.x1, line.x2, currentRoof.roofLine.x1, currentRoof.roofLine.x2), y: line.y1 } + endPoint = { x: Math.max(line.x1, line.x2, currentRoof.roofLine.x1, currentRoof.roofLine.x2), y: line.y2 } + } + if (isVertical) { + startPoint = { x: line.x1, y: Math.min(line.y1, line.y2, currentRoof.roofLine.y1, currentRoof.roofLine.y2) } + endPoint = { x: line.x2, y: Math.max(line.y1, line.y2, currentRoof.roofLine.y1, currentRoof.roofLine.y2) } + } + if (isDiagonal) { + startPoint = { x: line.x1, y: line.y1 } + endPoint = { x: line.x2, y: line.y2 } + } + + return { + startPoint, + endPoint, + length, + angleDegree, + normalizedAngle, + isHorizontal, + isVertical, + isDiagonal, + directionVector: { x: dx / length, y: dy / length }, + roofLine: currentRoof.roofLine, + roofVector, + } + } + + const isOverlapLine = (currAnalyze, checkAnalyze) => { + // 허용 오차 + const tolerance = 1 + + // 같은 방향인지 확인 + if ( + currAnalyze.isHorizontal !== checkAnalyze.isHorizontal || + currAnalyze.isVertical !== checkAnalyze.isVertical || + currAnalyze.isDiagonal !== checkAnalyze.isDiagonal + ) { + return false + } + + if (currAnalyze.isHorizontal && !(Math.abs(currAnalyze.startPoint.y - checkAnalyze.startPoint.y) < tolerance)) { + // 수평선: y좌표가 다른경우 false + return false + } else if (currAnalyze.isVertical && !(Math.abs(currAnalyze.startPoint.x - checkAnalyze.startPoint.x) < tolerance)) { + // 수직선: x좌표가 다른경우 false + return false + } + + // 3. 선분 구간 겹침 확인 + let range1, range2 + + if (currAnalyze.isHorizontal) { + // 수평선: x 범위로 비교 + range1 = { + min: Math.min(currAnalyze.startPoint.x, currAnalyze.endPoint.x), + max: Math.max(currAnalyze.startPoint.x, currAnalyze.endPoint.x), + } + range2 = { + min: Math.min(checkAnalyze.startPoint.x, checkAnalyze.endPoint.x), + max: Math.max(checkAnalyze.startPoint.x, checkAnalyze.endPoint.x), + } + } else { + // 수직선: y 범위로 비교 + range1 = { + min: Math.min(currAnalyze.startPoint.y, currAnalyze.endPoint.y), + max: Math.max(currAnalyze.startPoint.y, currAnalyze.endPoint.y), + } + range2 = { + min: Math.min(checkAnalyze.startPoint.y, checkAnalyze.endPoint.y), + max: Math.max(checkAnalyze.startPoint.y, checkAnalyze.endPoint.y), + } + } + // 구간 겹침 확인 + const overlapStart = Math.max(range1.min, range2.min) + const overlapEnd = Math.min(range1.max, range2.max) + + return Math.max(0, overlapEnd - overlapStart) > 0 + } + + /** + * 전체 처마 라인의 속성 확인 및 정리 + * @param lines + * @returns {{forwardLines: Array, backwardLines: Array}} + */ + const analyzeAllEavesLines = (lines) => { + let forwardLines = [] + let backwardLines = [] + lines.forEach((line) => { + const analyze = analyzeEavesLine(line) + if (analyze.isHorizontal) { + if (analyze.directionVector.x > 0) { + const overlapLines = forwardLines.filter((forwardLine) => forwardLine !== line && isOverlapLine(analyze, forwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: Math.min(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.startPoint.y, + } + const endPoint = { + x: Math.max(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.endPoint.y, + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + forwardLines = forwardLines.filter((forwardLine) => forwardLine !== overlap) + }) + } + forwardLines.push({ eaves: line, analyze }) + } + if (analyze.directionVector.x < 0) { + const overlapLines = backwardLines.filter((backwardLine) => backwardLine !== line && isOverlapLine(analyze, backwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: Math.min(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.startPoint.y, + } + const endPoint = { + x: Math.max(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.endPoint.y, + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + backwardLines = backwardLines.filter((backwardLine) => backwardLine !== overlap) + }) + } + backwardLines.push({ eaves: line, analyze }) + } + } + if (analyze.isVertical) { + if (analyze.directionVector.y > 0) { + const overlapLines = forwardLines.filter((forwardLine) => forwardLine !== line && isOverlapLine(analyze, forwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: analyze.startPoint.x, + y: Math.min(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + const endPoint = { + x: analyze.endPoint.x, + y: Math.max(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + forwardLines = forwardLines.filter((forwardLine) => forwardLine !== overlap) + }) + } + forwardLines.push({ eaves: line, analyze }) + } + if (analyze.directionVector.y < 0) { + const overlapLines = backwardLines.filter((backwardLine) => backwardLine !== line && isOverlapLine(analyze, backwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: analyze.startPoint.x, + y: Math.min(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + const endPoint = { + x: analyze.endPoint.x, + y: Math.max(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + backwardLines = backwardLines.filter((backwardLine) => backwardLine !== overlap) + }) + } + backwardLines.push({ eaves: line, analyze }) + } + } + }) + + forwardLines.sort((a, b) => { + if (a.analyze.isHorizontal && b.analyze.isHorizontal) { + return a.analyze.startPoint.x - b.analyze.startPoint.x + } else if (a.analyze.isVertical && b.analyze.isVertical) { + return a.analyze.startPoint.y - b.analyze.startPoint.y + } + }) + backwardLines.sort((a, b) => { + if (a.analyze.isHorizontal && b.analyze.isHorizontal) { + return a.analyze.startPoint.x - b.analyze.startPoint.x + } else if (a.analyze.isVertical && b.analyze.isVertical) { + return a.analyze.startPoint.y - b.analyze.startPoint.y + } + }) + + return { forwardLines, backwardLines } + } + + /** + * 지점 사이의 길이와 지점별 각도에 따라서 교점까지의 길이를 구한다. + * @param distance + * @param angleA + * @param angleB + * @returns {{a: number, b: number}} + */ + const calculateIntersection = (distance, angleA, angleB) => { + // A에서 B방향으로의 각도 (0도가 B방향) + const tanA = Math.tan((angleA * Math.PI) / 180) + // B에서 A방향으로의 각도 (180도가 A방향, 실제 계산에서는 180-angleB) + const tanB = Math.tan(((180 - angleB) * Math.PI) / 180) + + const a = Math.round(((distance * tanB) / (tanB - tanA)) * 10) / 10 + const b = Math.round(Math.abs(distance - a) * 10) / 10 + return { a, b } + } + + /** + * polygon에 line이 겹쳐지는 구간을 확인. + * @param polygon + * @param linePoints + * @returns + */ + const findPloygonLineOverlap = (polygon, linePoints) => { + const polygonPoints = polygon.points + const checkLine = { + x1: linePoints[0], + y1: linePoints[1], + x2: linePoints[2], + y2: linePoints[3], + } + let startPoint = { x: checkLine.x1, y: checkLine.y1 } + let endPoint = { x: checkLine.x2, y: checkLine.y2 } + const lineEdge = { vertex1: startPoint, vertex2: endPoint } + + const checkPolygon = new QPolygon(polygonPoints, {}) + + const isStartInside = checkPolygon.inPolygon(startPoint) + const isEndInside = checkPolygon.inPolygon(endPoint) + + const intersections = [] + + checkPolygon.lines.forEach((line) => { + const edge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const intersect = edgesIntersection(edge, lineEdge) + if ( + intersect && + isPointOnLineNew(line, intersect) && + isPointOnLineNew(checkLine, intersect) && + !(Math.abs(startPoint.x - intersect.x) < 1 && Math.abs(startPoint.y - intersect.y) < 1) && + !(Math.abs(endPoint.x - intersect.x) < 1 && Math.abs(endPoint.y - intersect.y) < 1) + ) { + intersections.push({ intersect, dist: Math.sqrt(Math.pow(startPoint.x - intersect.x, 2) + Math.pow(startPoint.y - intersect.y, 2)) }) + } + }) + + if (intersections.length > 0) { + intersections.sort((a, b) => a.dist - b.dist) + let i = 0 + if (!isStartInside) { + startPoint = { x: intersections[0].intersect.x, y: intersections[0].intersect.y } + i++ + } + endPoint = { x: intersections[i].intersect.x, y: intersections[i].intersect.y } + } + return [startPoint.x, startPoint.y, endPoint.x, endPoint.y] + } + + const { forwardLines, backwardLines } = analyzeAllEavesLines(eavesLines) + + forwardLines.forEach((current) => { + const currentLine = current.eaves + const analyze = current.analyze + const currentDegree = getDegreeByChon(currentLine.attributes.pitch) + const currentX1 = Math.min(currentLine.x1, currentLine.x2) + const currentX2 = Math.max(currentLine.x1, currentLine.x2) + const currentY1 = Math.min(currentLine.y1, currentLine.y2) + const currentY2 = Math.max(currentLine.y1, currentLine.y2) + + const x1 = analyze.startPoint.x + const x2 = analyze.endPoint.x + const y1 = analyze.startPoint.y + const y2 = analyze.endPoint.y + + const lineVector = { x: 0, y: 0 } + if (analyze.isHorizontal) { + lineVector.x = Math.sign(analyze.roofLine.y1 - currentLine.attributes.originPoint.y1) + } + if (analyze.isVertical) { + lineVector.y = Math.sign(analyze.roofLine.x1 - currentLine.attributes.originPoint.x1) + } + + const overlapLines = [] + const findBackWardLines = backwardLines.filter((backward) => { + const backwardVector = { x: 0, y: 0 } + if (analyze.isHorizontal) { + backwardVector.x = Math.sign(analyze.roofLine.y1 - backward.analyze.startPoint.y) + } + if (analyze.isVertical) { + backwardVector.y = Math.sign(analyze.roofLine.x1 - backward.analyze.startPoint.x) + } + return backwardVector.x === lineVector.x && backwardVector.y === lineVector.y + }) + const backWardLine = findBackWardLines.find((backward) => { + const backX1 = Math.min(backward.eaves.x1, backward.eaves.x2) + const backX2 = Math.max(backward.eaves.x1, backward.eaves.x2) + const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2) + const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2) + return ( + (analyze.isHorizontal && Math.abs(currentX1 - backX1) < 0.1 && Math.abs(currentX2 - backX2) < 0.1) || + (analyze.isVertical && Math.abs(currentY1 - backY1) < 0.1 && Math.abs(currentY2 - backY2) < 0.1) + ) + }) + if (backWardLine) { + overlapLines.push(backWardLine) + } else { + findBackWardLines.forEach((backward) => { + const backX1 = Math.min(backward.eaves.x1, backward.eaves.x2) + const backX2 = Math.max(backward.eaves.x1, backward.eaves.x2) + const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2) + const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2) + + if ( + analyze.isHorizontal && + ((currentX1 <= backX1 && currentX2 >= backX1) || + (currentX1 <= backX2 && currentX2 >= backX2) || + (backX1 < currentX1 && backX1 < currentX2 && backX2 > currentX1 && backX2 > currentX2)) + ) { + overlapLines.push(backward) + } + if ( + analyze.isVertical && + ((currentY1 <= backY1 && currentY2 >= backY1) || + (currentY1 <= backY2 && currentY2 >= backY2) || + (backY1 < currentY1 && backY1 < currentY2 && backY2 > currentY1 && backY2 > currentY2)) + ) { + overlapLines.push(backward) + } + }) + } + + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const overlapDegree = getDegreeByChon(overlap.eaves.attributes.pitch) + const ridgePoint = [] + if (analyze.isHorizontal) { + const overlapX1 = Math.min(overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x) + const overlapX2 = Math.max(overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x) + + // 각 라인 사이의 길이를 구해서 각도에 대한 중간 지점으로 마루를 생성. + const currentMidY = (currentLine.y1 + currentLine.y2) / 2 + const overlapMidY = (overlap.eaves.y1 + overlap.eaves.y2) / 2 + const distance = calculateIntersection(Math.abs(currentMidY - overlapMidY), currentDegree, overlapDegree) + const vectorToOverlap = Math.sign(overlap.eaves.y1 - currentLine.y1) + const pointY = currentLine.y1 + vectorToOverlap * distance.a + + if (x1 <= overlapX1 && overlapX1 <= x2) { + ridgePoint.push({ x: overlapX1, y: pointY }) + } else { + ridgePoint.push({ x: x1, y: pointY }) + } + if (x1 <= overlapX2 && overlapX2 <= x2) { + ridgePoint.push({ x: overlapX2, y: pointY }) + } else { + ridgePoint.push({ x: x2, y: pointY }) + } + } + if (analyze.isVertical) { + const overlapY1 = Math.min(overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y) + const overlapY2 = (overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y) + + // 각 라인 사이의 길이를 구해서 각도에 대한 중간 지점으로 마루를 생성. + const currentMidX = (currentLine.x1 + currentLine.x2) / 2 + const overlapMidX = (overlap.eaves.x1 + overlap.eaves.x2) / 2 + const distance = calculateIntersection(Math.abs(currentMidX - overlapMidX), currentDegree, overlapDegree) + const vectorToOverlap = Math.sign(overlap.eaves.x1 - currentLine.x1) + const pointX = currentLine.x1 + vectorToOverlap * distance.a + + if (y1 <= overlapY1 && overlapY1 <= y2) { + ridgePoint.push({ x: pointX, y: overlapY1 }) + } else { + ridgePoint.push({ x: pointX, y: y1 }) + } + if (y1 <= overlapY2 && overlapY2 <= y2) { + ridgePoint.push({ x: pointX, y: overlapY2 }) + } else { + ridgePoint.push({ x: pointX, y: y2 }) + } + } + const points = findPloygonLineOverlap(roof, [ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roofId) + ridgeLines.push(drawRidgeLine(points, canvas, roof, textMode)) + }) + }) + + /** + * currentLine {x1,y1}, {x2,y2} 좌표 안쪽에 있는 마루를 찾는다. + * @param currentLine + * @param roofVector + * @param lines + * @param tolerance + * @returns {*[]} + */ + const findInnerRidge = (currentLine, roofVector, lines, tolerance = 1) => { + const x1 = Math.min(currentLine.x1, currentLine.x2) + const y1 = Math.min(currentLine.y1, currentLine.y2) + const x2 = Math.max(currentLine.x1, currentLine.x2) + const y2 = Math.max(currentLine.y1, currentLine.y2) + const dx = Big(currentLine.x2).minus(Big(currentLine.x1)).toNumber() + const dy = Big(currentLine.y2).minus(Big(currentLine.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + let isHorizontal = false, + isVertical = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } + let innerRidgeLines = [] + if (isHorizontal) { + lines + .filter((line) => { + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + return normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance + }) + .filter((line) => { + const minX = Math.min(line.x1, line.x2) + const maxX = Math.max(line.x1, line.x2) + return x1 <= minX && maxX <= x2 && roofVector.y * -1 === Math.sign(line.y1 - y1) + }) + .forEach((line) => innerRidgeLines.push({ line, dist: Math.abs(currentLine.y1 - line.y1) })) + } + if (isVertical) { + lines + .filter((line) => { + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + return Math.abs(normalizedAngle - 90) <= tolerance + }) + .filter((line) => { + const minY = Math.min(line.y1, line.y2) + const maxY = Math.max(line.y1, line.y2) + return y1 <= minY && maxY <= y2 && roofVector.x * -1 === Math.sign(line.x1 - x1) + }) + .forEach((line) => innerRidgeLines.push({ line, dist: Math.abs(currentLine.x1 - line.x1) })) + } + innerRidgeLines.sort((a, b) => a.dist - b.dist) + + const ridge1 = innerRidgeLines.find((ridge) => { + if (isHorizontal) { + const minX = Math.min(ridge.line.x1, ridge.line.x2) + return Math.abs(x1 - minX) <= 1 + } + if (isVertical) { + const minY = Math.min(ridge.line.y1, ridge.line.y2) + return Math.abs(y1 - minY) <= 1 + } + }) + + const ridge2 = innerRidgeLines.find((ridge) => { + if (isHorizontal) { + const maxX = Math.max(ridge.line.x1, ridge.line.x2) + return Math.abs(x2 - maxX) <= 1 + } + if (isVertical) { + const maxY = Math.max(ridge.line.y1, ridge.line.y2) + return Math.abs(y2 - maxY) <= 1 + } + }) + if (ridge1 === ridge2) { + innerRidgeLines = [ridge1] + } else { + if (ridge1 && ridge2) { + let range1, range2 + if (isHorizontal) { + // 수평선: x 범위로 비교 + range1 = { + min: Math.min(ridge1.line.x1, ridge1.line.x2), + max: Math.max(ridge1.line.x1, ridge1.line.x2), + } + range2 = { + min: Math.min(ridge2.line.x1, ridge2.line.x2), + max: Math.max(ridge2.line.x1, ridge2.line.x2), + } + } else { + // 수직선: y 범위로 비교 + range1 = { + min: Math.min(ridge1.line.y1, ridge1.line.y2), + max: Math.max(ridge1.line.y1, ridge1.line.y2), + } + range2 = { + min: Math.min(ridge2.line.y1, ridge2.line.y2), + max: Math.max(ridge2.line.y1, ridge2.line.y2), + } + } + // 구간 겹침 확인 + const overlapStart = Math.max(range1.min, range2.min) + const overlapEnd = Math.min(range1.max, range2.max) + if (Math.max(0, overlapEnd - overlapStart) > 0) { + innerRidgeLines = [ridge1, ridge2] + } + } + } + return innerRidgeLines + } + + /** + * 지붕면을 그린다. + * @param currentLine + */ + const drawRoofPlane = (currentLine) => { + const currentDegree = getDegreeByChon(currentLine.eaves.attributes.pitch) + const analyze = currentLine.analyze + + // 현재라인 안쪽의 마루를 filter하여 계산에 사용. + const innerRidgeLines = findInnerRidge( + { x1: analyze.startPoint.x, y1: analyze.startPoint.y, x2: analyze.endPoint.x, y2: analyze.endPoint.y }, + analyze.roofVector, + ridgeLines, + 1, + ) + + // 안쪽의 마루가 있는 경우 마루의 연결 포인트를 설정 + if (innerRidgeLines.length > 0) { + const innerRidgePoints = innerRidgeLines + .map((innerRidgeLine) => [ + { x: innerRidgeLine.line.x1, y: innerRidgeLine.line.y1 }, + { x: innerRidgeLine.line.x2, y: innerRidgeLine.line.y2 }, + ]) + .flat() + const ridgeMinPoint = innerRidgePoints.reduce( + (min, curr) => (analyze.isHorizontal ? (curr.x < min.x ? curr : min) : curr.y < min.y ? curr : min), + innerRidgePoints[0], + ) + const ridgeMaxPoint = innerRidgePoints.reduce( + (max, curr) => (analyze.isHorizontal ? (curr.x > max.x ? curr : max) : curr.y > max.y ? curr : max), + innerRidgePoints[0], + ) + + const roofPlanePoint = [] + innerRidgeLines + .sort((a, b) => { + const line1 = a.line + const line2 = b.line + if (analyze.isHorizontal) { + return line1.x1 - line2.x1 + } + if (analyze.isVertical) { + return line1.y1 - line2.y1 + } + }) + .forEach((ridge, index) => { + const isFirst = index === 0 + const isLast = index === innerRidgeLines.length - 1 + const line = ridge.line + if (isFirst) { + roofPlanePoint.push({ x: line.x1, y: line.y1 }) + } + const nextRidge = innerRidgeLines[index + 1] + if (nextRidge) { + const nextLine = nextRidge.line + let range1, range2 + if (analyze.isHorizontal) { + // 수평선: x 범위로 비교 + range1 = { + min: Math.min(line.x1, line.x2), + max: Math.max(line.x1, line.x2), + } + range2 = { + min: Math.min(nextLine.x1, nextLine.x2), + max: Math.max(nextLine.x1, nextLine.x2), + } + // 겹쳐지는 구간 + const overlapX = [Math.max(range1.min, range2.min), Math.min(range1.max, range2.max)] + let linePoints = [ + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 }, + { x: nextLine.x1, y: nextLine.y1 }, + { x: nextLine.x2, y: nextLine.y2 }, + ].filter((point) => overlapX.includes(point.x)) + const firstPoint = + ridge.dist > nextRidge.dist + ? linePoints.find((point) => point.x === line.x1 || point.x === line.x2) + : linePoints.find((point) => point.x === nextLine.x1 || point.x === nextLine.x2) + const lastPoint = linePoints.find((point) => point !== firstPoint) + roofPlanePoint.push({ x: firstPoint.x, y: firstPoint.y }, { x: firstPoint.x, y: lastPoint.y }) + } else { + // 수직선: y 범위로 비교 + range1 = { + min: Math.min(line.y1, line.y2), + max: Math.max(line.y1, line.y2), + } + range2 = { + min: Math.min(nextLine.y1, nextLine.y2), + max: Math.max(nextLine.y1, nextLine.y2), + } + //겹쳐지는 구간 + const overlapY = [Math.max(range1.min, range2.min), Math.min(range1.max, range2.max)] + let linePoints = [ + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 }, + { x: nextLine.x1, y: nextLine.y1 }, + { x: nextLine.x2, y: nextLine.y2 }, + ].filter((point) => overlapY.includes(point.y)) + const firstPoint = + ridge.dist > nextRidge.dist + ? linePoints.find((point) => point.y === line.y1 || point.y === line.y2) + : linePoints.find((point) => point.y === nextLine.y1 || point.y === nextLine.y2) + const lastPoint = linePoints.find((point) => point !== firstPoint) + roofPlanePoint.push({ x: firstPoint.x, y: firstPoint.y }, { x: lastPoint.x, y: firstPoint.y }) + } + } + if (isLast) { + roofPlanePoint.push({ x: line.x2, y: line.y2 }) + } + }) + const maxDistRidge = innerRidgeLines.reduce((max, curr) => (curr.dist > max.dist ? curr : max), innerRidgeLines[0]) + // 지붕선에 맞닫는 포인트를 찾아서 지붕선의 모양에 따라 추가 한다. + let innerRoofLines = roof.lines + .filter((line) => { + //1.지붕선이 현재 라인의 안쪽에 있는지 판단 + const tolerance = 1 + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + let isHorizontal = false, + isVertical = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } + const minX = Math.min(line.x1, line.x2) + const maxX = Math.max(line.x1, line.x2) + const minY = Math.min(line.y1, line.y2) + const maxY = Math.max(line.y1, line.y2) + return ( + (analyze.isHorizontal && isHorizontal && analyze.startPoint.x <= minX && maxX <= analyze.endPoint.x) || + (analyze.isVertical && isVertical && analyze.startPoint.y <= minY && maxY <= analyze.endPoint.y) + ) + }) + .filter((line) => { + //2.지붕선이 현재 라인의 바깥에 있는지 확인. + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const length = Math.sqrt(dx * dx + dy * dy) + const lineVector = { x: 0, y: 0 } + if (analyze.isHorizontal) { + // lineVector.y = Math.sign(line.y1 - currentLine.eaves.attributes.originPoint.y1) + lineVector.y = Math.sign(line.y1 - maxDistRidge.line.y1) + } else if (analyze.isVertical) { + // lineVector.x = Math.sign(line.x1 - currentLine.eaves.attributes.originPoint.x1) + lineVector.x = Math.sign(line.x1 - maxDistRidge.line.x1) + } + return ( + analyze.roofVector.x === lineVector.x && + analyze.roofVector.y === lineVector.y && + analyze.directionVector.x === dx / length && + analyze.directionVector.y === dy / length + ) + }) + + // 패턴 방향에 따라 최소지점, 최대지점의 포인트에서 지붕선 방향에 만나는 포인트를 찾기위한 vector + const checkMinVector = { + vertex1: { x: ridgeMinPoint.x, y: ridgeMinPoint.y }, + vertex2: { x: ridgeMinPoint.x + analyze.roofVector.x, y: ridgeMinPoint.y + analyze.roofVector.y }, + } + const checkMaxVector = { + vertex1: { x: ridgeMaxPoint.x, y: ridgeMaxPoint.y }, + vertex2: { x: ridgeMaxPoint.x + analyze.roofVector.x, y: ridgeMaxPoint.y + analyze.roofVector.y }, + } + + // 최소, 최대 지점에 만나는 포인트들에 대한 정보 + const roofMinPoint = [], + roofMaxPoint = [] + + innerRoofLines.forEach((line) => { + const lineVector = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const minIntersect = edgesIntersection(checkMinVector, lineVector) + const maxIntersect = edgesIntersection(checkMaxVector, lineVector) + if (minIntersect) { + const distance = Math.abs( + analyze.isHorizontal ? ridgeMinPoint.x - Math.min(line.x1, line.x2) : ridgeMinPoint.y - Math.min(line.y1, line.y2), + ) + roofMinPoint.push({ x: minIntersect.x, y: minIntersect.y, isPointOnLine: isPointOnLineNew(line, minIntersect), distance, line }) + } + if (maxIntersect) { + const distance = Math.abs( + analyze.isHorizontal ? ridgeMaxPoint.x - Math.max(line.x1, line.x2) : ridgeMaxPoint.y - Math.max(line.y1, line.y2), + ) + roofMaxPoint.push({ x: maxIntersect.x, y: maxIntersect.y, isPointOnLine: isPointOnLineNew(line, maxIntersect), distance, line }) + } + }) + + // 최소지점, 최대지점에 연결되는 지붕선 + let minRoof = roofMinPoint.find((point) => point.isPointOnLine) + let maxRoof = roofMaxPoint.find((point) => point.isPointOnLine) + if (!minRoof) { + minRoof = roofMinPoint.sort((a, b) => a.distance - b.distance)[0] + } + if (!maxRoof) { + maxRoof = roofMaxPoint.sort((a, b) => a.distance - b.distance)[0] + } + + if (minRoof && maxRoof) { + // 1. 연결되는 지점의 포인트를 사용한다. + roofPlanePoint.push({ x: minRoof.x, y: minRoof.y }, { x: maxRoof.x, y: maxRoof.y }) + // 2. 최소지점, 최대지점에 연결되는 지붕선이 하나가 아닐경우 연결되는 지점외에 지붕선의 다른 포인트를 추가 해야 한다. + if (minRoof.line !== maxRoof.line) { + if (analyze.isHorizontal) { + Math.abs(minRoof.x - minRoof.line.x1) < Math.abs(minRoof.x - minRoof.line.x2) + ? roofPlanePoint.push({ x: minRoof.line.x2, y: minRoof.line.y2 }) + : roofPlanePoint.push({ x: minRoof.line.x1, y: minRoof.line.y1 }) + Math.abs(maxRoof.x - maxRoof.line.x1) < Math.abs(maxRoof.x - maxRoof.line.x2) + ? roofPlanePoint.push({ x: maxRoof.line.x2, y: maxRoof.line.y2 }) + : roofPlanePoint.push({ x: maxRoof.line.x1, y: maxRoof.line.y1 }) + } + if (analyze.isVertical) { + Math.abs(minRoof.y - minRoof.line.y1) < Math.abs(minRoof.y - minRoof.line.y2) + ? roofPlanePoint.push({ x: minRoof.line.x2, y: minRoof.line.y2 }) + : roofPlanePoint.push({ x: minRoof.line.x1, y: minRoof.line.y1 }) + Math.abs(maxRoof.y - maxRoof.line.y1) < Math.abs(maxRoof.y - maxRoof.line.y2) + ? roofPlanePoint.push({ x: maxRoof.line.x2, y: maxRoof.line.y2 }) + : roofPlanePoint.push({ x: maxRoof.line.x1, y: maxRoof.line.y1 }) + } + // 3.지붕선이 세개 이상일 경우 최소, 최대 연결지점의 지붕선을 제외한 나머지 지붕선은 모든 포인트를 사용한다. + const otherRoof = innerRoofLines.filter((line) => line !== minRoof.line && line !== maxRoof.line) + otherRoof.forEach((line) => { + roofPlanePoint.push({ x: line.x1, y: line.y1 }, { x: line.x2, y: line.y2 }) + }) + } + } + + //각 포인트들을 직교하도록 정렬 + const sortedPoints = getSortedOrthogonalPoints(roofPlanePoint) + sortedPoints.forEach((currPoint, index) => { + const nextPoint = sortedPoints[(index + 1) % sortedPoints.length] + const points = [currPoint.x, currPoint.y, nextPoint.x, nextPoint.y] + const isAlready = ridgeLines.find( + (ridge) => + (Math.abs(ridge.x1 - points[0]) < 1 && + Math.abs(ridge.y1 - points[1]) < 1 && + Math.abs(ridge.x2 - points[2]) < 1 && + Math.abs(ridge.y2 - points[3]) < 1) || + (Math.abs(ridge.x1 - points[2]) < 1 && + Math.abs(ridge.y1 - points[3]) < 1 && + Math.abs(ridge.x2 - points[0]) < 1 && + Math.abs(ridge.y2 - points[1]) < 1), + ) + if (isAlready) { + return true + } + + const tolerance = 1 + const dx = Big(points[2]).minus(Big(points[0])).toNumber() + const dy = Big(points[3]).minus(Big(points[1])).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + let isHorizontal = false, + isVertical = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } + if (analyze.isHorizontal) { + //현재라인이 수평선일때 + if (isHorizontal) { + //같은방향 처리 + innerLines.push(drawRoofLine(points, canvas, roof, textMode)) + } else { + //다른방향 처리 + innerLines.push(drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree)) + } + } else if (analyze.isVertical) { + //현재라인이 수직선일때 + if (isVertical) { + //같은방향 처리 + innerLines.push(drawRoofLine(points, canvas, roof, textMode)) + } else { + //다른방향 처리 + innerLines.push(drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree)) + } + } + }) + } + } + + forwardLines.forEach((forward) => { + drawRoofPlane(forward) + }) + backwardLines.forEach((backward) => { + drawRoofPlane(backward) + }) + roof.innerLines.push(...ridgeLines, ...innerLines) + canvas + .getObjects() + .filter((obj) => obj.name === 'check') + .forEach((obj) => canvas.remove(obj)) + canvas.renderAll() +} + /** * 한쪽흐름 지붕 * @param roofId @@ -8330,3 +9246,139 @@ const reCalculateSize = (line) => { ) return { planeSize, actualSize } } + +/** + * 직교 다각형(축에 평행한 변들로만 구성된 다각형)의 점들을 시계방향으로 정렬 + * @param points 정렬할 점들의 배열 + * @returns {[sortedPoints]} 정렬된 점들의 배열 + */ +const getSortedOrthogonalPoints = (points) => { + if (points.length < 3) return points + /** + * @param currentDirection 현재 방향 + * @param nextDirection 다음 방향 + * @param isClockWise 흐름 방향 + * @returns {boolean} 유효한 방향 전환인지 여부 + */ + const isValidNextDirection = (currentDirection, nextDirection, isClockWise = false) => { + if (!currentDirection) return true + + let validTransitions + if (!isClockWise) { + // 반 시계방향 진행 규칙 + validTransitions = { + right: ['up'], + down: ['right'], + left: ['down'], + up: ['left'], + } + } else { + // 시계방향 진행 규칙 + validTransitions = { + right: ['down'], + down: ['left'], + left: ['up'], + up: ['right'], + } + } + return !validTransitions[currentDirection] || validTransitions[currentDirection].includes(nextDirection) + } + + // 시작점: 왼쪽 위 점 (x가 최소이면서 y도 최소인 점) + const startPoint = points.reduce((min, point) => { + if (point.x < min.x || (point.x === min.x && point.y < min.y)) { + return point + } + return min + }) + + const sortedPoints = [startPoint] + const remainingPoints = points.filter((p) => p !== startPoint) + + let currentPoint = startPoint + let currentDirection = null + + while (remainingPoints.length > 0) { + let nextPoint = null + let nextDirection = null + let minDistance = Infinity + + // 현재 방향을 고려하여 다음 점 찾기 + for (const point of remainingPoints) { + const dx = point.x - currentPoint.x + const dy = point.y - currentPoint.y + + // 직교 다각형이므로 수직 또는 수평 방향만 고려 + if (Math.abs(dx) < 1e-10 && Math.abs(dy) > 1e-10) { + // 수직 이동 + const direction = dy > 0 ? 'down' : 'up' + const distance = Math.abs(dy) + + if (isValidNextDirection(currentDirection, direction) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } else if (Math.abs(dy) < 1e-10 && Math.abs(dx) > 1e-10) { + // 수평 이동 + const direction = dx > 0 ? 'right' : 'left' + const distance = Math.abs(dx) + + if (isValidNextDirection(currentDirection, direction) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } + } + + if (!nextPoint) { + for (const point of remainingPoints) { + const dx = point.x - currentPoint.x + const dy = point.y - currentPoint.y + + // 직교 다각형이므로 수직 또는 수평 방향만 고려 + if (Math.abs(dx) < 1e-10 && Math.abs(dy) > 1e-10) { + // 수직 이동 + const direction = dy > 0 ? 'down' : 'up' + const distance = Math.abs(dy) + + if (isValidNextDirection(currentDirection, direction, true) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } else if (Math.abs(dy) < 1e-10 && Math.abs(dx) > 1e-10) { + // 수평 이동 + const direction = dx > 0 ? 'right' : 'left' + const distance = Math.abs(dx) + + if (isValidNextDirection(currentDirection, direction, true) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } + } + } + if (nextPoint) { + sortedPoints.push(nextPoint) + remainingPoints.splice(remainingPoints.indexOf(nextPoint), 1) + currentPoint = nextPoint + currentDirection = nextDirection + } else { + // 다음 점을 찾을 수 없는 경우, 가장 가까운 점 선택 + const nearestPoint = remainingPoints.reduce((nearest, point) => { + const currentDist = Math.sqrt(Math.pow(point.x - currentPoint.x, 2) + Math.pow(point.y - currentPoint.y, 2)) + const nearestDist = Math.sqrt(Math.pow(nearest.x - currentPoint.x, 2) + Math.pow(nearest.y - currentPoint.y, 2)) + return currentDist < nearestDist ? point : nearest + }) + + sortedPoints.push(nearestPoint) + remainingPoints.splice(remainingPoints.indexOf(nearestPoint), 1) + currentPoint = nearestPoint + currentDirection = null + } + } + return sortedPoints +}