From 0acd9e422f0bb14a4f49c28f5085944a810038e9 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 23 Sep 2025 18:40:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?skeleton=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 929 +++++++++++++++++-------------------- 1 file changed, 435 insertions(+), 494 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 841bbcc2..ba5b4e87 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -3,26 +3,28 @@ import Big from 'big.js' import { SkeletonBuilder } from '@/lib/skeletons' import { arePointsEqual, calcLineActualSize, calcLinePlaneSize, calculateAngle, toGeoJSON } from '@/util/qpolygon-utils' import { QLine } from '@/components/fabric/QLine' -import { getDegreeByChon, isPointOnLine } from '@/util/canvas-util' +import { getDegreeByChon } from '@/util/canvas-util' + /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. * @param {string} roofId - 대상 지붕 객체의 ID * @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체 * @param {string} textMode - 텍스트 표시 모드 + * @param existingSkeletonLines */ -export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { +export const drawSkeletonRidgeRoof = (roofId, canvas, textMode, existingSkeletonLines = []) => { const roof = canvas?.getObjects().find((object) => object.id === roofId) if (!roof) { console.error(`Roof with id "${roofId}" not found.`); return; } - + const skeletonLines = [...existingSkeletonLines]; // 1. 기존 스켈레톤 라인 제거 - const existingSkeletonLines = canvas.getObjects().filter(obj => - obj.parentId === roofId && obj.attributes?.type === 'skeleton' - ); - existingSkeletonLines.forEach(line => canvas.remove(line)); + // const existingSkeletonLines = canvas.getObjects().filter(obj => + // obj.parentId === roofId && obj.attributes?.type === 'skeleton' + // ); + // existingSkeletonLines.forEach(line => canvas.remove(line)); // 2. 지붕 폴리곤 좌표 전처리 const coordinates = preprocessPolygonCoordinates(roof.points); @@ -45,7 +47,8 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) - skeletonBuilder(roofId, canvas, textMode, roof) + skeletonBuilder(roofId, canvas, textMode, roof, skeletonLines) + } /** @@ -175,7 +178,7 @@ export const transformMultipleEdges = (skeleton, edgeConfigs) => { * @param roof * @param edgeProperties */ -export const skeletonBuilder = (roofId, canvas, textMode, roof) => { +export const skeletonBuilder = (roofId, canvas, textMode, roof, skeletonLines) => { // 1. 다각형 좌표를 GeoJSON 형식으로 변환합니다. const geoJSONPolygon = toGeoJSON(roof.points) @@ -188,7 +191,7 @@ export const skeletonBuilder = (roofId, canvas, textMode, roof) => { console.log('Edge 분석:', skeleton.edge_analysis) // 3. 라인을 그림 - const innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode) + const innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode, skeletonLines) console.log("innerLines::", innerLines) // 4. 생성된 선들을 캔버스에 추가하고 지붕 객체에 저장 @@ -312,14 +315,14 @@ const mergeCollinearLines = (lines) => { } //조건에 따른 스켈레톤을 그린다. -const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMode) => { +const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMode, skeletonLines) => { console.log('=== Edge Properties 기반 후처리 시작 ===') if (!skeleton || !skeleton.Edges) return [] const innerLines = [] const processedInnerEdges = new Set() - const skeletonLines = [] + //const skeletonLines = [] // 1. 기본 skeleton에서 모든 내부 선분 수집 //edge 순서와 baseLines 순서가 같을수가 없다. @@ -464,59 +467,12 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, processedInnerEd x: (gableStart.x + gableEnd.x) / 2, y: (gableStart.y + gableEnd.y) / 2, } - // - // // polygonPoints와 gableMidpoint 비교: x 또는 y가 같은 점 찾기 (허용 오차 적용) - // const axisTolerance = 0.1 - // const sameXPoints = polygonPoints.filter((p) => Math.abs(p.x - gableMidpoint.x) < axisTolerance) - // const sameYPoints = polygonPoints.filter((p) => Math.abs(p.y - gableMidpoint.y) < axisTolerance) - // if (sameXPoints.length || sameYPoints.length) { - // console.log('GABLE: gableMidpoint와 같은 축의 폴리곤 점', { - // gableMidpoint, - // sameXPoints, - // sameYPoints, - // }) - // } - // + // 폴리곤 중심점 (대략적) const centerX = polygonPoints.reduce((sum, p) => sum + p.x, 0) / polygonPoints.length const centerY = polygonPoints.reduce((sum, p) => sum + p.y, 0) / polygonPoints.length const polygonCenter = { x: centerX, y: centerY } - // - // // 허용 오차 - // const colinearityTolerance = 0.1 - // - // // 폴리곤 선분 생성 (연속 점 쌍) - // const segments = [] - // for (let i = 0; i < polygonPoints.length; i++) { - // const p1 = polygonPoints[i] - // const p2 = polygonPoints[(i + 1) % polygonPoints.length] - // segments.push({ p1, p2 }) - // } - // - // // gableMidpoint와 같은 축(Y 또는 X)에 있는 수직/수평 선분만 추출 - // const sameAxisSegments = segments.filter(({ p1, p2 }) => { - // const isVertical = Math.abs(p1.x - p2.x) < colinearityTolerance - // const isHorizontal = Math.abs(p1.y - p2.y) < colinearityTolerance - // const sameXAxis = isVertical && Math.abs(p1.x - gableMidpoint.x) < axisTolerance - // const sameYAxis = isHorizontal && Math.abs(p1.y - gableMidpoint.y) < axisTolerance - // return sameXAxis || sameYAxis - // }) - // - // // 가장 가까운(또는 가장 긴) 용마루 후보 선택 - // let ridgeCandidate = null - // if (sameAxisSegments.length) { - // // 1) 중점과의 최단거리 기준 - // ridgeCandidate = sameAxisSegments.reduce((best, seg) => { - // const mid = { x: (seg.p1.x + seg.p2.x) / 2, y: (seg.p1.y + seg.p2.y) / 2 } - // const dist2 = (mid.x - gableMidpoint.x) ** 2 + (mid.y - gableMidpoint.y) ** 2 - // if (!best) return { seg, score: dist2 } - // return dist2 < best.score ? { seg, score: dist2 } : best - // }, null)?.seg - // - // - // } - // const selectBaseLine = baseLines[baseLineIndex]; @@ -555,214 +511,83 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, processedInnerEd } //확장 - // Extend lines that have endpoints in edgePoints to intersect with selectBaseLine - // Find diagonal lines (not horizontal or vertical) - // Extend lines that have endpoints in edgePoints + const breakLinePont = findDisconnectedSkeletonLines(skeletonLines, baseLines) + console.log('breakLinePont:', breakLinePont) - for (let i = 0; i < skeletonLines.length; i++) { - const line = skeletonLines[i]; - const p1 = line.p1; - const p2 = line.p2; - const lineP1 = { x: line.p1.x, y: line.p1.y }; - const lineP2 = { x: line.p2.x, y: line.p2.y }; - - let hasP1 = false; - let hasP2 = false; - console.log('edgeResult.Edge::',edgeResult.Edge) - - const lineInfo = findMatchingLinePoints(line, edgeResult.Polygon); - console.log(lineInfo); - - //대각선 - //직선(마루) - if(lineInfo.hasMatch) { - if (lineInfo.matches[0].type === 'diagonal') { - - const intersection2 = getLineIntersectionParametric(lineP1, lineP2, gableStart, gableEnd); - console.log('intersection2:', intersection2); - if (lineInfo.matches[0].linePoint === 'p1') { +if(breakLinePont.disconnectedLines.length > 0) { - skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: intersection2.x, y: intersection2.y }; - } else { + for (const dLine of breakLinePont.disconnectedLines) { + const inx = dLine.index; + const exLine = dLine.extendedLine; - skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: intersection2.x, y: intersection2.y }; - } + //확장 + if (dLine.p1Connected) { + skeletonLines[inx].p2 = { ...skeletonLines[inx].p2, x: exLine.p2.x, y: exLine.p2.y }; - } else if (lineInfo.matches[0].type === 'horizontal') { - if (lineInfo.matches[0].linePoint === 'p1') { - skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: edgeResult.Edge.Begin.X }; - } else { - skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: edgeResult.Edge.Begin.X }; - } - - } else if (lineInfo.matches[0].type === 'vertical') { - if (lineInfo.matches[0].linePoint === 'p1') { - skeletonLines[i].p1 = { ...skeletonLines[i].p1, y: edgeResult.Edge.Begin.Y }; - } else { - skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: edgeResult.Edge.Begin.Y }; - } + } else if (dLine.p2Connected) { + skeletonLines[inx].p1 = { ...skeletonLines[inx].p1, x: exLine.p1.x, y: exLine.p1.y }; } - } -// for (const polyPoint of edgeResult.Polygon) { + } +} + //확장(연장) +// for (let i = 0; i < skeletonLines.length; i++) { +// const line = skeletonLines[i]; +// const p1 = line.p1; +// const p2 = line.p2; +// const lineP1 = { x: line.p1.x, y: line.p1.y }; +// const lineP2 = { x: line.p2.x, y: line.p2.y }; // -// if (polyPoint.X === lineP1.x && polyPoint.Y === lineP1.y) { -// const extendedPoint1 = getExtensionIntersection(lineP2.x, lineP2.y, lineP1.x, lineP1.y, edgeResult.Edge.Begin.X, edgeResult.Edge.Begin.Y, edgeResult.Edge.End.X,edgeResult.Edge.End.Y); -// console.log('extendedPoint1:', extendedPoint1); +// let hasP1 = false; +// let hasP2 = false; +// console.log('edgeResult.Edge::',edgeResult.Edge) +// //선택한 라인과 다각형을 생성하는 라인 여부 +// const matchingLinePoint = findMatchingLinePoints(line, edgeResult.Polygon); +// console.log(matchingLinePoint); // -// skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: extendedPoint1.x, y: extendedPoint1.Y }; -// //skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: edgeResult.Edge.Begin.X}; +// +// if(matchingLinePoint.hasMatch) { +// +// if (matchingLinePoint.matches[0].type === 'diagonal') { +// console.log("lineP1:", lineP1) +// console.log("lineP2:", lineP2) +// const intersectionPoint = getLineIntersectionParametric(lineP1, lineP2, gableStart, gableEnd); +// console.log('intersectionPoint:', intersectionPoint); +// console.log('gableStart:', gableStart); +// console.log('gableEnd:', gableEnd); +// // 교차점이 생겼다면 절삭(교차점 이하(이상) 삭제) +// if (!intersectionPoint) { +// console.warn('No valid intersection point found between line and gable edge'); +// return; // or handle the null case appropriately +// } +// +// if (matchingLinePoint.matches[0].linePoint === 'p1') { +// skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: intersectionPoint.x, y: intersectionPoint.y }; +// } else { +// skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: intersectionPoint.x, y: intersectionPoint.y }; +// } +// +// } else if (matchingLinePoint.matches[0].type === 'horizontal') { +// if (matchingLinePoint.matches[0].linePoint === 'p1') { +// skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: edgeResult.Edge.Begin.X }; +// } else { +// skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: edgeResult.Edge.Begin.X }; +// } +// +// } else if (matchingLinePoint.matches[0].type === 'vertical') { +// if (matchingLinePoint.matches[0].linePoint === 'p1') { +// skeletonLines[i].p1 = { ...skeletonLines[i].p1, y: edgeResult.Edge.Begin.Y }; +// } else { +// skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: edgeResult.Edge.Begin.Y }; +// } // } // -// if (polyPoint.X === lineP2.x && polyPoint.Y === lineP2.y) { -// const extendedPoint2 = getExtensionIntersection(lineP1.x, lineP1.y,lineP2.x, lineP2.y, edgeResult.Edge.Begin.X, edgeResult.Edge.Begin.Y, edgeResult.Edge.End.X,edgeResult.Edge.End.Y); -// console.log('extendedPoint2:', extendedPoint2); -// -// skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: extendedPoint2.x, y: extendedPoint2.Y }; -// //skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: edgeResult.Edge.Begin.Y }; -// } -// -// // } - } +// +// } - /* - if (line.attributes.type === LINE_TYPE.SUBLINE.HIP || line.attributes.type === 'HIP') { - - // 선택한 기준선 을 중심으로 대각선 삭제 - // Get line and edge points - const edgeStart = { x: edgeResult.Edge.Begin.X, y: edgeResult.Edge.Begin.Y }; - const edgeEnd = { x: edgeResult.Edge.End.X, y: edgeResult.Edge.End.Y }; - const lineStart = { x: line.p1.x, y: line.p1.y }; - const lineEnd = { x: line.p2.x, y: line.p2.y }; - - const pointsEqual = (p1, p2) => { - return p1.x === p2.x && p1.y === p2.y; - } - // Check if line shares an endpoint with the edge - const sharesStartPoint = pointsEqual(edgeStart, lineStart) || pointsEqual(edgeStart, lineEnd); - const sharesEndPoint = pointsEqual(edgeEnd, lineStart) || pointsEqual(edgeEnd, lineEnd); - - if (sharesStartPoint || sharesEndPoint) { - skeletonLines.splice(i, 1); - // ridge extension logic can go here - //gableMidpoint까지 확장 - - }else{ - //선택한 baseLine 연장(edgeResult.Polygon 의 좌표와 동일한 좌표를 찾아서 연장) - for (const polyPoint of edgeResult.Polygon) { - if (Math.abs(polyPoint.X - lineEnd.x) < 0.1 && Math.abs(polyPoint.Y - lineEnd.y) < 0.1) { - // 연장 로직 - } - } - } - - }else if (line.attributes.type === LINE_TYPE.SUBLINE.RIDGE || line.attributes.type === 'RIDGE') { - //마루일때 - const lineP1 = { x: line.p1.x, y: line.p1.y }; - const lineP2 = { x: line.p2.x, y: line.p2.y }; - const extensionLine= { - maxX:'', - minX:'', - maxY:'', - minY:'', - } - - if(edgeResult.Polygon.length > 3){ - - let hasP1 = false; - let hasP2 = false; - - for (const polyPoint of edgeResult.Polygon) { - if (!hasP1 && polyPoint.X === lineP1.x && polyPoint.Y === lineP1.y) { - hasP1 = true; - } - if (!hasP2 && polyPoint.X === lineP2.x && polyPoint.Y === lineP2.y) { - hasP2 = true; - } - - // Early exit if both points are found - if (hasP1 && hasP2) break; - } - - if (hasP1 && hasP2) { - skeletonLines.splice(i, 1); - //양쪽 대각선이 있으면 서로 만난다. - for (const polyPoint of edgeResult.Polygon) { - - } - - - - //가운데 연장선을 추가 - skeletonLines.push({ - p1: {x: midPoint.x, y: midPoint.y}, - p2: {x: centerX, y: centerY}, - attributes: { - type: LINE_TYPE.SUBLINE.RIDGE, - planeSize: calcLinePlaneSize({ x1: midPoint.x, y1: midPoint.y, x2: centerX, y2: centerY }), - isRidge: true, - }, - lineStyle: { - color: '#FF0000', - width: 2 - }, - }) - } - - - - - - - }else{ - console.log("mpoint",gableMidpoint) - console.log("midPoint", midPoint) - console.log("lineP1",lineP1) - console.log("lineP2",lineP2) - //gableMidpoint까지 확장 (x or y 동일) - //가로일때 gableMidPoint.y 동일 - // Extend horizontal lines to gable midpoint - // - if (Math.abs(lineP1.y - lineP2.y) < 0.3) { // 가로 라인 - const extension = getExtensionLine(midPoint, lineP1, lineP2); - if (extension) { // null 체크 추가 - if (extension.isStartExtension) { - skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: extension.extensionPoint.x }; - } else { - skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: extension.extensionPoint.x }; - } - } - } else { // 세로 라인 - const extension = getExtensionLine(midPoint, lineP1, lineP2); - if (extension) { // null 체크 추가 - if (extension.isStartExtension) { - skeletonLines[i].p1 = { ...skeletonLines[i].p1, y: extension.extensionPoint.y }; - } else { - skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: extension.extensionPoint.y }; - } - } - } - - } - } - - console.log('result skeletonLines:', skeletonLines) -*/ - - - - // addRawLine( - // skeletonLines, - // processedInnerEdges, - // gableMidpoint, - // polygonCenter, - // 'RIDGE', - // '#0000FF', // 파란색으로 구분 - // 3, // 두껍게 - // ) } // ✅ 헬퍼 함수들 @@ -1092,6 +917,7 @@ const extendLineToOppositeEdge = (p1, p2, polygonPoints, selectedEdgeIndex) => { // 방향 벡터 정규화 const length = Math.sqrt(direction.x * direction.x + direction.y * direction.y) + if (length === 0) return null const normalizedDir = { @@ -1195,6 +1021,7 @@ const transformWallToRoof = (roof, canvas, edgeIndex) => { return roof } + /** * 사용 예제: 첫 번째 edge를 45도 각도로 변형 * drawSkeletonWithTransformedEdges(roofId, canvas, textMode, [ @@ -1558,7 +1385,7 @@ class Advanced2DRoofBuilder extends SkeletonBuilder { } if (edgeIndex < 0 || edgeIndex >= totalPoints) { - console.error(`edgeIndex ${edgeIndex}가 범위 [0, ${totalPoints - 1}]을 벗어났습니다`) + console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`) return } @@ -1658,9 +1485,13 @@ class Advanced2DRoofBuilder extends SkeletonBuilder { const centerX = validPoints.reduce((sum, p) => sum + p[0], 0) / totalPoints const centerY = validPoints.reduce((sum, p) => sum + p[1], 0) / totalPoints + console.log(`중심점: (${centerX}, ${centerY})`) + + // edge 중점 const midX = (p1[0] + p2[0]) / 2 const midY = (p1[1] + p2[1]) / 2 + // 외향 방향 const dirX = centerX - midX const dirY = centerY - midY const length = Math.sqrt(dirX * dirX + dirY * dirY) @@ -1778,87 +1609,6 @@ const isSameLine = (edgeStartX, edgeStartY, edgeEndX, edgeEndY, baseLine) => { return clockwiseMatch || counterClockwiseMatch } -/** - * 중간점과 선분의 끝점을 비교하여 연장선(extensionLine)을 결정합니다. - * @param {Object} midPoint - 중간점 좌표 {x, y} - * @param {Object} lineP1 - 선분의 첫 번째 끝점 {x, y} - * @param {Object} lineP2 - 선분의 두 번째 끝점 {x, y} - * @returns {Object|null} - 연장선 설정 또는 null (연장 불필요 시) - */ -function getExtensionLine(midPoint, lineP1, lineP2) { - // 선분의 방향 계산 - const isHorizontal = Math.abs(lineP1.y - lineP2.y) < 0.3; // y 좌표가 거의 같으면 수평선 - const isVertical = Math.abs(lineP1.x - lineP2.x) < 0.3; // x 좌표가 거의 같으면 수직선 - - if (isHorizontal) { - // 수평선인 경우 - y 좌표가 midPoint와 같은지 확인 - if (Math.abs(lineP1.y - midPoint.y) > 0.3) { - return null; // y 좌표가 다르면 연장하지 않음 - } - - // 중간점이 선분의 왼쪽에 있는 경우 - if (midPoint.x < Math.min(lineP1.x, lineP2.x)) { - return { - isHorizontal: true, - isStartExtension: lineP1.x < lineP2.x, - extensionPoint: { ...midPoint, y: lineP1.y } - }; - } - // 중간점이 선분의 오른쪽에 있는 경우 - else if (midPoint.x > Math.max(lineP1.x, lineP2.x)) { - return { - isHorizontal: true, - isStartExtension: lineP1.x > lineP2.x, - extensionPoint: { ...midPoint, y: lineP1.y } - }; - } - } - else if (isVertical) { - // 수직선인 경우 - x 좌표가 midPoint와 같은지 확인 - if (Math.abs(lineP1.x - midPoint.x) > 0.3) { - return null; // x 좌표가 다르면 연장하지 않음 - } - - // 중간점이 선분의 위에 있는 경우 - if (midPoint.y < Math.min(lineP1.y, lineP2.y)) { - return { - isHorizontal: false, - isStartExtension: lineP1.y < lineP2.y, - extensionPoint: { ...midPoint, x: lineP1.x } - }; - } - // 중간점이 선분의 아래에 있는 경우 - else if (midPoint.y > Math.max(lineP1.y, lineP2.y)) { - return { - isHorizontal: false, - isStartExtension: lineP1.y > lineP2.y, - extensionPoint: { ...midPoint, x: lineP1.x } - }; - } - } - - // 기본값 반환 (연장 불필요) - return null; -} -function convertToClockwise(points) { - // 1. 다각형의 면적 계산 (시계/반시계 방향 판단용) - let area = 0; - const n = points.length; - - for (let i = 0; i < n; i++) { - const j = (i + 1) % n; - area += (points[j].X - points[i].X) * (points[j].Y + points[i].Y); - } - - // 2. 반시계방향이면 배열을 뒤집어 시계방향으로 변환 - if (area < 0) { - return [...points].reverse(); - } - - // 3. 이미 시계방향이면 그대로 반환 - return [...points]; -} - /** * skeletonLines에서 특정 점(polyPoint)을 지나는 라인을 찾는 함수 * @param {Array} skeletonLines - 검색할 라인 배열 @@ -1912,198 +1662,389 @@ function findIntersection(p1, p2, p3, p4) { // 교차하지 않는 경우 return null; } -/** - * edgePoints와 skeletonLines의 교차점을 찾는 함수 - * @param {Array<{x: number, y: number}>} edgePoints - 엣지 포인트 배열 - * @param {Array} skeletonLines - 원시 라인 배열 (각 라인은 p1, p2 속성을 가짐) - * @returns {Array<{x: number, y: number, line: Object}>} 교차점과 해당 라인 정보 배열 - */ -function findIntersectionsWithEdgePoints(edgePoints, skeletonLines) { - const intersections = []; - // edgePoints를 순회하며 각 점을 지나는 라인 찾기 - for (let i = 0; i < edgePoints.length; i++) { - const point = edgePoints[i]; - const nextPoint = edgePoints[(i + 1) % edgePoints.length]; - // 현재 엣지 선분 - const edgeLine = { - x1: point.x, y1: point.y, - x2: nextPoint.x, y2: nextPoint.y +// baseLine 좌표 추출 헬퍼 함수 +const extractBaseLineCoordinates = (baseLine) => { + const left = baseLine.left || 0; + const top = baseLine.top || 0; + const width = baseLine.width || 0; + const height = baseLine.height || 0; + + // 수평선인 경우 (height가 0에 가까움) + if (Math.abs(height) < 0.1) { + return { + p1: { x: left, y: top }, + p2: { x: left + width, y: top } }; + } + // 수직선인 경우 (width가 0에 가까움) + else if (Math.abs(width) < 0.1) { + return { + p1: { x: left, y: top }, + p2: { x: left, y: top + height } + }; + } + // 기타 경우 (기본값) + else { + return { + p1: { x: left, y: top }, + p2: { x: left + width, y: top + height } + }; + } +}; - // 모든 skeletonLines와의 교차점 검사 - for (const rawLine of skeletonLines) { - // rawLine은 p1, p2 속성을 가짐 - const rawLineObj = { - x1: rawLine.p1.x, y1: rawLine.p1.y, - x2: rawLine.p2.x, y2: rawLine.p2.y - }; +// 연결이 끊어진 라인들을 찾는 함수 +export const findDisconnectedSkeletonLines = (skeletonLines, baseLines, options = {}) => { + const { + includeDiagonal = true, + includeStraight = true, + minLength = 0 + } = options; - // 선분 교차 검사 - const intersection = findIntersection( - edgeLine.x1, edgeLine.y1, edgeLine.x2, edgeLine.y2, - rawLineObj.x1, rawLineObj.y1, rawLineObj.x2, rawLineObj.y2 - ); - - if (intersection) { - intersections.push({ - x: intersection.x, - y: intersection.y, - edgeIndex: i, - line: rawLine - }); - } - } + if (!skeletonLines?.length) { + return { + disconnectedLines: [], + diagonalLines: [], + straightLines: [], + statistics: { total: 0, diagonal: 0, straight: 0, disconnected: 0 } + }; } - return intersections; -} + const disconnectedLines = []; + const diagonalLines = []; + const straightLines = []; -// Helper function to extend a line to intersect with polygon edges -function extendLineToIntersections(p1, p2, polygonPoints) { - let intersections = []; - const line = { p1, p2 }; + // 점 일치 확인 헬퍼 함수 + const pointsEqual = (p1, p2, epsilon = 0.1) => { + return Math.abs(p1.x - p2.x) < epsilon && Math.abs(p1.y - p2.y) < epsilon; + }; - // Check intersection with each polygon edge - for (let i = 0; i < polygonPoints.length; i++) { - const edgeP1 = polygonPoints[i]; - const edgeP2 = polygonPoints[(i + 1) % polygonPoints.length]; + // baseLine 좌표 추출 + const extractBaseLineCoordinates = (baseLine) => { + const left = baseLine.left || 0; + const top = baseLine.top || 0; + const width = baseLine.width || 0; + const height = baseLine.height || 0; - - } - - if (intersections.length < 2) return null; - - // Sort by distance from p1 - intersections.sort((a, b) => a.distance - b.distance); - - // Return the two farthest intersection points - return { - p1: { x: intersections[0].x, y: intersections[0].y }, - p2: { - x: intersections[intersections.length - 1].x, - y: intersections[intersections.length - 1].y + if (Math.abs(height) < 0.1) { + return { p1: { x: left, y: top }, p2: { x: left + width, y: top } }; + } else if (Math.abs(width) < 0.1) { + return { p1: { x: left, y: top }, p2: { x: left, y: top + height } }; + } else { + return { p1: { x: left, y: top }, p2: { x: left + width, y: top + height } }; } }; -} -function getExtensionIntersection( - startX, startY, // 대각선 시작점 - currentX, currentY, // 대각선 현재 위치 - lineStartX, lineStartY, // 연장할 선의 시작점 - lineEndX, lineEndY // 연장할 선의 끝점 -) { - // 대각선 방향 벡터 - const dx = currentX - startX; - const dy = currentY - startY; + // baseLine에 점이 있는지 확인 + const isPointOnBase = (point) => { + return baseLines?.some(baseLine => { + const coords = extractBaseLineCoordinates(baseLine); + return pointsEqual(point, coords.p1) || pointsEqual(point, coords.p2); + }) || false; + }; - // 연장할 선의 기울기 - const m = (lineEndY - lineStartY) / (lineEndX - lineStartX); + // baseLine과 교차하는지 확인 + const isIntersectingWithBase = (skeletonLine) => { + return baseLines?.some(baseLine => { + const coords = extractBaseLineCoordinates(baseLine); + const intersection = findIntersection( + skeletonLine.p1.x, skeletonLine.p1.y, skeletonLine.p2.x, skeletonLine.p2.y, + coords.p1.x, coords.p1.y, coords.p2.x, coords.p2.y + ); + return intersection !== null; + }) || false; + }; - // 매개변수 t 방정식에서 t를 구하기 위한 식 전개 - // 대각선의 parametric 방정식: x = startX + t*dx, y = startY + t*dy - // 연장할 선 방정식: y = m * (x - lineStartX) + lineStartY - // 이를 대입해 t 구함 - const numerator = m * (lineStartX - startX) + startY - lineStartY; - const denominator = dy - m * dx; + // 라인 타입 확인 + const getLineType = (p1, p2) => { + const dx = Math.abs(p2.x - p1.x); + const dy = Math.abs(p2.y - p1.y); + const tolerance = 0.1; - if (denominator === 0) { - // 평행하거나 일치하여 교점 없음 - return null; - } + if (dy < tolerance) return 'horizontal'; + if (dx < tolerance) return 'vertical'; + return 'diagonal'; + }; - const t = numerator / denominator; + // 라인 길이 계산 + const getLineLength = (p1, p2) => { + return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); + }; - const intersectX = startX + t * dx; - const intersectY = startY + t * dy; + /** + * 연결 상태 확인 함수 + * + * @param {Object} line - 검사할 skeletonLine (p1, p2) + * @param {number} lineIndex - 현재 라인의 인덱스 + * @returns {{isConnected: boolean, p1Connected: boolean, p2Connected: boolean}} 연결되어 있으면 true, 끊어져 있으면 false + * + * 연결 판단 기준: + * 1. p1이 baseLine과 연결되어 있는지 확인 + * 2. p1이 연결되어 있으면 p2가 skeletonLine과 연결되어 있는지 확인 + * 3. p1이 연결되어 있지 않으면 p2가 baseLine과 연결되어 있는지 확인 + * 4. p2도 연결되어 있지 않으면 p1과 p2가 skeletonLine과 연결되어 있는지 확인 + */ + const isConnected = (line, lineIndex) => { + const result= { + isConnected: false, + p1Connected: false, + p2Connected: false, + extendedLine: [] + } + const { p1, p2 } = line; - return { x: intersectX, y: intersectY }; -} + // 1. p1이 baseLine과 연결되어 있는지 확인 + const isP1OnBase = isPointOnBase(p1); + const isP2OnBase = isPointOnBase(p2); -function findMatchingLinePoints(Aline, APolygon, epsilon = 1e-10) { - const { p1, p2 } = Aline; - const matches = []; + if (isP1OnBase || isP2OnBase) { - // 선의 방향 판단 - function getLineDirection(point1, point2, epsilon = 1e-10) { - const deltaX = Math.abs(point1.x - point2.x); - const deltaY = Math.abs(point1.y - point2.y); - if (deltaX < epsilon && deltaY < epsilon) { - return { - type: 'point', - description: '점 (두 좌표가 동일)' - }; - } else if (deltaX < epsilon) { - return { - type: 'vertical', - description: '수직선 (세로)' - }; - } else if (deltaY < epsilon) { - return { - type: 'horizontal', - description: '수평선 (가로)' - }; + // 2. p1 또는 p2가 baseLine과 연결되어 있음 + // -> 연결되지 않은 점이 skeletonLine과 연결되어 있는지 확인 + for (let i = 0; i < skeletonLines.length; i++) { + if (i === lineIndex) continue; // 자기 자신은 제외 + + const otherLine = skeletonLines[i]; + const otherP1 = otherLine.p1; + const otherP2 = otherLine.p2; + + // p2가 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인 + if (pointsEqual(p2, otherP1) || pointsEqual(p2, otherP2)) { + result.p1Connected = true; + result.p2Connected = true; + result.isConnected = true; + // p2가 연결되어 있으므로 전체 라인이 연결됨 + }else{ + result.p1Connected = true; + result.p2Connected = false; + result.isConnected = false; + } + + if (pointsEqual(p1, otherP1) || pointsEqual(p1, otherP2)) { + result.p1Connected = true; + result.p2Connected = true; + // p1이 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인 + result.isConnected = true; + // p1이 연결되어 있으므로 전체 라인이 연결됨 + }else{ + result.p1Connected = true; + result.p2Connected = false; + result.isConnected = false; + } + + } + return result; + } else { - return { - type: 'diagonal', - description: '대각선' - }; - } - } + // 3. p1과 p2 모두 baseLine과 연결되어 있지 않음 + // -> p1과 p2가 skeletonLine과 연결되어 있는지 확인 + let p1Connected = false; // p1이 skeletonLine과 연결되어 있는지 + let p2Connected = false; // p2가 skeletonLine과 연결되어 있는지 - // 선의 방향 정보 계산 - const lineDirection = getLineDirection(p1, p2, epsilon); - APolygon.forEach((point, index) => { - // p1과 비교 - if (Math.abs(p1.x - point.X) < epsilon && Math.abs(p1.y - point.Y) < epsilon) { - matches.push({ - linePoint: 'p1', - polygonIndex: index, - coordinates: { x: point.X, y: point.Y }, - lineDirection: lineDirection, - type: lineDirection.type - }); + for (let i = 0; i < skeletonLines.length; i++) { + if (i === lineIndex) continue; // 자기 자신은 제외 + + const otherLine = skeletonLines[i]; + const otherP1 = otherLine.p1; + const otherP2 = otherLine.p2; + + // p1이 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인 + if (!p1Connected && (pointsEqual(p1, otherP1) || pointsEqual(p1, otherP2))) { + p1Connected = true; + result.p1Connected = true; + }else{ + // p1이 skeletonLine과 연결되지 않음 - baseLine까지 연장 + result.extendedLine = extendToBaseLine(p1, baseLines); + } + +// p2가 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인 + if (!p2Connected && (pointsEqual(p2, otherP1) || pointsEqual(p2, otherP2))) { + p2Connected = true; + result.p2Connected = true; + }else{ + // p2가 skeletonLine과 연결되지 않음 - baseLine까지 연장 + result.extendedLine = extendToBaseLine(p1, baseLines); + } + + // p1과 p2가 모두 연결되어 있으면 전체 라인이 연결됨 + if (p1Connected && p2Connected) { + result.isConnected = true; + } + + + } + + return result } - // p2와 비교 - if (Math.abs(p2.x - point.X) < epsilon && Math.abs(p2.y - point.Y) < epsilon) { - matches.push({ - linePoint: 'p2', - polygonIndex: index, - coordinates: { x: point.X, y: point.Y }, - lineDirection: lineDirection, - type: lineDirection.type + }; + + // 각 라인 분석 + skeletonLines.forEach((line, index) => { + const { p1, p2 } = line; + const length = getLineLength(p1, p2); + const type = getLineType(p1, p2); + + if (length < minLength) return; + + const connected = isConnected(line, index); + const extendedLine = connected.extendedLine; + const p1Connected = connected.p1Connected; + const p2Connected = connected.p2Connected; + + if (type === 'diagonal') { + diagonalLines.push({ line, index, length, type, connected}); + } else { + straightLines.push({ line, index, length, type, connected }); + } + + if (!connected.isConnected) { + disconnectedLines.push({ + line, index, length, type, + isDiagonal: type === 'diagonal', + isHorizontal: type === 'horizontal', + isVertical: type === 'vertical', + p1Connected: p1Connected, + p2Connected: p2Connected, + extendedLine: extendedLine, + + + }); } }); + const filteredDisconnected = includeDiagonal && includeStraight + ? disconnectedLines + : disconnectedLines.filter(item => + (includeDiagonal && item.isDiagonal) || + (includeStraight && (item.isHorizontal || item.isVertical)) + ); + return { - hasMatch: matches.length > 0, - lineDirection: lineDirection, - matches: matches + disconnectedLines: filteredDisconnected, + diagonalLines: includeDiagonal ? diagonalLines : [], + straightLines: includeStraight ? straightLines : [], + statistics: { + total: skeletonLines.length, + diagonal: diagonalLines.length, + straight: straightLines.length, + disconnected: filteredDisconnected.length + } }; -} +}; +const extendToBaseLine = (point, baseLines) => { + // point에서 가장 가까운 baseLine을 찾아서 연장 + let closestBaseLine = null; + let minDistance = Infinity; -function getLineIntersectionParametric(p1, p2, p3, p4) { - const d1 = { x: p2.x - p1.x, y: p2.y - p1.y }; // 첫번째 직선 방향벡터 - const d2 = { x: p4.x - p3.x, y: p4.y - p3.y }; // 두번째 직선 방향벡터 + for (const baseLine of baseLines) { + // point와 baseLine 사이의 거리 계산 + const distance = getDistanceToLine(point, baseLine); - // 평행선 체크 (외적이 0이면 평행) - const cross = d1.x * d2.y - d1.y * d2.x; - if (Math.abs(cross) < Number.EPSILON) { - return null; // 평행선 + if (distance < minDistance) { + minDistance = distance; + closestBaseLine = baseLine; + } } - // 매개변수 t 계산 - const dx = p3.x - p1.x; - const dy = p3.y - p1.y; - const t = (dx * d2.y - dy * d2.x) / cross; + if (closestBaseLine) { + // point에서 closestBaseLine으로 연장하는 라인 생성 + // 연장된 라인을 skeletonLines에 추가 + return { + p1: point, + p2: getProjectionPoint(point, closestBaseLine) + } + } +}; +/** + * 점과 선분 사이의 거리를 계산하는 함수 + * @param {Object} point - 거리를 계산할 점 {x, y} + * @param {Object} line - 선분 {x1, y1, x2, y2} + * @returns {number} 점과 선분 사이의 최단 거리 + */ +const getDistanceToLine = (point, line) => { + const { x: px, y: py } = point; + const { x1, y1, x2, y2 } = line; - // 교점: p1 + t * d1 - return { - x: p1.x + t * d1.x, - y: p1.y + t * d1.y - }; -} \ No newline at end of file + // 선분의 방향 벡터 + const dx = x2 - x1; + const dy = y2 - y1; + const lineLength = Math.sqrt(dx * dx + dy * dy); + + if (lineLength === 0) { + // 선분이 점인 경우 + return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1)); + } + + // 선분의 단위 방향 벡터 + const ux = dx / lineLength; + const uy = dy / lineLength; + + // 점에서 선분 시작점으로의 벡터 + const vx = px - x1; + const vy = py - y1; + + // 투영된 길이 (스칼라 투영) + const projectionLength = vx * ux + vy * uy; + + // 투영점이 선분 범위 내에 있는지 확인 + if (projectionLength >= 0 && projectionLength <= lineLength) { + // 투영점이 선분 내에 있음 + const distance = Math.sqrt(vx * vx + vy * vy - projectionLength * projectionLength); + return distance; + } else { + // 투영점이 선분 밖에 있음 - 끝점까지의 거리 중 작은 값 + const distToStart = Math.sqrt(vx * vx + vy * vy); + const distToEnd = Math.sqrt((px - x2) * (px - x2) + (py - y2) * (py - y2)); + return Math.min(distToStart, distToEnd); + } +}; + +/** + * 점을 선분에 투영한 점을 반환하는 함수 + * @param {Object} point - 투영할 점 {x, y} + * @param {Object} line - 선분 {x1, y1, x2, y2} + * @returns {Object} 투영된 점 {x, y} + */ +const getProjectionPoint = (point, line) => { + const { x: px, y: py } = point; + const { x1, y1, x2, y2 } = line; + + // 선분의 방향 벡터 + const dx = x2 - x1; + const dy = y2 - y1; + const lineLength = Math.sqrt(dx * dx + dy * dy); + + if (lineLength === 0) { + // 선분이 점인 경우 + return { x: x1, y: y1 }; + } + + // 선분의 단위 방향 벡터 + const ux = dx / lineLength; + const uy = dy / lineLength; + + // 점에서 선분 시작점으로의 벡터 + const vx = px - x1; + const vy = py - y1; + + // 투영된 길이 (스칼라 투영) + const projectionLength = vx * ux + vy * uy; + + // 투영점이 선분 범위 내에 있는지 확인 + if (projectionLength >= 0 && projectionLength <= lineLength) { + // 투영점이 선분 내에 있음 + const projX = x1 + ux * projectionLength; + const projY = y1 + uy * projectionLength; + return { x: projX, y: projY }; + } else if (projectionLength < 0) { + // 투영점이 시작점 앞에 있음 + return { x: x1, y: y1 }; + } else { + // 투영점이 끝점 뒤에 있음 + return { x: x2, y: y2 }; + } +}; \ No newline at end of file -- 2.47.2 From eeab13b9cdbbfd89c2564f763387d5c4199fb3c7 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 24 Sep 2025 10:02:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=B4=88=EA=B8=B0ALL=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/basic/step/Orientation.jsx | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/floor-plan/modal/basic/step/Orientation.jsx b/src/components/floor-plan/modal/basic/step/Orientation.jsx index 976e2cd1..96fbc6e4 100644 --- a/src/components/floor-plan/modal/basic/step/Orientation.jsx +++ b/src/components/floor-plan/modal/basic/step/Orientation.jsx @@ -77,7 +77,7 @@ export const Orientation = forwardRef((props, ref) => { }; useEffect(() => { - if (basicSetting.roofSizeSet == '3') { + if (basicSetting.roofSizeSet === '3') { restoreModuleInstArea() } }, []) @@ -187,7 +187,7 @@ export const Orientation = forwardRef((props, ref) => { title: getMessage('module.not.found'), icon: 'warning', }) - return + } } } @@ -250,8 +250,17 @@ export const Orientation = forwardRef((props, ref) => { // 필터링된 목록의 첫 번째 모듈을 자동 선택 if (filtered.length > 0) { - setSelectedModules(filtered[0]) + const firstModule = filtered[0] + setSelectedModules(firstModule) + // 상위 컴포넌트의 handleChangeModule 호출 + if (handleChangeModule) { + handleChangeModule(firstModule) + } } + } else { + // 모듈 리스트가 비어있는 경우 + setFilteredModuleList([]) + setSelectedModules(null) } } @@ -342,10 +351,14 @@ export const Orientation = forwardRef((props, ref) => { setSelectedModuleSeries(currentSeries) } else { setSelectedModuleSeries(allOption) + // "ALL"이 선택되면 자동으로 모듈 필터링 및 선택 실행 + setTimeout(() => handleChangeModuleSeries(allOption), 0) } } else { // 선택된 모듈이 없으면 "전체"를 기본 선택 setSelectedModuleSeries(allOption) + // "ALL"이 선택되면 자동으로 모듈 필터링 및 선택 실행 + setTimeout(() => handleChangeModuleSeries(allOption), 0) } } } @@ -369,6 +382,9 @@ export const Orientation = forwardRef((props, ref) => { if (filtered.length > 0 && !selectedModules) { setSelectedModules(filtered[0]) } + } else if (moduleList.length === 0 && filteredModuleList.length === 0 && selectedModuleSeries) { + // 모듈 리스트가 비어있는 경우 빈 배열로 설정 + setFilteredModuleList([]) } }, [moduleList, selectedModuleSeries]); return ( @@ -462,6 +478,7 @@ export const Orientation = forwardRef((props, ref) => { sourceKey={'itemId'} showKey={'itemNm'} onChange={(e) => handleChangeModule(e)} + showFirstOptionWhenEmpty = {true} /> )} @@ -512,7 +529,7 @@ export const Orientation = forwardRef((props, ref) => { - {basicSetting && basicSetting.roofSizeSet == '3' && ( + {basicSetting && basicSetting.roofSizeSet === '3' && (
{getMessage('modal.module.basic.setting.module.placement.area')}
@@ -523,7 +540,7 @@ export const Orientation = forwardRef((props, ref) => { )}
- {basicSetting && basicSetting.roofSizeSet != '3' && ( + {basicSetting && basicSetting.roofSizeSet !== '3' && (
-- 2.47.2