Merge pull request 'skeleton-utils' (#342) from dev into dev-deploy
Reviewed-on: #342
This commit is contained in:
commit
116612ec2b
229
src/util/skeleton-utils.js
Normal file
229
src/util/skeleton-utils.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* @file skeleton-utils.js
|
||||||
|
* @description 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 Fabric.js 캔버스에 그리는 유틸리티 함수들을 포함합니다.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SkeletonBuilder from '@/lib/skeletons/SkeletonBuilder';
|
||||||
|
import { fabric } from 'fabric';
|
||||||
|
import { LINE_TYPE } from '@/common/common';
|
||||||
|
import { QLine } from '@/components/fabric/QLine';
|
||||||
|
import { calcLinePlaneSize } from '@/util/qpolygon-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지붕 폴리곤의 좌표를 스켈레톤 생성에 적합한 형태로 전처리합니다.
|
||||||
|
* - 연속된 중복 좌표를 제거합니다.
|
||||||
|
* - 폴리곤이 닫힌 형태가 되도록 마지막 좌표를 확인하고 필요시 제거합니다.
|
||||||
|
* - 좌표를 시계 방향으로 정렬합니다.
|
||||||
|
* @param {Array<object>} initialPoints - 초기 폴리곤 좌표 배열 (e.g., [{x: 10, y: 10}, ...])
|
||||||
|
* @returns {Array<Array<number>>} 전처리된 좌표 배열 (e.g., [[10, 10], ...])
|
||||||
|
*/
|
||||||
|
const preprocessPolygonCoordinates = (initialPoints) => {
|
||||||
|
// fabric.Point 객체를 [x, y] 배열로 변환
|
||||||
|
let coordinates = initialPoints.map(point => [point.x, point.y]);
|
||||||
|
|
||||||
|
// 연속된 중복 좌표 제거
|
||||||
|
coordinates = coordinates.filter((coord, index) => {
|
||||||
|
if (index === 0) return true;
|
||||||
|
const prev = coordinates[index - 1];
|
||||||
|
return !(coord[0] === prev[0] && coord[1] === prev[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 폴리곤의 첫 점과 마지막 점이 동일하면 마지막 점을 제거하여 닫힌 구조 보장
|
||||||
|
if (coordinates.length > 1 &&
|
||||||
|
coordinates[0][0] === coordinates[coordinates.length - 1][0] &&
|
||||||
|
coordinates[0][1] === coordinates[coordinates.length - 1][1]) {
|
||||||
|
coordinates.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkeletonBuilder 라이브러리는 시계 방향 순서의 좌표를 요구하므로 배열을 뒤집습니다.
|
||||||
|
coordinates.reverse();
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성된 스켈레톤 엣지들로부터 중복되지 않는 고유한 선분 목록을 추출합니다.
|
||||||
|
* 각 엣지는 작은 폴리곤으로 구성되며, 인접한 엣지들은 선분을 공유하므로 중복 제거가 필요합니다.
|
||||||
|
* @param {Array<object>} skeletonEdges - SkeletonBuilder로부터 반환된 엣지 배열
|
||||||
|
* @returns {Array<object>} 고유한 선분 객체의 배열 (e.g., [{x1, y1, x2, y2, edgeIndex}, ...])
|
||||||
|
*/
|
||||||
|
const extractUniqueLinesFromEdges = (skeletonEdges) => {
|
||||||
|
const uniqueLines = new Set();
|
||||||
|
const linesToDraw = [];
|
||||||
|
|
||||||
|
skeletonEdges.forEach((edge, edgeIndex) => {
|
||||||
|
// 엣지 데이터가 유효한 폴리곤인지 확인
|
||||||
|
if (!edge || !edge.Polygon || edge.Polygon.length < 2) {
|
||||||
|
console.warn(`Edge ${edgeIndex} has invalid polygon data:`, edge.Polygon);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 폴리곤의 각 변을 선분으로 변환
|
||||||
|
for (let i = 0; i < edge.Polygon.length; i++) {
|
||||||
|
const p1 = edge.Polygon[i];
|
||||||
|
const p2 = edge.Polygon[(i + 1) % edge.Polygon.length]; // 다음 점 (마지막 점은 첫 점과 연결)
|
||||||
|
|
||||||
|
// 선분의 시작점과 끝점을 일관된 순서로 정렬하여 정규화된 문자열 키 생성
|
||||||
|
// 이를 통해 동일한 선분(방향만 다른 경우 포함)을 식별하고 중복을 방지
|
||||||
|
const normalizedLineKey = p1.X < p2.X || (p1.X === p2.X && p1.Y < p2.Y)
|
||||||
|
? `${p1.X},${p1.Y}-${p2.X},${p2.Y}`
|
||||||
|
: `${p2.X},${p2.Y}-${p1.X},${p1.Y}`;
|
||||||
|
|
||||||
|
// Set에 정규화된 키가 없으면 새로운 선분으로 간주하고 추가
|
||||||
|
if (!uniqueLines.has(normalizedLineKey)) {
|
||||||
|
uniqueLines.add(normalizedLineKey);
|
||||||
|
linesToDraw.push({
|
||||||
|
x1: p1.X, y1: p1.Y,
|
||||||
|
x2: p2.X, y2: p2.Y,
|
||||||
|
edgeIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return linesToDraw;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다.
|
||||||
|
* @param {string} roofId - 대상 지붕 객체의 ID
|
||||||
|
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
|
||||||
|
* @param {string} textMode - 텍스트 표시 모드
|
||||||
|
*/
|
||||||
|
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
|
||||||
|
try {
|
||||||
|
const roof = canvas?.getObjects().find((object) => object.id === roofId);
|
||||||
|
if (!roof) {
|
||||||
|
console.error(`Roof with id "${roofId}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 기존 스켈레톤 라인 제거
|
||||||
|
const existingSkeletonLines = canvas.getObjects().filter(obj =>
|
||||||
|
obj.parentId === roofId && obj.attributes?.type === 'skeleton'
|
||||||
|
);
|
||||||
|
existingSkeletonLines.forEach(line => canvas.remove(line));
|
||||||
|
|
||||||
|
// 2. 지붕 폴리곤 좌표 전처리
|
||||||
|
const coordinates = preprocessPolygonCoordinates(roof.points);
|
||||||
|
if (coordinates.length < 3) {
|
||||||
|
console.warn("Polygon has less than 3 unique points. Cannot generate skeleton.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 스켈레톤 생성
|
||||||
|
const multiPolygon = [[coordinates]]; // GeoJSON MultiPolygon 형식
|
||||||
|
const skeleton = SkeletonBuilder.BuildFromGeoJSON(multiPolygon);
|
||||||
|
|
||||||
|
if (!skeleton || !skeleton.Edges || skeleton.Edges.length === 0) {
|
||||||
|
console.log('No valid skeleton edges found for this roof.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 스켈레톤 엣지에서 고유 선분 추출
|
||||||
|
const linesToDraw = extractUniqueLinesFromEdges(skeleton.Edges);
|
||||||
|
|
||||||
|
// 5. 캔버스에 스켈레톤 라인 렌더링
|
||||||
|
const skeletonLines = [];
|
||||||
|
const outerLines = pointsToLines(coordinates);
|
||||||
|
|
||||||
|
linesToDraw.forEach((line, index) => {
|
||||||
|
// 외곽선과 겹치는 스켈레톤 라인은 그리지 않음
|
||||||
|
const isOverlapping = outerLines.some(outerLine => linesOverlap(line, outerLine));
|
||||||
|
if (isOverlapping) {
|
||||||
|
console.log(`Skeleton line (edge ${line.edgeIndex}) is overlapping with a baseLine. It will not be drawn.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDiagonal = Math.abs(line.x1 - line.x2) > 1e-6 && Math.abs(line.y1 - line.y2) > 1e-6;
|
||||||
|
|
||||||
|
const skeletonLine = new QLine([line.x1, line.y1, line.x2, line.y2], {
|
||||||
|
parentId: roofId,
|
||||||
|
stroke: '#ff0000', // 스켈레톤은 빨간색으로 표시
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDashArray: [3, 3], // 점선으로 표시
|
||||||
|
name: isDiagonal ? LINE_TYPE.SUBLINE.HIP : LINE_TYPE.SUBLINE.RIDGE,
|
||||||
|
fontSize: roof.fontSize || 12,
|
||||||
|
textMode: textMode,
|
||||||
|
attributes: {
|
||||||
|
roofId: roofId,
|
||||||
|
type: 'skeleton', // 스켈레톤 타입 식별자
|
||||||
|
skeletonIndex: line.edgeIndex,
|
||||||
|
lineIndex: index,
|
||||||
|
planeSize: calcLinePlaneSize(line),
|
||||||
|
actualSize: calcLinePlaneSize(line), // 실제 크기는 필요시 별도 계산 로직 추가
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
skeletonLine.startPoint = { x: line.x1, y: line.y1 };
|
||||||
|
skeletonLine.endPoint = { x: line.x2, y: line.y2 };
|
||||||
|
|
||||||
|
skeletonLines.push(skeletonLine);
|
||||||
|
canvas.add(skeletonLine);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. roof 객체에 스켈레톤 라인 정보 업데이트
|
||||||
|
roof.innerLines = [...(roof.innerLines || []), ...skeletonLines];
|
||||||
|
skeletonLines.forEach(line => line.bringToFront());
|
||||||
|
|
||||||
|
canvas.renderAll();
|
||||||
|
console.log(`Successfully drew ${linesToDraw.length} unique skeleton lines from ${skeleton.Edges.length} polygons.`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('An error occurred while generating the skeleton:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 선분이 동일 선상에 있으면서 서로 겹치는지 확인합니다.
|
||||||
|
* @param {object} line1 - 첫 번째 선분 객체 { x1, y1, x2, y2 }
|
||||||
|
* @param {object} line2 - 두 번째 선분 객체 { x1, y1, x2, y2 }
|
||||||
|
* @param {number} [epsilon=1e-6] - 부동 소수점 계산 오차 허용 범위
|
||||||
|
* @returns {boolean} 동일 선상에서 겹치면 true, 아니면 false
|
||||||
|
*/
|
||||||
|
function linesOverlap(line1, line2, epsilon = 1e-6) {
|
||||||
|
// 1. 세 점의 면적 계산을 통해 동일 선상에 있는지 확인 (면적이 0에 가까우면 동일 선상)
|
||||||
|
const area1 = (line1.x2 - line1.x1) * (line2.y1 - line1.y1) - (line2.x1 - line1.x1) * (line1.y2 - line1.y1);
|
||||||
|
const area2 = (line1.x2 - line1.x1) * (line2.y2 - line1.y1) - (line2.x2 - line1.x1) * (line1.y2 - line1.y1);
|
||||||
|
|
||||||
|
if (Math.abs(area1) > epsilon || Math.abs(area2) > epsilon) {
|
||||||
|
return false; // 동일 선상에 없음
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 동일 선상에 있을 경우, 두 선분의 범위(x, y)가 겹치는지 확인
|
||||||
|
const xOverlap = Math.max(line1.x1, line1.x2) >= Math.min(line2.x1, line2.x2) &&
|
||||||
|
Math.max(line2.x1, line2.x2) >= Math.min(line1.x1, line1.x2);
|
||||||
|
|
||||||
|
const yOverlap = Math.max(line1.y1, line1.y2) >= Math.min(line2.y1, line2.y2) &&
|
||||||
|
Math.max(line2.y1, line2.y2) >= Math.min(line1.y1, line1.y2);
|
||||||
|
|
||||||
|
return xOverlap && yOverlap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 점들의 배열을 닫힌 폴리곤을 구성하는 선분 객체의 배열로 변환합니다.
|
||||||
|
* @param {Array<Array<number>>} points - [x, y] 형태의 점 좌표 배열
|
||||||
|
* @returns {Array<{x1: number, y1: number, x2: number, y2: number}>} 선분 객체 배열
|
||||||
|
*/
|
||||||
|
function pointsToLines(points) {
|
||||||
|
if (!points || points.length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
const numPoints = points.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < numPoints; i++) {
|
||||||
|
const startPoint = points[i];
|
||||||
|
const endPoint = points[(i + 1) % numPoints]; // 마지막 점은 첫 번째 점과 연결
|
||||||
|
|
||||||
|
lines.push({
|
||||||
|
x1: startPoint[0],
|
||||||
|
y1: startPoint[1],
|
||||||
|
x2: endPoint[0],
|
||||||
|
y2: endPoint[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user