From 28d26511de36f6d8e41cf2370f5a7de3d051c7aa Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 5 Dec 2025 15:36:56 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=EB=B3=B5=EC=82=AC=20=ED=9B=84=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useCommonUtils.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/hooks/common/useCommonUtils.js b/src/hooks/common/useCommonUtils.js index c878bafd..9fe5a221 100644 --- a/src/hooks/common/useCommonUtils.js +++ b/src/hooks/common/useCommonUtils.js @@ -648,6 +648,7 @@ export function useCommonUtils() { lockMovementY: true, name: obj.name, editable: false, + selectable: true, // 복사된 객체 선택 가능하도록 설정 id: uuidv4(), //복사된 객체라 새로 따준다 }) @@ -656,19 +657,25 @@ export function useCommonUtils() { //배치면일 경우 if (obj.name === 'roof') { - clonedObj.setCoords() - clonedObj.fire('modified') - // clonedObj.fire('polygonMoved') + clonedObj.canvas = canvas // canvas 참조 설정 clonedObj.set({ direction: obj.direction, directionText: obj.directionText, roofMaterial: obj.roofMaterial, + stroke: 'black', // 복사된 객체는 선택 해제 상태의 색상으로 설정 + selectable: true, // 선택 가능하도록 설정 + evented: true, // 마우스 이벤트를 받을 수 있도록 설정 + isFixed: false, // containsPoint에서 특별 처리 방지 }) obj.lines.forEach((line, index) => { clonedObj.lines[index].set({ attributes: line.attributes }) }) + clonedObj.fire('polygonMoved') // 내부 좌표 재계산 (points, pathOffset) + clonedObj.fire('modified') + clonedObj.setCoords() // 모든 속성 설정 후 좌표 업데이트 + canvas.setActiveObject(clonedObj) canvas.renderAll() addLengthText(clonedObj) //수치 추가 drawDirectionArrow(clonedObj) //방향 화살표 추가 From 8943ab2f38a1e04384f1a51a975e46e7212d11d1 Mon Sep 17 00:00:00 2001 From: yscha Date: Sun, 7 Dec 2025 02:44:10 +0900 Subject: [PATCH 02/14] v0.2 --- .gitignore | 3 +- src/util/skeleton-utils.js | 2071 +++++++++++++++++++++++------------- 2 files changed, 1344 insertions(+), 730 deletions(-) diff --git a/.gitignore b/.gitignore index f3b61bd7..a235b0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ next-env.d.ts yarn.lock package-lock.json pnpm-lock.yaml -certificates \ No newline at end of file +certificates +.ai \ No newline at end of file diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 95c5cb7b..80763cfa 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -4,9 +4,7 @@ import { calcLineActualSize, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygo import { QLine } from '@/components/fabric/QLine' import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' -import { line } from 'framer-motion/m' import { QPolygon } from '@/components/fabric/QPolygon' -import { point } from '@turf/turf' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -15,7 +13,7 @@ import { point } from '@turf/turf' * @param {string} textMode - 텍스트 표시 모드 * @param pitch */ - +const EPSILON = 0.1 export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { @@ -185,127 +183,127 @@ const movingLineFromSkeleton = (roofId, canvas) => { let newEndPoint = {...originalEndPoint}; // 위치와 방향에 따라 좌표 조정 -/* - switch (position) { - case 'left': - if (moveDirection === 'up') { - newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber(); - newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber(); - } else if (moveDirection === 'down') { - newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber(); - newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber(); - } - break; - case 'right': - if (moveDirection === 'up') { - newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber(); - newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber(); - } else if (moveDirection === 'down') { - newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber(); - newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber(); - } - break; - case 'top': - if (moveDirection === 'up') { - newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber(); - newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber(); - } else if (moveDirection === 'down') { - newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber(); - newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber(); - } - break; - case 'bottom': - if (moveDirection === 'up') { - newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber(); - newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber(); - } else if (moveDirection === 'down') { - newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber(); - newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber(); - } - break; - } -*/ + /* + switch (position) { + case 'left': + if (moveDirection === 'up') { + newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber(); + } + break; + case 'right': + if (moveDirection === 'up') { + newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber(); + } + break; + case 'top': + if (moveDirection === 'up') { + newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber(); + } + break; + case 'bottom': + if (moveDirection === 'up') { + newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber(); + } + break; + } + */ // 원본 라인 업데이트 - // newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트 + // newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트 console.log('absMove::', absMove); - newPoints.forEach((point, index) => { - if(position === 'bottom'){ - if (moveDirection === 'in') { - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.y = Big(point.y).minus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).minus(absMove).toNumber(); - } - }else if (moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.y = Big(point.y).plus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).plus(absMove).toNumber(); - } + newPoints.forEach((point, index) => { + if(position === 'bottom'){ + if (moveDirection === 'in') { + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).minus(absMove).toNumber(); } - - }else if (position === 'top'){ - if(moveDirection === 'in'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.y = Big(point.y).plus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).plus(absMove).toNumber(); - } - }else if(moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.y = Big(point.y).minus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).minus(absMove).toNumber(); - } + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.y = Big(point.y).minus(absMove).toNumber(); + // } + }else if (moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).plus(absMove).toNumber(); } - - }else if(position === 'left'){ - if(moveDirection === 'in'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.x = Big(point.x).plus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).plus(absMove).toNumber(); - } - }else if(moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.x = Big(point.x).minus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).minus(absMove).toNumber(); - } - } - - }else if(position === 'right'){ - if(moveDirection === 'in'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.x = Big(point.x).minus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).minus(absMove).toNumber(); - } - }else if(moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.x = Big(point.x).plus(absMove).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).plus(absMove).toNumber(); - } - } - + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.y = Big(point.y).plus(absMove).toNumber(); + // } } - }); + }else if (position === 'top'){ + if(moveDirection === 'in'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.y = Big(point.y).plus(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).plus(absMove).toNumber(); + } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.y = Big(point.y).minus(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).minus(absMove).toNumber(); + } + } + + }else if(position === 'left'){ + if(moveDirection === 'in'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).plus(absMove).toNumber(); + } + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.x = Big(point.x).plus(absMove).toNumber(); + // } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).minus(absMove).toNumber(); + } + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.x = Big(point.x).minus(absMove).toNumber(); + // } + } + + }else if(position === 'right'){ + if(moveDirection === 'in'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.x = Big(point.x).minus(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).minus(absMove).toNumber(); + } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.x = Big(point.x).plus(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).plus(absMove).toNumber(); + } + } + + } - // 원본 baseLine도 업데이트 - line.startPoint = newStartPoint; - line.endPoint = newEndPoint; }); - return newPoints; + + // 원본 baseLine도 업데이트 + line.startPoint = newStartPoint; + line.endPoint = newEndPoint; + }); + return newPoints; } } @@ -404,6 +402,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { canvas.skeleton.lastPoints = points canvas.set("skeleton", cleanSkeleton); canvas.renderAll() + + console.log('skeleton rendered.', canvas); } catch (e) { console.error('스켈레톤 생성 중 오류 발생:', e) @@ -427,9 +427,61 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if (!skeleton?.Edges) return [] let roof = canvas?.getObjects().find((object) => object.id === roofId) + let wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) let skeletonLines = [] + let findPoints = []; + const processedInnerEdges = new Set() + const textElements = {}; + + const coordinateText = (line) => { + // Generate a stable ID for this line + const lineKey = `${line.x1},${line.y1},${line.x2},${line.y2}`; + + // Remove existing text elements for this line + if (textElements[lineKey]) { + textElements[lineKey].forEach(text => { + if (canvas.getObjects().includes(text)) { + canvas.remove(text); + } + }); + } + + // Create start point text + const startText = new fabric.Text(`(${Math.round(line.x1)}, ${Math.round(line.y1)})`, { + left: line.x1 + 5, + top: line.y1 - 20, + fontSize: 10, + fill: 'magenta', + fontFamily: 'Arial', + selectable: false, + hasControls: false, + hasBorders: false + }); + + // Create end point text + const endText = new fabric.Text(`(${Math.round(line.x2)}, ${Math.round(line.y2)})`, { + left: line.x2 + 5, + top: line.y2 - 20, + fontSize: 10, + fill: 'orange', + fontFamily: 'Arial', + selectable: false, + hasControls: false, + hasBorders: false + }); + + // Add to canvas + canvas.add(startText, endText); + + // Store references + textElements[lineKey] = [startText, endText]; + + // Bring lines to front + canvas.bringToFront(startText); + canvas.bringToFront(endText); + }; // 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다. skeleton.Edges.forEach((edgeResult, index) => { @@ -470,41 +522,46 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } }); -/* - //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. - const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); + /* + //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. + const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); - if(disconnectedLines.length > 0) { + if(disconnectedLines.length > 0) { - disconnectedLines.forEach(dLine => { - const { index, extendedLine, p1Connected, p2Connected } = dLine; - const newPoint = extendedLine?.point; - if (!newPoint) return; - // p1이 끊어졌으면 p1을, p2가 끊어졌으면 p2를 연장된 지점으로 업데이트 - if (p1Connected) { //p2 연장 - skeletonLines[index].p2 = { ...skeletonLines[index].p2, x: newPoint.x, y: newPoint.y }; - } else if (p2Connected) {//p1 연장 - skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y }; - } - }); + disconnectedLines.forEach(dLine => { + const { index, extendedLine, p1Connected, p2Connected } = dLine; + const newPoint = extendedLine?.point; + if (!newPoint) return; + // p1이 끊어졌으면 p1을, p2가 끊어졌으면 p2를 연장된 지점으로 업데이트 + if (p1Connected) { //p2 연장 + skeletonLines[index].p2 = { ...skeletonLines[index].p2, x: newPoint.x, y: newPoint.y }; + } else if (p2Connected) {//p1 연장 + skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y }; + } + }); - //2-1 확장된 스켈레톤 선이 연장되다가 서로 만나면 만난점(접점)에서 멈추어야 된다. - trimIntersectingExtendedLines(skeletonLines, disconnectedLines); + //2-1 확장된 스켈레톤 선이 연장되다가 서로 만나면 만난점(접점)에서 멈추어야 된다. + trimIntersectingExtendedLines(skeletonLines, disconnectedLines); - } -*/ + } + */ //2. 연결이 끊어진 라인이 있을경우 찾아서 추가한다(동 이동일때) // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다. const innerLines = []; + const addLines = [] const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set + //처마라인 + const roofLines = roof.lines + //벽라인 + const wallLines = wall.lines - skeletonLines.forEach(line => { - const { p1, p2, attributes, lineStyle } = line; + skeletonLines.forEach((sktLine, skIndex) => { + let { p1, p2, attributes, lineStyle } = sktLine; - // 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교) + // 중복방지 - 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교) const lineKey = [ [p1.x, p1.y].sort().join(','), [p2.x, p2.y].sort().join(',') @@ -516,181 +573,826 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } const direction = getLineDirection( - { x: line.p1.x, y: line.p1.y }, - { x: line.p2.x, y: line.p2.y } + { x: sktLine.p1.x, y: sktLine.p1.y }, + { x: sktLine.p2.x, y: sktLine.p2.y } ); - const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], { + //그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함 + let roofIdx = 0; + + // roofLines.forEach((roofLine) => { + // + // if (isSameLine(p1.x, p1.y, p2.x, p2.y, roofLine) || isSameLine(p2.x, p2.y, p1.x, p1.y, roofLine)) { + // roofIdx = roofLine.idx; + // console.log("roofIdx::::::", roofIdx) + // return false; // forEach 중단 + // } + // }); + + const skeletonLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, - stroke: lineStyle.color, + stroke: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color, strokeWidth: lineStyle.width, - name: (line.attributes.isOuterEdge)?'eaves': attributes.type, + name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type, attributes: attributes, direction: direction, - isBaseLine: line.attributes.isOuterEdge, - lineName: (line.attributes.isOuterEdge)?'outerLine': attributes.type, - selectable:(!line.attributes.isOuterEdge), - roofId: roofId, + isBaseLine: sktLine.attributes.isOuterEdge, + lineName: (sktLine.attributes.isOuterEdge)?'roofLine': attributes.type, + selectable:(!sktLine.attributes.isOuterEdge), + //visible: (!sktLine.attributes.isOuterEdge), }); + coordinateText(skeletonLine) + canvas.add(skeletonLine); + skeletonLine.bringToFront(); + existingLines.add(lineKey); // 추가된 라인을 추적 + + + //skeleton 라인에서 처마선은 삭제 - if(innerLine.lineName !== 'outerLine'){ - canvas.add(innerLine); - innerLine.bringToFront(); - existingLines.add(lineKey); // 추가된 라인을 추적 + if(skeletonLine.lineName === 'roofLine'){ + + skeletonLine.set('visible', false); //임시 + roof.set({ + //stroke: 'black', + strokeWidth: 4 + }); + + + + + }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', - selectable: true, - lockMovementX: false, - lockMovementY: false, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - name: 'lengthText' - }) - - canvas?.add(coordinateText) } - innerLines.push(innerLine) + + innerLines.push(skeletonLine) canvas.renderAll(); }); + if((roof.moveUpDown??0 > 0) ) { + + // 같은 라인이 없으므로 새 다각형 라인 생성 + //라인 편집 + // let i = 0 + const currentRoofLines = canvas.getObjects().filter((obj) => obj.lineName === 'roofLine' && obj.attributes.roofId === roofId) + let roofLineRects = canvas.getObjects().filter((obj) => obj.name === 'roofLineRect' && obj.roofId === roofId) + + + roofLineRects.forEach((roofLineRect) => { + canvas.remove(roofLineRect) + canvas.renderAll() + }) + + let helpLines = canvas.getObjects().filter((obj) => obj.lineName === 'helpLine' && obj.roofId === roofId) + helpLines.forEach((helpLine) => { + canvas.remove(helpLine) + canvas.renderAll() + }) + + function sortCurrentRoofLines(lines) { + return [...lines].sort((a, b) => { + // Get all coordinates in a consistent order + const getCoords = (line) => { + const x1 = line.x1 ?? line.get('x1'); + const y1 = line.y1 ?? line.get('y1'); + const x2 = line.x2 ?? line.get('x2'); + const y2 = line.y2 ?? line.get('y2'); + + // Sort points left-to-right, then top-to-bottom + return x1 < x2 || (x1 === x2 && y1 < y2) + ? [x1, y1, x2, y2] + : [x2, y2, x1, y1]; + }; + + const aCoords = getCoords(a); + const bCoords = getCoords(b); + + // Compare each coordinate in order + for (let i = 0; i < 4; i++) { + if (Math.abs(aCoords[i] - bCoords[i]) > 0.1) { + return aCoords[i] - bCoords[i]; + } + } + return 0; + }); + } + + + // function sortCurrentRoofLines(lines) { + // return [...lines].sort((a, b) => { + // const aX = a.x1 ?? a.get('x1') + // const aY = a.y1 ?? a.get('y1') + // const bX = b.x1 ?? b.get('x1') + // const bY = b.y1 ?? b.get('y1') + + // if (aX !== bX) return aX - bX + // return aY - bY + // }) + // } + + + // 각 라인 집합 정렬 + + // roofLines의 방향에 맞춰 currentRoofLines의 방향을 조정 + const alignLineDirection = (sourceLines, targetLines) => { + return sourceLines.map(sourceLine => { + // 가장 가까운 targetLine 찾기 + const nearestTarget = targetLines.reduce((nearest, targetLine) => { + const sourceCenter = { + x: (sourceLine.x1 + sourceLine.x2) / 2, + y: (sourceLine.y1 + sourceLine.y2) / 2 + }; + const targetCenter = { + x: (targetLine.x1 + targetLine.x2) / 2, + y: (targetLine.y1 + targetLine.y2) / 2 + }; + const distance = Math.hypot( + sourceCenter.x - targetCenter.x, + sourceCenter.y - targetCenter.y + ); + + return !nearest || distance < nearest.distance + ? { line: targetLine, distance } + : nearest; + }, null)?.line; + + if (!nearestTarget) return sourceLine; + + // 방향이 반대인지 확인 (벡터 내적을 사용) + const sourceVec = { + x: sourceLine.x2 - sourceLine.x1, + y: sourceLine.y2 - sourceLine.y1 + }; + const targetVec = { + x: nearestTarget.x2 - nearestTarget.x1, + y: nearestTarget.y2 - nearestTarget.y1 + }; + + const dotProduct = sourceVec.x * targetVec.x + sourceVec.y * targetVec.y; + + // 내적이 음수이면 방향이 반대이므로 뒤집기 + if (dotProduct < 0) { + return { + ...sourceLine, + x1: sourceLine.x2, + y1: sourceLine.y2, + x2: sourceLine.x1, + y2: sourceLine.y1 + }; + } + + return sourceLine; + }); + }; + + const sortedWallLines = sortCurrentRoofLines(wall.lines); + // roofLines의 방향에 맞춰 currentRoofLines 조정 후 정렬 + const alignedCurrentRoofLines = alignLineDirection(currentRoofLines, roofLines); + const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines); + const sortedRoofLines = sortCurrentRoofLines(roofLines); + const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines); + + + //wall.lines 는 기본 벽 라인 + //wall.baseLine은 움직인라인 + const movedLines = [] + + + wallLines.forEach((wallLine, index) => { + + + // const roofLine = sortedRoofLines[index]; + // const currentRoofLine = sortedCurrentRoofLines[index]; + // const moveLine = sortedWallBaseLines[index] + // const wallBaseLine = sortedWallBaseLines[index] + + + const roofLine = roofLines[index]; + const currentRoofLine = currentRoofLines[index]; + const moveLine = wall.baseLines[index] + const wallBaseLine = wall.baseLines[index] + + //roofline 외곽선 설정 + + + // Check if wallBaseLine is inside the polygon formed by sortedWallLines + + /* + console.log('=== Line Coordinates ==='); + console.table({ + 'Point' : ['X', 'Y'], + 'roofLine' : [roofLine.x1, roofLine.y1], + 'currentRoofLine': [currentRoofLine.x1, currentRoofLine.y1], + 'moveLine' : [moveLine.x1, moveLine.y1], + 'wallBaseLine' : [wallBaseLine.x1, wallBaseLine.y1] + }); + console.log('End Points:'); + console.table({ + 'Point' : ['X', 'Y'], + 'roofLine' : [roofLine.x2, roofLine.y2], + 'currentRoofLine': [currentRoofLine.x2, currentRoofLine.y2], + 'moveLine' : [moveLine.x2, moveLine.y2], + 'wallBaseLine' : [wallBaseLine.x2, wallBaseLine.y2] + }); + */ + const origin = moveLine.attributes?.originPoint + if (!origin) return + + if (isSamePoint(moveLine, wallLine)) { + + return false + } + + const movedStart = Math.abs(moveLine.x1 - wallLine.x1) > EPSILON || Math.abs(moveLine.y1 - origin.y1) > EPSILON + const movedEnd = Math.abs(moveLine.x2 - wallLine.x2) > EPSILON || Math.abs(moveLine.y2 - origin.y2) > EPSILON + + + const fullyMoved = movedStart && movedEnd + + +//반시계 방향 + let newPStart //= {x:roofLine.x1, y:roofLine.y1} + let newPEnd //= {x:movedLines.x2, y:movedLines.y2} + +//현재 roof는 무조건 시계방향 + + const getAddLine = (p1, p2, stroke = '') => { + movedLines.push({ index, p1, p2 }) + +// Usage: + // let mergeLines = mergeMovedLines(movedLines); + //console.log("mergeLines:::::::", mergeLines); + const line = new QLine([p1.x, p1.y, p2.x, p2.y], { + parentId : roof.id, + fontSize : roof.fontSize, + stroke : stroke, + strokeWidth: 4, + name : 'eaveHelpLine', + lineName : 'eaveHelpLine', + selectable : true, + visible : true, + roofId : roofId, + attributes : { + type: 'eaveHelpLine', + isStart : true + } + }); + coordinateText(line) + canvas.add(line) + canvas.renderAll(); + return line + } + + getAddLine(roofLine.startPoint, roofLine.endPoint, ) + + newPStart = { x: roofLine.x1, y: roofLine.y1 } + newPEnd = { x: roofLine.x2, y: roofLine.y2 } + + const getInnerLines = (lines, point) => { + + } + let isIn = false + let isOut = false + +//두 포인트가 변경된 라인인 + if (fullyMoved ) { + //반시계방향향 + console.log("moveFully:::::::::::::", wallBaseLine, newPStart, newPEnd) + console.log("moveFully:::::::::::::", roofLine.direction) + const mLine = getSelectLinePosition(wall, wallBaseLine) + + if (getOrientation(roofLine) === 'vertical') { + + if (['left', 'right'].includes(mLine.position)) { + if(wallLine.x1 === wallBaseLine.x1) { + return false + } + const positionType = + (mLine.position === 'left' && wallLine.x1 < wallBaseLine.x1) || + (mLine.position === 'right' && wallLine.x1 > wallBaseLine.x1) + ? 'in' : 'out'; + const condition = `${mLine.position}_${positionType}`; + let isStartEnd = findInteriorPoint(wallBaseLine, wall.baseLines) + let sPoint, ePoint; + if(condition === 'left_in') { + isIn = true + + if (isStartEnd.start ) { + newPEnd.y = roofLine.y2; + newPEnd.x = roofLine.x2; + + const moveDist = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + ePoint = {x: wallBaseLine.x1, y: wallBaseLine.y1}; + newPStart.y = wallBaseLine.y1 + + findPoints.push({ x: ePoint.x, y: ePoint.y }); + const newPointX = Big(roofLine.x1).plus(moveDist).abs().toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() + let idx = (0 > index - 1)?roofLines.length:index + const pLineX = roofLines[idx-1].x1 + + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + + if(Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + } + + if(isStartEnd.end) { + newPStart.y = roofLine.y1; + newPStart.x = roofLine.x1; + + const moveDist = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + ePoint = {x: wallBaseLine.x2, y: wallBaseLine.y2}; + newPEnd.y = wallBaseLine.y2 + + findPoints.push({ x: ePoint.x, y: ePoint.y }); + const newPointX = Big(roofLine.x1).plus(moveDist).toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y2).minus(0).abs().toNumber() + let idx = (roofLines.length < index + 1)?0:index + const pLineX = roofLines[idx+1].x2 + + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange') + + if(Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + } + + }else if(condition === 'left_out') { + console.log("left_out::::isStartEnd:::::", isStartEnd); + if(isStartEnd.start){ + + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y1).minus(moveDist).abs().toNumber() + const bStartY = Big(wallLine.y1).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x2 }) + + const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() + newPStart.y = aStartY + newPEnd.y = Big(roofLine.y2).minus(eLineY).toNumber() + let idx = (0 >= index - 1)?roofLines.length:index + const newLine = roofLines[idx-1]; + + if(Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + if(inLine){ + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') + getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') + findPoints.push({ y: aStartY, x: newPStart.x }); + }else{ + const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + newPStart.y = Big(newPStart.y).minus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if(inLine){ + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else { + newPStart.y = roofLine.y1; + } + + } + } + + + if(isStartEnd.end){ + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y2).plus(moveDist).toNumber() + const bStartY = Big(wallLine.y2).plus(moveDist).toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 }) + console.log("startLines:::::::", inLine); + const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() + newPEnd.y = aStartY + newPStart.y = Big(roofLine.y1).plus(eLineY).toNumber() + let idx = (roofLines.length < index + 1)?0:index + const newLine = roofLines[idx+1]; + + if(Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + if(inLine){ + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') + getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') + findPoints.push({ y: aStartY, x: newPEnd.x }); + }else{ + const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + newPEnd.y = Big(newPEnd.y).plus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if(inLine){ + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else { + newPEnd.y = roofLine.y2; + } + + } + } + }else if(condition === 'right_in') { + if (isStartEnd.start ) { + + newPEnd.y = roofLine.y2; + newPEnd.x = roofLine.x2; + + const moveDist = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + ePoint = {x: wallBaseLine.x1, y: wallBaseLine.y1}; + newPStart.y = wallBaseLine.y1 + + findPoints.push({ x: ePoint.x, y: ePoint.y }); + const newPointX = Big(roofLine.x1).minus(moveDist).abs().toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() + let idx = (0 >= index - 1)?roofLines.length:index + const pLineX = roofLines[idx-1].x1 + + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + + if(Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + } + + if(isStartEnd.end) { + newPStart.y = roofLine.y1; + newPStart.x = roofLine.x1; + + const moveDist = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + ePoint = {x: wallBaseLine.x2, y: wallBaseLine.y2}; + newPEnd.y = wallBaseLine.y2 + + findPoints.push({ x: ePoint.x, y: ePoint.y }); + const newPointX = Big(roofLine.x1).minus(moveDist).toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y2).minus(0).abs().toNumber() + let idx = (roofLines.length < index + 1)?0:index + const pLineX = roofLines[idx+1].x2 + + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange') + + if(Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + } + + }else if(condition === 'right_out') { + console.log("right_out::::isStartEnd:::::", isStartEnd); + if (isStartEnd.start ) { //x1 inside + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y1).plus(moveDist).abs().toNumber() + const bStartY = Big(wallLine.y1).plus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 }) + console.log("startLines:::::::", inLine); + const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() + newPStart.y = aStartY + newPEnd.y = Big(roofLine.y2).plus(eLineY).toNumber() + let idx = (0 >= index - 1)?roofLines.length:index + const newLine = roofLines[idx-1]; + + if(Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + if(inLine){ + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') + getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') + findPoints.push({ y: aStartY, x: newPEnd.x }); + }else{ + const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + newPStart.y = Big(newPStart.y).plus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if(inLine){ + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else { + newPStart.y = roofLine.y2; + } + + } + + } + + if(isStartEnd.end){ + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y2).minus(moveDist).abs().toNumber() + const bStartY = Big(wallLine.y2).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 }) + console.log("startLines:::::::", inLine); + const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() + newPEnd.y = aStartY + newPStart.y = Big(roofLine.y1).minus(eLineY).toNumber() + let idx = (roofLines.length < index + 1)?0:index + const newLine = roofLines[idx+1]; + if(inLine){ + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } + if(Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') + getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') + findPoints.push({ y: aStartY, x: newPEnd.x }); + }else{ + const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + newPEnd.y = Big(newPEnd.y).minus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if(inLine){ + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else { + newPEnd.y = roofLine.y2; + } + + } + } + } + + // switch (condition) { + // case 'left_in': + // break; + // case 'left_out': + // break; + // case 'right_in': + // break; + // case 'right_out': + // break; + // } + } + + + } else if (getOrientation(roofLine) === 'horizontal') { //red + + if (['top', 'bottom'].includes(mLine.position)) { + if(Math.abs(wallLine.y1 - wallBaseLine.y1) < 0.1) { + return false + } + const positionType = + (mLine.position === 'top' && wallLine.y1 < wallBaseLine.y1) || + (mLine.position === 'bottom' && wallLine.y1 > wallBaseLine.y1) + ? 'in' : 'out'; + + const condition = `${mLine.position}_${positionType}`; + let isStartEnd = findInteriorPoint(wallBaseLine, wall.baseLines) + + let sPoint, ePoint; + + if(condition === 'top_in') { + if (isStartEnd.start ) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + sPoint = {x: wallBaseLine.x1, y: wallBaseLine.y1}; + newPStart.x = wallBaseLine.x1; + + + const newPointY = Big(roofLine.y2).plus(moveDist).toNumber() + + const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber() + const pLineX = Big(roofLine.x1).minus(0).abs().toNumber() + let idx = (0 >= index - 1)?roofLines.length:index + const pLineY = roofLines[idx-1].y1 + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y }); + + if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') + } + + if(isStartEnd.end){ + const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } + newPEnd.x = wallBaseLine.x2 + + const newPointY = Big(roofLine.y1).plus(moveDist).toNumber() + + const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber() + const pLineX = Big(roofLine.x2).minus(0).abs().toNumber() + let idx = roofLines.length < index + 1 ? 0 : index + const pLineY = roofLines[idx + 1].y2 + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y }) + + if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + //getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') + } + + }else if(condition === 'top_out') { + console.log("top_out isStartEnd:::::::", isStartEnd); + if (isStartEnd.start ) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x1).plus(moveDist).toNumber() + const bStartX = Big(wallLine.x1).plus(moveDist).toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) + + const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber() + newPEnd.x = Big(newPEnd.x).plus(eLineX).toNumber() + newPStart.x = aStartX + let idx = (0 > index - 1)?roofLines.length:index + const newLine = roofLines[idx-1]; + + if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + if(inLine){ + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta') + getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y }); + }else{ + const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber() + newPStart.x = Big(newPStart.x).plus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if(inLine){ + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else { + newPStart.x = roofLine.x1; + } + + } + } + if(isStartEnd.end){ + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x2).minus(moveDist).abs().toNumber() + const bStartX = Big(wallLine.x2).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) + console.log("startLines:::::::", inLine); + const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() + newPStart.x = Big(newPStart.x).minus(eLineX).abs().toNumber() + newPEnd.x = aStartX + let idx = (roofLines.length < index + 1)?0:index + const newLine = roofLines[idx+1]; + + if(Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + if(inLine){ + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') + getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y }); + }else{ + const cLineX = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + newPEnd.x = Big(newPEnd.x).minus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if(inLine){ + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else { + newPEnd.x = roofLine.x2; + } + + } + } + }else if(condition === 'bottom_in') { + if (isStartEnd.start ) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + sPoint = {x: wallBaseLine.x1, y: wallBaseLine.y1}; + newPStart.x = wallBaseLine.x1; + + + const newPointY = Big(roofLine.y2).minus(moveDist).toNumber() + + const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber() + const pLineX = Big(roofLine.x1).minus(0).abs().toNumber() + let idx = (0 > index - 1)?roofLines.length:index + const pLineY = roofLines[idx-1].y1 + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y }); + + if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') + } + + if(isStartEnd.end){ + const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + sPoint = {x: wallBaseLine.x2, y: wallBaseLine.y2}; + newPEnd.x = wallBaseLine.x2; + + + const newPointY = Big(roofLine.y1).minus(moveDist).toNumber() + + const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber() + const pLineX = Big(roofLine.x2).minus(0).abs().toNumber() + let idx = (roofLines.length < index + 1)?0:index + const pLineY = roofLines[idx+1].y2 + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y }); + + if(Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') + + } + }else if(condition === 'bottom_out') { + console.log("bottom_out isStartEnd:::::::", isStartEnd); + if (isStartEnd.start ) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x1).minus(moveDist).abs().toNumber() + const bStartX = Big(wallLine.x1).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 }) + console.log("startLines:::::::", inLine); + const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber() + newPEnd.x = Big(roofLine.x2).minus(eLineX).toNumber() + newPStart.x = aStartX + let idx = (0 > index - 1)?roofLines.length:index + const newLine = roofLines[idx-1]; + + + if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + if(inLine){ + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta') + getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y }); + }else{ + const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber() + newPStart.x = Big(newPStart.x).minus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if(inLine){ + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + newPStart.x = roofLine.x1; + } + + } + } + + if(isStartEnd.end){ + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x2).plus(moveDist).toNumber() + const bStartX = Big(wallLine.x2).plus(moveDist).toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 }) + console.log("startLines:::::::", inLine); + const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() + newPEnd.x = aStartX + newPStart.x = Big(roofLine.x1).plus(eLineX).toNumber() + let idx = (0 > index - 1)?roofLines.length:index + const newLine = roofLines[idx-1]; + + if(Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + if(inLine){ + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') + getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y }); + }else{ + const cLineX = Big(wallBaseLine.y2).minus(wallLine.y2).abs().toNumber() + newPEnd.x = Big(newPEnd.x).plus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if(inLine){ + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + newPEnd.x = roofLine.x2; + } + + } + } + } + + // switch (condition) { + // case 'top_in': + // //console.log("findInteriorPoint result:::::::", isStartEnd); + // break; + // case 'top_out': + // //console.log("findInteriorPoint result:::::::", isStartEnd); + // break; + // case 'bottom_in': + // break; + // case 'bottom_out': + // break; + // } + } + } + getAddLine(newPStart, newPEnd, 'red') + } + canvas.renderAll() + }); + } + + if (findPoints.length > 0) { + // 모든 점에 대해 라인 업데이트를 누적 + return findPoints.reduce((lines, point) => { + return updateAndAddLine(lines, point); + }, [...innerLines]); + + } return innerLines; + } /** @@ -712,8 +1414,8 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { ); if(!outerLine) { - outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); - console.log('Has matching line:', outerLine); + outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); + console.log('Has matching line:', outerLine); } let pitch = outerLine?.attributes?.pitch??0 @@ -728,7 +1430,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { type: POLYGON_TYPE.ROOF, fill: false, stroke: 'blue', - strokeWidth: 8, + strokeWidth: 4, skeletonType: 'polygon', polygonName: '', parentId: roof.id, @@ -745,10 +1447,10 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { // 지붕 경계선과 교차 확인 및 클리핑 const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine); - console.log('clipped line', clippedLine.p1, clippedLine.p2); + //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', 'red', 5, pitch, isOuterLine); - // } + addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine); + // } } } @@ -1399,58 +2101,15 @@ const isPointOnSegment = (point, segStart, segEnd) => { export { findAllIntersections, collectAllPoints, - createPolygonsFromSkeletonLines + createPolygonsFromSkeletonLines, + preprocessPolygonCoordinates, + findOppositeLine, + createOrderedBasePoints, + createInnerLinesFromSkeleton }; -/** - * Finds lines in the roof that match certain criteria based on the given points - * @param {Array} lines - The roof lines to search through - * @param {Object} startPoint - The start point of the reference line - * @param {Object} endPoint - The end point of the reference line - * @param {Array} oldPoints - The old points to compare against - * @returns {Array} Array of matching line objects with their properties - */ -function findMatchingRoofLines(lines, startPoint, endPoint, oldPoints) { - const result = []; - // If no lines provided, return empty array - if (!lines || !lines.length) return result; - - // Process each line in the roof - for (const line of lines) { - // Get the start and end points of the current line - const p1 = { x: line.x1, y: line.y1 }; - const p2 = { x: line.x2, y: line.y2 }; - - // Check if both points exist in the oldPoints array - const p1Exists = oldPoints.some(p => - Math.abs(p.x - p1.x) < 0.0001 && Math.abs(p.y - p1.y) < 0.0001 - ); - - const p2Exists = oldPoints.some(p => - Math.abs(p.x - p2.x) < 0.0001 && Math.abs(p.y - p2.y) < 0.0001 - ); - - // If both points exist in oldPoints, add to results - if (p1Exists && p2Exists) { - // Calculate line position relative to the reference line - const position = getLinePosition( - { start: p1, end: p2 }, - { start: startPoint, end: endPoint } - ); - - result.push({ - start: p1, - end: p2, - position: position, - line: line - }); - } - } - - return result; -} /** * Finds the opposite line in a polygon based on the given line @@ -1525,55 +2184,7 @@ function findOppositeLine(edges, startPoint, endPoint, points) { }); } - // // 현재 선분의 기울기 계산 - // const currentSlope = calculateSlope(p1, p2); - // - // // 기울기가 같은지 확인 (평행한 선분) - // if (areLinesParallel(referenceSlope, currentSlope)) { - // // 동일한 선분이 아닌지 확인 - // if (!areSameLine(p1, p2, startPoint, endPoint)) { - // const position = getLinePosition( - // { start: p1, end: p2 }, - // { start: startPoint, end: endPoint } - // ); - // - // const lineMid = { - // x: (p1.x + p2.x) / 2, - // y: (p1.y + p2.y) / 2 - // }; - // - // const baseMid = { - // x: (startPoint.x + endPoint.x) / 2, - // y: (startPoint.y + endPoint.y) / 2 - // }; - // const distance = Math.sqrt( - // Math.pow(lineMid.x - baseMid.x, 2) + - // Math.pow(lineMid.y - baseMid.y, 2) - // ); - // - // const existingIndex = result.findIndex(line => line.position === position); - // - // if (existingIndex === -1) { - // // If no line with this position exists, add it - // result.push({ - // start: p1, - // end: p2, - // position: position, - // polygon: polygon, - // distance: distance - // }); - // } else if (distance > result[existingIndex].distance) { - // // If a line with this position exists but is closer, replace it - // result[existingIndex] = { - // start: p1, - // end: p2, - // position: position, - // polygon: polygon, - // distance: distance - // }; - // } - // } - // } + } } @@ -1585,19 +2196,19 @@ function getLinePosition(line, referenceLine) { // 대상선의 중점 const lineMidX = (line.start.x + line.end.x) / 2; const lineMidY = (line.start.y + line.end.y) / 2; - + // 참조선의 중점 const refMidX = (referenceLine.start.x + referenceLine.end.x) / 2; const refMidY = (referenceLine.start.y + referenceLine.end.y) / 2; - + // 단순히 좌표 차이로 판단 const deltaX = lineMidX - refMidX; const deltaY = lineMidY - refMidY; - + // 참조선의 기울기 const refDeltaX = referenceLine.end.x - referenceLine.start.x; const refDeltaY = referenceLine.end.y - referenceLine.start.y; - + // 참조선이 더 수평인지 수직인지 판단 if (Math.abs(refDeltaX) > Math.abs(refDeltaY)) { // 수평선에 가까운 경우 - Y 좌표로 판단 @@ -1622,23 +2233,7 @@ function calculateSlope(p1, p2) { return (p2.y - p1.y) / (p2.x - p1.x); } -// 두 직선이 평행한지 확인 -// function areLinesParallel(slope1, slope2) { -// // 두 직선 모두 수직선인 경우 -// if (slope1 === Infinity && slope2 === Infinity) return true; -// -// // 기울기의 차이가 매우 작으면 평행한 것으로 간주 -// const epsilon = 0.0001; -// return Math.abs(slope1 - slope2) < epsilon; -// } -// 두 선분이 동일한지 확인 -// function areSameLine(p1, p2, p3, p4) { -// return ( -// (isSamePoint(p1, p3) && isSamePoint(p2, p4)) || -// (isSamePoint(p1, p4) && isSamePoint(p2, p3)) -// ); -// } /** * Helper function to find the polygon containing the given line */ @@ -1686,7 +2281,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { // p1이 다각형 내부에 있는지 확인 const p1Inside = isPointInsidePolygon(p1, roofLines); - + // p2가 다각형 내부에 있는지 확인 const p2Inside = isPointInsidePolygon(p2, roofLines); @@ -1703,7 +2298,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { // 선분과 다각형 경계선의 교차점들을 찾음 const intersections = []; - + for (const line of roofLines) { const lineP1 = { x: line.x1, y: line.y1 }; const lineP2 = { x: line.x2, y: line.y2 }; @@ -1765,31 +2360,6 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { return { p1: clippedP1, p2: clippedP2 }; } -/** - * 점이 다각형 내부에 있는지 확인합니다 (Ray Casting 알고리즘 사용). - * @param {Object} point - 확인할 점 {x, y} - * @param {Array} roofLines - 다각형을 구성하는 선분들 - * @returns {boolean} 점이 다각형 내부에 있으면 true - */ -function isPointInsidePolygon2(point, roofLines) { - let inside = false; - const x = point.x; - const y = point.y; - - for (const line of roofLines) { - const x1 = line.x1; - const y1 = line.y1; - const x2 = line.x2; - const y2 = line.y2; - - // Ray casting: 점에서 오른쪽으로 수평선을 그었을 때 다각형 경계와 교차하는 횟수 확인 - if (((y1 > y) !== (y2 > y)) && (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1)) { - inside = !inside; - } - } - - return inside; -} function isPointInsidePolygon(point, roofLines) { // 1. 먼저 경계선 위에 있는지 확인 (방향 무관) @@ -1861,14 +2431,14 @@ function isPointOnLineSegmentDirectionIndependent(point, line, tolerance) { * 선분 위의 점에 대한 매개변수 t를 계산합니다. * p = p1 + t * (p2 - p1)에서 t 값을 구합니다. * @param {Object} p1 - 선분의 시작점 - * @param {Object} p2 - 선분의 끝점 + * @param {Object} p2 - 선분의 끝점 * @param {Object} point - 선분 위의 점 * @returns {number} 매개변수 t (0이면 p1, 1이면 p2) */ function getParameterT(p1, p2, point) { const dx = p2.x - p1.x; const dy = p2.y - p1.y; - + // x 좌표가 더 큰 변화를 보이면 x로 계산, 아니면 y로 계산 if (Math.abs(dx) > Math.abs(dy)) { return dx === 0 ? 0 : (point.x - p1.x) / dx; @@ -1908,6 +2478,10 @@ function getLineDirection(p1, p2) { return 'top'; // (-135 ~ -45) } + + + + // selectLine과 baseLines 비교하여 방향 찾기 function findLineDirection(selectLine, baseLines) { for (const baseLine of baseLines) { @@ -1939,153 +2513,6 @@ function findLineDirection(selectLine, baseLines) { return null; // 일치하는 라인이 없는 경우 } -function getLinePositionRelativeToWall(selectLine, wall) { - // wall의 경계를 가져옵니다. - const bounds = wall.getBoundingRect(); - const { left, top, width, height } = bounds; - const right = left + width; - const bottom = top + height; - - // selectLine의 중간점을 계산합니다. - const midX = (selectLine.startPoint.x + selectLine.endPoint.x) / 2; - const midY = (selectLine.startPoint.y + selectLine.endPoint.y) / 2; - - // 경계로부터의 거리를 계산합니다. - const distanceToLeft = Math.abs(midX - left); - const distanceToRight = Math.abs(midX - right); - const distanceToTop = Math.abs(midY - top); - const distanceToBottom = Math.abs(midY - bottom); - - // 가장 가까운 경계를 찾습니다. - const minDistance = Math.min( - distanceToLeft, - distanceToRight, - distanceToTop, - distanceToBottom - ); - - // 가장 가까운 경계를 반환합니다. - if (minDistance === distanceToLeft) return 'left'; - if (minDistance === distanceToRight) return 'right'; - if (minDistance === distanceToTop) return 'top'; - return 'bottom'; -} - -/** - * Convert a line into an array of coordinate points - * @param {Object} line - Line object with startPoint and endPoint - * @param {Object} line.startPoint - Start point with x, y coordinates - * @param {Object} line.endPoint - End point with x, y coordinates - * @param {number} [step=1] - Distance between points (default: 1) - * @returns {Array} Array of points [{x, y}, ...] - */ -function lineToPoints(line, step = 1) { - const { startPoint, endPoint } = line; - const points = []; - - // Add start point - points.push({ x: startPoint.x, y: startPoint.y }); - - // Calculate distance between points - const dx = endPoint.x - startPoint.x; - const dy = endPoint.y - startPoint.y; - const distance = Math.sqrt(dx * dx + dy * dy); - const steps = Math.ceil(distance / step); - - // Add intermediate points - for (let i = 1; i < steps; i++) { - const t = i / steps; - points.push({ - x: startPoint.x + dx * t, - y: startPoint.y + dy * t - }); - } - - // Add end point - points.push({ x: endPoint.x, y: endPoint.y }); - - return points; -} - -/** - * 다각형의 모든 좌표를 offset만큼 안쪽/바깥쪽으로 이동 - * @param {Array} points - 다각형 좌표 배열 [{x, y}, ...] - * @param {number} offset - offset 값 (양수: 안쪽, 음수: 바깥쪽) - * @returns {Array} offset이 적용된 새로운 좌표 배열 - */ -function offsetPolygon(points, offset) { - if (points.length < 3) return points; - - const offsetPoints = []; - const numPoints = points.length; - - for (let i = 0; i < numPoints; i++) { - const prevIndex = (i - 1 + numPoints) % numPoints; - const currentIndex = i; - const nextIndex = (i + 1) % numPoints; - - const prevPoint = points[prevIndex]; - const currentPoint = points[currentIndex]; - const nextPoint = points[nextIndex]; - - // 이전 변의 방향 벡터 - const prevVector = { - x: currentPoint.x - prevPoint.x, - y: currentPoint.y - prevPoint.y - }; - - // 다음 변의 방향 벡터 - const nextVector = { - x: nextPoint.x - currentPoint.x, - y: nextPoint.y - currentPoint.y - }; - - // 정규화 - const prevLength = Math.sqrt(prevVector.x * prevVector.x + prevVector.y * prevVector.y); - const nextLength = Math.sqrt(nextVector.x * nextVector.x + nextVector.y * nextVector.y); - - if (prevLength === 0 || nextLength === 0) continue; - - const prevNormal = { - x: -prevVector.y / prevLength, - y: prevVector.x / prevLength - }; - - const nextNormal = { - x: -nextVector.y / nextLength, - y: nextVector.x / nextLength - }; - - // 평균 법선 벡터 계산 - const avgNormal = { - x: (prevNormal.x + nextNormal.x) / 2, - y: (prevNormal.y + nextNormal.y) / 2 - }; - - // 평균 법선 벡터 정규화 - const avgLength = Math.sqrt(avgNormal.x * avgNormal.x + avgNormal.y * avgNormal.y); - if (avgLength === 0) continue; - - const normalizedAvg = { - x: avgNormal.x / avgLength, - y: avgNormal.y / avgLength - }; - - // 각도 보정 (예각일 때 offset 조정) - const cosAngle = prevNormal.x * nextNormal.x + prevNormal.y * nextNormal.y; - const adjustedOffset = Math.abs(cosAngle) > 0.1 ? offset / Math.abs(cosAngle) : offset; - - // 새로운 점 계산 - const offsetPoint = { - x: currentPoint.x + normalizedAvg.x * adjustedOffset, - y: currentPoint.y + normalizedAvg.y * adjustedOffset - }; - - offsetPoints.push(offsetPoint); - } - - return offsetPoints; -} /** * baseLines를 연결하여 다각형 순서로 정렬된 점들 반환 @@ -2254,11 +2681,11 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const leftIsInside = checkPointInPolygon(leftTestPoint, wall); const rightIsInside = checkPointInPolygon(rightTestPoint, wall); - if (debug) { - console.log('수직선 테스트:'); - console.log(' 왼쪽 포인트:', leftTestPoint, '-> 내부:', leftIsInside); - console.log(' 오른쪽 포인트:', rightTestPoint, '-> 내부:', rightIsInside); - } + // if (debug) { + // console.log('수직선 테스트:'); + // console.log(' 왼쪽 포인트:', leftTestPoint, '-> 내부:', leftIsInside); + // console.log(' 오른쪽 포인트:', rightTestPoint, '-> 내부:', rightIsInside); + // } // left 조건: 왼쪽이 외부, 오른쪽이 내부 if (!leftIsInside && rightIsInside) { @@ -2408,165 +2835,351 @@ const analyzeLineOrientation = (x1, y1, x2, y2, epsilon = 0.5) => { }; }; -function extendLineToBoundary(p1, p2, roofLines) { - // 1. Calculate line direction and length - const dx = p2.x - p1.x; - const dy = p2.y - p1.y; - const length = Math.sqrt(dx * dx + dy * dy); - if (length === 0) return { p1: { ...p1 }, p2: { ...p2 } }; - // 2. Get all polygon points - const points = []; - const seen = new Set(); +// 점에서 선분까지의 최단 거리를 계산하는 도우미 함수 +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; - for (const line of roofLines) { - const p1 = { x: line.x1, y: line.y1 }; - const p2 = { x: line.x2, y: line.y2 }; + const dot = A * C + B * D; + const lenSq = C * C + D * D; + let param = -1; - const key1 = `${p1.x},${p1.y}`; - const key2 = `${p2.x},${p2.y}`; + if (lenSq !== 0) { + param = dot / lenSq; + } - if (!seen.has(key1)) { - points.push(p1); - seen.add(key1); - } - if (!seen.has(key2)) { - points.push(p2); - seen.add(key2); + 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); +} + + +const getOrientation = (line, eps = 0.1) => { + const x1 = line.get('x1') + const y1 = line.get('y1') + const x2 = line.get('x2') + const y2 = line.get('y2') + const dx = Math.abs(x2 - x1) + const dy = Math.abs(y2 - y1) + + if (dx < eps && dy >= eps) return 'vertical' + if (dy < eps && dx >= eps) return 'horizontal' + if (dx < eps && dy < eps) return 'point' + return 'diagonal' +} + + + +export const processEaveHelpLines = (lines) => { + if (!lines || lines.length === 0) return []; + + // 수직/수평 라인 분류 (부동소수점 오차 고려) + const verticalLines = lines.filter(line => Math.abs(line.x1 - line.x2) < 0.1); + const horizontalLines = lines.filter(line => Math.abs(line.y1 - line.y2) < 0.1); + + // 라인 병합 (더 엄격한 조건으로) + const mergedVertical = mergeLines(verticalLines, 'vertical'); + const mergedHorizontal = mergeLines(horizontalLines, 'horizontal'); + + // 결과 확인용 로그 + console.log('Original lines:', lines.length); + console.log('Merged vertical:', mergedVertical.length); + console.log('Merged horizontal:', mergedHorizontal.length); + + return [...mergedVertical, ...mergedHorizontal]; +}; + +const mergeLines = (lines, direction) => { + if (!lines || lines.length < 2) return lines || []; + + // 방향에 따라 정렬 (수직: y1 기준, 수평: x1 기준) + lines.sort((a, b) => { + const aPos = direction === 'vertical' ? a.y1 : a.x1; + const bPos = direction === 'vertical' ? b.y1 : b.x1; + return aPos - bPos; + }); + + const merged = []; + let current = { ...lines[0] }; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + + // 같은 선상에 있는지 확인 (부동소수점 오차 고려) + const isSameLine = direction === 'vertical' + ? Math.abs(current.x1 - line.x1) < 0.1 + : Math.abs(current.y1 - line.y1) < 0.1; + + // 연결 가능한지 확인 (약간의 겹침 허용) + const isConnected = direction === 'vertical' + ? current.y2 + 0.1 >= line.y1 // 약간의 오차 허용 + : current.x2 + 0.1 >= line.x1; + + if (isSameLine && isConnected) { + // 라인 병합 + current.y2 = Math.max(current.y2, line.y2); + current.x2 = direction === 'vertical' ? current.x1 : current.x2; + } else { + merged.push(current); + current = { ...line }; } } + merged.push(current); - // 3. Find the bounding box - let minX = Infinity, minY = Infinity; - let maxX = -Infinity, maxY = -Infinity; + // 병합 결과 로그 + console.log(`Merged ${direction} lines:`, merged); - for (const p of points) { - minX = Math.min(minX, p.x); - minY = Math.min(minY, p.y); - maxX = Math.max(maxX, p.x); - maxY = Math.max(maxY, p.y); - } + return merged; +}; - // 4. Extend line to bounding box - const bboxLines = [ - { x1: minX, y1: minY, x2: maxX, y2: minY }, // top - { x1: maxX, y1: minY, x2: maxX, y2: maxY }, // right - { x1: maxX, y1: maxY, x2: minX, y2: maxY }, // bottom - { x1: minX, y1: maxY, x2: minX, y2: minY } // left - ]; - const intersections = []; - // 5. Find intersections with bounding box - for (const line of bboxLines) { - const intersect = getLineIntersection( - p1, p2, - { x: line.x1, y: line.y1 }, - { x: line.x2, y: line.y2 } - ); +/** + * 주어진 점을 포함하는 라인을 찾는 함수 + * @param {Array} lines - 검색할 라인 배열 (각 라인은 x1, y1, x2, y2 속성을 가져야 함) + * @param {Object} point - 찾고자 하는 점 {x, y} + * @param {number} [tolerance=0.1] - 점이 선분 위에 있는지 판단할 때의 허용 오차 + * @returns {Object|null} 점을 포함하는 첫 번째 라인 또는 null + */ +function findLineContainingPoint(lines, point, tolerance = 0.1) { + if (!point || !lines || !lines.length) return null; - if (intersect) { - const t = ((intersect.x - p1.x) * dx + (intersect.y - p1.y) * dy) / (length * length); - if (t >= 0 && t <= 1) { - intersections.push({ x: intersect.x, y: intersect.y, t }); - } - } - } - - // 6. If we have two intersections, use them - if (intersections.length >= 2) { - // Sort by t value - intersections.sort((a, b) => a.t - b.t); - return { - p1: { x: intersections[0].x, y: intersections[0].y }, - p2: { - x: intersections[intersections.length - 1].x, - y: intersections[intersections.length - 1].y - } - }; - } - - // 7. Fallback to original points - return { p1: { ...p1 }, p2: { ...p2 } }; + return lines.find(line => { + const { x1, y1, x2, y2 } = line; + return isPointOnLineSegment(point, {x: x1, y: y1}, {x: x2, y: y2}, tolerance); + }) || null; } /** - * 점에서 특정 방향으로 경계선과의 교차점을 찾습니다. - * @param {Object} point - 시작점 {x, y} - * @param {Object} direction - 방향 벡터 {x, y} (정규화된 값) - * @param {Array} roofLines - 지붕 경계선 배열 - * @returns {Object|null} 교차점 {x, y} 또는 null + * 점이 선분 위에 있는지 확인하는 함수 + * @param {Object} point - 확인할 점 {x, y} + * @param {Object} lineStart - 선분의 시작점 {x, y} + * @param {Object} lineEnd - 선분의 끝점 {x, y} + * @param {number} tolerance - 허용 오차 + * @returns {boolean} */ -function findBoundaryIntersection(point, direction, roofLines) { - let closestIntersection = null; - let minDistance = Infinity; +function isPointOnLineSegment(point, lineStart, lineEnd, tolerance = 0.1) { + const { x: px, y: py } = point; + const { x: x1, y: y1 } = lineStart; + const { x: x2, y: y2 } = lineEnd; - // 충분히 긴 거리로 광선 생성 (임의로 큰 값 사용) - const rayLength = 10000; - const rayEnd = { - x: point.x + direction.x * rayLength, - y: point.y + direction.y * rayLength + // 선분의 길이 + const lineLength = Math.hypot(x2 - x1, y2 - y1); + + // 점에서 선분의 양 끝점까지의 거리 합 + const dist1 = Math.hypot(px - x1, py - y1); + const dist2 = Math.hypot(px - x2, py - y2); + + // 점이 선분 위에 있는지 확인 (허용 오차 범위 내에서) + return Math.abs(dist1 + dist2 - lineLength) <= tolerance; +} + +/** + * Updates a line in the innerLines array and returns the updated array + * @param {Array} innerLines - Array of line objects to update + * @param {Object} targetPoint - The point to find the line {x, y} + * @param {Object} wallBaseLine - The base line containing new coordinates + * @param {Function} getAddLine - Function to add a new line + * @returns {Array} Updated array of lines + */ +function updateAndAddLine(innerLines, targetPoint) { + + // 1. Find the line containing the target point + const foundLine = findLineContainingPoint(innerLines, targetPoint); + if (!foundLine) { + console.warn('No line found containing the target point'); + return [...innerLines]; + } + + // 2. Create a new array without the found line + const updatedLines = innerLines.filter(line => + line !== foundLine && + !(line.x1 === foundLine.x1 && + line.y1 === foundLine.y1 && + line.x2 === foundLine.x2 && + line.y2 === foundLine.y2) + ); + + // Calculate distances to both endpoints + const distanceToStart = Math.hypot( + targetPoint.x - foundLine.x1, + targetPoint.y - foundLine.y1 + ); + const distanceToEnd = Math.hypot( + targetPoint.x - foundLine.x2, + targetPoint.y - foundLine.y2 + ); + + // 단순 거리 비교: 타겟 포인트가 시작점에 더 가까우면 시작점을 수정(isUpdatingStart = true) + //무조건 start + const isUpdatingStart = true //distanceToStart < distanceToEnd; + + const updatedLine = { + ...foundLine, + left: isUpdatingStart ? targetPoint.x : foundLine.x1, + top: isUpdatingStart ? targetPoint.y : foundLine.y1, + x1: isUpdatingStart ? targetPoint.x : foundLine.x1, + y1: isUpdatingStart ? targetPoint.y : foundLine.y1, + x2: isUpdatingStart ? foundLine.x2 : targetPoint.x, + y2: isUpdatingStart ? foundLine.y2 : targetPoint.y, + startPoint: { + x: isUpdatingStart ? targetPoint.x : foundLine.x1, + y: isUpdatingStart ? targetPoint.y : foundLine.y1 + }, + endPoint: { + x: isUpdatingStart ? foundLine.x2 : targetPoint.x, + y: isUpdatingStart ? foundLine.y2 : targetPoint.y + } }; - // 모든 경계선과의 교차점 확인 - for (const line of roofLines) { - const lineP1 = { x: line.x1, y: line.y1 }; - const lineP2 = { x: line.x2, y: line.y2 }; - - const intersection = getLineIntersection(point, rayEnd, lineP1, lineP2); - - if (intersection) { - // 교차점까지의 거리 계산 - const distance = Math.sqrt( - Math.pow(intersection.x - point.x, 2) + - Math.pow(intersection.y - point.y, 2) - ); - - // 가장 가까운 교차점 저장 (거리가 0보다 큰 경우만) - if (distance > 0.01 && distance < minDistance) { - minDistance = distance; - closestIntersection = intersection; - } - } + // 4. If it's a Fabric.js object, use set method if available + if (typeof foundLine.set === 'function') { + foundLine.set({ + x1: isUpdatingStart ? targetPoint.x : foundLine.x1, + y1: isUpdatingStart ? targetPoint.y : foundLine.y1, + x2: isUpdatingStart ? foundLine.x2 : targetPoint.x, + y2: isUpdatingStart ? foundLine.y2 : targetPoint.y + }); + updatedLines.push(foundLine); + } else { + updatedLines.push(updatedLine); } - return closestIntersection; + return updatedLines; +} + + + +/** + * 점이 선분 위에 있는지 확인 + * @param {Object} point - 확인할 점 {x, y} + * @param {Object} lineStart - 선분의 시작점 {x, y} + * @param {Object} lineEnd - 선분의 끝점 {x, y} + * @param {number} tolerance - 오차 허용 범위 + * @returns {boolean} - 점이 선분 위에 있으면 true, 아니면 false + */ +function isPointOnLineSegment2(point, lineStart, lineEnd, tolerance = 0.1) { + const { x: px, y: py } = point; + const { x: x1, y: y1 } = lineStart; + const { x: x2, y: y2 } = lineEnd; + + // 선분의 길이 + const lineLength = Math.hypot(x2 - x1, y2 - y1); + + // 점에서 선분의 양 끝점까지의 거리 + const dist1 = Math.hypot(px - x1, py - y1); + const dist2 = Math.hypot(px - x2, py - y2); + + // 점이 선분 위에 있는지 확인 (오차 허용 범위 내에서) + const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance; + + if (isOnSegment) { + console.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); + } + + return isOnSegment; } /** - * 점이 다른 스켈레톤 라인과의 교점인지 확인합니다. - * @param {Object} point - 확인할 점 {x, y} - * @param {Array} skeletonLines - 모든 스켈레톤 라인 배열 - * @param {Object} currentLine - 현재 라인 {p1, p2} (자기 자신 제외용) - * @param {number} tolerance - 허용 오차 - * @returns {boolean} 교점이면 true + * 세 점(p1 -> p2 -> p3)의 방향성을 계산합니다. (2D 외적) + * 반시계 방향(CCW)으로 그려진 폴리곤(Y축 Down) 기준: + * - 결과 > 0 : 오른쪽 턴 (Right Turn) -> 골짜기 (Valley/Reflex Vertex) + * - 결과 < 0 : 왼쪽 턴 (Left Turn) -> 외곽 모서리 (Convex Vertex) + * - 결과 = 0 : 직선 */ -function hasIntersectionWithOtherLines(point, skeletonLines, currentLine, tolerance = 0.5) { - if (!skeletonLines || skeletonLines.length === 0) { - return false; +function getTurnDirection(p1, p2, p3) { + // 벡터 a: p1 -> p2 + // 벡터 b: p2 -> p3 + const val = (p2.x - p1.x) * (p3.y - p2.y) - (p2.y - p1.y) * (p3.x - p2.x); + return val; +} + +/** + * 현재 점(point)을 기준으로 연결된 이전 라인과 다음 라인을 찾아 골짜기 여부 판단 + */ +function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { + const tolerance = 0.1; + + // 1. 연결된 '다른' 라인을 찾습니다. + // isStartVertex가 true면 : 이 점으로 '들어오는' 라인(Previous Line)을 찾아야 함 + // isStartVertex가 false면 : 이 점에서 '나가는' 라인(Next Line)을 찾아야 함 + + let neighborLine = null; + + if (isStartVertex) { + // targetPoint가 Start이므로, 어떤 라인의 End가 targetPoint와 같아야 함 (Previous Line) + neighborLine = allLines.find(l => + l !== connectedLine && + isSamePoint(l.endPoint || {x:l.x2, y:l.y2}, targetPoint, tolerance) + ); + } else { + // targetPoint가 End이므로, 어떤 라인의 Start가 targetPoint와 같아야 함 (Next Line) + neighborLine = allLines.find(l => + l !== connectedLine && + isSamePoint(l.startPoint || {x:l.x1, y:l.y1}, targetPoint, tolerance) + ); } - let connectionCount = 0; + // 연결된 라인을 못 찾았거나 끊겨있으면 판단 불가 (일단 false) + if (!neighborLine) return false; - for (const line of skeletonLines) { - // 자기 자신과의 비교는 제외 - if (line.p1 && line.p2 && currentLine.p1 && currentLine.p2) { - const isSameLineCheck = - (isSamePoint(line.p1, currentLine.p1, tolerance) && isSamePoint(line.p2, currentLine.p2, tolerance)) || - (isSamePoint(line.p1, currentLine.p2, tolerance) && isSamePoint(line.p2, currentLine.p1, tolerance)); + // 2. 세 점을 구성하여 회전 방향(Turn) 계산 + // 순서: PrevLine.Start -> [TargetVertex] -> NextLine.End + let p1, p2, p3; - if (isSameLineCheck) continue; - } - - // 다른 라인의 끝점이 현재 점과 일치하는지 확인 - if (line.p1 && isSamePoint(point, line.p1, tolerance)) { - connectionCount++; - } - if (line.p2 && isSamePoint(point, line.p2, tolerance)) { - connectionCount++; - } + if (isStartVertex) { + // neighbor(Prev) -> connected(Current) + p1 = neighborLine.startPoint || {x: neighborLine.x1, y: neighborLine.y1}; + p2 = targetPoint; // 접점 + p3 = connectedLine.endPoint || {x: connectedLine.x2, y: connectedLine.y2}; + } else { + // connected(Current) -> neighbor(Next) + p1 = connectedLine.startPoint || {x: connectedLine.x1, y: connectedLine.y1}; + p2 = targetPoint; // 접점 + p3 = neighborLine.endPoint || {x: neighborLine.x2, y: neighborLine.y2}; } - // 1개 이상의 다른 라인과 연결되어 있으면 교점으로 간주 - return connectionCount >= 1; + // 3. 외적 계산 (Y축이 아래로 증가하는 캔버스 좌표계 + CCW 진행 기준) + // 값이 양수(+)면 오른쪽 턴 = 골짜기 + const crossProduct = getTurnDirection(p1, p2, p3); + + return crossProduct > 0; +} + +function findInteriorPoint(line, polygonLines) { + const { x1, y1, x2, y2 } = line; + + // line 객체 포맷 통일 + const currentLine = { + ...line, + startPoint: { x: x1, y: y1 }, + endPoint: { x: x2, y: y2 } + }; + + // 1. 시작점이 골짜기인지 확인 (들어오는 라인과 나가는 라인의 각도) + const startIsValley = isValleyVertex(currentLine.startPoint, currentLine, polygonLines, true); + + // 2. 끝점이 골짜기인지 확인 + const endIsValley = isValleyVertex(currentLine.endPoint, currentLine, polygonLines, false); + + return { + start: startIsValley, + end: endIsValley + }; } \ No newline at end of file From dbb6f0af81c1153c97ed16d8110f1891a5be3b0e Mon Sep 17 00:00:00 2001 From: yscha Date: Sun, 7 Dec 2025 14:48:05 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=EB=8F=99=EC=9D=B4=EB=8F=99=20=EB=B0=8F?= =?UTF-8?q?=20fabric=20=EB=B2=84=EC=A0=84=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/components/fabric/QPolygon.js | 4 +- .../roofcover/useRoofAllocationSetting.js | 18 ++ src/hooks/usePolygon.js | 170 +++++++++++++----- src/util/skeleton-utils.js | 4 +- 5 files changed, 152 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 7be9b3f3..676d8f4f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "chart.js": "^4.4.6", "dayjs": "^1.11.13", "env-cmd": "^10.1.0", - "fabric": "^5.3.0", + "fabric": "^5.5.2", "framer-motion": "^11.2.13", "fs": "^0.0.1-security", "iron-session": "^8.0.2", diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 8186b449..3ca095a8 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -336,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/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index 799e3a72..811d6fd6 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -29,6 +29,8 @@ import { QcastContext } from '@/app/QcastProvider' import { usePlan } from '@/hooks/usePlan' import { roofsState } from '@/store/roofAtom' import { useText } from '@/hooks/useText' +import { processEaveHelpLines } from '@/util/skeleton-utils' +import { QLine } from '@/components/fabric/QLine' export function useRoofAllocationSetting(id) { const canvas = useRecoilValue(canvasState) @@ -404,6 +406,22 @@ export function useRoofAllocationSetting(id) { const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) roofBases.forEach((roofBase) => { try { + + const roofEaveHelpLines = canvas.getObjects().filter((obj) => obj.lineName === 'eaveHelpLine' && obj.roofId === roofBase.id) + if (roofEaveHelpLines.length > 0) { + if (roofBase.lines) { + // Filter out any eaveHelpLines that are already in lines to avoid duplicates + const existingEaveLineIds = new Set(roofBase.lines.map((line) => line.id)) + const newEaveLines = roofEaveHelpLines.filter((line) => !existingEaveLineIds.has(line.id)) + roofBase.lines = [...newEaveLines] + } else { + roofBase.lines = [...roofEaveHelpLines] + } + if (!roofBase.innerLines) { + roofBase.innerLines = [] + } + } + if (roofBase.separatePolygon.length > 0) { splitPolygonWithSeparate(roofBase.separatePolygon) } else { diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 2ae37440..5e21e902 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -845,6 +845,8 @@ export const usePolygon = () => { polygonLines.forEach((line) => { line.need = true }) + // 순서에 의존하지 않도록 모든 조합을 먼저 확인한 후 처리 + const innerLineMapping = new Map() // innerLine -> polygonLine 매핑 저장 // innerLines와 polygonLines의 겹침을 확인하고 type 변경 innerLines.forEach((innerLine) => { @@ -854,14 +856,28 @@ export const usePolygon = () => { if (innerLine.attributes && polygonLine.attributes.type) { // innerLine이 polygonLine보다 긴 경우 polygonLine.need를 false로 변경 if (polygonLine.length < innerLine.length) { - polygonLine.need = false + if(polygonLine.lineName !== 'eaveHelpLine'){ + polygonLine.need = false + } } - innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize - innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize - innerLine.attributes.type = polygonLine.attributes.type - innerLine.direction = polygonLine.direction - innerLine.attributes.isStart = true - innerLine.parentLine = polygonLine + // innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize + // innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize + // innerLine.attributes.type = polygonLine.attributes.type + // innerLine.direction = polygonLine.direction + // innerLine.attributes.isStart = true + // innerLine.parentLine = polygonLine + + + // 매핑된 innerLine의 attributes를 변경 (교차점 계산 전에 적용) + innerLineMapping.forEach((polygonLine, innerLine) => { + innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize + innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize + innerLine.attributes.type = polygonLine.attributes.type + innerLine.direction = polygonLine.direction + innerLine.attributes.isStart = true + innerLine.parentLine = polygonLine + }) + } } }) @@ -1371,7 +1387,7 @@ export const usePolygon = () => { let representLine // 지붕을 그리면서 기존 polygon의 line중 연결된 line을 찾는다. - ;[...polygonLines, ...innerLines].forEach((line) => { + [...polygonLines, ...innerLines].forEach((line) => { let startFlag = false let endFlag = false const startPoint = line.startPoint @@ -1567,52 +1583,126 @@ export const usePolygon = () => { // ==== Dijkstra pathfinding ==== + // function findShortestPath(start, end, graph, epsilon = 1) { + // const startKey = pointToKey(start, epsilon) + // const endKey = pointToKey(end, epsilon) + // + // const distances = {} + // const previous = {} + // const visited = new Set() + // const queue = [{ key: startKey, dist: 0 }] + // + // for (const key in graph) distances[key] = Infinity + // distances[startKey] = 0 + // + // while (queue.length > 0) { + // queue.sort((a, b) => a.dist - b.dist) + // const { key } = queue.shift() + // if (visited.has(key)) continue + // visited.add(key) + // + // for (const neighbor of graph[key] || []) { + // const neighborKey = pointToKey(neighbor.point, epsilon) + // const alt = distances[key] + neighbor.distance + // if (alt < distances[neighborKey]) { + // distances[neighborKey] = alt + // previous[neighborKey] = key + // queue.push({ key: neighborKey, dist: alt }) + // } + // } + // } + // + // const path = [] + // let currentKey = endKey + // + // if (!previous[currentKey]) return null + // + // while (currentKey !== startKey) { + // const [x, y] = currentKey.split(',').map(Number) + // path.unshift({ x, y }) + // currentKey = previous[currentKey] + // } + // + // const [sx, sy] = startKey.split(',').map(Number) + // path.unshift({ x: sx, y: sy }) + // + // return path + // } + function findShortestPath(start, end, graph, epsilon = 1) { - const startKey = pointToKey(start, epsilon) - const endKey = pointToKey(end, epsilon) + const startKey = pointToKey(start, epsilon); + const endKey = pointToKey(end, epsilon); - const distances = {} - const previous = {} - const visited = new Set() - const queue = [{ key: startKey, dist: 0 }] + // 거리와 이전 노드 추적 + const distances = { [startKey]: 0 }; + const previous = {}; + const visited = new Set(); - for (const key in graph) distances[key] = Infinity - distances[startKey] = 0 + // 우선순위 큐 (거리가 짧은 순으로 정렬) + const queue = [{ key: startKey, dist: 0 }]; - while (queue.length > 0) { - queue.sort((a, b) => a.dist - b.dist) - const { key } = queue.shift() - if (visited.has(key)) continue - visited.add(key) + // 모든 노드 초기화 + for (const key in graph) { + if (key !== startKey) { + distances[key] = Infinity; + } + } - for (const neighbor of graph[key] || []) { - const neighborKey = pointToKey(neighbor.point, epsilon) - const alt = distances[key] + neighbor.distance - if (alt < distances[neighborKey]) { - distances[neighborKey] = alt - previous[neighborKey] = key - queue.push({ key: neighborKey, dist: alt }) + // 우선순위 큐에서 다음 노드 선택 + const getNextNode = () => { + if (queue.length === 0) return null; + queue.sort((a, b) => a.dist - b.dist); + return queue.shift(); + }; + + let current; + while ((current = getNextNode())) { + const currentKey = current.key; + + // 목적지에 도달하면 종료 + if (currentKey === endKey) break; + + // 이미 방문한 노드는 건너뜀 + if (visited.has(currentKey)) continue; + visited.add(currentKey); + + // 인접 노드 탐색 + for (const neighbor of graph[currentKey] || []) { + const neighborKey = pointToKey(neighbor.point, epsilon); + if (visited.has(neighborKey)) continue; + + const alt = distances[currentKey] + neighbor.distance; + + // 더 짧은 경로를 찾은 경우 업데이트 + if (alt < (distances[neighborKey] || Infinity)) { + distances[neighborKey] = alt; + previous[neighborKey] = currentKey; + + // 우선순위 큐에 추가 + queue.push({ key: neighborKey, dist: alt }); } } } - const path = [] - let currentKey = endKey + // 경로 재구성 + const path = []; + let currentKey = endKey; - if (!previous[currentKey]) return null - - while (currentKey !== startKey) { - const [x, y] = currentKey.split(',').map(Number) - path.unshift({ x, y }) - currentKey = previous[currentKey] + // 시작점에 도달할 때까지 역추적 + while (previous[currentKey] !== undefined) { + const [x, y] = currentKey.split(',').map(Number); + path.unshift({ x, y }); + currentKey = previous[currentKey]; } - const [sx, sy] = startKey.split(',').map(Number) - path.unshift({ x: sx, y: sy }) + // 시작점 추가 + if (path.length > 0) { + const [sx, sy] = startKey.split(',').map(Number); + path.unshift({ x: sx, y: sy }); + } - return path + return path.length > 0 ? path : null; } - // 최종 함수 function getPath(start, end, graph, epsilon = 1) { // startPoint와 arrivalPoint가 될 수 있는 점은 line.attributes.type이 'default' 혹은 null이 아닌 line인 경우에만 가능 diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 80763cfa..ab72b12b 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -828,7 +828,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const line = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId : roof.id, fontSize : roof.fontSize, - stroke : stroke, + stroke : '#3FBAE6', strokeWidth: 4, name : 'eaveHelpLine', lineName : 'eaveHelpLine', @@ -963,8 +963,6 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } } - - if(isStartEnd.end){ const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() const aStartY = Big(roofLine.y2).plus(moveDist).toNumber() From 2220e2ea08d8202b91e6dcaebdb0141e683206ad Mon Sep 17 00:00:00 2001 From: yscha Date: Sun, 7 Dec 2025 16:05:28 +0900 Subject: [PATCH 04/14] v0.3 --- src/util/skeleton-utils.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index ab72b12b..7257ae6b 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -958,11 +958,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else { - newPStart.y = roofLine.y1; + newPStart.y = wallLine.y1; } } } + if(isStartEnd.end){ const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() const aStartY = Big(roofLine.y2).plus(moveDist).toNumber() @@ -989,7 +990,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else { - newPEnd.y = roofLine.y2; + newPEnd.y = wallLine.y2 } } @@ -1073,7 +1074,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else { - newPStart.y = roofLine.y2; + newPStart.y = wallLine.y1; } } @@ -1105,7 +1106,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else { - newPEnd.y = roofLine.y2; + newPEnd.y = wallLine.y2; } } @@ -1213,7 +1214,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else { - newPStart.x = roofLine.x1; + newPStart.x = wallLine.x1; } } @@ -1244,7 +1245,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else { - newPEnd.x = roofLine.x2; + newPEnd.x = wallLine.x2; } } @@ -1323,7 +1324,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else{ - newPStart.x = roofLine.x1; + newPStart.x = wallLine.x1; } } @@ -1355,7 +1356,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else{ - newPEnd.x = roofLine.x2; + newPEnd.x = wallLine.x2; } } From 5da154df5dc81c8e7f7ec2e7e05c14a334a82441 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 8 Dec 2025 10:31:32 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=EC=A2=8C=ED=91=9C=EC=82=AD=EC=A0=9C,=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=A4=84?= =?UTF-8?q?=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 7257ae6b..c7f3685b 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -603,7 +603,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //visible: (!sktLine.attributes.isOuterEdge), }); - coordinateText(skeletonLine) + //coordinateText(skeletonLine) canvas.add(skeletonLine); skeletonLine.bringToFront(); existingLines.add(lineKey); // 추가된 라인을 추적 @@ -829,7 +829,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { parentId : roof.id, fontSize : roof.fontSize, stroke : '#3FBAE6', - strokeWidth: 4, + strokeWidth: 2, name : 'eaveHelpLine', lineName : 'eaveHelpLine', selectable : true, @@ -840,7 +840,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { isStart : true } }); - coordinateText(line) + //coordinateText(line) canvas.add(line) canvas.renderAll(); return line From a8c7d052781864c90250e0baaf2703368204ae99 Mon Sep 17 00:00:00 2001 From: yscha Date: Tue, 9 Dec 2025 00:10:40 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=EB=8F=99=EC=9D=B4=EB=8F=99=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 75 ++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index c7f3685b..599345e2 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -837,7 +837,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { roofId : roofId, attributes : { type: 'eaveHelpLine', - isStart : true + isStart : true, + pitch: wallLine.attributes.pitch, } }); //coordinateText(line) @@ -888,7 +889,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ePoint = {x: wallBaseLine.x1, y: wallBaseLine.y1}; newPStart.y = wallBaseLine.y1 - findPoints.push({ x: ePoint.x, y: ePoint.y }); + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_start' }); const newPointX = Big(roofLine.x1).plus(moveDist).abs().toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() @@ -912,7 +913,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ePoint = {x: wallBaseLine.x2, y: wallBaseLine.y2}; newPEnd.y = wallBaseLine.y2 - findPoints.push({ x: ePoint.x, y: ePoint.y }); + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_end' }); const newPointX = Big(roofLine.x1).plus(moveDist).toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() const pLineY = Big(roofLine.y2).minus(0).abs().toNumber() @@ -950,7 +951,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') - findPoints.push({ y: aStartY, x: newPStart.x }); + findPoints.push({ y: aStartY, x: newPStart.x, position: 'left_out_start' }); }else{ const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() newPStart.y = Big(newPStart.y).minus(cLineY).toNumber(); @@ -964,6 +965,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } } + if(isStartEnd.end){ const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() const aStartY = Big(roofLine.y2).plus(moveDist).toNumber() @@ -982,7 +984,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') - findPoints.push({ y: aStartY, x: newPEnd.x }); + findPoints.push({ y: aStartY, x: newPEnd.x, position: 'left_out_end' }); }else{ const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() newPEnd.y = Big(newPEnd.y).plus(cLineY).toNumber(); @@ -990,6 +992,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(inLine){ getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') }else { + newPEnd.y = wallLine.y2 } @@ -1005,7 +1008,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ePoint = {x: wallBaseLine.x1, y: wallBaseLine.y1}; newPStart.y = wallBaseLine.y1 - findPoints.push({ x: ePoint.x, y: ePoint.y }); + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_start'}); const newPointX = Big(roofLine.x1).minus(moveDist).abs().toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() @@ -1029,7 +1032,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ePoint = {x: wallBaseLine.x2, y: wallBaseLine.y2}; newPEnd.y = wallBaseLine.y2 - findPoints.push({ x: ePoint.x, y: ePoint.y }); + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_end' }); const newPointX = Big(roofLine.x1).minus(moveDist).toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() const pLineY = Big(roofLine.y2).minus(0).abs().toNumber() @@ -1066,7 +1069,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') - findPoints.push({ y: aStartY, x: newPEnd.x }); + findPoints.push({ y: aStartY, x: newPEnd.x, position: 'right_out_start' }); }else{ const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() newPStart.y = Big(newPStart.y).plus(cLineY).toNumber(); @@ -1098,7 +1101,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') - findPoints.push({ y: aStartY, x: newPEnd.x }); + findPoints.push({ y: aStartY, x: newPEnd.x, position: 'right_out_end' }); }else{ const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() newPEnd.y = Big(newPEnd.y).minus(cLineY).toNumber(); @@ -1156,7 +1159,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { let idx = (0 >= index - 1)?roofLines.length:index const pLineY = roofLines[idx-1].y1 getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue') - findPoints.push({ x: sPoint.x, y: sPoint.y }); + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_start' }); if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') @@ -1177,7 +1180,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { let idx = roofLines.length < index + 1 ? 0 : index const pLineY = roofLines[idx + 1].y2 getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue') - findPoints.push({ x: sPoint.x, y: sPoint.y }) + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_end' }); if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') @@ -1206,7 +1209,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta') getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') - findPoints.push({ x: aStartX, y: newPEnd.y }); + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'top_out_start' }); }else{ const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber() newPStart.x = Big(newPStart.x).plus(cLineX).toNumber(); @@ -1237,7 +1240,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') - findPoints.push({ x: aStartX, y: newPEnd.y }); + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'top_out_end' }); }else{ const cLineX = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() newPEnd.x = Big(newPEnd.x).minus(cLineX).toNumber(); @@ -1264,7 +1267,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { let idx = (0 > index - 1)?roofLines.length:index const pLineY = roofLines[idx-1].y1 getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue') - findPoints.push({ x: sPoint.x, y: sPoint.y }); + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_start' }); if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') @@ -1286,7 +1289,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { let idx = (roofLines.length < index + 1)?0:index const pLineY = roofLines[idx+1].y2 getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue') - findPoints.push({ x: sPoint.x, y: sPoint.y }); + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_end' }); if(Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') @@ -1348,7 +1351,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') - findPoints.push({ x: aStartX, y: newPEnd.y }); + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'bottom_out_end' }); }else{ const cLineX = Big(wallBaseLine.y2).minus(wallLine.y2).abs().toNumber() newPEnd.x = Big(newPEnd.x).plus(cLineX).toNumber(); @@ -1385,8 +1388,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if (findPoints.length > 0) { // 모든 점에 대해 라인 업데이트를 누적 - return findPoints.reduce((lines, point) => { - return updateAndAddLine(lines, point); + return findPoints.reduce((innerLines, point) => { + return updateAndAddLine(innerLines, point); }, [...innerLines]); } @@ -3027,7 +3030,41 @@ function updateAndAddLine(innerLines, targetPoint) { // 단순 거리 비교: 타겟 포인트가 시작점에 더 가까우면 시작점을 수정(isUpdatingStart = true) //무조건 start - const isUpdatingStart = true //distanceToStart < distanceToEnd; + let isUpdatingStart = false //distanceToStart < distanceToEnd; + if(targetPoint.position === "top_in_start"){ + if(foundLine.y2 >= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "top_in_end"){ + if(foundLine.y2 >= foundLine.y1){ + isUpdatingStart = true; + } + + }else if(targetPoint.position === "bottom_in_start"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "bottom_in_end"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_in_start"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_in_end"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_in_start"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_in_end"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } + } const updatedLine = { ...foundLine, From 2625dcdd080c545f53f37dd166a6e686f997bd89 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 9 Dec 2025 10:40:51 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=EC=A0=88=EB=8B=A8=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 | 135 ++++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 18 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 599345e2..f9a9360d 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -947,7 +947,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { if(inLine){ - getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + if(inLine.x1 < inLine.x2) { + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + }else{ + getAddLine({ y: inLine.y2, x: inLine.x2 },{ y: bStartY, x: wallLine.x2 }, 'pink') + } + } getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') @@ -957,7 +962,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPStart.y = Big(newPStart.y).minus(cLineY).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) if(inLine){ - getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.x1 < inLine.x2) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1},{ y: newPStart.y, x: newPStart.x }, 'purple') + } }else { newPStart.y = wallLine.y1; } @@ -980,7 +989,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { if(inLine){ - getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + if(inLine.x1 < inLine.x2) { + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: bStartY, x: wallLine.x1 }, 'pink') + } } getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') @@ -990,14 +1003,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPEnd.y = Big(newPEnd.y).plus(cLineY).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ - getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.x1 < inLine.x2) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple') + } }else { newPEnd.y = wallLine.y2 } } - } + } }else if(condition === 'right_in') { if (isStartEnd.start ) { @@ -1065,7 +1082,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { if(inLine){ - getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + if(inLine.x2 < inLine.x1) { + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: bStartY, x: wallLine.x2 }, 'pink') + } } getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') @@ -1075,7 +1096,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPStart.y = Big(newPStart.y).plus(cLineY).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) if(inLine){ - getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.x2 < inLine.x1 ) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPStart.y, x: newPStart.x }, 'purple') + } }else { newPStart.y = wallLine.y1; } @@ -1096,7 +1121,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { let idx = (roofLines.length < index + 1)?0:index const newLine = roofLines[idx+1]; if(inLine){ - getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + if(inLine.x2 < inLine.x1) { + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: bStartY, x: wallLine.x1 }, 'pink') + } } if(Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') @@ -1107,7 +1136,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPEnd.y = Big(newPEnd.y).minus(cLineY).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ - getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.x2 < inLine.x1) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } }else { newPEnd.y = wallLine.y2; } @@ -1205,7 +1238,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { if(inLine){ - getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + if(inLine.y2 > inLine.y1 ) { + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + }else{ + getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink') + } } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta') getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') @@ -1215,7 +1252,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPStart.x = Big(newPStart.x).plus(cLineX).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) if(inLine){ - getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.y2 > inLine.y1 ) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x } , 'purple') + } + }else { newPStart.x = wallLine.x1; } @@ -1236,7 +1278,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { if(inLine){ - getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + if(inLine.y2 > inLine.y1 ){ + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + }else{ + getAddLine({ x: inLine.x1, y: inLine.y1 },{ x: bStartX, y: wallLine.y1 }, 'pink') + } + } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') @@ -1246,7 +1293,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPEnd.x = Big(newPEnd.x).minus(cLineX).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ - getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.y2 > inLine.y1 ) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple') + } }else { newPEnd.x = wallLine.x2; } @@ -1315,17 +1366,25 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { if(inLine){ - getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + if(inLine.y2 < inLine.y1 ) { + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + }else{ + getAddLine({ x: inLine.x1, y: inLine.y1 },{ x: bStartX, y: wallLine.y1 }, 'pink') + } } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta') getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') - findPoints.push({ x: aStartX, y: newPEnd.y }); + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'bottom_out_start' }); }else{ const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber() newPStart.x = Big(newPStart.x).minus(cLineX).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) if(inLine){ - getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.y2 < inLine.y1 ) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } }else{ newPStart.x = wallLine.x1; } @@ -1347,7 +1406,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if(Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { if(inLine){ - getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + if(inLine.y2 < inLine.y1 ) { + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + }else{ + getAddLine({ x: inLine.x2, y: inLine.y2 }, { x: bStartX, y: wallLine.y1 }, 'pink') + } } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') @@ -1357,7 +1420,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPEnd.x = Big(newPEnd.x).plus(cLineX).toNumber(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ - getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + if(inLine.y2 < inLine.y1 ) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + }else{ + getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple') + } }else{ newPEnd.x = wallLine.x2; } @@ -3064,6 +3131,38 @@ function updateAndAddLine(innerLines, targetPoint) { if(foundLine.x2 <= foundLine.x1){ isUpdatingStart = true; } + }else if(targetPoint.position === "top_out_start"){ + if(foundLine.y2 >= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "top_out_end"){ + if(foundLine.y2 > foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "bottom_out_start"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "bottom_out_end"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_out_start"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_out_end"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_out_start"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_out_end"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } } const updatedLine = { From 540812861dafb24603262b8917ba428a0ac3e2bc Mon Sep 17 00:00:00 2001 From: yscha Date: Wed, 10 Dec 2025 00:40:53 +0900 Subject: [PATCH 08/14] sortedBaseLines --- src/util/skeleton-utils.js | 105 ++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index f9a9360d..a7bcb46f 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -754,6 +754,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines); const sortedRoofLines = sortCurrentRoofLines(roofLines); const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines); + const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines); //wall.lines 는 기본 벽 라인 @@ -772,8 +773,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const roofLine = roofLines[index]; const currentRoofLine = currentRoofLines[index]; - const moveLine = wall.baseLines[index] - const wallBaseLine = wall.baseLines[index] + const moveLine = sortedBaseLines[index] + const wallBaseLine = sortedBaseLines[index] //roofline 외곽선 설정 @@ -861,14 +862,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //두 포인트가 변경된 라인인 if (fullyMoved ) { //반시계방향향 - console.log("moveFully:::::::::::::", wallBaseLine, newPStart, newPEnd) - console.log("moveFully:::::::::::::", roofLine.direction) + const mLine = getSelectLinePosition(wall, wallBaseLine) if (getOrientation(roofLine) === 'vertical') { if (['left', 'right'].includes(mLine.position)) { - if(wallLine.x1 === wallBaseLine.x1) { + if(Math.abs(wallLine.x1 - wallBaseLine.x1) < 0.1 || Math.abs(wallLine.x2 - wallBaseLine.x2) < 0.1) { return false } const positionType = @@ -876,7 +876,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { (mLine.position === 'right' && wallLine.x1 > wallBaseLine.x1) ? 'in' : 'out'; const condition = `${mLine.position}_${positionType}`; - let isStartEnd = findInteriorPoint(wallBaseLine, wall.baseLines) + let isStartEnd = findInteriorPoint(wallBaseLine, sortedBaseLines) let sPoint, ePoint; if(condition === 'left_in') { isIn = true @@ -1014,6 +1014,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } } + findPoints.push({ y: newPStart.y, x: newPEnd.x, position: 'left_out_end' }); } }else if(condition === 'right_in') { if (isStartEnd.start ) { @@ -1165,7 +1166,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } else if (getOrientation(roofLine) === 'horizontal') { //red if (['top', 'bottom'].includes(mLine.position)) { - if(Math.abs(wallLine.y1 - wallBaseLine.y1) < 0.1) { + if(Math.abs(wallLine.y1 - wallBaseLine.y1) < 0.1 || Math.abs(wallLine.y2 - wallBaseLine.y2) < 0.1) { return false } const positionType = @@ -1174,7 +1175,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ? 'in' : 'out'; const condition = `${mLine.position}_${positionType}`; - let isStartEnd = findInteriorPoint(wallBaseLine, wall.baseLines) + let isStartEnd = findInteriorPoint(wallBaseLine, sortedBaseLines) let sPoint, ePoint; @@ -1401,15 +1402,15 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() newPEnd.x = aStartX newPStart.x = Big(roofLine.x1).plus(eLineX).toNumber() - let idx = (0 > index - 1)?roofLines.length:index - const newLine = roofLines[idx-1]; + let idx = (roofLines.length < index + 1)?0:index + const newLine = roofLines[idx + 1]; if(Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { if(inLine){ if(inLine.y2 < inLine.y1 ) { getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') }else{ - getAddLine({ x: inLine.x2, y: inLine.y2 }, { x: bStartX, y: wallLine.y1 }, 'pink') + getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink') } } getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') @@ -3136,7 +3137,7 @@ function updateAndAddLine(innerLines, targetPoint) { isUpdatingStart = true; } }else if(targetPoint.position === "top_out_end"){ - if(foundLine.y2 > foundLine.y1){ + if(foundLine.y2 >= foundLine.y1){ isUpdatingStart = true; } }else if(targetPoint.position === "bottom_out_start"){ @@ -3317,4 +3318,82 @@ function findInteriorPoint(line, polygonLines) { start: startIsValley, end: endIsValley }; -} \ No newline at end of file +} + +/** + * baseLines의 순서를 wallLines의 순서와 일치시킵니다. + * 각 wallLine에 대해 가장 가깝고 평행한 baseLine을 찾아 정렬된 배열을 반환합니다. + * + * @param {Array} baseLines - 정렬할 원본 baseLine 배열 + * @param {Array} wallLines - 기준이 되는 wallLine 배열 + * @returns {Array} wallLines 순서에 맞춰 정렬된 baseLines + */ +export const sortBaseLinesByWallLines = (baseLines, wallLines) => { + if (!baseLines || !wallLines || baseLines.length === 0 || wallLines.length === 0) { + return baseLines; + } + + const sortedBaseLines = []; + const usedIndices = new Set(); // 이미 매칭된 baseLine 인덱스를 추적 + + wallLines.forEach((wallLine) => { + let bestMatchIndex = -1; + let minDistance = Infinity; + + // wallLine의 중점 계산 + const wMidX = (wallLine.x1 + wallLine.x2) / 2; + const wMidY = (wallLine.y1 + wallLine.y2) / 2; + + // wallLine의 방향 벡터 (평행 확인용) + const wDx = wallLine.x2 - wallLine.x1; + const wDy = wallLine.y2 - wallLine.y1; + const wLen = Math.hypot(wDx, wDy); + + 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; + } + } + } + }); + + 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]); + } + } + }); + + return sortedBaseLines; +}; \ No newline at end of file From c5d830f6c01ffe7ac35addf7f7c8fe8cb40c24fb Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 10 Dec 2025 10:24:37 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=ED=94=8C=EB=9E=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20=EC=8B=9C=20=EC=99=B8=EB=B2=BD=EC=84=A0=20=EB=82=A8=EC=95=84?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=A7=80=EB=B6=95=EC=9E=AC=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=A7=80=EB=B6=95=EC=9E=AC=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A7=80=EA=B3=A0=20=EC=9E=88=EB=8A=94=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=EC=A7=80=EB=B6=95=EB=93=A4=EC=9D=98=20=EC=A7=80?= =?UTF-8?q?=EB=B6=95=EC=9E=AC=EB=8F=84=20=EB=B3=80=EA=B2=BD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/CanvasFrame.jsx | 3 +++ src/hooks/roofcover/useRoofAllocationSetting.js | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/floor-plan/CanvasFrame.jsx b/src/components/floor-plan/CanvasFrame.jsx index 63dc523a..9441dc7c 100644 --- a/src/components/floor-plan/CanvasFrame.jsx +++ b/src/components/floor-plan/CanvasFrame.jsx @@ -32,6 +32,7 @@ import { useEvent } from '@/hooks/useEvent' import { compasDegAtom } from '@/store/orientationAtom' import { hotkeyStore } from '@/store/hotkeyAtom' import { usePopup } from '@/hooks/usePopup' +import { outerLinePointsState } from '@/store/outerLineAtom' export default function CanvasFrame() { const canvasRef = useRef(null) @@ -45,6 +46,7 @@ export default function CanvasFrame() { const totalDisplay = useRecoilValue(totalDisplaySelector) // 집계표 표시 여부 const { setIsGlobalLoading } = useContext(QcastContext) const resetModuleStatisticsState = useResetRecoilState(moduleStatisticsState) + const resetOuterLinePoints = useResetRecoilState(outerLinePointsState) const resetMakersState = useResetRecoilState(makersState) const resetSelectedMakerState = useResetRecoilState(selectedMakerState) const resetSeriesState = useResetRecoilState(seriesState) @@ -137,6 +139,7 @@ export default function CanvasFrame() { const resetRecoilData = () => { // resetModuleStatisticsState() + resetOuterLinePoints() resetMakersState() resetSelectedMakerState() resetSeriesState() diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index 3ea1894c..4bc26098 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -374,11 +374,18 @@ export function useRoofAllocationSetting(id) { setBasicSetting((prev) => { return { ...prev, selectedRoofMaterial: newRoofList.find((roof) => roof.selected) } }) + const selectedRoofMaterial = newRoofList.find((roof) => roof.selected) + const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && obj.roofMaterial?.index === selectedRoofMaterial.index) + + roofs.forEach((roof) => { + setSurfaceShapePattern(roof, roofDisplay.column, false, { ...selectedRoofMaterial }, true) + drawDirectionArrow(roof) + }) setRoofList(newRoofList) setRoofMaterials(newRoofList) setRoofsStore(newRoofList) - const selectedRoofMaterial = newRoofList.find((roof) => roof.selected) + setSurfaceShapePattern(currentObject, roofDisplay.column, false, selectedRoofMaterial, true) drawDirectionArrow(currentObject) modifyModuleSelectionData() From 2205809d0b2abe09bc125f89168ac4ec9ccd426f Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 10 Dec 2025 11:20:10 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=EC=8B=9C=EB=AE=AC=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/simulator/Simulator.jsx | 89 ++++++++++++-------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/src/components/simulator/Simulator.jsx b/src/components/simulator/Simulator.jsx index 9831d1b3..d873049f 100644 --- a/src/components/simulator/Simulator.jsx +++ b/src/components/simulator/Simulator.jsx @@ -264,7 +264,6 @@ export default function Simulator() { style={{ width: '30%' }} className="select-light" value={pwrGnrSimType} - defaultValue={`D`} onChange={(e) => { handleChartChangeData(e.target.value) setPwrGnrSimType(e.target.value) @@ -334,33 +333,31 @@ export default function Simulator() { - {moduleInfoList.length > 0 ? ( - moduleInfoList.map((moduleInfo) => { - return ( - <> - - {/* 지붕면 */} - {moduleInfo.roofSurface} - {/* 경사각 */} - - {convertNumberToPriceDecimal(moduleInfo.slopeAngle)} - {moduleInfo.classType == 0 ? '寸' : 'º'} - - {/* 방위각(도) */} - {convertNumberToPriceDecimal(moduleInfo.azimuth)} - {/* 태양전지모듈 */} - -
{moduleInfo.itemNo}
- - {/* 매수 */} - {convertNumberToPriceDecimal(moduleInfo.amount)} - - - ) - }) - ) : ( - - {getMessage('common.message.no.data')} + {moduleInfoList.length > 0 ? ( + moduleInfoList.map((moduleInfo) => { + return ( + + {/* 지붕면 */} + {moduleInfo.roofSurface} + {/* 경사각 */} + + {convertNumberToPriceDecimal(moduleInfo.slopeAngle)} + {moduleInfo.classType == 0 ? '寸' : 'º'} + + {/* 방위각(도) */} + {convertNumberToPriceDecimal(moduleInfo.azimuth)} + {/* 태양전지모듈 */} + +
{moduleInfo.itemNo}
+ + {/* 매수 */} + {convertNumberToPriceDecimal(moduleInfo.amount)} + + ) + }) + ) : ( + + {getMessage('common.message.no.data')} )} @@ -385,25 +382,23 @@ export default function Simulator() { - {pcsInfoList.length > 0 ? ( - pcsInfoList.map((pcsInfo) => { - return ( - <> - - {/* 파워컨디셔너 */} - -
{pcsInfo.itemNo}
- - {/* 대 */} - {convertNumberToPriceDecimal(pcsInfo.amount)} - - - ) - }) - ) : ( - - {getMessage('common.message.no.data')} - + {pcsInfoList.length > 0 ? ( + pcsInfoList.map((pcsInfo) => { + return ( + + {/* 파워컨디셔너 */} + +
{pcsInfo.itemNo}
+ + {/* 대 */} + {convertNumberToPriceDecimal(pcsInfo.amount)} + + ) + }) + ) : ( + + {getMessage('common.message.no.data')} + )} From fef352933f978a4934f6df941fae8beb87245a31 Mon Sep 17 00:00:00 2001 From: yscha Date: Thu, 11 Dec 2025 01:51:04 +0900 Subject: [PATCH 11/14] =?UTF-8?q?sortRoofLines=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 239 ++++++++++++++++++++++++------------- 1 file changed, 154 insertions(+), 85 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index a7bcb46f..c9a905d7 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' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -317,10 +318,29 @@ const movingLineFromSkeleton = (roofId, canvas) => { * @param baseLines */ 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) { + // 개선된 코드 (기하학적 매칭) + // or use some other unique properties + + + roof.lines.forEach((rLine, index) => { + // 벽 라인 중에서 시작점과 끝점이 일치하는 라인 찾기 + const wLine = wallObj.lines[index] + if (wLine) { + // 안정적인 ID 생성 + rLine.attributes.wallLine = wLine.id; // Use the stable ID + + // ... + } + }) + } const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE] const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] @@ -329,40 +349,35 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) //const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) - const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || []; - const baseLinePoints = baseLines.map((line) => ({x:line.left, y:line.top})); + const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || [] + const baseLinePoints = baseLines.map((line) => ({ x: line.left, y: line.top })) - const outerLines = canvas.getObjects().filter((object) => object.name === 'outerLinePoint') || []; - const outerLinePoints = outerLines.map((line) => ({x:line.left, y:line.top})) + const outerLines = canvas.getObjects().filter((object) => object.name === 'outerLinePoint') || [] + const outerLinePoints = outerLines.map((line) => ({ x: line.left, y: line.top })) - const hipLines = canvas.getObjects().filter((object) => object.name === 'hip' && object.parentId === roofId) || []; - const ridgeLines = canvas.getObjects().filter((object) => object.name === 'ridge' && object.parentId === roofId) || []; + const hipLines = canvas.getObjects().filter((object) => object.name === 'hip' && object.parentId === roofId) || [] + const ridgeLines = canvas.getObjects().filter((object) => object.name === 'ridge' && object.parentId === roofId) || [] //const skeletonLines = []; // 1. 지붕 폴리곤 좌표 전처리 - const coordinates = preprocessPolygonCoordinates(roof.points); + const coordinates = preprocessPolygonCoordinates(roof.points) if (coordinates.length < 3) { - console.warn("Polygon has less than 3 unique points. Cannot generate skeleton."); - return; + console.warn('Polygon has less than 3 unique points. Cannot generate skeleton.') + return } - const moveFlowLine = roof.moveFlowLine || 0; // Provide a default value - const moveUpDown = roof.moveUpDown || 0; // Provide a default value - - - - let points = roof.points; + const moveFlowLine = roof.moveFlowLine || 0 // Provide a default value + const moveUpDown = roof.moveUpDown || 0 // Provide a default value + let points = roof.points //마루이동 if (moveFlowLine !== 0 || moveUpDown !== 0) { - points = movingLineFromSkeleton(roofId, canvas) } - - console.log('points:', points); + console.log('points:', points) const geoJSONPolygon = toGeoJSON(points) try { @@ -371,7 +386,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) // 스켈레톤 데이터를 기반으로 내부선 생성 - roof.innerLines = roof.innerLines || []; + roof.innerLines = roof.innerLines || [] roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) // 캔버스에 스켈레톤 상태 저장 @@ -380,12 +395,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { canvas.skeletonLines = [] } canvas.skeletonStates[roofId] = true - canvas.skeletonLines = []; + canvas.skeletonLines = [] canvas.skeletonLines.push(...roof.innerLines) - roof.skeletonLines = canvas.skeletonLines; + roof.skeletonLines = canvas.skeletonLines const cleanSkeleton = { - Edges: skeleton.Edges.map(edge => ({ + Edges: skeleton.Edges.map((edge) => ({ X1: edge.Edge.Begin.X, Y1: edge.Edge.Begin.Y, X2: edge.Edge.End.X, @@ -396,15 +411,14 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { })), roofId: roofId, // Add other necessary top-level properties - }; - canvas.skeleton = []; + } + canvas.skeleton = [] canvas.skeleton = cleanSkeleton canvas.skeleton.lastPoints = points - canvas.set("skeleton", cleanSkeleton); + canvas.set('skeleton', cleanSkeleton) canvas.renderAll() - - console.log('skeleton rendered.', canvas); + console.log('skeleton rendered.', canvas) } catch (e) { console.error('스켈레톤 생성 중 오류 발생:', e) if (canvas.skeletonStates) { @@ -578,7 +592,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ); //그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함 - let roofIdx = 0; + // roofLines.forEach((roofLine) => { // @@ -589,13 +603,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 +773,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 +789,20 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // const moveLine = sortedWallBaseLines[index] // const wallBaseLine = sortedWallBaseLines[index] + const roofLine = sortRoofLines[index]; + if(roofLine.attributes.wallLine !== wallLine.id || (roofLine.idx - 1) !== index ){ + console.log("wallLine2::::", wallLine.id) + console.log('roofLine:::',roofLine.attributes.wallLine) + console.log("w:::",wallLine.startPoint, wallLine.endPoint) + console.log("R:::",roofLine.startPoint, roofLine.endPoint) + console.log("not matching roofLine", roofLine); + return false + }//roofLines.find(line => line.attributes.wallLineId === wallLine.attributes.wallId); - const roofLine = roofLines[index]; const currentRoofLine = currentRoofLines[index]; const moveLine = sortedBaseLines[index] const wallBaseLine = sortedBaseLines[index] + console.log("wallBaseLine", wallBaseLine); //roofline 외곽선 설정 @@ -1448,7 +1476,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // } } } + getAddLine(newPStart, newPEnd, 'red') + //canvas.remove(roofLine) } canvas.renderAll() }); @@ -1475,14 +1505,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 +1565,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 +1690,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 +1727,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 +3369,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 +3381,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 From a6e935bc15db88e0a9aee467cf49bc74241124fa Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 12 Dec 2025 15:20:28 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=EB=8F=84=EB=A8=B8=20=EC=98=A4=ED=94=84?= =?UTF-8?q?=EC=85=8B=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/object/DormerOffset.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/floor-plan/modal/object/DormerOffset.jsx b/src/components/floor-plan/modal/object/DormerOffset.jsx index fd3eb70a..1881b18e 100644 --- a/src/components/floor-plan/modal/object/DormerOffset.jsx +++ b/src/components/floor-plan/modal/object/DormerOffset.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { useMessage } from '@/hooks/useMessage' import WithDraggable from '@/components/common/draggable/WithDraggable' import { useRecoilValue } from 'recoil' @@ -15,8 +15,8 @@ export default function DormerOffset(props) { const { closePopup } = usePopup() const [arrow1, setArrow1] = useState(null) const [arrow2, setArrow2] = useState(null) - const arrow1LengthRef = useRef() - const arrow2LengthRef = useRef() + const arrow1LengthRef = useRef(0) + const arrow2LengthRef = useRef(0) const [arrow1Length, setArrow1Length] = useState(0) const [arrow2Length, setArrow2Length] = useState(0) @@ -59,12 +59,12 @@ export default function DormerOffset(props) { name="" label="" className="input-origin block" - value={arrow1LengthRef.current.value} + value={arrow1LengthRef.current.value ?? 0} ref={arrow1LengthRef} onChange={(value) => setArrow1Length(value)} options={{ allowNegative: false, - allowDecimal: false + allowDecimal: false, }} /> From 53a1f4ed0053b7833e49f71f25cbf506309da6f3 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 12 Dec 2025 15:27:26 +0900 Subject: [PATCH 13/14] =?UTF-8?q?group=20object=20=EC=A2=8C=ED=91=9C=20?= =?UTF-8?q?=EC=9E=AC=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/fabric-extensions.js | 37 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/util/fabric-extensions.js b/src/util/fabric-extensions.js index 9410f764..acccc601 100644 --- a/src/util/fabric-extensions.js +++ b/src/util/fabric-extensions.js @@ -29,22 +29,39 @@ fabric.Rect.prototype.getCurrentPoints = function () { /** * fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용) - * 그룹의 groupPoints를 다시 계산하여 반환 + * 그룹 내 객체들의 점들을 수집하여 현재 월드 좌표를 반환 */ fabric.Group.prototype.getCurrentPoints = function () { - // groupPoints를 다시 계산 + // 그룹 내 객체들로부터 실시간으로 점들을 계산 + if (this._objects && this._objects.length > 0) { + let allPoints = [] - // 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우) - if (this.groupPoints && Array.isArray(this.groupPoints)) { - const matrix = this.calcTransformMatrix() - console.log('this.groupPoints', this.groupPoints) - return this.groupPoints.map(function (p) { - const point = new fabric.Point(p.x, p.y) - return fabric.util.transformPoint(point, matrix) + // 그룹 내 모든 객체의 점들을 수집 + this._objects.forEach(function (obj) { + if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') { + const objPoints = obj.getCurrentPoints() + allPoints = allPoints.concat(objPoints) + } else if (obj.points && Array.isArray(obj.points)) { + const pathOffset = obj.pathOffset || { x: 0, y: 0 } + const matrix = obj.calcTransformMatrix() + const transformedPoints = obj.points + .map(function (p) { + return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y) + }) + .map(function (p) { + return fabric.util.transformPoint(p, matrix) + }) + allPoints = allPoints.concat(transformedPoints) + } }) + + if (allPoints.length > 0) { + // Convex Hull 알고리즘을 사용하여 외곽 점들만 반환 + return this.getConvexHull(allPoints) + } } - // groupPoints가 없으면 바운딩 박스를 사용 + // 객체가 없으면 바운딩 박스를 사용 const bounds = this.getBoundingRect() const points = [ { x: bounds.left, y: bounds.top }, From be6fdce63c06282c7f29e535de68a28893cbc63e Mon Sep 17 00:00:00 2001 From: yscha Date: Sat, 13 Dec 2025 17:04:59 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=ED=95=A0=EB=8B=B9=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index c9a905d7..cbbc4127 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -766,15 +766,19 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { }); }; - const sortedWallLines = sortCurrentRoofLines(wall.lines); + // const sortedWallLines = sortCurrentRoofLines(wall.lines); // roofLines의 방향에 맞춰 currentRoofLines 조정 후 정렬 const alignedCurrentRoofLines = alignLineDirection(currentRoofLines, roofLines); const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines); - const sortedRoofLines = sortCurrentRoofLines(roofLines); + // const sortedRoofLines = sortCurrentRoofLines(roofLines); const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines); - const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines); + // const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines); const sortRoofLines = sortBaseLinesByWallLines(roofLines, wallLines); + // 원본 wallLines를 복사하여 사용 + const sortedWallLines = [...wallLines]; + const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, sortedWallLines); + const sortedRoofLines = sortBaseLinesByWallLines(roofLines, sortedWallLines); //wall.lines 는 기본 벽 라인 //wall.baseLine은 움직인라인 @@ -802,7 +806,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const currentRoofLine = currentRoofLines[index]; const moveLine = sortedBaseLines[index] const wallBaseLine = sortedBaseLines[index] - console.log("wallBaseLine", wallBaseLine); + //console.log("wallBaseLine", wallBaseLine); //roofline 외곽선 설정 @@ -876,7 +880,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { return line } - getAddLine(roofLine.startPoint, roofLine.endPoint, ) + //getAddLine(roofLine.startPoint, roofLine.endPoint, ) //외곽선을 그린다 newPStart = { x: roofLine.x1, y: roofLine.y1 } newPEnd = { x: roofLine.x2, y: roofLine.y2 } @@ -1228,6 +1232,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') } //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') + } if(isStartEnd.end){ @@ -1479,6 +1484,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine(newPStart, newPEnd, 'red') //canvas.remove(roofLine) + }else{ + getAddLine(roofLine.startPoint, roofLine.endPoint, ) } canvas.renderAll() });