diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 816f4447..bad9bd69 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 @@ -510,11 +511,7 @@ export const drawGableRoof = (roofId, canvas, textMode) => { const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) - /** baseLine을 기준으로 확인용 polygon 작성 */ - const checkWallPolygon = new QPolygon(baseLinePoints, {}) - const eavesLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES) - const gableLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.GABLE) const ridgeLines = [] const innerLines = [] @@ -625,6 +622,7 @@ export const drawGableRoof = (roofId, canvas, textMode) => { isDiagonal, directionVector: { x: dx / length, y: dy / length }, roofLine: currentRoof.roofLine, + roofVector, } } @@ -794,73 +792,6 @@ export const drawGableRoof = (roofId, canvas, textMode) => { return { forwardLines, backwardLines } } - /** - * 라인의 지붕 면을 찾는다. - * @param currentLine - * @returns {*} - */ - const findCurrentRoof = (currentLine) => { - const analyze = analyzeEavesLine(currentLine) - const originPoint = currentLine.attributes.originPoint - const midX = (originPoint.x1 + originPoint.x2) / 2 - const midY = (originPoint.y1 + originPoint.y2) / 2 - const offset = currentLine.attributes.offset - - let currentRoof - let roofFindVector = { x: 0, y: 0 } - const checkRoofLines = roof.lines.filter((roof) => { - const dx = Big(roof.x2).minus(Big(roof.x1)).toNumber() - const dy = Big(roof.y2).minus(Big(roof.y1)).toNumber() - const length = Math.sqrt(dx * dx + dy * dy) - const directionVector = { x: dx / length, y: dy / length } - return analyze.directionVector.x === directionVector.x && analyze.directionVector.y === directionVector.y - }) - - if (analyze.isHorizontal) { - const checkPoint = { x: midX, y: midY + offset } - if (wall.inPolygon(checkPoint)) { - roofFindVector = { x: 0, y: -1 } - } else { - roofFindVector = { x: 0, y: 1 } - } - } - if (analyze.isVertical) { - const checkPoint = { x: midX + offset, y: midY } - if (wall.inPolygon(checkPoint)) { - roofFindVector = { x: -1, y: 0 } - } else { - roofFindVector = { x: 1, y: 0 } - } - } - const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofFindVector.x * offset, y: midY + roofFindVector.y * offset } } - const edgeDx = Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() - const edgeDy = 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)).toNumber() - const intersectDy = 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 }) - } - } - }) - - currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect)) - if (!currentRoof) { - currentRoof = intersectRoofLines.sort((a, b) => a.length - b.length)[0] - } - - return currentRoof - } - /** * 지점 사이의 길이와 지점별 각도에 따라서 교점까지의 길이를 구한다. * @param distance @@ -879,7 +810,59 @@ export const drawGableRoof = (roofId, canvas, textMode) => { 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 @@ -894,39 +877,62 @@ export const drawGableRoof = (roofId, canvas, textMode) => { const y1 = analyze.startPoint.y const y2 = analyze.endPoint.y - const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], { - stroke: 'red', - strokeWidth: 4, - parentId: roofId, - name: 'check', - }) - canvas.add(checkLine) - canvas.renderAll() + 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 = [] - backwardLines.forEach((backward) => { + 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) - - 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) - } + return ( + (analyze.isHorizontal && Math.abs(currentX1 - backX1) < 1 && Math.abs(currentX2 - backX2) < 1) || + (analyze.isVertical && Math.abs(currentY1 - backY1) < 1 && Math.abs(currentY2 - backY2) < 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 @@ -976,7 +982,8 @@ export const drawGableRoof = (roofId, canvas, textMode) => { ridgePoint.push({ x: pointX, y: y2 }) } } - ridgeLines.push(drawRidgeLine([ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roof, textMode)) + 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)) }) canvas @@ -986,96 +993,408 @@ export const drawGableRoof = (roofId, canvas, textMode) => { canvas.renderAll() }) + /** + * 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 - const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], { - stroke: 'red', - strokeWidth: 4, - parentId: roofId, - name: 'check', - }) - canvas.add(checkLine) - canvas.renderAll() - const innerRidgeLines = [] - if (analyze.isHorizontal) { - ridgeLines - .filter((ridgeLine) => { - const tolerance = 1 - const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber() - const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber() - const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 - return normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance - }) - .filter((ridgeLine) => { - const minX = Math.min(analyze.startPoint.x, analyze.endPoint.x) - const maxX = Math.max(analyze.startPoint.x, analyze.endPoint.x) - const ridgeLineX = (ridgeLine.x1 + ridgeLine.x2) / 2 - return ridgeLineX >= minX && ridgeLineX <= maxX - }) - .forEach((ridgeLine) => innerRidgeLines.push(ridgeLine)) - } - if (analyze.isVertical) { - ridgeLines - .filter((ridgeLine) => { - const tolerance = 1 - const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber() - const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber() - const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 - return Math.abs(normalizedAngle - 90) <= tolerance - }) - .filter((ridgeLine) => { - const minY = Math.min(analyze.startPoint.y, analyze.endPoint.y) - const maxY = Math.max(analyze.startPoint.y, analyze.endPoint.y) - const ridgeLineY = (ridgeLine.y1 + ridgeLine.y2) / 2 - return ridgeLineY >= minY && ridgeLineY <= maxY - }) - .forEach((ridgeLine) => innerRidgeLines.push(ridgeLine)) - } + // 현재라인 안쪽의 마루를 filter하여 계산에 사용. + const innerRidgeLines = findInnerRidge( + { x1: analyze.startPoint.x, y1: analyze.startPoint.y, x2: analyze.endPoint.x, y2: analyze.endPoint.y }, + analyze.roofVector, + ridgeLines, + 1, + ) - innerRidgeLines.forEach((ridgeLine) => { - const checkLine = new fabric.Line([ridgeLine.x1, ridgeLine.y1, ridgeLine.x2, ridgeLine.y2], { - stroke: 'red', - strokeWidth: 4, - parentId: roofId, - name: 'check', + // 안쪽의 마루가 있는 경우 마루의 연결 포인트를 설정 + 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 }) + } }) - canvas.add(checkLine) - canvas.renderAll() - }) - if (innerRidgeLines.length === 1) { - const innerRidgeLine = innerRidgeLines[0] - if (analyze.isHorizontal) { + // 최소지점, 최대지점에 연결되는 지붕선 + 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 (analyze.isVertical) { + 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 { + //다른방향 처리 + drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree) + } + } else if (analyze.isVertical) { + //현재라인이 수직선일때 + if (isVertical) { + //같은방향 처리 + drawRoofLine(points, canvas, roof, textMode) + } else { + //다른방향 처리 + drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree) + } + } + }) } - - canvas - .getObjects() - .filter((object) => object.name === 'check') - .forEach((object) => canvas.remove(object)) - canvas.renderAll() } forwardLines.forEach((forward) => { - const analyze = forward.analyze drawRoofPlane(forward) }) backwardLines.forEach((backward) => { - const analyze = backward.analyze drawRoofPlane(backward) }) - canvas - .getObjects() - .filter((object) => object.name === 'check') - .forEach((object) => canvas.remove(object)) - canvas.renderAll() + roof.innerLines.push(...ridgeLines, ...innerLines) } /** @@ -8919,3 +9238,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 +}