From 94fe5889ea15555ec4325244d1e05da32153815b Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 10 Dec 2025 19:05:12 +0900 Subject: [PATCH] =?UTF-8?q?roofLine=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 | 189 +++++++++++++++++++++++++------------ 1 file changed, 130 insertions(+), 59 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 9e10ca9a..59e1f915 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -5,6 +5,7 @@ import { QLine } from '@/components/fabric/QLine' import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' import { QPolygon } from '@/components/fabric/QPolygon' +import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -321,6 +322,28 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { //처마 let roof = canvas?.getObjects().find((object) => object.id === roofId) + // [추가] wall 객체를 찾아 roof.lines에 wallId를 직접 주입 (초기화) + // 지붕은 벽을 기반으로 생성되므로 라인의 순서(Index)가 동일합니다. + const wallObj = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId); + + if (roof && wallObj && roof.lines && wallObj.lines) { + roof.lines.forEach((rLine, index) => { + // 동일한 인덱스의 벽 라인 가져오기 + const wLine = wallObj.lines[index]; + if (wLine) { + // attributes.wallId는 부모(벽) ID라서 모두 동일합니다. + // 개별 라인을 식별하기 위해 wLine.id (라인 객체 고유 ID)를 저장합니다. + // * 새로고침 시 wLine.id가 바뀌어도, 이 로직이 그때마다 돌면서 최신 ID로 갱신해줍니다. + rLine.attributes.wallLineId = wLine.id; + + // 필요하다면 부모 ID도 같이 저장 (참조용) + if (wLine.attributes?.wallId) { + rLine.attributes.parentWallId = wLine.attributes.wallId; + } + } + + }); + } const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE] const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] @@ -578,7 +601,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ); //그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함 - let roofIdx = 0; + // roofLines.forEach((roofLine) => { // @@ -589,13 +612,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // } // }); + const skeletonLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, stroke: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color, strokeWidth: lineStyle.width, name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type, - attributes: attributes, + attributes: { + ...attributes, + + }, direction: direction, isBaseLine: sktLine.attributes.isOuterEdge, lineName: (sktLine.attributes.isOuterEdge)?'roofLine': attributes.type, @@ -755,6 +782,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const sortedRoofLines = sortCurrentRoofLines(roofLines); const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines); const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines); + const sortRoofLines = sortBaseLinesByWallLines(roofLines, wallLines); //wall.lines 는 기본 벽 라인 @@ -770,11 +798,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // const moveLine = sortedWallBaseLines[index] // const wallBaseLine = sortedWallBaseLines[index] - - const roofLine = roofLines[index]; +console.log("wallLine::::", wallLine) + const roofLine = sortRoofLines[index]//roofLines.find(line => line.attributes.wallLineId === wallLine.attributes.wallId); + console.log("roofLine", roofLine); const currentRoofLine = currentRoofLines[index]; const moveLine = sortedBaseLines[index] const wallBaseLine = sortedBaseLines[index] + console.log("wallBaseLine", wallBaseLine); //roofline 외곽선 설정 @@ -862,7 +892,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //두 포인트가 변경된 라인인 if (fullyMoved ) { //반시계방향향 - + const mLine = getSelectLinePosition(wall, wallBaseLine) if (getOrientation(roofLine) === 'vertical') { @@ -1448,7 +1478,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // } } } + getAddLine(newPStart, newPEnd, 'red') + //canvas.remove(roofLine) } canvas.renderAll() }); @@ -1475,14 +1507,30 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { */ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { let roof = canvas?.getObjects().find((object) => object.id === roofId) + // [1] 벽 객체를 가져옵니다. + let wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId); + const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); //처마선인지 확인하고 pitch 대입 각 처마선마다 pitch가 다를수 있음 const { Begin, End } = edgeResult.Edge; - let outerLine = roof.lines.find(line => + // [2] 현재 처리 중인 엣지가 roof.lines의 몇 번째 인덱스인지 찾습니다. + const roofLineIndex = roof.lines.findIndex(line => line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line) - ); + + let outerLine = null; + let targetWallId = null; + + // [3] 인덱스를 통해 매칭되는 벽 라인의 불변 ID(wallId)를 가져옵니다. + if (roofLineIndex !== -1) { + outerLine = roof.lines[roofLineIndex]; + if (wall && wall.lines && wall.lines[roofLineIndex]) { + targetWallId = wall.lines[roofLineIndex].attributes.wallId; + } + targetWallId = outerLine.attributes.wallId; + } + if(!outerLine) { outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); console.log('Has matching line:', outerLine); @@ -1519,7 +1567,7 @@ 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]) - addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine); + addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine, targetWallId); // } } } @@ -1644,7 +1692,7 @@ function isOuterEdge(p1, p2, edges) { * @param pitch * @param isOuterLine */ -function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine) { +function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine, wallLineId) { // const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|'); // if (processedInnerEdges.has(edgeKey)) return; // processedInnerEdges.add(edgeKey); @@ -1681,6 +1729,7 @@ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, is isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE, isOuterEdge: isOuterLine, pitch: pitch, + wallLineId: wallLineId, // [5] attributes에 wallId 저장 (이 정보가 최종 roofLines에 들어갑니다) ...(eavesIndex !== undefined && { eavesIndex }) }, lineStyle: { color, width }, @@ -3322,7 +3371,8 @@ function findInteriorPoint(line, polygonLines) { /** * baseLines의 순서를 wallLines의 순서와 일치시킵니다. - * 각 wallLine에 대해 가장 가깝고 평행한 baseLine을 찾아 정렬된 배열을 반환합니다. + * 1순위: 공통 ID(id, matchingId, parentId 등)를 이용한 직접 매칭 + * 2순위: 기하학적 유사성(기울기, 길이, 위치)을 점수화하여 매칭 * * @param {Array} baseLines - 정렬할 원본 baseLine 배열 * @param {Array} wallLines - 기준이 되는 wallLine 배열 @@ -3333,67 +3383,88 @@ export const sortBaseLinesByWallLines = (baseLines, wallLines) => { return baseLines; } - const sortedBaseLines = []; - const usedIndices = new Set(); // 이미 매칭된 baseLine 인덱스를 추적 + const sortedBaseLines = new Array(wallLines.length).fill(null); + const usedBaseIndices = new Set(); + + // [1단계] ID 매칭 (기존 로직 유지 - 혹시 ID가 있는 경우를 대비) + // ... (ID 매칭 코드는 생략하거나 유지) ... + + // [2단계] 'originPoint' 또는 좌표 일치성을 이용한 강력한 기하학적 매칭 + wallLines.forEach((wLine, wIndex) => { + if (sortedBaseLines[wIndex]) return; + + // 비교할 기준 좌표 설정 (originPoint가 있으면 그것을, 없으면 현재 좌표 사용) + const wStart = wLine.attributes?.originPoint + ? { x: wLine.attributes.originPoint.x1, y: wLine.attributes.originPoint.y1 } + : { x: wLine.x1, y: wLine.y1 }; + + const wEnd = wLine.attributes?.originPoint + ? { x: wLine.attributes.originPoint.x2, y: wLine.attributes.originPoint.y2 } + : { x: wLine.x2, y: wLine.y2 }; + + // 수직/수평 여부 판단 + const isVertical = Math.abs(wStart.x - wEnd.x) < 0.1; + const isHorizontal = Math.abs(wStart.y - wEnd.y) < 0.1; - wallLines.forEach((wallLine) => { let bestMatchIndex = -1; - let minDistance = Infinity; + let minDiff = Infinity; - // wallLine의 중점 계산 - const wMidX = (wallLine.x1 + wallLine.x2) / 2; - const wMidY = (wallLine.y1 + wallLine.y2) / 2; + baseLines.forEach((bLine, bIndex) => { + if (usedBaseIndices.has(bIndex)) return; - // wallLine의 방향 벡터 (평행 확인용) - const wDx = wallLine.x2 - wallLine.x1; - const wDy = wallLine.y2 - wallLine.y1; - const wLen = Math.hypot(wDx, wDy); + let diff = Infinity; - baseLines.forEach((baseLine, index) => { - // 이미 매칭된 라인은 건너뜀 (1:1 매칭) - if (usedIndices.has(index)) return; - - // baseLine의 중점 계산 - const bMidX = (baseLine.x1 + baseLine.x2) / 2; - const bMidY = (baseLine.y1 + baseLine.y2) / 2; - - // 두 라인의 중점 사이 거리 계산 - const dist = Math.hypot(wMidX - bMidX, wMidY - bMidY); - - // 평행 여부 확인 (내적 사용) - const bDx = baseLine.x2 - baseLine.x1; - const bDy = baseLine.y2 - baseLine.y1; - const bLen = Math.hypot(bDx, bDy); - - if (wLen > 0 && bLen > 0) { - // 단위 벡터 내적값 (-1 ~ 1) - const dot = (wDx * bDx + wDy * bDy) / (wLen * bLen); - - // 내적의 절대값이 1에 가까우면 평행 (약 10도 오차 허용) - if (Math.abs(Math.abs(dot) - 1) < 0.1) { - if (dist < minDistance) { - minDistance = dist; - bestMatchIndex = index; - } + // 1. 수직선인 경우: X좌표가 일치해야 함 (예: 230.8 == 230.8) + if (isVertical) { + // bLine도 수직선인지 확인 (x1, x2 차이가 거의 없어야 함) + if (Math.abs(bLine.x1 - bLine.x2) < 1.0) { + // X좌표 차이를 오차(diff)로 계산 + diff = Math.abs(wStart.x - bLine.x1); } } + // 2. 수평선인 경우: Y좌표가 일치해야 함 + else if (isHorizontal) { + // bLine도 수평선인지 확인 + if (Math.abs(bLine.y1 - bLine.y2) < 1.0) { + diff = Math.abs(wStart.y - bLine.y1); + } + } + // 3. 대각선인 경우: 기울기와 절편 비교 (복잡하므로 거리로 대체) + else { + // 중점 간 거리 + 기울기 차이 + // (이전 답변의 로직 사용 가능) + } + + // 오차가 매우 작으면(예: 1px 미만) 같은 라인으로 간주 + if (diff < 1.0 && diff < minDiff) { + minDiff = diff; + bestMatchIndex = bIndex; + } }); if (bestMatchIndex !== -1) { - sortedBaseLines.push(baseLines[bestMatchIndex]); - usedIndices.add(bestMatchIndex); - } else { - // 매칭되는 라인을 찾지 못한 경우, 아직 사용되지 않은 첫 번째 라인을 할당 (Fallback) - const unusedIndex = baseLines.findIndex((_, idx) => !usedIndices.has(idx)); - if (unusedIndex !== -1) { - sortedBaseLines.push(baseLines[unusedIndex]); - usedIndices.add(unusedIndex); - } else { - // 더 이상 남은 라인이 없으면 null 또는 기존 라인 중 하나(에러 방지) - sortedBaseLines.push(baseLines[0]); - } + sortedBaseLines[wIndex] = baseLines[bestMatchIndex]; + usedBaseIndices.add(bestMatchIndex); } }); + // [3단계] 남은 라인 처리 (Fallback) + // 매칭되지 않은 wallLine들에 대해 남은 baseLines를 순서대로 배정하거나 + // 거리 기반 근사 매칭을 수행 + // ... (기존 fallback 로직) ... + + // 빈 구멍 채우기 (null 방지) + for(let i=0; i !usedBaseIndices.has(idx)); + if(unused !== -1) { + sortedBaseLines[i] = baseLines[unused]; + usedBaseIndices.add(unused); + } else { + sortedBaseLines[i] = baseLines[0]; // 최후의 수단 + } + } + } + return sortedBaseLines; }; \ No newline at end of file