1401 lines
49 KiB
JavaScript
1401 lines
49 KiB
JavaScript
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 { line } from 'framer-motion/m'
|
|
|
|
/**
|
|
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다.
|
|
* @param {string} roofId - 대상 지붕 객체의 ID
|
|
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
|
|
* @param {string} textMode - 텍스트 표시 모드
|
|
* @param pitch
|
|
*/
|
|
|
|
|
|
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
|
|
|
|
// 2. 스켈레톤 생성 및 그리기
|
|
skeletonBuilder(roofId, canvas, textMode)
|
|
}
|
|
|
|
|
|
const movingRidgeFromSkeleton = (roofId, canvas) => {
|
|
|
|
let roof = canvas?.getObjects().find((object) => object.id === roofId)
|
|
let moveDirection = roof.moveDirect;
|
|
let moveFlowLine = roof.moveFlowLine??0;
|
|
const selectLine = roof.moveSelectLine;
|
|
|
|
const startPoint = selectLine.startPoint
|
|
const endPoint = selectLine.endPoint
|
|
const oldPoints = canvas?.movePoints?.points ?? roof.points
|
|
const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints);
|
|
|
|
if (oppositeLine) {
|
|
console.log('Opposite line found:', oppositeLine);
|
|
} else {
|
|
console.log('No opposite line found');
|
|
}
|
|
|
|
return oldPoints.map((point) => {
|
|
const newPoint = { ...point };
|
|
const absMove = Big(moveFlowLine).abs().times(2).div(10);
|
|
//console.log('absMove:', absMove);
|
|
|
|
const skeletonLines = canvas.skeletonLines;
|
|
|
|
console.log('skeleton line:', canvas.skeletonLines);
|
|
const changeSkeletonLine = (canvas, oldPoint, newPoint, str) => {
|
|
for (const line of canvas.skeletonLines) {
|
|
if (str === 'start' && isSamePoint(line.startPoint, oldPoint)) {
|
|
// Fabric.js 객체의 set 메서드로 속성 업데이트
|
|
line.set({
|
|
x1: newPoint.x,
|
|
y1: newPoint.y,
|
|
x2: line.x2 || line.endPoint?.x,
|
|
y2: line.y2 || line.endPoint?.y
|
|
});
|
|
line.startPoint = newPoint; // 참조 업데이트
|
|
}
|
|
else if (str === 'end' && isSamePoint(line.endPoint, oldPoint)) {
|
|
line.set({
|
|
x1: line.x1 || line.startPoint?.x,
|
|
y1: line.y1 || line.startPoint?.y,
|
|
x2: newPoint.x,
|
|
y2: newPoint.y
|
|
});
|
|
line.endPoint = newPoint; // 참조 업데이트
|
|
}
|
|
}
|
|
canvas.requestRenderAll();
|
|
console.log('skeleton line:', canvas.skeletonLines);
|
|
}
|
|
|
|
|
|
|
|
if(moveFlowLine > 0) {
|
|
if(moveDirection === 'down'){
|
|
moveDirection = 'up';
|
|
}else if(moveDirection === 'left'){
|
|
moveDirection = 'right';
|
|
}
|
|
}
|
|
|
|
console.log('skeletonBuilder moveDirection:', moveDirection);
|
|
|
|
switch (moveDirection) {
|
|
case 'left':
|
|
// Move left: decrease X
|
|
for (const line of oppositeLine) {
|
|
if (line.position === 'left') {
|
|
if (isSamePoint(newPoint, line.start)) {
|
|
newPoint.x = Big(line.start.x).minus(absMove).toNumber();
|
|
//changeSkeletonLine(canvas, line.start, newPoint, 'start')
|
|
} else if (isSamePoint(newPoint, line.end)) {
|
|
newPoint.x = Big(line.end.x).minus(absMove).toNumber();
|
|
//changeSkeletonLine(canvas, line.end, newPoint, 'end')
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 'right':
|
|
for (const line of oppositeLine) {
|
|
if (line.position === 'right') {
|
|
if (isSamePoint(newPoint, line.start)) {
|
|
newPoint.x = Big(line.start.x).plus(absMove).toNumber();
|
|
//changeSkeletonLine(canvas, line.start, newPoint, 'start')
|
|
} else if (isSamePoint(newPoint, line.end)) {
|
|
newPoint.x = Big(line.end.x).plus(absMove).toNumber();
|
|
//changeSkeletonLine(canvas, line.end, newPoint, 'end')
|
|
}
|
|
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();
|
|
//changeSkeletonLine(canvas, line.start, newPoint, 'start')
|
|
} else if (isSamePoint(newPoint, line.end)) {
|
|
newPoint.y = Big(line.end.y).minus(absMove).toNumber();
|
|
//changeSkeletonLine(canvas, line.end, newPoint, 'end')
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 'down':
|
|
// Move down: increase Y (toward bottom of screen)
|
|
for (const line of oppositeLine) {
|
|
if (line.position === 'bottom') {
|
|
if (isSamePoint(newPoint, line.start)) {
|
|
newPoint.y = Big(line.start.y).plus(absMove).toNumber();
|
|
//changeSkeletonLine(canvas, line.start, newPoint, 'start')
|
|
|
|
} else if (isSamePoint(newPoint, line.end)) {
|
|
newPoint.y = Big(line.end.y).plus(absMove).toNumber();
|
|
//changeSkeletonLine(canvas, line.end, newPoint, 'end')
|
|
}
|
|
break
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return newPoint;
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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 wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId)
|
|
|
|
// const hasNonParallelLines = roof.lines.filter((line) => Big(line.x1).minus(Big(line.x2)).gt(1) && Big(line.y1).minus(Big(line.y2)).gt(1))
|
|
// if (hasNonParallelLines.length > 0) {
|
|
// return
|
|
// }
|
|
|
|
const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE]
|
|
const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD]
|
|
|
|
/** 외벽선 */
|
|
const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0)
|
|
|
|
//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) {
|
|
points = movingRidgeFromSkeleton(roofId, canvas)
|
|
|
|
const movePoints = {
|
|
points: points,
|
|
roofId: roofId,
|
|
}
|
|
canvas.set("movePoints", movePoints)
|
|
|
|
}
|
|
//처마
|
|
if(moveUpDown !== 0) {
|
|
|
|
}
|
|
|
|
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)
|
|
canvas.set("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.set("skeleton", cleanSkeleton);
|
|
|
|
canvas.renderAll()
|
|
} 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<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
|
|
*/
|
|
const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
|
if (!skeleton?.Edges) return []
|
|
let roof = canvas?.getObjects().find((object) => object.id === roofId)
|
|
const skeletonLines = []
|
|
const processedInnerEdges = new Set()
|
|
|
|
// 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다.
|
|
|
|
skeleton.Edges.forEach((edgeResult, index) => {
|
|
// 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){
|
|
//
|
|
// for (const line of canvas.skeletonLines) {
|
|
// if (line.lineName === 'hip' && line.attributes.hipIndex === index)
|
|
// {
|
|
// outerLine = line;
|
|
// break; // Found the matching line, exit the loop
|
|
// }
|
|
// }
|
|
//
|
|
// }
|
|
// const pitch = outerLine.attributes?.pitch??0
|
|
// console.log("pitch", pitch)
|
|
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, baseLines);
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
// 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다.
|
|
const innerLines = [];
|
|
const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set
|
|
|
|
skeletonLines.forEach(line => {
|
|
const { p1, p2, attributes, lineStyle } = line;
|
|
|
|
// 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교)
|
|
const lineKey = [
|
|
[p1.x, p1.y].sort().join(','),
|
|
[p2.x, p2.y].sort().join(',')
|
|
].sort().join('|');
|
|
|
|
// 이미 추가된 라인인지 확인
|
|
if (existingLines.has(lineKey)) {
|
|
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: (line.attributes.isOuterEdge)?'eaves': attributes.type,
|
|
attributes: attributes,
|
|
isBaseLine: line.attributes.isOuterEdge,
|
|
lineName: (line.attributes.isOuterEdge)?'outerLine': attributes.type,
|
|
selectable:(!line.attributes.isOuterEdge),
|
|
roofId: roofId
|
|
});
|
|
|
|
//skeleton 라인에서 처마선은 삭제
|
|
if(innerLine.lineName !== 'outerLine'){
|
|
canvas.add(innerLine);
|
|
innerLine.bringToFront();
|
|
existingLines.add(lineKey); // 추가된 라인을 추적
|
|
}
|
|
innerLines.push(innerLine)
|
|
canvas.renderAll();
|
|
});
|
|
|
|
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
|
|
|
|
|
|
let eavesLines = []
|
|
for (let i = 0; i < polygonPoints.length; i++) {
|
|
const p1 = polygonPoints[i];
|
|
const p2 = polygonPoints[(i + 1) % polygonPoints.length];
|
|
|
|
// 외벽선에 해당하는 스켈레톤 선은 제외하고 내부선만 추가
|
|
// if (!isOuterEdge(p1, p2, [edgeResult.Edge])) {
|
|
//외벽선 밖으로 나간 선을 정리한다(roof.line의 교점까지 정리한다)
|
|
// 지붕 경계선과 교차 확인 및 클리핑
|
|
const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines);
|
|
console.log('clipped line', clippedLine.p1, clippedLine.p2);
|
|
const isOuterLine = isOuterEdge(p1, p2, [edgeResult.Edge])
|
|
addRawLine(roof.id, skeletonLines, p1, p2, 'ridge', '#FF0000', 3, 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<QLine>} 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<object>} 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 {Set} processedInnerEdges - 처리된 Edge 키 Set
|
|
* @param {object} p1 - 시작점
|
|
* @param {object} p2 - 끝점
|
|
* @param {string} lineType - 라인 타입
|
|
* @param {string} color - 색상
|
|
* @param {number} width - 두께
|
|
* @param currentDegree
|
|
*/
|
|
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<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} 동일 여부
|
|
*/
|
|
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<QLine>} 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<QLine>} 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<Array<Object>>} 다각형 좌표 배열의 배열
|
|
*/
|
|
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<Object>} 교차점 배열
|
|
*/
|
|
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<Object>} 모든 포인트 배열
|
|
*/
|
|
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
|
|
};
|
|
|
|
|
|
/**
|
|
* Finds lines in the roof that match certain criteria based on the given points
|
|
* @param {Array} lines - The roof lines to search through
|
|
* @param {Object} startPoint - The start point of the reference line
|
|
* @param {Object} endPoint - The end point of the reference line
|
|
* @param {Array} oldPoints - The old points to compare against
|
|
* @returns {Array} Array of matching line objects with their properties
|
|
*/
|
|
function findMatchingRoofLines(lines, startPoint, endPoint, oldPoints) {
|
|
const result = [];
|
|
|
|
// If no lines provided, return empty array
|
|
if (!lines || !lines.length) return result;
|
|
|
|
// Process each line in the roof
|
|
for (const line of lines) {
|
|
// Get the start and end points of the current line
|
|
const p1 = { x: line.x1, y: line.y1 };
|
|
const p2 = { x: line.x2, y: line.y2 };
|
|
|
|
// Check if both points exist in the oldPoints array
|
|
const p1Exists = oldPoints.some(p =>
|
|
Math.abs(p.x - p1.x) < 0.0001 && Math.abs(p.y - p1.y) < 0.0001
|
|
);
|
|
|
|
const p2Exists = oldPoints.some(p =>
|
|
Math.abs(p.x - p2.x) < 0.0001 && Math.abs(p.y - p2.y) < 0.0001
|
|
);
|
|
|
|
// If both points exist in oldPoints, add to results
|
|
if (p1Exists && p2Exists) {
|
|
// Calculate line position relative to the reference line
|
|
const position = getLinePosition(
|
|
{ start: p1, end: p2 },
|
|
{ start: startPoint, end: endPoint }
|
|
);
|
|
|
|
result.push({
|
|
start: p1,
|
|
end: p2,
|
|
position: position,
|
|
line: line
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
});
|
|
}
|
|
|
|
// // 현재 선분의 기울기 계산
|
|
// const currentSlope = calculateSlope(p1, p2);
|
|
//
|
|
// // 기울기가 같은지 확인 (평행한 선분)
|
|
// if (areLinesParallel(referenceSlope, currentSlope)) {
|
|
// // 동일한 선분이 아닌지 확인
|
|
// if (!areSameLine(p1, p2, startPoint, endPoint)) {
|
|
// const position = getLinePosition(
|
|
// { start: p1, end: p2 },
|
|
// { start: startPoint, end: endPoint }
|
|
// );
|
|
//
|
|
// const lineMid = {
|
|
// x: (p1.x + p2.x) / 2,
|
|
// y: (p1.y + p2.y) / 2
|
|
// };
|
|
//
|
|
// const baseMid = {
|
|
// x: (startPoint.x + endPoint.x) / 2,
|
|
// y: (startPoint.y + endPoint.y) / 2
|
|
// };
|
|
// const distance = Math.sqrt(
|
|
// Math.pow(lineMid.x - baseMid.x, 2) +
|
|
// Math.pow(lineMid.y - baseMid.y, 2)
|
|
// );
|
|
//
|
|
// const existingIndex = result.findIndex(line => line.position === position);
|
|
//
|
|
// if (existingIndex === -1) {
|
|
// // If no line with this position exists, add it
|
|
// result.push({
|
|
// start: p1,
|
|
// end: p2,
|
|
// position: position,
|
|
// polygon: polygon,
|
|
// distance: distance
|
|
// });
|
|
// } else if (distance > result[existingIndex].distance) {
|
|
// // If a line with this position exists but is closer, replace it
|
|
// result[existingIndex] = {
|
|
// start: p1,
|
|
// end: p2,
|
|
// position: position,
|
|
// polygon: polygon,
|
|
// distance: distance
|
|
// };
|
|
// }
|
|
// }
|
|
// }
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// Y축 차이가 더 크면 위/아래로 판단
|
|
// Y축 차이가 더 크면 위/아래로 판단
|
|
if (Math.abs(lineMidY - refMidY) > Math.abs(lineMidX - refMidX)) {
|
|
return lineMidY > refMidY ? 'bottom' : 'top';
|
|
}
|
|
// X축 차이가 더 크면 왼쪽/오른쪽으로 판단
|
|
else {
|
|
return lineMidX > refMidX ? '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);
|
|
}
|
|
|
|
// 두 직선이 평행한지 확인
|
|
// function areLinesParallel(slope1, slope2) {
|
|
// // 두 직선 모두 수직선인 경우
|
|
// if (slope1 === Infinity && slope2 === Infinity) return true;
|
|
//
|
|
// // 기울기의 차이가 매우 작으면 평행한 것으로 간주
|
|
// const epsilon = 0.0001;
|
|
// return Math.abs(slope1 - slope2) < epsilon;
|
|
// }
|
|
|
|
// 두 선분이 동일한지 확인
|
|
// function areSameLine(p1, p2, p3, p4) {
|
|
// return (
|
|
// (isSamePoint(p1, p3) && isSamePoint(p2, p4)) ||
|
|
// (isSamePoint(p1, p4) && isSamePoint(p2, p3))
|
|
// );
|
|
// }
|
|
/**
|
|
* 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와 교차하는 선분(p1, p2)을 찾아 교차점에서 자릅니다.
|
|
* @param {Object} p1 - 선분의 시작점 {x, y}
|
|
* @param {Object} p2 - 선분의 끝점 {x, y}
|
|
* @param {Array} roofLines - 지붕 경계선 배열 (QLine 객체의 배열)
|
|
* @returns {Object} {p1: {x, y}, p2: {x, y}} - 교차점에서 자른 선분 또는 원래 선분
|
|
*/
|
|
function clipLineToRoofBoundary(p1, p2, roofLines) {
|
|
if (!roofLines || !roofLines.length) return { p1, p2 };
|
|
|
|
let closestIntersection = null;
|
|
let minDistSq = Infinity;
|
|
const originalP1 = { ...p1 };
|
|
const originalP2 = { ...p2 };
|
|
|
|
// 모든 지붕 경계선과의 교차점을 찾음
|
|
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) {
|
|
// 교차점과 p1 사이의 거리 계산
|
|
const dx = intersection.x - p1.x;
|
|
const dy = intersection.y - p1.y;
|
|
const distSq = dx * dx + dy * dy;
|
|
|
|
// p1에 가장 가까운 교차점 찾기
|
|
if (distSq < minDistSq) {
|
|
minDistSq = distSq;
|
|
closestIntersection = intersection;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 교차점이 있으면 p2를 가장 가까운 교차점으로 업데이트
|
|
if (closestIntersection) {
|
|
return {
|
|
p1: originalP1,
|
|
p2: closestIntersection
|
|
};
|
|
}
|
|
|
|
// 교차점이 없으면 원래 선분 반환
|
|
return { p1: originalP1, p2: originalP2 };
|
|
}
|
|
|
|
// 기존 getLineIntersection 함수를 사용하거나, 없으면 아래 구현 사용
|