From d148077e6be1fa9716121044a2f845cbaf22490b Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 12 Nov 2025 10:40:57 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=80=EB=B3=84=EB=A1=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A4=91=EA=B0=84=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 1617 +++++++++++++++++++++++++++++++++++- 1 file changed, 1602 insertions(+), 15 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 33737856..3014e43e 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -6,9 +6,9 @@ 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 +const EPSILON = 1e-10 //좌표계산 시 최소 차이값 export const defineQPolygon = () => { fabric.QPolygon.fromObject = function (object, callback) { @@ -1492,6 +1492,1595 @@ export const drawShedRoof = (roofId, canvas, textMode) => { canvas.renderAll() } +const getInwardNormal = (v1, v2, isCCW) => { + const dx = v2.x - v1.x + const dy = v2.y - v1.y + const length = Math.sqrt(dx * dx + dy * dy) + + if (length === 0) return { x: 0, y: 0 } + + if (isCCW) { + return { x: -dy / length, y: dx / length } + } else { + return { x: dy / length, y: -dx / length } + } +} + +const isCounterClockwise = (vertices) => { + let sum = 0 + for (let i = 0; i < vertices.length; i++) { + const v1 = vertices[i] + const v2 = vertices[(i + 1) % vertices.length] + sum += (v2.x - v1.x) * (v2.y + v1.y) + } + return sum < 0 +} + +const isPointInPolygon = (point, polygon) => { + let inside = false + + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x, + yi = polygon[i].y + const xj = polygon[j].x, + yj = polygon[j].y + + const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi + + if (intersect) inside = !inside + } + + return inside +} + +const calculateAngleBisector = (prevVertex, currentVertex, nextVertex, polygonVertices) => { + const isCCW = isCounterClockwise(polygonVertices) + + // 이전 변의 내향 법선 + const norm1 = getInwardNormal(prevVertex, currentVertex, isCCW) + + // 다음 변의 내향 법선 + const norm2 = getInwardNormal(currentVertex, nextVertex, isCCW) + + // 이등분선 계산 + let bisectorX = norm1.x + norm2.x + let bisectorY = norm1.y + norm2.y + + const length = Math.sqrt(bisectorX * bisectorX + bisectorY * bisectorY) + + if (length < 1e-10) { + // 180도인 경우 + bisectorX = norm1.x + bisectorY = norm1.y + } else { + bisectorX /= length + bisectorY /= length + } + + const testPoint = { + x: currentVertex.x + bisectorX * 0.1, + y: currentVertex.y + bisectorY * 0.1, + } + + if (isPointInPolygon(testPoint, polygonVertices)) { + // 방향이 외부를 향하면 반전 + bisectorX = -bisectorX + bisectorY = -bisectorY + } + + return { x: bisectorX, y: bisectorY } +} + +const lineSegmentIntersection = (p1, p2, p3, p4) => { + const x1 = p1.x, + y1 = p1.y + const x2 = p2.x, + y2 = p2.y + const x3 = p3.x, + y3 = p3.y + const x4 = p4.x, + y4 = p4.y + + const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + + if (Math.abs(denom) < EPSILON) { + return null // 평행 + } + + const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom + const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom + + if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { + return { + x: Number((x1 + t * (x2 - x1)).toFixed(1)), + y: Number((y1 + t * (y2 - y1)).toFixed(1)), + } + } + + return null +} + +/** + * 두 점 사이의 거리 계산 + */ +const distanceBetweenPoints = (p1, p2) => { + const dx = p2.x - p1.x + const dy = p2.y - p1.y + return Math.sqrt(dx * dx + dy * dy) +} + +const findIntersectionPoint = (startPoint, direction, polygonVertices, divisionLines) => { + const rayEnd = { + x: startPoint.x + direction.x * 10000, + y: startPoint.y + direction.y * 10000, + } + + let closestIntersection = null + let minDistance = Infinity + + // 다각형 변과의 교점 + for (let i = 0; i < polygonVertices.length; i++) { + const v1 = polygonVertices[i] + const v2 = polygonVertices[(i + 1) % polygonVertices.length] + + const intersection = lineSegmentIntersection(startPoint, rayEnd, v1, v2) + + if (intersection) { + const dist = distanceBetweenPoints(startPoint, intersection) + if (dist > 0.1 && dist < minDistance) { + minDistance = dist + closestIntersection = intersection + } + } + } + + // 분할선분과의 교점 + for (const divLine of divisionLines) { + const intersection = lineSegmentIntersection(startPoint, rayEnd, { x: divLine.x1, y: divLine.y1 }, { x: divLine.x2, y: divLine.y2 }) + + if (intersection) { + const dist = distanceBetweenPoints(startPoint, intersection) + if (dist > 0.1 && dist < minDistance) { + minDistance = dist + closestIntersection = intersection + } + } + } + + return closestIntersection +} + +/** + * 변별로 설정된 지붕을 그린다. + * @param roofId + * @param canvas + * @param textMode + */ +export const drawRoofByAttribute = (roofId, canvas, textMode) => { + let roof = canvas?.getObjects().find((object) => object.id === roofId) + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + + /*const polygonVertices = roof.points.map((point) => ({ x: point.x, y: point.y })) + const L = polygonVertices.length + + const temporaryLines = [] + for (let i = 0; i < wall.points.length; i++) { + const currentVertex = polygonVertices[i] + const prevVertex = polygonVertices[(i - 1 + L) % L] + const nextVertex = polygonVertices[(i + 1) % L] + + const checkCircle1 = new fabric.Circle({ + left: currentVertex.x, + top: currentVertex.y, + radius: 4, + fill: 'red', + name: 'check', + }) + const checkCircle2 = new fabric.Circle({ + left: prevVertex.x, + top: prevVertex.y, + radius: 4, + fill: 'blue', + name: 'check', + }) + const checkCircle3 = new fabric.Circle({ + left: nextVertex.x, + top: nextVertex.y, + radius: 4, + fill: 'green', + name: 'check', + }) + canvas.add(checkCircle1, checkCircle2, checkCircle3) + canvas.renderAll() + + const bisector = calculateAngleBisector(prevVertex, currentVertex, nextVertex, polygonVertices) + + const divisionLines = [] + // 가선분의 종점 계산 (각이등분선과 다각형 변 또는 분할선분의 교점) + const endPoint = findIntersectionPoint(currentVertex, bisector, polygonVertices, divisionLines) + + if (endPoint) { + const tempLine = { + index: i, + startPoint: { ...currentVertex }, + endPoint: { ...endPoint }, + leftEdge: (i - 1 + L) % L, + rightEdge: i, + isActive: true, + intersectedWith: null, + minDistance: Infinity, + } + + const checkLine = new fabric.Line([tempLine.startPoint.x, tempLine.startPoint.y, tempLine.endPoint.x, tempLine.endPoint.y], { + stroke: 'red', + strokeWidth: 2, + parentId: roof.id, + name: 'check', + selectable: false, + }) + canvas.add(checkLine) + canvas.renderAll() + temporaryLines.push(tempLine) + } + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + } + + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + + return*/ + + const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) + const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) + + const checkWallPolygon = new QPolygon(baseLinePoints, {}) + + let ridgeLines = [] + let innerLines = [] + + /** 벽취합이 있는 경우 소매가 있다면 지붕 형상을 변경해야 한다. */ + baseLines + .filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.WALL && line.attributes.offset > 0) + .forEach((currentLine) => { + const prevLine = baseLines.find((line) => line.x2 === currentLine.x1 && line.y2 === currentLine.y1) + const nextLine = baseLines.find((line) => line.x1 === currentLine.x2 && line.y1 === currentLine.y2) + + const currentMidX = Big(currentLine.x1).plus(Big(currentLine.x2)).div(2).toNumber() + const currentMidY = Big(currentLine.y1).plus(Big(currentLine.y2)).div(2).toNumber() + const currentVectorX = Math.sign(currentLine.x2 - currentLine.x1) + const currentVectorY = Math.sign(currentLine.y2 - currentLine.y1) + + /** 현재 라인의 지붕 라인을 찾는다. */ + const intersectionRoofs = [] + let currentRoof + if (currentVectorX === 0) { + const checkEdge = { + vertex1: { x: prevLine.x1, y: currentMidY }, + vertex2: { x: currentMidX, y: currentMidY }, + } + roof.lines + .filter((line) => Math.sign(line.x2 - line.x1) === currentVectorX && Math.sign(line.y2 - line.y1) === currentVectorY) + .forEach((line) => { + const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const intersection = edgesIntersection(checkEdge, lineEdge) + if (intersection) { + if (isPointOnLine(line, intersection)) { + intersectionRoofs.push({ + line, + intersection, + size: Big(intersection.x).minus(currentMidX).abs().pow(2).plus(Big(intersection.y).minus(currentMidY).abs().pow(2)).sqrt(), + }) + } + } + }) + } else { + const checkEdge = { + vertex1: { x: currentMidX, y: prevLine.y1 }, + vertex2: { x: currentMidX, y: currentMidY }, + } + roof.lines + .filter((line) => Math.sign(line.x2 - line.x1) === currentVectorX && Math.sign(line.y2 - line.y1) === currentVectorY) + .forEach((line) => { + const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const intersection = edgesIntersection(checkEdge, lineEdge) + if (intersection) { + if (isPointOnLine(line, intersection)) { + intersectionRoofs.push({ + line, + intersection, + size: Big(intersection.x).minus(currentMidX).abs().pow(2).plus(Big(intersection.y).minus(currentMidY).abs().pow(2)).sqrt(), + }) + } + } + }) + } + if (intersectionRoofs.length > 0) { + currentRoof = intersectionRoofs.sort((a, b) => a.size - b.size)[0].line + } + if (currentRoof) { + const prevRoof = roof.lines.find((line) => line.x2 === currentRoof.x1 && line.y2 === currentRoof.y1) + const nextRoof = roof.lines.find((line) => line.x1 === currentRoof.x2 && line.y1 === currentRoof.y2) + + const prevOffset = prevLine.attributes.offset + const nextOffset = nextLine.attributes.offset + + currentRoof.set({ x1: currentLine.x1, y1: currentLine.y1, x2: currentLine.x2, y2: currentLine.y2 }) + + if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.WALL && prevOffset > 0) { + const addPoint1 = [] + const addPoint2 = [] + if (Math.sign(prevLine.y2 - prevLine.y1) === 0) { + addPoint1.push(prevRoof.x2, prevRoof.y2, prevRoof.x2, currentRoof.y1) + addPoint2.push(addPoint1[2], addPoint1[3], currentRoof.x1, currentRoof.y1) + } else { + addPoint1.push(prevRoof.x2, prevRoof.y2, currentRoof.x1, prevRoof.y2) + addPoint2.push(addPoint1[2], addPoint1[3], currentRoof.x1, currentRoof.y1) + } + const addRoofLine1 = new QLine(addPoint1, { + name: 'addRoofLine', + parentId: roof.id, + fontSize: roof.fontSize, + stroke: '#1083E3', + strokeWidth: 2, + textMode: textMode, + attributes: { + roofId: roofId, + type: LINE_TYPE.WALLLINE.ETC, + planeSize: calcLinePlaneSize({ + x1: addPoint1[0], + y1: addPoint1[1], + x2: addPoint1[2], + y2: addPoint1[3], + }), + actualSize: calcLinePlaneSize({ + x1: addPoint1[0], + y1: addPoint1[1], + x2: addPoint1[2], + y2: addPoint1[3], + }), + }, + }) + + const addRoofLine2 = new QLine(addPoint2, { + name: 'addRoofLine', + parentId: roof.id, + fontSize: roof.fontSize, + stroke: '#1083E3', + strokeWidth: 2, + textMode: textMode, + attributes: { + roofId: roofId, + type: LINE_TYPE.WALLLINE.ETC, + planeSize: calcLinePlaneSize({ + x1: addPoint2[0], + y1: addPoint2[1], + x2: addPoint2[2], + y2: addPoint2[3], + }), + actualSize: calcLinePlaneSize({ + x1: addPoint2[0], + y1: addPoint2[1], + x2: addPoint2[2], + y2: addPoint2[3], + }), + }, + }) + canvas.add(addRoofLine1, addRoofLine2) + canvas.renderAll() + + const prevIndex = roof.lines.indexOf(prevRoof) + if (prevIndex === roof.lines.length - 1) { + roof.lines.unshift(addRoofLine1, addRoofLine2) + } else { + roof.lines.splice(prevIndex + 1, 0, addRoofLine1, addRoofLine2) + } + } else if (prevLine.attributes.type === LINE_TYPE.WALLLINE.WALL && prevOffset > 0) { + if (Math.sign(prevLine.y2 - prevLine.y1) === 0) { + prevRoof.set({ x2: currentLine.x1, y2: prevRoof.y1 }) + } else { + prevRoof.set({ x2: prevRoof.x1, y2: currentLine.y1 }) + } + currentRoof.set({ x1: prevRoof.x2, y1: prevRoof.y2 }) + } else if (prevLine.attributes.type === LINE_TYPE.WALLLINE.WALL || prevOffset === 0) { + prevRoof.set({ x2: currentLine.x1, y2: currentLine.y1 }) + } + if (nextLine.attributes.type !== LINE_TYPE.WALLLINE.WALL && nextOffset > 0) { + const addPoint1 = [] + const addPoint2 = [] + if (Math.sign(nextLine.y2 - nextLine.y1) === 0) { + addPoint1.push(currentRoof.x2, currentRoof.y2, nextRoof.x1, currentRoof.y2) + addPoint2.push(addPoint1[2], addPoint1[3], nextRoof.x1, nextRoof.y1) + } else { + addPoint1.push(currentRoof.x2, currentRoof.y2, currentRoof.x2, nextRoof.y1) + addPoint2.push(addPoint1[2], addPoint1[3], nextRoof.x1, nextRoof.y1) + } + + const addRoofLine1 = new QLine(addPoint1, { + name: 'addRoofLine', + parentId: roof.id, + fontSize: roof.fontSize, + stroke: '#1083E3', + strokeWidth: 2, + textMode: textMode, + attributes: { + roofId: roofId, + type: LINE_TYPE.WALLLINE.ETC, + planeSize: calcLinePlaneSize({ + x1: addPoint1[0], + y1: addPoint1[1], + x2: addPoint1[2], + y2: addPoint1[3], + }), + actualSize: calcLinePlaneSize({ + x1: addPoint1[0], + y1: addPoint1[1], + x2: addPoint1[2], + y2: addPoint1[3], + }), + }, + }) + + const addRoofLine2 = new QLine(addPoint2, { + name: 'addRoofLine', + parentId: roof.id, + fontSize: roof.fontSize, + stroke: '#1083E3', + strokeWidth: 2, + textMode: textMode, + attributes: { + roofId: roofId, + type: LINE_TYPE.WALLLINE.ETC, + planeSize: calcLinePlaneSize({ + x1: addPoint2[0], + y1: addPoint2[1], + x2: addPoint2[2], + y2: addPoint2[3], + }), + actualSize: calcLinePlaneSize({ + x1: addPoint2[0], + y1: addPoint2[1], + x2: addPoint2[2], + y2: addPoint2[3], + }), + }, + }) + canvas.add(addRoofLine1, addRoofLine2) + canvas.renderAll() + + const nextIndex = roof.lines.indexOf(nextRoof) + if (nextIndex === 0) { + roof.lines.push(addRoofLine1, addRoofLine2) + } else { + roof.lines.splice(nextIndex, 0, addRoofLine1, addRoofLine2) + } + } else if (nextLine.attributes.type === LINE_TYPE.WALLLINE.WALL && nextOffset > 0) { + if (Math.sign(nextLine.y2 - nextLine.y1) === 0) { + nextRoof.set({ x1: currentLine.x2, y1: nextRoof.y1 }) + } else { + nextRoof.set({ x1: nextRoof.x1, y1: currentLine.y2 }) + } + currentRoof.set({ x2: nextRoof.x1, y2: nextRoof.y1 }) + } else if (nextLine.attributes.type === LINE_TYPE.WALLLINE.WALL || prevOffset === 0) { + nextRoof.set({ x1: currentLine.x2, y1: currentLine.y2 }) + } + + roof = reDrawPolygon(roof, canvas) + } + }) + + /** + * 라인의 속성을 분석한다. + * @param line + * @returns {{startPoint: {x: number, y}, endPoint: {x: number, y}, length: number, angleDegree: number, normalizedAngle: number, isHorizontal: boolean, isVertical: boolean, isDiagonal: boolean, directionVector: {x: number, y: number}, roofLine: *, roofVector: {x: number, y: number}}} + */ + const analyzeLine = (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 sheds = [] + const hipAndGables = [] + const eaves = [] + const jerkinHeads = [] + const gables = [] + baseLines.forEach((baseLine) => { + switch (baseLine.attributes.type) { + case LINE_TYPE.WALLLINE.SHED: + sheds.push(baseLine) + break + case LINE_TYPE.WALLLINE.HIPANDGABLE: + hipAndGables.push(baseLine) + break + case LINE_TYPE.WALLLINE.EAVES: + eaves.push(baseLine) + break + case LINE_TYPE.WALLLINE.JERKINHEAD: + jerkinHeads.push(baseLine) + break + case LINE_TYPE.WALLLINE.GABLE: + gables.push(baseLine) + break + default: + break + } + }) + + //지붕선 내부에 보조선을 그리기 위한 analysis + let linesAnalysis = [] + + //1. 한쪽흐름(단면경사) 판단, 단면경사는 양옆이 케라바(일반)여야 한다. 맞은편 지붕이 처마여야 한다. + sheds.forEach((currentLine) => { + let prevLine, nextLine + baseLines.forEach((baseLine, index) => { + if (baseLine === currentLine) { + nextLine = baseLines[(index + 1) % baseLines.length] + prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] + } + }) + + const analyze = analyzeLine(currentLine) + + //지붕선분을 innseLines에 추가한다. + const roofPoints = [analyze.roofLine.x1, analyze.roofLine.y1, analyze.roofLine.x2, analyze.roofLine.y2] + innerLines.push(drawRoofLine([roofPoints[0], roofPoints[1], roofPoints[2], roofPoints[3]], canvas, roof, textMode)) + + //지붕선을 임시선분에 추가한다. + /*const roofPoints = [analyze.roofLine.x1, analyze.roofLine.y1, analyze.roofLine.x2, analyze.roofLine.y2] + linesAnalysis.push({ + start: { x: roofPoints[0], y: roofPoints[1] }, // 시작점 + end: { x: roofPoints[2], y: roofPoints[3] }, // 끝점 + left: baseLines.findIndex((line) => line === prevLine), //이전라인 index + right: baseLines.findIndex((line) => line === currentLine), //현재라인 index + type: 'roof', //라인 타입 roof:지붕선(각도없음), hip:추녀마루(각도있음), ridge:(각도없음) + degree: 0, //라인 각도 + })*/ + //양옆이 케라바가 아닐때 제외 + /*if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.GABLE || nextLine.attributes.type !== LINE_TYPE.WALLLINE.GABLE) { + return + }*/ + + const eavesLines = [] + + // 맞은편 처마 지붕을 찾는다. 흐름 각도를 확인하기 위함. + baseLines + .filter((baseLine) => baseLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) + .forEach((baseLine) => { + const lineAnalyze = analyzeLine(baseLine) + //방향이 맞지 않으면 패스 + if ( + analyze.isHorizontal !== lineAnalyze.isHorizontal || + analyze.isVertical !== lineAnalyze.isVertical || + analyze.isDiagonal || + lineAnalyze.isDiagonal + ) { + return false + } + + // 수평 일때 + if ( + analyze.isHorizontal && + Math.min(baseLine.x1, baseLine.x2) <= Math.min(currentLine.x1, currentLine.x2) && + Math.max(baseLine.x1, baseLine.x2) >= Math.max(currentLine.x1, currentLine.x2) + ) { + eavesLines.push(baseLine) + } + + //수직 일때 + if ( + analyze.isVertical && + Math.min(baseLine.y1, baseLine.y2) <= Math.min(currentLine.y1, currentLine.y2) && + Math.max(baseLine.y1, baseLine.y2) >= Math.max(currentLine.y1, currentLine.y2) + ) { + eavesLines.push(baseLine) + } + }) + + let shedDegree = getDegreeByChon(4) + + if (eavesLines.length > 0) { + shedDegree = getDegreeByChon(eavesLines[0].attributes.pitch) + } + + //지붕좌표1에서 지붕 안쪽으로의 확인 방향 + const checkRoofVector1 = { + vertex1: { x: analyze.roofLine.x1, y: analyze.roofLine.y1 }, + vertex2: { x: analyze.roofLine.x1 + analyze.roofVector.x * -1, y: analyze.roofLine.y1 + analyze.roofVector.y * -1 }, + } + //지붕좌표2에서 지붕 안쪽으로의 확인 방향 + const checkRoofVector2 = { + vertex1: { x: analyze.roofLine.x2, y: analyze.roofLine.y2 }, + vertex2: { x: analyze.roofLine.x2 + analyze.roofVector.x * -1, y: analyze.roofLine.y2 + analyze.roofVector.y * -1 }, + } + + //좌표1에서의 교차점 + const intersectPoints1 = [] + //좌료2에서의 교차점 + const intersectPoints2 = [] + roof.lines + .filter((roofLine) => roofLine !== analyze.roofLine) // 같은 지붕선 제외 + .filter((roofLine) => { + const tolerance = 1 + const dx = Big(roofLine.x2).minus(Big(roofLine.x1)).toNumber() + const dy = Big(roofLine.y2).minus(Big(roofLine.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 + 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 + } + let otherSide = false + + // 수평 일때 반대쪽 라인이 현재 라인을 포함하는지 확인. + if ( + analyze.isHorizontal && + ((Math.min(roofLine.x1, roofLine.x2) <= Math.min(analyze.roofLine.x1, analyze.roofLine.x2) && + Math.max(roofLine.x1, roofLine.x2) >= Math.max(analyze.roofLine.x1, analyze.roofLine.x2)) || + (Math.min(analyze.roofLine.x1, analyze.roofLine.x2) <= Math.min(roofLine.x1, roofLine.x2) && + Math.max(analyze.roofLine.x1, analyze.roofLine.x2) >= Math.max(roofLine.x1, roofLine.x2))) && + angleDegree !== analyze.angleDegree + ) { + otherSide = true + } + + //수직 일때 반대쪽 라인이 현재 라인을 포함하는지 확인. + if ( + analyze.isVertical && + ((Math.min(roofLine.y1, roofLine.y2) <= Math.min(analyze.roofLine.y1, analyze.roofLine.y2) && + Math.max(roofLine.y1, roofLine.y2) >= Math.max(analyze.roofLine.y1, analyze.roofLine.y2)) || + (Math.min(analyze.roofLine.y1, analyze.roofLine.y2) <= Math.min(roofLine.y1, roofLine.y2) && + Math.max(analyze.roofLine.y1, analyze.roofLine.y2) >= Math.max(roofLine.y1, roofLine.y2))) && + angleDegree !== analyze.angleDegree + ) { + otherSide = true + } + + return analyze.isHorizontal === isHorizontal && analyze.isVertical === isVertical && analyze.isDiagonal === isDiagonal && otherSide + }) + .forEach((roofLine) => { + const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } + const intersect1 = edgesIntersection(lineEdge, checkRoofVector1) + const intersect2 = edgesIntersection(lineEdge, checkRoofVector2) + if (intersect1 && isPointOnLineNew(roofLine, intersect1)) { + const size = Math.sqrt(Math.pow(analyze.roofLine.x1 - intersect1.x, 2) + Math.pow(analyze.roofLine.y1 - intersect1.y, 2)) + intersectPoints1.push({ intersect: intersect1, size }) + } + if (intersect2 && isPointOnLineNew(roofLine, intersect2)) { + const size = Math.sqrt(Math.pow(analyze.roofLine.x2 - intersect2.x, 2) + Math.pow(analyze.roofLine.y2 - intersect2.y, 2)) + intersectPoints2.push({ intersect: intersect2, size }) + } + }) + intersectPoints1.sort((a, b) => b.size - a.size) + intersectPoints2.sort((a, b) => b.size - a.size) + + const points1 = [analyze.roofLine.x1, analyze.roofLine.y1, intersectPoints1[0].intersect.x, intersectPoints1[0].intersect.y] + const points2 = [analyze.roofLine.x2, analyze.roofLine.y2, intersectPoints2[0].intersect.x, intersectPoints2[0].intersect.y] + + /*const line1 = drawHipLine(points1, canvas, roof, textMode, null, shedDegree, shedDegree) + const line2 = drawHipLine(points2, canvas, roof, textMode, null, shedDegree, shedDegree) + const line3 = drawRoofLine([analyze.roofLine.x1, analyze.roofLine.y1, analyze.roofLine.x2, analyze.roofLine.y2], canvas, roof, textMode) + innerLines.push(line1, line2, line3)*/ + linesAnalysis.push( + { + start: { x: points1[0], y: points1[1] }, + end: { x: points1[2], y: points1[3] }, + left: baseLines.findIndex((line) => line === prevLine), + right: baseLines.findIndex((line) => line === currentLine), + type: 'hip', + degree: shedDegree, + }, + { + start: { x: points2[0], y: points2[1] }, + end: { x: points2[2], y: points2[3] }, + left: baseLines.findIndex((line) => line === currentLine), + right: baseLines.findIndex((line) => line === nextLine), + type: 'hip', + degree: shedDegree, + }, + ) + }) + + //2. 팔작지붕(이리모야) 판단, 팔작지붕은 양옆이 처마여야만 한다. + hipAndGables.forEach((currentLine) => { + let prevLine, nextLine + baseLines.forEach((baseLine, index) => { + if (baseLine === currentLine) { + nextLine = baseLines[(index + 1) % baseLines.length] + prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] + } + }) + + const analyze = analyzeLine(currentLine) + + //지붕선을 innerLines에 추가한다. + const roofPoints = [analyze.roofLine.x1, analyze.roofLine.y1, analyze.roofLine.x2, analyze.roofLine.y2] + innerLines.push(drawRoofLine([roofPoints[0], roofPoints[1], roofPoints[2], roofPoints[3]], canvas, roof, textMode)) + + //양옆이 처마가 아닐때 패스 + /*if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) { + return false + }*/ + + const prevDegree = getDegreeByChon(prevLine.attributes.pitch) + const nextDegree = getDegreeByChon(nextLine.attributes.pitch) + + const currentRoofLine = analyze.roofLine + let prevRoofLine, nextRoofLine + roof.lines.forEach((roofLine, index) => { + if (roofLine === currentRoofLine) { + nextRoofLine = roof.lines[(index + 1) % roof.lines.length] + prevRoofLine = roof.lines[(index - 1 + roof.lines.length) % roof.lines.length] + } + }) + /** 팔작지붕 두께*/ + const roofDx = currentRoofLine.x2 - currentRoofLine.x1 + const roofDy = currentRoofLine.y2 - currentRoofLine.y1 + const roofLength = Math.sqrt(roofDx * roofDx + roofDy * roofDy) + const lineWidth = currentLine.attributes.width <= roofLength / 2 ? currentLine.attributes.width : roofLength / 2 + + /** 이전, 다음라인의 사잇각의 vector를 구한다. */ + let prevVector = getHalfAngleVector(prevRoofLine, currentRoofLine) + let nextVector = getHalfAngleVector(currentRoofLine, nextRoofLine) + + /** 이전 라인과의 사이 추녀마루의 각도를 확인한다, 각도가 지붕안쪽으로 향하지 않을때 반대로 처리한다.*/ + const prevCheckPoint = { + x: currentRoofLine.x1 + prevVector.x * lineWidth, + y: currentRoofLine.y1 + prevVector.y * lineWidth, + } + /** 다음 라인과의 사이 추녀마루의 각도를 확인한다, 각도가 지붕안쪽으로 향하지 않을때 반대로 처리한다.*/ + const nextCheckPoint = { + x: currentRoofLine.x2 + nextVector.x * lineWidth, + y: currentRoofLine.y2 + nextVector.y * lineWidth, + } + + let prevHipVector, nextHipVector + + if (roof.inPolygon(prevCheckPoint)) { + prevHipVector = { x: prevVector.x, y: prevVector.y } + } else { + prevHipVector = { x: -prevVector.x, y: -prevVector.y } + } + + /** 다음 라인과의 사이 추녀마루의 각도를 확인한다, 각도가 지붕안쪽으로 향하지 않을때 반대로 처리한다.*/ + if (roof.inPolygon(nextCheckPoint)) { + nextHipVector = { x: nextVector.x, y: nextVector.y } + } else { + nextHipVector = { x: -nextVector.x, y: -nextVector.y } + } + + const prevHipPoint = [ + currentRoofLine.x1, + currentRoofLine.y1, + currentRoofLine.x1 + prevHipVector.x * lineWidth, + currentRoofLine.y1 + prevHipVector.y * lineWidth, + ] + const nextHipPoint = [ + currentRoofLine.x2, + currentRoofLine.y2, + currentRoofLine.x2 + nextHipVector.x * lineWidth, + currentRoofLine.y2 + nextHipVector.y * lineWidth, + ] + const gablePoint = [prevHipPoint[2], prevHipPoint[3], nextHipPoint[2], nextHipPoint[3]] + + innerLines.push(drawHipLine(prevHipPoint, canvas, roof, textMode, null, prevDegree, prevDegree)) + innerLines.push(drawHipLine(nextHipPoint, canvas, roof, textMode, null, nextDegree, nextDegree)) + innerLines.push(drawRoofLine(gablePoint, canvas, roof, textMode)) + + //양옆이 처마일경우 두개의 선, 아닐때 한개의 선, 좌우가 처마가 아닐때 안그려져야하는데 기존에 그려지는 경우가 있음 이유를 알 수 없음. + if (prevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES && nextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { + const midPoint = { x: (prevHipPoint[2] + nextHipPoint[2]) / 2, y: (prevHipPoint[3] + nextHipPoint[3]) / 2 } + const prevGablePoint = [gablePoint[0], gablePoint[1], midPoint.x, midPoint.y] + const nextGablePoint = [gablePoint[2], gablePoint[3], midPoint.x, midPoint.y] + const prevDx = prevGablePoint[0] - prevGablePoint[2] + const prevDy = prevGablePoint[1] - prevGablePoint[3] + const prevGableLength = Math.sqrt(prevDx * prevDx + prevDy * prevDy) + const nextDx = nextGablePoint[0] - nextGablePoint[2] + const nextDy = nextGablePoint[1] - nextGablePoint[3] + const nextGableLength = Math.sqrt(nextDx * nextDx + nextDy * nextDy) + if (prevGableLength >= 1) { + innerLines.push(drawHipLine(prevGablePoint, canvas, roof, textMode, null, prevDegree, prevDegree)) + } + if (nextGableLength >= 1) { + innerLines.push(drawHipLine(nextGablePoint, canvas, roof, textMode, null, nextDegree, nextDegree)) + } + const checkEdge = { + vertex1: { x: midPoint.x, y: midPoint.y }, + vertex2: { x: midPoint.x + -analyze.roofVector.x * 10000, y: midPoint.y + -analyze.roofVector.y * 10000 }, + } + const intersections = [] + roof.lines + .filter((line) => line !== currentRoofLine) + .forEach((line) => { + const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const intersect = edgesIntersection(lineEdge, checkEdge) + if (intersect && isPointOnLineNew(line, intersect)) { + const distance = Math.sqrt(Math.pow(midPoint.x - intersect.x, 2) + Math.pow(midPoint.y - intersect.y, 2)) + intersections.push({ intersect, distance }) + } + }) + intersections.sort((a, b) => a.distance - b.distance) + if (intersections.length > 0) { + const intersect = intersections[0].intersect + linesAnalysis.push({ + start: { x: midPoint.x, y: midPoint.y }, + end: { x: intersect.x, y: intersect.y }, + left: baseLines.findIndex((line) => line === prevLine), + right: baseLines.findIndex((line) => line === nextLine), + type: 'ridge', + degree: 0, + }) + } + } else { + const gableDx = gablePoint[0] - gablePoint[2] + const gableDy = gablePoint[1] - gablePoint[3] + const gableLength = Math.sqrt(gableDx * gableDx + gableDy * gableDy) + if (gableLength >= 1) { + innerLines.push(drawRoofLine(gablePoint, canvas, roof, textMode)) + } else { + const midPoint = { x: gablePoint[2], y: gablePoint[3] } + const checkEdge = { + vertex1: { x: midPoint.x, y: midPoint.y }, + vertex2: { x: midPoint.x + -analyze.roofVector.x * 10000, y: midPoint.y + -analyze.roofVector.y * 10000 }, + } + const intersections = [] + roof.lines + .filter((line) => line !== currentRoofLine) + .forEach((line) => { + const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const intersect = edgesIntersection(lineEdge, checkEdge) + if (intersect && isPointOnLineNew(line, intersect)) { + const distance = Math.sqrt(Math.pow(midPoint.x - intersect.x, 2) + Math.pow(midPoint.y - intersect.y, 2)) + intersections.push({ intersect, distance }) + } + }) + intersections.sort((a, b) => a.distance - b.distance) + if (intersections.length > 0) { + const intersect = intersections[0].intersect + linesAnalysis.push({ + start: { x: midPoint.x, y: midPoint.y }, + end: { x: intersect.x, y: intersect.y }, + left: baseLines.findIndex((line) => line === prevLine), + right: baseLines.findIndex((line) => line === nextLine), + type: 'ridge', + degree: 0, + }) + } + } + } + }) + //3. 처마지붕 판단, 처마는 옆이 처마여야한다. + let eavesHipLines = [] + + // 처마지붕 사이의 추녀마루를 작성하기 위한 포인트를 계산. + eaves.forEach((currentLine) => { + let prevLine + baseLines.forEach((baseLine, index) => { + if (baseLine === currentLine) { + prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] + } + }) + + const analyze = analyzeLine(currentLine) + + //지붕선을 innerLines에 추가한다. + const roofPoints = [analyze.roofLine.x1, analyze.roofLine.y1, analyze.roofLine.x2, analyze.roofLine.y2] + innerLines.push(drawRoofLine([roofPoints[0], roofPoints[1], roofPoints[2], roofPoints[3]], canvas, roof, textMode)) + + // 옆이 처마가 아니면 패스 + if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) { + return + } + + const currentDegree = getDegreeByChon(currentLine.attributes.pitch) + const checkVector = getHalfAngleVector(currentLine, prevLine) + const checkPoint = { + x: currentLine.x1 + (checkVector.x * analyze.length) / 2, + y: currentLine.y1 + (checkVector.y * analyze.length) / 2, + } + + let hipVector + if (checkWallPolygon.inPolygon(checkPoint)) { + hipVector = { x: checkVector.x, y: checkVector.y } + } else { + hipVector = { x: -checkVector.x, y: -checkVector.y } + } + + const hipEdge = { + vertex1: { x: currentLine.x1, y: currentLine.y1 }, + vertex2: { x: currentLine.x1 + hipVector.x * analyze.length, y: currentLine.y1 + hipVector.y * analyze.length }, + } + + const hipForwardVector = { x: Math.sign(hipEdge.vertex1.x - hipEdge.vertex2.x), y: Math.sign(hipEdge.vertex1.y - hipEdge.vertex2.y) } + const hipBackwardVector = { x: -hipForwardVector.x, y: -hipForwardVector.y } + const isForwardPoints = [] + const isBackwardPoints = [] + + roof.lines.forEach((roofLine) => { + const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } + const intersect = edgesIntersection(lineEdge, hipEdge) + if (intersect && isPointOnLineNew(roofLine, intersect)) { + const intersectVector = { x: Math.sign(hipEdge.vertex1.x - intersect.x), y: Math.sign(hipEdge.vertex1.y - intersect.y) } + if (intersectVector.x === hipForwardVector.x && intersectVector.y === hipForwardVector.y) { + const dx = hipEdge.vertex1.x - intersect.x + const dy = hipEdge.vertex1.y - intersect.y + const length = Math.sqrt(dx * dx + dy * dy) + isForwardPoints.push({ intersect, length }) + } + if (intersectVector.x === hipBackwardVector.x && intersectVector.y === hipBackwardVector.y) { + const dx = hipEdge.vertex2.x - intersect.x + const dy = hipEdge.vertex2.y - intersect.y + const length = Math.sqrt(dx * dx + dy * dy) + isBackwardPoints.push({ intersect, length }) + } + } + }) + isForwardPoints.sort((a, b) => a.length - b.length) + isBackwardPoints.sort((a, b) => a.length - b.length) + + const hipPoint = [ + isBackwardPoints[0].intersect.x, + isBackwardPoints[0].intersect.y, + isForwardPoints[0].intersect.x, + isForwardPoints[0].intersect.y, + ] + + linesAnalysis.push({ + start: { x: hipPoint[0], y: hipPoint[1] }, + end: { x: hipPoint[2], y: hipPoint[3] }, + left: baseLines.findIndex((line) => line === prevLine), + right: baseLines.findIndex((line) => line === currentLine), + type: 'hip', + degree: currentDegree, + }) + }) + + /*//추녀마루 포인트중 겹치는 라인을 찾아 조정한다. + eavesHipLines.forEach((eavesHipLine) => { + const { hipPoint, line1, line2, degree } = eavesHipLine + const checkLine = new fabric.Line(hipPoint, { + stroke: 'blue', + strokeWidth: 2, + parentId: roofId, + name: 'check', + }) + canvas.add(checkLine) + canvas.renderAll() + + const hipEdge = { vertex1: { x: hipPoint[0], y: hipPoint[1] }, vertex2: { x: hipPoint[2], y: hipPoint[3] } } + const intersectPoints = [] + eavesHipLines + .filter((line) => eavesHipLine !== line) + .forEach((hip) => { + const checkPoint = hip.hipPoint + const checkLine1 = hip.line1 + const checkLine2 = hip.line2 + const checkEdge = { vertex1: { x: checkPoint[0], y: checkPoint[1] }, vertex2: { x: checkPoint[2], y: checkPoint[3] } } + const intersect = edgesIntersection(checkEdge, hipEdge) + const checkLine = new fabric.Line(checkPoint, { + stroke: 'green', + strokeWidth: 2, + parentId: roofId, + name: 'check', + }) + canvas.add(checkLine) + canvas.renderAll() + + // 기준선내 겹치는 경우, line1, line2 중에서 비교선의 line1, line2가 겹칠때만 겹쳐질 수 있는 라인으로 판단한다. (샤프논문 참조) + if ( + intersect && + isPointOnLineNew({ x1: hipPoint[0], y1: hipPoint[1], x2: hipPoint[2], y2: hipPoint[3] }, intersect) && + isPointOnLineNew({ x1: checkPoint[0], y1: checkPoint[1], x2: checkPoint[2], y2: checkPoint[3] }, intersect) && + (line1 === checkLine1 || line1 === checkLine2 || line2 === checkLine1 || line2 === checkLine2) + ) { + const dx = hipPoint[0] - intersect.x + const dy = hipPoint[1] - intersect.y + const length = Math.sqrt(dx * dx + dy * dy) + intersectPoints.push({ intersect, hip, length }) + } + }) + intersectPoints.sort((a, b) => a.length - b.length) + if (intersectPoints.length === 0) { + innerLines.push(drawHipLine(hipPoint, canvas, roof, textMode, null, degree, degree)) + } else { + const intersect = intersectPoints[0].intersect + const linePoint = [hipPoint[0], hipPoint[1], intersect.x, intersect.y] + innerLines.push(drawHipLine(linePoint, canvas, roof, textMode, null, degree, degree)) + + //다른라인에서 사용 되지 않게 조정 + eavesHipLines.find((line) => line === eavesHipLine).hipPoint = linePoint + eavesHipLines.find((line) => line === intersectPoints[0].hip).hipPoint = [ + intersectPoints[0].hip.hipPoint[0], + intersectPoints[0].hip.hipPoint[1], + intersect.x, + intersect.y, + ] + } + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + })*/ + //4. 반절처(반절마루) 판단, 반절마루는 양옆이 처마여야 한다. + //5. 케라바 판단, 케라바는 양옆이 처마여야 한다. + + //각 선분의 교차점을 찾아 선을 그린다. + const MAX_ITERATIONS = 1000 //무한루프 방지 + let iterations = 0 + + console.log('linesAnalysis', linesAnalysis) + while (linesAnalysis.length > 0 && iterations < MAX_ITERATIONS) { + iterations++ + + // 각 가선분의 최단 교점 찾기 + const intersections = [] + for (let i = 0; i < linesAnalysis.length; i++) { + let minDistance = Infinity //최단거리 + let intersectPoint = null //교점 좌표 + let intersectIndex = [] //교점 선분의 index + + for (let j = 0; j < linesAnalysis.length; j++) { + if (i === j) continue + + const intersection = lineIntersection(linesAnalysis[i].start, linesAnalysis[i].end, linesAnalysis[j].start, linesAnalysis[j].end) + if (intersection) { + const distance = Math.sqrt((intersection.x - linesAnalysis[i].start.x) ** 2 + (intersection.y - linesAnalysis[i].start.y) ** 2) + if (distance > EPSILON) { + if (distance < minDistance && !almostEqual(distance, minDistance)) { + // console.log('intersection :', intersection, intersectPoint) + // console.log('distance :', distance, minDistance, almostEqual(distance, minDistance)) + minDistance = distance + intersectPoint = intersection + intersectIndex = [j] + } else if (almostEqual(distance, minDistance)) { + intersectIndex.push(j) + } + } + } + } + if (intersectPoint) { + intersections.push({ index: i, intersectPoint, intersectIndex, distance: minDistance }) + } + } + + // 교점에 대한 적합 여부 판단 및 처리. + let newAnalysis = [] //신규 발생 선 + const processed = new Set() //처리된 선 + console.log('intersections : ', intersections) + for (const { index, intersectPoint, intersectIndex } of intersections) { + const check1 = linesAnalysis[index] + const check2 = linesAnalysis[intersectIndex] + const checkLine1 = new fabric.Line([check1.start.x, check1.start.y, check1.end.x, check1.end.y], { + stroke: 'red', + strokeWidth: 2, + parentId: roofId, + name: 'check', + }) + const checkLine2 = new fabric.Line([check2.start.x, check2.start.y, check2.end.x, check2.end.y], { + stroke: 'green', + strokeWidth: 2, + parentId: roofId, + name: 'check', + }) + const checkCircle1 = new fabric.Circle({ left: check1.start.x, top: check1.start.y, radius: 5, fill: 'red', parentId: roofId, name: 'check' }) + const checkCircle2 = new fabric.Circle({ left: check2.start.x, top: check2.start.y, radius: 5, fill: 'red', parentId: roofId, name: 'check' }) + const checkCircle3 = new fabric.Circle({ left: check1.end.x, top: check1.end.y, radius: 5, fill: 'green', parentId: roofId, name: 'check' }) + const checkCircle4 = new fabric.Circle({ left: check2.end.x, top: check2.end.y, radius: 5, fill: 'green', parentId: roofId, name: 'check' }) + canvas.add(checkCircle1, checkCircle2, checkCircle3, checkCircle4) + canvas.add(checkLine1, checkLine2) + canvas.renderAll() + + //교점이 없거나, 이미 처리된 선분이면 처리하지 않는다. + if (!intersectPoint || processed.has(index)) continue + + const partner = intersections.find((p) => p.index === intersectIndex) + //교점이 없거나, 교점 선분이 없으면 처리하지 않는다. + if (!partner || !partner.intersectPoint) continue + + //상호 최단 교점 여부 확인. + if (partner.intersectIndex === index) { + const line1 = linesAnalysis[index] + const line2 = linesAnalysis[intersectIndex] + + //좌,우 선 중 공통 선 존재 확인. + const isSameLine = line1.left === line2.left || line1.left === line2.right || line1.right === line2.left || line1.right === line2.right + + if (isSameLine) { + const point1 = [line1.start.x, line1.start.y, intersectPoint.x, intersectPoint.y] + const point2 = [line2.start.x, line2.start.y, intersectPoint.x, intersectPoint.y] + + if (line1.type === 'hip') { + innerLines.push(drawHipLine(point1, canvas, roof, textMode, null, line1.degree, line1.degree)) + } else if (line1.type === 'ridge') { + innerLines.push(drawRidgeLine(point1, canvas, roof, textMode)) + } else if (line1.type === 'new') { + const isDiagonal = Math.abs(point1[0] - point1[2]) >= 1 && Math.abs(point1[1] - point1[3]) >= 1 + if (isDiagonal) { + innerLines.push(drawHipLine(point1, canvas, roof, textMode, null, line1.degree, line1.degree)) + } else { + innerLines.push(drawRidgeLine(point1, canvas, roof, textMode)) + } + } + + if (line2.type === 'hip') { + innerLines.push(drawHipLine(point2, canvas, roof, textMode, null, line2.degree, line2.degree)) + } else if (line2.type === 'ridge') { + innerLines.push(drawRidgeLine(point2, canvas, roof, textMode)) + } else if (line2.type === 'new') { + const isDiagonal = Math.abs(point2[0] - point2[2]) >= 1 && Math.abs(point2[1] - point2[3]) >= 1 + if (isDiagonal) { + innerLines.push(drawHipLine(point2, canvas, roof, textMode, null, line2.degree, line2.degree)) + } else { + innerLines.push(drawRidgeLine(point2, canvas, roof, textMode)) + } + } + + // 연결점에서 새로운 가선분을 생성 + const relationBaseLines = [line1.left, line1.right, line2.left, line2.right] + const uniqueBaseLines = [...new Set(relationBaseLines)] + + if (uniqueBaseLines.length === 3) { + const linesCounts = new Map() + relationBaseLines.forEach((e) => { + linesCounts.set(e, (linesCounts.get(e) || 0) + 1) + }) + + const uniqueLines = Array.from(linesCounts.entries()) + .filter(([_, count]) => count === 1) + .map(([line, _]) => line) + + if (uniqueLines.length === 2) { + // 두 변의 이등분선 방향 계산 + uniqueLines.sort((a, b) => a - b) + console.log('uniqueLines : ', uniqueLines) + const baseLine1 = baseLines[uniqueLines[0]] + const baseLine2 = baseLines[uniqueLines[1]] + + const checkLine1 = new fabric.Line([baseLine1.x1, baseLine1.y1, baseLine1.x2, baseLine1.y2], { + stroke: 'green', + strokeWidth: 4, + parentId: roofId, + name: 'check', + }) + const checkLine2 = new fabric.Line([baseLine2.x1, baseLine2.y1, baseLine2.x2, baseLine2.y2], { + stroke: 'blue', + strokeWidth: 4, + parentId: roofId, + name: 'check', + }) + canvas.add(checkLine1, checkLine2) + canvas.renderAll() + let bisector + console.log('isParallel(baseLine1, baseLine2)', isParallel(baseLine1, baseLine2)) + if (isParallel(baseLine1, baseLine2)) { + bisector = getBisectLines( + { x1: line1.start.x, x2: line1.end.x, y1: line1.start.y, y2: line1.end.y }, + { x1: line2.start.x, y1: line2.start.y, x2: line2.end.x, y2: line2.end.y }, + ) + } else { + //이등분선 + bisector = getBisectBaseLines(baseLine1, baseLine2, intersectPoint, canvas) + } + + //마주하는 지붕선을 찾는다. + const intersectionsByRoof = [] + const checkEdge = { + vertex1: { x: intersectPoint.x, y: intersectPoint.y }, + vertex2: { x: intersectPoint.x + bisector.x, y: intersectPoint.y + bisector.y }, + } + const checkVector = { x: Math.sign(checkEdge.vertex1.x - checkEdge.vertex2.x), y: Math.sign(checkEdge.vertex1.y - checkEdge.vertex2.y) } + roof.lines.forEach((line) => { + const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const is = edgesIntersection(lineEdge, checkEdge) + if (is && isPointOnLineNew(line, is)) { + const distance = Math.sqrt((is.x - intersectPoint.x) ** 2 + (is.y - intersectPoint.y) ** 2) + const isVector = { x: Math.sign(intersectPoint.x - is.x), y: Math.sign(intersectPoint.y - is.y) } + if (isVector.x === checkVector.x && isVector.y === checkVector.y) { + intersectionsByRoof.push({ is, distance }) + } + } + }) + intersectionsByRoof.sort((a, b) => a.distance - b.distance) + //지붕 선과의 교점이 존재 할때 + if (intersectionsByRoof.length > 0) { + const is = intersectionsByRoof[0].is + const checkNewLine = new fabric.Line([intersectPoint.x, intersectPoint.y, is.x, is.y], { + stroke: 'cyan', + strokeWidth: 4, + parentId: roofId, + name: 'check', + }) + canvas.add(checkNewLine) + canvas.renderAll() + newAnalysis.push({ + start: { x: intersectPoint.x, y: intersectPoint.y }, + end: { x: is.x, y: is.y }, + left: uniqueLines[0], + right: uniqueLines[1], + type: 'new', + degree: line1.degree, + }) + } + } + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + } + + processed.add(index) + processed.add(intersectIndex) + } + } + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + } + // 처리된 가선분 제외 + linesAnalysis = linesAnalysis.filter((_, index) => !processed.has(index)).concat(newAnalysis) + console.log('lineAnalysis: ', linesAnalysis) + /* + linesAnalysis.forEach((line) => { + const checkLine = new fabric.Line([line.start.x, line.start.y, line.end.x, line.end.y], { + stroke: 'red', + strokeWidth: 2, + parentId: roofId, + name: 'check', + }) + canvas.add(checkLine) + canvas.renderAll() + })*/ + + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + // 새로운 가선분이 없을때 종료 + if (newAnalysis.length === 0) break + } + + //지붕 innerLines에 추가. + roof.innerLines.push(...innerLines, ...ridgeLines) + + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() +} + +/** + * 선분과 선분의 교점을 구한다. + * @param line1Start + * @param line1End + * @param line2Start + * @param line2End + */ +const lineIntersection = (line1Start, line1End, line2Start, line2End) => { + const denominator = (line2End.y - line2Start.y) * (line1End.x - line1Start.x) - (line2End.x - line2Start.x) * (line1End.y - line1Start.y) + if (Math.abs(denominator) < EPSILON) return null // 평행 + + const ua = ((line2End.x - line2Start.x) * (line1Start.y - line2Start.y) - (line2End.y - line2Start.y) * (line1Start.x - line2Start.x)) / denominator + const ub = ((line1End.x - line1Start.x) * (line1Start.y - line2Start.y) - (line1End.y - line1Start.y) * (line1Start.x - line2Start.x)) / denominator + + if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { + return { + x: line1Start.x + ua * (line1End.x - line1Start.x), + y: line1Start.y + ua * (line1End.y - line1Start.y), + } + } + return null +} + +/** + * 실제 각도를 계산한다 대각선인 경우 각도 조정 + * @param points + * @param degree + */ +const getRealDegree = (points, degree) => { + const deltaX = Math.abs(points[2] - points[0]) + const deltaY = Math.abs(points[3] - points[1]) + if (deltaX < 1 || deltaY < 1) { + return degree + } + + const hypotenuse = Math.sqrt(deltaX ** 2 + deltaY ** 2) + const adjacent = Math.sqrt(Math.pow(hypotenuse, 2) / 2) + const height = adjacent * Math.tan((degree * Math.PI) / 180) + return Math.atan2(height, hypotenuse) * (180 / Math.PI) +} + +/** + * 두 라인이 평행한지 확인한다. + * @param line1 + * @param line2 + * @returns {boolean} + */ +const isParallel = (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 < EPSILON || length2 < EPSILON) { + return false + } + + // 정규화된 벡터 + const norm1x = v1x / length1 + const norm1y = v1y / length1 + const norm2x = v2x / length2 + const norm2y = v2y / length2 + + // 외적(cross product)을 이용한 평행 판단 + // 외적의 크기가 0에 가까우면 평행 + const crossProduct = Math.abs(norm1x * norm2y - norm1y * norm2x) + const isParallel = crossProduct < EPSILON + + // 또는 내적(dot product)을 이용한 방법 + // 내적의 절대값이 1에 가까우면 평행 또는 반평행 + const dotProduct = norm1x * norm2x + norm1y * norm2y + const isParallelOrAntiParallel = Math.abs(Math.abs(dotProduct) - 1) < EPSILON + + return isParallel || isParallelOrAntiParallel +} + +/** + * 두 라인의 방향에 따라 이등분선의 vector를 구한다. + * @param line1 + * @param line2 + * @param polygon + */ +const getBisectLines = (line1, line2) => { + const bisector = { x: 0, y: 0 } + + // 벡터 계산 + 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 < EPSILON || length2 < EPSILON) { + return bisector + } + + const lineEdge1 = { vertex1: { x: line1.x1, y: line1.y1 }, vertex2: { x: line1.x2, y: line1.y2 } } + const lineEdge2 = { vertex1: { x: line2.x1, y: line2.y1 }, vertex2: { x: line2.x2, y: line2.y2 } } + const is = edgesIntersection(lineEdge1, lineEdge2) + if (is) { + const dir1 = getDirection(line1, is) + const dir2 = getDirection(line2, is) + + // 이등분선 계산 + const bisectorX = dir1.x + dir2.x + const bisectorY = dir1.y + dir2.y + const length = Math.sqrt(bisectorX * bisectorX + bisectorY * bisectorY) + + bisector.x = bisectorX / length + bisector.y = bisectorY / length + } + + return bisector +} + +/** + * BaseLine의 교차상태에서의 bisect를 구한다. + * @param line1 + * @param line2 + * @param intersection + */ +const getBisectBaseLines = (line1, line2, intersection, canvas) => { + const checkLine1 = new fabric.Line([line1.x1, line1.y1, line1.x2, line1.y2], { + stroke: 'red', + strokeWidth: 4, + name: 'check', + }) + const checkLine2 = new fabric.Line([line2.x1, line2.y1, line2.x2, line2.y2], { + stroke: 'blue', + strokeWidth: 4, + name: 'check', + }) + const checkCircle = new fabric.Circle({ + left: intersection.x, + top: intersection.y, + radius: 5, + fill: 'cyan', + name: 'check', + }) + canvas.add(checkLine1, checkLine2, checkCircle) + canvas.renderAll() + + let bisector = { x: 0, y: 0 } + if (isParallel(line1, line2)) return bisector + + const lineEdge1 = { vertex1: { x: line1.x1, y: line1.y1 }, vertex2: { x: line1.x2, y: line1.y2 } } + const lineEdge2 = { vertex1: { x: line2.x1, y: line2.y1 }, vertex2: { x: line2.x2, y: line2.y2 } } + const is = edgesIntersection(lineEdge1, lineEdge2) + if (is) { + const checkCircle = new fabric.Circle({ + left: is.x, + top: is.y, + radius: 5, + fill: 'pink', + name: 'check', + }) + canvas.add(checkCircle) + canvas.renderAll() + bisector = { x: Math.sign(intersection.x - is.x), y: Math.sign(intersection.y - is.y) } + } + + canvas.remove(checkLine1, checkLine2, checkCircle) + canvas.renderAll() + + return bisector +} + +const almostEqual = (a, b, epsilon = 1e-10) => { + if (a === Infinity || b === Infinity) return false + const diff = Math.abs(a - b) + const larger = Math.max(Math.abs(a), Math.abs(b)) + + // 둘 다 0에 가까운 경우 + if (larger < epsilon) { + return diff < epsilon + } + + return diff <= larger * epsilon +} + +const getDirection = (segment, fromPoint) => { + // const dist1 = Math.sqrt(Math.pow(segment.x1 - fromPoint.x, 2) + Math.pow(segment.y1 - fromPoint.y, 2)) + // const dist2 = Math.sqrt(Math.pow(segment.x2 - fromPoint.x, 2) + Math.pow(segment.y2 - fromPoint.y, 2)) + + // const target = dist1 > dist2 ? { x: segment.x1, y: segment.y1 } : { x: segment.x2, y: segment.y2 } + const target = { x: segment.x2, y: segment.y2 } + + const dx = target.x - fromPoint.x + const dy = target.y - fromPoint.y + const length = Math.sqrt(dx * dx + dy * dy) + + return { x: dx / length, y: dy / length } +} /** * 마루가 있는 지붕을 그린다. * @param roofId @@ -4607,9 +6196,6 @@ export const drawRidgeRoof = (roofId, canvas, textMode) => { .filter((line) => (ridgeVectorX === 0 ? line.x1 !== line.x2 : line.y1 !== line.y2)) .filter((line) => (line.x1 === ridge.x2 && line.y1 === ridge.y2) || (line.x2 === ridge.x2 && line.y2 === ridge.y2)) .forEach((line) => secondGableLines.push(line)) - - baseHipLines - .filter((line) => (ridgeVectorX === 0 ? line.x1 !== line.x2 : line.y1 !== line.y2)) .filter((line) => (line.x1 === ridge.x1 && line.y1 === ridge.y1) || (line.x2 === ridge.x1 && line.y2 === ridge.y1)) .forEach((line) => firstGableLines.push(line)) baseHipLines @@ -7579,12 +9165,15 @@ export const drawRidgeRoof = (roofId, canvas, textMode) => { hipBasePoint = { x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2 } point = [mergePoint[0].x, mergePoint[0].y, mergePoint[3].x, mergePoint[3].y] - const theta = Big(Math.acos(Big(line.line.attributes.planeSize).div( - line.line.attributes.actualSize === 0 || - line.line.attributes.actualSize === '' || - line.line.attributes.actualSize === undefined ? - line.line.attributes.planeSize : line.line.attributes.actualSize - ))) + const theta = Big( + Math.acos( + Big(line.line.attributes.planeSize).div( + line.line.attributes.actualSize === 0 || line.line.attributes.actualSize === '' || line.line.attributes.actualSize === undefined + ? line.line.attributes.planeSize + : line.line.attributes.actualSize, + ), + ), + ) .times(180) .div(Math.PI) .round(1) @@ -9228,11 +10817,9 @@ const getSortedPoint = (points, lines) => { const reCalculateSize = (line) => { const oldPlaneSize = line.attributes.planeSize const oldActualSize = line.attributes.actualSize - const theta = Big(Math.acos(Big(oldPlaneSize).div( - oldActualSize === 0 || oldActualSize === '' || oldActualSize === undefined ? - oldPlaneSize : - oldActualSize - ))) + const theta = Big( + Math.acos(Big(oldPlaneSize).div(oldActualSize === 0 || oldActualSize === '' || oldActualSize === undefined ? oldPlaneSize : oldActualSize)), + ) .times(180) .div(Math.PI) const planeSize = calcLinePlaneSize({