From 7377b06e2af7df3e7a30d998c3d6c4eb9725153b Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 7 Nov 2025 07:57:41 +0900 Subject: [PATCH] =?UTF-8?q?log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QLine.js | 1 + src/components/fabric/QPolygon.js | 5 +- src/util/skeleton-utils.js | 843 +++++++++++++++++++++++++++--- 3 files changed, 788 insertions(+), 61 deletions(-) diff --git a/src/components/fabric/QLine.js b/src/components/fabric/QLine.js index 77254d5d..02448cd7 100644 --- a/src/components/fabric/QLine.js +++ b/src/components/fabric/QLine.js @@ -16,6 +16,7 @@ export const QLine = fabric.util.createClass(fabric.Line, { children: [], padding: 5, textVisible: true, + textBaseline: 'alphabetic', initialize: function (points, options, length = 0) { // 소수점 전부 제거 diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 1c2e3d65..3ca095a8 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -6,6 +6,7 @@ import { calculateAngle, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON } import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' +import { drawSkeletonRidgeRoof } from '@/util/skeleton-utils' export const QPolygon = fabric.util.createClass(fabric.Polygon, { type: 'QPolygon', @@ -335,8 +336,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) { // 용마루 -- straight-skeleton console.log('용마루 지붕') - drawRidgeRoof(this.id, this.canvas, textMode) - //drawSkeletonRidgeRoof(this.id, this.canvas, textMode); + //drawRidgeRoof(this.id, this.canvas, textMode) + drawSkeletonRidgeRoof(this.id, this.canvas, textMode); } else if (isGableRoof(types)) { // A형, B형 박공 지붕 console.log('패턴 지붕') diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 6294faaf..7f7ffcd7 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -376,6 +376,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { roof.innerLines = roof.innerLines || []; roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) + + // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {} @@ -385,7 +387,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, @@ -434,8 +436,10 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { skeleton.Edges.forEach((edgeResult, index) => { - + canvas.skeletonLines = []; processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines); + + }); // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다. @@ -470,7 +474,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } }); - +/* //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); @@ -492,15 +496,16 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { trimIntersectingExtendedLines(skeletonLines, disconnectedLines); } +*/ - + //2. 연결이 끊어진 라인이 있을경우 찾아서 추가한다(동 이동일때) // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다. const innerLines = []; const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set - - skeletonLines.forEach(line => { + console.log("length::::::::", skeletonLines.length); + skeletonLines.forEach((line, index) => { const { p1, p2, attributes, lineStyle } = line; // 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교) @@ -510,9 +515,11 @@ 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 }, @@ -533,18 +540,159 @@ 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{ + + + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + const wallLines = wall.baseLines + // 현재 지점과 다음 지점을 비교하기 위한 변수 + let changedLine = roof.moveSelectLine; + const roofLines = []; + + + if (!wall.lines || !wall.baseLines) { + return wall.baseLines || wall.lines || []; + } + + // 길이가 다른 경우 baseLines 반환 + if (wall.lines.length !== wall.baseLines.length) { + return wall.baseLines; + } + + for (let i = 0; i < wall.baseLines.length; i++) { + const baseLine = wall.baseLines[i]; + const line = wall.lines[i]; + + if (!line || + ((!isSamePoint(baseLine.startPoint, line.startPoint)) && // 시작점이 다르고 + (!isSamePoint(baseLine.endPoint, line.endPoint)))) { // 끝점도 다른 경우 + + } + } + + + const startClosest = findClosestRoofLine(p1, roof.lines); + const endClosest = findClosestRoofLine(p2, roof.lines); + + + const { point, closest, selectPoint, otherPoint } = + startClosest.distance > endClosest.distance + ? { + point : p2, + closest : endClosest, + otherPoint: p1 + } + : { + point : p1, + closest : startClosest, + otherPoint: p2 + }; + +// Log the relevant information + console.log("Point:", point); + console.log("Closest intersection:", closest); + console.log("moveSelectLinePoint:", selectPoint); + let isTarget = false; + for(const roofLine of roof.lines){ + if( isSamePoint(p1, roofLine.startPoint) || + isSamePoint(p2, roofLine.endPoint) || + isSamePoint(p1, roofLine.endPoint) || + isSamePoint(p2, roofLine.startPoint) ) { + isTarget = true ; + break + } + } + if (isTarget) { + console.warn("matching line found in roof.lines"); + return; // 또는 적절한 오류 처리 + } + + const innerLine2 = new QLine([p1.x, p1.y, p2.x, p2.y], { + parentId: roof.id, + fontSize: roof.fontSize, + stroke: 'red', + strokeWidth: lineStyle.width, + name: (line.attributes.isOuterEdge)?'eaves': attributes.type, + attributes: attributes, + direction: direction, + isBaseLine: line.attributes.isOuterEdge, + lineName: (line.attributes.isOuterEdge)?'addLine': attributes.type, + selectable:(!line.attributes.isOuterEdge), + roofId: roofId, + }); + + canvas.add(innerLine2); + //existingLines.add(lineKey); // 추가된 라인을 추적 + /* + //라인추가(까지 못할때때) 외벽선에서 추가 + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + const wallLines = wall.baseLines + // 현재 지점과 다음 지점을 비교하기 위한 변수 + let changedLine = roof.moveSelectLine; + const roofLines = []; + + + if (!wall.lines || !wall.baseLines) { + return wall.baseLines || wall.lines || []; + } + + // 길이가 다른 경우 baseLines 반환 + if (wall.lines.length !== wall.baseLines.length) { + return wall.baseLines; + } + + + //그려지는 처마라인이 처마 && 포인터모두가 wall.baseLine에 들어가 있는 경우 + const checkPoint = {x1:line.x1, y1:line.y1, x2:line.x2, y2:line.y2} + if(line.attributes.type === 'hip' && !checkPointInPolygon(checkPoint, wall)) { + const startClosest = findClosestRoofLine(p1, roof.lines); + const endClosest = findClosestRoofLine(p2, roof.lines); + console.log("Lindd::::",line) + const { point, closest, selectPoint, otherPoint } = + startClosest.distance > endClosest.distance + ? { + point : p2, + closest : endClosest, + //selectPoint : changedLine.endPoint, + otherPoint: p1 + } + : { + point : p1, + closest : startClosest, + //selectPoint : changedLine.startPoint, + otherPoint: p2 + }; + +// Log the relevant information + console.log("Point:", point); + console.log("Closest intersection:", closest); + console.log("moveSelectLinePoint:", selectPoint); + } +*/ + const coordinateText = new fabric.Text(`(${Math.round(p1.x)}, ${Math.round(p1.y)})`, { left: p1.x + 5, // 좌표점에서 약간 오른쪽으로 이동 top: p1.y - 20, // 좌표점에서 약간 위로 이동 fontSize: 13, fill: 'red', fontFamily: 'Arial', + textBaseline: 'alphabetic', // 올바른 값으로 설정 selectable: true, lockMovementX: false, lockMovementY: false, @@ -553,7 +701,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { lockScalingY: true, name: 'lengthText' }) - +//좌표점(임시) canvas?.add(coordinateText) } innerLines.push(innerLine) @@ -581,10 +729,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 @@ -608,6 +756,10 @@ 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]; @@ -615,11 +767,28 @@ 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); - // } + } + + //그려진 + } @@ -730,17 +899,64 @@ 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 * @param {Array} skeletonLines - 스켈레톤 라인 배열 - * @param {Set} processedInnerEdges - 처리된 Edge 키 Set * @param {object} p1 - 시작점 * @param {object} p2 - 끝점 * @param {string} lineType - 라인 타입 * @param {string} color - 색상 * @param {number} width - 두께 - * @param currentDegree + * @param pitch + * @param isOuterLine */ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine) { // const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|'); @@ -1492,23 +1708,27 @@ 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; -// } +// 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 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 */ @@ -1560,14 +1780,14 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { // p2가 다각형 내부에 있는지 확인 const p2Inside = isPointInsidePolygon(p2, roofLines); - console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); + //console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); // 두 점 모두 내부에 있으면 그대로 반환 if (p1Inside && p2Inside) { if(!selectLine || isDiagonal){ return { p1: clippedP1, p2: clippedP2 }; } - console.log('평행선::', clippedP1, clippedP2) + //console.log('평행선::', clippedP1, clippedP2) return { p1: clippedP1, p2: clippedP2 }; } @@ -1600,20 +1820,20 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { if (!p1Inside && !p2Inside) { // 두 점 모두 외부에 있는 경우 if (intersections.length >= 2) { - console.log('Both outside, using intersection points'); + //console.log('Both outside, using intersection points'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; clippedP2.x = intersections[1].point.x; clippedP2.y = intersections[1].point.y; } else { - console.log('Both outside, no valid intersections - returning original'); + //console.log('Both outside, no valid intersections - returning original'); // 교차점이 충분하지 않으면 원본 반환 return { p1: clippedP1, p2: clippedP2 }; } } else if (!p1Inside && p2Inside) { // p1이 외부, p2가 내부 if (intersections.length > 0) { - console.log('p1 outside, p2 inside - moving p1 to intersection'); + //console.log('p1 outside, p2 inside - moving p1 to intersection'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; // p2는 이미 내부에 있으므로 원본 유지 @@ -1623,7 +1843,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { } else if (p1Inside && !p2Inside) { // p1이 내부, p2가 외부 if (intersections.length > 0) { - console.log('p1 inside, p2 outside - moving p2 to intersection'); + //console.log('p1 inside, p2 outside - moving p2 to intersection'); // p1은 이미 내부에 있으므로 원본 유지 clippedP1.x = p1.x; clippedP1.y = p1.y; @@ -1641,7 +1861,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { * @param {Array} roofLines - 다각형을 구성하는 선분들 * @returns {boolean} 점이 다각형 내부에 있으면 true */ -function isPointInsidePolygon(point, roofLines) { +function isPointInsidePolygon2(point, roofLines) { let inside = false; const x = point.x; const y = point.y; @@ -1661,6 +1881,61 @@ function isPointInsidePolygon(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; + } + + let inside = false; + 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); + } + + // 점이 경계 박스 밖에 있으면 바로 false 반환 + if (x < minX || x > maxX || y < minY || y > maxY) { + 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; +} + /** * 선분 위의 점에 대한 매개변수 t를 계산합니다. * p = p1 + t * (p2 - p1)에서 t 값을 구합니다. @@ -1989,19 +2264,19 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const { x1, y1, x2, y2 } = lineCoords; - console.log('wall.points', wall.baseLines); + //console.log('wall.points', wall.baseLines); for(const line of wall.baseLines) { - console.log('line', line); + //console.log('line', line); const basePoint = extractLineCoords(line); const { x1: bx1, y1: by1, x2: bx2, y2: by2 } = basePoint; - console.log('x1, y1, x2, y2', bx1, by1, bx2, by2); + //console.log('x1, y1, x2, y2', bx1, by1, bx2, by2); // 객체 비교 대신 좌표값 비교 if (Math.abs(bx1 - x1) < 0.1 && Math.abs(by1 - y1) < 0.1 && Math.abs(bx2 - x2) < 0.1 && Math.abs(by2 - y2) < 0.1) { - console.log('basePoint 일치!!!', basePoint); + //console.log('basePoint 일치!!!', basePoint); } } @@ -2009,11 +2284,11 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { // 라인 방향 분석 const lineInfo = analyzeLineOrientation(x1, y1, x2, y2, epsilon); - if (debug) { - console.log('=== getSelectLinePosition ==='); - console.log('selectLine 좌표:', lineCoords); - console.log('라인 방향:', lineInfo.orientation); - } + // if (debug) { + // console.log('=== getSelectLinePosition ==='); + // console.log('selectLine 좌표:', lineCoords); + // console.log('라인 방향:', lineInfo.orientation); + // } // 라인의 중점 const midX = (x1 + x2) / 2; @@ -2032,11 +2307,11 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const topIsInside = checkPointInPolygon(topTestPoint, wall); const bottomIsInside = checkPointInPolygon(bottomTestPoint, wall); - if (debug) { - console.log('수평선 테스트:'); - console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); - console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); - } + // if (debug) { + // console.log('수평선 테스트:'); + // console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); + // console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); + // } // top 조건: 위쪽이 외부, 아래쪽이 내부 if (!topIsInside && bottomIsInside) { @@ -2094,9 +2369,9 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { midPoint: { x: midX, y: midY } }; - if (debug) { - console.log('최종 결과:', result); - } + // if (debug) { + // console.log('최종 결과:', result); + // } return result; }; @@ -2373,4 +2648,454 @@ 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); } \ No newline at end of file