import { fabric } from 'fabric' import { QLine } from '@/components/fabric/QLine' import { getAdjacent, getDegreeByChon, isPointOnLine, isPointOnLineNew } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' const TWO_PI = Math.PI * 2 const EPSILON = 1e-10 //좌표계산 시 최소 차이값 export const defineQPolygon = () => { fabric.QPolygon.fromObject = function (object, callback) { fabric.Object._fromObject('QPolygon', object, callback, 'points') } } /** * point1에서 point2를 잇는 방향의 각도를 구한다. * @param point1 * @param point2 * @returns {number} */ export const calculateAngle = (point1, point2) => { const deltaX = Big(point2.x ?? 0) .minus(point1.x ?? 0) .toNumber() const deltaY = Big(point2.y ?? 0) .minus(point1.y ?? 0) .toNumber() const angleInRadians = Math.atan2(deltaY, deltaX) return Big(angleInRadians * (180 / Math.PI)) .round() .toNumber() } function inwardEdgeNormal(vertex1, vertex2) { // Assuming that polygon vertices are in clockwise order const dx = vertex2.x - vertex1.x const dy = vertex2.y - vertex1.y const edgeLength = Math.sqrt(dx * dx + dy * dy) return { x: -dy / edgeLength, y: dx / edgeLength, } } function outwardEdgeNormal(vertex1, vertex2) { const n = inwardEdgeNormal(vertex1, vertex2) return { x: -n.x, y: -n.y, } } function createPolygon(vertices) { const edges = [] let minX = vertices.length > 0 ? vertices[0].x : undefined let minY = vertices.length > 0 ? vertices[0].y : undefined let maxX = minX let maxY = minY for (let i = 0; i < vertices.length; i++) { const vertex1 = vertices[i] const vertex2 = vertices[(i + 1) % vertices.length] const outwardNormal = outwardEdgeNormal(vertex1, vertex2) const inwardNormal = inwardEdgeNormal(vertex1, vertex2) const edge = { vertex1, vertex2, index: i, outwardNormal, inwardNormal, } edges.push(edge) const x = vertices[i].x const y = vertices[i].y minX = Math.min(x, minX) minY = Math.min(y, minY) maxX = Math.max(x, maxX) maxY = Math.max(y, maxY) } return { vertices, edges, minX, minY, maxX, maxY, } } /** * edgeA와 edgeB가 마주치는 포인트의 좌표를 반환한다. * @param edgeA * @param edgeB * @returns {{x: *, y: *, isIntersectionOutside: boolean}|null} */ function edgesIntersection(edgeA, edgeB) { const den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y) if (den === 0) { return null // 선들이 평행하거나 일치합니다 } const ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den const ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den // 교차점이 두 선분의 범위 내에 있는지 확인 const isIntersectionOutside = ua < 0 || ub < 0 || ua > 1 || ub > 1 return { x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y), isIntersectionOutside, } } function appendArc(arcSegments, vertices, center, radius, startVertex, endVertex, isPaddingBoundary) { let startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x) let endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x) if (startAngle < 0) { startAngle += TWO_PI } if (endAngle < 0) { endAngle += TWO_PI } const angle = startAngle > endAngle ? startAngle - endAngle : startAngle + TWO_PI - endAngle const angleStep = (isPaddingBoundary ? -angle : TWO_PI - angle) / arcSegments vertices.push(startVertex) for (let i = 1; i < arcSegments; ++i) { const angle = startAngle + angleStep * i const vertex = { x: center.x + Math.cos(angle) * radius, y: center.y + Math.sin(angle) * radius, } vertices.push(vertex) } vertices.push(endVertex) } function createOffsetEdge(edge, dx, dy) { return { vertex1: { x: edge.vertex1.x + dx, y: edge.vertex1.y + dy, }, vertex2: { x: edge.vertex2.x + dx, y: edge.vertex2.y + dy, }, } } function createMarginPolygon(polygon, offset, arcSegments = 0) { const offsetEdges = [] for (let i = 0; i < polygon.edges.length; i++) { const edge = polygon.edges[i] const dx = edge.outwardNormal.x * offset const dy = edge.outwardNormal.y * offset offsetEdges.push(createOffsetEdge(edge, dx, dy)) } const vertices = [] for (let i = 0; i < offsetEdges.length; i++) { const thisEdge = offsetEdges[i] const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length] const vertex = edgesIntersection(prevEdge, thisEdge) if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) { vertices.push({ x: vertex.x, y: vertex.y, }) } else { const arcCenter = polygon.edges[i].vertex1 appendArc(arcSegments, vertices, arcCenter, offset, prevEdge.vertex2, thisEdge.vertex1, false) } } const marginPolygon = createPolygon(vertices) marginPolygon.offsetEdges = offsetEdges return marginPolygon } function createPaddingPolygon(polygon, offset, arcSegments = 0) { const offsetEdges = [] for (let i = 0; i < polygon.edges.length; i++) { const edge = polygon.edges[i] const dx = edge.inwardNormal.x * offset const dy = edge.inwardNormal.y * offset offsetEdges.push(createOffsetEdge(edge, dx, dy)) } const vertices = [] for (let i = 0; i < offsetEdges.length; i++) { const thisEdge = offsetEdges[i] const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length] const vertex = edgesIntersection(prevEdge, thisEdge) if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) { vertices.push({ x: vertex.x, y: vertex.y, }) } else { const arcCenter = polygon.edges[i].vertex1 appendArc(arcSegments, vertices, arcCenter, offset, prevEdge.vertex2, thisEdge.vertex1, true) } } const paddingPolygon = createPolygon(vertices) paddingPolygon.offsetEdges = offsetEdges return paddingPolygon } export default function offsetPolygon(vertices, offset) { const polygon = createPolygon(vertices) const arcSegments = 0 const originPolygon = new QPolygon(vertices, { fontSize: 0 }) originPolygon.setViewLengthText(false) if (offset > 0) { let result = createMarginPolygon(polygon, offset, arcSegments).vertices const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point)) if (allPointsOutside) { return createMarginPolygon(polygon, offset, arcSegments).vertices } else { return createPaddingPolygon(polygon, offset, arcSegments).vertices } } else { let result = createPaddingPolygon(polygon, offset, arcSegments).vertices const allPointsInside = result.every((point) => originPolygon.inPolygon(point)) if (allPointsInside) { return createPaddingPolygon(polygon, offset, arcSegments).vertices } else { return createMarginPolygon(polygon, offset, arcSegments).vertices } } } function normalizePoint(point) { return { x: Math.round(point.x), y: Math.round(point.y), } } function arePolygonsEqual(polygon1, polygon2) { if (polygon1.length !== polygon2.length) return false const normalizedPolygon1 = polygon1.map(normalizePoint).sort((a, b) => a.x - b.x || a.y - b.y) const normalizedPolygon2 = polygon2.map(normalizePoint).sort((a, b) => a.x - b.x || a.y - b.y) return normalizedPolygon1.every((point, index) => arePointsEqual(point, normalizedPolygon2[index])) } export function removeDuplicatePolygons(polygons, hasAuxiliaryLine = false) { let uniquePolygons = [] // x가 전부 같거나, y가 전부 같은 경우 제거 polygons.forEach((polygon) => { const isDuplicate = uniquePolygons.some((uniquePolygon) => arePolygonsEqual(polygon, uniquePolygon)) if (!isDuplicate) { uniquePolygons.push(polygon) } }) uniquePolygons = uniquePolygons.filter((polygon) => { return !checkPolygonSelfIntersection(polygon) }) // uniquePolygons = uniquePolygons.filter((polygon) => { // return isValidPoints(polygon) // }) return uniquePolygons } /** * 두 선분이 교차하는지 확인하는 함수 * @param {Object} p1 첫 번째 선분의 시작점 {x, y} * @param {Object} q1 첫 번째 선분의 끝점 {x, y} * @param {Object} p2 두 번째 선분의 시작점 {x, y} * @param {Object} q2 두 번째 선분의 끝점 {x, y} * @returns {boolean} 교차하면 true, 아니면 false */ function doSegmentsIntersect(p1, q1, p2, q2) { // CCW (Counter-Clockwise) 방향 확인 함수 function orientation(p, q, r) { const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y) if (val === 0) return 0 // 일직선 return val > 0 ? 1 : 2 // 시계방향 또는 반시계방향 } // 점 q가 선분 pr 위에 있는지 확인 function onSegment(p, q, r) { return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y) } // 같은 끝점을 공유하는 경우는 교차로 보지 않음 if ((p1.x === p2.x && p1.y === p2.y) || (p1.x === q2.x && p1.y === q2.y) || (q1.x === p2.x && q1.y === p2.y) || (q1.x === q2.x && q1.y === q2.y)) { return false } const o1 = orientation(p1, q1, p2) const o2 = orientation(p1, q1, q2) const o3 = orientation(p2, q2, p1) const o4 = orientation(p2, q2, q1) // 일반적인 교차 경우 if (o1 !== o2 && o3 !== o4) return true // 특별한 경우들 (한 점이 다른 선분 위에 있는 경우) if (o1 === 0 && onSegment(p1, p2, q1)) return true if (o2 === 0 && onSegment(p1, q2, q1)) return true if (o3 === 0 && onSegment(p2, p1, q2)) return true if (o4 === 0 && onSegment(p2, q1, q2)) return true return false } /** * 다각형의 자기 교차를 검사하는 메인 함수 * @param {Array} coordinates 다각형의 좌표 배열 [{x, y}, ...] * @returns {Object} 검사 결과 {hasSelfIntersection: boolean, intersections: Array} */ function checkPolygonSelfIntersection(coordinates) { if (coordinates.length < 3) { return { hasSelfIntersection: false, intersections: [], error: '다각형은 최소 3개의 점이 필요합니다.', } } // 모든 점이 같은 직선상에 있는지 검사 (degenerate polygon) const allSameX = coordinates.every((point) => point.x === coordinates[0].x) const allSameY = coordinates.every((point) => point.y === coordinates[0].y) if (allSameX || allSameY) { return true // 직선상의 점들은 유효하지 않은 다각형이므로 true 반환 } const intersections = [] const edges = [] // 모든 변(edge) 생성 for (let i = 0; i < coordinates.length; i++) { const start = coordinates[i] const end = coordinates[(i + 1) % coordinates.length] edges.push({ start: start, end: end, index: i, }) } // 모든 변 쌍에 대해 교차 검사 for (let i = 0; i < edges.length; i++) { for (let j = i + 1; j < edges.length; j++) { // 인접한 변들은 제외 (끝점을 공유하므로) if (Math.abs(i - j) === 1 || (i === 0 && j === edges.length - 1)) { continue } const edge1 = edges[i] const edge2 = edges[j] if (doSegmentsIntersect(edge1.start, edge1.end, edge2.start, edge2.end)) { intersections.push({ edge1Index: i, edge2Index: j, edge1: { from: edge1.start, to: edge1.end, }, edge2: { from: edge2.start, to: edge2.end, }, }) } } } return intersections.length > 0 } // 같은 직선상에 있는지 확인 같은 직선이라면 polygon을 생성할 수 없으므로 false const isValidPoints = (points) => { // 연속된 3개 이상의 점이 같은 x 또는 y 값을 가지는지 확인 (원형 배열로 처리) for (let i = 0; i < points.length; i++) { const point1 = points[i] const point2 = points[(i + 1) % points.length] const point3 = points[(i + 2) % points.length] // x값이 같은 연속된 3개 점 확인 if (point1.x === point2.x && point2.x === point3.x) { return false } // y값이 같은 연속된 3개 점 확인 if (point1.y === point2.y && point2.y === point3.y) { return false } } function isColinear(p1, p2, p3) { return (p2.x - p1.x) * (p3.y - p1.y) === (p3.x - p1.x) * (p2.y - p1.y) } function segmentsOverlap(a1, a2, b1, b2) { // 같은 직선 상에 있는가? if (!isColinear(a1, a2, b1) || !isColinear(a1, a2, b2)) { return false } const isHorizontal = a1.y === a2.y if (isHorizontal) { const aMin = Math.min(a1.x, a2.x), aMax = Math.max(a1.x, a2.x) const bMin = Math.min(b1.x, b2.x), bMax = Math.max(b1.x, b2.x) return Math.max(aMin, bMin) < Math.min(aMax, bMax) } else { const aMin = Math.min(a1.y, a2.y), aMax = Math.max(a1.y, a2.y) const bMin = Math.min(b1.y, b2.y), bMax = Math.max(b1.y, b2.y) return Math.max(aMin, bMin) < Math.min(aMax, bMax) } } for (let i = 0; i < points.length - 2; i++) { const a1 = points[i] const a2 = points[i + 1] const b1 = points[i + 1] // 연속되는 점 const b2 = points[i + 2] if (segmentsOverlap(a1, a2, b1, b2)) { return false } } return true } export const isSamePoint = (a, b) => { if (!a || !b) { return false } return Math.abs(Math.round(a.x) - Math.round(b.x)) <= 2 && Math.abs(Math.round(a.y) - Math.round(b.y)) <= 2 } /** * 박공지붕(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 = Math.max(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, currentDegree, currentDegree)) } } else if (analyze.isVertical) { //현재라인이 수직선일때 if (isVertical) { //같은방향 처리 innerLines.push(drawRoofLine(points, canvas, roof, textMode)) } else { //다른방향 처리 innerLines.push(drawHipLine(points, canvas, roof, textMode, 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 * @param canvas * @param textMode */ export const drawShedRoof = (roofId, canvas, textMode) => { const roof = canvas?.getObjects().find((object) => object.id === roofId) const hasNonParallelLines = roof.lines.filter((line) => Math.abs(line.x1 - line.x2) > 1 && Math.abs(line.y1 - line.y2) > 1) if (hasNonParallelLines.length > 0) { // alert('대각선이 존재합니다.') return } const sheds = roof.lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.SHED) const gables = roof.lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.GABLE) const eaves = roof.lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.EAVES) const shedDegree = getDegreeByChon(sheds[0].attributes.pitch) gables.forEach( (gable) => (gable.attributes.actualSize = calcLineActualSize( { x1: gable.x1, y1: gable.y1, x2: gable.x2, y2: gable.y2, }, shedDegree, )), ) const pitchSizeLines = [] sheds.forEach((shed) => { let points = [] let x1, y1, x2, y2 const signX = Math.sign(shed.x1 - shed.x2) if (signX !== 0) { points.push(shed.x1, shed.x2) y1 = shed.y1 } else { points.push(shed.y1, shed.y2) x1 = shed.x1 } eaves.forEach((eave) => { if (signX !== 0) { points.push(eave.x1, eave.x2) points.sort((a, b) => a - b) x1 = (points[1] + points[2]) / 2 x2 = (points[1] + points[2]) / 2 y2 = eave.y1 } else { points.push(eave.y1, eave.y2) points.sort((a, b) => a - b) y1 = (points[1] + points[2]) / 2 y2 = (points[1] + points[2]) / 2 x2 = eave.x1 } points.sort((a, b) => a - b) const actualSize = calcLineActualSize({ x1, y1, x2, y2 }, shedDegree) const line = new QLine([x1, y1, x2, y2], { parentId: roof.id, stroke: '#000000', strokeWidth: 2, strokeDashArray: [5, 5], selectable: false, fontSize: roof.fontSize, textMode: textMode, attributes: { roofId: roof.id, type: 'pitchSizeLine', planeSize: actualSize, actualSize: actualSize, }, }) pitchSizeLines.push(line) }) }) const maxLine = pitchSizeLines.reduce((prev, current) => (prev.length > current.length ? prev : current), pitchSizeLines[0]) canvas.add(maxLine) canvas.renderAll() } /** * 변별로 설정된 지붕을 그린다. * @param roofId * @param canvas * @param textMode */ export const drawRoofByAttribute = (roofId, canvas, textMode) => { const TYPES = { HIP: 'hip', RIDGE: 'ridge', GABLE_LINE: 'gableLine', NEW: 'new' } 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 zeroLines = [] wall.baseLines.forEach((line, index) => (line.attributes.planeSize < EPSILON ? zeroLines.push(index) : null)) let baseLines = [] if (zeroLines.length > 0) { //형이동으로 인해 뭉쳐지는 라인을 찾는다. const mergedLines = [] zeroLines.forEach((zeroIndex) => { const prevIndex = (zeroIndex - 1 + wall.baseLines.length) % wall.baseLines.length const nextIndex = (zeroIndex + 1) % wall.baseLines.length mergedLines.push({ prevIndex, nextIndex }) }) const proceedPair = [] wall.baseLines.forEach((line, index) => { //뭉쳐지는 라인이면 하나로 합치고 baseLines에 추가. 아니면 baseLines에 바로 추가. if (!zeroLines.includes(index) && !mergedLines.find((mergedLine) => mergedLine.prevIndex === index || mergedLine.nextIndex === index)) { baseLines.push(line) } else { if (zeroLines.includes(index)) { mergedLines.forEach((indexPair) => { //이미 처리된 라인이편 패스 if (proceedPair.includes(indexPair.prevIndex) || proceedPair.includes(indexPair.nextIndex)) return let prevLine = wall.baseLines[indexPair.prevIndex] const lineVector = { x: Math.sign(prevLine.x2 - prevLine.x1), y: Math.sign(prevLine.y2 - prevLine.y1) } //중복되는 지붕선을 병합한다. 라인 vector가 같아야한다. const indexList = [indexPair.prevIndex, indexPair.nextIndex] mergedLines .filter((pair) => pair !== indexPair) .forEach((pair) => { const pLine = wall.baseLines[pair.prevIndex] const nLine = wall.baseLines[pair.nextIndex] const pVector = { x: Math.sign(pLine.x2 - pLine.x1), y: Math.sign(pLine.y2 - pLine.y1) } const nVector = { x: Math.sign(nLine.x2 - nLine.x1), y: Math.sign(nLine.y2 - nLine.y1) } if ( pVector.x === lineVector.x && pVector.y === lineVector.y && nVector.x === lineVector.x && nVector.y === lineVector.y && (indexList.includes(pair.prevIndex) || indexList.includes(pair.nextIndex)) ) { indexList.push(pair.prevIndex, pair.nextIndex) } }) const startLine = wall.baseLines[Math.min(...indexList)] const endLine = wall.baseLines[Math.max(...indexList)] const points = [startLine.x1, startLine.y1, endLine.x2, endLine.y2] const size = calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3] }) //라인 좌표 조정. startLine.set({ x1: points[0], y1: points[1], x2: points[2], y2: points[3], startPoint: { x: points[0], y: points[1] }, endPoint: { x: points[2], y: points[3] }, }) startLine.setCoords() startLine.attributes.planeSize = size startLine.attributes.actualSize = size startLine.fire('setLength') //처리된 index 추가. proceedPair.push(...indexList) //조정된라인 baseLine에 추가. baseLines.push(startLine) }) } } }) } else { baseLines = wall.baseLines } const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) const checkWallPolygon = new QPolygon(baseLinePoints, {}) 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, } } /** * * @param testPoint * @param prevLine * @param nextLine * @param baseLines */ const getRidgeDrivePoint = (testPoint = [], prevLine, nextLine, baseLines) => { if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES && nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) return null let prevPoint = null, nextPoint = null const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) const checkWallPolygon = new QPolygon(baseLinePoints, {}) const checkEdge = { vertex1: { x: testPoint[0], y: testPoint[1] }, vertex2: { x: testPoint[2], y: testPoint[3] } } let prevHasGable = false, nextHasGable = false if (prevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { const index = baseLines.findIndex((line) => line === prevLine) const beforePrevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] if (!beforePrevLine) { prevPoint = null } else if (beforePrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { const analyze = analyzeLine(beforePrevLine) const roofEdge = { vertex1: { x: analyze.roofLine.x1, y: analyze.roofLine.y1 }, vertex2: { x: analyze.roofLine.x2, y: analyze.roofLine.y2 } } const intersection = edgesIntersection(checkEdge, roofEdge) if (intersection) { prevHasGable = true const distance = Math.sqrt((intersection.x - testPoint[0]) ** 2 + (intersection.y - testPoint[1]) ** 2) prevPoint = { intersection, distance } } } else if (beforePrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { let prevVector = getHalfAngleVector(beforePrevLine, prevLine) const prevCheckPoint = { x: prevLine.x1 + prevVector.x * 10, y: prevLine.y1 + prevVector.y * 10, } let prevHipVector if (checkWallPolygon.inPolygon(prevCheckPoint)) { prevHipVector = { x: prevVector.x, y: prevVector.y } } else { prevHipVector = { x: -prevVector.x, y: -prevVector.y } } const prevHipEdge = { vertex1: { x: prevLine.x1, y: prevLine.y1 }, vertex2: { x: prevLine.x1 + prevHipVector.x * 10, y: prevLine.y1 + prevHipVector.y * 10 }, } const intersection = edgesIntersection(prevHipEdge, checkEdge) if (intersection) { const checkVector = { x: Math.sign(testPoint[0] - testPoint[2]), y: Math.sign(testPoint[1] - testPoint[3]) } const intersectVector = { x: Math.sign(testPoint[0] - intersection.x), y: Math.sign(testPoint[1] - intersection.y) } if (checkVector.x === intersectVector.x && checkVector.y === intersectVector.y) { const distance = Math.sqrt((intersection.x - testPoint[0]) ** 2 + (intersection.y - testPoint[1]) ** 2) prevPoint = { intersection, distance } } if (almostEqual(intersection.x, testPoint[0]) && almostEqual(intersection.y, testPoint[1])) { prevPoint = { intersection, distance: 0 } } } } } if (nextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { const index = baseLines.findIndex((line) => line === nextLine) const afterNextLine = baseLines[(index + 1) % baseLines.length] if (!afterNextLine) { nextPoint = null } else if (afterNextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { const analyze = analyzeLine(afterNextLine) const roofEdge = { vertex1: { x: analyze.roofLine.x1, y: analyze.roofLine.y1 }, vertex2: { x: analyze.roofLine.x2, y: analyze.roofLine.y2 } } const intersection = edgesIntersection(checkEdge, roofEdge) if (intersection) { nextHasGable = true const distance = Math.sqrt((intersection.x - testPoint[2]) ** 2 + (intersection.y - testPoint[3]) ** 2) nextPoint = { intersection, distance } } } else if (afterNextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { let nextVector = getHalfAngleVector(nextLine, afterNextLine) const nextCheckPoint = { x: nextLine.x2 + nextVector.x * 10, y: nextLine.y2 + nextVector.y * 10, } let nextHipVector if (checkWallPolygon.inPolygon(nextCheckPoint)) { nextHipVector = { x: nextVector.x, y: nextVector.y } } else { nextHipVector = { x: -nextVector.x, y: -nextVector.y } } const nextHipEdge = { vertex1: { x: nextLine.x2, y: nextLine.y2 }, vertex2: { x: nextLine.x2 + nextHipVector.x * 10, y: nextLine.y2 + nextHipVector.y * 10 }, } const intersection = edgesIntersection(nextHipEdge, checkEdge) if (intersection) { const checkVector = { x: Math.sign(testPoint[0] - testPoint[2]), y: Math.sign(testPoint[1] - testPoint[3]) } const intersectVector = { x: Math.sign(testPoint[0] - intersection.x), y: Math.sign(testPoint[1] - intersection.y) } if (checkVector.x === intersectVector.x && checkVector.y === intersectVector.y) { const distance = Math.sqrt((intersection.x - testPoint[0]) ** 2 + (intersection.y - testPoint[1]) ** 2) nextPoint = { intersection, distance } } if (almostEqual(intersection.x, testPoint[0]) && almostEqual(intersection.y, testPoint[1])) { nextPoint = { intersection, distance: 0 } } } } } if (prevHasGable || nextHasGable) { const prevLength = Math.sqrt((prevLine.x1 - prevLine.x2) ** 2 + (prevLine.y1 - prevLine.y2) ** 2) const nextLength = Math.sqrt((nextLine.x1 - nextLine.x2) ** 2 + (nextLine.y1 - nextLine.y2) ** 2) if (prevPoint && prevHasGable && prevLength <= nextLength) { return prevPoint.intersection } if (nextPoint && nextHasGable && prevLength > nextLength) { return nextPoint.intersection } } if (prevPoint && nextPoint) { return prevPoint.distance < nextPoint.distance ? prevPoint.intersection : nextPoint.intersection } else if (prevPoint) { return prevPoint.intersection } else if (nextPoint) { return nextPoint.intersection } else { return null } } /** * 지붕의 모양을 판단하여 각 변에 맞는 라인을 처리 한다. */ 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) //양옆이 케라바가 아닐때 제외 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 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 prevIndex = baseLines.findIndex((baseLine) => baseLine === prevLine) const beforePrevIndex = baseLines.findIndex((baseLine) => baseLine === baseLines[(prevIndex - 1 + baseLines.length) % baseLines.length]) const nextIndex = baseLines.findIndex((baseLine) => baseLine === nextLine) const afterNextIndex = baseLines.findIndex((baseLine) => baseLine === baseLines[(nextIndex + 1) % baseLines.length]) linesAnalysis.push( { start: { x: points1[0], y: points1[1] }, end: { x: points1[2], y: points1[3] }, left: beforePrevIndex, right: prevIndex, type: TYPES.HIP, degree: shedDegree, }, { start: { x: points2[0], y: points2[1] }, end: { x: points2[2], y: points2[3] }, left: nextIndex, right: afterNextIndex, type: TYPES.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) //양옆이 처마가 아닐때 패스 if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) { return } let beforePrevLine, afterNextLine baseLines.forEach((baseLine, index) => { if (baseLine === prevLine) { beforePrevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] } if (baseLine === nextLine) { afterNextLine = baseLines[(index + 1) % baseLines.length] } }) const prevLineVector = { x: Math.sign(prevLine.x1 - prevLine.x2), y: Math.sign(prevLine.y1 - prevLine.y2) } const nextLineVector = { x: Math.sign(nextLine.x1 - nextLine.x2), y: Math.sign(nextLine.y1 - nextLine.y2) } //반절마루 생성불가이므로 지붕선만 추가하고 끝냄 if (prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y) { return } 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, prevDegree, prevDegree)) innerLines.push(drawHipLine(nextHipPoint, canvas, roof, textMode, 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, prevDegree, prevDegree)) } if (nextGableLength >= 1) { innerLines.push(drawHipLine(nextGablePoint, canvas, roof, textMode, 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 const point = [midPoint.x, midPoint.y, intersect.x, intersect.y] //마루가 최대로 뻗어나갈수 있는 포인트. null일때는 맞은편 지붕선 까지로 판단한다. const drivePoint = getRidgeDrivePoint(point, prevLine, nextLine, baseLines) const isOverlapBefore = analyze.isHorizontal ? almostEqual(Math.min(beforePrevLine.x1, beforePrevLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(beforePrevLine.x1, beforePrevLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(beforePrevLine.y1, beforePrevLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(beforePrevLine.y1, beforePrevLine.y2), Math.max(currentLine.y1, currentLine.y2)) const isOverlapAfter = analyze.isHorizontal ? almostEqual(Math.min(afterNextLine.x1, afterNextLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(afterNextLine.x1, afterNextLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(afterNextLine.y1, afterNextLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(afterNextLine.y1, afterNextLine.y2), Math.max(currentLine.y1, currentLine.y2)) if (isOverlapBefore || isOverlapAfter) { const oppLine = isOverlapBefore ? beforePrevLine : afterNextLine const otherGable = baseLines.find( (l) => l !== currentLine && l !== prevLine && l !== nextLine && l !== oppLine && l.attributes.type === LINE_TYPE.WALLLINE.GABLE, ) const pointVector = { x: Math.sign(clamp01(point[0] - point[2])), y: Math.sign(clamp01(point[1] - point[3])) } if (!otherGable) { let offset = 0 switch (oppLine.attributes.type) { case LINE_TYPE.WALLLINE.HIPANDGABLE: offset = oppLine.attributes.width break case LINE_TYPE.WALLLINE.JERKINHEAD: offset = oppLine.attributes.width / 2 break case LINE_TYPE.WALLLINE.WALL: offset = 0 break default: break } point[2] += pointVector.x * offset point[3] += pointVector.y * offset } else { if (drivePoint) { point[2] = drivePoint.x point[3] = drivePoint.y } } } else if (drivePoint) { point[2] = drivePoint.x point[3] = drivePoint.y } linesAnalysis.push({ start: { x: point[0], y: point[1] }, end: { x: point[2], y: point[3] }, left: baseLines.findIndex((line) => line === prevLine), right: baseLines.findIndex((line) => line === nextLine), type: TYPES.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: TYPES.RIDGE, degree: 0, }) } } } }) eaves.sort((a, b) => a.attributes.planeSize - b.attributes.planeSize) //3. 처마지붕 판단, 처마는 옆이 처마여야한다. const ridgeEaves = eaves.filter((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 prevLineVector = { x: Math.sign(prevLine.x1 - prevLine.x2), y: Math.sign(prevLine.y1 - prevLine.y2) } const nextLineVector = { x: Math.sign(nextLine.x1 - nextLine.x2), y: Math.sign(nextLine.y1 - nextLine.y2) } const inPolygonPoint = { x: (currentLine.x1 + currentLine.x2) / 2 + Math.sign(nextLine.x2 - nextLine.x1), y: (currentLine.y1 + currentLine.y2) / 2 + Math.sign(nextLine.y2 - nextLine.y1), } //좌우 라인이 서로 다른방향이고 지붕 안쪽으로 들어가지 않을때 const isAbleShape = (prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y) && checkWallPolygon.inPolygon(inPolygonPoint) const isAbleAttribute = prevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES && nextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES return isAbleAttribute && isAbleShape }) const hipedEaves = eaves.filter((line) => !ridgeEaves.includes(line)) ridgeEaves.sort((a, b) => a.attributes.planeSize - b.attributes.planeSize) let proceedEaves = [] // left: 이전, right:다음, point:그려지는 포인트, length:길이 let proceedRidges = [] // left: 이전, right:다음, point:그려지는 포인트, length:길이 ridgeEaves.forEach((currentLine) => { let prevLine, nextLine, currentI, prevI, nextI baseLines.forEach((baseLine, index) => { if (baseLine === currentLine) { currentI = index prevI = (index - 1 + baseLines.length) % baseLines.length nextI = (index + 1) % baseLines.length } }) prevLine = baseLines[prevI] nextLine = baseLines[nextI] let beforePrevLine, afterNextLine baseLines.forEach((baseLine, index) => { if (baseLine === prevLine) { beforePrevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] } if (baseLine === nextLine) { afterNextLine = baseLines[(index + 1) % baseLines.length] } }) const analyze = analyzeLine(currentLine) let pHipVector = getHalfAngleVector(currentLine, prevLine) let nHipVector = getHalfAngleVector(currentLine, nextLine) const pCheckPoint = { x: currentLine.x1 + (pHipVector.x * 10) / 2, y: currentLine.y1 + (pHipVector.y * 10) / 2, } const nCheckPoint = { x: currentLine.x2 + (nHipVector.x * 10) / 2, y: currentLine.y2 + (nHipVector.y * 10) / 2, } if (!checkWallPolygon.inPolygon(pCheckPoint)) { pHipVector = { x: -pHipVector.x, y: -pHipVector.y } } if (!checkWallPolygon.inPolygon(nCheckPoint)) { nHipVector = { x: -nHipVector.x, y: -nHipVector.y } } const prevCheckPoint = [currentLine.x1, currentLine.y1, currentLine.x1 + pHipVector.x * 1000, currentLine.y1 + pHipVector.y * 1000] const nextCheckPoint = [currentLine.x2, currentLine.y2, currentLine.x2 + nHipVector.x * 1000, currentLine.y2 + nHipVector.y * 1000] const findRoofPoints = (points) => { const hipEdge = { vertex1: { x: points[0], y: points[1] }, vertex2: { x: points[2], y: points[3] } } 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) || (intersectVector.x === 0 && intersectVector.y === 0) ) { 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) if (isForwardPoints.length > 0 && isBackwardPoints.length > 0) { return { forward: isForwardPoints[0].intersect, backward: isBackwardPoints[0].intersect } } else if (isForwardPoints.length === 0 && isBackwardPoints.length > 1) { return { forward: isBackwardPoints[0].intersect, backward: isBackwardPoints[1].intersect } } else if (isForwardPoints.length > 1 && isBackwardPoints.length === 0) { return { forward: isForwardPoints[1].intersect, backward: isForwardPoints[0].intersect } } else { return null } } const pRoofPoints = findRoofPoints(prevCheckPoint) const nRoofPoints = findRoofPoints(nextCheckPoint) let prevHipPoint = { x1: pRoofPoints.backward.x, y1: pRoofPoints.backward.y, x2: pRoofPoints.forward.x, y2: pRoofPoints.forward.y } let nextHipPoint = { x1: nRoofPoints.backward.x, y1: nRoofPoints.backward.y, x2: nRoofPoints.forward.x, y2: nRoofPoints.forward.y } const prevEdge = { vertex1: { x: prevHipPoint.x1, y: prevHipPoint.y1 }, vertex2: { x: prevHipPoint.x2, y: prevHipPoint.y2 } } const nextEdge = { vertex1: { x: nextHipPoint.x1, y: nextHipPoint.y1 }, vertex2: { x: nextHipPoint.x2, y: nextHipPoint.y2 } } const intersect = edgesIntersection(prevEdge, nextEdge) if (intersect && isPointOnLineNew(prevHipPoint, intersect) && isPointOnLineNew(nextHipPoint, intersect)) { prevHipPoint.x2 = intersect.x prevHipPoint.y2 = intersect.y nextHipPoint.x2 = intersect.x nextHipPoint.y2 = intersect.y } let isRidgePrev, isRidgeNext let minPrevDist = Infinity let minNextDist = Infinity proceedRidges.forEach((ridge) => { const ridgeEdge = { vertex1: { x: ridge.point.x1, y: ridge.point.y1 }, vertex2: { x: ridge.point.x2, y: ridge.point.y2 } } const isPrev = edgesIntersection(ridgeEdge, prevEdge) const isNext = edgesIntersection(ridgeEdge, nextEdge) if ( isPrev && isPointOnLineNew(prevHipPoint, isPrev) && (ridge.prev === prevI || ridge.prev === nextI || ridge.next === prevI || ridge.next === nextI) ) { const distance = Math.sqrt((isPrev.x - prevHipPoint.x1) ** 2 + (isPrev.y - prevHipPoint.y1) ** 2) if (distance < minPrevDist) { minPrevDist = distance isRidgePrev = isPrev } } if ( isNext && isPointOnLineNew(nextHipPoint, isNext) && (ridge.prev === prevI || ridge.prev === nextI || ridge.next === prevI || ridge.next === nextI) ) { const distance = Math.sqrt((isNext.x - nextHipPoint.x1) ** 2 + (isNext.y - nextHipPoint.y1) ** 2) if (distance < minNextDist) { minNextDist = distance isRidgeNext = isNext } } }) //접하는 라인에서 파생된 마루선이 겹칠경우 라인보정을 종료 if (isRidgePrev) { prevHipPoint = { x1: pRoofPoints.backward.x, y1: pRoofPoints.backward.y, x2: pRoofPoints.forward.x, y2: pRoofPoints.forward.y } } if (isRidgeNext) { nextHipPoint = { x1: nRoofPoints.backward.x, y1: nRoofPoints.backward.y, x2: nRoofPoints.forward.x, y2: nRoofPoints.forward.y } } let prevHipLength = Math.sqrt((prevHipPoint.x2 - prevHipPoint.x1) ** 2 + (prevHipPoint.y2 - prevHipPoint.y1) ** 2) let nextHipLength = Math.sqrt((nextHipPoint.x2 - nextHipPoint.x1) ** 2 + (nextHipPoint.y2 - nextHipPoint.y1) ** 2) const alreadyPrev = proceedEaves.filter((line) => almostEqual(line.point.x1, prevHipPoint.x1) && almostEqual(line.point.y1, prevHipPoint.y1)) const alreadyNext = proceedEaves.filter((line) => almostEqual(line.point.x1, nextHipPoint.x1) && almostEqual(line.point.y1, nextHipPoint.y1)) if (alreadyPrev.length) { alreadyPrev.sort((a, b) => a.length - b.length) const alrPrev = alreadyPrev[0] if (isRidgePrev) { alrPrev.prev = prevI alrPrev.current = currentI alrPrev.point = prevHipPoint alrPrev.length = prevHipLength } else { if (prevHipLength < alrPrev.length) { //겹치는데 지금 만들어지는 라인이 더 짧은경우 다른 라인제거 하고 현재 라인을 추가. alrPrev.prev = prevI alrPrev.current = currentI alrPrev.point = prevHipPoint alrPrev.length = prevHipLength } else { prevHipPoint = alrPrev.point } } } else { proceedEaves.push({ prev: prevI, current: currentI, point: prevHipPoint, length: prevHipLength }) } if (alreadyNext.length) { alreadyNext.sort((a, b) => a.length - b.length) const alrNext = alreadyNext[0] if (isRidgeNext) { alrNext.prev = currentI alrNext.current = nextI alrNext.point = nextHipPoint alrNext.length = nextHipLength } else { if (nextHipLength < alrNext.length) { //겹치는데 지금 만들어지는 라인이 더 짧은경우 다른 라인제거 하고 현재 라인을 추가. alrNext.prev = currentI alrNext.current = nextI alrNext.point = nextHipPoint alrNext.length = nextHipLength } else { nextHipPoint = alrNext.point } } } else { proceedEaves.push({ prev: currentI, current: nextI, point: nextHipPoint, length: nextHipLength }) } let ridgePoint if (almostEqual(prevHipPoint.x2, nextHipPoint.x2) && almostEqual(prevHipPoint.y2, nextHipPoint.y2)) { const ridgeStartPoint = { x: prevHipPoint.x2, y: prevHipPoint.y2 } let ridgeVector = { x: Math.sign(clamp01(nextLine.x2 - nextLine.x1)), y: Math.sign(clamp01(nextLine.y2 - nextLine.y1)) } const midX = (currentLine.x1 + currentLine.x2) / 2 const midY = (currentLine.y1 + currentLine.y2) / 2 let checkPoint = { x: midX + ridgeVector.x, y: midY + ridgeVector.y } if (!checkWallPolygon.inPolygon(checkPoint)) { ridgeVector = { x: -ridgeVector.x, y: -ridgeVector.y } } ridgePoint = { x1: ridgeStartPoint.x, y1: ridgeStartPoint.y, x2: ridgeStartPoint.x + ridgeVector.x, y2: ridgeStartPoint.y + ridgeVector.y } const ridgeEdge = { vertex1: { x: ridgePoint.x1, y: ridgePoint.y1 }, vertex2: { x: ridgePoint.x2, y: ridgePoint.y2 } } let roofIs let minDistance = Infinity roof.lines.forEach((line) => { const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } const intersect = edgesIntersection(lineEdge, ridgeEdge) if (intersect && isPointOnLineNew(line, intersect)) { const isVector = { x: Math.sign(clamp01(intersect.x - ridgeStartPoint.x)), y: Math.sign(clamp01(intersect.y - ridgeStartPoint.y)) } const distance = Math.sqrt(Math.pow(ridgeStartPoint.x - intersect.x, 2) + Math.pow(ridgeStartPoint.y - intersect.y, 2)) if (distance < minDistance && isVector.x === ridgeVector.x && isVector.y === ridgeVector.y) { minDistance = distance roofIs = intersect } } }) if (roofIs) { ridgePoint.x2 = roofIs.x ridgePoint.y2 = roofIs.y } const drivePoint = getRidgeDrivePoint([ridgePoint.x1, ridgePoint.y1, ridgePoint.x2, ridgePoint.y2], prevLine, nextLine, baseLines) const isOverlapBefore = analyze.isHorizontal ? almostEqual(Math.min(beforePrevLine.x1, beforePrevLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(beforePrevLine.x1, beforePrevLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(beforePrevLine.y1, beforePrevLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(beforePrevLine.y1, beforePrevLine.y2), Math.max(currentLine.y1, currentLine.y2)) const isOverlapAfter = analyze.isHorizontal ? almostEqual(Math.min(afterNextLine.x1, afterNextLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(afterNextLine.x1, afterNextLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(afterNextLine.y1, afterNextLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(afterNextLine.y1, afterNextLine.y2), Math.max(currentLine.y1, currentLine.y2)) if (isOverlapBefore || isOverlapAfter) { const oppLine = isOverlapBefore ? beforePrevLine : afterNextLine const otherGable = baseLines.find( (l) => l !== currentLine && l !== prevLine && l !== nextLine && l !== oppLine && l.attributes.type === LINE_TYPE.WALLLINE.GABLE, ) const pointVector = { x: Math.sign(clamp01(ridgePoint.x1 - ridgePoint.x2)), y: Math.sign(clamp01(ridgePoint.y1 - ridgePoint.y2)) } let offset = 0 switch (oppLine.attributes.type) { case LINE_TYPE.WALLLINE.HIPANDGABLE: offset = oppLine.attributes.width break case LINE_TYPE.WALLLINE.JERKINHEAD: offset = oppLine.attributes.width / 2 break case LINE_TYPE.WALLLINE.WALL: offset = 0 break default: break } if (!otherGable) { if (oppLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { const oppIndex = baseLines.findIndex((l) => l === oppLine) const oppPrevLine = baseLines[(oppIndex - 1 + baseLines.length) % baseLines.length] const oppNextLine = baseLines[(oppIndex + 1) % baseLines.length] if (oppPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES || oppNextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { const currentLength = Math.sqrt(Math.pow(currentLine.x1 - currentLine.x2, 2) + Math.pow(currentLine.y1 - currentLine.y2, 2)) const oppLength = Math.sqrt(Math.pow(oppLine.x1 - oppLine.x2, 2) + Math.pow(oppLine.y1 - oppLine.y2, 2)) if (almostEqual(currentLength, oppLength)) { if (drivePoint) { ridgePoint.x2 = drivePoint.x ridgePoint.y2 = drivePoint.y } } else { ridgePoint.x2 += pointVector.x * offset ridgePoint.y2 += pointVector.y * offset } } } else { ridgePoint.x2 += pointVector.x * offset ridgePoint.y2 += pointVector.y * offset } // } } else if (drivePoint) { ridgePoint.x2 = drivePoint.x ridgePoint.y2 = drivePoint.y } } else if (drivePoint) { ridgePoint.x2 = drivePoint.x ridgePoint.y2 = drivePoint.y } } if (ridgePoint) { const alreadyRidge = proceedRidges.find( (line) => almostEqual(line.point.x1, ridgePoint.x1) && almostEqual(line.point.y1, ridgePoint.y1) && almostEqual(line.point.x2, ridgePoint.x2) && almostEqual(line.point.y2, ridgePoint.y2), ) if (!alreadyRidge) { proceedRidges.push({ prev: prevI, next: nextI, point: ridgePoint }) } } canvas .getObjects() .filter((o) => o.name === 'check') .forEach((o) => canvas.remove(o)) canvas.renderAll() }) hipedEaves.forEach((currentLine) => { let prevLine baseLines.forEach((baseLine, index) => { if (baseLine === currentLine) { prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] } }) const analyze = analyzeLine(currentLine) // 옆이 처마가 아니면 패스 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 * 10) / 2, y: currentLine.y1 + (checkVector.y * 10) / 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) || (intersectVector.x === 0 && intersectVector.y === 0) ) { 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) let hipPoint if (isForwardPoints.length > 0 && isBackwardPoints.length > 0) { hipPoint = { x1: isBackwardPoints[0].intersect.x, y1: isBackwardPoints[0].intersect.y, x2: isForwardPoints[0].intersect.x, y2: isForwardPoints[0].intersect.y, } } else { if (isBackwardPoints.length === 0 && isForwardPoints.length > 1) { hipPoint = { x1: isForwardPoints[0].intersect.x, y1: isForwardPoints[0].intersect.y, x2: isForwardPoints[1].intersect.x, y2: isForwardPoints[1].intersect.y, } } if (isForwardPoints.length === 0 && isBackwardPoints.length > 1) { hipPoint = { x1: isBackwardPoints[0].intersect.x, y1: isBackwardPoints[0].intersect.y, x2: isBackwardPoints[1].intersect.x, y2: isBackwardPoints[1].intersect.y, } } } if (hipPoint) { const alreadyLine = proceedEaves.filter((line) => almostEqual(line.point.x1, hipPoint.x1) && almostEqual(line.point.y1, hipPoint.y1)) //겹쳐지는 라인이 있는경우 조정한다. if (alreadyLine.length === 0) { linesAnalysis.push({ start: { x: hipPoint.x1, y: hipPoint.y1 }, end: { x: hipPoint.x2, y: hipPoint.y2 }, left: baseLines.findIndex((line) => line === prevLine), right: baseLines.findIndex((line) => line === currentLine), type: TYPES.HIP, degree: currentDegree, }) } } }) //작성된 라인을 analyze에 추가한다. const pIndexEaves = [] proceedEaves.forEach((eaves, index) => { if (pIndexEaves.includes(index)) return const { prev, current, point } = eaves const currentDegree = getDegreeByChon(baseLines[current].attributes.pitch) const jointEaves = proceedEaves .filter((e) => e !== eaves) .filter((e) => almostEqual(e.point.x2, eaves.point.x2) && almostEqual(e.point.y2, eaves.point.y2)) if (jointEaves.length === 1 && ridgeEaves.length > 1) { innerLines.push(drawHipLine([point.x1, point.y1, point.x2, point.y2], canvas, roof, textMode, currentDegree, currentDegree)) pIndexEaves.push(index) } else if (jointEaves.length === 2) { const jointIndex = [index] let jointLines = [] jointEaves.forEach((e) => jointIndex.push(proceedEaves.findIndex((p) => p === e))) const jointVectors = [] //연결된 라인 생성 jointIndex.forEach((i) => { const ev = proceedEaves[i] const degree = getDegreeByChon(baseLines[ev.current].attributes.pitch) innerLines.push(drawHipLine([ev.point.x1, ev.point.y1, ev.point.x2, ev.point.y2], canvas, roof, textMode, degree, degree)) pIndexEaves.push(i) jointLines.push(ev.prev, ev.current) jointVectors.push({ x: Math.sign(ev.point.x2 - ev.point.x1), y: Math.sign(ev.point.y2 - ev.point.y1) }) }) //연결된 지점에서 파생된 마루선 제거 const removeRidge = proceedRidges.filter((ridge) => almostEqual(ridge.point.x1, eaves.point.x2) && almostEqual(ridge.point.y1, eaves.point.y2)) proceedRidges = proceedRidges.filter((ridge) => !removeRidge.includes(ridge)) let dneVector = jointVectors.find((v) => !jointVectors.find((v2) => v2.x === -v.x && v2.y === -v.y)) const findRoofEdge = { vertex1: { x: eaves.point.x2, y: eaves.point.y2 }, vertex2: { x: eaves.point.x2 + dneVector.x, y: eaves.point.y2 + dneVector.y }, } let minDistance = Infinity let isPoint roof.lines.forEach((line) => { const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } const intersect = edgesIntersection(lineEdge, findRoofEdge) if (intersect && isPointOnLineNew(line, intersect)) { const distance = Math.sqrt(Math.pow(intersect.x - eaves.point.x2, 2) + Math.pow(intersect.y - eaves.point.y2, 2)) if (distance < minDistance) { minDistance = distance isPoint = intersect } } }) if (isPoint) { const countMap = new Map() jointLines.forEach((value) => { countMap.set(value, (countMap.get(value) || 0) + 1) }) const uniqueLine = jointLines.filter((value) => countMap.get(value) === 1) linesAnalysis.push({ start: { x: eaves.point.x2, y: eaves.point.y2 }, end: { x: isPoint.x, y: isPoint.y }, left: uniqueLine[0], right: uniqueLine[1], type: TYPES.HIP, degree: currentDegree, }) } } else { linesAnalysis.push({ start: { x: point.x1, y: point.y1 }, end: { x: point.x2, y: point.y2 }, left: prev, right: current, type: TYPES.HIP, degree: currentDegree, }) pIndexEaves.push(index) } }) proceedRidges.forEach(({ prev, next, point }) => linesAnalysis.push({ start: { x: point.x1, y: point.y1 }, end: { x: point.x2, y: point.y2 }, left: prev, right: next, type: TYPES.RIDGE, degree: 0, }), ) //4. 반절처(반절마루) 판단, 반절마루는 양옆이 처마여야 한다. jerkinHeads.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] } }) //양옆이 처마가 아닐때 패스 if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) { return } let beforePrevLine, afterNextLine baseLines.forEach((baseLine, index) => { if (baseLine === prevLine) { beforePrevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] } if (baseLine === nextLine) { afterNextLine = baseLines[(index + 1) % baseLines.length] } }) const analyze = analyzeLine(currentLine) const roofPoints = [analyze.roofLine.x1, analyze.roofLine.y1, analyze.roofLine.x2, analyze.roofLine.y2] const prevLineVector = { x: Math.sign(prevLine.x1 - prevLine.x2), y: Math.sign(prevLine.y1 - prevLine.y2) } const nextLineVector = { x: Math.sign(nextLine.x1 - nextLine.x2), y: Math.sign(nextLine.y1 - nextLine.y2) } if (prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y) { //반절마루 생성불가이므로 지붕선만 추가하고 끝냄 // innerLines.push(drawRoofLine(roofPoints, canvas, roof, textMode)) return } const currentDegree = getDegreeByChon(currentLine.attributes.pitch) const prevDegree = getDegreeByChon(prevLine.attributes.pitch) const nextDegree = getDegreeByChon(nextLine.attributes.pitch) const gableWidth = currentLine.attributes.width const roofVector = { x: Math.sign(roofPoints[2] - roofPoints[0]), y: Math.sign(roofPoints[3] - roofPoints[1]) } const roofDx = roofPoints[0] - roofPoints[2] const roofDy = roofPoints[1] - roofPoints[3] const roofLength = Math.sqrt(roofDx * roofDx + roofDy * roofDy) const lineLength = (roofLength - gableWidth) / 2 const jerkinPoints = [] jerkinPoints.push( { x: roofPoints[0], y: roofPoints[1] }, { x: roofPoints[0] + roofVector.x * lineLength, y: roofPoints[1] + roofVector.y * lineLength }, ) jerkinPoints.push({ x: jerkinPoints[1].x + roofVector.x * gableWidth, y: jerkinPoints[1].y + roofVector.y * gableWidth }) jerkinPoints.push({ x: jerkinPoints[2].x + roofVector.x * lineLength, y: jerkinPoints[2].y + roofVector.y * lineLength }) if (gableWidth >= roofLength) { innerLines.push(drawRoofLine(roofPoints, canvas, roof, textMode)) } else if (jerkinPoints.length === 4) { const gablePoint1 = [jerkinPoints[0].x, jerkinPoints[0].y, jerkinPoints[1].x, jerkinPoints[1].y] const gablePoint2 = [jerkinPoints[1].x, jerkinPoints[1].y, jerkinPoints[2].x, jerkinPoints[2].y] const gablePoint3 = [jerkinPoints[2].x, jerkinPoints[2].y, jerkinPoints[3].x, jerkinPoints[3].y] const gableLength1 = Math.sqrt(Math.pow(gablePoint1[2] - gablePoint1[0], 2) + Math.pow(gablePoint1[3] - gablePoint1[1], 2)) const gableLength2 = Math.sqrt(Math.pow(gablePoint2[2] - gablePoint2[0], 2) + Math.pow(gablePoint2[3] - gablePoint2[1], 2)) const gableLength3 = Math.sqrt(Math.pow(gablePoint3[2] - gablePoint3[0], 2) + Math.pow(gablePoint3[3] - gablePoint3[1], 2)) if (gableLength1 >= 1) { innerLines.push(drawHipLine(gablePoint1, canvas, roof, textMode, prevDegree, prevDegree)) } if (gableLength2 >= 1) { innerLines.push(drawRoofLine(gablePoint2, canvas, roof, textMode)) } if (gableLength3 >= 1) { innerLines.push(drawHipLine(gablePoint3, canvas, roof, textMode, nextDegree, nextDegree)) } 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] } }) /** 이전, 다음라인의 사잇각의 vector를 구한다. */ let prevVector = getHalfAngleVector(prevRoofLine, currentRoofLine) let nextVector = getHalfAngleVector(currentRoofLine, nextRoofLine) /** 이전 라인과의 사이 추녀마루의 각도를 확인한다, 각도가 지붕안쪽으로 향하지 않을때 반대로 처리한다.*/ const prevCheckPoint = { x: currentRoofLine.x1 + prevVector.x * 10, y: currentRoofLine.y1 + prevVector.y * 10, } /** 다음 라인과의 사이 추녀마루의 각도를 확인한다, 각도가 지붕안쪽으로 향하지 않을때 반대로 처리한다.*/ const nextCheckPoint = { x: currentRoofLine.x2 + nextVector.x * 10, y: currentRoofLine.y2 + nextVector.y * 10, } 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 prevHipEdge = { vertex1: { x: gablePoint2[0], y: gablePoint2[1] }, vertex2: { x: gablePoint2[0] + prevHipVector.x * gableWidth, y: gablePoint2[1] + prevHipVector.y * gableWidth }, } const nextHipEdge = { vertex1: { x: gablePoint2[2], y: gablePoint2[3] }, vertex2: { x: gablePoint2[2] + nextHipVector.x * gableWidth, y: gablePoint2[3] + nextHipVector.y * gableWidth }, } const hipIntersection = edgesIntersection(prevHipEdge, nextHipEdge) if (hipIntersection) { const prevHipPoint = [prevHipEdge.vertex1.x, prevHipEdge.vertex1.y, hipIntersection.x, hipIntersection.y] const nextHipPoint = [nextHipEdge.vertex1.x, nextHipEdge.vertex1.y, hipIntersection.x, hipIntersection.y] innerLines.push(drawHipLine(prevHipPoint, canvas, roof, textMode, currentDegree, currentDegree)) innerLines.push(drawHipLine(nextHipPoint, canvas, roof, textMode, currentDegree, currentDegree)) const midPoint = { x: hipIntersection.x, y: hipIntersection.y } 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 const point = [midPoint.x, midPoint.y, intersect.x, intersect.y] //마루가 최대로 뻗어나갈수 있는 포인트. null일때는 맞은편 지붕선 까지로 판단한다. const drivePoint = getRidgeDrivePoint(point, prevLine, nextLine, baseLines) const isOverlapBefore = analyze.isHorizontal ? almostEqual(Math.min(beforePrevLine.x1, beforePrevLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(beforePrevLine.x1, beforePrevLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(beforePrevLine.y1, beforePrevLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(beforePrevLine.y1, beforePrevLine.y2), Math.max(currentLine.y1, currentLine.y2)) const isOverlapAfter = analyze.isHorizontal ? almostEqual(Math.min(afterNextLine.x1, afterNextLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(afterNextLine.x1, afterNextLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(afterNextLine.y1, afterNextLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(afterNextLine.y1, afterNextLine.y2), Math.max(currentLine.y1, currentLine.y2)) if (isOverlapBefore || isOverlapAfter) { const oppLine = isOverlapBefore ? beforePrevLine : afterNextLine const otherGable = baseLines.find( (l) => l !== currentLine && l !== prevLine && l !== nextLine && l !== oppLine && l.attributes.type === LINE_TYPE.WALLLINE.GABLE, ) if (!otherGable) { const pointVector = { x: Math.sign(clamp01(point[0] - point[2])), y: Math.sign(clamp01(point[1] - point[3])) } let offset = 0 switch (oppLine.attributes.type) { case LINE_TYPE.WALLLINE.HIPANDGABLE: offset = oppLine.attributes.width break case LINE_TYPE.WALLLINE.JERKINHEAD: offset = oppLine.attributes.width / 2 break case LINE_TYPE.WALLLINE.WALL: offset = 0 break default: break } if (oppLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { if (drivePoint) { point[2] = drivePoint.x point[3] = drivePoint.y } else { point[2] += pointVector.x * offset point[3] += pointVector.y * offset } } else { point[2] += pointVector.x * offset point[3] += pointVector.y * offset } } else if (drivePoint) { point[2] = drivePoint.x point[3] = drivePoint.y } } else if (drivePoint) { point[2] = drivePoint.x point[3] = drivePoint.y } linesAnalysis.push({ start: { x: point[0], y: point[1] }, end: { x: point[2], y: point[3] }, left: baseLines.findIndex((line) => line === prevLine), right: baseLines.findIndex((line) => line === nextLine), type: TYPES.RIDGE, degree: 0, }) } } } }) //5. 케라바 판단, 케라바는 양옆이 처마여야 한다. gables.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] } }) //양옆이 처마가 아닐때 패스 if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) { return } const prevLineVector = { x: Math.sign(clamp01(prevLine.x1 - prevLine.x2)), y: Math.sign(clamp01(prevLine.y1 - prevLine.y2)) } const nextLineVector = { x: Math.sign(clamp01(nextLine.x1 - nextLine.x2)), y: Math.sign(clamp01(nextLine.y1 - nextLine.y2)) } const inPolygonPoint = { x: (currentLine.x1 + currentLine.x2) / 2 + Math.sign(nextLine.x2 - nextLine.x1), y: (currentLine.y1 + currentLine.y2) / 2 + Math.sign(nextLine.y2 - nextLine.y1), } //좌우 라인이 서로 다른방향일때 if ((prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y) && checkWallPolygon.inPolygon(inPolygonPoint)) { const analyze = analyzeLine(currentLine) let beforePrevLine, afterNextLine baseLines.forEach((baseLine, index) => { if (baseLine === prevLine) { beforePrevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] } if (baseLine === nextLine) { afterNextLine = baseLines[(index + 1) % baseLines.length] } }) const currentVector = { x: Math.sign(clamp01(currentLine.x1 - currentLine.x2)), y: Math.sign(clamp01(currentLine.y1 - currentLine.y2)) } // 마루 진행 최대 길이 let prevDistance = 0, nextDistance = 0 // 반대쪽 라인이 특정조건일때 마루선을 반대쪽까지로 처리한다.. const isOverlapBefore = analyze.isHorizontal ? almostEqual(Math.min(beforePrevLine.x1, beforePrevLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(beforePrevLine.x1, beforePrevLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(beforePrevLine.y1, beforePrevLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(beforePrevLine.y1, beforePrevLine.y2), Math.max(currentLine.y1, currentLine.y2)) const isOverlapAfter = analyze.isHorizontal ? almostEqual(Math.min(afterNextLine.x1, afterNextLine.x2), Math.min(currentLine.x1, currentLine.x2)) && almostEqual(Math.max(afterNextLine.x1, afterNextLine.x2), Math.max(currentLine.x1, currentLine.x2)) : almostEqual(Math.min(afterNextLine.y1, afterNextLine.y2), Math.min(currentLine.y1, currentLine.y2)) && almostEqual(Math.max(afterNextLine.y1, afterNextLine.y2), Math.max(currentLine.y1, currentLine.y2)) let isOverlap = false if (isOverlapBefore || isOverlapAfter) { const oppLine = isOverlapBefore ? beforePrevLine : afterNextLine const otherGable = baseLines.find( (l) => l !== currentLine && l !== prevLine && l !== nextLine && l !== oppLine && l.attributes.type === LINE_TYPE.WALLLINE.GABLE, ) if (!otherGable) { isOverlap = true } } if (isOverlap) { const oppLine = isOverlapBefore ? beforePrevLine : afterNextLine const cMidX = (currentLine.x1 + currentLine.x2) / 2 const cMidY = (currentLine.y1 + currentLine.y2) / 2 const oMidX = (oppLine.x1 + oppLine.x2) / 2 const oMidY = (oppLine.y1 + oppLine.y2) / 2 const cOffset = currentLine.attributes.offset const oOffset = oppLine.attributes.type === LINE_TYPE.WALLLINE.WALL ? 0 : oppLine.attributes.offset const ridgeVector = { x: Math.sign(clamp01(cMidX - oMidX)), y: Math.sign(clamp01(cMidY - oMidY)) } const ridgePoint = [ cMidX + ridgeVector.x * cOffset, cMidY + ridgeVector.y * cOffset, oMidX + -ridgeVector.x * oOffset, oMidY + -ridgeVector.y * oOffset, ] let offset = 0 switch (oppLine.attributes.type) { case LINE_TYPE.WALLLINE.HIPANDGABLE: offset = oppLine.attributes.width break case LINE_TYPE.WALLLINE.JERKINHEAD: offset = oppLine.attributes.width / 2 break case LINE_TYPE.WALLLINE.WALL: offset = 0 break default: break } ridgePoint[2] += ridgeVector.x * offset ridgePoint[3] += ridgeVector.y * offset linesAnalysis.push({ start: { x: ridgePoint[0], y: ridgePoint[1] }, end: { x: ridgePoint[2], y: ridgePoint[3] }, left: baseLines.findIndex((line) => line === prevLine), right: baseLines.findIndex((line) => line === nextLine), type: TYPES.RIDGE, degree: 0, }) } else { if (beforePrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { const beforeVector = { x: Math.sign(clamp01(beforePrevLine.x1 - beforePrevLine.x2)), y: Math.sign(clamp01(beforePrevLine.y1 - beforePrevLine.y2)), } prevDistance = Math.sqrt(Math.pow(prevLine.x2 - prevLine.x1, 2) + Math.pow(prevLine.y2 - prevLine.y1, 2)) + currentLine.attributes.offset if (beforeVector.x === currentVector.x && beforeVector.y === currentVector.y) { prevDistance = prevDistance - beforePrevLine.attributes.offset } else { prevDistance = prevDistance + beforePrevLine.attributes.offset } } else { prevDistance = Math.sqrt(Math.pow(prevLine.x2 - prevLine.x1, 2) + Math.pow(prevLine.y2 - prevLine.y1, 2)) + currentLine.attributes.offset + beforePrevLine.attributes.offset } if (afterNextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { const afterVector = { x: Math.sign(clamp01(afterNextLine.x1 - afterNextLine.x2)), y: Math.sign(clamp01(afterNextLine.y1 - afterNextLine.y2)), } nextDistance = Math.sqrt(Math.pow(nextLine.x2 - nextLine.x1, 2) + Math.pow(nextLine.y2 - nextLine.y1, 2)) + currentLine.attributes.offset if (afterVector.x === currentVector.x && afterVector.y === currentVector.y) { nextDistance = nextDistance - afterNextLine.attributes.offset } else { nextDistance = nextDistance + afterNextLine.attributes.offset } } else { nextDistance = Math.sqrt(Math.pow(nextLine.x2 - nextLine.x1, 2) + Math.pow(nextLine.y2 - nextLine.y1, 2)) + currentLine.attributes.offset + afterNextLine.attributes.offset } //좌우 선분 의 이전 다음 선이 케라바인경우 둘 중 긴 쪽이 기준선이 된다. 아닌경우 짧은쪽 let stdLine let stdFindOppVector if (beforePrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE && afterNextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { if (nextDistance <= prevDistance) { stdLine = prevLine if (prevLineVector.x === 0) { stdFindOppVector = { x: Math.sign(clamp01(prevLine.x1 - nextLine.x1)), y: 0 } } else { stdFindOppVector = { x: 0, y: Math.sign(clamp01(prevLine.y1 - nextLine.y1)) } } } else { stdLine = nextLine if (nextLineVector.x === 0) { stdFindOppVector = { x: Math.sign(clamp01(nextLine.x1 - prevLine.x1)), y: 0 } } else { stdFindOppVector = { x: 0, y: Math.sign(clamp01(nextLine.y1 - prevLine.y1)) } } } } else { if (nextDistance <= prevDistance) { stdLine = nextLine if (nextLineVector.x === 0) { stdFindOppVector = { x: Math.sign(clamp01(nextLine.x1 - prevLine.x1)), y: 0 } } else { stdFindOppVector = { x: 0, y: Math.sign(clamp01(nextLine.y1 - prevLine.y1)) } } } else { stdLine = prevLine if (prevLineVector.x === 0) { stdFindOppVector = { x: Math.sign(clamp01(prevLine.x1 - nextLine.x1)), y: 0 } } else { stdFindOppVector = { x: 0, y: Math.sign(clamp01(prevLine.y1 - nextLine.y1)) } } } } const stdAnalyze = analyzeLine(stdLine) let stdPoints = [] const stdPrevLine = baseLines[(baseLines.indexOf(stdLine) - 1 + baseLines.length) % baseLines.length] const stdNextLine = baseLines[(baseLines.indexOf(stdLine) + 1) % baseLines.length] const stdVector = { x: Math.sign(clamp01(stdLine.x1 - stdLine.x2)), y: Math.sign(clamp01(stdLine.y1 - stdLine.y2)) } const stdPrevVector = { x: Math.sign(clamp01(stdPrevLine.x1 - stdPrevLine.x2)), y: Math.sign(clamp01(stdPrevLine.y1 - stdPrevLine.y2)) } const stdNextVector = { x: Math.sign(clamp01(stdNextLine.x1 - stdNextLine.x2)), y: Math.sign(clamp01(stdNextLine.y1 - stdNextLine.y2)) } let stdAdjustVector = { x: 0, y: 0 } if (stdPrevVector.x === stdNextVector.x && stdPrevVector.y === stdNextVector.y) { if (stdAnalyze.isHorizontal) { if (stdVector.x === 1) { if (stdPrevLine.y1 > stdNextLine.y1) { stdPoints.push(stdLine.x1 + stdPrevLine.attributes.offset, stdLine.y1) if (stdNextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x2 + stdNextLine.attributes.offset, stdLine.y2) } else { stdPoints.push(stdLine.x2, stdLine.y2) } } else { if (stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x1 - stdPrevLine.attributes.offset, stdLine.y1) } else { stdPoints.push(stdLine.x1, stdLine.y1) } stdPoints.push(stdLine.x2 - stdNextLine.attributes.offset, stdLine.y2) } } else { if (stdPrevLine.y1 < stdNextLine.y1) { stdPoints.push(stdLine.x1 - stdPrevLine.attributes.offset, stdLine.y1) if (stdNextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x2 - stdNextLine.attributes.offset, stdLine.y2) } else { stdPoints.push(stdLine.x2, stdLine.y2) } } else { if (stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x1 + stdPrevLine.attributes.offset, stdLine.y1) } else { stdPoints.push(stdLine.x1, stdLine.y1) } stdPoints.push(stdLine.x2 + stdNextLine.attributes.offset, stdLine.y2) } } } if (stdAnalyze.isVertical) { if (stdVector.y === 1) { if (stdPrevLine.x1 > stdNextLine.x1) { if (stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x1, stdLine.y1 - stdPrevLine.attributes.offset) } else { stdPoints.push(stdLine.x1, stdLine.y1) } stdPoints.push(stdLine.x2, stdLine.y2 - stdNextLine.attributes.offset) } else { stdPoints.push(stdLine.x1, stdLine.y1 + stdPrevLine.attributes.offset) if (stdNextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x2, stdLine.y2 + stdNextLine.attributes.offset) } else { stdPoints.push(stdLine.x2, stdLine.y2) } } } else { if (stdPrevLine.x1 < stdNextLine.x1) { if (stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x1, stdLine.y1 + stdPrevLine.attributes.offset) } else { stdPoints.push(stdLine.x1, stdLine.y1) } stdPoints.push(stdLine.x2, stdLine.y2 + stdNextLine.attributes.offset) } else { stdPoints.push(stdLine.x1, stdLine.y1 - stdPrevLine.attributes.offset) if (stdNextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { stdPoints.push(stdLine.x2, stdLine.y2 - stdNextLine.attributes.offset) } else { stdPoints.push(stdLine.x2, stdLine.y2) } } } } } else { const stdAddPrevVector = { x: Math.sign(clamp01(stdLine.x1 - stdLine.x2)), y: Math.sign(clamp01(stdLine.y1 - stdLine.y2)) } const stdAddNextVector = { x: Math.sign(clamp01(stdLine.x2 - stdLine.x1)), y: Math.sign(clamp01(stdLine.y2 - stdLine.y1)) } stdPoints = [ stdLine.x1 + stdAddPrevVector.x * stdPrevLine.attributes.offset, stdLine.y1 + stdAddPrevVector.y * stdPrevLine.attributes.offset, stdLine.x2 + stdAddNextVector.x * stdNextLine.attributes.offset, stdLine.y2 + stdAddNextVector.y * stdNextLine.attributes.offset, ] } //기준지붕선의 반대쪽선 const oppositeLine = [] const startX = Math.min(stdLine.x1, stdLine.x2) const endX = Math.max(stdLine.x1, stdLine.x2) const startY = Math.min(stdLine.y1, stdLine.y2) const endY = Math.max(stdLine.y1, stdLine.y2) baseLines .filter((line) => line !== stdLine && line !== currentLine && line.attributes.type !== LINE_TYPE.WALLLINE.SHED) .filter((line) => { const lineVector = { x: Math.sign(clamp01(line.x1 - line.x2)), y: Math.sign(clamp01(line.y1 - line.y2)) } let oppVector = { x: 0, y: 0 } if (stdVector.x === 0) { oppVector = { x: Math.sign(clamp01(stdLine.x1 - line.x1)), y: 0 } } if (stdVector.y === 0) { oppVector = { x: 0, y: Math.sign(clamp01(stdLine.y1 - line.y1)) } } const rightDirection = (stdVector.x === lineVector.x && stdVector.y !== lineVector.y) || (stdVector.x !== lineVector.x && stdVector.y === lineVector.y) const rightOpp = stdFindOppVector.x === oppVector.x && stdFindOppVector.y === oppVector.y return rightDirection && rightOpp }) .forEach((line) => { const lineStartX = Math.min(line.x1, line.x2) const lineEndX = Math.max(line.x1, line.x2) const lineStartY = Math.min(line.y1, line.y2) const lineEndY = Math.max(line.y1, line.y2) if (stdAnalyze.isHorizontal) { //full overlap if (lineStartX <= startX && endX <= lineEndX) { oppositeLine.push({ line, distance: Math.abs(lineStartY - startY) }) } else if ( (startX < lineStartX && lineStartX < endX) || (startX < lineStartX && lineEndX < endX) || (lineStartX === startX && lineEndX <= endX) || (lineEndX === endX && lineStartX <= startX) ) { oppositeLine.push({ line, distance: Math.abs(lineStartY - startY) }) } } if (stdAnalyze.isVertical) { //full overlap if (lineStartY <= startY && endY <= lineEndY) { oppositeLine.push({ line, distance: Math.abs(lineStartX - startX) }) } else if ( (startY < lineStartY && lineStartY < endY) || (startY < lineEndY && lineEndY < endY) || (lineStartY === startY && lineEndY <= endY) || (lineEndY === endY && lineStartY <= startY) ) { oppositeLine.push({ line, distance: Math.abs(lineStartX - startX) }) } } }) if (oppositeLine.length > 0) { const ridgePoints = [] oppositeLine.sort((a, b) => a.distance - b.distance) oppositeLine.forEach((opposite) => { const oppLine = opposite.line const oppIndex = baseLines.findIndex((line) => line === oppLine) //마주하는 라인의 이전 다음 라인. const oppPrevLine = baseLines[(oppIndex - 1 + baseLines.length) % baseLines.length] const oppNextLine = baseLines[(oppIndex + 1) % baseLines.length] const oppVector = { x: Math.sign(oppLine.x2 - oppLine.x1), y: Math.sign(oppLine.y2 - oppLine.y1) } let ridgePoint const oppPrevVector = { x: Math.sign(oppPrevLine.x1 - oppPrevLine.x2), y: Math.sign(oppPrevLine.y1 - oppPrevLine.y2) } const oppNextVector = { x: Math.sign(oppNextLine.x1 - oppNextLine.x2), y: Math.sign(oppNextLine.y1 - oppNextLine.y2) } if (oppPrevVector.x === oppNextVector.x && oppPrevVector.y === oppNextVector.y) { const addOffsetX1 = oppPrevVector.y * oppPrevLine.attributes.offset const addOffsetY1 = -oppPrevVector.x * oppPrevLine.attributes.offset const addOffsetX2 = oppPrevVector.y * oppNextLine.attributes.offset const addOffsetY2 = -oppPrevVector.x * oppNextLine.attributes.offset ridgePoint = [oppLine.x1 + addOffsetX1, oppLine.y1 + addOffsetY1, oppLine.x2 + addOffsetX2, oppLine.y2 + addOffsetY2] } else { const inPolygonPoint = { x: (oppLine.x1 + oppLine.x2) / 2 + Math.sign(oppNextLine.x2 - oppNextLine.x1), y: (oppLine.y1 + oppLine.y2) / 2 + Math.sign(oppNextLine.y2 - oppNextLine.y1), } if (checkWallPolygon.inPolygon(inPolygonPoint)) { ridgePoint = [ oppLine.x1 + -oppVector.x * oppPrevLine.attributes.offset, oppLine.y1 + -oppVector.y * oppPrevLine.attributes.offset, oppLine.x2 + oppVector.x * oppNextLine.attributes.offset, oppLine.y2 + oppVector.y * oppNextLine.attributes.offset, ] } else { ridgePoint = [ oppLine.x1 + oppVector.x * oppPrevLine.attributes.offset, oppLine.y1 + oppVector.y * oppPrevLine.attributes.offset, oppLine.x2 + -oppVector.x * oppNextLine.attributes.offset, oppLine.y2 + -oppVector.y * oppNextLine.attributes.offset, ] } } if (stdAnalyze.isHorizontal) { ridgePoint[1] = (stdLine.y1 + oppLine.y1) / 2 ridgePoint[3] = (stdLine.y2 + oppLine.y2) / 2 } if (stdAnalyze.isVertical) { ridgePoint[0] = (stdLine.x1 + oppLine.x1) / 2 ridgePoint[2] = (stdLine.x2 + oppLine.x2) / 2 } if ( (oppPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES || oppNextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) && !(oppPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES && oppNextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) ) { const oppLineLength = Math.sqrt(Math.pow(oppLine.x2 - oppLine.x1, 2) + Math.pow(oppLine.y2 - oppLine.y1, 2)) const stdLineLength = Math.sqrt(Math.pow(stdLine.x2 - stdLine.x1, 2) + Math.pow(stdLine.y2 - stdLine.y1, 2)) //기준선이 반대선보다 길이가 짧을때는 추녀마루에 대한 교점 처리 패스 if (stdLineLength >= oppLineLength) { if (oppPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { //처마라인이 아닌 반대쪽부터 마루선을 시작하도록 포인트 변경 const oppNextAnalyze = analyzeLine(oppNextLine) if (oppNextAnalyze.isHorizontal) { const dist1 = Math.abs(oppNextLine.y1 - ridgePoint[1]) const dist2 = Math.abs(oppNextLine.y1 - ridgePoint[3]) if (dist1 > dist2) { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[0], ridgePoint[1]] } } if (oppNextAnalyze.isVertical) { const dist1 = Math.abs(oppNextLine.x1 - ridgePoint[0]) const dist2 = Math.abs(oppNextLine.x1 - ridgePoint[2]) if (dist1 > dist2) { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[0], ridgePoint[1]] } } const checkEdge = { vertex1: { x: ridgePoint[0], y: ridgePoint[1] }, vertex2: { x: ridgePoint[2], y: ridgePoint[3] } } const checkVector = getHalfAngleVector(oppLine, oppPrevLine) const checkPoint = { x: oppLine.x1 + (checkVector.x * 10) / 2, y: oppLine.y1 + (checkVector.y * 10) / 2, } let hipVector if (checkWallPolygon.inPolygon(checkPoint)) { hipVector = { x: checkVector.x, y: checkVector.y } } else { hipVector = { x: -checkVector.x, y: -checkVector.y } } const ridgeVector = { x: Math.sign(ridgePoint[0] - ridgePoint[2]), y: Math.sign(ridgePoint[1] - ridgePoint[3]) } const hipEdge = { vertex1: { x: oppLine.x1, y: oppLine.y1 }, vertex2: { x: oppLine.x1 + hipVector.x, y: oppLine.y1 + hipVector.y } } const intersect = edgesIntersection(hipEdge, checkEdge) if (intersect) { const intersectVector = { x: Math.sign(ridgePoint[0] - intersect.x), y: Math.sign(ridgePoint[1] - intersect.y) } if (ridgeVector.x === intersectVector.x && ridgeVector.y === intersectVector.y) { ridgePoint = [ridgePoint[0], ridgePoint[1], intersect.x, intersect.y] } else { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[2], ridgePoint[3]] } } } if (oppNextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { //처마라인이 아닌 반대쪽부터 마루선을 시작하도록 포인트 변경 const oppPrevAnalyze = analyzeLine(oppPrevLine) if (oppPrevAnalyze.isHorizontal) { const dist1 = Math.abs(oppPrevLine.y1 - ridgePoint[1]) const dist2 = Math.abs(oppPrevLine.y1 - ridgePoint[3]) if (dist1 > dist2) { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[0], ridgePoint[1]] } } if (oppPrevAnalyze.isVertical) { const dist1 = Math.abs(oppPrevLine.x1 - ridgePoint[0]) const dist2 = Math.abs(oppPrevLine.x1 - ridgePoint[2]) if (dist1 > dist2) { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[0], ridgePoint[1]] } } const checkEdge = { vertex1: { x: ridgePoint[0], y: ridgePoint[1] }, vertex2: { x: ridgePoint[2], y: ridgePoint[3] } } const checkVector = getHalfAngleVector(oppLine, oppNextLine) const checkPoint = { x: oppLine.x2 + (checkVector.x * 10) / 2, y: oppLine.y2 + (checkVector.y * 10) / 2 } let hipVector if (checkWallPolygon.inPolygon(checkPoint)) { hipVector = { x: checkVector.x, y: checkVector.y } } else { hipVector = { x: -checkVector.x, y: -checkVector.y } } const ridgeVector = { x: Math.sign(ridgePoint[0] - ridgePoint[2]), y: Math.sign(ridgePoint[1] - ridgePoint[3]) } const hipEdge = { vertex1: { x: oppLine.x2, y: oppLine.y2 }, vertex2: { x: oppLine.x2 + hipVector.x, y: oppLine.y2 + hipVector.y } } const intersect = edgesIntersection(hipEdge, checkEdge) if (intersect) { const intersectVector = { x: Math.sign(ridgePoint[0] - intersect.x), y: Math.sign(ridgePoint[1] - intersect.y) } if (ridgeVector.x === intersectVector.x && ridgeVector.y === intersectVector.y) { ridgePoint = [ridgePoint[0], ridgePoint[1], intersect.x, intersect.y] } else { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[2], ridgePoint[3]] } } } } else { if (stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES || stdNextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { const vectorLine = stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES ? stdPrevLine : stdNextLine let driveVector = getHalfAngleVector(vectorLine, stdLine) const allPoints = [ { x: stdLine.x1, y: stdLine.y1 }, { x: stdLine.x2, y: stdLine.y2 }, { x: vectorLine.x1, y: vectorLine.y1 }, { x: vectorLine.x2, y: vectorLine.y2 }, ] let startPoint for (let i = 0; i < allPoints.length; i++) { const point = allPoints[i] for (let j = i + 1; j < allPoints.length; j++) { if (i === j) continue const point2 = allPoints[j] if (almostEqual(point.x, point2.x) && almostEqual(point.y, point2.y)) { startPoint = point2 break } } if (startPoint) break } const checkDrivePoint = { x: startPoint.x + driveVector.x * 5, y: startPoint.y + driveVector.y * 5 } if (!checkWallPolygon.inPolygon(checkDrivePoint)) { driveVector = { x: -driveVector.x, y: -driveVector.y } } const driveEdge = { vertex1: { x: startPoint.x, y: startPoint.y }, vertex2: { x: startPoint.x + driveVector.x, y: startPoint.y + driveVector.y }, } const ridgeEdge = { vertex1: { x: ridgePoint[0], y: ridgePoint[1] }, vertex2: { x: ridgePoint[2], y: ridgePoint[3] } } const intersect = edgesIntersection(driveEdge, ridgeEdge) const rLength = Math.sqrt(Math.pow(ridgePoint[2] - ridgePoint[0], 2) + Math.pow(ridgePoint[3] - ridgePoint[1], 2)) if (intersect) { const length1 = Math.sqrt(Math.pow(intersect.x - ridgePoint[0], 2) + Math.pow(intersect.y - ridgePoint[1], 2)) const length2 = Math.sqrt(Math.pow(intersect.x - ridgePoint[2], 2) + Math.pow(intersect.y - ridgePoint[3], 2)) if (rLength < length1 && rLength < length2) { if (length1 >= length2) { ridgePoint = [ridgePoint[0], ridgePoint[1], intersect.x, intersect.y] } else { ridgePoint = [ridgePoint[2], ridgePoint[3], intersect.x, intersect.y] } } } } } } if (stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES || stdNextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { const vectorLine = stdPrevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES ? stdPrevLine : stdNextLine let driveVector = getHalfAngleVector(vectorLine, stdLine) const allPoints = [ { x: stdLine.x1, y: stdLine.y1 }, { x: stdLine.x2, y: stdLine.y2 }, { x: vectorLine.x1, y: vectorLine.y1 }, { x: vectorLine.x2, y: vectorLine.y2 }, ] let startPoint for (let i = 0; i < allPoints.length; i++) { const point = allPoints[i] for (let j = i + 1; j < allPoints.length; j++) { if (i === j) continue const point2 = allPoints[j] if (almostEqual(point.x, point2.x) && almostEqual(point.y, point2.y)) { startPoint = point2 break } } if (startPoint) break } const checkDrivePoint = { x: startPoint.x + driveVector.x * 5, y: startPoint.y + driveVector.y * 5 } if (!checkWallPolygon.inPolygon(checkDrivePoint)) { driveVector = { x: -driveVector.x, y: -driveVector.y } } const driveEdge = { vertex1: { x: startPoint.x, y: startPoint.y }, vertex2: { x: startPoint.x + driveVector.x, y: startPoint.y + driveVector.y }, } const ridgeEdge = { vertex1: { x: ridgePoint[0], y: ridgePoint[1] }, vertex2: { x: ridgePoint[2], y: ridgePoint[3] } } const intersect = edgesIntersection(driveEdge, ridgeEdge) if (intersect) { if (almostEqual(stdLine.x1, startPoint.x) && stdAnalyze.isHorizontal) { stdPoints[0] = intersect.x } if (almostEqual(stdLine.y1, startPoint.y) && stdAnalyze.isVertical) { stdPoints[1] = intersect.y } if (almostEqual(stdLine.x2, startPoint.x) && stdAnalyze.isHorizontal) { stdPoints[2] = intersect.x } if (almostEqual(stdLine.y2, startPoint.y) && stdAnalyze.isVertical) { stdPoints[3] = intersect.y } } } const stdMinX = Math.min(stdPoints[0], stdPoints[2]) const stdMaxX = Math.max(stdPoints[0], stdPoints[2]) const stdMinY = Math.min(stdPoints[1], stdPoints[3]) const stdMaxY = Math.max(stdPoints[1], stdPoints[3]) const rMinX = Math.min(ridgePoint[0], ridgePoint[2]) const rMaxX = Math.max(ridgePoint[0], ridgePoint[2]) const rMinY = Math.min(ridgePoint[1], ridgePoint[3]) const rMaxY = Math.max(ridgePoint[1], ridgePoint[3]) const overlapMinX = Math.max(stdMinX, rMinX) const overlapMaxX = Math.min(stdMaxX, rMaxX) const overlapMinY = Math.max(stdMinY, rMinY) const overlapMaxY = Math.min(stdMaxY, rMaxY) if (stdAnalyze.isHorizontal) { if (overlapMinX > overlapMaxX) { return } const dist1 = Math.abs(ridgePoint[0] - overlapMinX) const dist2 = Math.abs(ridgePoint[0] - overlapMaxX) if (dist1 < dist2) { ridgePoint = [overlapMinX, ridgePoint[1], overlapMaxX, ridgePoint[3]] } else { ridgePoint = [overlapMaxX, ridgePoint[1], overlapMinX, ridgePoint[3]] } const ridgeVector = Math.sign(clamp01(ridgePoint[0] - ridgePoint[2])) if (stdVector.x !== ridgeVector) { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[0], ridgePoint[1]] } } if (stdAnalyze.isVertical) { if (overlapMinY > overlapMaxY) { return } const dist1 = Math.abs(ridgePoint[1] - overlapMinY) const dist2 = Math.abs(ridgePoint[1] - overlapMaxY) if (dist1 < dist2) { ridgePoint = [ridgePoint[0], overlapMinY, ridgePoint[2], overlapMaxY] } else { ridgePoint = [ridgePoint[0], overlapMaxY, ridgePoint[2], overlapMinY] } const ridgeVector = Math.sign(clamp01(ridgePoint[1] - ridgePoint[3])) if (stdVector.y !== ridgeVector) { ridgePoint = [ridgePoint[2], ridgePoint[3], ridgePoint[0], ridgePoint[1]] } } ridgePoints.push({ point: ridgePoint, left: stdLine, right: oppLine }) canvas .getObjects() .filter((obj) => obj.name === 'check') .forEach((obj) => canvas.remove(obj)) canvas.renderAll() }) ridgePoints.forEach((r) => { let point = r.point const inPolygon1 = roof.inPolygon({ x: point[0], y: point[1] }) || roof.lines.find((line) => isPointOnLineNew(line, { x: point[0], y: point[1] })) !== undefined const inPolygon2 = roof.inPolygon({ x: point[2], y: point[3] }) || roof.lines.find((line) => isPointOnLineNew(line, { x: point[2], y: point[3] })) !== undefined //시작점이 지붕 밖에 있을때 vector내의 가까운 지붕선으로 변경 if (!inPolygon1) { const checkVector = { x: Math.sign(point[0] - point[2]), y: Math.sign(point[1] - point[3]) } const checkEdge = { vertex1: { x: point[0], y: point[1] }, vertex2: { x: point[2], y: point[3] } } let minDistance = Infinity let correctPoint roof.lines.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) { const distance = Math.sqrt(Math.pow(intersect.x - point[0], 2) + Math.pow(intersect.y - point[1], 2)) const intersectVector = { x: Math.sign(point[0] - intersect.x), y: Math.sign(point[1] - intersect.y) } if (distance < minDistance && intersectVector.x === checkVector.x && intersectVector.y === checkVector.y) { minDistance = distance correctPoint = intersect } } }) if (correctPoint) { point = [correctPoint.x, correctPoint.y, point[2], point[3]] } } //종료점이 지붕밖에 있을때 vector내의 가까운 지붕선으로 변경 if (!inPolygon2) { const checkVector = { x: Math.sign(point[2] - point[0]), y: Math.sign(point[3] - point[1]) } const checkEdge = { vertex1: { x: point[2], y: point[3] }, vertex2: { x: point[0], y: point[1] } } let minDistance = Infinity let correctPoint roof.lines.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) { const distance = Math.sqrt(Math.pow(intersect.x - point[2], 2) + Math.pow(intersect.y - point[3], 2)) const intersectVector = { x: Math.sign(point[2] - intersect.x), y: Math.sign(point[3] - intersect.y) } if (distance < minDistance && intersectVector.x === checkVector.x && intersectVector.y === checkVector.y) { minDistance = distance correctPoint = intersect } } }) if (correctPoint) { point = [point[0], point[1], correctPoint.x, correctPoint.y] } } const ridgeLength = Math.sqrt(Math.pow(point[2] - point[0], 2) + Math.pow(point[3] - point[1], 2)) if (ridgeLength > EPSILON) { //마루 포인트 중 1개만 지붕선에 붙어있을경우 해당 포인트를 시작점으로 한다. const isPointOnRoof1 = roof.lines.find((line) => isPointOnLineNew(line, { x: point[0], y: point[1] })) !== undefined const isPointOnRoof2 = roof.lines.find((line) => isPointOnLineNew(line, { x: point[2], y: point[3] })) !== undefined let startPoint, endPoint if ((isPointOnRoof1 && isPointOnRoof2) || (!isPointOnRoof1 && !isPointOnRoof2)) { startPoint = { x: point[0], y: point[1] } endPoint = { x: point[2], y: point[3] } } else { if (isPointOnRoof1) { startPoint = { x: point[0], y: point[1] } endPoint = { x: point[2], y: point[3] } } if (isPointOnRoof2) { startPoint = { x: point[2], y: point[3] } endPoint = { x: point[0], y: point[1] } } } linesAnalysis.push({ start: startPoint, end: endPoint, left: baseLines.findIndex((line) => line === r.left), right: baseLines.findIndex((line) => line === r.right), type: TYPES.RIDGE, degree: 0, }) } }) } } } //반절마루 생성불가이므로 지붕선 분기를 해야하는지 확인 후 처리. if (prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y) { const analyze = analyzeLine(currentLine) const roofLine = analyze.roofLine const checkVector = { x: Math.sign(prevLine.y2 - prevLine.y1), y: Math.sign(prevLine.x1 - prevLine.x2) } const checkEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } let maxDistance = 0 let correctPoint roof.lines .filter((line) => { const roofIndex = roof.lines.indexOf(roofLine) const prevRoofLine = roof.lines[(roofIndex - 1 + roof.lines.length) % roof.lines.length] const nextRoofLine = roof.lines[(roofIndex + 1) % roof.lines.length] return line !== roofLine && line !== prevRoofLine && line !== nextRoofLine }) .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)) { intersect.x = almostEqual(intersect.x, roofLine.x1) ? roofLine.x1 : intersect.x intersect.y = almostEqual(intersect.y, roofLine.y1) ? roofLine.y1 : intersect.y const distance = Math.sqrt(Math.pow(intersect.x - roofLine.x1, 2) + Math.pow(intersect.y - roofLine.y1, 2)) const vector = { x: Math.sign(intersect.x - roofLine.x1), y: Math.sign(intersect.y - roofLine.y1) } if (distance > maxDistance && vector.x === checkVector.x && vector.y === checkVector.y) { maxDistance = distance correctPoint = intersect } } }) if (correctPoint) { const startPoint = Math.sqrt(Math.pow(correctPoint.x - roofLine.x1, 2) + Math.pow(correctPoint.y - roofLine.y1, 2)) > Math.sqrt(Math.pow(correctPoint.x - roofLine.x2, 2) + Math.pow(correctPoint.y - roofLine.y2, 2)) ? { x: roofLine.x1, y: roofLine.y1 } : { x: roofLine.x2, y: roofLine.y2 } linesAnalysis.push({ start: startPoint, end: { x: correctPoint.x, y: correctPoint.y }, left: baseLines.findIndex((line) => line === prevLine), right: baseLines.findIndex((line) => line === nextLine), type: TYPES.GABLE_LINE, degree: getDegreeByChon(prevLine.attributes.pitch), gableId: baseLines.findIndex((line) => line === currentLine), connectCnt: 0, }) } } canvas .getObjects() .filter((obj) => obj.name === 'check') .forEach((obj) => canvas.remove(obj)) canvas.renderAll() }) //각 선분의 교차점을 찾아 선을 그린다. const MAX_ITERATIONS = 1000 //무한루프 방지 let iterations = 0 while (linesAnalysis.length > 0 && iterations < MAX_ITERATIONS) { iterations++ const intersections = [] linesAnalysis.forEach((currLine, i) => { let minDistance = Infinity let intersectPoint = null let linePoint = null let partner = null const cLength = Math.sqrt(Math.pow(currLine.end.x - currLine.start.x, 2) + Math.pow(currLine.end.y - currLine.start.y, 2)) //남은 길이가 0이면 무시 if (cLength < EPSILON) return // 하단레벨 케라바 라인인데 연결점이 2개 이상이면 무시 if (currLine.type === TYPES.GABLE_LINE && currLine.connectCnt > 1) return linesAnalysis.forEach((nextLine, j) => { if (i === j) return if (currLine.type === TYPES.GABLE_LINE && nextLine.type === TYPES.GABLE_LINE && currLine.gableId === nextLine.gableId) return if (nextLine.type === TYPES.GABLE_LINE && nextLine.connectCnt > 1) return const intersect = lineIntersection(currLine.start, currLine.end, nextLine.start, nextLine.end, canvas) if (intersect) { let distance1 = Math.sqrt(Math.pow(intersect.x - currLine.start.x, 2) + Math.pow(intersect.y - currLine.start.y, 2)) let distance2 = Math.sqrt(Math.pow(intersect.x - nextLine.start.x, 2) + Math.pow(intersect.y - nextLine.start.y, 2)) let point = [currLine.start.x, currLine.start.y, intersect.x, intersect.y] if (distance1 < EPSILON) { distance1 = Math.sqrt(Math.pow(intersect.x - currLine.end.x, 2) + Math.pow(intersect.y - currLine.end.y, 2)) distance2 = Math.sqrt(Math.pow(intersect.x - nextLine.end.x, 2) + Math.pow(intersect.y - nextLine.end.y, 2)) point = [currLine.end.x, currLine.end.y, intersect.x, intersect.y] } // 하단레벨 케라바 라인인데 남은 길이가 없는 경우 무시 if (currLine.type === TYPES.GABLE_LINE && distance1 < EPSILON) return //교점까지의 길이가 최소 길이보다 작을 경우, 최소 길이와 교점을 교체 if (distance1 < minDistance && !almostEqual(distance1, minDistance) && !(distance1 < EPSILON && distance2 < EPSILON)) { minDistance = distance1 intersectPoint = intersect linePoint = point partner = j } else if (almostEqual(distance1, minDistance)) { const pLine = linesAnalysis[partner] const isSameLine = currLine.left === pLine.left || currLine.left === pLine.right || currLine.right === pLine.left || currLine.right === pLine.right if (!isSameLine) { partner = j } } } }) if (intersectPoint) { intersections.push({ index: i, intersect: intersectPoint, linePoint, partner }) } }) const intersectPoints = intersections .map((item) => item.intersect) .filter((point, index, self) => self.findIndex((p) => almostEqual(p.x, point.x) && almostEqual(p.y, point.y)) === index) if (intersectPoints.length === 1 && intersections.length > 1) { intersections[0].partner = intersections[1].index intersections[1].partner = intersections[0].index } let newAnalysis = [] //신규 발생 선 const processed = new Set() //처리된 선 for (const { index, intersect, linePoint, partner } of intersections) { const cLine = linesAnalysis[index] const pLine = linesAnalysis[partner] //교점이 없거나, 이미 처리된 선분이면 처리하지 않는다. if (!intersect || processed.has(index)) continue //교점이 없거나, 교점 선분이 없으면 처리하지 않는다. const pIntersection = intersections.find((p) => p.index === partner) if (!pIntersection || !pIntersection.intersect) continue //상호 최단 교점이 아닐경우 처리하지 않는다. if (pIntersection.partner !== index) continue //공통선분이 없으면 처리하지 않는다. const isSameLine = cLine.left === pLine.left || cLine.left === pLine.right || cLine.right === pLine.left || cLine.right === pLine.right if (!isSameLine) continue //처리 된 라인으로 설정 processed.add(index).add(partner) let point1 = linePoint let point2 = pIntersection.linePoint let length1 = Math.sqrt(Math.pow(point1[2] - point1[0], 2) + Math.pow(point1[3] - point1[1], 2)) let length2 = Math.sqrt(Math.pow(point2[2] - point2[0], 2) + Math.pow(point2[3] - point2[1], 2)) //gable라인과 붙는경우 length가 0에 가까우면 포인트를 뒤집는다. if (cLine.type === TYPES.GABLE_LINE && length2 < EPSILON) { point2 = [pLine.end.x, pLine.end.y, intersect.x, intersect.y] length2 = Math.sqrt((point2[2] - point2[0]) ** 2 + (point2[3] - point2[1]) ** 2) } if (pLine.type === TYPES.GABLE_LINE && length1 < EPSILON) { point1 = [cLine.end.x, cLine.end.y, intersect.x, intersect.y] length1 = Math.sqrt((point1[2] - point1[0]) ** 2 + (point1[3] - point1[1]) ** 2) } if (length1 > 0 && !alreadyPoints(innerLines, point1)) { if (cLine.type === TYPES.HIP) { innerLines.push(drawHipLine(point1, canvas, roof, textMode, cLine.degree, cLine.degree)) } else if (cLine.type === TYPES.RIDGE) { innerLines.push(drawRidgeLine(point1, canvas, roof, textMode)) } else if (cLine.type === TYPES.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, cLine.degree, cLine.degree)) } else { innerLines.push(drawRidgeLine(point1, canvas, roof, textMode)) } } else if (cLine.type === TYPES.GABLE_LINE) { if (cLine.degree > 0) { innerLines.push(drawHipLine(point1, canvas, roof, textMode, cLine.degree, cLine.degree)) } else { innerLines.push(drawRidgeLine(point1, canvas, roof, textMode)) } } } if (length2 > 0 && !alreadyPoints(innerLines, point2)) { if (pLine.type === TYPES.HIP) { innerLines.push(drawHipLine(point2, canvas, roof, textMode, pLine.degree, pLine.degree)) } else if (pLine.type === TYPES.RIDGE) { innerLines.push(drawRidgeLine(point2, canvas, roof, textMode)) } else if (pLine.type === TYPES.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, pLine.degree, pLine.degree)) } else { innerLines.push(drawRidgeLine(point2, canvas, roof, textMode)) } } else if (pLine.type === TYPES.GABLE_LINE) { if (pLine.degree > 0) { innerLines.push(drawHipLine(point2, canvas, roof, textMode, pLine.degree, pLine.degree)) } else { innerLines.push(drawRidgeLine(point2, canvas, roof, textMode)) } } } const otherIs = intersections .filter((is) => is.index !== index && is.index !== partner) .filter((is) => almostEqual(is.intersect.x, intersect.x) && almostEqual(is.intersect.y, intersect.y)) let relationBaseLines = [cLine.left, cLine.right, pLine.left, pLine.right] if (otherIs.length > 0 && cLine.type !== TYPES.GABLE_LINE && pLine.type !== TYPES.GABLE_LINE) { for (let i = 0; i < otherIs.length; i++) { const oLine = linesAnalysis[otherIs[i].index] if (oLine.type === TYPES.RIDGE && linesAnalysis.length > 3) continue const isSameLine = relationBaseLines.includes(oLine.left) || relationBaseLines.includes(oLine.right) if (isSameLine) { let oPoint = otherIs[i].linePoint let oLength = Math.sqrt(Math.pow(oPoint[2] - oPoint[0], 2) + Math.pow(oPoint[3] - oPoint[1], 2)) if (oLength > 0 && !alreadyPoints(innerLines, oPoint)) { if (oLine.type === TYPES.HIP) { innerLines.push(drawHipLine(oPoint, canvas, roof, textMode, null, oLine.degree, oLine.degree)) } else if (oLine.type === TYPES.RIDGE) { innerLines.push(drawRidgeLine(oPoint, canvas, roof, textMode)) } else if (oLine.type === TYPES.NEW) { const isDiagonal = Math.abs(oPoint[0] - oPoint[2]) >= 1 && Math.abs(oPoint[1] - oPoint[3]) >= 1 if (isDiagonal) { innerLines.push(drawHipLine(oPoint, canvas, roof, textMode, oLine.degree, oLine.degree)) } else { innerLines.push(drawRidgeLine(oPoint, canvas, roof, textMode)) } } else if (oLine.type === TYPES.GABLE_LINE) { if (oLine.degree > 0) { innerLines.push(drawHipLine(oPoint, canvas, roof, textMode, oLine.degree, oLine.degree)) } else { innerLines.push(drawRidgeLine(oPoint, canvas, roof, textMode)) } } } processed.add(otherIs[i].index) relationBaseLines.push(oLine.left, oLine.right) } } } if (cLine.type === TYPES.GABLE_LINE || pLine.type === TYPES.GABLE_LINE) { relationBaseLines = [cLine.left, cLine.right, pLine.left, pLine.right] const gableLine = cLine.type === TYPES.GABLE_LINE ? cLine : pLine gableLine.connectCnt++ const uniqueBaseLines = [...new Set(relationBaseLines)] // 연결점에서 새로운 가선분을 생성 if (uniqueBaseLines.length > 2) { 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) { if (gableLine.connectCnt < 2) { newAnalysis.push({ start: { x: intersect.x, y: intersect.y }, end: { x: gableLine.end.x, y: gableLine.end.y }, left: gableLine.left, right: gableLine.right, type: TYPES.GABLE_LINE, degree: getDegreeByChon(baseLines[gableLine.right].attributes.pitch), gableId: gableLine.gableId, connectCnt: gableLine.connectCnt, }) } if (gableLine.connectCnt >= 2) { //가선분 발생만 시키는 더미 생성. newAnalysis.push({ start: { x: intersect.x, y: intersect.y }, end: { x: intersect.x, y: intersect.y }, left: gableLine.gableId, right: gableLine.gableId, type: TYPES.HIP, degree: 0, }) } } } } else { const uniqueBaseLines = [...new Set(relationBaseLines)] // 연결점에서 새로운 가선분을 생성 if (uniqueBaseLines.length > 2) { 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) const baseLine1 = baseLines[uniqueLines[0]] const baseLine2 = baseLines[uniqueLines[1]] let bisector if (isParallel(baseLine1, baseLine2)) { let cPoint = [cLine.start.x, cLine.start.y, cLine.end.x, cLine.end.y] let pPoint = [pLine.start.x, pLine.start.y, pLine.end.x, pLine.end.y] const length1 = Math.sqrt(Math.pow(intersect.x - cPoint[2], 2) + Math.pow(intersect.y - cPoint[3], 2)) const length2 = Math.sqrt(Math.pow(intersect.x - pPoint[2], 2) + Math.pow(intersect.y - pPoint[3], 2)) //교점 밖으로 뻗어나가는 선의 길이가 다를 경우 이등분선 계산이 틀어져서 조정 if (!almostEqual(length1, length2)) { const vector1 = { x: Math.sign(clamp01(cPoint[2] - cPoint[0])), y: Math.sign(clamp01(cPoint[3] - cPoint[1])) } const vector2 = { x: Math.sign(clamp01(pPoint[2] - pPoint[0])), y: Math.sign(clamp01(pPoint[3] - pPoint[1])) } const addLength = 100 cPoint[2] = intersect.x + vector1.x * addLength cPoint[3] = intersect.y + vector1.y * addLength pPoint[2] = intersect.x + vector2.x * addLength pPoint[3] = intersect.y + vector2.y * addLength } bisector = getBisectLines( { x1: cPoint[0], y1: cPoint[1], x2: cPoint[2], y2: cPoint[3] }, { x1: pPoint[0], y1: pPoint[1], x2: pPoint[2], y2: pPoint[3] }, ) } else { bisector = getBisectBaseLines(baseLine1, baseLine2, intersect, canvas) } //마주하는 지붕선을 찾는다. const intersectionsByRoof = [] const checkEdge = { vertex1: { x: intersect.x, y: intersect.y }, vertex2: { x: intersect.x + bisector.x, y: intersect.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 - intersect.x) ** 2 + (is.y - intersect.y) ** 2) const isVector = { x: Math.sign(intersect.x - is.x), y: Math.sign(intersect.y - is.y) } if (isVector.x === checkVector.x && isVector.y === checkVector.y) { intersectionsByRoof.push({ is, distance }) } } }) intersectionsByRoof.sort((a, b) => a.distance - b.distance) //기 존재하는 analyze 라인이 있으면 newAnalyzeLine을 생성하지 않고 교체한다. const otherIs = intersections.filter( (is) => !processed.has(is.index) && almostEqual(is.intersect.x, intersect.x) && almostEqual(is.intersect.y, intersect.y), ) if (otherIs.length > 0) { const analyze = linesAnalysis[otherIs[0].index] processed.add(otherIs[0].index) newAnalysis.push({ start: { x: intersect.x, y: intersect.y }, end: { x: analyze.end.x, y: analyze.end.y }, left: analyze.left, right: analyze.right, type: analyze.type, degree: analyze.degree, gableId: analyze.gableId, connectCnt: analyze.connectCnt, }) } else if (intersectionsByRoof.length > 0) { //지붕선과 교점이 발생할때 let is = intersectionsByRoof[0].is let linePoint = [intersect.x, intersect.y, is.x, is.y] const isDiagonal = Math.abs(is.x - intersect.x) >= 1 && Math.abs(is.y - intersect.y) >= 1 const length = Math.sqrt((linePoint[2] - linePoint[0]) ** 2 + (linePoint[3] - linePoint[1]) ** 2) const line1 = baseLines[uniqueLines[0]] const line2 = baseLines[uniqueLines[1]] const vector1 = { x: Math.sign(line1.x1 - line1.x2), y: Math.sign(line1.y1 - line1.y2) } const prevLine = checkVector.x === vector1.x && checkVector.y === vector1.y ? line2 : line1 const nextLine = checkVector.x === vector1.x && checkVector.y === vector1.y ? line1 : line2 if (!isDiagonal) { const drivePoint = getRidgeDrivePoint(linePoint, prevLine, nextLine, baseLines) if (drivePoint !== null) { const driveLength = Math.sqrt((drivePoint.x - intersect.x) ** 2 + (drivePoint.y - intersect.y) ** 2) if (driveLength < length) { linePoint = [intersect.x, intersect.y, drivePoint.x, drivePoint.y] } } } newAnalysis.push({ start: { x: linePoint[0], y: linePoint[1] }, end: { x: linePoint[2], y: linePoint[3] }, left: uniqueLines[0], right: uniqueLines[1], type: TYPES.NEW, degree: isDiagonal ? cLine.degree : 0, }) } } } } canvas .getObjects() .filter((object) => object.name === 'check' || object.name === 'checkAnalysis') .forEach((object) => canvas.remove(object)) canvas.renderAll() if (newAnalysis.length > 0) break } // 처리된 가선분 제외 linesAnalysis = newAnalysis.concat(linesAnalysis.filter((_, index) => !processed.has(index))) canvas .getObjects() .filter((object) => object.name === 'check' || object.name === 'checkAnalysis') .forEach((object) => canvas.remove(object)) canvas.renderAll() if (newAnalysis.length === 0) break } // 가선분 중 처리가 안되어있는 붙어있는 라인에 대한 예외처리. const proceedAnalysis = [] linesAnalysis .filter((line) => { const dx = line.end.x - line.start.x const dy = line.end.y - line.start.y const length = Math.sqrt(dx ** 2 + dy ** 2) return length > 0 }) .forEach((currentLine) => { if (proceedAnalysis.find((p) => p === currentLine)) return //현재와 같으면 제외, 이미 처리된 라인은 제외, 현재와 공통선분이 존재하지 않으면 제외 linesAnalysis .filter((line) => { const dx = line.end.x - line.start.x const dy = line.end.y - line.start.y const length = Math.sqrt(dx ** 2 + dy ** 2) return length > 0 }) .filter( (partnerLine) => partnerLine !== currentLine && !proceedAnalysis.find((p) => p === partnerLine) && (currentLine.left === partnerLine.left || currentLine.left === partnerLine.right || partnerLine.left === currentLine.left || partnerLine.left === currentLine.right), ) .forEach((partnerLine) => { const dx1 = currentLine.end.x - currentLine.start.x const dy1 = currentLine.end.y - currentLine.start.y const dx2 = partnerLine.end.x - partnerLine.start.x const dy2 = partnerLine.end.y - partnerLine.start.y const denominator = dy2 * dx1 - dx2 * dy1 const isOpposite = dx1 * dx2 + dy1 * dy2 < 0 //평행하지 않으면 제외 if (Math.abs(denominator) > EPSILON) return const currentDegree = getDegreeByChon(baseLines[currentLine.left].attributes.pitch) if (isOpposite) { const points = [currentLine.start.x, currentLine.start.y, partnerLine.start.x, partnerLine.start.y] const length = Math.sqrt((points[0] - points[2]) ** 2 + (points[1] - points[3]) ** 2) if (length > 0) { if (currentLine.type === TYPES.HIP) { innerLines.push(drawHipLine(points, canvas, roof, textMode, currentDegree, currentDegree)) } else if (currentLine.type === TYPES.RIDGE) { innerLines.push(drawRidgeLine(points, canvas, roof, textMode)) } else if (currentLine.type === TYPES.NEW) { const isDiagonal = Math.abs(points[0] - points[2]) >= 1 && Math.abs(points[1] - points[3]) >= 1 if (isDiagonal && almostEqual(Math.abs(points[0] - points[2]), Math.abs(points[1] - points[3]))) { innerLines.push(drawHipLine(points, canvas, roof, textMode, currentDegree, currentDegree)) } if (!isDiagonal) { innerLines.push(drawRidgeLine(points, canvas, roof, textMode)) } } proceedAnalysis.push(currentLine, partnerLine) } } else { const allPoints = [currentLine.start, currentLine.end, partnerLine.start, partnerLine.end] let points = [] allPoints.forEach((point) => { let count = 0 allPoints.forEach((p) => { if (almostEqual(point.x, p.x) && almostEqual(point.y, p.y)) count++ }) if (count === 1) points.push(point) }) if (points.length === 2) { const length = Math.sqrt((points[0].x - points[1].x) ** 2 + (points[0].y - points[1].y) ** 2) if (length < EPSILON) return const isDiagonal = Math.abs(points[0].x - points[1].x) >= 1 && Math.abs(points[0].y - points[1].y) >= 1 if (isDiagonal) { innerLines.push( drawHipLine([points[0].x, points[0].y, points[1].x, points[1].y], canvas, roof, textMode, currentDegree, currentDegree), ) } else { innerLines.push(drawRidgeLine([points[0].x, points[0].y, points[1].x, points[1].y], canvas, roof, textMode)) } proceedAnalysis.push(currentLine, partnerLine) } } }) }) linesAnalysis = linesAnalysis.filter((line) => !proceedAnalysis.find((p) => p === line)) // 최종 라인중 벽에서 시작해서 벽에서 끝나는 라인이 남을 경우(벽취함) if (linesAnalysis.length > 0) { linesAnalysis .filter((line) => { const dx = line.end.x - line.start.x const dy = line.end.y - line.start.y const length = Math.sqrt(dx ** 2 + dy ** 2) return length > 0 }) .forEach((line) => { const startOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.start)) const endOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.end)) const allLinesPoints = [] innerLines.forEach((innerLine) => { allLinesPoints.push({ x: innerLine.x1, y: innerLine.y1 }, { x: innerLine.x2, y: innerLine.y2 }) }) if ( allLinesPoints.filter((p) => almostEqual(p.x, line.start.x) && almostEqual(p.y, line.start.y)).length < 3 && allLinesPoints.filter((p) => almostEqual(p.x, line.end.x) && almostEqual(p.y, line.end.y)).length === 0 ) { if (line.type === TYPES.RIDGE && baseLines.length === 4 && (startOnLine || endOnLine)) { innerLines.push(drawRidgeLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode)) } else { if (startOnLine && endOnLine) { if (line.degree === 0) { innerLines.push(drawRoofLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode)) } else { innerLines.push(drawHipLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode, line.degree, line.degree)) } } } } }) } //케라바에서 파생된 하단 지붕 라인처리 const downRoofGable = [] //처마에서 파생된 하단 지붕 라인 처리 const downRoofEaves = [] //roof lines에 조정해야하는 라인 처리 // const adjustRoofLines = [] baseLines.forEach((baseLine, index) => { const nextLine = baseLines[(index + 1) % baseLines.length] const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] const prevLineVector = { x: Math.sign(prevLine.x1 - prevLine.x2), y: Math.sign(prevLine.y1 - prevLine.y2) } const nextLineVector = { x: Math.sign(nextLine.x1 - nextLine.x2), y: Math.sign(nextLine.y1 - nextLine.y2) } const midX = (baseLine.x1 + baseLine.x2) / 2 const midY = (baseLine.y1 + baseLine.y2) / 2 const checkPoint = { x: midX + nextLineVector.x, y: midY + nextLineVector.y } //반절마루 생성불가이므로 지붕선 분기를 해야하는지 확인 후 처리. if ( prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y && baseLine.attributes.type === LINE_TYPE.WALLLINE.EAVES && (prevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE || nextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) ) { downRoofGable.push({ currLine: baseLine, currIndex: index, type: 'A' }) } if ( (prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y) && checkWallPolygon.inPolygon(checkPoint) && prevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE && nextLine.attributes.type === LINE_TYPE.WALLLINE.GABLE ) { downRoofGable.push({ currLine: baseLine, currIndex: index, type: 'B' }) } const originPoint = baseLine.attributes.originPoint if ( prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y && baseLine.attributes.type === LINE_TYPE.WALLLINE.EAVES && (prevLine.attributes.type === LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) && ((originPoint.x1 !== baseLine.x1 && originPoint.x2 !== baseLine.x2) || (originPoint.y1 !== baseLine.y1 && originPoint.y2 !== baseLine.y2)) ) { downRoofEaves.push({ currLine: baseLine, currIndex: index }) } }) const downRoofLines = [] // 하단지붕 파생 라인 처리후 innerLines에 추가. //A타입 하단 지붕 prevLine과 nextLine이 같은 방향으로 가는 경우 downRoofGable .filter((l) => l.type === 'A') .forEach(({ currLine, currIndex }) => { // 라인의 방향 const currVector = { x: Math.sign(clamp01(currLine.x1 - currLine.x2)), y: Math.sign(clamp01(currLine.y1 - currLine.y2)) } //어느쪽이 기준인지 확인. //대각선 제외 if (currVector.x !== 0 && currVector.y !== 0) return const prevLine = baseLines[(currIndex - 1 + baseLines.length) % baseLines.length] const nextLine = baseLines[(currIndex + 1) % baseLines.length] const prevVector = { x: Math.sign(clamp01(prevLine.x1 - prevLine.x2)), y: Math.sign(clamp01(prevLine.y1 - prevLine.y2)) } const nextVector = { x: Math.sign(clamp01(nextLine.x1 - nextLine.x2)), y: Math.sign(clamp01(nextLine.y1 - nextLine.y2)) } let gableLine //가로선 if (currVector.y === 0) { if (currVector.x === 1) { if (prevVector.y === 1 && nextVector.y === 1) { gableLine = nextLine } if (prevVector.y === -1 && nextVector.y === -1) { gableLine = prevLine } } if (currVector.x === -1) { if (prevVector.y === 1 && nextVector.y === 1) { gableLine = prevLine } if (prevVector.y === -1 && nextVector.y === -1) { gableLine = nextLine } } } //세로선 if (currVector.x === 0) { if (currVector.y === 1) { if (prevVector.x === 1 && nextVector.x === 1) { gableLine = prevLine } if (prevVector.x === -1 && nextVector.x === -1) { gableLine = nextLine } } if (currVector.y === -1) { if (prevVector.x === 1 && nextVector.x === 1) { gableLine = nextLine } if (prevVector.x === -1 && nextVector.x === -1) { gableLine = prevLine } } } if (gableLine.attributes.type !== LINE_TYPE.WALLLINE.GABLE) return //기준점 let stdPoint = [] //반대쪽 라인을 찾기위한 vector let oppFindVector if (gableLine === prevLine) { stdPoint.push(currLine.x1, currLine.y1) stdPoint.push(currLine.x2 + -currVector.x * nextLine.attributes.offset, currLine.y2 + -currVector.y * nextLine.attributes.offset) oppFindVector = { x: Math.sign(clamp01(nextLine.x2 - nextLine.x1)), y: Math.sign(clamp01(nextLine.y2 - nextLine.y1)) } } else { stdPoint.push(currLine.x2, currLine.y2) stdPoint.push(currLine.x1 + currVector.x * prevLine.attributes.offset, currLine.y1 + currVector.y * prevLine.attributes.offset) oppFindVector = { x: Math.sign(clamp01(prevLine.x1 - prevLine.x2)), y: Math.sign(clamp01(prevLine.y1 - prevLine.y2)) } } //반대쪽 라인들 (마루선을 위함) const oppLines = baseLines.filter((line) => { const lineVector = { x: Math.sign(clamp01(line.x1 - line.x2)), y: Math.sign(clamp01(line.y1 - line.y2)) } const oppVector = lineVector.x === 0 ? { x: Math.sign(clamp01(line.x1 - stdPoint[0])), y: 0 } : { x: 0, y: Math.sign(clamp01(line.y1 - stdPoint[1])) } const rightDirection = (currVector.x === lineVector.x && currVector.y !== lineVector.y) || (currVector.x !== lineVector.x && currVector.y === lineVector.y) const rightOpp = oppFindVector.x === oppVector.x && oppFindVector.y === oppVector.y return rightDirection && rightOpp }) const innerRidge = innerLines .filter((line) => line.name === LINE_TYPE.SUBLINE.RIDGE) .filter((line) => { const lineVector = { x: Math.sign(clamp01(line.x1 - line.x2)), y: Math.sign(clamp01(line.y1 - line.y2)) } //마루선을 찾는다. if ((currVector.x === 0 && lineVector.x === 0) || (currVector.y === 0 && lineVector.y === 0)) { //세로선 if (lineVector.x === 0) { const minY = Math.min(line.y1, line.y2) const maxY = Math.max(line.y1, line.y2) // 기준 라인 안에 들어있는 경우에만 처리. if ( Math.min(stdPoint[1], stdPoint[3]) <= minY && maxY <= Math.max(stdPoint[1], stdPoint[3]) && Math.min(stdPoint[0], ...oppLines.map((line) => line.x1)) <= line.x1 && line.x1 <= Math.max(stdPoint[0], ...oppLines.map((line) => line.x1)) ) { return true } } //가로선 if (lineVector.y === 0) { const minX = Math.min(line.x1, line.x2) const maxX = Math.max(line.x1, line.x2) // 기준 라인 안에 들어있는 경우에만 처리 if ( Math.min(stdPoint[0], stdPoint[2]) <= minX && maxX <= Math.max(stdPoint[0], stdPoint[2]) && Math.min(stdPoint[1], ...oppLines.map((line) => line.y1)) <= line.y1 && line.y1 <= Math.max(stdPoint[1], ...oppLines.map((line) => line.y1)) ) { return true } } } }) //1. 현재 라인을 기준으로 지붕선 추가. //1-1 stdPoint을 현재라인의 지붕 출폭 만큼 조정 const currOffset = currLine.attributes.offset let roofLinePoint = stdPoint if (currVector.x === 0) { //세로일때 //x축 조정 roofLinePoint[0] = roofLinePoint[0] + currVector.y * currOffset roofLinePoint[2] = roofLinePoint[2] + currVector.y * currOffset } else if (currVector.y === 0) { //가로일때 //y축 조정 roofLinePoint[1] = roofLinePoint[1] - currVector.x * currOffset roofLinePoint[3] = roofLinePoint[3] - currVector.x * currOffset } //지붕선추가. downRoofLines.push(drawRoofLine(roofLinePoint, canvas, roof, textMode)) //1-2 지붕선에서 oppLine으로 향하는 중 가장 가까운 마루선까지의 연결선 생성 const findRidgeEdge = { vertex1: { x: roofLinePoint[0], y: roofLinePoint[1] }, vertex2: { x: roofLinePoint[0] + oppFindVector.x, y: roofLinePoint[1] + oppFindVector.y }, } let minDistance = Infinity let minPoint let isLine innerRidge.forEach((line) => { const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } const intersect = edgesIntersection(lineEdge, findRidgeEdge) if (intersect) { let distance = Infinity if (currVector.x === 0) { const lineDistance1 = Math.abs(line.y1 - roofLinePoint[1]) const lineDistance2 = Math.abs(line.y2 - roofLinePoint[1]) distance = Math.min(lineDistance1, lineDistance2) } else if (currVector.y === 0) { const lineDistance1 = Math.abs(line.x1 - roofLinePoint[0]) const lineDistance2 = Math.abs(line.x2 - roofLinePoint[0]) distance = Math.min(lineDistance1, lineDistance2) } if (distance < minDistance) { minDistance = distance minPoint = intersect isLine = line } } }) if (minPoint) { const hipPoint = [roofLinePoint[0], roofLinePoint[1], minPoint.x, minPoint.y] downRoofLines.push( drawHipLine(hipPoint, canvas, roof, textMode, getDegreeByChon(currLine.attributes.pitch), getDegreeByChon(currLine.attributes.pitch)), ) if (isLine) { const newRidgePoint = [minPoint.x, minPoint.y] const distance1 = Math.sqrt(Math.pow(minPoint.x - isLine.x1, 2) + Math.pow(minPoint.y - isLine.y1, 2)) const distance2 = Math.sqrt(Math.pow(minPoint.x - isLine.x2, 2) + Math.pow(minPoint.y - isLine.y2, 2)) if (distance2 < distance1) { newRidgePoint.push(isLine.x1, isLine.y1) } else { newRidgePoint.push(isLine.x2, isLine.y2) } downRoofLines.push(drawRoofLine(newRidgePoint, canvas, roof, textMode)) } } }) // B타입 하단지붕 prevLine과 nextLine의 방향이 반대인데 현재 라인이 지붕 안쪽으로 들어가는 모양. downRoofGable .filter((l) => l.type === 'B') .forEach(({ currLine, currIndex }) => { // 라인의 방향 const currVector = { x: Math.sign(clamp01(currLine.x1 - currLine.x2)), y: Math.sign(clamp01(currLine.y1 - currLine.y2)) } //어느쪽이 기준인지 확인. //대각선 제외 if (currVector.x !== 0 && currVector.y !== 0) return const prevLine = baseLines[(currIndex - 1 + baseLines.length) % baseLines.length] const nextLine = baseLines[(currIndex + 1) % baseLines.length] const prevVector = { x: Math.sign(clamp01(prevLine.x1 - prevLine.x2)), y: Math.sign(clamp01(prevLine.y1 - prevLine.y2)) } const nextVector = { x: Math.sign(clamp01(nextLine.x1 - nextLine.x2)), y: Math.sign(clamp01(nextLine.y1 - nextLine.y2)) } const minX = Math.min(currLine.x1, currLine.x2) const maxX = Math.max(currLine.x1, currLine.x2) const minY = Math.min(currLine.y1, currLine.y2) const maxY = Math.max(currLine.y1, currLine.y2) const midX = (currLine.x1 + currLine.x2) / 2 const midY = (currLine.y1 + currLine.y2) / 2 let ridgeFindVector = { x: nextVector.x, y: nextVector.y } if (!checkWallPolygon.inPolygon({ x: midX, y: midY })) { ridgeFindVector = { x: prevVector.x, y: prevVector.y } } // 마루를 따라 생성되어야 하는 지붕선 추가. const oppLines = innerLines .filter((l) => l.name === LINE_TYPE.SUBLINE.RIDGE) .filter((line) => { const lineVector = { x: Math.sign(clamp01(line.x1 - line.x2)), y: Math.sign(clamp01(line.y1 - line.y2)) } let oppVector if (currVector.x === 0) { if (currVector.y === 1) { oppVector = { x: Math.sign(clamp01(currLine.x1 - line.x1)), y: 0 } } else if (currVector.y === -1) { oppVector = { x: Math.sign(clamp01(line.x1 - currLine.x1)), y: 0 } } } else if (currVector.y === 0) { if (currVector.x === 1) { oppVector = { x: 0, y: Math.sign(clamp01(line.y1 - currLine.y1)) } } else if (currVector.x === -1) { oppVector = { x: 0, y: Math.sign(clamp01(currLine.y1 - line.y1)) } } } const lineMinX = Math.min(line.x1, line.x2) const lineMaxX = Math.max(line.x1, line.x2) const lineMinY = Math.min(line.y1, line.y2) const lineMaxY = Math.max(line.y1, line.y2) const isInside = lineVector.y === 0 ? minX <= lineMinX && lineMaxX <= maxX : minY <= lineMinY && lineMaxY <= maxY const rightOpp = ridgeFindVector.x === oppVector.x && ridgeFindVector.y === oppVector.y return rightOpp && isInside }) // 현재 라인의 지붕선 추가. const currOffset = currLine.attributes.offset const roofPoint = [ currLine.x1 + -nextVector.x * currOffset, currLine.y1 + -nextVector.y * currOffset, currLine.x2 + -nextVector.x * currOffset, currLine.y2 + -nextVector.y * currOffset, ] downRoofLines.push(drawRoofLine(roofPoint, canvas, roof, textMode)) // 현재 라인 좌표의 시작과 끝 포인트에서 직교하는 포인트와 라인을 찾는다 let orthogonalStartPoint, orthogonalStartDistance = Infinity, orthogonalStartLine let orthogonalEndPoint, orthogonalEndDistance = Infinity, orthogonalEndLine oppLines.forEach((line) => { if (currVector.x === 0) { //세로선 // 시작포인트와 가까운 포인트의 길이 const lineStartDist = Math.abs(currLine.y1 - line.y1) < Math.abs(currLine.y1 - line.y2) ? Math.abs(currLine.y1 - line.y1) : Math.abs(currLine.y1 - line.y2) const lineStartPoint = Math.abs(currLine.y1 - line.y1) < Math.abs(currLine.y1 - line.y2) ? { x: line.x1, y: currLine.y1 } : { x: line.x2, y: currLine.y1 } if (lineStartDist < orthogonalStartDistance) { orthogonalStartDistance = lineStartDist orthogonalStartPoint = lineStartPoint orthogonalStartLine = line } // 종료포인트와 가까운 포인트의 길이 const lineEndDist = Math.abs(currLine.y2 - line.y1) < Math.abs(currLine.y2 - line.y2) ? Math.abs(currLine.y2 - line.y2) : Math.abs(currLine.y2 - line.y1) const lineEndPoint = Math.abs(currLine.y2 - line.y1) < Math.abs(currLine.y2 - line.y2) ? { x: line.x2, y: currLine.y2 } : { x: line.x1, y: currLine.y2 } if (lineEndDist < orthogonalEndDistance) { orthogonalEndDistance = lineEndDist orthogonalEndPoint = lineEndPoint orthogonalEndLine = line } } else if (currVector.y === 0) { //가로선 // 시작포인트와 가까운 포인트의 길이 const lineStartDist = Math.abs(currLine.x1 - line.x1) < Math.abs(currLine.x1 - line.x2) ? Math.abs(currLine.x1 - line.x1) : Math.abs(currLine.x1 - line.x2) const lineStartPoint = Math.abs(currLine.x1 - line.x1) < Math.abs(currLine.x1 - line.x2) ? { x: currLine.x1, y: line.y1 } : { x: currLine.x1, y: line.y2 } if (lineStartDist < orthogonalStartDistance) { orthogonalStartDistance = lineStartDist orthogonalStartPoint = lineStartPoint orthogonalStartLine = line } //종료포인트와 가까운 포인트의 길이 const lineEndDist = Math.abs(currLine.x2 - line.x1) < Math.abs(currLine.x2 - line.x2) ? Math.abs(currLine.x2 - line.x1) : Math.abs(currLine.x2 - line.x2) const lineEndPoint = Math.abs(currLine.x2 - line.x1) < Math.abs(currLine.x2 - line.x2) ? { x: currLine.x2, y: line.y1 } : { x: currLine.x2, y: line.y2 } if (lineEndDist < orthogonalEndDistance) { orthogonalEndDistance = lineEndDist orthogonalEndPoint = lineEndPoint orthogonalEndLine = line } } }) //직교 라인이 있는 경우 if (orthogonalStartLine !== undefined && orthogonalEndLine !== undefined) { if (orthogonalStartLine === orthogonalEndLine) { //직교 라인이 1개일때 처리 downRoofLines.push( drawRoofLine([orthogonalStartPoint.x, orthogonalStartPoint.y, orthogonalEndPoint.x, orthogonalEndPoint.y], canvas, roof, textMode), ) } else { //직교 라인이 2개일때 처리 // 시작 라인 처리 const startDist1 = Math.sqrt( Math.pow(orthogonalStartPoint.x - orthogonalStartLine.x1, 2) + Math.pow(orthogonalStartPoint.y - orthogonalStartLine.y1, 2), ) const startDist2 = Math.sqrt( Math.pow(orthogonalStartPoint.x - orthogonalStartLine.x2, 2) + Math.pow(orthogonalStartPoint.y - orthogonalStartLine.y2, 2), ) const otherStartPoint = startDist1 > startDist2 ? { x: orthogonalStartLine.x1, y: orthogonalStartLine.y1 } : { x: orthogonalStartLine.x2, y: orthogonalStartLine.y2 } downRoofLines.push( drawRoofLine([orthogonalStartPoint.x, orthogonalStartPoint.y, otherStartPoint.x, otherStartPoint.y], canvas, roof, textMode), ) const endDist1 = Math.sqrt( Math.pow(orthogonalEndPoint.x - orthogonalEndLine.x1, 2) + Math.pow(orthogonalEndPoint.y - orthogonalEndLine.y1, 2), ) const endDist2 = Math.sqrt( Math.pow(orthogonalEndPoint.x - orthogonalEndLine.x2, 2) + Math.pow(orthogonalEndPoint.y - orthogonalEndLine.y2, 2), ) const otherEndPoint = endDist1 > endDist2 ? { x: orthogonalEndLine.x1, y: orthogonalEndLine.y1 } : { x: orthogonalEndLine.x2, y: orthogonalEndLine.y2 } downRoofLines.push(drawRoofLine([orthogonalEndPoint.x, orthogonalEndPoint.y, otherEndPoint.x, otherEndPoint.y], canvas, roof, textMode)) } //지붕선(roofPoint)에서 직교포인트까지 연결하는 라인을 추가한다. const orthogonalPoint1 = [roofPoint[0], roofPoint[1], orthogonalStartPoint.x, orthogonalStartPoint.y] const orthogonalPoint2 = [roofPoint[2], roofPoint[3], orthogonalEndPoint.x, orthogonalEndPoint.y] downRoofLines.push( drawHipLine( orthogonalPoint1, canvas, roof, textMode, getDegreeByChon(currLine.attributes.pitch), getDegreeByChon(currLine.attributes.pitch), ), ) downRoofLines.push( drawHipLine( orthogonalPoint2, canvas, roof, textMode, getDegreeByChon(currLine.attributes.pitch), getDegreeByChon(currLine.attributes.pitch), ), ) } }) //처마에서 파생된 하단 지붕 라인 처리. downRoofEaves.forEach(({ currLine, currIndex }) => { const prevIndex = (currIndex - 1 + baseLines.length) % baseLines.length const nextIndex = (currIndex + 1) % baseLines.length const analyze = analyzeLine(currLine) const currMidX = (currLine.x1 + currLine.x2) / 2 const currMidY = (currLine.y1 + currLine.y2) / 2 const roofLine = analyze.roofLine const roofVector = { x: Math.sign(clamp01(roofLine.x2 - roofLine.x1)), y: Math.sign(clamp01(roofLine.y2 - roofLine.y1)) } const originPoint = currLine.attributes.originPoint let isFlowInside // 조정된 형이 안쪽인지 바깥쪽인지 판단. let flowDistance = 0 // 조정된 거리 if (analyze.isVertical) { //세로선 flowDistance = Math.abs(currLine.x1 - originPoint.x1) const isLeftIn = checkWallPolygon.inPolygon({ x: currMidX - 1, y: currMidY }) const vector = Math.sign(currLine.x1 - originPoint.x1) isFlowInside = isLeftIn ? vector < 0 : vector > 0 } else if (analyze.isHorizontal) { //가로선 flowDistance = Math.abs(currLine.y1 - originPoint.y1) const isTopIn = checkWallPolygon.inPolygon({ x: currMidX, y: currMidY - 1 }) const vector = Math.sign(currLine.y1 - originPoint.y1) isFlowInside = isTopIn ? vector < 0 : vector > 0 } const roofCheckPoint = { x: roofLine.x2 + roofVector.x, y: roofLine.y2 + roofVector.y } let otherLine = roof.inPolygon(roofCheckPoint) ? baseLines[nextIndex] : baseLines[prevIndex] //상단 지붕 const upLine = isFlowInside ? otherLine : currLine //하단 지붕 const downLine = isFlowInside ? currLine : otherLine const allLinePoint = [ { x: upLine.x1, y: upLine.y1 }, { x: upLine.x2, y: upLine.y2 }, { x: downLine.x1, y: downLine.y1 }, { x: downLine.x2, y: downLine.y2 }, ] //두 라인이 만나는 포인트 let joinPoint allLinePoint.forEach((point, i) => { allLinePoint.forEach((point2, j) => { if (i !== j && point.x === point2.x && point.y === point2.y) { joinPoint = point } }) }) const upAnalyze = analyzeLine(upLine) const upRoofLine = upAnalyze.roofLine const addUpOffset = flowDistance //상단 지붕선의 추가길이 const upRoofVector = { x: Math.sign(clamp01(upRoofLine.x2 - upRoofLine.x1)), y: Math.sign(clamp01(upRoofLine.y2 - upRoofLine.y1)) } const upRoofPoint = [] //상단 지붕선 let upHipStartPoint if (roof.inPolygon({ x: upRoofLine.x2 + upRoofVector.x * addUpOffset, y: upRoofLine.y2 + upRoofVector.y * addUpOffset })) { upRoofPoint.push(upRoofLine.x1, upRoofLine.y1, upRoofLine.x2 + upRoofVector.x * addUpOffset, upRoofLine.y2 + upRoofVector.y * addUpOffset) upHipStartPoint = { x: upRoofLine.x1, y: upRoofLine.y1 } } else { upRoofPoint.push(upRoofLine.x1 - upRoofVector.x * addUpOffset, upRoofLine.y1 - upRoofVector.y * addUpOffset, upRoofLine.x2, upRoofLine.y2) upHipStartPoint = { x: upRoofLine.x2, y: upRoofLine.y2 } } const downDegree = getDegreeByChon(downLine.attributes.pitch) const downAnalyze = analyzeLine(downLine) const downRoofLine = downAnalyze.roofLine const addDownOffset = upLine.attributes.offset // 하단 지붕선의 추가길이 const downRoofVector = { x: Math.sign(clamp01(downRoofLine.x2 - downRoofLine.x1)), y: Math.sign(clamp01(downRoofLine.y2 - downRoofLine.y1)) } const downRoofPoint = [] // 하단 지붕선 let downHipStartPoint if (roof.inPolygon({ x: downRoofLine.x2 + downRoofVector.x * addDownOffset, y: downRoofLine.y2 + downRoofVector.y * addDownOffset })) { downRoofPoint.push( downRoofLine.x1, downRoofLine.y1, downRoofLine.x2 + downRoofVector.x * addDownOffset, downRoofLine.y2 + downRoofVector.y * addDownOffset, ) downHipStartPoint = { x: downRoofPoint[2], y: downRoofPoint[3] } } else { downRoofPoint.push( downRoofLine.x1 - downRoofVector.x * addDownOffset, downRoofLine.y1 - downRoofVector.y * addDownOffset, downRoofLine.x2, downRoofLine.y2, ) downHipStartPoint = { x: downRoofPoint[0], y: downRoofPoint[1] } } //상단지붕선과 만나는 innerLines 조정 const upRoofEdge = { vertex1: { x: upRoofPoint[0], y: upRoofPoint[1] }, vertex2: { x: upRoofPoint[2], y: upRoofPoint[3] } } let upHipVector = getHalfAngleVector(upLine, downLine) const vectorCheckPoint = { x: joinPoint.x + upHipVector.x, y: joinPoint.y + upHipVector.y } if (!checkWallPolygon.inPolygon(vectorCheckPoint)) { upHipVector = { x: -upHipVector.x, y: -upHipVector.y } } const findUpP1 = { x: joinPoint.x, y: joinPoint.y } const findUpP2 = { x: joinPoint.x + upHipVector.x, y: joinPoint.y + upHipVector.y } // 상하단 지붕이 만나는 교점에서 파생되는 추녀마루 확인. let joinHipLine = innerLines.find((line) => { return isPointOnLineNew(line, findUpP1) && isPointOnLineNew(line, findUpP2) }) if (joinHipLine) { // 상단 지붕선과 추녀마루의 교점 파악. const joinHipEdge = { vertex1: { x: joinHipLine.x1, y: joinHipLine.y1 }, vertex2: { x: joinHipLine.x2, y: joinHipLine.y2 } } const intersectJoin = edgesIntersection(upRoofEdge, joinHipEdge) // 연결된 추녀마루 포인트 조정 const pointVector1 = { x: Math.sign(clamp01(joinHipLine.x1 - intersectJoin.x)), y: Math.sign(clamp01(joinHipLine.y1 - intersectJoin.y)) } const pointVector2 = { x: Math.sign(clamp01(joinHipLine.x2 - intersectJoin.x)), y: Math.sign(clamp01(joinHipLine.y2 - intersectJoin.y)) } let joinEndPoint if (pointVector1.x === upHipVector.x && pointVector1.y === upHipVector.y) { joinHipLine.set({ x1: intersectJoin.x, y1: intersectJoin.y, x2: joinHipLine.x1, y2: joinHipLine.y1, }) joinHipLine.setCoords() joinEndPoint = { x: joinHipLine.x1, y: joinHipLine.y1 } } else if (pointVector2.x === upHipVector.x && pointVector2.y === upHipVector.y) { joinHipLine.set({ x1: intersectJoin.x, y1: intersectJoin.y, x2: joinHipLine.x2, y2: joinHipLine.y2, }) joinHipLine.setCoords() joinEndPoint = { x: joinHipLine.x2, y: joinHipLine.y2 } } let upSideLine baseLines.forEach((line, index) => { if (line === upLine) { const upPrevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] const upNextLine = baseLines[(index + 1) % baseLines.length] upSideLine = upPrevLine === downLine ? upNextLine : upPrevLine } }) //상단 지붕선과 붙어있는 다른 지붕선이 처마일때 추녀마루가 포함될수 있기 때문에 확인하여 라인조정 if (upSideLine && upSideLine.attributes.type === LINE_TYPE.WALLLINE.EAVES) { const hipDegree = getDegreeByChon(upSideLine.attributes.pitch) let minDistance = Infinity let connectPoint innerLines .filter((line) => line !== joinHipLine) .forEach((line) => { const lineEdge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } const intersect = edgesIntersection(upRoofEdge, lineEdge) if ( intersect && isPointOnLineNew(line, intersect) && isPointOnLineNew({ x1: upRoofPoint[0], y1: upRoofPoint[1], x2: upRoofPoint[2], y2: upRoofPoint[3] }, intersect) ) { const distance = Math.sqrt(Math.pow(intersect.x - upHipStartPoint.x, 2) + Math.pow(intersect.y - upHipStartPoint.y, 2)) if (distance < minDistance) { minDistance = distance connectPoint = intersect } } }) if (connectPoint && minDistance > EPSILON) { let upHipPoint if (upHipStartPoint.x === upRoofLine.x1 && upHipStartPoint.y === upRoofLine.y1) { upHipPoint = [upHipStartPoint.x, upHipStartPoint.y, connectPoint.x, connectPoint.y] } else { upHipPoint = [connectPoint.x, connectPoint.y, upHipStartPoint.x, upHipStartPoint.y] } downRoofLines.push(drawHipLine(upHipPoint, canvas, roof, textMode, hipDegree, hipDegree)) //각도 있는 처마지붕선 downRoofLines.push(drawRoofLine([connectPoint.x, connectPoint.y, intersectJoin.x, intersectJoin.y], canvas, roof, textMode)) //각도 없는 처마지붕선 roof.adjustRoofLines.push({ point: upHipPoint, roofIdx: upRoofLine.idx }) roof.adjustRoofLines.push({ point: [connectPoint.x, connectPoint.y, intersectJoin.x, intersectJoin.y], roofIdx: upRoofLine.idx }) } else { downRoofLines.push(drawRoofLine([upHipStartPoint.x, upHipStartPoint.y, intersectJoin.x, intersectJoin.y], canvas, roof, textMode)) roof.adjustRoofLines.push({ point: [upHipStartPoint.x, upHipStartPoint.y, intersectJoin.x, intersectJoin.y], roofIdx: upRoofLine.idx }) } } else { downRoofLines.push(drawRoofLine([upHipStartPoint.x, upHipStartPoint.y, intersectJoin.x, intersectJoin.y], canvas, roof, textMode)) roof.adjustRoofLines.push({ point: [upHipStartPoint.x, upHipStartPoint.y, intersectJoin.x, intersectJoin.y], roofIdx: upRoofLine.idx }) } //하단지붕선 추가. const downHipEdge = { vertex1: { x: downHipStartPoint.x, y: downHipStartPoint.y }, vertex2: { x: downHipStartPoint.x + upRoofVector.x, y: downHipStartPoint.y + upRoofVector.y }, } const intersectDownJoin = edgesIntersection(downHipEdge, joinHipEdge) //하단 지붕선에는 지붕선, 추녀마루1, 추녀마루2 가 생성되는것이 기본모양임. if (intersectDownJoin && isPointOnLineNew(joinHipLine, intersectDownJoin)) { //지붕선 downRoofLines.push(drawRoofLine(downRoofPoint, canvas, roof, textMode)) //지붕선에서 올라가는 추녀마루1 downRoofLines.push( drawHipLine( [downHipStartPoint.x, downHipStartPoint.y, intersectDownJoin.x, intersectDownJoin.y], canvas, roof, textMode, downDegree, downDegree, ), ) //추녀마루에서 마루로 연결되는 추녀마루2 downRoofLines.push( drawHipLine([intersectDownJoin.x, intersectDownJoin.y, joinEndPoint.x, joinEndPoint.y], canvas, roof, textMode, downDegree, downDegree), ) roof.adjustRoofLines.push({ point: downRoofPoint, roofIdx: downRoofLine.idx }) } } }) //추가된 하단 지붕 라인 innerLines에 추가. innerLines.push(...downRoofLines) //지붕선에 따라 라인추가 작업 처리. const innerLinesPoints = [] innerLines.forEach((line) => { const hasCoord1 = innerLinesPoints.find((p) => p.x === line.x1 && p.y === line.y1) const hasCoord2 = innerLinesPoints.find((p) => p.x === line.x2 && p.y === line.y2) if (!hasCoord1) innerLinesPoints.push({ x: line.x1, y: line.y1 }) if (!hasCoord2) innerLinesPoints.push({ x: line.x2, y: line.y2 }) }) roof.lines.forEach((currentLine, index) => { const prevLine = roof.lines[(index - 1 + roof.lines.length) % roof.lines.length] const nextLine = roof.lines[(index + 1) % roof.lines.length] const prevDegree = prevLine.attributes.pitch ? getDegreeByChon(prevLine.attributes.pitch) : 0 const nextDegree = nextLine.attributes.pitch ? getDegreeByChon(nextLine.attributes.pitch) : 0 const splitPoint = [] let hasOverlapLine = false const minX = Math.min(currentLine.x1, currentLine.x2) const maxX = Math.max(currentLine.x1, currentLine.x2) const minY = Math.min(currentLine.y1, currentLine.y2) const maxY = Math.max(currentLine.y1, currentLine.y2) innerLines.forEach((innerLine) => { const innerLineMinX = Math.min(innerLine.x1, innerLine.x2) const innerLineMaxX = Math.max(innerLine.x1, innerLine.x2) const innerLineMinY = Math.min(innerLine.y1, innerLine.y2) const innerLineMaxY = Math.max(innerLine.y1, innerLine.y2) if (innerLineMinX <= minX && innerLineMaxX >= maxX && innerLineMinY <= minY && innerLineMaxY >= maxY) { hasOverlapLine = true } if (minX <= innerLineMinX && maxX >= innerLineMaxX && minY <= innerLineMinY && maxY >= innerLineMaxY) { hasOverlapLine = true } }) if (hasOverlapLine) return innerLinesPoints.forEach((point) => { if ( isPointOnLineNew(currentLine, point) && !(almostEqual(currentLine.x1, point.x) && almostEqual(currentLine.y1, point.y)) && !(almostEqual(currentLine.x2, point.x) && almostEqual(currentLine.y2, point.y)) ) { const distance = Math.sqrt((point.x - currentLine.x1) ** 2 + (point.y - currentLine.y1) ** 2) splitPoint.push({ point, distance }) } }) if (splitPoint.length > 0) { splitPoint.sort((a, b) => a[1] - b[1]) let startPoint = { x: currentLine.x1, y: currentLine.y1 } for (let i = 0; i < splitPoint.length; i++) { const point = splitPoint[i].point if (i === 0) { innerLines.push(drawHipLine([startPoint.x, startPoint.y, point.x, point.y], canvas, roof, textMode, prevDegree, prevDegree)) } else { innerLines.push(drawRoofLine([startPoint.x, startPoint.y, point.x, point.y], canvas, roof, textMode)) } startPoint = point } if (splitPoint.length === 1) { innerLines.push(drawRoofLine([startPoint.x, startPoint.y, currentLine.x2, currentLine.y2], canvas, roof, textMode)) } else { innerLines.push(drawHipLine([startPoint.x, startPoint.y, currentLine.x2, currentLine.y2], canvas, roof, textMode, nextDegree, nextDegree)) } } else { innerLines.push(drawRoofLine([currentLine.x1, currentLine.y1, currentLine.x2, currentLine.y2], canvas, roof, textMode)) } }) //지붕 innerLines에 추가. roof.innerLines = innerLines canvas .getObjects() .filter((object) => object.name === 'check') .forEach((object) => canvas.remove(object)) canvas.renderAll() } /** * 라인 방향 확인 용 * @param x1 * @param y1 * @param x2 * @param y2 * @param color * @param roofId * @returns {*} */ export const createArrow = ({ x1, y1, x2, y2 }, color, roofId) => { const angle = Math.atan2(y2 - y1, x2 - x1) const headLength = 15 const points = [ { x: x1, y: y1 }, { x: x2, y: y2 }, { x: x2 - headLength * Math.cos(angle - Math.PI / 7), y: y2 - headLength * Math.sin(angle - Math.PI / 7), }, { x: x2, y: y2 }, { x: x2 - headLength * Math.cos(angle + Math.PI / 7), y: y2 - headLength * Math.sin(angle + Math.PI / 7), }, ] return new fabric.Polyline(points, { fill: 'transparent', stroke: color, strokeWidth: 3, selectable: false, parentId: roofId, name: 'check', }) } /** * 선분과 선분의 교점을 구한다. * @param line1Start * @param line1End * @param line2Start * @param line2End */ const lineIntersection = (line1Start, line1End, line2Start, line2End) => { const dx1 = line1End.x - line1Start.x const dy1 = line1End.y - line1Start.y const dx2 = line2End.x - line2Start.x const dy2 = line2End.y - line2Start.y // const denominator = (line2End.y - line2Start.y) * (line1End.x - line1Start.x) - (line2End.x - line2Start.x) * (line1End.y - line1Start.y) const denominator = dy2 * dx1 - dx2 * dy1 if (Math.abs(denominator) < EPSILON) { const isOpposite = dx1 * dx2 + dy1 * dy2 < 0 const isOverlap = isPointOnLineNew({ x1: line1Start.x, y1: line1Start.y, x2: line1End.x, y2: line1End.y }, { x: line2Start.x, y: line2Start.y }) if (isOpposite && isOverlap) { return { x: line2Start.x, y: line2Start.y } } return null } // 평행 let ua = (dx2 * (line1Start.y - line2Start.y) - dy2 * (line1Start.x - line2Start.x)) / denominator let ub = (dx1 * (line1Start.y - line2Start.y) - dy1 * (line1Start.x - line2Start.x)) / denominator ua = clamp01(ua) ub = clamp01(ub) if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { return { x: line1Start.x + ua * dx1, y: line1Start.y + ua * dy1, } } return null } /** * 경계값 보정용 헬퍼 * @param t * @returns {*|number} */ const clamp01 = (t) => { if (almostEqual(t, 0, EPSILON)) return 0 if (almostEqual(t, 1, EPSILON)) return 1 return t } /** * 두 라인이 평행한지 확인한다. * @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 */ 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) => { 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) { bisector = { x: Math.sign(intersection.x - is.x), y: Math.sign(intersection.y - is.y) } } return bisector } /** * a 와 b 가 epsilon내 오차인지 확인한다. * @param a * @param b * @param epsilon * @returns {boolean} */ const almostEqual = (a, b, epsilon = 1e-10) => { if (a === Infinity || b === Infinity) return false return Math.abs(a - b) <= epsilon } /** * segment가 fromPoint에서 떨어진 방향을 구한다. * @param segment * @param fromPoint * @returns {{x, y}} */ const getDirection = (segment, fromPoint) => { 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 } } /** * lines중 points가 이미 존재하는지 확인한다. * @param lines * @param points * @returns {boolean} */ const alreadyPoints = (lines, points) => { let has = false lines.forEach((line) => { const linePoints = [line.x1, line.y1, line.x2, line.y2] if ( (almostEqual(linePoints[0], points[0], 1) && almostEqual(linePoints[1], points[1], 1) && almostEqual(linePoints[2], points[2], 1) && almostEqual(linePoints[3], points[3], 1)) || (almostEqual(linePoints[0], points[2], 1) && almostEqual(linePoints[1], points[3], 1) && almostEqual(linePoints[2], points[0], 1) && almostEqual(linePoints[3], points[1], 1)) ) { has = true } }) return has } /** * 추녀 마루를 그린다. * @param points * @param canvas * @param roof * @param textMode * @param prevDegree * @param currentDegree */ const drawHipLine = (points, canvas, roof, textMode, prevDegree, currentDegree) => { /** 대각선인 경우 경사를 조정해서 계산*/ const baseX = Big(points[0]).minus(Big(points[2])).abs() const baseY = Big(points[1]).minus(Big(points[3])).abs() if (baseX.gt(1) && baseY.gt(1)) { const hypotenuse = calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3] }) const base = getAdjacent(hypotenuse) const heightX = base * Math.tan((currentDegree * Math.PI) / 180) const heightY = base * Math.tan((prevDegree * Math.PI) / 180) const degreeX = Math.atan2(heightX, hypotenuse) * (180 / Math.PI) const degreeY = Math.atan2(heightY, hypotenuse) * (180 / Math.PI) if (Math.abs(degreeX - degreeY) < 1) { currentDegree = degreeX prevDegree = degreeY } } const hip = new QLine(points, { parentId: roof.id, fontSize: roof.fontSize, stroke: '#1083E3', strokeWidth: 2, name: LINE_TYPE.SUBLINE.HIP, textMode: textMode, attributes: { roofId: roof.id, planeSize: calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3], }), actualSize: prevDegree === currentDegree ? calcLineActualSize( { x1: points[0], y1: points[1], x2: points[2], y2: points[3], }, currentDegree, ) : 0, }, }) canvas.add(hip) hip.bringToFront() canvas.renderAll() return hip } /** * 마루를 그린다. * @param points * @param canvas * @param roof * @param textMode * @returns {*} */ const drawRidgeLine = (points, canvas, roof, textMode) => { const ridge = new QLine(points, { parentId: roof.id, fontSize: roof.fontSize, stroke: '#1083E3', strokeWidth: 2, name: LINE_TYPE.SUBLINE.RIDGE, textMode: textMode, attributes: { roofId: roof.id, planeSize: calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3], }), actualSize: calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3], }), }, }) canvas.add(ridge) ridge.bringToFront() canvas.renderAll() return ridge } /** * 지붕선을 그린다. * @param points * @param canvas * @param roof * @param textMode * @returns {*} */ const drawRoofLine = (points, canvas, roof, textMode) => { const ridge = new QLine(points, { parentId: roof.id, fontSize: roof.fontSize, stroke: '#1083E3', strokeWidth: 2, name: LINE_TYPE.SUBLINE.HIP, textMode: textMode, attributes: { roofId: roof.id, planeSize: calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3], }), actualSize: calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3], }), }, }) canvas.add(ridge) ridge.bringToFront() canvas.renderAll() return ridge } /** * 벡터를 정규화(Normalization)하는 함수 * @param v * @returns {{x: *, y: *}|{x: number, y: number}} */ const normalizeVector = (v) => { /** 벡터의 크기(길이)*/ const magnitude = Big(v.x).pow(2).plus(Big(v.y).pow(2)).sqrt() if (magnitude.eq(0)) return { x: 0, y: 0 } // 크기가 0일 경우 (예외 처리) return { x: Math.sign(Big(v.x).div(magnitude).toNumber()), y: Math.sign(Big(v.y).div(magnitude).toNumber()) } } /** * 사잇각의 절반 방향 벡터 계산 함수 * @param line1 * @param line2 * @returns {{x: *, y: *}|{x: number, y: number}} */ const getHalfAngleVector = (line1, line2) => { const v1 = { x: Big(line1.x2).minus(Big(line1.x1)).toNumber(), y: Big(line1.y1).minus(Big(line1.y2)).toNumber() } const v2 = { x: Big(line2.x2).minus(Big(line2.x1)).toNumber(), y: Big(line2.y1).minus(Big(line2.y2)).toNumber() } /** * 벡터 정규화 * @type {{x: *, y: *}|{x: number, y: number}} */ const unitV1 = normalizeVector(v1) // 첫 번째 벡터를 정규화 const unitV2 = normalizeVector(v2) // 두 번째 벡터를 정규화 /** * 두 벡터를 더합니다 * @type {{x: *, y: *}} */ const summedVector = { x: Big(unitV1.x).plus(Big(unitV2.x)).toNumber(), y: Big(unitV1.y).plus(Big(unitV2.y)).toNumber(), } /** 결과 벡터를 정규화하여 사잇각 벡터를 반환합니다 */ return normalizeVector(summedVector) } /** * 지붕 모양 을 변경한다. * @param polygon * @param canvas */ const reDrawPolygon = (polygon, canvas) => { const lines = polygon.lines let point = [] lines.forEach((line) => point.push({ x: line.x1, y: line.y1 })) canvas?.remove(polygon) const newPolygon = new QPolygon(point, { id: polygon.id, name: polygon.name, fill: polygon.fill, stroke: polygon.stroke, strokeWidth: polygon.strokeWidth, selectable: polygon.selectable, fontSize: polygon.fontSize, wall: polygon.wall !== undefined ? polygon.wall : null, originX: polygon.originX, originY: polygon.originY, }) const newLines = newPolygon.lines newLines.forEach((line) => { lines.forEach((l) => { if (line.x1 === l.x1 && line.y1 === l.y1) { line.id = l.id line.attributes = l.attributes } }) const lineLength = calcLinePlaneSize({ x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2 }) if (line.attributes !== undefined) { line.attributes.planeSize = lineLength line.attributes.actualSize = lineLength } else { line.attributes = { roofId: newPolygon.id, planeSize: lineLength, actualSize: lineLength, } } }) canvas.add(newPolygon) canvas.renderAll() return newPolygon } /** * 지붕의 centerLine을 그린다. * @param roof * @param canvas * @param textMode */ /*const drawCenterLine = (roof, canvas, textMode) => { //현재 지붕의 centerLine을 다 지운다. canvas .getObjects() .filter((object) => object.attributes?.roofId === roof.id) .filter((line) => line.attributes?.type === 'pitchSizeLine') .forEach((line) => canvas.remove(line)) const roofLines = roof.lines roofLines.forEach((currentRoof) => { const hips = roof.innerLines.filter( (line) => (currentRoof.x1 === line.x1 && currentRoof.y1 === line.y1) || (currentRoof.x2 === line.x1 && currentRoof.y2 === line.y1), ) const ridge = roof.innerLines .filter((line) => line.name === LINE_TYPE.SUBLINE.RIDGE) .filter((line) => { if (currentRoof.x1 === currentRoof.x2) { if (line.x1 === line.x2) { return line } } else { if (line.y1 === line.y2) { return line } } }) .reduce((prev, current) => { let currentDistance, prevDistance if (Math.sign(currentRoof.x1 - currentRoof.x2) === 0) { currentDistance = Math.abs(currentRoof.y1 - current.y1) prevDistance = prev ? Math.abs(currentRoof.y1 - prev.y1) : Infinity } else { currentDistance = Math.abs(currentRoof.y1 - current.y2) prevDistance = prev ? Math.abs(currentRoof.y1 - current.y2) : Infinity } return prevDistance < currentDistance ? prev : current }, null) let points = [] if (hips.length === 2 && Math.abs(hips[0].x2 - hips[1].x2) < 1 && Math.abs(hips[0].y2 - hips[1].y2) < 1) { const x1 = (currentRoof.x1 + currentRoof.x2) / 2 const y1 = (currentRoof.y1 + currentRoof.y2) / 2 points.push(x1, y1, hips[0].x2, hips[0].y2) } else if (hips.length > 1) { if ( ((ridge?.x1 === hips[0].x2 && ridge?.y1 === hips[0].y2) || (ridge?.x2 === hips[0].x2 && ridge?.y2 === hips[0].y2)) && ((ridge?.x1 === hips[1].x2 && ridge?.y1 === hips[1].y2) || (ridge?.x2 === hips[1].x2 && ridge?.y2 === hips[1].y2)) ) { //사각이면 마루와 현재 라인 사이에 길이를 구한다 if (Math.sign(currentRoof.x1 - currentRoof.x2) === 0) { const yPoints = [currentRoof.y1, currentRoof.y2, ridge.y1, ridge.y2].sort((a, b) => a - b) const x1 = (currentRoof.x1 + currentRoof.x2) / 2 const x2 = (ridge.x1 + ridge.x2) / 2 const y = (yPoints[1] + yPoints[2]) / 2 if ( ((currentRoof.y1 <= y && y <= currentRoof.y2) || (currentRoof.y2 <= y && y <= currentRoof.y1)) && ((ridge.y1 <= y && y <= ridge.y2) || (ridge.y2 <= y && y <= ridge.y1)) ) { points.push(x1, y, x2, y) } } else { const xPoints = [currentRoof.x1, currentRoof.x2, ridge.x1, ridge.x2].sort((a, b) => a - b) const y1 = (currentRoof.y1 + currentRoof.y2) / 2 const y2 = (ridge.y1 + ridge.y2) / 2 const x = (xPoints[1] + xPoints[2]) / 2 if ( ((currentRoof.x1 <= x && x <= currentRoof.x2) || (currentRoof.x2 <= x && x <= currentRoof.x1)) && ((ridge.x1 <= x && x <= ridge.x2) || (ridge.x2 <= x && x <= ridge.x1)) ) { points.push(x, y1, x, y2) } } } else { if (Math.sign(currentRoof.x1 - currentRoof.x2) === 0) { let xPoints = [] xPoints.push(ridge?.x1, ridge?.x2) hips.forEach((hip) => { xPoints.push(hip.x2) }) let maxPoint = xPoints.reduce((prev, current) => { const currentDistance = Math.abs(currentRoof.x1 - current) const prevDistance = prev ? Math.abs(currentRoof.x1 - prev) : 0 return prevDistance > currentDistance ? prev : current }, null) xPoints = xPoints.filter((point) => point === maxPoint) let oppositeLine if (xPoints.length === 1) { if (ridge?.x1 === xPoints[0] || ridge?.x2 === xPoints[0]) { oppositeLine = ridge } if (oppositeLine === undefined) { oppositeLine = hips.find((hip) => hip.x2 === xPoints[0]) } points.push(currentRoof.x1, oppositeLine.y2, oppositeLine.x2, oppositeLine.y2) } else if (xPoints.length > 1) { xPoints = [...new Set(xPoints)] // 중복제거 if (ridge?.length > 0) { let boolX1 = xPoints.some((x) => x === ridge.x1) let boolX2 = xPoints.some((x) => x === ridge.x2) if (boolX1 && boolX2) { oppositeLine = ridge } if (oppositeLine) { const sortPoints = [currentRoof.y1, currentRoof.y2, oppositeLine.y1, oppositeLine.y2].sort((a, b) => a - b) const y = (sortPoints[1] + sortPoints[2]) / 2 if ( ((currentRoof.y1 <= y && y <= currentRoof.y2) || (currentRoof.y2 <= y && y <= currentRoof.y1)) && ((oppositeLine.y1 <= y && y <= oppositeLine.y2) || (oppositeLine.y2 <= y && y <= oppositeLine.y1)) ) { points.push(currentRoof.x1, y, oppositeLine.x1, y) } } } } } else { let yPoints = [] yPoints.push(ridge?.y1, ridge?.y2) hips.forEach((hip) => { yPoints.push(hip.y2) }) const maxPoint = yPoints.reduce((prev, current) => { const currentDistance = Math.abs(currentRoof.y1 - current) const prevDistance = prev ? Math.abs(currentRoof.y1 - prev) : 0 return prevDistance > currentDistance ? prev : current }) yPoints = yPoints.filter((y) => y === maxPoint) let oppositeLine if (yPoints.length === 1) { if (ridge?.y1 === yPoints[0] || ridge?.y2 === yPoints[0]) { oppositeLine = ridge } if (oppositeLine === undefined) { oppositeLine = hips.find((hip) => hip.y2 === yPoints[0]) } points.push(oppositeLine.x2, currentRoof.y1, oppositeLine.x2, oppositeLine.y2) } else if (yPoints.length > 1) { let boolY1 = yPoints.some((y) => y === ridge?.y1) let boolY2 = yPoints.some((y) => y === ridge?.y2) if (boolY1 && boolY2) { oppositeLine = ridge } if (oppositeLine) { const sortPoints = [currentRoof.x1, currentRoof.x2, oppositeLine.x1, oppositeLine.x2].sort((a, b) => a - b) const x = (sortPoints[1] + sortPoints[2]) / 2 if ( ((currentRoof.x1 <= x && x <= currentRoof.x2) || (currentRoof.x2 <= x && x <= currentRoof.x1)) && ((oppositeLine.x1 <= x && x <= oppositeLine.x2) || (oppositeLine.x2 <= x && x <= oppositeLine.x1)) ) { points.push(x, currentRoof.y1, x, oppositeLine.y1) } } } } } } else { if (currentRoof.attributes.type === LINE_TYPE.WALLLINE.JERKINHEAD) { const gables = canvas .getObjects() .filter((object) => object.attributes?.currentRoofId === currentRoof.id && object.name === LINE_TYPE.SUBLINE.GABLE) let x1, y1, x2, y2 x1 = (currentRoof.x1 + currentRoof.x2) / 2 y1 = (currentRoof.y1 + currentRoof.y2) / 2 if (currentRoof.x1 === currentRoof.x2) { const xPoints = [] gables.forEach((gable) => { if (gable.x1 !== x1) { xPoints.push(gable.x1) } else if (gable.x2 !== x1) { xPoints.push(gable.x2) } }) x2 = xPoints.reduce((sum, current) => sum + current, 0) / xPoints.length y2 = y1 } else { const yPoints = [] gables.forEach((gable) => { if (gable.y1 !== y1) { yPoints.push(gable.y1) } else if (gable.y2 !== y1) { yPoints.push(gable.y2) } }) x2 = x1 y2 = yPoints.reduce((sum, current) => sum + current, 0) / yPoints.length } points.push(x1, y1, x2, y2) } } if (points?.length > 0) { const currentDegree = getDegreeByChon(currentRoof.attributes.pitch) const length = currentDegree !== undefined && currentDegree > 0 ? calcLineActualSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3] }, currentDegree) : calcLinePlaneSize({ x1: points[0], y1: points[1], x2: points[2], y2: points[3] }) const pitchSizeLine = new QLine(points, { parentId: roof.id, stroke: '#000000', strokeWidth: 2, strokeDashArray: [5, 5], selectable: false, fontSize: roof.fontSize, textMode: textMode, attributes: { roofId: roof.id, type: 'pitchSizeLine', planeSize: length, actualSize: length, }, }) if (length > 0) { canvas.add(pitchSizeLine) canvas.renderAll() } } }) }*/ function arePointsEqual(point1, point2) { return Math.abs(point1.x - point2.x) <= 1 && Math.abs(point1.y - point2.y) <= 1 } export const toGeoJSON = (pointsArray) => { // 객체 배열을 GeoJSON 형식의 좌표 배열로 변환 const coordinates = pointsArray.map((point) => [point.x, point.y]) // 닫힌 다각형을 만들기 위해 첫 번째 점을 마지막에 추가 coordinates.push([pointsArray[0].x, pointsArray[0].y]) return coordinates } /*export const inPolygon = (polygonPoints, rectPoints) => { const polygonCoordinates = toGeoJSON(polygonPoints) const rectCoordinates = toGeoJSON(rectPoints) const polygonFeature = turf.polygon([polygonCoordinates]) const rectFeature = turf.polygon([rectCoordinates]) // 사각형의 모든 꼭짓점이 다각형 내부에 있는지 확인 const allPointsInsidePolygon = rectCoordinates.every((coordinate) => { const point = turf.point(coordinate) return turf.booleanPointInPolygon(point, polygonFeature) }) // 다각형의 모든 점이 사각형 내부에 있지 않은지 확인 const noPolygonPointsInsideRect = polygonCoordinates.every((coordinate) => { const point = turf.point(coordinate) return !turf.booleanPointInPolygon(point, rectFeature) }) return allPointsInsidePolygon && noPolygonPointsInsideRect }*/ /** * 포인트를 기준으로 선의 길이를 구한다. 선의 길이는 10을 곱하여 사용한다. * @param points * @returns number */ export const calcLinePlaneSize = (points) => { const { x1, y1, x2, y2 } = points return Big(x1).minus(x2).abs().pow(2).plus(Big(y1).minus(y2).abs().pow(2)).sqrt().times(10).round().toNumber() } /** * 포인트와 기울기를 기준으로 선의 길이를 구한다. * @param points * @param degree * @returns number */ export const calcLineActualSize = (points, degree = 0) => { const planeSize = calcLinePlaneSize(points) const theta = Big(Math.cos(Big(degree).times(Math.PI).div(180))) return Big(planeSize).div(theta).round().toNumber() } export const createLinesFromPolygon = (points) => { const lines = [] for (let i = 0; i < points.length; i++) { const nextIndex = (i + 1) % points.length const line = new fabric.Line([points[i].x, points[i].y, points[nextIndex].x, points[nextIndex].y], { stroke: 'red', strokeWidth: 2, selectable: false, evented: false, }) lines.push(line) } return lines } /** 포인트 정렬 가장왼쪽, 가장위 부터 */ /*const getSortedPoint = (points, lines) => { const startPoint = points .filter((point) => point.x === Math.min(...points.map((point) => point.x))) .reduce((prev, curr) => { return prev.y < curr.y ? prev : curr }) const sortedPoints = [] sortedPoints.push(startPoint) let prevPoint = startPoint let prevDirection for (let i = 0; i < points.length - 1; i++) { const samePoint = [] if (i === 0) { points.forEach((point) => { if (point.x === prevPoint.x && point.y > prevPoint.y) { samePoint.push({ point, direction: 'bottom', size: Math.abs(prevPoint.y - point.y) }) } }) if (samePoint.length > 0) { samePoint.sort((a, b) => a.size - b.size) sortedPoints.push(samePoint[0].point) prevDirection = samePoint[0].direction prevPoint = samePoint[0].point } else { points.forEach((point) => { if (point.y === prevPoint.y && point.x > prevPoint.x) { samePoint.push({ point, direction: 'right', size: Math.abs(prevPoint.x - point.x) }) } }) if (samePoint.length > 0) { samePoint.sort((a, b) => a.size - b.size) sortedPoints.push(samePoint[0].point) prevDirection = samePoint[0].direction prevPoint = samePoint[0].point } } } else { points .filter((point) => !sortedPoints.includes(point)) .forEach((point) => { if ((prevDirection === 'top' || prevDirection === 'bottom') && point.y === prevPoint.y) { const direction = point.x > prevPoint.x ? 'right' : 'left' const size = Math.abs(point.x - prevPoint.x) samePoint.push({ point, direction, size }) } if ((prevDirection === 'left' || prevDirection === 'right') && point.x === prevPoint.x) { const direction = point.y > prevPoint.y ? 'bottom' : 'top' const size = Math.abs(point.y - prevPoint.y) samePoint.push({ point, direction, size }) } if (Math.round(Math.abs(point.x - prevPoint.x)) === Math.round(Math.abs(point.y - prevPoint.y))) { const isLinePoint = lines.filter( (line) => (line.x1 === prevPoint.x && line.y1 === prevPoint.y && line.x2 === point.x && line.y2 === point.y) || (line.x2 === prevPoint.x && line.y2 === prevPoint.y && line.x1 === point.x && line.y1 === point.y), ).length > 0 if (isLinePoint) { const direction = prevDirection const size = Big(point.x).minus(prevPoint.x).abs().pow(2).plus(Big(point.y).minus(prevPoint.y).abs().pow(2)).sqrt().round().toNumber() samePoint.push({ point, direction, size }) } } }) if (samePoint.length > 0) { samePoint.sort((a, b) => a.size - b.size) sortedPoints.push(samePoint[0].point) prevDirection = samePoint[0].direction prevPoint = samePoint[0].point } } } return sortedPoints }*/ /* 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)), ) .times(180) .div(Math.PI) const planeSize = calcLinePlaneSize({ x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, }) const actualSize = planeSize === oldActualSize ? 0 : calcLineActualSize( { x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, }, theta, ) 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 }