skeleton v6
This commit is contained in:
parent
ef910385d3
commit
1ddc176e7b
@ -4,11 +4,12 @@ import { calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
|
||||
/**
|
||||
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다.
|
||||
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 갱신합니다.
|
||||
* 이 함수는 지붕의 스켈레톤 상태를 관리하며, 외곽선의 속성(gable 등) 변경에 따라
|
||||
* 스켈레톤을 재계산하고 다시 그립니다.
|
||||
* @param {string} roofId - 대상 지붕 객체의 ID
|
||||
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
|
||||
* @param {string} textMode - 텍스트 표시 모드
|
||||
* @param {Array<QLine>} existingSkeletonLines - 기존에 생성된 스켈레톤 라인
|
||||
*/
|
||||
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
|
||||
const roof = canvas?.getObjects().find((object) => object.id === roofId)
|
||||
@ -16,121 +17,88 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
|
||||
console.error(`Roof with id "${roofId}" not found.`);
|
||||
return;
|
||||
}
|
||||
//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;
|
||||
|
||||
// 기존에 그려진 내부선을 모두 제거합니다.
|
||||
if (roof.innerLines) {
|
||||
roof.innerLines.forEach(line => canvas.remove(line));
|
||||
roof.innerLines = [];
|
||||
}
|
||||
|
||||
const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId)
|
||||
if (!wall) {
|
||||
console.error(`Wall for roof id "${roofId}" not found.`);
|
||||
return;
|
||||
}
|
||||
const baseLines = roof.lines;
|
||||
|
||||
// 2. 스켈레톤 생성 및 그리기
|
||||
skeletonBuilder(roofId, canvas, textMode, roof)
|
||||
}
|
||||
|
||||
/**
|
||||
* SkeletonBuilder를 사용하여 스켈레톤을 생성하고 내부선을 그립니다.
|
||||
* @param {string} roofId - 지붕 ID
|
||||
* @param {fabric.Canvas} canvas - 캔버스 객체
|
||||
* @param {string} textMode - 텍스트 모드
|
||||
* @param {fabric.Object} roof - 지붕 객체
|
||||
* @param {Array<QLine>} skeletonLines - 스켈레톤 라인 배열
|
||||
*/
|
||||
export const skeletonBuilder = (roofId, canvas, textMode, roof) => {
|
||||
const geoJSONPolygon = toGeoJSON(roof.points)
|
||||
const skeletonLines = []
|
||||
try {
|
||||
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
|
||||
geoJSONPolygon.pop()
|
||||
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
||||
// --- 1. 항상 최신 폴리곤 정보로 기본 스켈레톤 생성 ---
|
||||
const geoJSONPolygon = toGeoJSON(roof.points);
|
||||
geoJSONPolygon.pop(); // SkeletonBuilder는 닫히지 않은 폴리곤을 기대
|
||||
const initialSkeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]);
|
||||
console.log("Skeleton recalculated from base polygon.", initialSkeleton.edge_analysis);
|
||||
|
||||
console.log(`지붕 형태: ${skeleton.roof_type}`, skeleton.edge_analysis)
|
||||
const skeletonLines = [];
|
||||
const processedInnerEdges = new Set();
|
||||
|
||||
// 스켈레톤 데이터를 기반으로 내부선 생성
|
||||
roof.innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode, skeletonLines)
|
||||
// --- 2. 외곽선 속성에 따라 내부선 생성 여부 결정 ---
|
||||
initialSkeleton.Edges.forEach(edgeResult => {
|
||||
const { Begin, End } = edgeResult.Edge;
|
||||
const baseLine = baseLines.find(line => isSameLine(Begin.X, Begin.Y, End.X, End.Y, line));
|
||||
|
||||
// 캔버스에 스켈레톤 상태 저장
|
||||
if (!canvas.skeletonStates) {
|
||||
canvas.skeletonStates = {}
|
||||
canvas.skeletonLines = []
|
||||
}
|
||||
canvas.skeletonStates[roofId] = true
|
||||
|
||||
canvas.renderAll()
|
||||
} catch (e) {
|
||||
console.error('스켈레톤 생성 중 오류 발생:', e)
|
||||
if (canvas.skeletonStates) {
|
||||
canvas.skeletonStates[roofId] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스켈레톤 결과와 외벽선 정보를 바탕으로 내부선(용마루, 추녀)을 생성합니다.
|
||||
* @param {object} skeleton - SkeletonBuilder로부터 반환된 스켈레톤 객체
|
||||
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
|
||||
* @param {fabric.Object} roof - 대상 지붕 객체
|
||||
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
|
||||
* @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
|
||||
* @param {Array<QLine>} skeletonLines - 스켈레톤 라인 배열 (수정 대상)
|
||||
* @returns {Array<QLine>} 생성된 내부선(QLine) 배열
|
||||
*/
|
||||
const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMode, skeletonLines) => {
|
||||
if (!skeleton?.Edges) return []
|
||||
|
||||
const processedInnerEdges = new Set()
|
||||
|
||||
// 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다.
|
||||
skeleton.Edges.forEach(edgeResult => {
|
||||
processEavesEdge(edgeResult, skeletonLines, processedInnerEdges)
|
||||
});
|
||||
|
||||
|
||||
// 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다.
|
||||
skeleton.Edges.forEach(edgeResult => {
|
||||
|
||||
const { Begin, End } = edgeResult.Edge;
|
||||
const gableBaseLine = baseLines.find(line =>
|
||||
line.attributes.type === 'gable' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
|
||||
);
|
||||
|
||||
if (gableBaseLine) {
|
||||
if(canvas.skeletonLines.length > 0){
|
||||
skeletonLines = canvas.skeletonLines;
|
||||
// 외곽선이 'gable'이 아닌 경우에만 해당 면의 내부선을 생성합니다.
|
||||
if (baseLine && baseLine.attributes.type !== 'gable') {
|
||||
processEavesEdge(edgeResult, skeletonLines, processedInnerEdges);
|
||||
}
|
||||
processGableEdge(edgeResult, baseLines, skeletonLines, gableBaseLine);
|
||||
canvas.skeletonLines = skeletonLines;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다.
|
||||
const innerLines = [];
|
||||
skeletonLines.forEach(line => {
|
||||
const { p1, p2, attributes, lineStyle } = line;
|
||||
const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
|
||||
parentId: roof.id,
|
||||
fontSize: roof.fontSize,
|
||||
stroke: lineStyle.color,
|
||||
strokeWidth: lineStyle.width,
|
||||
name: attributes.type,
|
||||
textMode: textMode,
|
||||
attributes: attributes,
|
||||
// 'gable'인 경우, 의도적으로 내부선을 생성하지 않아 빈 공간을 만듭니다.
|
||||
});
|
||||
|
||||
canvas.add(innerLine);
|
||||
innerLine.bringToFront();
|
||||
innerLines.push(innerLine);
|
||||
});
|
||||
// --- 3. 연결이 끊어진 선(케라바로 인해 생성됨)을 찾아 연장 ---
|
||||
const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, baseLines);
|
||||
disconnectedLines.forEach(dLine => {
|
||||
const { index, extendedLine, p1Connected, p2Connected } = dLine;
|
||||
const newPoint = extendedLine?.point;
|
||||
if (!newPoint) return;
|
||||
|
||||
canvas.renderAll();
|
||||
return innerLines;
|
||||
if (!p1Connected) { // p1 연장
|
||||
skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y };
|
||||
} else if (!p2Connected) { //p2 연장
|
||||
skeletonLines[index].p2 = { ...skeletonLines[index].p2, x: newPoint.x, y: newPoint.y };
|
||||
}
|
||||
});
|
||||
|
||||
// --- 4. 최종 결과물을 지붕 객체에 저장하고 캔버스에 그리기 ---
|
||||
roof.skeletonLines = skeletonLines;
|
||||
roof.skeleton = rebuildSkeletonFromLines(skeletonLines, baseLines); // 데이터 구조 일관성 유지
|
||||
console.log("Skeleton processing complete. Storing final state.", roof.skeleton);
|
||||
|
||||
const innerLines = [];
|
||||
if (roof.skeletonLines) {
|
||||
roof.skeletonLines.forEach(line => {
|
||||
const { p1, p2, attributes, lineStyle } = line;
|
||||
if (!p1 || !p2 || !attributes || !lineStyle) {
|
||||
console.warn("Skipping incomplete skeleton line:", line);
|
||||
return;
|
||||
}
|
||||
const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
|
||||
parentId: roof.id,
|
||||
fontSize: roof.fontSize,
|
||||
stroke: lineStyle.color,
|
||||
strokeWidth: lineStyle.width,
|
||||
name: attributes.type,
|
||||
textMode: textMode,
|
||||
attributes: attributes,
|
||||
});
|
||||
canvas.add(innerLine);
|
||||
innerLine.bringToFront();
|
||||
innerLines.push(innerLine);
|
||||
});
|
||||
}
|
||||
roof.innerLines = innerLines;
|
||||
|
||||
} catch (e) {
|
||||
console.error('Skeleton processing failed:', e);
|
||||
// 에러 발생 시 상태를 초기화하여 다음 실행에 영향 없도록 함
|
||||
roof.skeleton = null;
|
||||
roof.skeletonLines = [];
|
||||
} finally {
|
||||
canvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,44 +121,6 @@ function processEavesEdge(edgeResult, skeletonLines, processedInnerEdges) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GABLE(케라바) Edge를 처리하여 스켈레톤 선을 정리하고 연장합니다.
|
||||
* @param {object} edgeResult - 스켈레톤 Edge 데이터
|
||||
* @param {Array<QLine>} baseLines - 전체 외벽선 배열
|
||||
* @param {Array} skeletonLines - 전체 스켈레톤 라인 배열
|
||||
*/
|
||||
function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine) {
|
||||
const edgePoints = [{ x:selectBaseLine.startPoint.x, y:selectBaseLine.startPoint.y}, {x:selectBaseLine.endPoint.x, y:selectBaseLine.endPoint.y}]//edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y }));
|
||||
const polygons = createPolygonsFromSkeletonLines(skeletonLines, selectBaseLine);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다.
|
||||
const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, baseLines);
|
||||
|
||||
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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
/**
|
||||
@ -243,24 +173,6 @@ function addRawLine(skeletonLines, processedInnerEdges, p1, p2, lineType, color,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 폴리곤 좌표를 스켈레톤 생성에 적합하게 전처리합니다 (중복 제거, 시계 방향 정렬).
|
||||
* @param {Array<object>} initialPoints - 초기 폴리곤 좌표 배열
|
||||
* @returns {Array<Array<number>>} 전처리된 좌표 배열 (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} 동일 여부
|
||||
@ -341,7 +253,8 @@ function extendFromP2TowardP1(p1, p2, baseLines, skeletonLines, excludeIndex) {
|
||||
let closestHit = null;
|
||||
|
||||
const checkHit = (hit) => {
|
||||
if (hit && hit.t > len - 0.1) { // 원래 선분의 끝점(p1) 너머에서 교차하는지 확인
|
||||
// 교차점이 원래 선분의 길이(len)보다 멀리 있어야 유효한 연장으로 간주
|
||||
if (hit && hit.t > len - 0.1) {
|
||||
if (!closestHit || hit.t < closestHit.t) {
|
||||
closestHit = hit;
|
||||
}
|
||||
@ -376,7 +289,6 @@ 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 => {
|
||||
@ -413,7 +325,6 @@ export const findDisconnectedSkeletonLines = (skeletonLines, baseLines) => {
|
||||
if (!p1Connected) {
|
||||
extendedLine = extendFromP2TowardP1(line.p1, line.p2, baseLines, skeletonLines, index);
|
||||
if (!extendedLine) {
|
||||
let closestBaseLine = null;
|
||||
let minDistance = Infinity;
|
||||
let projection = null;
|
||||
baseLines.forEach(base => {
|
||||
@ -421,7 +332,6 @@ export const findDisconnectedSkeletonLines = (skeletonLines, baseLines) => {
|
||||
const d = Math.sqrt(Math.pow(line.p1.x - p.x, 2) + Math.pow(line.p1.y - p.y, 2));
|
||||
if (d < minDistance) {
|
||||
minDistance = d;
|
||||
closestBaseLine = base;
|
||||
projection = p;
|
||||
}
|
||||
});
|
||||
@ -430,7 +340,6 @@ export const findDisconnectedSkeletonLines = (skeletonLines, baseLines) => {
|
||||
} else if (!p2Connected) {
|
||||
extendedLine = extendFromP2TowardP1(line.p2, line.p1, baseLines, skeletonLines, index);
|
||||
if (!extendedLine) {
|
||||
let closestBaseLine = null;
|
||||
let minDistance = Infinity;
|
||||
let projection = null;
|
||||
baseLines.forEach(base => {
|
||||
@ -438,7 +347,6 @@ export const findDisconnectedSkeletonLines = (skeletonLines, baseLines) => {
|
||||
const d = Math.sqrt(Math.pow(line.p2.x - p.x, 2) + Math.pow(line.p2.y - p.y, 2));
|
||||
if (d < minDistance) {
|
||||
minDistance = d;
|
||||
closestBaseLine = base;
|
||||
projection = p;
|
||||
}
|
||||
});
|
||||
@ -452,148 +360,206 @@ export const findDisconnectedSkeletonLines = (skeletonLines, baseLines) => {
|
||||
return { disconnectedLines };
|
||||
};
|
||||
|
||||
|
||||
// --- Skeleton Rebuilding Functions ---
|
||||
|
||||
/**
|
||||
* skeletonLines와 selectBaseLine을 이용하여 다각형이 되는 좌표를 구합니다.
|
||||
* selectBaseLine의 좌표는 제외합니다.
|
||||
* @param {Array} skeletonLines - 스켈레톤 라인 배열
|
||||
* @param {Object} selectBaseLine - 선택된 베이스 라인 (p1, p2 속성을 가진 객체)
|
||||
* @returns {Array<Array<Object>>} 다각형 좌표 배열의 배열
|
||||
* 두 점이 거의 같은 위치에 있는지 확인합니다.
|
||||
* @param {object} p1 - 점1 {x, y}
|
||||
* @param {object} p2 - 점2 {x, y}
|
||||
* @param {number} [epsilon=0.1] - 허용 오차
|
||||
* @returns {boolean} 동일한지 여부
|
||||
*/
|
||||
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
|
||||
);
|
||||
});
|
||||
|
||||
const pointsEqual = (p1, p2, epsilon = 0.1) => {
|
||||
return Math.abs(p1.x - p2.x) < epsilon && Math.abs(p1.y - p2.y) < epsilon;
|
||||
};
|
||||
|
||||
/**
|
||||
* 스켈레톤 라인들 간의 모든 교차점을 찾습니다.
|
||||
* @param {Array} skeletonLines - 스켈레톤 라인 배열 (각 요소는 {p1: {x, y}, p2: {x, y}} 형태)
|
||||
* @returns {Array<Object>} 교차점 배열
|
||||
* 점 객체를 고유한 문자열 키로 변환합니다. 정밀도 문제를 피하기 위해 소수점 자리를 고정합니다.
|
||||
* @param {object} p - 점 {x, y}
|
||||
* @returns {string} - 고유 키 (e.g., "123.456,789.012")
|
||||
*/
|
||||
const findAllIntersections = (skeletonLines) => {
|
||||
const intersections = [];
|
||||
const processedPairs = new Set();
|
||||
const pointToKey = (p) => {
|
||||
return `${p.x.toFixed(3)},${p.y.toFixed(3)}`;
|
||||
};
|
||||
|
||||
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);
|
||||
/**
|
||||
* 문자열 키를 점 객체로 변환합니다.
|
||||
* @param {string} key - 고유 키
|
||||
* @returns {object} - 점 {x, y}
|
||||
*/
|
||||
const keyToPoint = (key) => {
|
||||
const [x, y] = key.split(',');
|
||||
return { x: parseFloat(x), y: parseFloat(y) };
|
||||
};
|
||||
|
||||
const line1 = skeletonLines[i];
|
||||
const line2 = skeletonLines[j];
|
||||
/**
|
||||
* 모든 라인 세그먼트로부터 그래프를 구축합니다.
|
||||
* 각 정점(포인트)에 대해 연결된 이웃 정점 목록을 각도순으로 정렬하여 저장합니다.
|
||||
* 이는 면(face)을 시계 반대 방향으로 순회하는 데 필수적입니다.
|
||||
* @param {Array<object>} lines - `{p1, p2}` 형태의 라인 배열
|
||||
* @returns {Map<string, Array<{point: object, angle: number}>>} - 그래프 데이터 구조
|
||||
*/
|
||||
const buildAngularSortedGraph = (lines) => {
|
||||
const graph = new Map();
|
||||
|
||||
// 두 라인이 교차하는지 확인
|
||||
const intersection = getLineIntersection(
|
||||
line1.p1, line1.p2,
|
||||
line2.p1, line2.p2
|
||||
);
|
||||
// 그래프에 양방향 간선을 추가하는 헬퍼 함수
|
||||
const addEdge = (p1, p2) => {
|
||||
const key1 = pointToKey(p1);
|
||||
const key2 = pointToKey(p2);
|
||||
|
||||
if (intersection) {
|
||||
// 교차점이 실제로 두 선분 위에 있는지 확인
|
||||
if (isPointOnSegment(intersection, line1.p1, line1.p2) &&
|
||||
isPointOnSegment(intersection, line2.p1, line2.p2)) {
|
||||
intersections.push(intersection);
|
||||
if (!graph.has(key1)) graph.set(key1, []);
|
||||
if (!graph.has(key2)) graph.set(key2, []);
|
||||
|
||||
const angle1 = Math.atan2(p2.y - p1.y, p2.x - p1.x);
|
||||
const angle2 = Math.atan2(p1.y - p2.y, p1.x - p2.x);
|
||||
|
||||
graph.get(key1).push({ point: p2, angle: angle1 });
|
||||
graph.get(key2).push({ point: p1, angle: angle2 });
|
||||
};
|
||||
|
||||
lines.forEach(line => addEdge(line.p1, line.p2));
|
||||
|
||||
// 각 정점의 이웃 리스트를 각도 기준으로 정렬
|
||||
for (const neighbors of graph.values()) {
|
||||
neighbors.sort((a, b) => a.angle - b.angle);
|
||||
}
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
/**
|
||||
* 각도순으로 정렬된 그래프에서 모든 면(폴리곤)을 찾습니다.
|
||||
* 평면 그래프의 모든 간선을 순회하며 아직 방문하지 않은 간선에서 출발하여
|
||||
* 하나의 면을 구성하는 사이클을 찾습니다.
|
||||
* @param {Map} graph - `buildAngularSortedGraph`로 생성된 그래프
|
||||
* @returns {Array<Array<object>>} - 폴리곤(점들의 배열)들의 배열
|
||||
*/
|
||||
const findFaces = (graph) => {
|
||||
const polygons = [];
|
||||
const visitedHalfEdges = new Set(); // "p1_key->p2_key" 형식으로 방문한 반-간선 저장
|
||||
|
||||
for (const [p1Key, neighbors] of graph.entries()) {
|
||||
for (const neighbor of neighbors) {
|
||||
const p2Key = pointToKey(neighbor.point);
|
||||
const halfEdge = `${p1Key}->${p2Key}`;
|
||||
|
||||
if (visitedHalfEdges.has(halfEdge)) {
|
||||
continue; // 이미 다른 면을 통해 방문한 간선
|
||||
}
|
||||
|
||||
// 새로운 면 탐색 시작
|
||||
const newPolygon = [];
|
||||
let currentHalfEdge = halfEdge;
|
||||
|
||||
while (!visitedHalfEdges.has(currentHalfEdge)) {
|
||||
if (visitedHalfEdges.size > graph.size * 2) { // 무한 루프 방지
|
||||
console.error("Infinite loop detected in face finding.");
|
||||
return [];
|
||||
}
|
||||
visitedHalfEdges.add(currentHalfEdge);
|
||||
|
||||
const [startKey, endKey] = currentHalfEdge.split('->');
|
||||
newPolygon.push(keyToPoint(startKey));
|
||||
|
||||
// 현재 간선의 끝점에서, 들어온 간선의 다음(CCW) 간선을 찾아 다음 경로로 설정
|
||||
const endNodeNeighbors = graph.get(endKey);
|
||||
const incomingEdgeIndex = endNodeNeighbors.findIndex(n => pointToKey(n.point) === startKey);
|
||||
|
||||
if (incomingEdgeIndex === -1) {
|
||||
console.error("Graph is inconsistent.");
|
||||
break;
|
||||
}
|
||||
|
||||
const nextNeighbor = endNodeNeighbors[(incomingEdgeIndex + 1) % endNodeNeighbors.length];
|
||||
currentHalfEdge = `${endKey}->${pointToKey(nextNeighbor.point)}`;
|
||||
}
|
||||
|
||||
if (newPolygon.length > 2) {
|
||||
// 중복 폴리곤 방지
|
||||
const polygonKey = newPolygon.map(p => pointToKey(p)).sort().join('|');
|
||||
if (!polygons.some(p => p.key === polygonKey)) {
|
||||
polygons.push({ key: polygonKey, points: newPolygon });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
return polygons.map(p => p.points);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 스켈레톤 라인들과 교차점들을 모아서 모든 포인트를 수집합니다.
|
||||
* @param {Array} skeletonLines - 스켈레톤 라인 배열
|
||||
* @param {Array} intersections - 교차점 배열
|
||||
* @returns {Array<Object>} 모든 포인트 배열
|
||||
* 후처리된 스켈레톤 라인과 외벽선을 기반으로 스켈레톤 유사 구조를 재생성합니다.
|
||||
* @param {Array<object>} skeletonLines - 내부 스켈레톤 라인 배열. {p1, p2} 또는 QLine({x1, y1, x2, y2}) 형식을 지원합니다.
|
||||
* @param {Array<object>} baseLines - 외벽선 QLine 객체 배열. (e.g., fabric.Line)
|
||||
* x1, y1, x2, y2 속성을 가져야 합니다.
|
||||
* @returns {object|null} - 원본 스켈레톤과 유사한 구조의 객체 { Edges: [...] }. 실패 시 null.
|
||||
*/
|
||||
const collectAllPoints = (skeletonLines, intersections) => {
|
||||
const allPoints = new Map();
|
||||
const pointKey = (point) => `${point.x.toFixed(3)},${point.y.toFixed(3)}`;
|
||||
export const rebuildSkeletonFromLines = (skeletonLines, baseLines) => {
|
||||
if (!skeletonLines || !baseLines) return null;
|
||||
|
||||
// 스켈레톤 라인의 엔드포인트들 추가
|
||||
skeletonLines.forEach(line => {
|
||||
const key1 = pointKey(line.p1);
|
||||
const key2 = pointKey(line.p2);
|
||||
|
||||
if (!allPoints.has(key1)) {
|
||||
allPoints.set(key1, { ...line.p1 });
|
||||
// 1. 모든 선분(내부선 + 외벽선)을 동일한 형식({p1, p2})으로 변환하여 결합합니다.
|
||||
// 입력되는 skeletonLines의 타입이 QLine({x1, y1, x2, y2}) 형식일 수 있으므로 두 경우 모두 처리합니다.
|
||||
const allLines = skeletonLines.map(line => {
|
||||
// { p1, p2 } 형태의 raw 객체 처리
|
||||
if (line.p1 && line.p2) {
|
||||
return { p1: line.p1, p2: line.p2 };
|
||||
}
|
||||
if (!allPoints.has(key2)) {
|
||||
allPoints.set(key2, { ...line.p2 });
|
||||
// QLine 또는 fabric.Line과 같이 x1, y1, x2, y2 속성을 가진 객체 처리
|
||||
if (typeof line.x1 === 'number' && typeof line.y1 === 'number' &&
|
||||
typeof line.x2 === 'number' && typeof line.y2 === 'number') {
|
||||
return { p1: { x: line.x1, y: line.y1 }, p2: { x: line.x2, y: line.y2 } };
|
||||
}
|
||||
console.warn('Unsupported line format in skeletonLines:', line);
|
||||
return null;
|
||||
}).filter(Boolean); // 유효하지 않은 형식은 걸러냅니다.
|
||||
|
||||
baseLines.forEach(line => {
|
||||
allLines.push({
|
||||
p1: { x: line.x1, y: line.y1 },
|
||||
p2: { x: line.x2, y: line.y2 }
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 그래프를 구축
|
||||
const graph = buildAngularSortedGraph(allLines);
|
||||
if(graph.size === 0) return { Edges: [] };
|
||||
|
||||
// 3. 그래프에서 모든 면(폴리곤)을 찾음
|
||||
const polygons = findFaces(graph);
|
||||
|
||||
// 4. 각 외벽선에 해당하는 폴리곤을 찾아 스켈레톤 Edge 구조를 만듦
|
||||
const rebuiltEdges = [];
|
||||
baseLines.forEach(baseLine => {
|
||||
const p1 = { x: baseLine.x1, y: baseLine.y1 };
|
||||
const p2 = { x: baseLine.x2, y: baseLine.y2 };
|
||||
|
||||
// 이 baseLine을 변으로 포함하는 폴리곤을 찾음
|
||||
const associatedPolygon = polygons.find(polygon => {
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const polyP1 = polygon[i];
|
||||
const polyP2 = polygon[(i + 1) % polygon.length];
|
||||
if ((pointsEqual(p1, polyP1) && pointsEqual(p2, polyP2)) ||
|
||||
(pointsEqual(p1, polyP2) && pointsEqual(p2, polyP1))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (associatedPolygon) {
|
||||
rebuiltEdges.push({
|
||||
Edge: {
|
||||
Begin: { X: p1.x, Y: p1.y },
|
||||
End: { X: p2.x, Y: p2.y }
|
||||
},
|
||||
Polygon: associatedPolygon.map(p => ({ X: p.x, Y: p.y })),
|
||||
// 원본 skeleton 객체의 다른 속성들(e.g., roof_type)은
|
||||
// 라인 정보만으로는 재생성할 수 없으므로 포함하지 않습니다.
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 교차점들 추가
|
||||
intersections.forEach(intersection => {
|
||||
const key = pointKey(intersection);
|
||||
if (!allPoints.has(key)) {
|
||||
allPoints.set(key, { ...intersection });
|
||||
}
|
||||
});
|
||||
return { Edges: rebuiltEdges };
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user