This commit is contained in:
ysCha 2025-10-17 18:26:01 +09:00
parent 15270b3d7d
commit 6d20488feb
2 changed files with 673 additions and 62 deletions

View File

@ -6,6 +6,7 @@ import { calculateAngle, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON }
import * as turf from '@turf/turf'
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
import Big from 'big.js'
import { drawSkeletonRidgeRoof } from '@/util/skeleton-utils'
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
type: 'QPolygon',
@ -250,6 +251,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
obj.parentId === this.id &&
obj.name !== POLYGON_TYPE.WALL &&
obj.name !== POLYGON_TYPE.ROOF &&
obj.name !== 'lengthText' &&
obj.name !== 'outerLine' &&
obj.name !== 'baseLine',
// && obj.name !== 'outerLinePoint',
@ -334,7 +336,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) {
// 용마루 -- straight-skeleton
console.log('용마루 지붕')
drawRidgeRoof(this.id, this.canvas, textMode)
//drawRidgeRoof(this.id, this.canvas, textMode)
drawSkeletonRidgeRoof(this.id, this.canvas, textMode);
} else if (isGableRoof(types)) {
// A형, B형 박공 지붕
console.log('패턴 지붕')

View File

@ -1,39 +1,159 @@
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
import { SkeletonBuilder } from '@/lib/skeletons'
import { calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils'
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 {Array<QLine>} existingSkeletonLines - 기존에 생성된 스켈레톤 라인
* @param pitch
*/
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
const roof = canvas?.getObjects().find((object) => object.id === roofId)
if (!roof) {
console.error(`Roof with id "${roofId}" not found.`);
return;
}
//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 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;
}
// 2. 스켈레톤 생성 및 그리기
skeletonBuilder(roofId, canvas, textMode, roof)
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;
})
}
/**
@ -42,20 +162,67 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
* @param {fabric.Canvas} canvas - 캔버스 객체
* @param {string} textMode - 텍스트 모드
* @param {fabric.Object} roof - 지붕 객체
* @param {Array<QLine>} skeletonLines - 스켈레톤 라인 배열
* @param baseLines
*/
export const skeletonBuilder = (roofId, canvas, textMode, roof) => {
const geoJSONPolygon = toGeoJSON(roof.points)
const skeletonLines = []
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]])
console.log(`지붕 형태: ${skeleton.roof_type}`, skeleton.edge_analysis)
// 스켈레톤 데이터를 기반으로 내부선 생성
roof.innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode, skeletonLines)
roof.innerLines = roof.innerLines || [];
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
// 캔버스에 스켈레톤 상태 저장
if (!canvas.skeletonStates) {
@ -63,12 +230,35 @@ export const skeletonBuilder = (roofId, canvas, textMode, roof) => {
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 = []
}
}
}
@ -76,30 +266,49 @@ export const skeletonBuilder = (roofId, canvas, textMode, roof) => {
/**
* 스켈레톤 결과와 외벽선 정보를 바탕으로 내부선(용마루, 추녀) 생성합니다.
* @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) 배열
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
*/
const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMode, skeletonLines) => {
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 => {
processEavesEdge(edgeResult, skeletonLines, processedInnerEdges)
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 = baseLines.find(line =>
const gableBaseLine = roof.lines.find(line =>
line.attributes.type === 'gable' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
);
@ -127,7 +336,7 @@ const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMod
}
});
*
//2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다.
const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, baseLines);
@ -150,28 +359,49 @@ const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMod
}
*/
// 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: attributes.type,
textMode: textMode,
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
});
canvas.add(innerLine);
innerLine.bringToFront();
innerLines.push(innerLine);
//skeleton 라인에서 처마선은 삭제
if(innerLine.lineName !== 'outerLine'){
canvas.add(innerLine);
innerLine.bringToFront();
existingLines.add(lineKey); // 추가된 라인을 추적
}
innerLines.push(innerLine)
canvas.renderAll();
});
canvas.renderAll();
return innerLines;
}
@ -180,21 +410,68 @@ const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMod
* @param {object} edgeResult - 스켈레톤 Edge 데이터
* @param {Array} skeletonLines - 스켈레톤 라인 배열
* @param {Set} processedInnerEdges - 중복 처리를 방지하기 위한 Set
* @param roof
* @param pitch
*/
function processEavesEdge(edgeResult, skeletonLines, processedInnerEdges) {
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])) {
addRawLine(skeletonLines, processedInnerEdges, p1, p2, 'RIDGE', '#FF0000', 3);
}
// 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 데이터
@ -206,7 +483,7 @@ function processEavesEdge(edgeResult, skeletonLines, processedInnerEdges) {
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)
//console.log("edgePoints::::::", edgePoints)
// 1. Initialize processedLines with a deep copy of lastSkeletonLines
let processedLines = []
// 1. 케라바 면과 관련된 불필요한 스켈레톤 선을 제거합니다.
@ -221,8 +498,8 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine,
}
}
console.log("skeletonLines::::::", skeletonLines)
console.log("lastSkeletonLines", lastSkeletonLines)
//console.log("skeletonLines::::::", skeletonLines)
//console.log("lastSkeletonLines", lastSkeletonLines)
// 2. Find common lines between skeletonLines and lastSkeletonLines
skeletonLines.forEach(line => {
@ -248,9 +525,9 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine,
// return !isEdgeLine;
// });
console.log("skeletonLines::::::", skeletonLines);
console.log("lastSkeletonLines", lastSkeletonLines);
console.log("processedLines after filtering", processedLines);
//console.log("skeletonLines::::::", skeletonLines);
//console.log("lastSkeletonLines", lastSkeletonLines);
//console.log("processedLines after filtering", processedLines);
return processedLines;
@ -279,6 +556,7 @@ function isOuterEdge(p1, p2, edges) {
/**
* 스켈레톤 라인 배열에 새로운 라인을 추가합니다. (중복 방지)
* @param id
* @param {Array} skeletonLines - 스켈레톤 라인 배열
* @param {Set} processedInnerEdges - 처리된 Edge Set
* @param {object} p1 - 시작점
@ -286,27 +564,52 @@ function isOuterEdge(p1, p2, edges) {
* @param {string} lineType - 라인 타입
* @param {string} color - 색상
* @param {number} width - 두께
* @param currentDegree
*/
function addRawLine(skeletonLines, processedInnerEdges, p1, p2, lineType, color, width) {
const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|');
if (processedInnerEdges.has(edgeKey)) return;
processedInnerEdges.add(edgeKey);
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;
skeletonLines.push({
// 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);
}
/**
@ -783,6 +1086,8 @@ const isPointOnSegment = (point, segStart, segEnd) => {
return dotProduct >= 0 && dotProduct <= squaredLength;
};
// Export all necessary functions
export {
findAllIntersections,
@ -790,3 +1095,306 @@ export {
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 함수를 사용하거나, 없으면 아래 구현 사용