Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into dev_ysCha

# Conflicts:
#	src/util/skeleton-utils.js
This commit is contained in:
ysCha 2025-11-07 08:14:28 +09:00
commit 97725c1a3d

View File

@ -376,8 +376,6 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
roof.innerLines = roof.innerLines || [];
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
// 캔버스에 스켈레톤 상태 저장
if (!canvas.skeletonStates) {
canvas.skeletonStates = {}
@ -387,7 +385,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
canvas.skeletonLines = [];
canvas.skeletonLines.push(...roof.innerLines)
roof.skeletonLines = canvas.skeletonLines;
canvas.renderAll()
const cleanSkeleton = {
Edges: skeleton.Edges.map(edge => ({
X1: edge.Edge.Begin.X,
@ -436,10 +434,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
skeleton.Edges.forEach((edgeResult, index) => {
canvas.skeletonLines = [];
processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines);
});
// 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다.
@ -504,8 +500,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
const innerLines = [];
const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set
console.log("length::::::::", skeletonLines.length);
skeletonLines.forEach((line, index) => {
skeletonLines.forEach(line => {
const { p1, p2, attributes, lineStyle } = line;
// 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교)
@ -515,11 +511,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
].sort().join('|');
// 이미 추가된 라인인지 확인
// if (existingLines.has(lineKey)) {
// return; // 이미 있는 라인이면 스킵
// }
if (existingLines.has(lineKey)) {
return; // 이미 있는 라인이면 스킵
}
const direction = getLineDirection(
{ x: line.p1.x, y: line.p1.y },
@ -540,22 +534,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
roofId: roofId,
});
console.log('Processing line:', {
p1: {x: p1.x, y: p1.y},
p2: {x: p2.x, y: p2.y},
attributes,
lineName: (line.attributes.isOuterEdge) ? 'outerLine' : attributes.type
});
console.log('innerLines', innerLine.lineName);
console.log("index::::",index)
//skeleton 라인에서 처마선은 삭제
if(innerLine.lineName !== 'outerLine'){
canvas.add(innerLine);
innerLine.bringToFront();
//existingLines.add(lineKey); // 추가된 라인을 추적
existingLines.add(lineKey); // 추가된 라인을 추적
}else{
@ -692,7 +675,6 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
fontSize: 13,
fill: 'red',
fontFamily: 'Arial',
textBaseline: 'alphabetic', // 올바른 값으로 설정
selectable: true,
lockMovementX: false,
lockMovementY: false,
@ -701,7 +683,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
lockScalingY: true,
name: 'lengthText'
})
//좌표점(임시)
canvas?.add(coordinateText)
}
innerLines.push(innerLine)
@ -729,10 +711,10 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
);
// if(!outerLine) {
// outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points);
// console.log('Has matching line:', outerLine);
// }
if(!outerLine) {
outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points);
console.log('Has matching line:', outerLine);
}
let pitch = outerLine?.attributes?.pitch??0
@ -756,10 +738,6 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
}
let eavesLines = []
// 이 다각형이 roof.lines와 일치하는 변을 가지고 있는지 확인
const isolatedPolygons = [];
for (let i = 0; i < polygonPoints.length; i++) {
const p1 = polygonPoints[i];
const p2 = polygonPoints[(i + 1) % polygonPoints.length];
@ -767,28 +745,11 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
// 지붕 경계선과 교차 확인 및 클리핑
const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine);
console.log('clipped line', clippedLine.p1, clippedLine.p2);
const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge])
//현이동에 의해 스켈레톤 라인이 내부에서 끝난경우 roof.lines까지 수평은 수직, 수직은 수평되게 설정
// 다각형이 roof.lines와 일치하는 변이 하나도 없는 경우에만 확장
// 이 다각형이 roof.lines와 일치하는 변을 가지고 있는지 확인
// 다각형의 모든 변이 roof.lines와 일치하지 않는 경우에만 확장
//const extendLine = extendLineToRoofBoundary(clippedLine, roof, convertedPolygon)
const isIsolated = !convertedPolygon.some(pp =>
roof.points.some(rp =>
Math.abs(pp.x - rp.x) < 0.5 && Math.abs(pp.y - rp.y) < 0.5
));
//console.log("skeletonLines::::",skeletonLines);
addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', 'red', 5, pitch, isOuterLine);
// }
}
//그려진
}
@ -899,53 +860,6 @@ function isOuterEdge(p1, p2, edges) {
});
}
function isOuterRoofLine(p1, p2, lines) {
const tolerance = 0.5;
let foundLine = null;
let isForward = false;
let isBackward = false;
for (const line of lines) {
console.log("lines of line::::", line.startPoint, line.endPoint);
const lineStart = { x: line.startPoint.x, y: line.startPoint.y };
const lineEnd = { x: line.endPoint.x, y: line.endPoint.y };
let p1X = Math.abs(lineStart.x - p1.x) < tolerance && Math.abs(lineEnd.x - p2.x) < tolerance
let p2X = Math.abs(lineEnd.x - p1.x) < tolerance && Math.abs(lineStart.x - p2.x) < tolerance
let p1Y = Math.abs(lineStart.y - p1.y) < tolerance && Math.abs(lineEnd.y - p2.y) < tolerance
let p2Y = Math.abs(lineEnd.y - p2.y) < tolerance && Math.abs(lineStart.y - p2.y) < tolerance
if (p1X || p2X) {
foundLine = line;
p1.y = p1X? lineStart.y : lineEnd.y
p2.y = p2X? lineEnd.y : lineStart.y
break; // 매칭되는 라인을 찾으면 루프 종료
}else if(p1Y || p2Y) {
foundLine = line;
p1.x = p1Y? lineStart.x : lineEnd.x
p2.x = p2Y? lineEnd.x : lineStart.x
break; // 매칭되는 라인을 찾으면 루프 종료
}
}
if (foundLine) {
return {
result: true,
line: foundLine,
p1: p1,
p2: p2
};
}
return {
result: false,
line: null,
pX: false,
p2: false,
};
}
/**
* 스켈레톤 라인 배열에 새로운 라인을 추가합니다. (중복 방지)
* @param id
@ -1708,27 +1622,23 @@ function calculateSlope(p1, p2) {
return (p2.y - p1.y) / (p2.x - p1.x);
}
// Helper function to calculate slope of a line
function getSlope(line) {
const dx = line.endPoint.x - line.startPoint.x;
// Avoid division by zero for vertical lines
return dx === 0 ? Infinity : (line.endPoint.y - line.startPoint.y) / dx;
}
// Check if two lines are parallel
function areLinesParallel(line1, line2) {
const slope1 = getSlope(line1);
const slope2 = getSlope(line2);
// Both lines are vertical
if (slope1 === Infinity && slope2 === Infinity) return true;
// Check if slopes are approximately equal
const epsilon = 0.0001;
return Math.abs(slope1 - slope2) < epsilon;
}
// 두 직선이 평행한지 확인
// 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
*/
@ -1881,59 +1791,70 @@ function isPointInsidePolygon2(point, roofLines) {
return inside;
}
/**
* 점이 다각형 내부에 있는지 확인합니다 (Ray Casting 알고리즘 사용).
* @param {Object} point - 확인할 {x, y}
* @param {Array} polygonLines - 다각형을 구성하는 선분들의 배열
* @returns {boolean} 점이 다각형 내부에 있으면 true
*/
function isPointInsidePolygon(point, polygonLines) {
if (!polygonLines || polygonLines.length < 3) {
return false;
function isPointInsidePolygon(point, roofLines) {
// 1. 먼저 경계선 위에 있는지 확인 (방향 무관)
if (isOnBoundaryDirectionIndependent(point, roofLines)) {
return true;
}
let inside = false;
// 2. 내부/외부 판단 (기존 알고리즘)
let winding = 0;
const x = point.x;
const y = point.y;
const n = polygonLines.length;
// 경계 박스(bounding box) 체크로 빠르게 필터링
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (const line of polygonLines) {
minX = Math.min(minX, line.x1, line.x2);
maxX = Math.max(maxX, line.x1, line.x2);
minY = Math.min(minY, line.y1, line.y2);
maxY = Math.max(maxY, line.y1, line.y2);
for (let i = 0; i < roofLines.length; i++) {
const line = roofLines[i];
const x1 = line.x1, y1 = line.y1;
const x2 = line.x2, y2 = line.y2;
if (y1 <= y) {
if (y2 > y) {
const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
if (orientation > 0) winding++;
}
} else {
if (y2 <= y) {
const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
if (orientation < 0) winding--;
}
}
}
// 점이 경계 박스 밖에 있으면 바로 false 반환
if (x < minX || x > maxX || y < minY || y > maxY) {
return winding !== 0;
}
// 방향에 무관한 경계선 검사
function isOnBoundaryDirectionIndependent(point, roofLines) {
const tolerance = 1e-10;
for (const line of roofLines) {
if (isPointOnLineSegmentDirectionIndependent(point, line, tolerance)) {
return true;
}
}
return false;
}
// 핵심: 방향에 무관한 선분 위 점 검사
function isPointOnLineSegmentDirectionIndependent(point, line, tolerance) {
const x = point.x, y = point.y;
const x1 = line.x1, y1 = line.y1;
const x2 = line.x2, y2 = line.y2;
// 방향에 무관하게 경계 상자 체크
const minX = Math.min(x1, x2);
const maxX = Math.max(x1, x2);
const minY = Math.min(y1, y2);
const maxY = Math.max(y1, y2);
if (x < minX - tolerance || x > maxX + tolerance ||
y < minY - tolerance || y > maxY + tolerance) {
return false;
}
// Ray Casting 알고리즘
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = polygonLines[i].x1;
const yi = polygonLines[i].y1;
const xj = polygonLines[j].x1;
const yj = polygonLines[j].y1;
// 점이 정점 위에 있는 경우
if ((xi === x && yi === y) || (xj === x && yj === y)) {
return true;
}
// 수평선과 교차하는지 확인
const intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) {
inside = !inside;
}
}
return inside;
// 외적을 이용한 직선 위 판단 (방향 무관)
const cross = (y - y1) * (x2 - x1) - (x - x1) * (y2 - y1);
return Math.abs(cross) < tolerance;
}
/**
@ -2648,454 +2569,4 @@ function hasIntersectionWithOtherLines(point, skeletonLines, currentLine, tolera
// 1개 이상의 다른 라인과 연결되어 있으면 교점으로 간주
return connectionCount >= 1;
}
/**
* 대각선의 끝점을 roof.lines의 가장 가까운 접점까지 확장합니다.
* @param {Object} p1 - 대각선의 시작점 {x, y}
* @param {Object} p2 - 대각선의 끝점 {x, y}
* @param {Array} roofLines - 지붕 경계선 배열
* @returns {Object|null} 확장된 라인 {p1: {x, y}, p2: {x, y}} 또는 null
*/
function extendDiagonalToRoofBoundary(p1, p2, roofLines) {
if (!roofLines || roofLines.length === 0) {
return null;
}
const tolerance = 0.5;
const extendedLine = {
p1: { ...p1 },
p2: { ...p2 }
};
// p1이 roof.lines 위에 있는지 확인
const p1OnBoundary = roofLines.some(line => {
return isPointOnLineSegment(p1,
{ x: line.x1, y: line.y1 },
{ x: line.x2, y: line.y2 },
tolerance
);
});
// p2가 roof.lines 위에 있는지 확인
const p2OnBoundary = roofLines.some(line => {
return isPointOnLineSegment(p2,
{ x: line.x1, y: line.y1 },
{ x: line.x2, y: line.y2 },
tolerance
);
});
// p1을 확장해야 하는 경우
if (!p1OnBoundary) {
const extendedP1 = findClosestBoundaryPoint(p1, p2, roofLines, 'backward');
if (extendedP1) {
extendedLine.p1 = extendedP1;
}
}
// p2를 확장해야 하는 경우
if (!p2OnBoundary) {
const extendedP2 = findClosestBoundaryPoint(p2, p1, roofLines, 'forward');
if (extendedP2) {
extendedLine.p2 = extendedP2;
}
}
return extendedLine;
}
/**
* 점이 선분 위에 있는지 확인합니다 (허용 오차 포함).
* @param {Object} point - 확인할 {x, y}
* @param {Object} lineStart - 선분 시작점 {x, y}
* @param {Object} lineEnd - 선분 끝점 {x, y}
* @param {number} tolerance - 허용 오차
* @returns {boolean} 선분 위에 있으면 true
*/
function isPointOnLineSegment(point, lineStart, lineEnd, tolerance = 0.5) {
const dist = Math.sqrt(
Math.pow(lineEnd.x - lineStart.x, 2) +
Math.pow(lineEnd.y - lineStart.y, 2)
);
const dist1 = Math.sqrt(
Math.pow(point.x - lineStart.x, 2) +
Math.pow(point.y - lineStart.y, 2)
);
const dist2 = Math.sqrt(
Math.pow(point.x - lineEnd.x, 2) +
Math.pow(point.y - lineEnd.y, 2)
);
return Math.abs(dist - (dist1 + dist2)) < tolerance;
}
/**
* 점에서 다른 방향으로 연장하여 가장 가까운 roof.lines 교차점을 찾습니다.
* @param {Object} fromPoint - 연장할 시작점
* @param {Object} directionPoint - 방향을 결정하는
* @param {Array} roofLines - 지붕 경계선 배열
* @param {string} direction - 'forward' 또는 'backward'
* @returns {Object|null} 교차점 {x, y} 또는 null
*/
function findClosestBoundaryPoint(fromPoint, directionPoint, roofLines, direction = 'forward') {
// 방향 벡터 계산
const dx = directionPoint.x - fromPoint.x;
const dy = directionPoint.y - fromPoint.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) return null;
// 정규화된 방향 벡터
const dirVec = {
x: dx / length,
y: dy / length
};
// backward 방향인 경우 벡터를 반대로
if (direction === 'backward') {
dirVec.x = -dirVec.x;
dirVec.y = -dirVec.y;
}
let closestIntersection = null;
let minDistance = Infinity;
// 모든 roof.lines와의 교차점 찾기
for (const line of roofLines) {
const lineP1 = { x: line.x1, y: line.y1 };
const lineP2 = { x: line.x2, y: line.y2 };
// 무한 직선과의 교차점 계산
const intersection = getRayIntersectionWithSegment(
fromPoint,
dirVec,
lineP1,
lineP2
);
if (intersection && intersection.t > 0.1) { // 약간의 여유를 둠
const distance = Math.sqrt(
Math.pow(intersection.point.x - fromPoint.x, 2) +
Math.pow(intersection.point.y - fromPoint.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
closestIntersection = intersection.point;
}
}
}
return closestIntersection;
}
/**
* 다각형이 roof.lines와 일치하는 (edge) 하나 이상 가지고 있는지 확인합니다.
* @param {Array} polygonPoints - 다각형의 점들 배열 [{x, y}, ...]
* @param {Array} roofLines - 지붕 경계선 배열
* @returns {boolean} 일치하는 변이 하나라도 있으면 true, 하나도 없으면 false
*/
function polygonHasMatchingRoofLine(polygonPoints, roofLines) {
if (!polygonPoints || polygonPoints.length < 2) return false;
if (!roofLines || roofLines.length === 0) return false;
const tolerance = 0.5;
// 다각형의 각 변을 순회
for (let i = 0; i < polygonPoints.length; i++) {
const p1 = polygonPoints[i];
const p2 = polygonPoints[(i + 1) % polygonPoints.length];
// 이 변이 roof.lines 중 하나와 일치하는지 확인
for (const roofLine of roofLines) {
const rp1 = { x: roofLine.x1, y: roofLine.y1 };
const rp2 = { x: roofLine.x2, y: roofLine.y2 };
// 정방향 또는 역방향으로 일치하는지 확인
const forwardMatch =
isSamePoint(p1, rp1, tolerance) && isSamePoint(p2, rp2, tolerance);
const backwardMatch =
isSamePoint(p1, rp2, tolerance) && isSamePoint(p2, rp1, tolerance);
if (forwardMatch || backwardMatch) {
console.log('Found matching edge - this polygon touches roof.lines:', {
polygonEdge: { p1, p2 },
roofLine: { p1: rp1, p2: rp2 }
});
return true; // 하나라도 일치하면 즉시 true 반환
}
}
}
console.log('No matching edges - this polygon is isolated from roof.lines');
return false; // 모든 변을 확인했는데 일치하는 게 없음
}
function extendLineToRoofBoundary(clippedLine, roof, convertedPolygon) {
const isIsolated = !convertedPolygon.some(pp =>
roof.points.some(rp =>
Math.abs(pp.x - rp.x) < 0.5 && Math.abs(pp.y - rp.y) < 0.5
));
// 대각선 라인 선택해서 가장 가까운 경계선 까지 확장
const dx = Math.abs(clippedLine.p2.x - clippedLine.p1.x);
const dy = Math.abs(clippedLine.p2.y - clippedLine.p1.y);
const isDiagonal = dx > 0.5 && dy > 0.5;
if (isIsolated && isDiagonal) {
// moveSelectLine의 방향 벡터 계산
const selectDx = roof.moveSelectLine.endPoint.x - roof.moveSelectLine.startPoint.x;
const selectDy = roof.moveSelectLine.endPoint.y - roof.moveSelectLine.startPoint.y;
const selectLength = Math.sqrt(selectDx * selectDx + selectDy * selectDy);
const selectDir = { x: selectDx / selectLength, y: selectDy / selectLength };
// moveSelectLine의 중점
const selectMidX = (roof.moveSelectLine.startPoint.x + roof.moveSelectLine.endPoint.x) / 2;
const selectMidY = (roof.moveSelectLine.startPoint.y + roof.moveSelectLine.endPoint.y) / 2;
// 이동 방향 확인
const moveDirection = roof.moveDirect; // 'up', 'down', 'left', 'right', 'in', 'out'
const movePosiotn = roof.movePosition
// moveSelectLine과 평행한 경계선 찾기
let closestParallelLine = null;
let minDistToLine = Infinity;
// 대각선의 중점
const midX = (clippedLine.p1.x + clippedLine.p2.x) / 2;
const midY = (clippedLine.p1.y + clippedLine.p2.y) / 2;
for (const roofLine of roof.lines) {
const lineP1 = { x: roofLine.x1, y: roofLine.y1 };
const lineP2 = { x: roofLine.x2, y: roofLine.y2 };
// 경계선의 방향 벡터 계산
const lineDx = lineP2.x - lineP1.x;
const lineDy = lineP2.y - lineP1.y;
const lineLength = Math.sqrt(lineDx * lineDx + lineDy * lineDy);
const lineDir = { x: lineDx / lineLength, y: lineDy / lineLength };
// moveSelectLine과 평행한지 확인
const dotProduct = Math.abs(selectDir.x * lineDir.x + selectDir.y * lineDir.y);
const isParallel = dotProduct > 0.95;
if (!isParallel) continue;
// 대각선 중점에서 경계선까지의 거리
const lineMidX = (lineP1.x + lineP2.x) / 2;
const lineMidY = (lineP1.y + lineP2.y) / 2;
const dist = Math.sqrt(
Math.pow(midX - lineMidX, 2) +
Math.pow(midY - lineMidY, 2)
);
if (dist < minDistToLine) {
minDistToLine = dist;
closestParallelLine = roofLine;
}
}
if (closestParallelLine) {
const lineP1 = { x: closestParallelLine.x1, y: closestParallelLine.y1 };
const lineP2 = { x: closestParallelLine.x2, y: closestParallelLine.y2 };
const lineMidX = (lineP1.x + lineP2.x) / 2;
const lineMidY = (lineP1.y + lineP2.y) / 2;
// 이동 방향에 따라 확장할 끝점 결정
let shouldExtendP1 = false;
switch (movePosiotn) {
case 'down':
// moveSelectLine이 아래/밖으로 이동 -> 아래/밖에 있는 점 확장
//shouldExtendP1 = (lineMidY > selectMidY) ? (clippedLine.p1.y > clippedLine.p2.y) : (clippedLine.p1.y < clippedLine.p2.y);
break;
case 'up':
// moveSelectLine이 위/안으로 이동 -> 위/안에 있는 점 확장
//shouldExtendP1 = (lineMidY < selectMidY) ? (clippedLine.p1.y < clippedLine.p2.y) : (clippedLine.p1.y > clippedLine.p2.y);
break;
case 'left':
// moveSelectLine이 왼쪽으로 이동 -> 왼쪽에 있는 점 확장
//shouldExtendP1 = (lineMidX < selectMidX) ? (clippedLine.p1.x < clippedLine.p2.x) : (clippedLine.p1.x > clippedLine.p2.x);
break;
case 'right':
// moveSelectLine이 오른쪽으로 이동 -> 오른쪽에 있는 점 확장
//shouldExtendP1 = (lineMidX > selectMidX) ? (clippedLine.p1.x > clippedLine.p2.x) : (clippedLine.p1.x < clippedLine.p2.x);
break;
}
const endPoint = shouldExtendP1 ? clippedLine.p1 : clippedLine.p2;
const startPoint = shouldExtendP1 ? clippedLine.p2 : clippedLine.p1;
// 대각선 방향 벡터
const dirX = endPoint.x - startPoint.x;
const dirY = endPoint.y - startPoint.y;
// endPoint에서 방향으로 연장
const farEnd = {
x: endPoint.x + dirX * 10000,
y: endPoint.y + dirY * 10000
};
const intersection = getLineIntersection(startPoint, farEnd, lineP1, lineP2);
if (intersection) {
// endPoint를 교차점으로 확장
if (shouldExtendP1) {
clippedLine.p1 = intersection;
} else {
clippedLine.p2 = intersection;
}
console.log('고립된 다각형 대각선 확장됨 (방향 고려):', clippedLine);
return clippedLine
}
}
return null;
}
return undefined
}
function getIntersectionDetails(p1, p2, line) {
const { startPoint: lineStart, endPoint: lineEnd } = line;
const lineIsVertical = Math.abs(lineEnd.x - lineStart.x) < 0.0001;
const lineIsHorizontal = Math.abs(lineEnd.y - lineStart.y) < 0.0001;
// Calculate intersection point
const d1 = (p2.x - p1.x) * (lineStart.y - p1.y) - (p2.y - p1.y) * (lineStart.x - p1.x);
const d2 = (p2.x - p1.x) * (lineEnd.y - p1.y) - (p2.y - p1.y) * (lineEnd.x - p1.x);
const d3 = (lineEnd.x - lineStart.x) * (p1.y - lineStart.y) - (lineEnd.y - lineStart.y) * (p1.x - lineStart.x);
const d4 = (lineEnd.x - lineStart.x) * (p2.y - lineStart.y) - (lineEnd.y - lineStart.y) * (p2.x - lineStart.x);
if ((d1 * d2 < 0) && (d3 * d4 < 0)) {
// Calculate intersection point
const t = d3 / (d3 - d4);
const ix = p1.x + t * (p2.x - p1.x);
const iy = p1.y + t * (p2.y - p1.y);
// Calculate distances to determine which point is closer
const distToP1 = Math.hypot(ix - p1.x, iy - p1.y);
const distToP2 = Math.hypot(ix - p2.x, iy - p2.y);
// Determine if intersection is closer to start or end of the line segment
const distToLineStart = Math.hypot(ix - lineStart.x, iy - lineStart.y);
const distToLineEnd = Math.hypot(ix - lineEnd.x, iy - lineEnd.y);
return {
intersects: true,
point: { x: ix, y: iy },
// Which point of the segment (p1 or p2) is closer to intersection
segmentPoint: distToP1 < distToP2 ? 'p1' : 'p2',
// Which point of the moveSelectLine is closer to intersection
linePoint: distToLineStart < distToLineEnd ? 'start' : 'end',
// Line orientation
lineOrientation: lineIsVertical ? 'vertical' : lineIsHorizontal ? 'horizontal' : 'diagonal'
};
}
return { intersects: false };
}
function findClosestParallelLine(intersection, roofLines, orientation) {
if (!intersection || !roofLines?.length) return null;
let closest = null;
let minDist = Infinity;
for (const line of roofLines) {
const p1 = line.x1 !== undefined ? {x: line.x1, y: line.y1} : line.startPoint;
const p2 = line.x2 !== undefined ? {x: line.x2, y: line.y2} : line.endPoint;
// Check line orientation
const dx = Math.abs(p2.x - p1.x);
const dy = Math.abs(p2.y - p1.y);
const lineOrientation = dx < 0.0001 ? 'vertical' :
dy < 0.0001 ? 'horizontal' : 'diagonal';
if (lineOrientation !== orientation) continue;
// Calculate distance to line
const dist = Math.min(
Math.hypot(p1.x - intersection.x, p1.y - intersection.y),
Math.hypot(p2.x - intersection.x, p2.y - intersection.y)
);
if (dist < minDist) {
minDist = dist;
closest = { startPoint: p1, endPoint: p2 };
}
}
return closest;
}
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;
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);
}