import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import { SkeletonBuilder } from '@/lib/skeletons' import { calcLineActualSize, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils' import { QLine } from '@/components/fabric/QLine' import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' import { QPolygon } from '@/components/fabric/QPolygon' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. * @param {string} roofId - 대상 지붕 객체의 ID * @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체 * @param {string} textMode - 텍스트 표시 모드 * @param pitch */ const EPSILON = 0.1 export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { // 2. 스켈레톤 생성 및 그리기 skeletonBuilder(roofId, canvas, textMode) } const movingLineFromSkeleton = (roofId, canvas) => { let roof = canvas?.getObjects().find((object) => object.id === roofId) let moveDirection = roof.moveDirect; let moveFlowLine = roof.moveFlowLine??0; let moveUpDown = roof.moveUpDown??0; const getSelectLine = () => roof.moveSelectLine; const selectLine = getSelectLine(); let movePosition = roof.movePosition; const startPoint = selectLine.startPoint const endPoint = selectLine.endPoint const orgRoofPoints = roof.points; // orgPoint를 orgPoints로 변경 const oldPoints = canvas?.skeleton.lastPoints ?? orgRoofPoints // 여기도 변경 const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) const baseLines = wall.baseLines roof.basePoints = createOrderedBasePoints(roof.points, baseLines) const skeletonPolygon = canvas.getObjects().filter((object) => object.skeletonType === 'polygon' && object.parentId === roofId) const skeletonLines = canvas.getObjects().filter((object) => object.skeletonType === 'line' && object.parentId === roofId) if (oppositeLine) { console.log('Opposite line found:', oppositeLine); } else { console.log('No opposite line found'); } if(moveFlowLine !== 0) { return oldPoints.map((point, index) => { console.log('Point:', point); const newPoint = { ...point }; const absMove = Big(moveFlowLine).times(2).div(10); console.log('skeletonBuilder moveDirection:', moveDirection); switch (moveDirection) { case 'left': // Move left: decrease X if (moveFlowLine !== 0) { for (const line of oppositeLine) { if (line.position === 'left') { if (isSamePoint(newPoint, line.start)) { newPoint.x = Big(line.start.x).plus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { newPoint.x = Big(line.end.x).plus(absMove).toNumber(); } break; } } } else if (moveUpDown !== 0) { } break; case 'right': for (const line of oppositeLine) { if (line.position === 'right') { if (isSamePoint(newPoint, line.start)) { newPoint.x = Big(line.start.x).minus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { newPoint.x = Big(line.end.x).minus(absMove).toNumber(); } break } } break; case 'up': // Move up: decrease Y (toward top of screen) for (const line of oppositeLine) { if (line.position === 'top') { if (isSamePoint(newPoint, line.start)) { newPoint.y = Big(line.start.y).minus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { newPoint.y = Big(line.end.y).minus(absMove).toNumber(); } break; } } break; case 'down': // Move down: increase Y (toward bottom of screen) for (const line of oppositeLine) { if (line.position === 'bottom') { console.log('oldPoint:', point); if (isSamePoint(newPoint, line.start)) { newPoint.y = Big(line.start.y).minus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { newPoint.y = Big(line.end.y).minus(absMove).toNumber(); } break; } } break; default : // 사용 예시 } console.log('newPoint:', newPoint); //baseline 변경 return newPoint; }) } else if(moveUpDown !== 0) { // const selectLine = getSelectLine(); // // console.log("wall::::", wall.points) // console.log("저장된 3333moveSelectLine:", roof.moveSelectLine); // console.log("저장된 3moveSelectLine:", selectLine); // const result = getSelectLinePosition(wall, selectLine, { // testDistance: 5, // 테스트 거리 // debug: true // 디버깅 로그 출력 // }); // console.log("3333linePosition:::::", result.position); const position = movePosition //result.position; const absMove = Big(moveUpDown).times(1).div(10); const modifiedStartPoints = []; // oldPoints를 복사해서 새로운 points 배열 생성 let newPoints = oldPoints.map(point => ({...point})); // selectLine과 일치하는 baseLines 찾기 const matchingLines = baseLines .map((line, index) => ({ ...line, findIndex: index })) .filter(line => (isSamePoint(line.startPoint, selectLine.startPoint) && isSamePoint(line.endPoint, selectLine.endPoint)) || (isSamePoint(line.startPoint, selectLine.endPoint) && isSamePoint(line.endPoint, selectLine.startPoint)) ); matchingLines.forEach(line => { const originalStartPoint = line.startPoint; const originalEndPoint = line.endPoint; const offset = line.attributes.offset // 새로운 좌표 계산 let newStartPoint = {...originalStartPoint}; 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; } */ // 원본 라인 업데이트 // newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트 console.log('absMove::', absMove); 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(); } // 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(); } // 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; } } /** * SkeletonBuilder를 사용하여 스켈레톤을 생성하고 내부선을 그립니다. * @param {string} roofId - 지붕 ID * @param {fabric.Canvas} canvas - 캔버스 객체 * @param {string} textMode - 텍스트 모드 * @param {fabric.Object} roof - 지붕 객체 * @param baseLines */ export const skeletonBuilder = (roofId, canvas, textMode) => { //처마 let roof = canvas?.getObjects().find((object) => object.id === roofId) const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE] const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] /** 외벽선 */ 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 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 skeletonLines = []; // 1. 지붕 폴리곤 좌표 전처리 const coordinates = preprocessPolygonCoordinates(roof.points); if (coordinates.length < 3) { 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; //마루이동 if (moveFlowLine !== 0 || moveUpDown !== 0) { points = movingLineFromSkeleton(roofId, canvas) } console.log('points:', points); const geoJSONPolygon = toGeoJSON(points) try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 geoJSONPolygon.pop() const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) // 스켈레톤 데이터를 기반으로 내부선 생성 roof.innerLines = roof.innerLines || []; roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {} canvas.skeletonLines = [] } canvas.skeletonStates[roofId] = true canvas.skeletonLines = []; canvas.skeletonLines.push(...roof.innerLines) roof.skeletonLines = canvas.skeletonLines; const cleanSkeleton = { Edges: skeleton.Edges.map(edge => ({ X1: edge.Edge.Begin.X, Y1: edge.Edge.Begin.Y, X2: edge.Edge.End.X, Y2: edge.Edge.End.Y, Polygon: edge.Polygon, // Add other necessary properties, but skip circular references })), roofId: roofId, // Add other necessary top-level properties }; canvas.skeleton = []; canvas.skeleton = cleanSkeleton canvas.skeleton.lastPoints = points canvas.set("skeleton", cleanSkeleton); canvas.renderAll() console.log('skeleton rendered.', canvas); } catch (e) { console.error('스켈레톤 생성 중 오류 발생:', e) if (canvas.skeletonStates) { canvas.skeletonStates[roofId] = false canvas.skeletonStates = {} canvas.skeletonLines = [] } } } /** * 스켈레톤 결과와 외벽선 정보를 바탕으로 내부선(용마루, 추녀)을 생성합니다. * @param {object} skeleton - SkeletonBuilder로부터 반환된 스켈레톤 객체 * @param {fabric.Object} roof - 대상 지붕 객체 * @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체 * @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none') * @param {Array} baseLines - 원본 외벽선 QLine 객체 배열 */ 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) => { processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines); }); // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다. skeleton.Edges.forEach(edgeResult => { const { Begin, End } = edgeResult.Edge; const gableBaseLine = roof.lines.find(line => line.attributes.type === 'gable' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line) ); if (gableBaseLine) { // Store current state before processing const beforeGableProcessing = JSON.parse(JSON.stringify(skeletonLines)); // if(canvas.skeletonLines.length > 0){ // skeletonLines = canvas.skeletonLines; // } // Process gable edge with both current and previous states const processedLines = processGableEdge( edgeResult, baseLines, [...skeletonLines], // Current state gableBaseLine, beforeGableProcessing // Previous state ); // Update canvas with processed lines canvas.skeletonLines = processedLines; skeletonLines = processedLines; } }); /* //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); 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 }; } }); //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((sktLine, skIndex) => { let { p1, p2, attributes, lineStyle } = sktLine; // 중복방지 - 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교) const lineKey = [ [p1.x, p1.y].sort().join(','), [p2.x, p2.y].sort().join(',') ].sort().join('|'); // 이미 추가된 라인인지 확인 if (existingLines.has(lineKey)) { return; // 이미 있는 라인이면 스킵 } const direction = getLineDirection( { x: sktLine.p1.x, y: sktLine.p1.y }, { x: sktLine.p2.x, y: sktLine.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: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color, strokeWidth: lineStyle.width, name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type, attributes: attributes, direction: direction, 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(skeletonLine.lineName === 'roofLine'){ skeletonLine.set('visible', false); //임시 roof.set({ //stroke: 'black', strokeWidth: 4 }); }else{ } 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, pitch: wallLine.attributes.pitch, } }); 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, 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() 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, 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() 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){ 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') 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(); const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) if(inLine){ 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; } } } 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){ 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') 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(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ 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 ) { 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, 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() 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, 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() 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){ 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') 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(); const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) if(inLine){ 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; } } } 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){ 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') getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') 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(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ 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; } } } } // 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, position: 'top_in_start' }); 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, position: 'top_in_end' }); 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){ 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, position: 'top_out_start' }); }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){ 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; } } } 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){ 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') 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(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ 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; } } } }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, position: 'bottom_in_start' }); 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, position: 'bottom_in_end' }); 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){ 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, 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){ 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; } } } 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){ 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') 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(); const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) if(inLine){ 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; } } } } // 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((innerLines, point) => { return updateAndAddLine(innerLines, point); }, [...innerLines]); } return innerLines; } /** * EAVES(처마) Edge를 처리하여 내부 스켈레톤 선을 추가합니다. * @param {object} edgeResult - 스켈레톤 Edge 데이터 * @param {Array} skeletonLines - 스켈레톤 라인 배열 * @param {Set} processedInnerEdges - 중복 처리를 방지하기 위한 Set * @param roof * @param pitch */ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { let roof = canvas?.getObjects().find((object) => object.id === 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 => line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line) ); if(!outerLine) { outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); console.log('Has matching line:', outerLine); } let pitch = outerLine?.attributes?.pitch??0 const convertedPolygon = edgeResult.Polygon?.map(point => ({ x: typeof point.X === 'number' ? parseFloat(point.X) : 0, y: typeof point.Y === 'number' ? parseFloat(point.Y) : 0 })).filter(point => point.x !== 0 || point.y !== 0) || []; if (convertedPolygon.length > 0) { const skeletonPolygon = new QPolygon(convertedPolygon, { type: POLYGON_TYPE.ROOF, fill: false, stroke: 'blue', strokeWidth: 4, skeletonType: 'polygon', polygonName: '', parentId: roof.id, }); //canvas?.add(skeletonPolygon) //canvas.renderAll() } let eavesLines = [] for (let i = 0; i < polygonPoints.length; i++) { const p1 = polygonPoints[i]; const p2 = polygonPoints[(i + 1) % polygonPoints.length]; // 지붕 경계선과 교차 확인 및 클리핑 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); // } } } function findMatchingLine(edgePolygon, roof, roofPoints) { const edgePoints = edgePolygon.map(p => ({ x: p.X, y: p.Y })); for (let i = 0; i < edgePoints.length; i++) { const p1 = edgePoints[i]; const p2 = edgePoints[(i + 1) % edgePoints.length]; for (let j = 0; j < roofPoints.length; j++) { const rp1 = roofPoints[j]; const rp2 = roofPoints[(j + 1) % roofPoints.length]; if ((isSamePoint(p1, rp1) && isSamePoint(p2, rp2)) || (isSamePoint(p1, rp2) && isSamePoint(p2, rp1))) { // 매칭되는 라인을 찾아서 반환 return roof.lines.find(line => (isSamePoint(line.p1, rp1) && isSamePoint(line.p2, rp2)) || (isSamePoint(line.p1, rp2) && isSamePoint(line.p2, rp1)) ); } } } return null; } /** * GABLE(케라바) Edge를 처리하여 스켈레톤 선을 정리하고 연장합니다. * @param {object} edgeResult - 스켈레톤 Edge 데이터 * @param {Array} baseLines - 전체 외벽선 배열 * @param {Array} skeletonLines - 전체 스켈레톤 라인 배열 * @param selectBaseLine * @param lastSkeletonLines */ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine, lastSkeletonLines) { const edgePoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); //const polygons = createPolygonsFromSkeletonLines(skeletonLines, selectBaseLine); //console.log("edgePoints::::::", edgePoints) // 1. Initialize processedLines with a deep copy of lastSkeletonLines let processedLines = [] // 1. 케라바 면과 관련된 불필요한 스켈레톤 선을 제거합니다. for (let i = skeletonLines.length - 1; i >= 0; i--) { const line = skeletonLines[i]; const isEdgeLine = line.p1 && line.p2 && edgePoints.some(ep => Math.abs(ep.x - line.p1.x) < 0.001 && Math.abs(ep.y - line.p1.y) < 0.001) && edgePoints.some(ep => Math.abs(ep.x - line.p2.x) < 0.001 && Math.abs(ep.y - line.p2.y) < 0.001); if (isEdgeLine) { skeletonLines.splice(i, 1); } } //console.log("skeletonLines::::::", skeletonLines) //console.log("lastSkeletonLines", lastSkeletonLines) // 2. Find common lines between skeletonLines and lastSkeletonLines skeletonLines.forEach(line => { const matchingLine = lastSkeletonLines?.find(pl => pl.p1 && pl.p2 && line.p1 && line.p2 && ((Math.abs(pl.p1.x - line.p1.x) < 0.001 && Math.abs(pl.p1.y - line.p1.y) < 0.001 && Math.abs(pl.p2.x - line.p2.x) < 0.001 && Math.abs(pl.p2.y - line.p2.y) < 0.001) || (Math.abs(pl.p1.x - line.p2.x) < 0.001 && Math.abs(pl.p1.y - line.p2.y) < 0.001 && Math.abs(pl.p2.x - line.p1.x) < 0.001 && Math.abs(pl.p2.y - line.p1.y) < 0.001)) ); if (matchingLine) { processedLines.push({...matchingLine}); } }); // // 3. Remove lines that are part of the gable edge // processedLines = processedLines.filter(line => { // const isEdgeLine = line.p1 && line.p2 && // edgePoints.some(ep => Math.abs(ep.x - line.p1.x) < 0.001 && Math.abs(ep.y - line.p1.y) < 0.001) && // edgePoints.some(ep => Math.abs(ep.x - line.p2.x) < 0.001 && Math.abs(ep.y - line.p2.y) < 0.001); // // return !isEdgeLine; // }); //console.log("skeletonLines::::::", skeletonLines); //console.log("lastSkeletonLines", lastSkeletonLines); //console.log("processedLines after filtering", processedLines); return processedLines; } // --- Helper Functions --- /** * 두 점으로 이루어진 선분이 외벽선인지 확인합니다. * @param {object} p1 - 점1 {x, y} * @param {object} p2 - 점2 {x, y} * @param {Array} edges - 확인할 외벽선 Edge 배열 * @returns {boolean} 외벽선 여부 */ function isOuterEdge(p1, p2, edges) { const tolerance = 0.1; return edges.some(edge => { const lineStart = { x: edge.Begin.X, y: edge.Begin.Y }; const lineEnd = { x: edge.End.X, y: edge.End.Y }; const forwardMatch = 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; const backwardMatch = 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; return forwardMatch || backwardMatch; }); } /** * 스켈레톤 라인 배열에 새로운 라인을 추가합니다. (중복 방지) * @param id * @param {Array} skeletonLines - 스켈레톤 라인 배열 * @param {object} p1 - 시작점 * @param {object} p2 - 끝점 * @param {string} lineType - 라인 타입 * @param {string} color - 색상 * @param {number} width - 두께 * @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('|'); // if (processedInnerEdges.has(edgeKey)) return; // processedInnerEdges.add(edgeKey); const currentDegree = getDegreeByChon(pitch) const dx = Math.abs(p2.x - p1.x); const dy = Math.abs(p2.y - p1.y); const isDiagonal = dx > 0.1 && dy > 0.1; const normalizedType = isDiagonal ? LINE_TYPE.SUBLINE.HIP : lineType; // Count existing HIP lines const existingEavesCount = skeletonLines.filter(line => line.lineName === LINE_TYPE.SUBLINE.RIDGE ).length; // If this is a HIP line, its index will be the existing count const eavesIndex = normalizedType === LINE_TYPE.SUBLINE.RIDGE ? existingEavesCount : undefined; const newLine = { p1, p2, attributes: { roofId: id, actualSize: (isDiagonal) ? calcLineActualSize( { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }, currentDegree ) : calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), type: normalizedType, planeSize: calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE, isOuterEdge: isOuterLine, pitch: pitch, ...(eavesIndex !== undefined && { eavesIndex }) }, lineStyle: { color, width }, }; skeletonLines.push(newLine); //console.log('skeletonLines', skeletonLines); } /** * 폴리곤 좌표를 스켈레톤 생성에 적합하게 전처리합니다 (중복 제거, 시계 방향 정렬). * @param {Array} initialPoints - 초기 폴리곤 좌표 배열 * @returns {Array>} 전처리된 좌표 배열 (e.g., [[10, 10], ...]) */ const preprocessPolygonCoordinates = (initialPoints) => { 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(); } return coordinates.reverse(); }; /** * 스켈레톤 Edge와 외벽선이 동일한지 확인합니다. * @returns {boolean} 동일 여부 */ const isSameLine = (edgeStartX, edgeStartY, edgeEndX, edgeEndY, baseLine) => { const tolerance = 0.1; const { x1, y1, x2, y2 } = baseLine; const forwardMatch = Math.abs(edgeStartX - x1) < tolerance && Math.abs(edgeStartY - y1) < tolerance && Math.abs(edgeEndX - x2) < tolerance && Math.abs(edgeEndY - y2) < tolerance; const backwardMatch = Math.abs(edgeStartX - x2) < tolerance && Math.abs(edgeStartY - y2) < tolerance && Math.abs(edgeEndX - x1) < tolerance && Math.abs(edgeEndY - y1) < tolerance; return forwardMatch || backwardMatch; }; // --- Disconnected Line Processing --- /** * 점을 선분에 투영한 점의 좌표를 반환합니다. * @param {object} point - 투영할 점 {x, y} * @param {object} line - 기준 선분 {x1, y1, x2, y2} * @returns {object} 투영된 점의 좌표 {x, y} */ const getProjectionPoint = (point, line) => { const { x: px, y: py } = point; const { x1, y1, x2, y2 } = line; const dx = x2 - x1; const dy = y2 - y1; const lineLengthSq = dx * dx + dy * dy; if (lineLengthSq === 0) return { x: x1, y: y1 }; const t = ((px - x1) * dx + (py - y1) * dy) / lineLengthSq; if (t < 0) return { x: x1, y: y1 }; if (t > 1) return { x: x2, y: y2 }; return { x: x1 + t * dx, y: y1 + t * dy }; }; /** * 광선(Ray)과 선분(Segment)의 교차점을 찾습니다. * @param {object} rayStart - 광선의 시작점 * @param {object} rayDir - 광선의 방향 벡터 * @param {object} segA - 선분의 시작점 * @param {object} segB - 선분의 끝점 * @returns {{point: object, t: number}|null} 교차점 정보 또는 null */ function getRayIntersectionWithSegment(rayStart, rayDir, segA, segB) { const p = rayStart; const r = rayDir; const q = segA; const s = { x: segB.x - segA.x, y: segB.y - segA.y }; const rxs = r.x * s.y - r.y * s.x; if (Math.abs(rxs) < 1e-6) return null; // 평행 const q_p = { x: q.x - p.x, y: q.y - p.y }; const t = (q_p.x * s.y - q_p.y * s.x) / rxs; const u = (q_p.x * r.y - q_p.y * r.x) / rxs; if (t >= -1e-6 && u >= -1e-6 && u <= 1 + 1e-6) { return { point: { x: p.x + t * r.x, y: p.y + t * r.y }, t }; } return null; } /** * 한 점에서 다른 점 방향으로 광선을 쏘아 가장 가까운 교차점을 찾습니다. * @param {object} p1 - 광선의 방향을 결정하는 끝점 * @param {object} p2 - 광선의 시작점 * @param {Array} baseLines - 외벽선 배열 * @param {Array} skeletonLines - 스켈레톤 라인 배열 * @param {number} excludeIndex - 검사에서 제외할 현재 라인의 인덱스 * @returns {object|null} 가장 가까운 교차점 정보 또는 null */ function extendFromP2TowardP1(p1, p2, baseLines, skeletonLines, excludeIndex) { const dirVec = { x: p1.x - p2.x, y: p1.y - p2.y }; const len = Math.sqrt(dirVec.x * dirVec.x + dirVec.y * dirVec.y) || 1; const dir = { x: dirVec.x / len, y: dirVec.y / len }; let closestHit = null; const checkHit = (hit) => { if (hit && hit.t > len - 0.1) { // 원래 선분의 끝점(p1) 너머에서 교차하는지 확인 if (!closestHit || hit.t < closestHit.t) { closestHit = hit; } } }; if (Array.isArray(baseLines)) { baseLines.forEach(baseLine => { const hit = getRayIntersectionWithSegment(p2, dir, { x: baseLine.x1, y: baseLine.y1 }, { x: baseLine.x2, y: baseLine.y2 }); checkHit(hit); }); } if (Array.isArray(skeletonLines)) { skeletonLines.forEach((seg, i) => { if (i === excludeIndex) return; const hit = getRayIntersectionWithSegment(p2, dir, seg.p1, seg.p2); checkHit(hit); }); } return closestHit; } /** * 연결이 끊어진 스켈레톤 라인들을 찾아 연장 정보를 계산합니다. * @param {Array} skeletonLines - 스켈레톤 라인 배열 * @param {Array} baseLines - 외벽선 배열 * @returns {object} 끊어진 라인 정보가 담긴 객체 */ export const findDisconnectedSkeletonLines = (skeletonLines, baseLines) => { if (!skeletonLines?.length) return { disconnectedLines: [] }; const disconnectedLines = []; const pointsEqual = (p1, p2, epsilon = 0.1) => Math.abs(p1.x - p2.x) < epsilon && Math.abs(p1.y - p2.y) < epsilon; const isPointOnBase = (point) => baseLines?.some(baseLine => { const { x1, y1, x2, y2 } = baseLine; if (pointsEqual(point, { x: x1, y: y1 }) || pointsEqual(point, { x: x2, y: y2 })) return true; const dist = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); const dist1 = Math.sqrt(Math.pow(point.x - x1, 2) + Math.pow(point.y - y1, 2)); const dist2 = Math.sqrt(Math.pow(point.x - x2, 2) + Math.pow(point.y - y2, 2)); return Math.abs(dist - (dist1 + dist2)) < 0.1; }) || false; const isConnected = (line, lineIndex) => { const { p1, p2 } = line; let p1Connected = isPointOnBase(p1); let p2Connected = isPointOnBase(p2); if (!p1Connected || !p2Connected) { for (let i = 0; i < skeletonLines.length; i++) { if (i === lineIndex) continue; const other = skeletonLines[i]; if (!p1Connected && (pointsEqual(p1, other.p1) || pointsEqual(p1, other.p2))) p1Connected = true; if (!p2Connected && (pointsEqual(p2, other.p1) || pointsEqual(p2, other.p2))) p2Connected = true; if (p1Connected && p2Connected) break; } } return { p1Connected, p2Connected }; }; skeletonLines.forEach((line, index) => { const { p1Connected, p2Connected } = isConnected(line, index); if (p1Connected && p2Connected) return; let extendedLine = null; if (!p1Connected) { extendedLine = extendFromP2TowardP1(line.p1, line.p2, baseLines, skeletonLines, index); // [수정] 1차 연장 시도(Raycast) 실패 시, 수직 투영(Projection) 대신 모든 선분과의 교차점을 찾는 방식으로 변경 if (!extendedLine) { let closestIntersection = null; let minDistance = Infinity; // 모든 외벽선과 다른 내부선을 타겟으로 설정 const allTargetLines = [ ...baseLines.map(l => ({ p1: {x: l.x1, y: l.y1}, p2: {x: l.x2, y: l.y2} })), ...skeletonLines.filter((_, i) => i !== index) ]; allTargetLines.forEach(targetLine => { // 무한 직선 간의 교차점을 찾음 const intersection = getInfiniteLineIntersection(line.p1, line.p2, targetLine.p1, targetLine.p2); // 교차점이 존재하고, 타겟 '선분' 위에 있는지 확인 if (intersection && isPointOnSegmentForExtension(intersection, targetLine.p1, targetLine.p2)) { // 연장 방향이 올바른지 확인 (뒤로 가지 않도록) const lineVec = { x: line.p1.x - line.p2.x, y: line.p1.y - line.p2.y }; const intersectVec = { x: intersection.x - line.p1.x, y: intersection.y - line.p1.y }; const dotProduct = lineVec.x * intersectVec.x + lineVec.y * intersectVec.y; if (dotProduct >= -1e-6) { // 교차점이 p1 기준으로 '앞'에 있을 경우 const dist = Math.sqrt(Math.pow(line.p1.x - intersection.x, 2) + Math.pow(line.p1.y - intersection.y, 2)); if (dist > 0.1 && dist < minDistance) { // 자기 자신이 아니고, 가장 가까운 교차점 갱신 minDistance = dist; closestIntersection = intersection; } } } }); if (closestIntersection) { extendedLine = { point: closestIntersection }; } } } else if (!p2Connected) { extendedLine = extendFromP2TowardP1(line.p2, line.p1, baseLines, skeletonLines, index); // [수정] 1차 연장 시도(Raycast) 실패 시, 수직 투영(Projection) 대신 모든 선분과의 교차점을 찾는 방식으로 변경 if (!extendedLine) { let closestIntersection = null; let minDistance = Infinity; // 모든 외벽선과 다른 내부선을 타겟으로 설정 const allTargetLines = [ ...baseLines.map(l => ({ p1: {x: l.x1, y: l.y1}, p2: {x: l.x2, y: l.y2} })), ...skeletonLines.filter((_, i) => i !== index) ]; allTargetLines.forEach(targetLine => { // 무한 직선 간의 교차점을 찾음 const intersection = getInfiniteLineIntersection(line.p2, line.p1, targetLine.p1, targetLine.p2); // 교차점이 존재하고, 타겟 '선분' 위에 있는지 확인 if (intersection && isPointOnSegmentForExtension(intersection, targetLine.p1, targetLine.p2)) { // 연장 방향이 올바른지 확인 (뒤로 가지 않도록) const lineVec = { x: line.p2.x - line.p1.x, y: line.p2.y - line.p1.y }; const intersectVec = { x: intersection.x - line.p2.x, y: intersection.y - line.p2.y }; const dotProduct = lineVec.x * intersectVec.x + lineVec.y * intersectVec.y; if (dotProduct >= -1e-6) { // 교차점이 p2 기준으로 '앞'에 있을 경우 const dist = Math.sqrt(Math.pow(line.p2.x - intersection.x, 2) + Math.pow(line.p2.y - intersection.y, 2)); if (dist > 0.1 && dist < minDistance) { // 자기 자신이 아니고, 가장 가까운 교차점 갱신 minDistance = dist; closestIntersection = intersection; } } } }); if (closestIntersection) { extendedLine = { point: closestIntersection }; } } } disconnectedLines.push({ line, index, p1Connected, p2Connected, extendedLine }); }); return { disconnectedLines }; }; /** * 연장된 스켈레톤 라인들이 서로 교차하는 경우, 교차점에서 잘라냅니다. * 이 함수는 skeletonLines 배열의 요소를 직접 수정하여 접점에서 선이 멈추도록 합니다. * @param {Array} skeletonLines - (수정될) 전체 스켈레톤 라인 배열 * @param {Array} disconnectedLines - 연장 정보가 담긴 배열 */ const trimIntersectingExtendedLines = (skeletonLines, disconnectedLines) => { // disconnectedLines에는 연장된 선들의 정보가 들어있음 for (let i = 0; i < disconnectedLines.length; i++) { for (let j = i + 1; j < disconnectedLines.length; j++) { const dLine1 = disconnectedLines[i]; const dLine2 = disconnectedLines[j]; // skeletonLines 배열에서 직접 참조를 가져오므로, 여기서 line1, line2를 수정하면 // 원본 skeletonLines 배열의 내용이 변경됩니다. const line1 = skeletonLines[dLine1.index]; const line2 = skeletonLines[dLine2.index]; if(!line1 || !line2) continue; // 두 연장된 선분이 교차하는지 확인 const intersection = getLineIntersection(line1.p1, line1.p2, line2.p1, line2.p2); if (intersection) { // 교차점이 있다면, 각 선의 연장된 끝점을 교차점으로 업데이트합니다. // 이 변경 사항은 skeletonLines 배열에 바로 반영됩니다. if (!dLine1.p1Connected) { // p1이 연장된 점이었으면 line1.p1 = intersection; } else { // p2가 연장된 점이었으면 line1.p2 = intersection; } if (!dLine2.p1Connected) { // p1이 연장된 점이었으면 line2.p1 = intersection; } else { // p2가 연장된 점이었으면 line2.p2 = intersection; } } } } } /** * skeletonLines와 selectBaseLine을 이용하여 다각형이 되는 좌표를 구합니다. * selectBaseLine의 좌표는 제외합니다. * @param {Array} skeletonLines - 스켈레톤 라인 배열 * @param {Object} selectBaseLine - 선택된 베이스 라인 (p1, p2 속성을 가진 객체) * @returns {Array>} 다각형 좌표 배열의 배열 */ const createPolygonsFromSkeletonLines = (skeletonLines, selectBaseLine) => { if (!skeletonLines?.length) return []; // 1. 모든 교차점 찾기 const intersections = findAllIntersections(skeletonLines); // 2. 모든 포인트 수집 (엔드포인트 + 교차점) const allPoints = collectAllPoints(skeletonLines, intersections); // 3. selectBaseLine 상의 점들 제외 const filteredPoints = allPoints.filter(point => { if (!selectBaseLine?.startPoint || !selectBaseLine?.endPoint) return true; // 점이 selectBaseLine 상에 있는지 확인 return !isPointOnSegment( point, selectBaseLine.startPoint, selectBaseLine.endPoint ); }); }; /** * 두 무한 직선의 교차점을 찾습니다. (선분X) * @param {object} p1 - 직선1의 점1 * @param {object} p2 - 직선1의 점2 * @param {object} p3 - 직선2의 점1 * @param {object} p4 - 직선2의 점2 * @returns {object|null} 교차점 좌표 또는 null (평행/동일선) */ const getInfiniteLineIntersection = (p1, p2, p3, p4) => { const x1 = p1.x, y1 = p1.y; const x2 = p2.x, y2 = p2.y; const x3 = p3.x, y3 = p3.y; const x4 = p4.x, y4 = p4.y; const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (Math.abs(denom) < 1e-10) return null; // 평행 또는 동일선 const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; return { x: x1 + t * (x2 - x1), y: y1 + t * (y2 - y1) }; }; /** * 점이 선분 위에 있는지 확인합니다. (연장 로직용) * @param {object} point - 확인할 점 * @param {object} segStart - 선분 시작점 * @param {object} segEnd - 선분 끝점 * @param {number} tolerance - 허용 오차 * @returns {boolean} 선분 위 여부 */ const isPointOnSegmentForExtension = (point, segStart, segEnd, tolerance = 0.1) => { const dist = Math.sqrt(Math.pow(segEnd.x - segStart.x, 2) + Math.pow(segEnd.y - segStart.y, 2)); const dist1 = Math.sqrt(Math.pow(point.x - segStart.x, 2) + Math.pow(point.y - segStart.y, 2)); const dist2 = Math.sqrt(Math.pow(point.x - segEnd.x, 2) + Math.pow(point.y - segEnd.y, 2)); return Math.abs(dist - (dist1 + dist2)) < tolerance; }; /** * 스켈레톤 라인들 간의 모든 교차점을 찾습니다. * @param {Array} skeletonLines - 스켈레톤 라인 배열 (각 요소는 {p1: {x, y}, p2: {x, y}} 형태) * @returns {Array} 교차점 배열 */ const findAllIntersections = (skeletonLines) => { const intersections = []; const processedPairs = new Set(); for (let i = 0; i < skeletonLines.length; i++) { for (let j = i + 1; j < skeletonLines.length; j++) { const pairKey = `${i}-${j}`; if (processedPairs.has(pairKey)) continue; processedPairs.add(pairKey); const line1 = skeletonLines[i]; const line2 = skeletonLines[j]; // 두 라인이 교차하는지 확인 const intersection = getLineIntersection( line1.p1, line1.p2, line2.p1, line2.p2 ); if (intersection) { // 교차점이 실제로 두 선분 위에 있는지 확인 if (isPointOnSegment(intersection, line1.p1, line1.p2) && isPointOnSegment(intersection, line2.p1, line2.p2)) { intersections.push(intersection); } } } } return intersections; }; /** * 스켈레톤 라인들과 교차점들을 모아서 모든 포인트를 수집합니다. * @param {Array} skeletonLines - 스켈레톤 라인 배열 * @param {Array} intersections - 교차점 배열 * @returns {Array} 모든 포인트 배열 */ const collectAllPoints = (skeletonLines, intersections) => { const allPoints = new Map(); const pointKey = (point) => `${point.x.toFixed(3)},${point.y.toFixed(3)}`; // 스켈레톤 라인의 엔드포인트들 추가 skeletonLines.forEach(line => { const key1 = pointKey(line.p1); const key2 = pointKey(line.p2); if (!allPoints.has(key1)) { allPoints.set(key1, { ...line.p1 }); } if (!allPoints.has(key2)) { allPoints.set(key2, { ...line.p2 }); } }); // 교차점들 추가 intersections.forEach(intersection => { const key = pointKey(intersection); if (!allPoints.has(key)) { allPoints.set(key, { ...intersection }); } }); return Array.from(allPoints.values()); }; // 필요한 유틸리티 함수들 const getLineIntersection = (p1, p2, p3, p4) => { const x1 = p1.x, y1 = p1.y; const x2 = p2.x, y2 = p2.y; const x3 = p3.x, y3 = p3.y; const x4 = p4.x, y4 = p4.y; const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (Math.abs(denom) < 1e-10) return null; const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { return { x: x1 + t * (x2 - x1), y: y1 + t * (y2 - y1) }; } return null; }; const isPointOnSegment = (point, segStart, segEnd) => { const tolerance = 1e-6; const crossProduct = (point.y - segStart.y) * (segEnd.x - segStart.x) - (point.x - segStart.x) * (segEnd.y - segStart.y); if (Math.abs(crossProduct) > tolerance) return false; const dotProduct = (point.x - segStart.x) * (segEnd.x - segStart.x) + (point.y - segStart.y) * (segEnd.y - segStart.y); const squaredLength = (segEnd.x - segStart.x) ** 2 + (segEnd.y - segStart.y) ** 2; return dotProduct >= 0 && dotProduct <= squaredLength; }; // Export all necessary functions export { findAllIntersections, collectAllPoints, createPolygonsFromSkeletonLines, preprocessPolygonCoordinates, findOppositeLine, createOrderedBasePoints, createInnerLinesFromSkeleton }; /** * Finds the opposite line in a polygon based on the given line * @param {Array} edges - The polygon edges from canvas.skeleton.Edges * @param {Object} startPoint - The start point of the line to find opposite for * @param {Object} endPoint - The end point of the line to find opposite for * @param targetPosition * @returns {Object|null} The opposite line if found, null otherwise */ function findOppositeLine(edges, startPoint, endPoint, points) { const result = []; // 1. 다각형 찾기 const polygons = findPolygonsContainingLine(edges, startPoint, endPoint); if (polygons.length === 0) return null; const referenceSlope = calculateSlope(startPoint, endPoint); // 각 다각형에 대해 처리 for (const polygon of polygons) { // 2. 기준 선분의 인덱스 찾기 let baseIndex = -1; for (let i = 0; i < polygon.length; i++) { const p1 = { x: polygon[i].X, y: polygon[i].Y }; const p2 = { x: polygon[(i + 1) % polygon.length].X, y: polygon[(i + 1) % polygon.length].Y }; if ((isSamePoint(p1, startPoint) && isSamePoint(p2, endPoint)) || (isSamePoint(p1, endPoint) && isSamePoint(p2, startPoint))) { baseIndex = i; break; } } if (baseIndex === -1) continue; // 현재 다각형에서 기준 선분을 찾지 못한 경우 // 3. 다각형의 각 선분을 순회하면서 평행한 선분 찾기 const polyLength = polygon.length; for (let i = 0; i < polyLength; i++) { if (i === baseIndex) continue; // 기준 선분은 제외 const p1 = { x: polygon[i].X, y: polygon[i].Y }; const p2 = { x: polygon[(i + 1) % polyLength].X, y: polygon[(i + 1) % polyLength].Y }; const p1Exist = points.some(p => Math.abs(p.x - p1.x) < 0.0001 && Math.abs(p.y - p1.y) < 0.0001 ); const p2Exist = points.some(p => Math.abs(p.x - p2.x) < 0.0001 && Math.abs(p.y - p2.y) < 0.0001 ); if(p1Exist && p2Exist){ const position = getLinePosition( { start: p1, end: p2 }, { start: startPoint, end: endPoint } ); result.push({ start: p1, end: p2, position: position, polygon: polygon }); } } } return result.length > 0 ? result:[]; } 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 좌표로 판단 return deltaY > 0 ? 'bottom' : 'top'; } else { // 수직선에 가까운 경우 - X 좌표로 판단 return deltaX > 0 ? 'right' : 'left'; } } /** * Helper function to find if two points are the same within a tolerance */ function isSamePoint(p1, p2, tolerance = 0.1) { return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance; } // 두 점을 지나는 직선의 기울기 계산 function calculateSlope(p1, p2) { // 수직선인 경우 (기울기 무한대) if (p1.x === p2.x) return Infinity; return (p2.y - p1.y) / (p2.x - p1.x); } /** * Helper function to find the polygon containing the given line */ function findPolygonsContainingLine(edges, p1, p2) { const polygons = []; for (const edge of edges) { const polygon = edge.Polygon; for (let i = 0; i < polygon.length; i++) { const ep1 = { x: polygon[i].X, y: polygon[i].Y }; const ep2 = { x: polygon[(i + 1) % polygon.length].X, y: polygon[(i + 1) % polygon.length].Y }; if ((isSamePoint(ep1, p1) && isSamePoint(ep2, p2)) || (isSamePoint(ep1, p2) && isSamePoint(ep2, p1))) { polygons.push(polygon); break; // 이 다각형에 대한 검사 완료 } } } return polygons; // 일치하는 모든 다각형 반환 } /** * roof.lines로 만들어진 다각형 내부에만 선분이 존재하도록 클리핑합니다. * @param {Object} p1 - 선분의 시작점 {x, y} * @param {Object} p2 - 선분의 끝점 {x, y} * @param {Array} roofLines - 지붕 경계선 배열 (QLine 객체의 배열) * @param skeletonLines * @returns {Object} {p1: {x, y}, p2: {x, y}} - 다각형 내부로 클리핑된 선분 */ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { if (!roofLines || !roofLines.length) { return { p1: { ...p1 }, p2: { ...p2 } }; } const dx = Math.abs(p2.x - p1.x); const dy = Math.abs(p2.y - p1.y); const isDiagonal = dx > 0.5 && dy > 0.5; // 기본값으로 원본 좌표 설정 let clippedP1 = { x: p1.x, y: p1.y }; let clippedP2 = { x: p2.x, y: p2.y }; // p1이 다각형 내부에 있는지 확인 const p1Inside = isPointInsidePolygon(p1, roofLines); // p2가 다각형 내부에 있는지 확인 const p2Inside = isPointInsidePolygon(p2, roofLines); //console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); // 두 점 모두 내부에 있으면 그대로 반환 if (p1Inside && p2Inside) { if(!selectLine || isDiagonal){ return { p1: clippedP1, p2: clippedP2 }; } //console.log('평행선::', clippedP1, clippedP2) return { p1: clippedP1, p2: clippedP2 }; } // 선분과 다각형 경계선의 교차점들을 찾음 const intersections = []; for (const line of roofLines) { const lineP1 = { x: line.x1, y: line.y1 }; const lineP2 = { x: line.x2, y: line.y2 }; const intersection = getLineIntersection(p1, p2, lineP1, lineP2); if (intersection) { // 교차점이 선분 위에 있는지 확인 const t = getParameterT(p1, p2, intersection); if (t >= 0 && t <= 1) { intersections.push({ point: intersection, t: t }); } } } //console.log('Found intersections:', intersections.length); // 교차점들을 t 값으로 정렬 intersections.sort((a, b) => a.t - b.t); if (!p1Inside && !p2Inside) { // 두 점 모두 외부에 있는 경우 if (intersections.length >= 2) { //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'); // 교차점이 충분하지 않으면 원본 반환 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'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; // p2는 이미 내부에 있으므로 원본 유지 clippedP2.x = p2.x; clippedP2.y = p2.y; } } else if (p1Inside && !p2Inside) { // p1이 내부, p2가 외부 if (intersections.length > 0) { //console.log('p1 inside, p2 outside - moving p2 to intersection'); // p1은 이미 내부에 있으므로 원본 유지 clippedP1.x = p1.x; clippedP1.y = p1.y; clippedP2.x = intersections[0].point.x; clippedP2.y = intersections[0].point.y; } } return { p1: clippedP1, p2: clippedP2 }; } 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 값을 구합니다. * @param {Object} p1 - 선분의 시작점 * @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; } else { return dy === 0 ? 0 : (point.y - p1.y) / dy; } } export const convertBaseLinesToPoints = (baseLines) => { const points = []; const pointSet = new Set(); baseLines.forEach((line) => { [ { x: line.x1, y: line.y1 }, { x: line.x2, y: line.y2 } ].forEach(point => { const key = `${point.x},${point.y}`; if (!pointSet.has(key)) { pointSet.add(key); points.push(point); } }); }); return points; }; function getLineDirection(p1, p2) { const dx = p2.x - p1.x; const dy = p2.y - p1.y; const angle = Math.atan2(dy, dx) * 180 / Math.PI; // 각도 범위에 따라 방향 반환 if ((angle >= -45 && angle < 45)) return 'right'; if ((angle >= 45 && angle < 135)) return 'bottom'; if ((angle >= 135 || angle < -135)) return 'left'; return 'top'; // (-135 ~ -45) } // selectLine과 baseLines 비교하여 방향 찾기 function findLineDirection(selectLine, baseLines) { for (const baseLine of baseLines) { // baseLine의 시작점과 끝점 const baseStart = baseLine.startPoint; const baseEnd = baseLine.endPoint; // selectLine의 시작점과 끝점 const selectStart = selectLine.startPoint; const selectEnd = selectLine.endPoint; // 정방향 또는 역방향으로 일치하는지 확인 if ((isSamePoint(baseStart, selectStart) && isSamePoint(baseEnd, selectEnd)) || (isSamePoint(baseStart, selectEnd) && isSamePoint(baseEnd, selectStart))) { // baseLine의 방향 계산 const dx = baseEnd.x - baseStart.x; const dy = baseEnd.y - baseStart.y; // 기울기를 바탕으로 방향 판단 if (Math.abs(dx) > Math.abs(dy)) { return dx > 0 ? 'right' : 'left'; } else { return dy > 0 ? 'down' : 'up'; } } } return null; // 일치하는 라인이 없는 경우 } /** * baseLines를 연결하여 다각형 순서로 정렬된 점들 반환 * @param {Array} baseLines - 라인 배열 * @returns {Array} 순서대로 정렬된 점들의 배열 */ function getOrderedBasePoints(baseLines) { if (baseLines.length === 0) return []; const points = []; const usedLines = new Set(); // 첫 번째 라인으로 시작 let currentLine = baseLines[0]; points.push({ ...currentLine.startPoint }); points.push({ ...currentLine.endPoint }); usedLines.add(0); let lastPoint = currentLine.endPoint; // 연결된 라인들을 찾아가며 점들 수집 while (usedLines.size < baseLines.length) { let foundNext = false; for (let i = 0; i < baseLines.length; i++) { if (usedLines.has(i)) continue; const line = baseLines[i]; // 현재 끝점과 연결되는 라인 찾기 if (isSamePoint(lastPoint, line.startPoint)) { points.push({ ...line.endPoint }); lastPoint = line.endPoint; usedLines.add(i); foundNext = true; break; } else if (isSamePoint(lastPoint, line.endPoint)) { points.push({ ...line.startPoint }); lastPoint = line.startPoint; usedLines.add(i); foundNext = true; break; } } if (!foundNext) break; // 연결되지 않는 경우 중단 } // 마지막 점이 첫 번째 점과 같으면 제거 (닫힌 다각형) if (points.length > 2 && isSamePoint(points[0], points[points.length - 1])) { points.pop(); } return points; } /** * roof.points와 baseLines가 정확히 대응되는 경우의 간단한 버전 */ function createOrderedBasePoints(roofPoints, baseLines) { const basePoints = []; // baseLines에서 연결된 순서대로 점들을 추출 const orderedBasePoints = getOrderedBasePoints(baseLines); // roofPoints의 개수와 맞추기 if (orderedBasePoints.length >= roofPoints.length) { return orderedBasePoints.slice(0, roofPoints.length); } // 부족한 경우 roofPoints 기반으로 보완 roofPoints.forEach((roofPoint, index) => { if (index < orderedBasePoints.length) { basePoints.push(orderedBasePoints[index]); } else { basePoints.push({ ...roofPoint }); // fallback } }); return basePoints; } export const getSelectLinePosition = (wall, selectLine, options = {}) => { const { testDistance = 10, epsilon = 0.5, debug = false } = options; if (!wall || !selectLine) { if (debug) console.log('ERROR: wall 또는 selectLine이 없음'); return { position: 'unknown', orientation: 'unknown', error: 'invalid_input' }; } // selectLine의 좌표 추출 const lineCoords = extractLineCoords(selectLine); if (!lineCoords.valid) { if (debug) console.log('ERROR: selectLine 좌표가 유효하지 않음'); return { position: 'unknown', orientation: 'unknown', error: 'invalid_coords' }; } const { x1, y1, x2, y2 } = lineCoords; //console.log('wall.points', wall.baseLines); for(const line of wall.baseLines) { //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); // 객체 비교 대신 좌표값 비교 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); } } // 라인 방향 분석 const lineInfo = analyzeLineOrientation(x1, y1, x2, y2, epsilon); // if (debug) { // console.log('=== getSelectLinePosition ==='); // console.log('selectLine 좌표:', lineCoords); // console.log('라인 방향:', lineInfo.orientation); // } // 라인의 중점 const midX = (x1 + x2) / 2; const midY = (y1 + y2) / 2; let position = 'unknown'; if (lineInfo.orientation === 'horizontal') { // 수평선: top 또는 bottom 판단 // 바로 위쪽 테스트 포인트 const topTestPoint = { x: midX, y: midY - testDistance }; // 바로 아래쪽 테스트 포인트 const bottomTestPoint = { x: midX, y: midY + testDistance }; const topIsInside = checkPointInPolygon(topTestPoint, wall); const bottomIsInside = checkPointInPolygon(bottomTestPoint, wall); // if (debug) { // console.log('수평선 테스트:'); // console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); // console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); // } // top 조건: 위쪽이 외부, 아래쪽이 내부 if (!topIsInside && bottomIsInside) { position = 'top'; } // bottom 조건: 위쪽이 내부, 아래쪽이 외부 else if (topIsInside && !bottomIsInside) { position = 'bottom'; } } else if (lineInfo.orientation === 'vertical') { // 수직선: left 또는 right 판단 // 바로 왼쪽 테스트 포인트 const leftTestPoint = { x: midX - testDistance, y: midY }; // 바로 오른쪽 테스트 포인트 const rightTestPoint = { x: midX + testDistance, y: midY }; const leftIsInside = checkPointInPolygon(leftTestPoint, wall); const rightIsInside = checkPointInPolygon(rightTestPoint, wall); // if (debug) { // console.log('수직선 테스트:'); // console.log(' 왼쪽 포인트:', leftTestPoint, '-> 내부:', leftIsInside); // console.log(' 오른쪽 포인트:', rightTestPoint, '-> 내부:', rightIsInside); // } // left 조건: 왼쪽이 외부, 오른쪽이 내부 if (!leftIsInside && rightIsInside) { position = 'left'; } // right 조건: 오른쪽이 외부, 왼쪽이 내부 else if (leftIsInside && !rightIsInside) { position = 'right'; } } else { // 대각선 if (debug) console.log('대각선은 지원하지 않음'); return { position: 'unknown', orientation: 'diagonal', error: 'not_supported' }; } const result = { position, orientation: lineInfo.orientation, method: 'inside_outside_test', confidence: position !== 'unknown' ? 1.0 : 0.0, testPoints: lineInfo.orientation === 'horizontal' ? { top: { x: midX, y: midY - testDistance }, bottom: { x: midX, y: midY + testDistance } } : { left: { x: midX - testDistance, y: midY }, right: { x: midX + testDistance, y: midY } }, midPoint: { x: midX, y: midY } }; // if (debug) { // console.log('최종 결과:', result); // } return result; }; // 점이 다각형 내부에 있는지 확인하는 함수 const checkPointInPolygon = (point, wall) => { // 2. wall.baseLines를 이용한 Ray Casting Algorithm if (!wall.baseLines || !Array.isArray(wall.baseLines)) { console.warn('wall.baseLines가 없습니다'); return false; } return raycastingAlgorithm(point, wall.baseLines); }; // Ray Casting Algorithm 구현 const raycastingAlgorithm = (point, lines) => { const { x, y } = point; let intersectionCount = 0; for (const line of lines) { const coords = extractLineCoords(line); if (!coords.valid) continue; const { x1, y1, x2, y2 } = coords; // Ray casting: 점에서 오른쪽으로 수평선을 그어서 다각형 경계와의 교점 개수를 셈 // 교점 개수가 홀수면 내부, 짝수면 외부 // 선분의 y 범위 확인 if ((y1 > y) !== (y2 > y)) { // x 좌표에서의 교점 계산 const intersectX = (x2 - x1) * (y - y1) / (y2 - y1) + x1; // 점의 오른쪽에 교점이 있으면 카운트 if (x < intersectX) { intersectionCount++; } } } // 홀수면 내부, 짝수면 외부 return intersectionCount % 2 === 1; }; // 라인 객체에서 좌표를 추출하는 헬퍼 함수 (중복 방지용 - 이미 있다면 제거) const extractLineCoords = (line) => { if (!line) { return { x1: 0, y1: 0, x2: 0, y2: 0, valid: false }; } let x1, y1, x2, y2; // 다양한 라인 객체 형태에 대응 if (line.x1 !== undefined && line.y1 !== undefined && line.x2 !== undefined && line.y2 !== undefined) { x1 = line.x1; y1 = line.y1; x2 = line.x2; y2 = line.y2; } else if (line.startPoint && line.endPoint) { x1 = line.startPoint.x; y1 = line.startPoint.y; x2 = line.endPoint.x; y2 = line.endPoint.y; } else if (line.p1 && line.p2) { x1 = line.p1.x; y1 = line.p1.y; x2 = line.p2.x; y2 = line.p2.y; } else { return { x1: 0, y1: 0, x2: 0, y2: 0, valid: false }; } const coords = [x1, y1, x2, y2]; const valid = coords.every(coord => typeof coord === 'number' && !Number.isNaN(coord) && Number.isFinite(coord) ); return { x1, y1, x2, y2, valid }; }; // 라인 방향 분석 함수 (중복 방지용 - 이미 있다면 제거) const analyzeLineOrientation = (x1, y1, x2, y2, epsilon = 0.5) => { const dx = x2 - x1; const dy = y2 - y1; const absDx = Math.abs(dx); const absDy = Math.abs(dy); const length = Math.sqrt(dx * dx + dy * dy); let orientation; if (absDy < epsilon && absDx >= epsilon) { orientation = 'horizontal'; } else if (absDx < epsilon && absDy >= epsilon) { orientation = 'vertical'; } else { orientation = 'diagonal'; } return { orientation, dx, dy, absDx, absDy, length, midX: (x1 + x2) / 2, midY: (y1 + y2) / 2, isHorizontal: orientation === 'horizontal', isVertical: orientation === 'vertical' }; }; // 점에서 선분까지의 최단 거리를 계산하는 도우미 함수 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; const dot = A * C + B * D; const lenSq = C * C + D * D; let param = -1; if (lenSq !== 0) { param = dot / lenSq; } 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); // 병합 결과 로그 console.log(`Merged ${direction} lines:`, merged); return merged; }; /** * 주어진 점을 포함하는 라인을 찾는 함수 * @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; 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} lineStart - 선분의 시작점 {x, y} * @param {Object} lineEnd - 선분의 끝점 {x, y} * @param {number} tolerance - 허용 오차 * @returns {boolean} */ 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 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 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; } }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 = { ...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 } }; // 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 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; } /** * 세 점(p1 -> p2 -> p3)의 방향성을 계산합니다. (2D 외적) * 반시계 방향(CCW)으로 그려진 폴리곤(Y축 Down) 기준: * - 결과 > 0 : 오른쪽 턴 (Right Turn) -> 골짜기 (Valley/Reflex Vertex) * - 결과 < 0 : 왼쪽 턴 (Left Turn) -> 외곽 모서리 (Convex Vertex) * - 결과 = 0 : 직선 */ 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) ); } // 연결된 라인을 못 찾았거나 끊겨있으면 판단 불가 (일단 false) if (!neighborLine) return false; // 2. 세 점을 구성하여 회전 방향(Turn) 계산 // 순서: PrevLine.Start -> [TargetVertex] -> NextLine.End let p1, p2, p3; 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}; } // 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 }; }