diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index ded5c063..092254ba 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -2016,54 +2016,6 @@ export { }; -/** - * Finds lines in the roof that match certain criteria based on the given points - * @param {Array} lines - The roof lines to search through - * @param {Object} startPoint - The start point of the reference line - * @param {Object} endPoint - The end point of the reference line - * @param {Array} oldPoints - The old points to compare against - * @returns {Array} Array of matching line objects with their properties - */ -function findMatchingRoofLines(lines, startPoint, endPoint, oldPoints) { - const result = []; - - // If no lines provided, return empty array - if (!lines || !lines.length) return result; - - // Process each line in the roof - for (const line of lines) { - // Get the start and end points of the current line - const p1 = { x: line.x1, y: line.y1 }; - const p2 = { x: line.x2, y: line.y2 }; - - // Check if both points exist in the oldPoints array - const p1Exists = oldPoints.some(p => - Math.abs(p.x - p1.x) < 0.0001 && Math.abs(p.y - p1.y) < 0.0001 - ); - - const p2Exists = oldPoints.some(p => - Math.abs(p.x - p2.x) < 0.0001 && Math.abs(p.y - p2.y) < 0.0001 - ); - - // If both points exist in oldPoints, add to results - if (p1Exists && p2Exists) { - // Calculate line position relative to the reference line - const position = getLinePosition( - { start: p1, end: p2 }, - { start: startPoint, end: endPoint } - ); - - result.push({ - start: p1, - end: p2, - position: position, - line: line - }); - } - } - - return result; -} /** * Finds the opposite line in a polygon based on the given line @@ -2138,55 +2090,7 @@ function findOppositeLine(edges, startPoint, endPoint, points) { }); } - // // 현재 선분의 기울기 계산 - // const currentSlope = calculateSlope(p1, p2); - // - // // 기울기가 같은지 확인 (평행한 선분) - // if (areLinesParallel(referenceSlope, currentSlope)) { - // // 동일한 선분이 아닌지 확인 - // if (!areSameLine(p1, p2, startPoint, endPoint)) { - // const position = getLinePosition( - // { start: p1, end: p2 }, - // { start: startPoint, end: endPoint } - // ); - // - // const lineMid = { - // x: (p1.x + p2.x) / 2, - // y: (p1.y + p2.y) / 2 - // }; - // - // const baseMid = { - // x: (startPoint.x + endPoint.x) / 2, - // y: (startPoint.y + endPoint.y) / 2 - // }; - // const distance = Math.sqrt( - // Math.pow(lineMid.x - baseMid.x, 2) + - // Math.pow(lineMid.y - baseMid.y, 2) - // ); - // - // const existingIndex = result.findIndex(line => line.position === position); - // - // if (existingIndex === -1) { - // // If no line with this position exists, add it - // result.push({ - // start: p1, - // end: p2, - // position: position, - // polygon: polygon, - // distance: distance - // }); - // } else if (distance > result[existingIndex].distance) { - // // If a line with this position exists but is closer, replace it - // result[existingIndex] = { - // start: p1, - // end: p2, - // position: position, - // polygon: polygon, - // distance: distance - // }; - // } - // } - // } + } } @@ -2235,23 +2139,7 @@ function calculateSlope(p1, p2) { return (p2.y - p1.y) / (p2.x - p1.x); } -// 두 직선이 평행한지 확인 -// function areLinesParallel(slope1, slope2) { -// // 두 직선 모두 수직선인 경우 -// if (slope1 === Infinity && slope2 === Infinity) return true; -// -// // 기울기의 차이가 매우 작으면 평행한 것으로 간주 -// const epsilon = 0.0001; -// return Math.abs(slope1 - slope2) < epsilon; -// } -// 두 선분이 동일한지 확인 -// function areSameLine(p1, p2, p3, p4) { -// return ( -// (isSamePoint(p1, p3) && isSamePoint(p2, p4)) || -// (isSamePoint(p1, p4) && isSamePoint(p2, p3)) -// ); -// } /** * Helper function to find the polygon containing the given line */ @@ -2378,31 +2266,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { return { p1: clippedP1, p2: clippedP2 }; } -/** - * 점이 다각형 내부에 있는지 확인합니다 (Ray Casting 알고리즘 사용). - * @param {Object} point - 확인할 점 {x, y} - * @param {Array} roofLines - 다각형을 구성하는 선분들 - * @returns {boolean} 점이 다각형 내부에 있으면 true - */ -function isPointInsidePolygon2(point, roofLines) { - let inside = false; - const x = point.x; - const y = point.y; - for (const line of roofLines) { - const x1 = line.x1; - const y1 = line.y1; - const x2 = line.x2; - const y2 = line.y2; - - // Ray casting: 점에서 오른쪽으로 수평선을 그었을 때 다각형 경계와 교차하는 횟수 확인 - if (((y1 > y) !== (y2 > y)) && (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1)) { - inside = !inside; - } - } - - return inside; -} function isPointInsidePolygon(point, roofLines) { // 1. 먼저 경계선 위에 있는지 확인 (방향 무관) @@ -2521,329 +2385,8 @@ function getLineDirection(p1, p2) { return 'top'; // (-135 ~ -45) } -/** - * 라인의 방향과 wall line에서 뻗어 들어가는지(내부로) 아니면 뻗어 나가는지(외부로)를 판단합니다. - * @param {Object} p1 - 라인의 시작점 {x, y} - * @param {Object} p2 - 라인의 끝점 {x, y} - * @param {Object} wall - wall 객체 (checkPointInPolygon 메서드 사용 가능) - * @returns {Object} {direction: string, orientation: 'inward'|'outward'|'unknown'} - */ -function getLineDirectionWithOrientation(p1, p2, wall) { - const direction = getLineDirection(p1, p2); - if (!wall) { - return { direction, orientation: 'unknown' }; - } - // 라인의 중점과 방향 벡터 계산 - const midX = (p1.x + p2.x) / 2; - const midY = (p1.y + p2.y) / 2; - const dx = p2.x - p1.x; - const dy = p2.y - p1.y; - const length = Math.sqrt(dx * dx + dy * dy); - - if (length === 0) { - return { direction, orientation: 'unknown' }; - } - - // 중점에서 라인 방향으로 약간 이동한 점이 wall 내부에 있는지 확인 - const testOffset = 10; - const testPoint = { - x: midX + (dx / length) * testOffset, - y: midY + (dy / length) * testOffset - }; - - const isInside = checkPointInPolygon(testPoint, wall); - - return { - direction, - orientation: isInside ? 'inward' : 'outward' - }; -} - -/** - * 점이 선분 위에 있는지 확인하는 헬퍼 함수 - * @param {Object} point - 확인할 점 {x, y} - * @param {Object} lineStart - 선분의 시작점 {x, y} - * @param {Object} lineEnd - 선분의 끝점 {x, y} - * @param {number} epsilon - 허용 오차 - * @returns {boolean} - */ -// function isPointOnLineSegment(point, lineStart, lineEnd, epsilon = 0.1) { -// const dx = lineEnd.x - lineStart.x; -// const dy = lineEnd.y - lineStart.y; -// const length = Math.sqrt(dx * dx + dy * dy); -// -// if (length === 0) { -// // 선분의 길이가 0이면 시작점과의 거리만 확인 -// return Math.abs(point.x - lineStart.x) < epsilon && Math.abs(point.y - lineStart.y) < epsilon; -// } -// -// // 점에서 선분의 시작점까지의 벡터 -// const toPoint = { x: point.x - lineStart.x, y: point.y - lineStart.y }; -// -// // 선분 방향으로의 투영 길이 -// const t = (toPoint.x * dx + toPoint.y * dy) / (length * length); -// -// // t가 0과 1 사이에 있어야 선분 위에 있음 -// if (t < 0 || t > 1) { -// return false; -// } -// -// // 선분 위의 가장 가까운 점 -// const closestPoint = { -// x: lineStart.x + t * dx, -// y: lineStart.y + t * dy -// }; -// -// // 점과 가장 가까운 점 사이의 거리 -// const dist = Math.sqrt( -// Math.pow(point.x - closestPoint.x, 2) + -// Math.pow(point.y - closestPoint.y, 2) -// ); -// -// return dist < epsilon; -// } - -// selectLine과 baseLines 비교하여 방향 찾기 -function findLineDirection(selectLine, baseLines) { - for (const baseLine of baseLines) { - // baseLine의 시작점과 끝점 - const baseStart = baseLine.startPoint; - const baseEnd = baseLine.endPoint; - - // selectLine의 시작점과 끝점 - const selectStart = selectLine.startPoint; - const selectEnd = selectLine.endPoint; - - // 정방향 또는 역방향으로 일치하는지 확인 - if ((isSamePoint(baseStart, selectStart) && isSamePoint(baseEnd, selectEnd)) || - (isSamePoint(baseStart, selectEnd) && isSamePoint(baseEnd, selectStart))) { - - // baseLine의 방향 계산 - const dx = baseEnd.x - baseStart.x; - const dy = baseEnd.y - baseStart.y; - - // 기울기를 바탕으로 방향 판단 - if (Math.abs(dx) > Math.abs(dy)) { - return dx > 0 ? 'right' : 'left'; - } else { - return dy > 0 ? 'down' : 'up'; - } - } - } - - return null; // 일치하는 라인이 없는 경우 -} - -/** - * outerLine의 방향에 따라 올바른 시작점과 끝점을 반환합니다. - * 예를 들어 왼쪽으로 진행하는 라인의 경우, x 좌표가 작은 쪽이 끝점, 큰 쪽이 시작점입니다. - * @param {Object} outerLine - QLine 객체 - * @returns {Object} {startPoint: {x, y}, endPoint: {x, y}, direction: string} - */ -function getOuterLinePointsWithDirection(outerLine) { - const direction = getLineDirection(outerLine.startPoint, outerLine.endPoint); - - let startPoint, endPoint; - - switch (direction) { - case 'left': - // 왼쪽으로 진행: x 좌표가 큰 쪽이 시작점, 작은 쪽이 끝점 - if (outerLine.startPoint.x > outerLine.endPoint.x) { - startPoint = outerLine.startPoint; - endPoint = outerLine.endPoint; - } else { - startPoint = outerLine.endPoint; - endPoint = outerLine.startPoint; - } - break; - case 'right': - // 오른쪽으로 진행: x 좌표가 작은 쪽이 시작점, 큰 쪽이 끝점 - if (outerLine.startPoint.x < outerLine.endPoint.x) { - startPoint = outerLine.startPoint; - endPoint = outerLine.endPoint; - } else { - startPoint = outerLine.endPoint; - endPoint = outerLine.startPoint; - } - break; - case 'top': - // 위로 진행: y 좌표가 큰 쪽이 시작점, 작은 쪽이 끝점 - if (outerLine.startPoint.y > outerLine.endPoint.y) { - startPoint = outerLine.startPoint; - endPoint = outerLine.endPoint; - } else { - startPoint = outerLine.endPoint; - endPoint = outerLine.startPoint; - } - break; - case 'bottom': - // 아래로 진행: y 좌표가 작은 쪽이 시작점, 큰 쪽이 끝점 - if (outerLine.startPoint.y < outerLine.endPoint.y) { - startPoint = outerLine.startPoint; - endPoint = outerLine.endPoint; - } else { - startPoint = outerLine.endPoint; - endPoint = outerLine.startPoint; - } - break; - default: - // 기본값: 원래대로 반환 - startPoint = outerLine.startPoint; - endPoint = outerLine.endPoint; - } - - return { startPoint, endPoint, direction }; -} - -function getLinePositionRelativeToWall(selectLine, wall) { - // wall의 경계를 가져옵니다. - const bounds = wall.getBoundingRect(); - const { left, top, width, height } = bounds; - const right = left + width; - const bottom = top + height; - - // selectLine의 중간점을 계산합니다. - const midX = (selectLine.startPoint.x + selectLine.endPoint.x) / 2; - const midY = (selectLine.startPoint.y + selectLine.endPoint.y) / 2; - - // 경계로부터의 거리를 계산합니다. - const distanceToLeft = Math.abs(midX - left); - const distanceToRight = Math.abs(midX - right); - const distanceToTop = Math.abs(midY - top); - const distanceToBottom = Math.abs(midY - bottom); - - // 가장 가까운 경계를 찾습니다. - const minDistance = Math.min( - distanceToLeft, - distanceToRight, - distanceToTop, - distanceToBottom - ); - - // 가장 가까운 경계를 반환합니다. - if (minDistance === distanceToLeft) return 'left'; - if (minDistance === distanceToRight) return 'right'; - if (minDistance === distanceToTop) return 'top'; - return 'bottom'; -} - -/** - * Convert a line into an array of coordinate points - * @param {Object} line - Line object with startPoint and endPoint - * @param {Object} line.startPoint - Start point with x, y coordinates - * @param {Object} line.endPoint - End point with x, y coordinates - * @param {number} [step=1] - Distance between points (default: 1) - * @returns {Array} Array of points [{x, y}, ...] - */ -function lineToPoints(line, step = 1) { - const { startPoint, endPoint } = line; - const points = []; - - // Add start point - points.push({ x: startPoint.x, y: startPoint.y }); - - // Calculate distance between points - const dx = endPoint.x - startPoint.x; - const dy = endPoint.y - startPoint.y; - const distance = Math.sqrt(dx * dx + dy * dy); - const steps = Math.ceil(distance / step); - - // Add intermediate points - for (let i = 1; i < steps; i++) { - const t = i / steps; - points.push({ - x: startPoint.x + dx * t, - y: startPoint.y + dy * t - }); - } - - // Add end point - points.push({ x: endPoint.x, y: endPoint.y }); - - return points; -} - -/** - * 다각형의 모든 좌표를 offset만큼 안쪽/바깥쪽으로 이동 - * @param {Array} points - 다각형 좌표 배열 [{x, y}, ...] - * @param {number} offset - offset 값 (양수: 안쪽, 음수: 바깥쪽) - * @returns {Array} offset이 적용된 새로운 좌표 배열 - */ -function offsetPolygon(points, offset) { - if (points.length < 3) return points; - - const offsetPoints = []; - const numPoints = points.length; - - for (let i = 0; i < numPoints; i++) { - const prevIndex = (i - 1 + numPoints) % numPoints; - const currentIndex = i; - const nextIndex = (i + 1) % numPoints; - - const prevPoint = points[prevIndex]; - const currentPoint = points[currentIndex]; - const nextPoint = points[nextIndex]; - - // 이전 변의 방향 벡터 - const prevVector = { - x: currentPoint.x - prevPoint.x, - y: currentPoint.y - prevPoint.y - }; - - // 다음 변의 방향 벡터 - const nextVector = { - x: nextPoint.x - currentPoint.x, - y: nextPoint.y - currentPoint.y - }; - - // 정규화 - const prevLength = Math.sqrt(prevVector.x * prevVector.x + prevVector.y * prevVector.y); - const nextLength = Math.sqrt(nextVector.x * nextVector.x + nextVector.y * nextVector.y); - - if (prevLength === 0 || nextLength === 0) continue; - - const prevNormal = { - x: -prevVector.y / prevLength, - y: prevVector.x / prevLength - }; - - const nextNormal = { - x: -nextVector.y / nextLength, - y: nextVector.x / nextLength - }; - - // 평균 법선 벡터 계산 - const avgNormal = { - x: (prevNormal.x + nextNormal.x) / 2, - y: (prevNormal.y + nextNormal.y) / 2 - }; - - // 평균 법선 벡터 정규화 - const avgLength = Math.sqrt(avgNormal.x * avgNormal.x + avgNormal.y * avgNormal.y); - if (avgLength === 0) continue; - - const normalizedAvg = { - x: avgNormal.x / avgLength, - y: avgNormal.y / avgLength - }; - - // 각도 보정 (예각일 때 offset 조정) - const cosAngle = prevNormal.x * nextNormal.x + prevNormal.y * nextNormal.y; - const adjustedOffset = Math.abs(cosAngle) > 0.1 ? offset / Math.abs(cosAngle) : offset; - - // 새로운 점 계산 - const offsetPoint = { - x: currentPoint.x + normalizedAvg.x * adjustedOffset, - y: currentPoint.y + normalizedAvg.y * adjustedOffset - }; - - offsetPoints.push(offsetPoint); - } - - return offsetPoints; -} /** * baseLines를 연결하여 다각형 순서로 정렬된 점들 반환 @@ -3248,494 +2791,9 @@ function extendLineToBoundary(p1, p2, roofLines) { return { p1: { ...p1 }, p2: { ...p2 } }; } -/** - * 점에서 특정 방향으로 경계선과의 교차점을 찾습니다. - * @param {Object} point - 시작점 {x, y} - * @param {Object} direction - 방향 벡터 {x, y} (정규화된 값) - * @param {Array} roofLines - 지붕 경계선 배열 - * @returns {Object|null} 교차점 {x, y} 또는 null - */ -function findBoundaryIntersection(point, direction, roofLines) { - let closestIntersection = null; - let minDistance = Infinity; - // 충분히 긴 거리로 광선 생성 (임의로 큰 값 사용) - const rayLength = 10000; - const rayEnd = { - x: point.x + direction.x * rayLength, - y: point.y + direction.y * rayLength - }; - // 모든 경계선과의 교차점 확인 - for (const line of roofLines) { - const lineP1 = { x: line.x1, y: line.y1 }; - const lineP2 = { x: line.x2, y: line.y2 }; - const intersection = getLineIntersection(point, rayEnd, lineP1, lineP2); - - if (intersection) { - // 교차점까지의 거리 계산 - const distance = Math.sqrt( - Math.pow(intersection.x - point.x, 2) + - Math.pow(intersection.y - point.y, 2) - ); - - // 가장 가까운 교차점 저장 (거리가 0보다 큰 경우만) - if (distance > 0.01 && distance < minDistance) { - minDistance = distance; - closestIntersection = intersection; - } - } - } - - return closestIntersection; -} - -/** - * 점이 다른 스켈레톤 라인과의 교점인지 확인합니다. - * @param {Object} point - 확인할 점 {x, y} - * @param {Array} skeletonLines - 모든 스켈레톤 라인 배열 - * @param {Object} currentLine - 현재 라인 {p1, p2} (자기 자신 제외용) - * @param {number} tolerance - 허용 오차 - * @returns {boolean} 교점이면 true - */ -function hasIntersectionWithOtherLines(point, skeletonLines, currentLine, tolerance = 0.5) { - if (!skeletonLines || skeletonLines.length === 0) { - return false; - } - - let connectionCount = 0; - - for (const line of skeletonLines) { - // 자기 자신과의 비교는 제외 - if (line.p1 && line.p2 && currentLine.p1 && currentLine.p2) { - const isSameLineCheck = - (isSamePoint(line.p1, currentLine.p1, tolerance) && isSamePoint(line.p2, currentLine.p2, tolerance)) || - (isSamePoint(line.p1, currentLine.p2, tolerance) && isSamePoint(line.p2, currentLine.p1, tolerance)); - - if (isSameLineCheck) continue; - } - - // 다른 라인의 끝점이 현재 점과 일치하는지 확인 - if (line.p1 && isSamePoint(point, line.p1, tolerance)) { - connectionCount++; - } - if (line.p2 && isSamePoint(point, line.p2, tolerance)) { - connectionCount++; - } - } - - // 1개 이상의 다른 라인과 연결되어 있으면 교점으로 간주 - return connectionCount >= 1; -} - -function findClosestRoofLine(point, roofLines) { - let closestLine = null; - let minDistance = Infinity; - let roofLineIndex = 0; - let interPoint = null; - - roofLines.forEach((roofLine, index) => { - const lineP1 = roofLine.startPoint; - const lineP2 = roofLine.endPoint; - - // 점에서 선분까지의 최단 거리 계산 - const distance = pointToLineDistance(point, lineP1, lineP2); - - // 점에서 수직으로 내린 교점 계산 - const intersection = getProjectionPoint(point, { - x1: lineP1.x, - y1: lineP1.y, - x2: lineP2.x, - y2: lineP2.y - }); - - if (distance < minDistance) { - minDistance = distance < 0.1 ? 0 : distance; //거리가 0.1보다 작으면 0으로 처리 - closestLine = roofLine; - roofLineIndex = index - interPoint = intersection; - } - }); - - return { line: closestLine, distance: minDistance, index: roofLineIndex, intersectionPoint: interPoint }; -} - -// 점에서 선분까지의 최단 거리를 계산하는 도우미 함수 -function pointToLineDistance(point, lineP1, lineP2) { - const A = point.x - lineP1.x; - const B = point.y - lineP1.y; - const C = lineP2.x - lineP1.x; - const D = lineP2.y - lineP1.y; - - const dot = A * C + B * D; - const lenSq = C * C + D * D; - let param = -1; - - if (lenSq !== 0) { - param = dot / lenSq; - } - - let xx, yy; - - if (param < 0) { - xx = lineP1.x; - yy = lineP1.y; - } else if (param > 1) { - xx = lineP2.x; - yy = lineP2.y; - } else { - xx = lineP1.x + param * C; - yy = lineP1.y + param * D; - } - - const dx = point.x - xx; - const dy = point.y - yy; - return Math.sqrt(dx * dx + dy * dy); -} - -/** - * Moves both p1 and p2 in the specified direction by a given distance - * @param {Object} p1 - The first point {x, y} - * @param {Object} p2 - The second point {x, y} - * @param {string} direction - Direction to move ('up', 'down', 'left', 'right') - * @param {number} distance - Distance to move - * @returns {Object} Object containing the new positions of p1 and p2 - */ -function moveLineInDirection(p1, p2, direction, distance) { - // Create copies to avoid mutating the original points - const newP1 = { ...p1 }; - const newP2 = { ...p2 }; - - const move = (point) => { - switch (direction.toLowerCase()) { - case 'up': - point.y -= distance; - break; - case 'down': - point.y += distance; - break; - case 'left': - point.x -= distance; - break; - case 'right': - point.x += distance; - break; - default: - throw new Error('Invalid direction. Use "up", "down", "left", or "right"'); - } - return point; - }; - - return { - p1: move(newP1), - p2: move(newP2) - }; -} - -/** - * Determines the direction and distance between original points (p1, p2) and moved points (newP1, newP2) - * @param {Object} p1 - Original first point {x, y} - * @param {Object} p2 - Original second point {x, y} - * @param {Object} newP1 - Moved first point {x, y} - * @param {Object} newP2 - Moved second point {x, y} - * @returns {Object} Object containing direction and distance of movement - */ -function getMovementInfo(p1, p2, newP1, newP2) { - // Calculate the movement vector for both points - const dx1 = newP1.x - p1.x; - const dy1 = newP1.y - p1.y; - const dx2 = newP2.x - p2.x; - const dy2 = newP2.y - p2.y; - - // Verify that both points moved by the same amount - if (dx1 !== dx2 || dy1 !== dy2) { - throw new Error('Points did not move in parallel'); - } - - // Determine the primary direction of movement - let direction; - const absDx = Math.abs(dx1); - const absDy = Math.abs(dy1); - - if (absDx > absDy) { - // Horizontal movement is dominant - direction = dx1 > 0 ? 'right' : 'left'; - } else { - // Vertical movement is dominant - direction = dy1 > 0 ? 'down' : 'up'; - } - - // Calculate the actual distance moved - const distance = Math.sqrt(dx1 * dx1 + dy1 * dy1); - - return { - direction, - distance, - dx: dx1, - dy: dy1 - }; -} - -function getLineAngleDirection(line, isLeftSide = true) { - const dx = line.x2 - line.x1; - const dy = line.y2 - line.y1; - - // 수평선인 경우 (y 좌표가 거의 같은 경우) - if (Math.abs(dy) < 0.1) { - // x 좌표 비교로 좌우 방향 결정 - return line.x2 > line.x1 ? 'right' : 'left'; - } - - // 수직선인 경우 (x 좌표가 거의 같은 경우) - if (Math.abs(dx) < 0.1) { - // y 좌표 비교로 상하 방향 결정 - return line.y2 > line.y1 ? 'down' : 'up'; - } - - // 대각선의 경우 기존 로직 유지 - const angle = Math.atan2(dy, dx) * (180 / Math.PI); - const normalizedAngle = (angle + 360) % 360; - - if (normalizedAngle >= 45 && normalizedAngle < 135) { - return 'up'; - } else if (normalizedAngle >= 135 && normalizedAngle < 225) { - return 'left'; - } else if (normalizedAngle >= 225 && normalizedAngle < 315) { - return 'down'; - } else { - return 'right'; - } -} - -function findRoofLineIndex(roof, p1, p2) { - if (!roof || !roof.lines || !Array.isArray(roof.lines)) { - console.error("Invalid roof object or lines array"); - return -1; - } - - // Create a tolerance for floating point comparison - const TOLERANCE = 0.1; - - // Try to find a line that matches either (p1,p2) or (p2,p1) - const index = roof.lines.findIndex(line => { - // Check if points match in order - const matchOrder = - (Math.abs(line.x1 - p1.x) < TOLERANCE && - Math.abs(line.y1 - p1.y) < TOLERANCE && - Math.abs(line.x2 - p2.x) < TOLERANCE && - Math.abs(line.y2 - p2.y) < TOLERANCE); - - // Check if points match in reverse order - const matchReverse = - (Math.abs(line.x1 - p2.x) < TOLERANCE && - Math.abs(line.y1 - p2.y) < TOLERANCE && - Math.abs(line.x2 - p1.x) < TOLERANCE && - Math.abs(line.y2 - p1.y) < TOLERANCE); - - return matchOrder || matchReverse; - }); - - if (index === -1) { - console.warn("No matching roof line found for points:", p1, p2); - } - - return index; -} - -function findClosestParallelLine(roofLine, currentRoofLines) { - // Determine if the line is horizontal or vertical - const isHorizontal = Math.abs(roofLine.y2 - roofLine.y1) < 0.001; // Using a small threshold for floating point comparison - const isVertical = Math.abs(roofLine.x2 - roofLine.x1) < 0.001; - - if (!isHorizontal && !isVertical) { - console.warn('Line is neither perfectly horizontal nor vertical'); - return null; - } - - // Calculate the reference point (midpoint of the line) - const refX = (roofLine.x1 + roofLine.x2) / 2; - const refY = (roofLine.y1 + roofLine.y2) / 2; - - let closestLine = null; - let minDistance = Infinity; - - currentRoofLines.forEach(line => { - // Skip the same line - if (line === roofLine) return; - - // Check if the line is parallel (same orientation) - const lineIsHorizontal = Math.abs(line.y2 - line.y1) < 0.001; - const lineIsVertical = Math.abs(line.x2 - line.x1) < 0.001; - - if ((isHorizontal && lineIsHorizontal) || (isVertical && lineIsVertical)) { - // Calculate midpoint of the current line - const lineMidX = (line.x1 + line.x2) / 2; - const lineMidY = (line.y1 + line.y2) / 2; - - // Calculate distance between midpoints - const distance = Math.sqrt( - Math.pow(lineMidX - refX, 2) + - Math.pow(lineMidY - refY, 2) - ); - - // Update closest line if this one is closer - if (distance < minDistance) { - minDistance = distance; - closestLine = line; - } - } - }); - - return closestLine; -} - -function doLinesIntersect(line1, line2) { - const x1 = line1.x1, y1 = line1.y1; - const x2 = line1.x2, y2 = line1.y2; - const x3 = line2.x1, y3 = line2.y1; - const x4 = line2.x2, y4 = line2.y2; - - // Calculate the direction of the lines - const uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); - const uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); - - // If uA and uB are between 0-1, lines are colliding - return (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1); -} - -const getOrientation = (line, eps = 0.1) => { - const x1 = line.get('x1') - const y1 = line.get('y1') - const x2 = line.get('x2') - const y2 = line.get('y2') - const dx = Math.abs(x2 - x1) - const dy = Math.abs(y2 - y1) - - if (dx < eps && dy >= eps) return 'vertical' - if (dy < eps && dx >= eps) return 'horizontal' - if (dx < eps && dy < eps) return 'point' - return 'diagonal' -} - -/** - * 두 선분이 교차하는지 확인하는 헬퍼 함수 - * (끝점이 닿아있는 경우도 교차로 간주) - */ -function checkIntersection(p1, p2, p3, p4) { - // CCW (Counter Clockwise) 알고리즘을 이용한 교차 판별 - function ccw(a, b, c) { - const val = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); - if (val < 0) return -1; - if (val > 0) return 1; - return 0; - } - - const abc = ccw(p1, p2, p3); - const abd = ccw(p1, p2, p4); - const cda = ccw(p3, p4, p1); - const cdb = ccw(p3, p4, p2); - - // 두 선분이 일직선 상에 있을 때 (겹치는지 확인) - if (abc === 0 && abd === 0) { - // x축, y축 순서대로 정렬하여 겹침 여부 확인 - if (p1.x > p2.x || (p1.x === p2.x && p1.y > p2.y)) [p1, p2] = [p2, p1]; - if (p3.x > p4.x || (p3.x === p4.x && p3.y > p4.y)) [p3, p4] = [p4, p3]; - return p2.x >= p3.x && p2.y >= p3.y && p4.x >= p1.x && p4.y >= p1.y; - } - - return abc * abd <= 0 && cda * cdb <= 0; -} - -/** -* aLine의 좌표를 추출하는 함수 -*/ -function getACoords(line) { - return { - start: { x: line.newPStart.x, y: line.newPStart.y }, - end: { x: line.newPEnd.x, y: line.newPEnd.y } - }; -} - -/** -* bLine의 좌표를 추출하는 함수 -* (left, top을 시작점으로 보고 width, height를 더해 끝점을 계산) -*/ -function getBCoords(line) { - // QLine 데이터 구조상 left/top이 시작점, width/height가 델타값으로 가정 - return { - start: { x: line.left, y: line.top }, - end: { x: line.left + line.width, y: line.top + line.height } - }; -} - -/** -* 메인 로직 함수 -* 1. aLines 순회 -* 2. aLine과 교차하는 bLines 찾기 (Level 1) -* 3. 찾은 bLine과 교차하는 또 다른 bLines 찾기 (Level 2) -*/ -function findConnectedLines(aLines, bLines, canvas, roofId, roof) { - const results = []; - - aLines.forEach(aLine => { - const aCoords = getACoords(aLine); - const intersections = []; - - // 1단계: aLine과 교차하는 bLines 찾기 - bLines.forEach(bLine1 => { - const bCoords1 = getBCoords(bLine1); - - if (checkIntersection(aCoords.start, aCoords.end, bCoords1.start, bCoords1.end)) { - - // 2단계: 위에서 찾은 bLine1과 교차하는 다른 bLines 찾기 - const connectedToB1 = []; - bLines.forEach(bLine2 => { - // 자기 자신은 제외 - if (bLine1 === bLine2) return; - - const bCoords2 = getBCoords(bLine2); - if (checkIntersection(bCoords1.start, bCoords1.end, bCoords2.start, bCoords2.end)) { - connectedToB1.push(bLine2); - - let testLine = new QLine([bLine2.x1, bLine2.y1, bLine2.x2, bLine2.y2], { - stroke: 'orange', - strokeWidth: 10, - property: 'normal', - fontSize: 14, - lineName: 'helpLine', - roofId:roofId, - parentId: roof.id, - - }); - canvas.add(testLine) - } - - - }); - - intersections.push({ - targetBLine: bLine1, // aLine과 만난 녀석 - connectedBLines: connectedToB1 // 그 녀석과 만난 다른 bLines - }); - - - } - }); - - // 결과가 있는 경우에만 저장 (필요에 따라 조건 제거 가능) - if (intersections.length > 0) { - results.push({ - sourceALine: aLine, - intersections: intersections - }); - - - } - }); - - return results; -} export const processEaveHelpLines = (lines) => { if (!lines || lines.length === 0) return []; @@ -3799,155 +2857,7 @@ const mergeLines = (lines, direction) => { return merged; }; -function mergeMovedLines(movedLines) { - if (!movedLines || movedLines.length < 2) return movedLines; - const result = [...movedLines]; // Start with all original lines - const processed = new Set(); - - // First pass: find and merge connected lines - for (let i = 0; i < result.length; i++) { - if (processed.has(i)) continue; - - for (let j = i + 1; j < result.length; j++) { - if (processed.has(j)) continue; - - const line1 = result[i]; - const line2 = result[j]; - - // Skip if lines are not the same type (vertical/horizontal) - const line1Type = getLineType(line1); - const line2Type = getLineType(line2); - if (line1Type !== line2Type) continue; - - if (areLinesConnected(line1, line2, line1Type)) { - // Merge the lines - const merged = mergeTwoLines(line1, line2, line1Type); - - // Replace the first line with merged result - result[i] = merged; - // Mark the second line for removal - processed.add(j); - } - } - } - - // Remove processed lines and keep the order - return result.filter((_, index) => !processed.has(index)); -} - -function getLineType(line) { - if (Math.abs(line.p1.x - line.p2.x) < 0.1) return 'vertical'; - if (Math.abs(line.p1.y - line.p2.y) < 0.1) return 'horizontal'; - return 'other'; -} - -function areLinesConnected(line1, line2, type) { - if (type === 'vertical') { - // For vertical lines, check if x coordinates are the same and y ranges overlap - return Math.abs(line1.p1.x - line2.p1.x) < 0.1 && - Math.min(line1.p2.y, line2.p2.y) >= Math.max(line1.p1.y, line2.p1.y) - 0.1; - } else if (type === 'horizontal') { - // For horizontal lines, check if y coordinates are the same and x ranges overlap - return Math.abs(line1.p1.y - line2.p1.y) < 0.1 && - Math.min(line1.p2.x, line2.p2.x) >= Math.max(line1.p1.x, line2.p1.x) - 0.1; - } - return false; -} - -function mergeTwoLines(line1, line2, type) { - if (type === 'vertical') { - return { - ...line1, // Preserve original properties - p1: { - x: line1.p1.x, - y: Math.min(line1.p1.y, line1.p2.y, line2.p1.y, line2.p2.y) - }, - p2: { - x: line1.p1.x, - y: Math.max(line1.p1.y, line1.p2.y, line2.p1.y, line2.p2.y) - } - }; - } else { // horizontal - return { - ...line1, // Preserve original properties - p1: { - x: Math.min(line1.p1.x, line1.p2.x, line2.p1.x, line2.p2.x), - y: line1.p1.y - }, - p2: { - x: Math.max(line1.p1.x, line1.p2.x, line2.p1.x, line2.p2.x), - y: line1.p1.y - } - }; - } -} - - -/** - * Adjusts line points based on movement type and orientation - * @param {Object} params - Configuration object - * @param {Object} params.roofLine - The original roof line - * @param {Object} params.currentRoofLine - The current roof line after movement - * @param {Object} params.wallBaseLine - The wall base line - * @param {Object} params.origin - The original position before movement - * @param {string} params.moveType - Type of movement: 'start' | 'end' | 'both' - * @returns {{newPStart: {x: number, y: number}, newPEnd: {x: number, y: number}}} - */ -function adjustLinePoints({ roofLine, currentRoofLine, wallBaseLine, origin, moveType }) { - const isHorizontal = getOrientation(roofLine) === 'horizontal'; - const isVertical = !isHorizontal; - - // Initialize points - let newPStart = { x: roofLine.x1, y: roofLine.y1 }; - let newPEnd = { x: roofLine.x2, y: roofLine.y2 }; - - // Check if lines cross (same as original isCross logic) - let isCross = false; - if (isVertical) { - isCross = Math.abs(currentRoofLine.x2 - roofLine.x1) < 0.1 || - Math.abs(currentRoofLine.x1 - roofLine.x2) < 0.1; - } else { - isCross = Math.abs(currentRoofLine.y1 - roofLine.y2) < 0.1 || - Math.abs(currentRoofLine.y2 - roofLine.y1) < 0.1; - } - - // Determine which points to adjust - const adjustStart = moveType === 'start' || moveType === 'both'; - const adjustEnd = moveType === 'end' || moveType === 'both'; - - if (isVertical) { - // Vertical line adjustments - if (adjustStart) { - newPStart = { - x: roofLine.x1, - y: isCross ? currentRoofLine.y1 : wallBaseLine.y1 - }; - } - if (adjustEnd) { - newPEnd = { - x: roofLine.x2, - y: isCross ? currentRoofLine.y2 : wallBaseLine.y2 - }; - } - } else { - // Horizontal line adjustments - if (adjustStart) { - newPStart = { - y: roofLine.y1, - x: isCross ? currentRoofLine.x1 : wallBaseLine.x1 - }; - } - if (adjustEnd) { - newPEnd = { - y: roofLine.y2, - x: isCross ? currentRoofLine.x2 : wallBaseLine.x2 - }; - } - } - - return { newPStart, newPEnd }; -} /** * 주어진 점을 포함하는 라인을 찾는 함수 @@ -4064,276 +2974,10 @@ function updateAndAddLine(innerLines, targetPoint) { } -/** - * A, B, C: 각각 QLine 1개 (x1,y1,x2,y2 존재) - * - * 의미: - * - 'in' : C와 B의 y가 같고, C의 x범위가 B 안에 완전히 포함 - * - 'center' : C의 y가 A와 B 사이(위/아래 원하는 대로 조정 가능)이고, - * C의 x범위가 B와 겹침 - * - 'out' : 그 외 - */ -function classifyLineABC(A, B, C) { - if (!A || !B || !C) return undefined; - - function toHSegment(line) { - let { x1, y1, x2, y2 } = line; - if (x1 > x2) { - const tx = x1; x1 = x2; x2 = tx; - } - return { x1, x2, y: y1 }; - } - - const a = toHSegment(A); - const b = toHSegment(B); - const c = toHSegment(C); - - // x 겹침 / 포함 - function isXOverlap(s1, s2) { - return !(s1.x2 < s2.x1 || s2.x2 < s1.x1); - } - function isXInside(inner, outer) { - return inner.x1 >= outer.x1 && inner.x2 <= outer.x2; - } - - // 1) C가 B와 같은 y 이고, x 범위가 B 안에 완전히 포함되면 -> in - const sameYasB = Math.abs(c.y - b.y) < 1e-6; - if (sameYasB && isXInside(c, b)) { - return 'in'; - } - - // 2) C가 A와 B 사이 y 이고, x 가 B 와 겹치면 -> center - const minY = Math.min(a.y, b.y); - const maxY = Math.max(a.y, b.y); - const betweenY = c.y > minY && c.y < maxY; - - if (betweenY && isXOverlap(c, b)) { - return 'center'; - } - - // 3) 나머지 -> out - return 'out'; -} - -function PointBasedOnBaseLength(x1, y1, x2, y2) { - // 밑변 중점 좌표 계산 - const midX = (x1 + x2) / 2; - const midY = (y1 + y2) / 2; - - // 밑변 길이 계산 - const baseLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); - - // 밑변 벡터 - const vecX = x2 - x1; - const vecY = y2 - y1; - - // 벡터 길이 (밑변 길이) - const length = baseLength; - - // 수직 방향 단위 벡터 계산 (벡터를 90도 회전) - const perpX = -vecY / length; - const perpY = vecX / length; - - // 높이를 밑변 길이로 가정하여 나머지 꼭짓점 좌표 계산 (중점에서 수직 방향으로 이동) - const thirdX1 = midX + perpX * baseLength; - const thirdY1 = midY + perpY * baseLength; - - const thirdX2 = midX - perpX * baseLength; - const thirdY2 = midY - perpY * baseLength; - - // 두 가지 가능한 좌표 반환 (높이 방향 선택 가능하도록) - return [ - { x: thirdX1, y: thirdY1 }, - { x: thirdX2, y: thirdY2 } - ]; -} - -/** - * 선분의 시작점과 끝점 중 어느 쪽이 내부를 향하는지 반환 - * @param {Object} line - {x1, y1, x2, y2} 형태의 선분 - * @param {Array} polygonLines - 폴리곤을 구성하는 선분들의 배열 - * @returns {Object} - { start: boolean, end: boolean } 시작점과 끝점의 내부 여부 - */ -// function findInteriorPoint(line, polygonLines) { -// const { x1, y1, x2, y2 } = line; -// -// // 선분의 방향 벡터 -// const dx = x2 - x1; -// const dy = y2 - y1; -// -// // 수직 벡터 (왼쪽으로 90도 회전) -// const perpX = -dy; -// const perpY = dx; -// -// // 정규화 -// const length = Math.sqrt(perpX * perpX + perpY * perpY); -// const nx = perpX / length; -// const ny = perpY / length; -// -// // 오프셋 계산 (선분 길이의 1% 또는 최소 0.1) -// const lineLength = Math.sqrt(dx*dx + dy*dy); -// const offset = Math.max(0.1, lineLength * 0.01); -// -// // 시작점에서 수직 방향으로 약간 떨어진 점 -// const testPoint = { -// x: x1 + nx * offset, -// y: y1 + ny * offset -// }; -// -// // 반대 방향 점 추가 확인 (라인이 중앙에 있는 경우를 위해) -// const testPoint2 = { -// x: x1 - nx * offset, -// y: y1 - ny * offset -// }; -// -// // 이 점이 폴리곤 내부에 있는지 확인 -// const isInside = isPointInPolygon(testPoint, polygonLines); -// const isInside2 = isPointInPolygon(testPoint2, polygonLines); -// -// // 라인이 폴리곤 내부에 완전히 포함된 경우 (중앙) -> 양쪽 다 true -// if (isInside && isInside2) { -// return { -// start: true, -// end: true -// }; -// } -// -// // 시작점이 내부를 향하는지 여부 -// return { -// start: isInside, -// // 끝점 방향은 시작점과 반대 -// end: !isInside -// }; -// } - -/** - * 레이저(Ray)가 폴리곤의 다른 선분과 교차하는지 확인하는 헬퍼 함수 - * @param {Object} origin - 레이저 시작점 {x, y} - * @param {Object} dir - 레이저 방향 벡터 {x, y} (정규화됨) - * @param {Array} lines - 검사할 선분들 - * @param {Object} excludeLine - 자기 자신 라인 (제외) - */ -function checkRayIntersection(origin, dir, lines, excludeLine) { - // 레이저를 아주 멀리까지 쏘아봅니다 (도면 크기에 따라 조절, 충분히 큰 값) - const rayLength = 100000; - const rayEnd = { - x: origin.x + dir.x * rayLength, - y: origin.y + dir.y * rayLength - }; - - for (const line of lines) { - // 자기 자신은 검사 제외 - if (line === excludeLine) continue; - // 좌표 기반 비교로도 자기 자신 체크 (객체 참조가 다를 수 있으므로) - if (Math.abs(line.x1 - excludeLine.x1) < 0.1 && Math.abs(line.y1 - excludeLine.y1) < 0.1 && - Math.abs(line.x2 - excludeLine.x2) < 0.1 && Math.abs(line.y2 - excludeLine.y2) < 0.1) continue; - - const lineStart = { x: line.x1, y: line.y1 }; - const lineEnd = { x: line.x2, y: line.y2 }; - - // 선분 교차 검사 - const intersect = getLineIntersection(origin, rayEnd, lineStart, lineEnd); - - if (intersect) { - // 교차점이 시작점(origin)과 너무 가까우면(코너 등) 무시하고, - // 확실히 전방에 있는 경우만 인정 - const dist = Math.hypot(intersect.x - origin.x, intersect.y - origin.y); - if (dist > 1.0) { // 1.0 정도의 여유를 둠 - return true; // 부딪힘! (빨간 라인 후보) - } - } - } - return false; // 허공! (파란 라인 후보) -} -// 점이 선분의 어느 쪽에 있는지 확인 -function isPointInDirection(line, point) { - const {x1, y1, x2, y2} = line; - const {x, y} = point; - return ((x2 - x1) * (y - y1) - (y2 - y1) * (x - x1)) > 0; -} -/** - * 점이 폴리곤 내부에 있는지 확인 (Ray Casting 알고리즘) - * @param {Object} point - {x, y} 형태의 점 - * @param {Array} polygonLines - 폴리곤을 구성하는 선분들의 배열 - * @returns {boolean} - 점이 폴리곤 내부에 있으면 true, 아니면 false - */ -function isPointInPolygon(point, polygonLines) { - if (!polygonLines || polygonLines.length === 0) { - console.log("경고: 폴리곤 선분이 비어있습니다."); - return false; - } - const x = point.x; - const y = point.y; - let inside = false; - let intersections = 0; - - // 폴리곤의 모든 선분에 대해 반복 - for (let i = 0; i < polygonLines.length; i++) { - const line = polygonLines[i]; - const x1 = line.x1; - const y1 = line.y1; - const x2 = line.x2; - const y2 = line.y2; - - // 점이 선분 위에 있는지 확인 - if (isPointOnLineSegment2(point, {x: x1, y: y1}, {x: x2, y: y2}, 0.1)) { - console.log(`점 (${x}, ${y})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); - return true; // 경계선 위의 점은 내부로 간주 - } - - // Ray casting 알고리즘 - // 점의 y 좌표가 선분의 y 범위 내에 있는지 확인 - const yInRange = (y1 > y) !== (y2 > y); - - if (yInRange) { - // x 교차점 계산 - const xIntersect = (y - y1) * (x2 - x1) / (y2 - y1) + x1; - - // 교차점이 점의 오른쪽에 있는지 확인 - if (x <= xIntersect) { - inside = !inside; - intersections++; - } - } - } - - console.log(`폴리곤 검사 결과: 점 (${x}, ${y})은 ${inside ? '내부' : '외부'}에 있습니다. (교차점 수: ${intersections})`); - return inside; -} - -/** - * 점이 선분 위에 있는지 확인 - * @param {Object} point - 확인할 점 {x, y} - * @param {Object} lineStart - 선분의 시작점 {x, y} - * @param {Object} lineEnd - 선분의 끝점 {x, y} - * @param {number} tolerance - 오차 허용 범위 - * @returns {boolean} - 점이 선분 위에 있으면 true, 아니면 false - */ -function isPointOnLineSegment2(point, lineStart, lineEnd, tolerance = 0.1) { - const { x: px, y: py } = point; - const { x: x1, y: y1 } = lineStart; - const { x: x2, y: y2 } = lineEnd; - - // 선분의 길이 - const lineLength = Math.hypot(x2 - x1, y2 - y1); - - // 점에서 선분의 양 끝점까지의 거리 - const dist1 = Math.hypot(px - x1, py - y1); - const dist2 = Math.hypot(px - x2, py - y2); - - // 점이 선분 위에 있는지 확인 (오차 허용 범위 내에서) - const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance; - - if (isOnSegment) { - console.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); - } - - return isOnSegment; -} /** * 세 점(p1 -> p2 -> p3)의 방향성을 계산합니다. (2D 외적)