diff --git a/src/components/fabric/QLine.js b/src/components/fabric/QLine.js index 77254d5d..02448cd7 100644 --- a/src/components/fabric/QLine.js +++ b/src/components/fabric/QLine.js @@ -16,6 +16,7 @@ export const QLine = fabric.util.createClass(fabric.Line, { children: [], padding: 5, textVisible: true, + textBaseline: 'alphabetic', initialize: function (points, options, length = 0) { // 소수점 전부 제거 diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 1c2e3d65..8186b449 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -6,6 +6,7 @@ import { calculateAngle, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON } import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' +import { drawSkeletonRidgeRoof } from '@/util/skeleton-utils' export const QPolygon = fabric.util.createClass(fabric.Polygon, { type: 'QPolygon', diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index 8b1f7dc8..51c16c1a 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -370,7 +370,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla }} options={{ allowNegative: false, - allowDecimal: false //(index !== 0), + allowDecimal: true //(index !== 0), }} /> diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 6294faaf..95c5cb7b 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -470,7 +470,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } }); - +/* //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); @@ -492,8 +492,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { trimIntersectingExtendedLines(skeletonLines, disconnectedLines); } +*/ - + //2. 연결이 끊어진 라인이 있을경우 찾아서 추가한다(동 이동일때) // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다. const innerLines = []; @@ -539,6 +540,135 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { innerLine.bringToFront(); existingLines.add(lineKey); // 추가된 라인을 추적 }else{ + + + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + const wallLines = wall.baseLines + // 현재 지점과 다음 지점을 비교하기 위한 변수 + let changedLine = roof.moveSelectLine; + const roofLines = []; + + + if (!wall.lines || !wall.baseLines) { + return wall.baseLines || wall.lines || []; + } + + // 길이가 다른 경우 baseLines 반환 + if (wall.lines.length !== wall.baseLines.length) { + return wall.baseLines; + } + + for (let i = 0; i < wall.baseLines.length; i++) { + const baseLine = wall.baseLines[i]; + const line = wall.lines[i]; + + if (!line || + ((!isSamePoint(baseLine.startPoint, line.startPoint)) && // 시작점이 다르고 + (!isSamePoint(baseLine.endPoint, line.endPoint)))) { // 끝점도 다른 경우 + + } + } + + + const startClosest = findClosestRoofLine(p1, roof.lines); + const endClosest = findClosestRoofLine(p2, roof.lines); + + + const { point, closest, selectPoint, otherPoint } = + startClosest.distance > endClosest.distance + ? { + point : p2, + closest : endClosest, + otherPoint: p1 + } + : { + point : p1, + closest : startClosest, + otherPoint: p2 + }; + +// Log the relevant information + console.log("Point:", point); + console.log("Closest intersection:", closest); + console.log("moveSelectLinePoint:", selectPoint); + let isTarget = false; + for(const roofLine of roof.lines){ + if( isSamePoint(p1, roofLine.startPoint) || + isSamePoint(p2, roofLine.endPoint) || + isSamePoint(p1, roofLine.endPoint) || + isSamePoint(p2, roofLine.startPoint) ) { + isTarget = true ; + break + } + } + if (isTarget) { + console.warn("matching line found in roof.lines"); + return; // 또는 적절한 오류 처리 + } + + const innerLine2 = new QLine([p1.x, p1.y, p2.x, p2.y], { + parentId: roof.id, + fontSize: roof.fontSize, + stroke: 'red', + strokeWidth: lineStyle.width, + name: (line.attributes.isOuterEdge)?'eaves': attributes.type, + attributes: attributes, + direction: direction, + isBaseLine: line.attributes.isOuterEdge, + lineName: (line.attributes.isOuterEdge)?'addLine': attributes.type, + selectable:(!line.attributes.isOuterEdge), + roofId: roofId, + }); + + canvas.add(innerLine2); + //existingLines.add(lineKey); // 추가된 라인을 추적 + /* + //라인추가(까지 못할때때) 외벽선에서 추가 + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + const wallLines = wall.baseLines + // 현재 지점과 다음 지점을 비교하기 위한 변수 + let changedLine = roof.moveSelectLine; + const roofLines = []; + + + if (!wall.lines || !wall.baseLines) { + return wall.baseLines || wall.lines || []; + } + + // 길이가 다른 경우 baseLines 반환 + if (wall.lines.length !== wall.baseLines.length) { + return wall.baseLines; + } + + + //그려지는 처마라인이 처마 && 포인터모두가 wall.baseLine에 들어가 있는 경우 + const checkPoint = {x1:line.x1, y1:line.y1, x2:line.x2, y2:line.y2} + if(line.attributes.type === 'hip' && !checkPointInPolygon(checkPoint, wall)) { + const startClosest = findClosestRoofLine(p1, roof.lines); + const endClosest = findClosestRoofLine(p2, roof.lines); + console.log("Lindd::::",line) + const { point, closest, selectPoint, otherPoint } = + startClosest.distance > endClosest.distance + ? { + point : p2, + closest : endClosest, + //selectPoint : changedLine.endPoint, + otherPoint: p1 + } + : { + point : p1, + closest : startClosest, + //selectPoint : changedLine.startPoint, + otherPoint: p2 + }; + +// Log the relevant information + console.log("Point:", point); + console.log("Closest intersection:", closest); + console.log("moveSelectLinePoint:", selectPoint); + } +*/ + const coordinateText = new fabric.Text(`(${Math.round(p1.x)}, ${Math.round(p1.y)})`, { left: p1.x + 5, // 좌표점에서 약간 오른쪽으로 이동 top: p1.y - 20, // 좌표점에서 약간 위로 이동 @@ -582,8 +712,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 @@ -615,7 +745,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); + 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); // } @@ -734,13 +864,13 @@ function isOuterEdge(p1, p2, edges) { * 스켈레톤 라인 배열에 새로운 라인을 추가합니다. (중복 방지) * @param id * @param {Array} skeletonLines - 스켈레톤 라인 배열 - * @param {Set} processedInnerEdges - 처리된 Edge 키 Set * @param {object} p1 - 시작점 * @param {object} p2 - 끝점 * @param {string} lineType - 라인 타입 * @param {string} color - 색상 * @param {number} width - 두께 - * @param currentDegree + * @param pitch + * @param isOuterLine */ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine) { // const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|'); @@ -1560,14 +1690,14 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { // p2가 다각형 내부에 있는지 확인 const p2Inside = isPointInsidePolygon(p2, roofLines); - console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); + //console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); // 두 점 모두 내부에 있으면 그대로 반환 if (p1Inside && p2Inside) { if(!selectLine || isDiagonal){ return { p1: clippedP1, p2: clippedP2 }; } - console.log('평행선::', clippedP1, clippedP2) + //console.log('평행선::', clippedP1, clippedP2) return { p1: clippedP1, p2: clippedP2 }; } @@ -1600,20 +1730,20 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { if (!p1Inside && !p2Inside) { // 두 점 모두 외부에 있는 경우 if (intersections.length >= 2) { - console.log('Both outside, using intersection points'); + //console.log('Both outside, using intersection points'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; clippedP2.x = intersections[1].point.x; clippedP2.y = intersections[1].point.y; } else { - console.log('Both outside, no valid intersections - returning original'); + //console.log('Both outside, no valid intersections - returning original'); // 교차점이 충분하지 않으면 원본 반환 return { p1: clippedP1, p2: clippedP2 }; } } else if (!p1Inside && p2Inside) { // p1이 외부, p2가 내부 if (intersections.length > 0) { - console.log('p1 outside, p2 inside - moving p1 to intersection'); + //console.log('p1 outside, p2 inside - moving p1 to intersection'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; // p2는 이미 내부에 있으므로 원본 유지 @@ -1623,7 +1753,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { } else if (p1Inside && !p2Inside) { // p1이 내부, p2가 외부 if (intersections.length > 0) { - console.log('p1 inside, p2 outside - moving p2 to intersection'); + //console.log('p1 inside, p2 outside - moving p2 to intersection'); // p1은 이미 내부에 있으므로 원본 유지 clippedP1.x = p1.x; clippedP1.y = p1.y; @@ -1641,7 +1771,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { * @param {Array} roofLines - 다각형을 구성하는 선분들 * @returns {boolean} 점이 다각형 내부에 있으면 true */ -function isPointInsidePolygon(point, roofLines) { +function isPointInsidePolygon2(point, roofLines) { let inside = false; const x = point.x; const y = point.y; @@ -1661,6 +1791,72 @@ function isPointInsidePolygon(point, roofLines) { return inside; } +function isPointInsidePolygon(point, roofLines) { + // 1. 먼저 경계선 위에 있는지 확인 (방향 무관) + if (isOnBoundaryDirectionIndependent(point, roofLines)) { + return true; + } + + // 2. 내부/외부 판단 (기존 알고리즘) + let winding = 0; + const x = point.x; + const y = point.y; + + for (let i = 0; i < roofLines.length; i++) { + const line = roofLines[i]; + const x1 = line.x1, y1 = line.y1; + const x2 = line.x2, y2 = line.y2; + + if (y1 <= y) { + if (y2 > y) { + const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1); + if (orientation > 0) winding++; + } + } else { + if (y2 <= y) { + const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1); + if (orientation < 0) winding--; + } + } + } + + return winding !== 0; +} + +// 방향에 무관한 경계선 검사 +function isOnBoundaryDirectionIndependent(point, roofLines) { + const tolerance = 1e-10; + + for (const line of roofLines) { + if (isPointOnLineSegmentDirectionIndependent(point, line, tolerance)) { + return true; + } + } + return false; +} + +// 핵심: 방향에 무관한 선분 위 점 검사 +function isPointOnLineSegmentDirectionIndependent(point, line, tolerance) { + const x = point.x, y = point.y; + const x1 = line.x1, y1 = line.y1; + const x2 = line.x2, y2 = line.y2; + + // 방향에 무관하게 경계 상자 체크 + const minX = Math.min(x1, x2); + const maxX = Math.max(x1, x2); + const minY = Math.min(y1, y2); + const maxY = Math.max(y1, y2); + + if (x < minX - tolerance || x > maxX + tolerance || + y < minY - tolerance || y > maxY + tolerance) { + return false; + } + + // 외적을 이용한 직선 위 판단 (방향 무관) + const cross = (y - y1) * (x2 - x1) - (x - x1) * (y2 - y1); + return Math.abs(cross) < tolerance; +} + /** * 선분 위의 점에 대한 매개변수 t를 계산합니다. * p = p1 + t * (p2 - p1)에서 t 값을 구합니다. @@ -1989,19 +2185,19 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const { x1, y1, x2, y2 } = lineCoords; - console.log('wall.points', wall.baseLines); + //console.log('wall.points', wall.baseLines); for(const line of wall.baseLines) { - console.log('line', line); + //console.log('line', line); const basePoint = extractLineCoords(line); const { x1: bx1, y1: by1, x2: bx2, y2: by2 } = basePoint; - console.log('x1, y1, x2, y2', bx1, by1, bx2, by2); + //console.log('x1, y1, x2, y2', bx1, by1, bx2, by2); // 객체 비교 대신 좌표값 비교 if (Math.abs(bx1 - x1) < 0.1 && Math.abs(by1 - y1) < 0.1 && Math.abs(bx2 - x2) < 0.1 && Math.abs(by2 - y2) < 0.1) { - console.log('basePoint 일치!!!', basePoint); + //console.log('basePoint 일치!!!', basePoint); } } @@ -2009,11 +2205,11 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { // 라인 방향 분석 const lineInfo = analyzeLineOrientation(x1, y1, x2, y2, epsilon); - if (debug) { - console.log('=== getSelectLinePosition ==='); - console.log('selectLine 좌표:', lineCoords); - console.log('라인 방향:', lineInfo.orientation); - } + // if (debug) { + // console.log('=== getSelectLinePosition ==='); + // console.log('selectLine 좌표:', lineCoords); + // console.log('라인 방향:', lineInfo.orientation); + // } // 라인의 중점 const midX = (x1 + x2) / 2; @@ -2032,11 +2228,11 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const topIsInside = checkPointInPolygon(topTestPoint, wall); const bottomIsInside = checkPointInPolygon(bottomTestPoint, wall); - if (debug) { - console.log('수평선 테스트:'); - console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); - console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); - } + // if (debug) { + // console.log('수평선 테스트:'); + // console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); + // console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); + // } // top 조건: 위쪽이 외부, 아래쪽이 내부 if (!topIsInside && bottomIsInside) { @@ -2094,9 +2290,9 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { midPoint: { x: midX, y: midY } }; - if (debug) { - console.log('최종 결과:', result); - } + // if (debug) { + // console.log('최종 결과:', result); + // } return result; };