import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' import { SkeletonBuilder } from '@/lib/skeletons' import { arePointsEqual, calcLineActualSize, calcLinePlaneSize, calculateAngle, toGeoJSON } from '@/util/qpolygon-utils' import { QLine } from '@/components/fabric/QLine' import { getDegreeByChon, isPointOnLine } from '@/util/canvas-util' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. * @param {string} roofId - 대상 지붕 객체의 ID * @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체 * @param {string} textMode - 텍스트 표시 모드 */ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { const roof = canvas?.getObjects().find((object) => object.id === roofId) if (!roof) { console.error(`Roof with id "${roofId}" not found.`); return; } // 1. 기존 스켈레톤 라인 제거 const existingSkeletonLines = canvas.getObjects().filter(obj => obj.parentId === roofId && obj.attributes?.type === 'skeleton' ); existingSkeletonLines.forEach(line => canvas.remove(line)); // 2. 지붕 폴리곤 좌표 전처리 const coordinates = preprocessPolygonCoordinates(roof.points); if (coordinates.length < 3) { console.warn("Polygon has less than 3 unique points. Cannot generate skeleton."); return; } const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) //평행선 여부 const hasNonParallelLines = roof.lines.filter((line) => Big(line.x1).minus(Big(line.x2)).gt(1) && Big(line.y1).minus(Big(line.y2)).gt(1)) if (hasNonParallelLines.length > 0) { return } /** 외벽선 */ const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) skeletonBuilder(roofId, canvas, textMode, roof) } /** * 스켈레톤의 edge를 각도가 있는 구간으로 변형합니다. * @param {Object} skeleton - 스켈레톤 객체 * @param {number} edgeIndex - 변형할 edge의 인덱스 * @param {number} angleOffset - 추가할 각도 (도 단위) * @param {number} splitRatio - 분할 비율 (0-1 사이, 0.5면 중간점) * @returns {Object} 변형된 스켈레톤 객체 */ export const transformEdgeWithAngle = (skeleton, edgeIndex, angleOffset = 45, splitRatio = 0.5) => { if (!skeleton || !skeleton.Edges || edgeIndex >= skeleton.Edges.length || edgeIndex < 0) { console.warn('유효하지 않은 스켈레톤 또는 edge 인덱스입니다.') return skeleton } const edgeResult = skeleton.Edges[edgeIndex] if (!edgeResult || !edgeResult.Polygon || !Array.isArray(edgeResult.Polygon)) { console.warn('유효하지 않은 edge 또는 Polygon 데이터입니다.') return skeleton } const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y })) // 변형할 edge 찾기 (가장 긴 내부 선분을 대상으로 함) let longestEdge = null let longestLength = 0 let longestEdgeIndex = -1 for (let i = 0; i < polygonPoints.length; i++) { const p1 = polygonPoints[i] const p2 = polygonPoints[(i + 1) % polygonPoints.length] const length = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) if (length > longestLength) { longestLength = length longestEdge = { p1, p2, index: i } longestEdgeIndex = i } } if (!longestEdge) return skeleton // 중간점 계산 const midPoint = { x: longestEdge.p1.x + (longestEdge.p2.x - longestEdge.p1.x) * splitRatio, y: longestEdge.p1.y + (longestEdge.p2.y - longestEdge.p1.y) * splitRatio, } // 원래 선분의 방향 벡터 const originalVector = { x: longestEdge.p2.x - longestEdge.p1.x, y: longestEdge.p2.y - longestEdge.p1.y, } // 각도 변형을 위한 새로운 점 계산 const angleRad = (angleOffset * Math.PI) / 180 const perpVector = { x: -originalVector.y, y: originalVector.x, } // 정규화 const perpLength = Math.sqrt(perpVector.x * perpVector.x + perpVector.y * perpVector.y) const normalizedPerp = { x: perpVector.x / perpLength, y: perpVector.y / perpLength, } // 각도 변형을 위한 오프셋 거리 (선분 길이의 10%) const offsetDistance = longestLength * 0.1 // 새로운 각도 점 const anglePoint = { x: midPoint.x + normalizedPerp.x * offsetDistance * Math.sin(angleRad), y: midPoint.y + normalizedPerp.y * offsetDistance * Math.sin(angleRad), } // 새로운 폴리곤 점들 생성 const newPolygonPoints = [...polygonPoints] // 기존 점을 제거하고 새로운 세 점으로 교체 newPolygonPoints.splice(longestEdgeIndex + 1, 0, anglePoint) // 스켈레톤 객체 업데이트 - 순환 참조 문제를 방지하기 위해 안전한 복사 방식 사용 const newSkeleton = { ...skeleton, Edges: skeleton.Edges.map((edge, idx) => { if (idx === edgeIndex) { return { ...edge, Polygon: newPolygonPoints.map((p) => ({ X: p.x, Y: p.y })), } } return edge }), } return newSkeleton } /** * 여러 edge를 한 번에 변형합니다. * @param {Object} skeleton - 스켈레톤 객체 * @param {Array} edgeConfigs - 변형 설정 배열 [{edgeIndex, angleOffset, splitRatio}] * @returns {Object} 변형된 스켈레톤 객체 */ export const transformMultipleEdges = (skeleton, edgeConfigs) => { let transformedSkeleton = skeleton // 인덱스 역순으로 정렬하여 변형 시 인덱스 변화를 방지 edgeConfigs.sort((a, b) => b.edgeIndex - a.edgeIndex) edgeConfigs.forEach((config) => { transformedSkeleton = transformEdgeWithAngle(transformedSkeleton, config.edgeIndex, config.angleOffset || 45, config.splitRatio || 0.5) }) return transformedSkeleton } /** * 마루가 있는 지붕을 그린다. * @param roofId * @param canvas * @param textMode * @param roof * @param edgeProperties */ export const skeletonBuilder = (roofId, canvas, textMode, roof) => { // 1. 다각형 좌표를 GeoJSON 형식으로 변환합니다. const geoJSONPolygon = toGeoJSON(roof.points) try { // 2. SkeletonBuilder를 사용하여 스켈레톤을 생성합니다. geoJSONPolygon.pop() let skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) console.log(`지붕 형태: ${skeleton.roof_type}`) // "complex" console.log('Edge 분석:', skeleton.edge_analysis) // 3. 라인을 그림 const innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode) console.log("innerLines::", innerLines) // 4. 생성된 선들을 캔버스에 추가하고 지붕 객체에 저장 // innerLines.forEach((line) => { // canvas.add(line) // line.bringToFront() // canvas.renderAll() // }) roof.innerLines = innerLines // canvas에 skeleton 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {} } canvas.skeletonStates[roofId] = true canvas.renderAll() } catch (e) { console.error('지붕 생성 중 오류 발생:', e) // 오류 발생 시 기존 로직으로 대체하거나 사용자에게 알림 if (canvas.skeletonStates) { canvas.skeletonStates[roofId] = false } } } /** * 스켈레톤 결과와 원본 외벽선 정보를 바탕으로 내부선(마루, 추녀)들을 생성합니다. * @param {object} skeleton - SkeletonBuilder로부터 반환된 스켈레톤 객체 * @param {Array} baseLines - 원본 외벽선 QLine 객체 배열 * @param {QPolygon} roof - 대상 지붕 QPolygon 객체 * @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체 * @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none') * @returns {Array} 생성된 내부선(QLine) 배열 */ // 두 선분이 같은 직선상에 있고 겹치는지 확인하는 함수 const areLinesCollinearAndOverlapping = (line1, line2) => { // 두 선분이 같은 직선상에 있는지 확인 const areCollinear = (p1, p2, p3, p4) => { const area1 = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) const area2 = (p2.x - p1.x) * (p4.y - p1.y) - (p2.y - p1.y) * (p4.x - p1.x) return Math.abs(area1) < 1 && Math.abs(area2) < 1 } // 두 선분이 겹치는지 확인 const isOverlapping = (a1, a2, b1, b2) => { // x축에 평행한 경우 if (Math.abs(a1.y - a2.y) < 1 && Math.abs(b1.y - b2.y) < 1) { if (Math.abs(a1.y - b1.y) > 1) return false return !(Math.max(a1.x, a2.x) < Math.min(b1.x, b2.x) || Math.min(a1.x, a2.x) > Math.max(b1.x, b2.x)) } // y축에 평행한 경우 if (Math.abs(a1.x - a2.x) < 1 && Math.abs(b1.x - b2.x) < 1) { if (Math.abs(a1.x - b1.x) > 1) return false return !(Math.max(a1.y, a2.y) < Math.min(b1.y, b2.y) || Math.min(a1.y, a2.y) > Math.max(b1.y, b2.y)) } return false } return areCollinear(line1.p1, line1.p2, line2.p1, line2.p2) && isOverlapping(line1.p1, line1.p2, line2.p1, line2.p2) } // 겹치는 선분을 하나로 합치는 함수 const mergeCollinearLines = (lines) => { if (lines.length <= 1) return lines const merged = [] const processed = new Set() for (let i = 0; i < lines.length; i++) { if (processed.has(i)) continue let currentLine = lines[i] let mergedLine = { ...currentLine } let wasMerged = false for (let j = i + 1; j < lines.length; j++) { if (processed.has(j)) continue const otherLine = lines[j] if (areLinesCollinearAndOverlapping(mergedLine, otherLine)) { // 겹치는 선분을 하나로 합침 const allPoints = [ { x: mergedLine.p1.x, y: mergedLine.p1.y }, { x: mergedLine.p2.x, y: mergedLine.p2.y }, { x: otherLine.p1.x, y: otherLine.p1.y }, { x: otherLine.p2.x, y: otherLine.p2.y }, ] // x축에 평행한 경우 x 좌표로 정렬 if (Math.abs(mergedLine.p1.y - mergedLine.p2.y) < 1) { allPoints.sort((a, b) => a.x - b.x) mergedLine = { p1: allPoints[0], p2: allPoints[allPoints.length - 1], attributes: mergedLine.attributes, } } // y축에 평행한 경우 y 좌표로 정렬 else { allPoints.sort((a, b) => a.y - b.y) mergedLine = { p1: allPoints[0], p2: allPoints[allPoints.length - 1], attributes: mergedLine.attributes, } } wasMerged = true processed.add(j) } } merged.push(mergedLine) processed.add(i) } return merged } //조건에 따른 스켈레톤을 그린다. const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMode) => { console.log('=== Edge Properties 기반 후처리 시작 ===') if (!skeleton || !skeleton.Edges) return [] const innerLines = [] const processedInnerEdges = new Set() const skeletonLines = [] // 1. 기본 skeleton에서 모든 내부 선분 수집 //edge 순서와 baseLines 순서가 같을수가 없다. for (let edgeIndex = 0; edgeIndex < skeleton.Edges.length; edgeIndex++) { console.log('edgeIndex:::', edgeIndex) let changeEdgeIndex = edgeIndex //입력 폴리곤이 왼쪽 상단에서 시작하면 시계 방향으로 진행합니다. //오른쪽 하단에서 시작하면 그 지점에서부터 시계 방향으로 진행합니다. //edgeIndex 대신에 실제 baseLines 선택라인을 찾아야 한다. const edgeResult = skeleton.Edges[edgeIndex] console.log(edgeResult) // 방향을 고려하지 않고 같은 라인인지 확인하는 함수 let edgeType = 'eaves' let baseLineIndex = 0 processEavesEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges) } for (let edgeIndex = 0; edgeIndex < skeleton.Edges.length; edgeIndex++) { const edgeResult = skeleton.Edges[edgeIndex] const startX = edgeResult.Edge.Begin.X const startY = edgeResult.Edge.Begin.Y const endX = edgeResult.Edge.End.X const endY = edgeResult.Edge.End.Y //외벽선 라인과 같은 edgeResult를 찾는다 for (let baseLineIndex = 0; baseLineIndex < baseLines.length; baseLineIndex++) { if (baseLines[baseLineIndex].attributes.type === 'gable') { // 일다 그려서 skeletonLines를 만들어 //외벽선 동일 라인이면 if (isSameLine(startX, startY, endX, endY, baseLines[baseLineIndex])) { processGableEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges, edgeIndex, baseLineIndex) // break // 매칭되는 라인을 찾았으므로 루프 종료 } } } } console.log(`처리된 skeletonLines: ${skeletonLines.length}개`) // 2. 겹치는 선분 병합 // const mergedLines = mergeCollinearLines(skeletonLines) // console.log('mergedLines', mergedLines) // 3. QLine 객체로 변환 for (const line of skeletonLines) { const { p1, p2, attributes, lineStyle } = line const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, stroke: lineStyle.color, strokeWidth: lineStyle?.width, name: attributes.type, textMode: textMode, attributes: attributes, }) canvas.add(innerLine) innerLine.bringToFront() canvas.renderAll() innerLines.push(innerLine) } return innerLines } // ✅ EAVES (처마) 처리 - 기본 skeleton 모두 사용 function processEavesEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges) { console.log(`processEavesEdge::`, skeletonLines) const begin = edgeResult.Edge.Begin const end = edgeResult.Edge.End //내부 선분 수집 (스케레톤은 다각형) const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y })) for (let i = 0; i < polygonPoints.length; i++) { //시계방향 const p1 = polygonPoints[i] const p2 = polygonPoints[(i + 1) % polygonPoints.length] // 외벽선 제외 후 추가 if(begin !== edgeResult.Polygon[i] && end !== edgeResult.Polygon[i] ) { addRawLine(skeletonLines, processedInnerEdges, p1, p2, 'RIDGE', '#FF0000', 3) } } } // ✅ WALL (벽) 처리 - 선분 개수 최소화 function processWallEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges, edgeIndex) { console.log(`WALL Edge ${edgeIndex}: 내부 선분 최소화`) const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y })) // 벽면은 내부 구조를 단순화 - 주요 선분만 선택 for (let i = 0; i < polygonPoints.length; i++) { const p1 = polygonPoints[i] const p2 = polygonPoints[(i + 1) % polygonPoints.length] if (!isOuterEdge(p1, p2, baseLines)) { // 선분 길이 확인 - 긴 선분만 사용 (짧은 선분 제거) const lineLength = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2) if (lineLength > 10) { // 최소 길이 조건 addRawLine(skeletonLines, processedInnerEdges, p1, p2, 'HIP', '#000000', 2) } else { console.log(`WALL: 짧은 선분 제거 (길이: ${lineLength.toFixed(1)})`) } } } } // ✅ GABLE (케라바) 처리 - 직선 생성, 다른 선분 제거 function processGableEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges, edgeIndex, baseLineIndex) { console.log(`GABLE Edge ${edgeResult}: 직선 skeleton 생성`) const diagonalLine = []; //대각선 라인 const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y })) console.log('polygonPoints::', polygonPoints) // ✅ 케라바는 직선 패턴으로 변경 // 1. 기존 복잡한 skeleton 선분들 무시 // 2. GABLE edge에 수직인 직선 생성 const sourceEdge = edgeResult.Edge const gableStart = { x: sourceEdge.Begin.X, y: sourceEdge.Begin.Y } const gableEnd = { x: sourceEdge.End.X, y: sourceEdge.End.Y } // GABLE edge 중점 const gableMidpoint = { x: (gableStart.x + gableEnd.x) / 2, y: (gableStart.y + gableEnd.y) / 2, } // // // polygonPoints와 gableMidpoint 비교: x 또는 y가 같은 점 찾기 (허용 오차 적용) // const axisTolerance = 0.1 // const sameXPoints = polygonPoints.filter((p) => Math.abs(p.x - gableMidpoint.x) < axisTolerance) // const sameYPoints = polygonPoints.filter((p) => Math.abs(p.y - gableMidpoint.y) < axisTolerance) // if (sameXPoints.length || sameYPoints.length) { // console.log('GABLE: gableMidpoint와 같은 축의 폴리곤 점', { // gableMidpoint, // sameXPoints, // sameYPoints, // }) // } // // 폴리곤 중심점 (대략적) const centerX = polygonPoints.reduce((sum, p) => sum + p.x, 0) / polygonPoints.length const centerY = polygonPoints.reduce((sum, p) => sum + p.y, 0) / polygonPoints.length const polygonCenter = { x: centerX, y: centerY } // // // 허용 오차 // const colinearityTolerance = 0.1 // // // 폴리곤 선분 생성 (연속 점 쌍) // const segments = [] // for (let i = 0; i < polygonPoints.length; i++) { // const p1 = polygonPoints[i] // const p2 = polygonPoints[(i + 1) % polygonPoints.length] // segments.push({ p1, p2 }) // } // // // gableMidpoint와 같은 축(Y 또는 X)에 있는 수직/수평 선분만 추출 // const sameAxisSegments = segments.filter(({ p1, p2 }) => { // const isVertical = Math.abs(p1.x - p2.x) < colinearityTolerance // const isHorizontal = Math.abs(p1.y - p2.y) < colinearityTolerance // const sameXAxis = isVertical && Math.abs(p1.x - gableMidpoint.x) < axisTolerance // const sameYAxis = isHorizontal && Math.abs(p1.y - gableMidpoint.y) < axisTolerance // return sameXAxis || sameYAxis // }) // // // 가장 가까운(또는 가장 긴) 용마루 후보 선택 // let ridgeCandidate = null // if (sameAxisSegments.length) { // // 1) 중점과의 최단거리 기준 // ridgeCandidate = sameAxisSegments.reduce((best, seg) => { // const mid = { x: (seg.p1.x + seg.p2.x) / 2, y: (seg.p1.y + seg.p2.y) / 2 } // const dist2 = (mid.x - gableMidpoint.x) ** 2 + (mid.y - gableMidpoint.y) ** 2 // if (!best) return { seg, score: dist2 } // return dist2 < best.score ? { seg, score: dist2 } : best // }, null)?.seg // // // } // const selectBaseLine = baseLines[baseLineIndex]; console.log('selectBaseLine:', selectBaseLine); console.log('skeletonLines:', skeletonLines) // selectBaseLine의 중간 좌표 계산 const midPoint = { x: (selectBaseLine.x1 + selectBaseLine.x2) / 2, y: (selectBaseLine.y1 + selectBaseLine.y2) / 2 }; console.log('midPoint of selectBaseLine:', midPoint); // 대각선 보정(fallback) 제거: 항상 수평/수직 내부 용마루만 생성 const edgePoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); //제거 for (let i = skeletonLines.length - 1; i >= 0; i--) { const line = skeletonLines[i]; console.log('line:', line) console.log('line.attributes.type:', line.attributes.type) const linePoints = [line.p1, line.p2]; // Check if both points of the line are in the edgePoints const isEdgeLine = linePoints.every(point => edgePoints.some(ep => Math.abs(ep.x - point.x) < 0.001 && Math.abs(ep.y - point.y) < 0.001 ) ); if (isEdgeLine) { skeletonLines.splice(i, 1); } } //확장 // Extend lines that have endpoints in edgePoints to intersect with selectBaseLine // Find diagonal lines (not horizontal or vertical) // Extend lines that have endpoints in edgePoints for (let i = 0; i < skeletonLines.length; i++) { const line = skeletonLines[i]; const p1 = line.p1; const p2 = line.p2; const lineP1 = { x: line.p1.x, y: line.p1.y }; const lineP2 = { x: line.p2.x, y: line.p2.y }; let hasP1 = false; let hasP2 = false; console.log('edgeResult.Edge::',edgeResult.Edge) const lineInfo = findMatchingLinePoints(line, edgeResult.Polygon); console.log(lineInfo); //대각선 //직선(마루) if(lineInfo.hasMatch) { if (lineInfo.matches[0].type === 'diagonal') { const intersection2 = getLineIntersectionParametric(lineP1, lineP2, gableStart, gableEnd); console.log('intersection2:', intersection2); if (lineInfo.matches[0].linePoint === 'p1') { skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: intersection2.x, y: intersection2.y }; } else { skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: intersection2.x, y: intersection2.y }; } } else if (lineInfo.matches[0].type === 'horizontal') { if (lineInfo.matches[0].linePoint === 'p1') { skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: edgeResult.Edge.Begin.X }; } else { skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: edgeResult.Edge.Begin.X }; } } else if (lineInfo.matches[0].type === 'vertical') { if (lineInfo.matches[0].linePoint === 'p1') { skeletonLines[i].p1 = { ...skeletonLines[i].p1, y: edgeResult.Edge.Begin.Y }; } else { skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: edgeResult.Edge.Begin.Y }; } } } // for (const polyPoint of edgeResult.Polygon) { // // if (polyPoint.X === lineP1.x && polyPoint.Y === lineP1.y) { // const extendedPoint1 = getExtensionIntersection(lineP2.x, lineP2.y, lineP1.x, lineP1.y, edgeResult.Edge.Begin.X, edgeResult.Edge.Begin.Y, edgeResult.Edge.End.X,edgeResult.Edge.End.Y); // console.log('extendedPoint1:', extendedPoint1); // // skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: extendedPoint1.x, y: extendedPoint1.Y }; // //skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: edgeResult.Edge.Begin.X}; // } // // if (polyPoint.X === lineP2.x && polyPoint.Y === lineP2.y) { // const extendedPoint2 = getExtensionIntersection(lineP1.x, lineP1.y,lineP2.x, lineP2.y, edgeResult.Edge.Begin.X, edgeResult.Edge.Begin.Y, edgeResult.Edge.End.X,edgeResult.Edge.End.Y); // console.log('extendedPoint2:', extendedPoint2); // // skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: extendedPoint2.x, y: extendedPoint2.Y }; // //skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: edgeResult.Edge.Begin.Y }; // } // // // } } /* if (line.attributes.type === LINE_TYPE.SUBLINE.HIP || line.attributes.type === 'HIP') { // 선택한 기준선 을 중심으로 대각선 삭제 // Get line and edge points const edgeStart = { x: edgeResult.Edge.Begin.X, y: edgeResult.Edge.Begin.Y }; const edgeEnd = { x: edgeResult.Edge.End.X, y: edgeResult.Edge.End.Y }; const lineStart = { x: line.p1.x, y: line.p1.y }; const lineEnd = { x: line.p2.x, y: line.p2.y }; const pointsEqual = (p1, p2) => { return p1.x === p2.x && p1.y === p2.y; } // Check if line shares an endpoint with the edge const sharesStartPoint = pointsEqual(edgeStart, lineStart) || pointsEqual(edgeStart, lineEnd); const sharesEndPoint = pointsEqual(edgeEnd, lineStart) || pointsEqual(edgeEnd, lineEnd); if (sharesStartPoint || sharesEndPoint) { skeletonLines.splice(i, 1); // ridge extension logic can go here //gableMidpoint까지 확장 }else{ //선택한 baseLine 연장(edgeResult.Polygon 의 좌표와 동일한 좌표를 찾아서 연장) for (const polyPoint of edgeResult.Polygon) { if (Math.abs(polyPoint.X - lineEnd.x) < 0.1 && Math.abs(polyPoint.Y - lineEnd.y) < 0.1) { // 연장 로직 } } } }else if (line.attributes.type === LINE_TYPE.SUBLINE.RIDGE || line.attributes.type === 'RIDGE') { //마루일때 const lineP1 = { x: line.p1.x, y: line.p1.y }; const lineP2 = { x: line.p2.x, y: line.p2.y }; const extensionLine= { maxX:'', minX:'', maxY:'', minY:'', } if(edgeResult.Polygon.length > 3){ let hasP1 = false; let hasP2 = false; for (const polyPoint of edgeResult.Polygon) { if (!hasP1 && polyPoint.X === lineP1.x && polyPoint.Y === lineP1.y) { hasP1 = true; } if (!hasP2 && polyPoint.X === lineP2.x && polyPoint.Y === lineP2.y) { hasP2 = true; } // Early exit if both points are found if (hasP1 && hasP2) break; } if (hasP1 && hasP2) { skeletonLines.splice(i, 1); //양쪽 대각선이 있으면 서로 만난다. for (const polyPoint of edgeResult.Polygon) { } //가운데 연장선을 추가 skeletonLines.push({ p1: {x: midPoint.x, y: midPoint.y}, p2: {x: centerX, y: centerY}, attributes: { type: LINE_TYPE.SUBLINE.RIDGE, planeSize: calcLinePlaneSize({ x1: midPoint.x, y1: midPoint.y, x2: centerX, y2: centerY }), isRidge: true, }, lineStyle: { color: '#FF0000', width: 2 }, }) } }else{ console.log("mpoint",gableMidpoint) console.log("midPoint", midPoint) console.log("lineP1",lineP1) console.log("lineP2",lineP2) //gableMidpoint까지 확장 (x or y 동일) //가로일때 gableMidPoint.y 동일 // Extend horizontal lines to gable midpoint // if (Math.abs(lineP1.y - lineP2.y) < 0.3) { // 가로 라인 const extension = getExtensionLine(midPoint, lineP1, lineP2); if (extension) { // null 체크 추가 if (extension.isStartExtension) { skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: extension.extensionPoint.x }; } else { skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: extension.extensionPoint.x }; } } } else { // 세로 라인 const extension = getExtensionLine(midPoint, lineP1, lineP2); if (extension) { // null 체크 추가 if (extension.isStartExtension) { skeletonLines[i].p1 = { ...skeletonLines[i].p1, y: extension.extensionPoint.y }; } else { skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: extension.extensionPoint.y }; } } } } } console.log('result skeletonLines:', skeletonLines) */ // addRawLine( // skeletonLines, // processedInnerEdges, // gableMidpoint, // polygonCenter, // 'RIDGE', // '#0000FF', // 파란색으로 구분 // 3, // 두껍게 // ) } // ✅ 헬퍼 함수들 function isOuterEdge(p1, p2, baseLines) { const tolerance = 0.1 return baseLines.some((line) => { const lineStart = line.startPoint || { x: line.x1, y: line.y1 } const lineEnd = line.endPoint || { x: line.x2, y: line.y2 } return ( (Math.abs(lineStart.x - p1.x) < tolerance && Math.abs(lineStart.y - p1.y) < tolerance && Math.abs(lineEnd.x - p2.x) < tolerance && Math.abs(lineEnd.y - p2.y) < tolerance) || (Math.abs(lineStart.x - p2.x) < tolerance && Math.abs(lineStart.y - p2.y) < tolerance && Math.abs(lineEnd.x - p1.x) < tolerance && Math.abs(lineEnd.y - p1.y) < tolerance) ) }) } function addRawLine(skeletonLines, processedInnerEdges, p1, p2, lineType, color, width) { 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) // 라인 타입을 상수로 정규화 const inputNormalizedType = lineType === LINE_TYPE.SUBLINE.RIDGE || lineType === 'RIDGE' ? LINE_TYPE.SUBLINE.RIDGE : lineType === LINE_TYPE.SUBLINE.HIP || lineType === 'HIP' ? LINE_TYPE.SUBLINE.HIP : lineType // 대각선 여부 판단 (수평/수직이 아닌 경우) const dx = Math.abs(p2.x - p1.x) const dy = Math.abs(p2.y - p1.y) const tolerance = 0.1 const isHorizontal = dy < tolerance const isVertical = dx < tolerance const isDiagonal = !isHorizontal && !isVertical // 대각선일 때 lineType을 HIP로 지정 const normalizedType = isDiagonal ? LINE_TYPE.SUBLINE.HIP : inputNormalizedType skeletonLines.push({ p1: p1, p2: p2, attributes: { type: normalizedType, planeSize: calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE, }, lineStyle: { color: color, width: width, }, }) } /** * 특정 roof의 edge를 캐라바로 설정하여 다시 그립니다. * @param {string} roofId - 지붕 ID * @param {fabric.Canvas} canvas - 캔버스 객체 * @param {string} textMode - 텍스트 모드 * @param {number} selectedEdgeIndex - 선택된 외곽선의 인덱스 */ export const drawSkeletonWithTransformedEdges = (roofId, canvas, textMode, selectedEdgeIndex) => { let roof = canvas?.getObjects().find((object) => object.id === roofId) if (!roof) { console.warn('Roof object not found') return } // Clear existing inner lines if any if (roof.innerLines) { roof.innerLines.forEach((line) => canvas.remove(line)) roof.innerLines = [] } // Transform the selected wall into a roof transformWallToRoof(roof, canvas, selectedEdgeIndex) canvas.renderAll() } /** * 삼각형에 대한 캐라바 처리 * @param {QPolygon} roof - 지붕 객체 * @param {fabric.Canvas} canvas - 캔버스 객체 * @param {string} textMode - 텍스트 모드 * @param {number} selectedEdgeIndex - 선택된 외곽선의 인덱스 */ const drawCarabaForTriangle = (roof, canvas, textMode, edgeIndex) => { const points = roof.getCurrentPoints() if (!points || points.length !== 3) { console.warn('삼각형이 아니거나 유효하지 않은 점 데이터입니다.') return } if (edgeIndex < 0 || edgeIndex >= 3) { console.warn('유효하지 않은 edge 인덱스입니다.') return } // 선택된 edge의 두 꼭짓점을 제외한 나머지 점 찾기 const oppositeVertexIndex = (edgeIndex + 2) % 3 const oppositeVertex = points[oppositeVertexIndex] // 선택된 edge의 시작점과 끝점 const edgeStartIndex = edgeIndex const edgeEndIndex = (edgeIndex + 1) % 3 const edgeStart = points[edgeStartIndex] const edgeEnd = points[edgeEndIndex] // 선택된 edge의 중점 계산 const edgeMidPoint = { x: (edgeStart.x + edgeEnd.x) / 2, y: (edgeStart.y + edgeEnd.y) / 2, } // 맞은편 꼭짓점에서 선택된 edge의 중점으로 가는 직선 생성 const carabaLine = new QLine([oppositeVertex.x, oppositeVertex.y, edgeMidPoint.x, edgeMidPoint.y], { parentId: roof.id, fontSize: roof.fontSize, stroke: '#FF0000', strokeWidth: 2, name: LINE_TYPE.SUBLINE.RIDGE, textMode: textMode, attributes: { type: LINE_TYPE.SUBLINE.RIDGE, planeSize: calcLinePlaneSize({ x1: oppositeVertex.x, y1: oppositeVertex.y, x2: edgeMidPoint.x, y2: edgeMidPoint.y, }), actualSize: calcLineActualSize( { x1: oppositeVertex.x, y1: oppositeVertex.y, x2: edgeMidPoint.x, y2: edgeMidPoint.y, }, getDegreeByChon(roof.lines[edgeIndex]?.attributes?.pitch || 30), ), roofId: roof.id, isRidge: true, }, }) // 캔버스에 추가 canvas.add(carabaLine) carabaLine.bringToFront() // 지붕 객체에 저장 roof.innerLines = [carabaLine] // canvas에 skeleton 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {} } canvas.skeletonStates[roof.id] = true canvas.renderAll() } /** * 다각형에 대한 캐라바 처리 * @param {QPolygon} roof - 지붕 객체 * @param {fabric.Canvas} canvas - 캔버스 객체 * @param {string} textMode - 텍스트 모드 * @param {number} selectedEdgeIndex - 선택된 외곽선의 인덱스 */ const drawCarabaForPolygon = (roof, canvas, textMode, selectedEdgeIndex) => { const points = roof.getCurrentPoints() if (!points || points.length < 3) { console.warn('유효하지 않은 다각형 점 데이터입니다.') return } if (selectedEdgeIndex < 0 || selectedEdgeIndex >= points.length) { console.warn('유효하지 않은 edge 인덱스입니다.') return } // 삼각형인 경우 기존 로직 사용 if (points.length === 3) { drawCarabaForTriangle(roof, canvas, textMode, selectedEdgeIndex) return } // 먼저 스켈레톤을 생성하여 내부 구조를 파악 const geoJSONPolygon = toGeoJSON(points) geoJSONPolygon.pop() // 마지막 좌표 제거 let skeleton try { skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) } catch (e) { console.error('스켈레톤 생성 중 오류:', e) return } if (!skeleton || !skeleton.Edges) { console.warn('스켈레톤 생성에 실패했습니다.') return } // 선택된 외곽선의 시작점과 끝점 const selectedStart = points[selectedEdgeIndex] const selectedEnd = points[(selectedEdgeIndex + 1) % points.length] const innerLines = [] const processedInnerEdges = new Set() // 스켈레톤의 모든 내부선을 수집 for (const edgeResult of skeleton.Edges) { const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y })) for (let i = 0; i < polygonPoints.length; i++) { const p1 = polygonPoints[i] const p2 = polygonPoints[(i + 1) % polygonPoints.length] // 선택된 외곽선은 제외 const isSelectedEdge = (arePointsEqual(selectedStart, p1) && arePointsEqual(selectedEnd, p2)) || (arePointsEqual(selectedStart, p2) && arePointsEqual(selectedEnd, p1)) if (isSelectedEdge) continue // 다른 외곽선들은 제외 const isOtherOuterEdge = roof.lines.some((line, idx) => { if (idx === selectedEdgeIndex) return false return ( (arePointsEqual(line.startPoint, p1) && arePointsEqual(line.endPoint, p2)) || (arePointsEqual(line.startPoint, p2) && arePointsEqual(line.endPoint, p1)) ) }) if (isOtherOuterEdge) continue const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|') if (processedInnerEdges.has(edgeKey)) continue processedInnerEdges.add(edgeKey) // 선택된 외곽선에 수직으로 연장되는 선들만 처리 const selectedLineAngle = calculateAngle(selectedStart, selectedEnd) const innerLineAngle = calculateAngle(p1, p2) const angleDiff = Math.abs(selectedLineAngle - innerLineAngle) const isPerpendicular = Math.abs(angleDiff - 90) < 5 || Math.abs(angleDiff - 270) < 5 if (isPerpendicular) { // 선택된 외곽선 방향으로 연장 const extendedLine = extendLineToOppositeEdge(p1, p2, points, selectedEdgeIndex) if (extendedLine) { const carabaLine = new QLine([extendedLine.start.x, extendedLine.start.y, extendedLine.end.x, extendedLine.end.y], { parentId: roof.id, fontSize: roof.fontSize, stroke: '#FF0000', strokeWidth: 2, name: LINE_TYPE.SUBLINE.RIDGE, textMode: textMode, attributes: { type: LINE_TYPE.SUBLINE.RIDGE, planeSize: calcLinePlaneSize({ x1: extendedLine.start.x, y1: extendedLine.start.y, x2: extendedLine.end.x, y2: extendedLine.end.y, }), actualSize: calcLineActualSize( { x1: extendedLine.start.x, y1: extendedLine.start.y, x2: extendedLine.end.x, y2: extendedLine.end.y, }, getDegreeByChon(roof.lines[selectedEdgeIndex]?.attributes?.pitch || 30), ), roofId: roof.id, isRidge: true, }, }) innerLines.push(carabaLine) } } } } // 캔버스에 추가 innerLines.forEach((line) => { canvas.add(line) line.bringToFront() }) // 지붕 객체에 저장 roof.innerLines = innerLines // canvas에 skeleton 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {} } canvas.skeletonStates[roof.id] = true canvas.renderAll() } /** * 선분을 맞은편 외곽선까지 연장하는 함수 */ const extendLineToOppositeEdge = (p1, p2, polygonPoints, selectedEdgeIndex) => { // 선분의 방향 벡터 계산 const direction = { x: p2.x - p1.x, y: p2.y - p1.y, } // 방향 벡터 정규화 const length = Math.sqrt(direction.x * direction.x + direction.y * direction.y) if (length === 0) return null const normalizedDir = { x: direction.x / length, y: direction.y / length, } // 선택된 외곽선의 반대편 찾기 const oppositeEdgeIndex = (selectedEdgeIndex + Math.floor(polygonPoints.length / 2)) % polygonPoints.length const oppositeStart = polygonPoints[oppositeEdgeIndex] const oppositeEnd = polygonPoints[(oppositeEdgeIndex + 1) % polygonPoints.length] // p1에서 시작해서 반대편까지 연장 const extendedStart = { x: p1.x, y: p1.y } const extendedEnd = findIntersectionWithEdge(p1, normalizedDir, oppositeStart, oppositeEnd) || { x: p2.x, y: p2.y } return { start: extendedStart, end: extendedEnd, } } /** * 선분과 외곽선의 교점을 찾는 함수 */ const findIntersectionWithEdge = (lineStart, lineDir, edgeStart, edgeEnd) => { const edgeDir = { x: edgeEnd.x - edgeStart.x, y: edgeEnd.y - edgeStart.y, } const denominator = lineDir.x * edgeDir.y - lineDir.y * edgeDir.x if (Math.abs(denominator) < 1e-10) return null // 평행선 const t = ((edgeStart.x - lineStart.x) * edgeDir.y - (edgeStart.y - lineStart.y) * edgeDir.x) / denominator const u = ((edgeStart.x - lineStart.x) * lineDir.y - (edgeStart.y - lineStart.y) * lineDir.x) / denominator if (t >= 0 && u >= 0 && u <= 1) { return { x: lineStart.x + t * lineDir.x, y: lineStart.y + t * lineDir.y, } } return null } /** * Transforms the selected wall line into a roof structure * @param {QPolygon} roof - The roof object * @param {fabric.Canvas} canvas - The canvas object * @param {number} edgeIndex - Index of the selected edge */ const transformWallToRoof = (roof, canvas, edgeIndex) => { // Get the current points const points = roof.getCurrentPoints() if (!points || points.length < 3) { console.warn('Invalid polygon points') return } // Get the selected edge points const p1 = points[edgeIndex] const p2 = points[(edgeIndex + 1) % points.length] // Calculate mid point of the selected edge const midX = (p1.x + p2.x) / 2 const midY = (p1.y + p2.y) / 2 // Calculate the perpendicular vector (for the roof ridge) const dx = p2.x - p1.x const dy = p2.y - p1.y const length = Math.sqrt(dx * dx + dy * dy) // Normal vector (perpendicular to the edge) const nx = -dy / length const ny = dx / length // Calculate the ridge point (extending inward from the middle of the edge) const ridgeLength = length * 0.4 // Adjust this factor as needed const ridgeX = midX + nx * ridgeLength const ridgeY = midY + ny * ridgeLength // Create the new points for the roof const newPoints = [...points] newPoints.splice(edgeIndex + 1, 0, { x: ridgeX, y: ridgeY }) // Update the roof with new points roof.set({ points: newPoints, // Ensure the polygon is re-rendered dirty: true, }) // Update the polygon's path roof.setCoords() // Force a re-render of the canvas canvas.renderAll() return roof } /** * 사용 예제: 첫 번째 edge를 45도 각도로 변형 * drawSkeletonWithTransformedEdges(roofId, canvas, textMode, [ * { edgeIndex: 0, angleOffset: 45, splitRatio: 0.5 } * ]); */ class Advanced2DRoofBuilder extends SkeletonBuilder { static Build2DRoofFromAdvancedProperties(geoJsonPolygon, edgeProperties) { // 입력 데이터 검증 if (!geoJsonPolygon || !Array.isArray(geoJsonPolygon) || geoJsonPolygon.length === 0) { throw new Error('geoJsonPolygon이 유효하지 않습니다') } if (!edgeProperties || !Array.isArray(edgeProperties)) { throw new Error('edgeProperties가 유효하지 않습니다') } console.log('입력 검증 통과') console.log('geoJsonPolygon:', geoJsonPolygon) console.log('edgeProperties:', edgeProperties) // 1. 입력 폴리곤을 edgeProperties에 따라 수정 const modifiedPolygon = this.preprocessPolygonByEdgeTypes(geoJsonPolygon, edgeProperties) // 2. 수정된 폴리곤으로 skeleton 생성 const skeleton = SkeletonBuilder.BuildFromGeoJSON([[modifiedPolygon]]) if (!skeleton || !skeleton.Edges) { throw new Error('Skeleton 생성 실패') } // 3. Edge 분석 const edgeAnalysis = this.analyzeAdvancedEdgeTypes(edgeProperties) return { skeleton: skeleton, original_polygon: geoJsonPolygon, modified_polygon: modifiedPolygon, roof_type: edgeAnalysis.roof_type, edge_analysis: edgeAnalysis, } } /** * ✅ 안전한 폴리곤 전처리 */ static preprocessPolygonByEdgeTypes(geoJsonPolygon, edgeProperties) { try { const originalRing = geoJsonPolygon if (!Array.isArray(originalRing) || originalRing.length < 4) { throw new Error('외곽선이 유효하지 않습니다') } const modifiedRing = originalRing.map((point) => { if (!Array.isArray(point) || point.length < 2) { throw new Error('좌표점 형식이 잘못되었습니다') } return [point[0], point[1]] }) const isClosedPolygon = this.isPolygonClosed(modifiedRing) if (isClosedPolygon) { modifiedRing.pop() } const actualEdgeCount = modifiedRing.length const edgeCountToProcess = Math.min(edgeProperties.length, actualEdgeCount) for (let i = 0; i < edgeCountToProcess; i++) { const edgeProp = edgeProperties[i] const edgeType = edgeProp?.edge_type console.log(`Processing edge ${i}: ${edgeType}`) try { switch (edgeType) { case 'EAVES': // ✅ 수정: 처마는 기본 상태이므로 수정하지 않음 console.log(`Edge ${i}: EAVES - 기본 처마 상태 유지`) break case 'WALL': // ✅ 수정: 처마를 벽으로 변경 this.transformEavesToWall(modifiedRing, i, edgeProp) break case 'GABLE': // ✅ 수정: 처마를 케라바로 변경 this.transformEavesToGable(modifiedRing, i, edgeProp) break default: console.warn(`알 수 없는 edge 타입: ${edgeType}, 기본 EAVES로 처리`) } } catch (edgeError) { console.error(`Edge ${i} 처리 중 오류:`, edgeError) } } const finalPolygon = this.prepareFinalPolygon(modifiedRing) return finalPolygon } catch (error) { console.error('폴리곤 전처리 오류:', error) throw error } } /** * ✅ 처마를 벽으로 변경 (내부로 수축) */ static transformEavesToWall(ring, edgeIndex, edgeProp) { console.log(`transformEavesToWall: edgeIndex=${edgeIndex}`) if (!ring || !Array.isArray(ring)) { console.error('ring이 배열이 아니거나 undefined입니다') return } const totalPoints = ring.length if (edgeIndex < 0 || edgeIndex >= totalPoints) { console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`) return } const p1 = ring[edgeIndex] const nextIndex = (edgeIndex + 1) % totalPoints const p2 = ring[nextIndex] if (!Array.isArray(p1) || p1.length < 2 || !Array.isArray(p2) || p2.length < 2) { console.error('점 형식이 잘못되었습니다') return } try { // 폴리곤 중심 계산 const centerX = ring.reduce((sum, p) => sum + p[0], 0) / totalPoints const centerY = ring.reduce((sum, p) => sum + p[1], 0) / totalPoints // edge 중점 const midX = (p1[0] + p2[0]) / 2 const midY = (p1[1] + p2[1]) / 2 // 내향 방향 (처마 → 벽: 안쪽으로 수축) const dirX = centerX - midX const dirY = centerY - midY const length = Math.sqrt(dirX * dirX + dirY * dirY) if (length < 0.001) { console.warn('내향 방향 벡터 길이가 거의 0입니다') return } const unitX = dirX / length const unitY = dirY / length const shrinkDistance = edgeProp.shrink_distance || 0.8 // 벽 수축 거리 // 점들을 내부로 이동 ring[edgeIndex] = [p1[0] + unitX * shrinkDistance, p1[1] + unitY * shrinkDistance] ring[nextIndex] = [p2[0] + unitX * shrinkDistance, p2[1] + unitY * shrinkDistance] console.log(`✅ WALL: Edge ${edgeIndex} 내부로 수축 완료 (${shrinkDistance})`) } catch (calcError) { console.error('벽 변환 계산 중 오류:', calcError) } } /** * ✅ 처마를 케라바로 변경 (특별한 형태로 변형) */ // static transformEavesToGable(ring, edgeIndex, edgeProp) { // console.log(`transformEavesToGable: edgeIndex=${edgeIndex}`); // // // 안전성 검증 // if (!ring || !Array.isArray(ring)) { // console.error('ring이 배열이 아니거나 undefined입니다'); // return; // } // // const totalPoints = ring.length; // // if (edgeIndex < 0 || edgeIndex >= totalPoints) { // console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`); // return; // } // // const p1 = ring[edgeIndex]; // const nextIndex = (edgeIndex + 1) % totalPoints; // const p2 = ring[nextIndex]; // // if (!Array.isArray(p1) || p1.length < 2 || // !Array.isArray(p2) || p2.length < 2) { // console.error('점 형식이 잘못되었습니다'); // return; // } // // try { // // 케라바 변형: edge를 직선화하고 약간 내부로 이동 // const centerX = ring.reduce((sum, p) => sum + p[0], 0) / totalPoints; // const centerY = ring.reduce((sum, p) => sum + p[1], 0) / totalPoints; // // const midX = (p1[0] + p2[0]) / 2; // const midY = (p1[1] + p2[1]) / 2; // // // 내향 방향으로 약간 이동 // const dirX = centerX - midX; // const dirY = centerY - midY; // const length = Math.sqrt(dirX * dirX + dirY * dirY); // // if (length < 0.001) { // console.warn('내향 방향 벡터 길이가 거의 0입니다'); // return; // } // // const unitX = dirX / length; // const unitY = dirY / length; // const gableInset = edgeProp.gable_inset || 0.3; // 케라바 안쪽 이동 거리 // // // edge의 방향 벡터 // const edgeVecX = p2[0] - p1[0]; // const edgeVecY = p2[1] - p1[1]; // // // 새로운 중점 (안쪽으로 이동) // const newMidX = midX + unitX * gableInset; // const newMidY = midY + unitY * gableInset; // // // 케라바를 위한 직선화된 점들 // ring[edgeIndex] = [ // newMidX - edgeVecX * 0.5, // newMidY - edgeVecY * 0.5 // ]; // // ring[nextIndex] = [ // newMidX + edgeVecX * 0.5, // newMidY + edgeVecY * 0.5 // ]; // // console.log(`✅ GABLE: Edge ${edgeIndex} 케라바 변형 완료`); // // } catch (calcError) { // console.error('케라바 변환 계산 중 오류:', calcError); // } // } static transformEavesToGable(ring, edgeIndex, edgeProp) { // ✅ 캐라바면을 위한 특별 처리 // 해당 edge를 "직선 제약 조건"으로 만들어야 함 const p1 = ring[edgeIndex] const nextIndex = (edgeIndex + 1) % ring.length const p2 = ring[nextIndex] // 캐라바면: edge를 완전히 직선으로 고정 // 이렇게 하면 skeleton이 이 edge에 수직으로만 생성됨 // 중간점들을 제거하여 직선화 const midX = (p1[0] + p2[0]) / 2 const midY = (p1[1] + p2[1]) / 2 // 캐라바 edge를 단순 직선으로 만들어 // SkeletonBuilder가 여기서 직선 skeleton을 생성하도록 유도 console.log(`✅ GABLE: Edge ${edgeIndex}를 직선 캐라바로 설정`) } // analyzeAdvancedEdgeTypes도 수정 static analyzeAdvancedEdgeTypes(edgeProperties) { const eavesEdges = [] // 기본 처마 (수정 안함) const wallEdges = [] // 처마→벽 변경 const gableEdges = [] // 처마→케라바 변경 edgeProperties.forEach((prop, i) => { switch (prop?.edge_type) { case 'EAVES': eavesEdges.push(i) break case 'WALL': wallEdges.push(i) break case 'GABLE': gableEdges.push(i) break default: console.warn(`Edge ${i}: 알 수 없는 타입 ${prop?.edge_type}, 기본 EAVES로 처리`) eavesEdges.push(i) } }) let roofType if (wallEdges.length === 0 && gableEdges.length === 0) { roofType = 'pavilion' // 모든 면이 처마 } else if (wallEdges.length === 4 && gableEdges.length === 0) { roofType = 'hipped' // 모든 면이 벽 } else if (gableEdges.length === 2 && wallEdges.length === 2) { roofType = 'gabled' // 박공지붕 } else { roofType = 'complex' // 복합지붕 } return { roof_type: roofType, eaves_edges: eavesEdges, // 기본 처마 wall_edges: wallEdges, // 처마→벽 gable_edges: gableEdges, // 처마→케라바 } } /** * ✅ 폴리곤이 닫혀있는지 확인 */ static isPolygonClosed(ring) { if (!ring || ring.length < 2) return false const firstPoint = ring[0] const lastPoint = ring[ring.length - 1] const tolerance = 0.0001 // 부동소수점 허용 오차 return Math.abs(firstPoint[0] - lastPoint[0]) < tolerance && Math.abs(firstPoint[1] - lastPoint[1]) < tolerance } /** * ✅ BuildFromGeoJSON용 최종 polygon 준비 */ static prepareFinalPolygon(ring) { // 1. 최소 점 개수 확인 if (ring.length < 3) { throw new Error(`폴리곤 점이 부족합니다: ${ring.length}개 (최소 3개 필요)`) } // 2. 닫힌 폴리곤인지 다시 확인 const isClosed = this.isPolygonClosed(ring) if (isClosed) { console.log('여전히 닫힌 폴리곤입니다. 마지막 점 제거') return ring.slice(0, -1) // 마지막 점 제거 } // 3. 열린 폴리곤이면 그대로 반환 console.log('열린 폴리곤 상태로 BuildFromGeoJSON에 전달') return [...ring] // 복사본 반환 } static expandEdgeForEaves(ring, edgeIndex, overhang) { console.log(`expandEdgeForEaves 시작: edgeIndex=${edgeIndex}, overhang=${overhang}`) // 안전성 검증 if (!ring || !Array.isArray(ring)) { console.error('ring이 배열이 아니거나 undefined입니다') return } const totalPoints = ring.length - 1 // 마지막 중복점 제외 console.log(`ring 길이: ${ring.length}, totalPoints: ${totalPoints}`) if (totalPoints <= 2) { console.error('ring 점 개수가 부족합니다') return } if (edgeIndex < 0 || edgeIndex >= totalPoints) { console.error(`edgeIndex ${edgeIndex}가 범위 [0, ${totalPoints - 1}]을 벗어났습니다`) return } // 안전한 점 접근 const p1 = ring[edgeIndex] const nextIndex = (edgeIndex + 1) % totalPoints const p2 = ring[nextIndex] console.log(`p1 (index ${edgeIndex}):`, p1) console.log(`p2 (index ${nextIndex}):`, p2) if (!Array.isArray(p1) || p1.length < 2 || !Array.isArray(p2) || p2.length < 2) { console.error('점 형식이 잘못되었습니다') console.error('p1:', p1, 'p2:', p2) return } if (typeof p1[0] !== 'number' || typeof p1[1] !== 'number' || typeof p2[0] !== 'number' || typeof p2[1] !== 'number') { console.error('좌표값이 숫자가 아닙니다') return } try { // 폴리곤 중심 계산 (마지막 중복점 제외) const validPoints = ring.slice(0, totalPoints) const centerX = validPoints.reduce((sum, p) => sum + p[0], 0) / totalPoints const centerY = validPoints.reduce((sum, p) => sum + p[1], 0) / totalPoints console.log(`중심점: (${centerX}, ${centerY})`) // edge 중점 const midX = (p1[0] + p2[0]) / 2 const midY = (p1[1] + p2[1]) / 2 // 외향 방향 const dirX = midX - centerX const dirY = midY - centerY const length = Math.sqrt(dirX * dirX + dirY * dirY) if (length < 0.001) { // 거의 0인 경우 console.warn('외향 방향 벡터 길이가 거의 0입니다, 확장하지 않습니다') return } const unitX = dirX / length const unitY = dirY / length // 안전하게 점 수정 ring[edgeIndex] = [p1[0] + unitX * overhang, p1[1] + unitY * overhang] ring[nextIndex] = [p2[0] + unitX * overhang, p2[1] + unitY * overhang] console.log(`✅ EAVES: Edge ${edgeIndex} 확장 완료 (${overhang})`) console.log('수정된 p1:', ring[edgeIndex]) console.log('수정된 p2:', ring[nextIndex]) } catch (calcError) { console.error('계산 중 오류:', calcError) } } /** * ✅ 안전한 박공 조정 */ static adjustEdgeForGable(ring, edgeIndex, gableHeight) { console.log(`adjustEdgeForGable 시작: edgeIndex=${edgeIndex}`) // 안전성 검증 (동일한 패턴) if (!ring || !Array.isArray(ring)) { console.error('ring이 배열이 아니거나 undefined입니다') return } const totalPoints = ring.length - 1 if (totalPoints <= 2) { console.error('ring 점 개수가 부족합니다') return } if (edgeIndex < 0 || edgeIndex >= totalPoints) { console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`) return } const p1 = ring[edgeIndex] const nextIndex = (edgeIndex + 1) % totalPoints const p2 = ring[nextIndex] if (!Array.isArray(p1) || p1.length < 2 || !Array.isArray(p2) || p2.length < 2) { console.error('점 형식이 잘못되었습니다') return } try { const validPoints = ring.slice(0, totalPoints) const centerX = validPoints.reduce((sum, p) => sum + p[0], 0) / totalPoints const centerY = validPoints.reduce((sum, p) => sum + p[1], 0) / totalPoints const midX = (p1[0] + p2[0]) / 2 const midY = (p1[1] + p2[1]) / 2 const dirX = centerX - midX const dirY = centerY - midY const length = Math.sqrt(dirX * dirX + dirY * dirY) if (length < 0.001) { console.warn('중심 방향 벡터 길이가 거의 0입니다') return } const unitX = dirX / length const unitY = dirY / length const insetDistance = 0.5 const newMidX = midX + unitX * insetDistance const newMidY = midY + unitY * insetDistance const edgeVecX = p2[0] - p1[0] const edgeVecY = p2[1] - p1[1] ring[edgeIndex] = [newMidX - edgeVecX * 0.5, newMidY - edgeVecY * 0.5] ring[nextIndex] = [newMidX + edgeVecX * 0.5, newMidY + edgeVecY * 0.5] console.log(`✅ GABLE: Edge ${edgeIndex} 조정 완료`) } catch (calcError) { console.error('박공 조정 계산 중 오류:', calcError) } } static processGableSkeleton(skeleton, gableEdgeIndex, originalPolygon) { // ✅ Gable edge에 해당하는 skeleton 정점들을 찾아서 // 해당 edge의 중점으로 강제 이동 const gableEdge = originalPolygon[gableEdgeIndex] const edgeMidpoint = calculateMidpoint(gableEdge) // skeleton 정점들을 edge 중점으로 "압축" skeleton.Edges.forEach((edge) => { if (isRelatedToGableEdge(edge, gableEdgeIndex)) { // 해당 edge 관련 skeleton 정점들을 직선으로 정렬 straightenSkeletonToEdge(edge, edgeMidpoint) } }) } // ✅ Gable edge에 제약 조건을 추가하여 skeleton 생성 static buildConstrainedSkeleton(polygon, edgeConstraints) { const constraints = edgeConstraints .map((constraint) => { if (constraint.type === 'GABLE') { return { edgeIndex: constraint.edgeIndex, forceLinear: true, // 직선 강제 fixToMidpoint: true, // 중점 고정 } } return null }) .filter((c) => c !== null) // 제약 조건이 적용된 skeleton 생성 return SkeletonBuilder.build(polygon, constraints) } } /** * 지붕 폴리곤의 좌표를 스켈레톤 생성에 적합한 형태로 전처리합니다. * - 연속된 중복 좌표를 제거합니다. * - 폴리곤이 닫힌 형태가 되도록 마지막 좌표를 확인하고 필요시 제거합니다. * - 좌표를 시계 방향으로 정렬합니다. * @param {Array} initialPoints - 초기 폴리곤 좌표 배열 (e.g., [{x: 10, y: 10}, ...]) * @returns {Array>} 전처리된 좌표 배열 (e.g., [[10, 10], ...]) */ const preprocessPolygonCoordinates = (initialPoints) => { // fabric.Point 객체를 [x, y] 배열로 변환 let coordinates = initialPoints.map(point => [point.x, point.y]); // 연속된 중복 좌표 제거 coordinates = coordinates.filter((coord, index) => { if (index === 0) return true; const prev = coordinates[index - 1]; return !(coord[0] === prev[0] && coord[1] === prev[1]); }); // 폴리곤의 첫 점과 마지막 점이 동일하면 마지막 점을 제거하여 닫힌 구조 보장 if (coordinates.length > 1 && coordinates[0][0] === coordinates[coordinates.length - 1][0] && coordinates[0][1] === coordinates[coordinates.length - 1][1]) { coordinates.pop(); } // SkeletonBuilder 라이브러리는 시계 방향 순서의 좌표를 요구하므로 배열을 뒤집습니다. coordinates.reverse(); return coordinates; }; const isSameLine = (edgeStartX, edgeStartY, edgeEndX, edgeEndY, baseLine) => { const tolerance = 0.1 // 시계방향 매칭 (edgeResult.Edge.Begin -> End와 baseLine.x1 -> x2) const clockwiseMatch = Math.abs(edgeStartX - baseLine.x1) < tolerance && Math.abs(edgeStartY - baseLine.y1) < tolerance && Math.abs(edgeEndX - baseLine.x2) < tolerance && Math.abs(edgeEndY - baseLine.y2) < tolerance // 반시계방향 매칭 (edgeResult.Edge.Begin -> End와 baseLine.x2 -> x1) const counterClockwiseMatch = Math.abs(edgeStartX - baseLine.x2) < tolerance && Math.abs(edgeStartY - baseLine.y2) < tolerance && Math.abs(edgeEndX - baseLine.x1) < tolerance && Math.abs(edgeEndY - baseLine.y1) < tolerance return clockwiseMatch || counterClockwiseMatch } /** * 중간점과 선분의 끝점을 비교하여 연장선(extensionLine)을 결정합니다. * @param {Object} midPoint - 중간점 좌표 {x, y} * @param {Object} lineP1 - 선분의 첫 번째 끝점 {x, y} * @param {Object} lineP2 - 선분의 두 번째 끝점 {x, y} * @returns {Object|null} - 연장선 설정 또는 null (연장 불필요 시) */ function getExtensionLine(midPoint, lineP1, lineP2) { // 선분의 방향 계산 const isHorizontal = Math.abs(lineP1.y - lineP2.y) < 0.3; // y 좌표가 거의 같으면 수평선 const isVertical = Math.abs(lineP1.x - lineP2.x) < 0.3; // x 좌표가 거의 같으면 수직선 if (isHorizontal) { // 수평선인 경우 - y 좌표가 midPoint와 같은지 확인 if (Math.abs(lineP1.y - midPoint.y) > 0.3) { return null; // y 좌표가 다르면 연장하지 않음 } // 중간점이 선분의 왼쪽에 있는 경우 if (midPoint.x < Math.min(lineP1.x, lineP2.x)) { return { isHorizontal: true, isStartExtension: lineP1.x < lineP2.x, extensionPoint: { ...midPoint, y: lineP1.y } }; } // 중간점이 선분의 오른쪽에 있는 경우 else if (midPoint.x > Math.max(lineP1.x, lineP2.x)) { return { isHorizontal: true, isStartExtension: lineP1.x > lineP2.x, extensionPoint: { ...midPoint, y: lineP1.y } }; } } else if (isVertical) { // 수직선인 경우 - x 좌표가 midPoint와 같은지 확인 if (Math.abs(lineP1.x - midPoint.x) > 0.3) { return null; // x 좌표가 다르면 연장하지 않음 } // 중간점이 선분의 위에 있는 경우 if (midPoint.y < Math.min(lineP1.y, lineP2.y)) { return { isHorizontal: false, isStartExtension: lineP1.y < lineP2.y, extensionPoint: { ...midPoint, x: lineP1.x } }; } // 중간점이 선분의 아래에 있는 경우 else if (midPoint.y > Math.max(lineP1.y, lineP2.y)) { return { isHorizontal: false, isStartExtension: lineP1.y > lineP2.y, extensionPoint: { ...midPoint, x: lineP1.x } }; } } // 기본값 반환 (연장 불필요) return null; } function convertToClockwise(points) { // 1. 다각형의 면적 계산 (시계/반시계 방향 판단용) let area = 0; const n = points.length; for (let i = 0; i < n; i++) { const j = (i + 1) % n; area += (points[j].X - points[i].X) * (points[j].Y + points[i].Y); } // 2. 반시계방향이면 배열을 뒤집어 시계방향으로 변환 if (area < 0) { return [...points].reverse(); } // 3. 이미 시계방향이면 그대로 반환 return [...points]; } /** * skeletonLines에서 특정 점(polyPoint)을 지나는 라인을 찾는 함수 * @param {Array} skeletonLines - 검색할 라인 배열 * @param {Object} polyPoint - 검색할 점 {X, Y} * @returns {Array} - 일치하는 라인 배열 */ function findLinesPassingPoint(skeletonLines, polyPoint) { return skeletonLines.filter(line => { // 라인의 시작점이나 끝점이 polyPoint와 일치하는지 확인 const isP1Match = (Math.abs(line.p1.x - polyPoint.X) < 0.001 && Math.abs(line.p1.y - polyPoint.Y) < 0.001); const isP2Match = (Math.abs(line.p2.x - polyPoint.X) < 0.001 && Math.abs(line.p2.y - polyPoint.Y) < 0.001); return isP1Match || isP2Match; }); } // 두 선분의 교차점을 찾는 함수 // 두 선분의 교차점을 찾는 함수 (개선된 버전) function findIntersection(p1, p2, p3, p4) { // 선분1: p1 -> p2 // 선분2: p3 -> p4 // 선분 방향 벡터 const d1x = p2.x - p1.x; const d1y = p2.y - p1.y; const d2x = p4.x - p3.x; const d2y = p4.y - p3.y; // 분모 계산 const denominator = d1x * d2y - d1y * d2x; // 평행한 경우 (또는 매우 가까운 경우) // if (Math.abs(denominator) < 0.0001) { // return null; // } // 매개변수 t와 u 계산 const t = ((p3.x - p1.x) * d2y - (p3.y - p1.y) * d2x) / denominator; const u = ((p3.x - p1.x) * d1y - (p3.y - p1.y) * d1x) / denominator; // 두 선분이 교차하는지 확인 (0 <= t <= 1, 0 <= u <= 1) if (t >= -0.001 && t <= 1.001 && u >= -0.001 && u <= 1.001) { // 교차점 계산 const x = p1.x + t * d1x; const y = p1.y + t * d1y; return { x, y }; } // 교차하지 않는 경우 return null; } /** * edgePoints와 skeletonLines의 교차점을 찾는 함수 * @param {Array<{x: number, y: number}>} edgePoints - 엣지 포인트 배열 * @param {Array} skeletonLines - 원시 라인 배열 (각 라인은 p1, p2 속성을 가짐) * @returns {Array<{x: number, y: number, line: Object}>} 교차점과 해당 라인 정보 배열 */ function findIntersectionsWithEdgePoints(edgePoints, skeletonLines) { const intersections = []; // edgePoints를 순회하며 각 점을 지나는 라인 찾기 for (let i = 0; i < edgePoints.length; i++) { const point = edgePoints[i]; const nextPoint = edgePoints[(i + 1) % edgePoints.length]; // 현재 엣지 선분 const edgeLine = { x1: point.x, y1: point.y, x2: nextPoint.x, y2: nextPoint.y }; // 모든 skeletonLines와의 교차점 검사 for (const rawLine of skeletonLines) { // rawLine은 p1, p2 속성을 가짐 const rawLineObj = { x1: rawLine.p1.x, y1: rawLine.p1.y, x2: rawLine.p2.x, y2: rawLine.p2.y }; // 선분 교차 검사 const intersection = findIntersection( edgeLine.x1, edgeLine.y1, edgeLine.x2, edgeLine.y2, rawLineObj.x1, rawLineObj.y1, rawLineObj.x2, rawLineObj.y2 ); if (intersection) { intersections.push({ x: intersection.x, y: intersection.y, edgeIndex: i, line: rawLine }); } } } return intersections; } // Helper function to extend a line to intersect with polygon edges function extendLineToIntersections(p1, p2, polygonPoints) { let intersections = []; const line = { p1, p2 }; // Check intersection with each polygon edge for (let i = 0; i < polygonPoints.length; i++) { const edgeP1 = polygonPoints[i]; const edgeP2 = polygonPoints[(i + 1) % polygonPoints.length]; } if (intersections.length < 2) return null; // Sort by distance from p1 intersections.sort((a, b) => a.distance - b.distance); // Return the two farthest intersection points return { p1: { x: intersections[0].x, y: intersections[0].y }, p2: { x: intersections[intersections.length - 1].x, y: intersections[intersections.length - 1].y } }; } function getExtensionIntersection( startX, startY, // 대각선 시작점 currentX, currentY, // 대각선 현재 위치 lineStartX, lineStartY, // 연장할 선의 시작점 lineEndX, lineEndY // 연장할 선의 끝점 ) { // 대각선 방향 벡터 const dx = currentX - startX; const dy = currentY - startY; // 연장할 선의 기울기 const m = (lineEndY - lineStartY) / (lineEndX - lineStartX); // 매개변수 t 방정식에서 t를 구하기 위한 식 전개 // 대각선의 parametric 방정식: x = startX + t*dx, y = startY + t*dy // 연장할 선 방정식: y = m * (x - lineStartX) + lineStartY // 이를 대입해 t 구함 const numerator = m * (lineStartX - startX) + startY - lineStartY; const denominator = dy - m * dx; if (denominator === 0) { // 평행하거나 일치하여 교점 없음 return null; } const t = numerator / denominator; const intersectX = startX + t * dx; const intersectY = startY + t * dy; return { x: intersectX, y: intersectY }; } function findMatchingLinePoints(Aline, APolygon, epsilon = 1e-10) { const { p1, p2 } = Aline; const matches = []; // 선의 방향 판단 function getLineDirection(point1, point2, epsilon = 1e-10) { const deltaX = Math.abs(point1.x - point2.x); const deltaY = Math.abs(point1.y - point2.y); if (deltaX < epsilon && deltaY < epsilon) { return { type: 'point', description: '점 (두 좌표가 동일)' }; } else if (deltaX < epsilon) { return { type: 'vertical', description: '수직선 (세로)' }; } else if (deltaY < epsilon) { return { type: 'horizontal', description: '수평선 (가로)' }; } else { return { type: 'diagonal', description: '대각선' }; } } // 선의 방향 정보 계산 const lineDirection = getLineDirection(p1, p2, epsilon); APolygon.forEach((point, index) => { // p1과 비교 if (Math.abs(p1.x - point.X) < epsilon && Math.abs(p1.y - point.Y) < epsilon) { matches.push({ linePoint: 'p1', polygonIndex: index, coordinates: { x: point.X, y: point.Y }, lineDirection: lineDirection, type: lineDirection.type }); } // p2와 비교 if (Math.abs(p2.x - point.X) < epsilon && Math.abs(p2.y - point.Y) < epsilon) { matches.push({ linePoint: 'p2', polygonIndex: index, coordinates: { x: point.X, y: point.Y }, lineDirection: lineDirection, type: lineDirection.type }); } }); return { hasMatch: matches.length > 0, lineDirection: lineDirection, matches: matches }; } function getLineIntersectionParametric(p1, p2, p3, p4) { const d1 = { x: p2.x - p1.x, y: p2.y - p1.y }; // 첫번째 직선 방향벡터 const d2 = { x: p4.x - p3.x, y: p4.y - p3.y }; // 두번째 직선 방향벡터 // 평행선 체크 (외적이 0이면 평행) const cross = d1.x * d2.y - d1.y * d2.x; if (Math.abs(cross) < Number.EPSILON) { return null; // 평행선 } // 매개변수 t 계산 const dx = p3.x - p1.x; const dy = p3.y - p1.y; const t = (dx * d2.y - dy * d2.x) / cross; // 교점: p1 + t * d1 return { x: p1.x + t * d1.x, y: p1.y + t * d1.y }; }