qcast-front/src/util/skeleton-utils.js
2025-11-27 18:47:49 +09:00

3790 lines
127 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 { findClosestLineToPoint, findOrthogonalPoint, getDegreeByChon } from '@/util/canvas-util'
import Big from 'big.js'
import { line } from 'framer-motion/m'
import { QPolygon } from '@/components/fabric/QPolygon'
import { point } from '@turf/turf'
import { add, forEach } from 'mathjs'
import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine'
import * as conole from 'mathjs'
/**
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다.
* @param {string} roofId - 대상 지붕 객체의 ID
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
* @param {string} textMode - 텍스트 표시 모드
* @param pitch
*/
const EPSILON = 0.1
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
// 2. 스켈레톤 생성 및 그리기
skeletonBuilder(roofId, canvas, textMode)
}
const movingLineFromSkeleton = (roofId, canvas) => {
let roof = canvas?.getObjects().find((object) => object.id === roofId)
let moveDirection = roof.moveDirect;
let moveFlowLine = roof.moveFlowLine??0;
let moveUpDown = roof.moveUpDown??0;
const getSelectLine = () => roof.moveSelectLine;
const selectLine = getSelectLine();
let movePosition = roof.movePosition;
const startPoint = selectLine.startPoint
const endPoint = selectLine.endPoint
const orgRoofPoints = roof.points; // orgPoint를 orgPoints로 변경
const oldPoints = canvas?.skeleton.lastPoints ?? orgRoofPoints // 여기도 변경
const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints);
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
const baseLines = wall.baseLines
roof.basePoints = createOrderedBasePoints(roof.points, baseLines)
const skeletonPolygon = canvas.getObjects().filter((object) => object.skeletonType === 'polygon' && object.parentId === roofId)
const skeletonLines = canvas.getObjects().filter((object) => object.skeletonType === 'line' && object.parentId === roofId)
if (oppositeLine) {
console.log('Opposite line found:', oppositeLine);
} else {
console.log('No opposite line found');
}
if(moveFlowLine !== 0) {
return oldPoints.map((point, index) => {
console.log('Point:', point);
const newPoint = { ...point };
const absMove = Big(moveFlowLine).times(2).div(10);
console.log('skeletonBuilder moveDirection:', moveDirection);
switch (moveDirection) {
case 'left':
// Move left: decrease X
if (moveFlowLine !== 0) {
for (const line of oppositeLine) {
if (line.position === 'left') {
if (isSamePoint(newPoint, line.start)) {
newPoint.x = Big(line.start.x).plus(absMove).toNumber();
} else if (isSamePoint(newPoint, line.end)) {
newPoint.x = Big(line.end.x).plus(absMove).toNumber();
}
break;
}
}
} else if (moveUpDown !== 0) {
}
break;
case 'right':
for (const line of oppositeLine) {
if (line.position === 'right') {
if (isSamePoint(newPoint, line.start)) {
newPoint.x = Big(line.start.x).minus(absMove).toNumber();
} else if (isSamePoint(newPoint, line.end)) {
newPoint.x = Big(line.end.x).minus(absMove).toNumber();
}
break
}
}
break;
case 'up':
// Move up: decrease Y (toward top of screen)
for (const line of oppositeLine) {
if (line.position === 'top') {
if (isSamePoint(newPoint, line.start)) {
newPoint.y = Big(line.start.y).minus(absMove).toNumber();
} else if (isSamePoint(newPoint, line.end)) {
newPoint.y = Big(line.end.y).minus(absMove).toNumber();
}
break;
}
}
break;
case 'down':
// Move down: increase Y (toward bottom of screen)
for (const line of oppositeLine) {
if (line.position === 'bottom') {
console.log('oldPoint:', point);
if (isSamePoint(newPoint, line.start)) {
newPoint.y = Big(line.start.y).minus(absMove).toNumber();
} else if (isSamePoint(newPoint, line.end)) {
newPoint.y = Big(line.end.y).minus(absMove).toNumber();
}
break;
}
}
break;
default :
// 사용 예시
}
console.log('newPoint:', newPoint);
//baseline 변경
return newPoint;
})
} else if(moveUpDown !== 0) {
// const selectLine = getSelectLine();
//
// console.log("wall::::", wall.points)
// console.log("저장된 3333moveSelectLine:", roof.moveSelectLine);
// console.log("저장된 3moveSelectLine:", selectLine);
// const result = getSelectLinePosition(wall, selectLine, {
// testDistance: 5, // 테스트 거리
// debug: true // 디버깅 로그 출력
// });
// console.log("3333linePosition:::::", result.position);
const position = movePosition //result.position;
const absMove = Big(moveUpDown).times(1).div(10);
const modifiedStartPoints = [];
// oldPoints를 복사해서 새로운 points 배열 생성
let newPoints = oldPoints.map(point => ({...point}));
// selectLine과 일치하는 baseLines 찾기
const matchingLines = baseLines
.map((line, index) => ({ ...line, findIndex: index }))
.filter(line =>
(isSamePoint(line.startPoint, selectLine.startPoint) &&
isSamePoint(line.endPoint, selectLine.endPoint)) ||
(isSamePoint(line.startPoint, selectLine.endPoint) &&
isSamePoint(line.endPoint, selectLine.startPoint))
);
matchingLines.forEach(line => {
const originalStartPoint = line.startPoint;
const originalEndPoint = line.endPoint;
const offset = line.attributes.offset
// 새로운 좌표 계산
let newStartPoint = {...originalStartPoint};
let newEndPoint = {...originalEndPoint};
// 위치와 방향에 따라 좌표 조정
/*
switch (position) {
case 'left':
if (moveDirection === 'up') {
newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber();
newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber();
} else if (moveDirection === 'down') {
newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber();
newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber();
}
break;
case 'right':
if (moveDirection === 'up') {
newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber();
newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber();
} else if (moveDirection === 'down') {
newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber();
newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber();
}
break;
case 'top':
if (moveDirection === 'up') {
newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber();
newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber();
} else if (moveDirection === 'down') {
newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber();
newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber();
}
break;
case 'bottom':
if (moveDirection === 'up') {
newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber();
newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber();
} else if (moveDirection === 'down') {
newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber();
newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber();
}
break;
}
*/
// 원본 라인 업데이트
// newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트
console.log('absMove::', absMove);
newPoints.forEach((point, index) => {
if(position === 'bottom'){
if (moveDirection === 'in') {
if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.y = Big(point.y).minus(absMove).toNumber();
}
// if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
// point.y = Big(point.y).minus(absMove).toNumber();
// }
}else if (moveDirection === 'out'){
if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.y = Big(point.y).plus(absMove).toNumber();
}
// if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
// point.y = Big(point.y).plus(absMove).toNumber();
// }
}
}else if (position === 'top'){
if(moveDirection === 'in'){
if(isSamePoint(roof.basePoints[index], originalStartPoint)) {
point.y = Big(point.y).plus(absMove).toNumber();
}
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.y = Big(point.y).plus(absMove).toNumber();
}
}else if(moveDirection === 'out'){
if(isSamePoint(roof.basePoints[index], originalStartPoint)) {
point.y = Big(point.y).minus(absMove).toNumber();
}
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.y = Big(point.y).minus(absMove).toNumber();
}
}
}else if(position === 'left'){
if(moveDirection === 'in'){
if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.x = Big(point.x).plus(absMove).toNumber();
}
// if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
// point.x = Big(point.x).plus(absMove).toNumber();
// }
}else if(moveDirection === 'out'){
if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.x = Big(point.x).minus(absMove).toNumber();
}
// if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
// point.x = Big(point.x).minus(absMove).toNumber();
// }
}
}else if(position === 'right'){
if(moveDirection === 'in'){
if(isSamePoint(roof.basePoints[index], originalStartPoint)) {
point.x = Big(point.x).minus(absMove).toNumber();
}
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.x = Big(point.x).minus(absMove).toNumber();
}
}else if(moveDirection === 'out'){
if(isSamePoint(roof.basePoints[index], originalStartPoint)) {
point.x = Big(point.x).plus(absMove).toNumber();
}
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.x = Big(point.x).plus(absMove).toNumber();
}
}
}
});
// 원본 baseLine도 업데이트
line.startPoint = newStartPoint;
line.endPoint = newEndPoint;
});
return newPoints;
}
}
/**
* SkeletonBuilder를 사용하여 스켈레톤을 생성하고 내부선을 그립니다.
* @param {string} roofId - 지붕 ID
* @param {fabric.Canvas} canvas - 캔버스 객체
* @param {string} textMode - 텍스트 모드
* @param {fabric.Object} roof - 지붕 객체
* @param baseLines
*/
export const skeletonBuilder = (roofId, canvas, textMode) => {
//처마
let roof = canvas?.getObjects().find((object) => object.id === roofId)
const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE]
const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD]
/** 외벽선 */
const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId)
//const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0)
const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || [];
const baseLinePoints = baseLines.map((line) => ({x:line.left, y:line.top}));
const outerLines = canvas.getObjects().filter((object) => object.name === 'outerLinePoint') || [];
const outerLinePoints = outerLines.map((line) => ({x:line.left, y:line.top}))
const hipLines = canvas.getObjects().filter((object) => object.name === 'hip' && object.parentId === roofId) || [];
const ridgeLines = canvas.getObjects().filter((object) => object.name === 'ridge' && object.parentId === roofId) || [];
//const skeletonLines = [];
// 1. 지붕 폴리곤 좌표 전처리
const coordinates = preprocessPolygonCoordinates(roof.points);
if (coordinates.length < 3) {
console.warn("Polygon has less than 3 unique points. Cannot generate skeleton.");
return;
}
const moveFlowLine = roof.moveFlowLine || 0; // Provide a default value
const moveUpDown = roof.moveUpDown || 0; // Provide a default value
let points = roof.points;
//마루이동
if (moveFlowLine !== 0 || moveUpDown !== 0) {
points = movingLineFromSkeleton(roofId, canvas)
}
console.log('points:', points);
const geoJSONPolygon = toGeoJSON(points)
try {
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
geoJSONPolygon.pop()
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
// 스켈레톤 데이터를 기반으로 내부선 생성
roof.innerLines = roof.innerLines || [];
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
// 캔버스에 스켈레톤 상태 저장
if (!canvas.skeletonStates) {
canvas.skeletonStates = {}
canvas.skeletonLines = []
}
canvas.skeletonStates[roofId] = true
canvas.skeletonLines = [];
canvas.skeletonLines.push(...roof.innerLines)
roof.skeletonLines = canvas.skeletonLines;
const cleanSkeleton = {
Edges: skeleton.Edges.map(edge => ({
X1: edge.Edge.Begin.X,
Y1: edge.Edge.Begin.Y,
X2: edge.Edge.End.X,
Y2: edge.Edge.End.Y,
Polygon: edge.Polygon,
// Add other necessary properties, but skip circular references
})),
roofId: roofId,
// Add other necessary top-level properties
};
canvas.skeleton = [];
canvas.skeleton = cleanSkeleton
canvas.skeleton.lastPoints = points
canvas.set("skeleton", cleanSkeleton);
canvas.renderAll()
console.log('skeleton rendered.', canvas);
} catch (e) {
console.error('스켈레톤 생성 중 오류 발생:', e)
if (canvas.skeletonStates) {
canvas.skeletonStates[roofId] = false
canvas.skeletonStates = {}
canvas.skeletonLines = []
}
}
}
/**
* 스켈레톤 결과와 외벽선 정보를 바탕으로 내부선(용마루, 추녀)을 생성합니다.
* @param {object} skeleton - SkeletonBuilder로부터 반환된 스켈레톤 객체
* @param {fabric.Object} roof - 대상 지붕 객체
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
* @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
*/
const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
if (!skeleton?.Edges) return []
let roof = canvas?.getObjects().find((object) => object.id === roofId)
let wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
let skeletonLines = []
const processedInnerEdges = new Set()
const textElements = {};
const coordinateText = (line) => {
// Generate a stable ID for this line
const lineKey = `${line.x1},${line.y1},${line.x2},${line.y2}`;
// Remove existing text elements for this line
if (textElements[lineKey]) {
textElements[lineKey].forEach(text => {
if (canvas.getObjects().includes(text)) {
canvas.remove(text);
}
});
}
// Create start point text
const startText = new fabric.Text(`(${Math.round(line.x1)}, ${Math.round(line.y1)})`, {
left: line.x1 + 5,
top: line.y1 - 20,
fontSize: 10,
fill: 'green',
fontFamily: 'Arial',
selectable: false,
hasControls: false,
hasBorders: false
});
// Create end point text
const endText = new fabric.Text(`(${Math.round(line.x2)}, ${Math.round(line.y2)})`, {
left: line.x2 + 5,
top: line.y2 - 20,
fontSize: 10,
fill: 'orange',
fontFamily: 'Arial',
selectable: false,
hasControls: false,
hasBorders: false
});
// Add to canvas
canvas.add(startText, endText);
// Store references
textElements[lineKey] = [startText, endText];
// Bring lines to front
canvas.bringToFront(startText);
canvas.bringToFront(endText);
};
// 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다.
skeleton.Edges.forEach((edgeResult, index) => {
processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines);
});
// 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다.
skeleton.Edges.forEach(edgeResult => {
const { Begin, End } = edgeResult.Edge;
const gableBaseLine = roof.lines.find(line =>
line.attributes.type === 'gable' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
);
if (gableBaseLine) {
// Store current state before processing
const beforeGableProcessing = JSON.parse(JSON.stringify(skeletonLines));
// if(canvas.skeletonLines.length > 0){
// skeletonLines = canvas.skeletonLines;
// }
// Process gable edge with both current and previous states
const processedLines = processGableEdge(
edgeResult,
baseLines,
[...skeletonLines], // Current state
gableBaseLine,
beforeGableProcessing // Previous state
);
// Update canvas with processed lines
canvas.skeletonLines = processedLines;
skeletonLines = processedLines;
}
});
/*
//2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다.
const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines);
if(disconnectedLines.length > 0) {
disconnectedLines.forEach(dLine => {
const { index, extendedLine, p1Connected, p2Connected } = dLine;
const newPoint = extendedLine?.point;
if (!newPoint) return;
// p1이 끊어졌으면 p1을, p2가 끊어졌으면 p2를 연장된 지점으로 업데이트
if (p1Connected) { //p2 연장
skeletonLines[index].p2 = { ...skeletonLines[index].p2, x: newPoint.x, y: newPoint.y };
} else if (p2Connected) {//p1 연장
skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y };
}
});
//2-1 확장된 스켈레톤 선이 연장되다가 서로 만나면 만난점(접점)에서 멈추어야 된다.
trimIntersectingExtendedLines(skeletonLines, disconnectedLines);
}
*/
//2. 연결이 끊어진 라인이 있을경우 찾아서 추가한다(동 이동일때)
// 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다.
const innerLines = [];
const addLines = []
const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set
//처마라인
const roofLines = roof.lines
//벽라인
const wallLines = wall.lines
skeletonLines.forEach((sktLine, skIndex) => {
let { p1, p2, attributes, lineStyle } = sktLine;
// 중복방지 - 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교)
const lineKey = [
[p1.x, p1.y].sort().join(','),
[p2.x, p2.y].sort().join(',')
].sort().join('|');
// 이미 추가된 라인인지 확인
if (existingLines.has(lineKey)) {
return; // 이미 있는 라인이면 스킵
}
const direction = getLineDirection(
{ x: sktLine.p1.x, y: sktLine.p1.y },
{ x: sktLine.p2.x, y: sktLine.p2.y }
);
//그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함
let roofIdx = 0;
// roofLines.forEach((roofLine) => {
//
// if (isSameLine(p1.x, p1.y, p2.x, p2.y, roofLine) || isSameLine(p2.x, p2.y, p1.x, p1.y, roofLine)) {
// roofIdx = roofLine.idx;
// console.log("roofIdx::::::", roofIdx)
// return false; // forEach 중단
// }
// });
const skeletonLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId: roof.id,
fontSize: roof.fontSize,
stroke: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color,
strokeWidth: lineStyle.width,
name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type,
attributes: attributes,
direction: direction,
isBaseLine: sktLine.attributes.isOuterEdge,
lineName: (sktLine.attributes.isOuterEdge)?'roofLine': attributes.type,
selectable:(!sktLine.attributes.isOuterEdge),
//visible: (!sktLine.attributes.isOuterEdge),
});
coordinateText(skeletonLine)
canvas.add(skeletonLine);
skeletonLine.bringToFront();
existingLines.add(lineKey); // 추가된 라인을 추적
//skeleton 라인에서 처마선은 삭제
if(skeletonLine.lineName === 'roofLine'){
skeletonLine.set('visible', false); //임시
roof.set({
//stroke: 'black',
strokeWidth: 4
});
}else{
}
innerLines.push(skeletonLine)
canvas.renderAll();
});
if(roof.moveUpDown??0 > 0) {
// 같은 라인이 없으므로 새 다각형 라인 생성
//라인 편집
// let i = 0
const currentRoofLines = canvas.getObjects().filter((obj) => obj.lineName === 'roofLine' && obj.attributes.roofId === roofId)
let roofLineRects = canvas.getObjects().filter((obj) => obj.name === 'roofLineRect' && obj.roofId === roofId)
roofLineRects.forEach((roofLineRect) => {
canvas.remove(roofLineRect)
canvas.renderAll()
})
let helpLines = canvas.getObjects().filter((obj) => obj.lineName === 'helpLine' && obj.roofId === roofId)
helpLines.forEach((helpLine) => {
canvas.remove(helpLine)
canvas.renderAll()
})
function sortCurrentRoofLines(lines) {
return [...lines].sort((a, b) => {
// Get all coordinates in a consistent order
const getCoords = (line) => {
const x1 = line.x1 ?? line.get('x1');
const y1 = line.y1 ?? line.get('y1');
const x2 = line.x2 ?? line.get('x2');
const y2 = line.y2 ?? line.get('y2');
// Sort points left-to-right, then top-to-bottom
return x1 < x2 || (x1 === x2 && y1 < y2)
? [x1, y1, x2, y2]
: [x2, y2, x1, y1];
};
const aCoords = getCoords(a);
const bCoords = getCoords(b);
// Compare each coordinate in order
for (let i = 0; i < 4; i++) {
if (Math.abs(aCoords[i] - bCoords[i]) > 0.1) {
return aCoords[i] - bCoords[i];
}
}
return 0;
});
}
// function sortCurrentRoofLines(lines) {
// return [...lines].sort((a, b) => {
// const aX = a.x1 ?? a.get('x1')
// const aY = a.y1 ?? a.get('y1')
// const bX = b.x1 ?? b.get('x1')
// const bY = b.y1 ?? b.get('y1')
// if (aX !== bX) return aX - bX
// return aY - bY
// })
// }
// 각 라인 집합 정렬
// roofLines의 방향에 맞춰 currentRoofLines의 방향을 조정
const alignLineDirection = (sourceLines, targetLines) => {
return sourceLines.map(sourceLine => {
// 가장 가까운 targetLine 찾기
const nearestTarget = targetLines.reduce((nearest, targetLine) => {
const sourceCenter = {
x: (sourceLine.x1 + sourceLine.x2) / 2,
y: (sourceLine.y1 + sourceLine.y2) / 2
};
const targetCenter = {
x: (targetLine.x1 + targetLine.x2) / 2,
y: (targetLine.y1 + targetLine.y2) / 2
};
const distance = Math.hypot(
sourceCenter.x - targetCenter.x,
sourceCenter.y - targetCenter.y
);
return !nearest || distance < nearest.distance
? { line: targetLine, distance }
: nearest;
}, null)?.line;
if (!nearestTarget) return sourceLine;
// 방향이 반대인지 확인 (벡터 내적을 사용)
const sourceVec = {
x: sourceLine.x2 - sourceLine.x1,
y: sourceLine.y2 - sourceLine.y1
};
const targetVec = {
x: nearestTarget.x2 - nearestTarget.x1,
y: nearestTarget.y2 - nearestTarget.y1
};
const dotProduct = sourceVec.x * targetVec.x + sourceVec.y * targetVec.y;
// 내적이 음수이면 방향이 반대이므로 뒤집기
if (dotProduct < 0) {
return {
...sourceLine,
x1: sourceLine.x2,
y1: sourceLine.y2,
x2: sourceLine.x1,
y2: sourceLine.y1
};
}
return sourceLine;
});
};
const sortedWallLines = sortCurrentRoofLines(wall.lines);
// roofLines의 방향에 맞춰 currentRoofLines 조정 후 정렬
const alignedCurrentRoofLines = alignLineDirection(currentRoofLines, roofLines);
const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines);
const sortedRoofLines = sortCurrentRoofLines(roofLines);
const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines);
//wall.lines 는 기본 벽 라인
//wall.baseLine은 움직인라인
const movedLines = []
sortedWallLines.forEach((wallLine, index) => {
const roofLine = sortedRoofLines[index];
const currentRoofLine = sortedCurrentRoofLines[index];
const moveLine = sortedWallBaseLines[index]
const wallBaseLine = sortedWallBaseLines[index]
//roofline 외곽선 설정
console.log('=== Line Coordinates ===');
console.table({
'Point' : ['X', 'Y'],
'roofLine' : [roofLine.x1, roofLine.y1],
'currentRoofLine': [currentRoofLine.x1, currentRoofLine.y1],
'moveLine' : [moveLine.x1, moveLine.y1],
'wallBaseLine' : [wallBaseLine.x1, wallBaseLine.y1]
});
console.log('End Points:');
console.table({
'Point' : ['X', 'Y'],
'roofLine' : [roofLine.x2, roofLine.y2],
'currentRoofLine': [currentRoofLine.x2, currentRoofLine.y2],
'moveLine' : [moveLine.x2, moveLine.y2],
'wallBaseLine' : [wallBaseLine.x2, wallBaseLine.y2]
});
const origin = moveLine.attributes?.originPoint
if (!origin) return
if (isSamePoint(moveLine, wallLine)) {
return false
}
const movedStart = Math.abs(moveLine.x1 - wallLine.x1) > EPSILON || Math.abs(moveLine.y1 - origin.y1) > EPSILON
const movedEnd = Math.abs(moveLine.x2 - wallLine.x2) > EPSILON || Math.abs(moveLine.y2 - origin.y2) > EPSILON
const fullyMoved = movedStart && movedEnd
//반시계 방향
let newPStart //= {x:roofLine.x1, y:roofLine.y1}
let newPEnd //= {x:movedLines.x2, y:movedLines.y2}
//현재 roof는 무조건 시계방향
const getAddLine = (p1, p2, stroke = '#1083E3') => {
movedLines.push({ index, p1, p2 })
// Usage:
let mergeLines = mergeMovedLines(movedLines);
console.log("mergeLines:::::::", mergeLines);
const line = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId : roof.id,
fontSize : roof.fontSize,
stroke : stroke,
strokeWidth: 4,
name : 'eaveHelpLine',
lineName : 'eaveHelpLine',
selectable : true,
visible : true,
roofId : roofId,
attributes : {
type: 'eaveHelpLine',
isStart : true
}
});
coordinateText(line)
canvas.add(line)
canvas.renderAll();
return line
}
getAddLine(roofLine.startPoint, roofLine.endPoint)
newPStart = { x: roofLine.x1, y: roofLine.y1 }
newPEnd = { x: roofLine.x2, y: roofLine.y2 }
// Usage in your code:
// if (fullyMoved) {
// const result = adjustLinePoints({
// roofLine,
// currentRoofLine,
// wallBaseLine,
// origin,
// moveType: 'both' // Adjust both start and end points
// });
// newPStart = result.newPStart;
// newPEnd = result.newPEnd;
// getAddLine(newPStart, newPEnd, 'red');
// }
// else if (movedStart) {
// const result = adjustLinePoints({
// roofLine,
// currentRoofLine,
// wallBaseLine,
// origin,
// moveType: 'start' // Only adjust start point
// });
// newPStart = result.newPStart;
// getAddLine(newPStart, newPEnd, 'green');
// }
// else if (movedEnd) {
// const result = adjustLinePoints({
// roofLine,
// currentRoofLine,
// wallBaseLine,
// origin,
// moveType: 'end' // Only adjust end point
// });
// newPEnd = result.newPEnd;
// getAddLine(newPStart, newPEnd, 'orange');
// }
// canvas.renderAll()
//두 포인트가 변경된 라인인
if (fullyMoved) {
//반시계방향향
console.log("moveFully:::::::::::::", wallBaseLine, newPStart, newPEnd)
if (getOrientation(roofLine) === 'vertical') {
//왼쪽 부터 roofLine, wallBaseLine
if (newPEnd.y <= wallBaseLine.y2 && wallBaseLine.y2 <= newPStart.y && newPStart.y <= wallBaseLine.y1) {
newPStart.y = wallBaseLine.y1;
getAddLine({ x: newPEnd.x, y: wallBaseLine.y1 }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
} else if (wallBaseLine.y2 <= newPEnd.y && newPEnd.y <= wallBaseLine.y1 && wallBaseLine.y1 <= newPStart.y) {
newPEnd.y = wallBaseLine.y2;
getAddLine({ x: newPEnd.x, y: wallBaseLine.y2 }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
} else if (newPStart.y <= wallBaseLine.y1 && wallBaseLine.y1 <= newPEnd.y && newPEnd.y <= wallBaseLine.y2) {
newPEnd.y = wallBaseLine.y2;
getAddLine({ x: newPEnd.x, y: wallBaseLine.y2 }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
} else if (wallBaseLine.y1 <= newPStart.y && newPStart.y <= wallBaseLine.y2 && wallBaseLine.y2 <= newPEnd.y) {
newPStart.y = wallBaseLine.y1;
getAddLine({ x: newPEnd.x, y: wallBaseLine.y1 }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
} else if (wallBaseLine.y2 <= newPEnd.y && newPEnd.y <= newPStart.y && newPStart.y <= wallBaseLine.y1) { // 위가운데
newPEnd.y = wallBaseLine.y2;
getAddLine({ x: newPEnd.x, y: wallBaseLine.y2 }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
newPStart.y = wallBaseLine.y1;
getAddLine({ x: newPEnd.x, y: wallBaseLine.y1 }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
} else if (wallBaseLine.y1 <= newPStart.y && newPStart.y <= newPEnd.y &&newPEnd.y <= wallBaseLine.y2) { // 아래가운데
newPEnd.y = wallBaseLine.y1;
getAddLine({ x: newPEnd.x, y: wallBaseLine.y1 }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
newPStart.y = wallBaseLine.y2;
getAddLine({ x: newPStart.x, y: wallBaseLine.y2 }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
}
} else if (getOrientation(roofLine) === 'horizontal') {
if (newPEnd.x <= wallBaseLine.x2 && wallBaseLine.x2 <= newPStart.x && newPStart.x <= wallBaseLine.x1) { //위 왼쪽
newPStart.x = wallBaseLine.x1;
getAddLine({ x: wallBaseLine.x1, y: newPEnd.y }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
} else if (wallBaseLine.x2 <= newPEnd.x && newPEnd.x <= wallBaseLine.x1 && wallBaseLine.x1 <= newPStart.x) { //아래오르쪽
newPEnd.x = wallBaseLine.x2;
getAddLine({ x: wallBaseLine.x2, y: newPEnd.y }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
} else if (newPStart.x <= wallBaseLine.x1 && wallBaseLine.x1 <= newPEnd.x && newPEnd.x <= wallBaseLine.x2) { //위 오른쪽
newPEnd.x = wallBaseLine.x2;
getAddLine({ x: wallBaseLine.x2, y: newPEnd.y }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
} else if (wallBaseLine.x1 <= newPStart.x && newPStart.x <= wallBaseLine.x2 && wallBaseLine.x2 <= newPEnd.x) { //아래 왼쪽
newPStart.x = wallBaseLine.x1;
getAddLine({ x: wallBaseLine.x1, y: newPEnd.y }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
} else if (wallBaseLine.x2 <= newPEnd.x && newPEnd.x <= newPStart.x && newPStart.x <= wallBaseLine.x1) { // 위가운데
newPEnd.x = wallBaseLine.x2;
getAddLine({ x: wallBaseLine.x2, y: newPEnd.y }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
newPStart.x = wallBaseLine.x1;
getAddLine({ x: wallBaseLine.x1, y: newPEnd.y }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
} else if (wallBaseLine.x1 <= newPStart.x && newPStart.x <= newPEnd.x && newPEnd.x <= wallBaseLine.x2) { // 아래가운데
newPEnd.x = wallBaseLine.x1;
getAddLine({ x: wallBaseLine.x1, y: newPEnd.y }, { x: wallBaseLine.x1, y: wallBaseLine.y1 })
newPStart.x = wallBaseLine.x2;
getAddLine({ x: wallBaseLine.x2, y: newPEnd.y }, { x: wallBaseLine.x2, y: wallBaseLine.y2 })
}
}
getAddLine(newPStart, newPEnd, 'red')
} else if (movedStart) { //end 변경경
if (getOrientation(roofLine) === 'vertical') {
let isCross = false
if (Math.abs(currentRoofLine.x2 - roofLine.x1) < 0.1 || Math.abs(currentRoofLine.x1 - roofLine.x2) < 0.1) {
isCross = true;
}
if(newPStart.y <= wallBaseLine.y1 && wallBaseLine.y1 < wallBaseLine.y2 && wallBaseLine.y2 < newPEnd.y){//가장 왼쪽v
newPStart = { x: roofLine.x1, y: roofLine.y1 }
newPEnd = { x: roofLine.x2, y: (isCross) ? currentRoofLine.y1 : wallBaseLine.y1 }
}else if(newPEnd.y <= wallBaseLine.y2 && wallBaseLine.y2 < wallBaseLine.y1 && wallBaseLine.y1 <= newPStart.y){ //하단 오른쪽v
newPStart = { x: roofLine.x1, y: roofLine.y1 }
newPEnd = { x: roofLine.x2, y: (isCross) ? currentRoofLine.y1 : wallBaseLine.y1 }
}else if(newPEnd.y <= wallBaseLine.y2 && wallBaseLine.y2 <= newPStart.y && newPStart.y <= wallBaseLine.y1) { //상단 왼쪽v
newPStart = { x: roofLine.x1, y: (isCross) ? currentRoofLine.y1 : wallBaseLine.y1 }
newPEnd ={ x: roofLine.x2, y: roofLine.y2 }
}else if(newPStart.y <= wallBaseLine.y1 && wallBaseLine.y1 <= newPEnd.y && newPEnd.y <= wallBaseLine.y2) {//상단 오르쪽
newPStart = { x: roofLine.x1, y: roofLine.y1 }
newPEnd = { x: roofLine.x2, y: (isCross) ? currentRoofLine.y1 : wallBaseLine.y1 }
}else if(wallBaseLine.y1 <= newPStart.y && newPStart.y <= wallBaseLine.y2 && wallBaseLine.y2 <= newPEnd.y) { //하단 오른쪽v
newPStart = { x: roofLine.x1, y: (isCross) ? currentRoofLine.y1 : wallBaseLine.y1 }
newPEnd = { x: roofLine.x2, y: roofLine.y2 }
}else if (wallBaseLine.y2 <= newPEnd.y && newPEnd.y <= wallBaseLine.y1 && wallBaseLine.y1 <= newPStart.y) { //하단 왼쪽
newPStart = { x: roofLine.x1, y: (isCross) ? currentRoofLine.y1 : wallBaseLine.y1 }
newPEnd ={ x: roofLine.x2, y: roofLine.y2 }
}
} else if (getOrientation(roofLine) === 'horizontal') {
let isCross = false
if (Math.abs(currentRoofLine.y1 - roofLine.y2) < 0.1 || Math.abs(currentRoofLine.y2 - roofLine.y1) < 0.1) {
isCross = true;
}
if(newPStart.x <= wallBaseLine.x1 && wallBaseLine.x1 < wallBaseLine.x2 && wallBaseLine.x2 < newPEnd.x){//가장 왼쪽v
newPStart = { y: roofLine.y1, x: roofLine.x1 }
newPEnd = { y: roofLine.y2, x: (isCross) ? currentRoofLine.x1 : wallBaseLine.x1 }
}else if(newPEnd.x <= wallBaseLine.x2 && wallBaseLine.x2 < wallBaseLine.x1 && wallBaseLine.x1 <= newPStart.x){ //하단 오른쪽v
newPStart = { y: roofLine.y1, x: roofLine.x1 }
newPEnd = { y: roofLine.y2, x: (isCross) ? currentRoofLine.x1 : wallBaseLine.x1 }
}else if(newPEnd.x <= wallBaseLine.x2 && wallBaseLine.x2 <= newPStart.x && newPStart.x <= wallBaseLine.x1) { //상단 왼쪽v
newPStart = { y: roofLine.y1, x: (isCross) ? currentRoofLine.x1 : wallBaseLine.x1 }
newPEnd ={ y: roofLine.y2, x: roofLine.x2 }
}else if(newPStart.x <= wallBaseLine.x1 && wallBaseLine.x1 <= newPEnd.x && newPEnd.x <= wallBaseLine.x2) {//상단 오르쪽
newPStart = { y: roofLine.y1, x: (isCross) ? currentRoofLine.x1 : wallBaseLine.x1 }
newPEnd ={ y: roofLine.y2, x: roofLine.x2 }
}else if(wallBaseLine.x1 <= newPStart.x && newPStart.x <= wallBaseLine.x2 && wallBaseLine.x2 <= newPEnd.x) { //하단 오른쪽v
newPStart = { y: roofLine.y1, x: (isCross) ? currentRoofLine.y1 : wallBaseLine.x1 }
newPEnd = { y: roofLine.y2, x: roofLine.x2 }
}else if (wallBaseLine.x2 <= newPEnd.x && newPEnd.x <= wallBaseLine.x1 && wallBaseLine.x1 <= newPStart.x) { //right / top
newPStart = { y: roofLine.y1, x: roofLine.x1 }
newPEnd = { y: roofLine.y2, x: (isCross) ? currentRoofLine.x1 : wallBaseLine.x1 }
}
//newPEnd = { x: (isCross) ? currentRoofLine.x1 : origin.x1, y: roofLine.y1 } //수직라인 접점까지지
}
getAddLine(newPStart, newPEnd, 'green')
//movedLines.push({ index, newPStart, newPEnd })
console.log("moveStart:::::::::::::", origin, newPStart, newPEnd)
} else if (movedEnd) { //start변경
//반시계방향
if (getOrientation(roofLine) === 'vertical') {
let isCross = false
if (Math.abs(currentRoofLine.x2 - roofLine.x1) < 0.1 || Math.abs(currentRoofLine.x1 - roofLine.x2) < 0.1) {
isCross = true;
}
if(newPStart.y <= wallBaseLine.y1 && wallBaseLine.y1 < wallBaseLine.y2 && wallBaseLine.y2 < newPEnd.y){//bottom leftv
newPStart = { x: roofLine.x1, y: (isCross) ? currentRoofLine.y2 : wallBaseLine.y2 }
newPEnd = { x: roofLine.x2, y: roofLine.y2 }
}else if(newPEnd.y <= wallBaseLine.y2 && wallBaseLine.y2 < wallBaseLine.y1 && wallBaseLine.y1 <= newPStart.y){ //top /right
newPStart = { x: roofLine.x1, y: (isCross) ? currentRoofLine.y2 : wallBaseLine.y2 }
newPEnd = { x: roofLine.x2, y: roofLine.y2 }
}else if(newPEnd.y <= wallBaseLine.y2 && wallBaseLine.y2 <= newPStart.y && newPStart.y <= wallBaseLine.y1) { //top / left
newPStart = { x: roofLine.x1, y: (isCross) ? currentRoofLine.y2 : wallBaseLine.y2 }
newPEnd ={ x: roofLine.x2, y: roofLine.y2 }
}else if(newPStart.y <= wallBaseLine.y1 && wallBaseLine.y1 <= newPEnd.y && newPEnd.y <= wallBaseLine.y2) {//top / righty 오르쪽v
newPStart = { x: roofLine.x1, y: roofLine.y1 }
newPEnd = { x: roofLine.x2, y: (isCross) ? currentRoofLine.y2 : wallBaseLine.y2 }
}else if(wallBaseLine.y1 <= newPStart.y && newPStart.y <= wallBaseLine.y2 && wallBaseLine.y2 <= newPEnd.y) { //하단 오른쪽v
newPStart = { x: roofLine.x1, y: (isCross) ? currentRoofLine.y1 : wallBaseLine.y1 }
newPEnd = { x: roofLine.x2, y: roofLine.y2 }
}else if (wallBaseLine.y2 <= newPEnd.y && newPEnd.y <= wallBaseLine.y1 && wallBaseLine.y1 <= newPStart.y) { //하단 왼쪽
newPStart = { x: roofLine.x1, y: roofLine.y1 }
newPEnd = { x: roofLine.x2, y: (isCross) ? currentRoofLine.y2 : wallBaseLine.y2 }
}
} else if (getOrientation(roofLine) === 'horizontal') {
let isCross = false
if (Math.abs(currentRoofLine.y2 - roofLine.y1) < 0.1 || Math.abs(currentRoofLine.y1 - roofLine.y2) < 0.1) {
isCross = true;
}
if(newPStart.x <= wallBaseLine.x1 && wallBaseLine.x1 < wallBaseLine.x2 && wallBaseLine.x2 < newPEnd.x){//right / bottom
newPStart = { y: roofLine.y1, x: (isCross) ? currentRoofLine.x2 : wallBaseLine.x2 }
newPEnd = { y: roofLine.y2, x: roofLine.x2 }
}else if(newPEnd.x <= wallBaseLine.x2 && wallBaseLine.x2 < wallBaseLine.x1 && wallBaseLine.x1 <= newPStart.x){ //left / top
newPStart = { y: roofLine.y1, x: (isCross) ? currentRoofLine.x2 : wallBaseLine.x2 }
newPEnd = { y: roofLine.y2, x: roofLine.x2 }
}else if(newPEnd.x <= wallBaseLine.x2 && wallBaseLine.x2 <= newPStart.x && newPStart.x <= wallBaseLine.x1) { //left top
newPStart = { y: roofLine.y1, x: (isCross) ? currentRoofLine.x2 : wallBaseLine.x2 }
newPEnd ={ y: roofLine.y2, x: roofLine.x2 }
}else if(newPStart.x <= wallBaseLine.x1 && wallBaseLine.x1 <= newPEnd.x && newPEnd.x <= wallBaseLine.x2) {//상단 오르쪽v
newPStart = { y: roofLine.y1, x: roofLine.x1 }
newPEnd = { y: roofLine.y2, x: (isCross) ? currentRoofLine.x2 : wallBaseLine.x2 }
}else if(wallBaseLine.x1 <= newPStart.x && newPStart.x <= wallBaseLine.x2 && wallBaseLine.x2 <= newPEnd.x) { //하단 오른쪽v
newPStart = { y: roofLine.y1, x: (isCross) ? currentRoofLine.x1 : wallBaseLine.x1 }
newPEnd = { y: roofLine.y2, x: roofLine.x2 }
}else if (wallBaseLine.x2 <= newPEnd.x && newPEnd.x <= wallBaseLine.x1 && wallBaseLine.x1 <= newPStart.x) { //하단 왼쪽
newPStart = { y: roofLine.y1, x: roofLine.x1 }
newPEnd = { y: roofLine.y2, x: (isCross) ? currentRoofLine.x2 : wallBaseLine.x2 }
}
// newPStart = { x: roofLine.x2, y: roofLine.y2 }
// newPEnd = { x: (isCross) ? currentRoofLine.x2 : origin.x2, y: roofLine.y2 } //수직라인 접점까지지
}
console.log("movedEnd:::::::::::::", origin, newPStart, newPEnd)
getAddLine(newPStart, newPEnd, 'orange')
//movedLines.push({ index, newPStart, newPEnd })
}
canvas.renderAll()
});
//polygon 만들기
console.log("innerLines:::::", innerLines)
console.log("movedLines", movedLines)
}
// --- 사용 예시 ---
// const polygons = findConnectedLines(movedLines, innerLines, canvas, roofId, roof);
// console.log("polygon", polygons);
// 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
const convertedPolygon = edgeResult.Polygon?.map(point => ({
x: typeof point.X === 'number' ? parseFloat(point.X) : 0,
y: typeof point.Y === 'number' ? parseFloat(point.Y) : 0
})).filter(point => point.x !== 0 || point.y !== 0) || [];
if (convertedPolygon.length > 0) {
const skeletonPolygon = new QPolygon(convertedPolygon, {
type: POLYGON_TYPE.ROOF,
fill: false,
stroke: 'blue',
strokeWidth: 4,
skeletonType: 'polygon',
polygonName: '',
parentId: roof.id,
});
//canvas?.add(skeletonPolygon)
//canvas.renderAll()
}
let eavesLines = []
for (let i = 0; i < polygonPoints.length; i++) {
const p1 = polygonPoints[i];
const p2 = polygonPoints[(i + 1) % polygonPoints.length];
// 지붕 경계선과 교차 확인 및 클리핑
const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine);
//console.log('clipped line', clippedLine.p1, clippedLine.p2);
const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge])
addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine);
// }
}
}
function findMatchingLine(edgePolygon, roof, roofPoints) {
const edgePoints = edgePolygon.map(p => ({ x: p.X, y: p.Y }));
for (let i = 0; i < edgePoints.length; i++) {
const p1 = edgePoints[i];
const p2 = edgePoints[(i + 1) % edgePoints.length];
for (let j = 0; j < roofPoints.length; j++) {
const rp1 = roofPoints[j];
const rp2 = roofPoints[(j + 1) % roofPoints.length];
if ((isSamePoint(p1, rp1) && isSamePoint(p2, rp2)) ||
(isSamePoint(p1, rp2) && isSamePoint(p2, rp1))) {
// 매칭되는 라인을 찾아서 반환
return roof.lines.find(line =>
(isSamePoint(line.p1, rp1) && isSamePoint(line.p2, rp2)) ||
(isSamePoint(line.p1, rp2) && isSamePoint(line.p2, rp1))
);
}
}
}
return null;
}
/**
* GABLE(케라바) Edge를 처리하여 스켈레톤 선을 정리하고 연장합니다.
* @param {object} edgeResult - 스켈레톤 Edge 데이터
* @param {Array<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 {object} p1 - 시작점
* @param {object} p2 - 끝점
* @param {string} lineType - 라인 타입
* @param {string} color - 색상
* @param {number} width - 두께
* @param pitch
* @param isOuterLine
*/
function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine) {
// const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|');
// if (processedInnerEdges.has(edgeKey)) return;
// processedInnerEdges.add(edgeKey);
const currentDegree = getDegreeByChon(pitch)
const dx = Math.abs(p2.x - p1.x);
const dy = Math.abs(p2.y - p1.y);
const isDiagonal = dx > 0.1 && dy > 0.1;
const normalizedType = isDiagonal ? LINE_TYPE.SUBLINE.HIP : lineType;
// Count existing HIP lines
const existingEavesCount = skeletonLines.filter(line =>
line.lineName === LINE_TYPE.SUBLINE.RIDGE
).length;
// If this is a HIP line, its index will be the existing count
const eavesIndex = normalizedType === LINE_TYPE.SUBLINE.RIDGE ? existingEavesCount : undefined;
const newLine = {
p1,
p2,
attributes: {
roofId: id,
actualSize: (isDiagonal) ? calcLineActualSize(
{
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y
},
currentDegree
) : calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }),
type: normalizedType,
planeSize: calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }),
isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE,
isOuterEdge: isOuterLine,
pitch: pitch,
...(eavesIndex !== undefined && { eavesIndex })
},
lineStyle: { color, width },
};
skeletonLines.push(newLine);
//console.log('skeletonLines', skeletonLines);
}
/**
* 폴리곤 좌표를 스켈레톤 생성에 적합하게 전처리합니다 (중복 제거, 시계 방향 정렬).
* @param {Array<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;
// 단순히 좌표 차이로 판단
const deltaX = lineMidX - refMidX;
const deltaY = lineMidY - refMidY;
// 참조선의 기울기
const refDeltaX = referenceLine.end.x - referenceLine.start.x;
const refDeltaY = referenceLine.end.y - referenceLine.start.y;
// 참조선이 더 수평인지 수직인지 판단
if (Math.abs(refDeltaX) > Math.abs(refDeltaY)) {
// 수평선에 가까운 경우 - Y 좌표로 판단
return deltaY > 0 ? 'bottom' : 'top';
} else {
// 수직선에 가까운 경우 - X 좌표로 판단
return deltaX > 0 ? 'right' : 'left';
}
}
/**
* Helper function to find if two points are the same within a tolerance
*/
function isSamePoint(p1, p2, tolerance = 0.1) {
return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance;
}
// 두 점을 지나는 직선의 기울기 계산
function calculateSlope(p1, p2) {
// 수직선인 경우 (기울기 무한대)
if (p1.x === p2.x) return Infinity;
return (p2.y - p1.y) / (p2.x - p1.x);
}
// 두 직선이 평행한지 확인
// 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로 만들어진 다각형 내부에만 선분이 존재하도록 클리핑합니다.
* @param {Object} p1 - 선분의 시작점 {x, y}
* @param {Object} p2 - 선분의 끝점 {x, y}
* @param {Array} roofLines - 지붕 경계선 배열 (QLine 객체의 배열)
* @param skeletonLines
* @returns {Object} {p1: {x, y}, p2: {x, y}} - 다각형 내부로 클리핑된 선분
*/
function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) {
if (!roofLines || !roofLines.length) {
return { p1: { ...p1 }, p2: { ...p2 } };
}
const dx = Math.abs(p2.x - p1.x);
const dy = Math.abs(p2.y - p1.y);
const isDiagonal = dx > 0.5 && dy > 0.5;
// 기본값으로 원본 좌표 설정
let clippedP1 = { x: p1.x, y: p1.y };
let clippedP2 = { x: p2.x, y: p2.y };
// p1이 다각형 내부에 있는지 확인
const p1Inside = isPointInsidePolygon(p1, roofLines);
// p2가 다각형 내부에 있는지 확인
const p2Inside = isPointInsidePolygon(p2, roofLines);
//console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside);
// 두 점 모두 내부에 있으면 그대로 반환
if (p1Inside && p2Inside) {
if(!selectLine || isDiagonal){
return { p1: clippedP1, p2: clippedP2 };
}
//console.log('평행선::', clippedP1, clippedP2)
return { p1: clippedP1, p2: clippedP2 };
}
// 선분과 다각형 경계선의 교차점들을 찾음
const intersections = [];
for (const line of roofLines) {
const lineP1 = { x: line.x1, y: line.y1 };
const lineP2 = { x: line.x2, y: line.y2 };
const intersection = getLineIntersection(p1, p2, lineP1, lineP2);
if (intersection) {
// 교차점이 선분 위에 있는지 확인
const t = getParameterT(p1, p2, intersection);
if (t >= 0 && t <= 1) {
intersections.push({
point: intersection,
t: t
});
}
}
}
//console.log('Found intersections:', intersections.length);
// 교차점들을 t 값으로 정렬
intersections.sort((a, b) => a.t - b.t);
if (!p1Inside && !p2Inside) {
// 두 점 모두 외부에 있는 경우
if (intersections.length >= 2) {
//console.log('Both outside, using intersection points');
clippedP1.x = intersections[0].point.x;
clippedP1.y = intersections[0].point.y;
clippedP2.x = intersections[1].point.x;
clippedP2.y = intersections[1].point.y;
} else {
//console.log('Both outside, no valid intersections - returning original');
// 교차점이 충분하지 않으면 원본 반환
return { p1: clippedP1, p2: clippedP2 };
}
} else if (!p1Inside && p2Inside) {
// p1이 외부, p2가 내부
if (intersections.length > 0) {
//console.log('p1 outside, p2 inside - moving p1 to intersection');
clippedP1.x = intersections[0].point.x;
clippedP1.y = intersections[0].point.y;
// p2는 이미 내부에 있으므로 원본 유지
clippedP2.x = p2.x;
clippedP2.y = p2.y;
}
} else if (p1Inside && !p2Inside) {
// p1이 내부, p2가 외부
if (intersections.length > 0) {
//console.log('p1 inside, p2 outside - moving p2 to intersection');
// p1은 이미 내부에 있으므로 원본 유지
clippedP1.x = p1.x;
clippedP1.y = p1.y;
clippedP2.x = intersections[0].point.x;
clippedP2.y = intersections[0].point.y;
}
}
return { p1: clippedP1, p2: clippedP2 };
}
/**
* 점이 다각형 내부에 있는지 확인합니다 (Ray Casting 알고리즘 사용).
* @param {Object} point - 확인할 점 {x, y}
* @param {Array} roofLines - 다각형을 구성하는 선분들
* @returns {boolean} 점이 다각형 내부에 있으면 true
*/
function isPointInsidePolygon2(point, roofLines) {
let inside = false;
const x = point.x;
const y = point.y;
for (const line of roofLines) {
const x1 = line.x1;
const y1 = line.y1;
const x2 = line.x2;
const y2 = line.y2;
// Ray casting: 점에서 오른쪽으로 수평선을 그었을 때 다각형 경계와 교차하는 횟수 확인
if (((y1 > y) !== (y2 > y)) && (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1)) {
inside = !inside;
}
}
return inside;
}
function isPointInsidePolygon(point, roofLines) {
// 1. 먼저 경계선 위에 있는지 확인 (방향 무관)
if (isOnBoundaryDirectionIndependent(point, roofLines)) {
return true;
}
// 2. 내부/외부 판단 (기존 알고리즘)
let winding = 0;
const x = point.x;
const y = point.y;
for (let i = 0; i < roofLines.length; i++) {
const line = roofLines[i];
const x1 = line.x1, y1 = line.y1;
const x2 = line.x2, y2 = line.y2;
if (y1 <= y) {
if (y2 > y) {
const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
if (orientation > 0) winding++;
}
} else {
if (y2 <= y) {
const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
if (orientation < 0) winding--;
}
}
}
return winding !== 0;
}
// 방향에 무관한 경계선 검사
function isOnBoundaryDirectionIndependent(point, roofLines) {
const tolerance = 1e-10;
for (const line of roofLines) {
if (isPointOnLineSegmentDirectionIndependent(point, line, tolerance)) {
return true;
}
}
return false;
}
// 핵심: 방향에 무관한 선분 위 점 검사
function isPointOnLineSegmentDirectionIndependent(point, line, tolerance) {
const x = point.x, y = point.y;
const x1 = line.x1, y1 = line.y1;
const x2 = line.x2, y2 = line.y2;
// 방향에 무관하게 경계 상자 체크
const minX = Math.min(x1, x2);
const maxX = Math.max(x1, x2);
const minY = Math.min(y1, y2);
const maxY = Math.max(y1, y2);
if (x < minX - tolerance || x > maxX + tolerance ||
y < minY - tolerance || y > maxY + tolerance) {
return false;
}
// 외적을 이용한 직선 위 판단 (방향 무관)
const cross = (y - y1) * (x2 - x1) - (x - x1) * (y2 - y1);
return Math.abs(cross) < tolerance;
}
/**
* 선분 위의 점에 대한 매개변수 t를 계산합니다.
* p = p1 + t * (p2 - p1)에서 t 값을 구합니다.
* @param {Object} p1 - 선분의 시작점
* @param {Object} p2 - 선분의 끝점
* @param {Object} point - 선분 위의 점
* @returns {number} 매개변수 t (0이면 p1, 1이면 p2)
*/
function getParameterT(p1, p2, point) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
// x 좌표가 더 큰 변화를 보이면 x로 계산, 아니면 y로 계산
if (Math.abs(dx) > Math.abs(dy)) {
return dx === 0 ? 0 : (point.x - p1.x) / dx;
} else {
return dy === 0 ? 0 : (point.y - p1.y) / dy;
}
}
export const convertBaseLinesToPoints = (baseLines) => {
const points = [];
const pointSet = new Set();
baseLines.forEach((line) => {
[
{ x: line.x1, y: line.y1 },
{ x: line.x2, y: line.y2 }
].forEach(point => {
const key = `${point.x},${point.y}`;
if (!pointSet.has(key)) {
pointSet.add(key);
points.push(point);
}
});
});
return points;
};
function getLineDirection(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
// 각도 범위에 따라 방향 반환
if ((angle >= -45 && angle < 45)) return 'right';
if ((angle >= 45 && angle < 135)) return 'bottom';
if ((angle >= 135 || angle < -135)) return 'left';
return 'top'; // (-135 ~ -45)
}
/**
* 라인의 방향과 wall line에서 뻗어 들어가는지(내부로) 아니면 뻗어 나가는지(외부로)를 판단합니다.
* @param {Object} p1 - 라인의 시작점 {x, y}
* @param {Object} p2 - 라인의 끝점 {x, y}
* @param {Object} wall - wall 객체 (checkPointInPolygon 메서드 사용 가능)
* @returns {Object} {direction: string, orientation: 'inward'|'outward'|'unknown'}
*/
function getLineDirectionWithOrientation(p1, p2, wall) {
const direction = getLineDirection(p1, p2);
if (!wall) {
return { direction, orientation: 'unknown' };
}
// 라인의 중점과 방향 벡터 계산
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2;
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) {
return { direction, orientation: 'unknown' };
}
// 중점에서 라인 방향으로 약간 이동한 점이 wall 내부에 있는지 확인
const testOffset = 10;
const testPoint = {
x: midX + (dx / length) * testOffset,
y: midY + (dy / length) * testOffset
};
const isInside = checkPointInPolygon(testPoint, wall);
return {
direction,
orientation: isInside ? 'inward' : 'outward'
};
}
/**
* 점이 선분 위에 있는지 확인하는 헬퍼 함수
* @param {Object} point - 확인할 점 {x, y}
* @param {Object} lineStart - 선분의 시작점 {x, y}
* @param {Object} lineEnd - 선분의 끝점 {x, y}
* @param {number} epsilon - 허용 오차
* @returns {boolean}
*/
function isPointOnLineSegment(point, lineStart, lineEnd, epsilon = 0.1) {
const dx = lineEnd.x - lineStart.x;
const dy = lineEnd.y - lineStart.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) {
// 선분의 길이가 0이면 시작점과의 거리만 확인
return Math.abs(point.x - lineStart.x) < epsilon && Math.abs(point.y - lineStart.y) < epsilon;
}
// 점에서 선분의 시작점까지의 벡터
const toPoint = { x: point.x - lineStart.x, y: point.y - lineStart.y };
// 선분 방향으로의 투영 길이
const t = (toPoint.x * dx + toPoint.y * dy) / (length * length);
// t가 0과 1 사이에 있어야 선분 위에 있음
if (t < 0 || t > 1) {
return false;
}
// 선분 위의 가장 가까운 점
const closestPoint = {
x: lineStart.x + t * dx,
y: lineStart.y + t * dy
};
// 점과 가장 가까운 점 사이의 거리
const dist = Math.sqrt(
Math.pow(point.x - closestPoint.x, 2) +
Math.pow(point.y - closestPoint.y, 2)
);
return dist < epsilon;
}
// selectLine과 baseLines 비교하여 방향 찾기
function findLineDirection(selectLine, baseLines) {
for (const baseLine of baseLines) {
// baseLine의 시작점과 끝점
const baseStart = baseLine.startPoint;
const baseEnd = baseLine.endPoint;
// selectLine의 시작점과 끝점
const selectStart = selectLine.startPoint;
const selectEnd = selectLine.endPoint;
// 정방향 또는 역방향으로 일치하는지 확인
if ((isSamePoint(baseStart, selectStart) && isSamePoint(baseEnd, selectEnd)) ||
(isSamePoint(baseStart, selectEnd) && isSamePoint(baseEnd, selectStart))) {
// baseLine의 방향 계산
const dx = baseEnd.x - baseStart.x;
const dy = baseEnd.y - baseStart.y;
// 기울기를 바탕으로 방향 판단
if (Math.abs(dx) > Math.abs(dy)) {
return dx > 0 ? 'right' : 'left';
} else {
return dy > 0 ? 'down' : 'up';
}
}
}
return null; // 일치하는 라인이 없는 경우
}
/**
* outerLine의 방향에 따라 올바른 시작점과 끝점을 반환합니다.
* 예를 들어 왼쪽으로 진행하는 라인의 경우, x 좌표가 작은 쪽이 끝점, 큰 쪽이 시작점입니다.
* @param {Object} outerLine - QLine 객체
* @returns {Object} {startPoint: {x, y}, endPoint: {x, y}, direction: string}
*/
function getOuterLinePointsWithDirection(outerLine) {
const direction = getLineDirection(outerLine.startPoint, outerLine.endPoint);
let startPoint, endPoint;
switch (direction) {
case 'left':
// 왼쪽으로 진행: x 좌표가 큰 쪽이 시작점, 작은 쪽이 끝점
if (outerLine.startPoint.x > outerLine.endPoint.x) {
startPoint = outerLine.startPoint;
endPoint = outerLine.endPoint;
} else {
startPoint = outerLine.endPoint;
endPoint = outerLine.startPoint;
}
break;
case 'right':
// 오른쪽으로 진행: x 좌표가 작은 쪽이 시작점, 큰 쪽이 끝점
if (outerLine.startPoint.x < outerLine.endPoint.x) {
startPoint = outerLine.startPoint;
endPoint = outerLine.endPoint;
} else {
startPoint = outerLine.endPoint;
endPoint = outerLine.startPoint;
}
break;
case 'top':
// 위로 진행: y 좌표가 큰 쪽이 시작점, 작은 쪽이 끝점
if (outerLine.startPoint.y > outerLine.endPoint.y) {
startPoint = outerLine.startPoint;
endPoint = outerLine.endPoint;
} else {
startPoint = outerLine.endPoint;
endPoint = outerLine.startPoint;
}
break;
case 'bottom':
// 아래로 진행: y 좌표가 작은 쪽이 시작점, 큰 쪽이 끝점
if (outerLine.startPoint.y < outerLine.endPoint.y) {
startPoint = outerLine.startPoint;
endPoint = outerLine.endPoint;
} else {
startPoint = outerLine.endPoint;
endPoint = outerLine.startPoint;
}
break;
default:
// 기본값: 원래대로 반환
startPoint = outerLine.startPoint;
endPoint = outerLine.endPoint;
}
return { startPoint, endPoint, direction };
}
function getLinePositionRelativeToWall(selectLine, wall) {
// wall의 경계를 가져옵니다.
const bounds = wall.getBoundingRect();
const { left, top, width, height } = bounds;
const right = left + width;
const bottom = top + height;
// selectLine의 중간점을 계산합니다.
const midX = (selectLine.startPoint.x + selectLine.endPoint.x) / 2;
const midY = (selectLine.startPoint.y + selectLine.endPoint.y) / 2;
// 경계로부터의 거리를 계산합니다.
const distanceToLeft = Math.abs(midX - left);
const distanceToRight = Math.abs(midX - right);
const distanceToTop = Math.abs(midY - top);
const distanceToBottom = Math.abs(midY - bottom);
// 가장 가까운 경계를 찾습니다.
const minDistance = Math.min(
distanceToLeft,
distanceToRight,
distanceToTop,
distanceToBottom
);
// 가장 가까운 경계를 반환합니다.
if (minDistance === distanceToLeft) return 'left';
if (minDistance === distanceToRight) return 'right';
if (minDistance === distanceToTop) return 'top';
return 'bottom';
}
/**
* Convert a line into an array of coordinate points
* @param {Object} line - Line object with startPoint and endPoint
* @param {Object} line.startPoint - Start point with x, y coordinates
* @param {Object} line.endPoint - End point with x, y coordinates
* @param {number} [step=1] - Distance between points (default: 1)
* @returns {Array} Array of points [{x, y}, ...]
*/
function lineToPoints(line, step = 1) {
const { startPoint, endPoint } = line;
const points = [];
// Add start point
points.push({ x: startPoint.x, y: startPoint.y });
// Calculate distance between points
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const steps = Math.ceil(distance / step);
// Add intermediate points
for (let i = 1; i < steps; i++) {
const t = i / steps;
points.push({
x: startPoint.x + dx * t,
y: startPoint.y + dy * t
});
}
// Add end point
points.push({ x: endPoint.x, y: endPoint.y });
return points;
}
/**
* 다각형의 모든 좌표를 offset만큼 안쪽/바깥쪽으로 이동
* @param {Array} points - 다각형 좌표 배열 [{x, y}, ...]
* @param {number} offset - offset 값 (양수: 안쪽, 음수: 바깥쪽)
* @returns {Array} offset이 적용된 새로운 좌표 배열
*/
function offsetPolygon(points, offset) {
if (points.length < 3) return points;
const offsetPoints = [];
const numPoints = points.length;
for (let i = 0; i < numPoints; i++) {
const prevIndex = (i - 1 + numPoints) % numPoints;
const currentIndex = i;
const nextIndex = (i + 1) % numPoints;
const prevPoint = points[prevIndex];
const currentPoint = points[currentIndex];
const nextPoint = points[nextIndex];
// 이전 변의 방향 벡터
const prevVector = {
x: currentPoint.x - prevPoint.x,
y: currentPoint.y - prevPoint.y
};
// 다음 변의 방향 벡터
const nextVector = {
x: nextPoint.x - currentPoint.x,
y: nextPoint.y - currentPoint.y
};
// 정규화
const prevLength = Math.sqrt(prevVector.x * prevVector.x + prevVector.y * prevVector.y);
const nextLength = Math.sqrt(nextVector.x * nextVector.x + nextVector.y * nextVector.y);
if (prevLength === 0 || nextLength === 0) continue;
const prevNormal = {
x: -prevVector.y / prevLength,
y: prevVector.x / prevLength
};
const nextNormal = {
x: -nextVector.y / nextLength,
y: nextVector.x / nextLength
};
// 평균 법선 벡터 계산
const avgNormal = {
x: (prevNormal.x + nextNormal.x) / 2,
y: (prevNormal.y + nextNormal.y) / 2
};
// 평균 법선 벡터 정규화
const avgLength = Math.sqrt(avgNormal.x * avgNormal.x + avgNormal.y * avgNormal.y);
if (avgLength === 0) continue;
const normalizedAvg = {
x: avgNormal.x / avgLength,
y: avgNormal.y / avgLength
};
// 각도 보정 (예각일 때 offset 조정)
const cosAngle = prevNormal.x * nextNormal.x + prevNormal.y * nextNormal.y;
const adjustedOffset = Math.abs(cosAngle) > 0.1 ? offset / Math.abs(cosAngle) : offset;
// 새로운 점 계산
const offsetPoint = {
x: currentPoint.x + normalizedAvg.x * adjustedOffset,
y: currentPoint.y + normalizedAvg.y * adjustedOffset
};
offsetPoints.push(offsetPoint);
}
return offsetPoints;
}
/**
* baseLines를 연결하여 다각형 순서로 정렬된 점들 반환
* @param {Array} baseLines - 라인 배열
* @returns {Array} 순서대로 정렬된 점들의 배열
*/
function getOrderedBasePoints(baseLines) {
if (baseLines.length === 0) return [];
const points = [];
const usedLines = new Set();
// 첫 번째 라인으로 시작
let currentLine = baseLines[0];
points.push({ ...currentLine.startPoint });
points.push({ ...currentLine.endPoint });
usedLines.add(0);
let lastPoint = currentLine.endPoint;
// 연결된 라인들을 찾아가며 점들 수집
while (usedLines.size < baseLines.length) {
let foundNext = false;
for (let i = 0; i < baseLines.length; i++) {
if (usedLines.has(i)) continue;
const line = baseLines[i];
// 현재 끝점과 연결되는 라인 찾기
if (isSamePoint(lastPoint, line.startPoint)) {
points.push({ ...line.endPoint });
lastPoint = line.endPoint;
usedLines.add(i);
foundNext = true;
break;
} else if (isSamePoint(lastPoint, line.endPoint)) {
points.push({ ...line.startPoint });
lastPoint = line.startPoint;
usedLines.add(i);
foundNext = true;
break;
}
}
if (!foundNext) break; // 연결되지 않는 경우 중단
}
// 마지막 점이 첫 번째 점과 같으면 제거 (닫힌 다각형)
if (points.length > 2 && isSamePoint(points[0], points[points.length - 1])) {
points.pop();
}
return points;
}
/**
* roof.points와 baseLines가 정확히 대응되는 경우의 간단한 버전
*/
function createOrderedBasePoints(roofPoints, baseLines) {
const basePoints = [];
// baseLines에서 연결된 순서대로 점들을 추출
const orderedBasePoints = getOrderedBasePoints(baseLines);
// roofPoints의 개수와 맞추기
if (orderedBasePoints.length >= roofPoints.length) {
return orderedBasePoints.slice(0, roofPoints.length);
}
// 부족한 경우 roofPoints 기반으로 보완
roofPoints.forEach((roofPoint, index) => {
if (index < orderedBasePoints.length) {
basePoints.push(orderedBasePoints[index]);
} else {
basePoints.push({ ...roofPoint }); // fallback
}
});
return basePoints;
}
export const getSelectLinePosition = (wall, selectLine, options = {}) => {
const { testDistance = 10, epsilon = 0.5, debug = false } = options;
if (!wall || !selectLine) {
if (debug) console.log('ERROR: wall 또는 selectLine이 없음');
return { position: 'unknown', orientation: 'unknown', error: 'invalid_input' };
}
// selectLine의 좌표 추출
const lineCoords = extractLineCoords(selectLine);
if (!lineCoords.valid) {
if (debug) console.log('ERROR: selectLine 좌표가 유효하지 않음');
return { position: 'unknown', orientation: 'unknown', error: 'invalid_coords' };
}
const { x1, y1, x2, y2 } = lineCoords;
//console.log('wall.points', wall.baseLines);
for(const line of wall.baseLines) {
//console.log('line', line);
const basePoint = extractLineCoords(line);
const { x1: bx1, y1: by1, x2: bx2, y2: by2 } = basePoint;
//console.log('x1, y1, x2, y2', bx1, by1, bx2, by2);
// 객체 비교 대신 좌표값 비교
if (Math.abs(bx1 - x1) < 0.1 &&
Math.abs(by1 - y1) < 0.1 &&
Math.abs(bx2 - x2) < 0.1 &&
Math.abs(by2 - y2) < 0.1) {
//console.log('basePoint 일치!!!', basePoint);
}
}
// 라인 방향 분석
const lineInfo = analyzeLineOrientation(x1, y1, x2, y2, epsilon);
// if (debug) {
// console.log('=== getSelectLinePosition ===');
// console.log('selectLine 좌표:', lineCoords);
// console.log('라인 방향:', lineInfo.orientation);
// }
// 라인의 중점
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
let position = 'unknown';
if (lineInfo.orientation === 'horizontal') {
// 수평선: top 또는 bottom 판단
// 바로 위쪽 테스트 포인트
const topTestPoint = { x: midX, y: midY - testDistance };
// 바로 아래쪽 테스트 포인트
const bottomTestPoint = { x: midX, y: midY + testDistance };
const topIsInside = checkPointInPolygon(topTestPoint, wall);
const bottomIsInside = checkPointInPolygon(bottomTestPoint, wall);
// if (debug) {
// console.log('수평선 테스트:');
// console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside);
// console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside);
// }
// top 조건: 위쪽이 외부, 아래쪽이 내부
if (!topIsInside && bottomIsInside) {
position = 'top';
}
// bottom 조건: 위쪽이 내부, 아래쪽이 외부
else if (topIsInside && !bottomIsInside) {
position = 'bottom';
}
} else if (lineInfo.orientation === 'vertical') {
// 수직선: left 또는 right 판단
// 바로 왼쪽 테스트 포인트
const leftTestPoint = { x: midX - testDistance, y: midY };
// 바로 오른쪽 테스트 포인트
const rightTestPoint = { x: midX + testDistance, y: midY };
const leftIsInside = checkPointInPolygon(leftTestPoint, wall);
const rightIsInside = checkPointInPolygon(rightTestPoint, wall);
// if (debug) {
// console.log('수직선 테스트:');
// console.log(' 왼쪽 포인트:', leftTestPoint, '-> 내부:', leftIsInside);
// console.log(' 오른쪽 포인트:', rightTestPoint, '-> 내부:', rightIsInside);
// }
// left 조건: 왼쪽이 외부, 오른쪽이 내부
if (!leftIsInside && rightIsInside) {
position = 'left';
}
// right 조건: 오른쪽이 외부, 왼쪽이 내부
else if (leftIsInside && !rightIsInside) {
position = 'right';
}
} else {
// 대각선
if (debug) console.log('대각선은 지원하지 않음');
return { position: 'unknown', orientation: 'diagonal', error: 'not_supported' };
}
const result = {
position,
orientation: lineInfo.orientation,
method: 'inside_outside_test',
confidence: position !== 'unknown' ? 1.0 : 0.0,
testPoints: lineInfo.orientation === 'horizontal' ? {
top: { x: midX, y: midY - testDistance },
bottom: { x: midX, y: midY + testDistance }
} : {
left: { x: midX - testDistance, y: midY },
right: { x: midX + testDistance, y: midY }
},
midPoint: { x: midX, y: midY }
};
// if (debug) {
// console.log('최종 결과:', result);
// }
return result;
};
// 점이 다각형 내부에 있는지 확인하는 함수
const checkPointInPolygon = (point, wall) => {
// 2. wall.baseLines를 이용한 Ray Casting Algorithm
if (!wall.baseLines || !Array.isArray(wall.baseLines)) {
console.warn('wall.baseLines가 없습니다');
return false;
}
return raycastingAlgorithm(point, wall.baseLines);
};
// Ray Casting Algorithm 구현
const raycastingAlgorithm = (point, lines) => {
const { x, y } = point;
let intersectionCount = 0;
for (const line of lines) {
const coords = extractLineCoords(line);
if (!coords.valid) continue;
const { x1, y1, x2, y2 } = coords;
// Ray casting: 점에서 오른쪽으로 수평선을 그어서 다각형 경계와의 교점 개수를 셈
// 교점 개수가 홀수면 내부, 짝수면 외부
// 선분의 y 범위 확인
if ((y1 > y) !== (y2 > y)) {
// x 좌표에서의 교점 계산
const intersectX = (x2 - x1) * (y - y1) / (y2 - y1) + x1;
// 점의 오른쪽에 교점이 있으면 카운트
if (x < intersectX) {
intersectionCount++;
}
}
}
// 홀수면 내부, 짝수면 외부
return intersectionCount % 2 === 1;
};
// 라인 객체에서 좌표를 추출하는 헬퍼 함수 (중복 방지용 - 이미 있다면 제거)
const extractLineCoords = (line) => {
if (!line) {
return { x1: 0, y1: 0, x2: 0, y2: 0, valid: false };
}
let x1, y1, x2, y2;
// 다양한 라인 객체 형태에 대응
if (line.x1 !== undefined && line.y1 !== undefined &&
line.x2 !== undefined && line.y2 !== undefined) {
x1 = line.x1;
y1 = line.y1;
x2 = line.x2;
y2 = line.y2;
}
else if (line.startPoint && line.endPoint) {
x1 = line.startPoint.x;
y1 = line.startPoint.y;
x2 = line.endPoint.x;
y2 = line.endPoint.y;
}
else if (line.p1 && line.p2) {
x1 = line.p1.x;
y1 = line.p1.y;
x2 = line.p2.x;
y2 = line.p2.y;
}
else {
return { x1: 0, y1: 0, x2: 0, y2: 0, valid: false };
}
const coords = [x1, y1, x2, y2];
const valid = coords.every(coord =>
typeof coord === 'number' &&
!Number.isNaN(coord) &&
Number.isFinite(coord)
);
return { x1, y1, x2, y2, valid };
};
// 라인 방향 분석 함수 (중복 방지용 - 이미 있다면 제거)
const analyzeLineOrientation = (x1, y1, x2, y2, epsilon = 0.5) => {
const dx = x2 - x1;
const dy = y2 - y1;
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
const length = Math.sqrt(dx * dx + dy * dy);
let orientation;
if (absDy < epsilon && absDx >= epsilon) {
orientation = 'horizontal';
} else if (absDx < epsilon && absDy >= epsilon) {
orientation = 'vertical';
} else {
orientation = 'diagonal';
}
return {
orientation,
dx, dy, absDx, absDy, length,
midX: (x1 + x2) / 2,
midY: (y1 + y2) / 2,
isHorizontal: orientation === 'horizontal',
isVertical: orientation === 'vertical'
};
};
function extendLineToBoundary(p1, p2, roofLines) {
// 1. Calculate line direction and length
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) return { p1: { ...p1 }, p2: { ...p2 } };
// 2. Get all polygon points
const points = [];
const seen = new Set();
for (const line of roofLines) {
const p1 = { x: line.x1, y: line.y1 };
const p2 = { x: line.x2, y: line.y2 };
const key1 = `${p1.x},${p1.y}`;
const key2 = `${p2.x},${p2.y}`;
if (!seen.has(key1)) {
points.push(p1);
seen.add(key1);
}
if (!seen.has(key2)) {
points.push(p2);
seen.add(key2);
}
}
// 3. Find the bounding box
let minX = Infinity, minY = Infinity;
let maxX = -Infinity, maxY = -Infinity;
for (const p of points) {
minX = Math.min(minX, p.x);
minY = Math.min(minY, p.y);
maxX = Math.max(maxX, p.x);
maxY = Math.max(maxY, p.y);
}
// 4. Extend line to bounding box
const bboxLines = [
{ x1: minX, y1: minY, x2: maxX, y2: minY }, // top
{ x1: maxX, y1: minY, x2: maxX, y2: maxY }, // right
{ x1: maxX, y1: maxY, x2: minX, y2: maxY }, // bottom
{ x1: minX, y1: maxY, x2: minX, y2: minY } // left
];
const intersections = [];
// 5. Find intersections with bounding box
for (const line of bboxLines) {
const intersect = getLineIntersection(
p1, p2,
{ x: line.x1, y: line.y1 },
{ x: line.x2, y: line.y2 }
);
if (intersect) {
const t = ((intersect.x - p1.x) * dx + (intersect.y - p1.y) * dy) / (length * length);
if (t >= 0 && t <= 1) {
intersections.push({ x: intersect.x, y: intersect.y, t });
}
}
}
// 6. If we have two intersections, use them
if (intersections.length >= 2) {
// Sort by t value
intersections.sort((a, b) => a.t - b.t);
return {
p1: { x: intersections[0].x, y: intersections[0].y },
p2: {
x: intersections[intersections.length - 1].x,
y: intersections[intersections.length - 1].y
}
};
}
// 7. Fallback to original points
return { p1: { ...p1 }, p2: { ...p2 } };
}
/**
* 점에서 특정 방향으로 경계선과의 교차점을 찾습니다.
* @param {Object} point - 시작점 {x, y}
* @param {Object} direction - 방향 벡터 {x, y} (정규화된 값)
* @param {Array} roofLines - 지붕 경계선 배열
* @returns {Object|null} 교차점 {x, y} 또는 null
*/
function findBoundaryIntersection(point, direction, roofLines) {
let closestIntersection = null;
let minDistance = Infinity;
// 충분히 긴 거리로 광선 생성 (임의로 큰 값 사용)
const rayLength = 10000;
const rayEnd = {
x: point.x + direction.x * rayLength,
y: point.y + direction.y * rayLength
};
// 모든 경계선과의 교차점 확인
for (const line of roofLines) {
const lineP1 = { x: line.x1, y: line.y1 };
const lineP2 = { x: line.x2, y: line.y2 };
const intersection = getLineIntersection(point, rayEnd, lineP1, lineP2);
if (intersection) {
// 교차점까지의 거리 계산
const distance = Math.sqrt(
Math.pow(intersection.x - point.x, 2) +
Math.pow(intersection.y - point.y, 2)
);
// 가장 가까운 교차점 저장 (거리가 0보다 큰 경우만)
if (distance > 0.01 && distance < minDistance) {
minDistance = distance;
closestIntersection = intersection;
}
}
}
return closestIntersection;
}
/**
* 점이 다른 스켈레톤 라인과의 교점인지 확인합니다.
* @param {Object} point - 확인할 점 {x, y}
* @param {Array} skeletonLines - 모든 스켈레톤 라인 배열
* @param {Object} currentLine - 현재 라인 {p1, p2} (자기 자신 제외용)
* @param {number} tolerance - 허용 오차
* @returns {boolean} 교점이면 true
*/
function hasIntersectionWithOtherLines(point, skeletonLines, currentLine, tolerance = 0.5) {
if (!skeletonLines || skeletonLines.length === 0) {
return false;
}
let connectionCount = 0;
for (const line of skeletonLines) {
// 자기 자신과의 비교는 제외
if (line.p1 && line.p2 && currentLine.p1 && currentLine.p2) {
const isSameLineCheck =
(isSamePoint(line.p1, currentLine.p1, tolerance) && isSamePoint(line.p2, currentLine.p2, tolerance)) ||
(isSamePoint(line.p1, currentLine.p2, tolerance) && isSamePoint(line.p2, currentLine.p1, tolerance));
if (isSameLineCheck) continue;
}
// 다른 라인의 끝점이 현재 점과 일치하는지 확인
if (line.p1 && isSamePoint(point, line.p1, tolerance)) {
connectionCount++;
}
if (line.p2 && isSamePoint(point, line.p2, tolerance)) {
connectionCount++;
}
}
// 1개 이상의 다른 라인과 연결되어 있으면 교점으로 간주
return connectionCount >= 1;
}
function findClosestRoofLine(point, roofLines) {
let closestLine = null;
let minDistance = Infinity;
let roofLineIndex = 0;
let interPoint = null;
roofLines.forEach((roofLine, index) => {
const lineP1 = roofLine.startPoint;
const lineP2 = roofLine.endPoint;
// 점에서 선분까지의 최단 거리 계산
const distance = pointToLineDistance(point, lineP1, lineP2);
// 점에서 수직으로 내린 교점 계산
const intersection = getProjectionPoint(point, {
x1: lineP1.x,
y1: lineP1.y,
x2: lineP2.x,
y2: lineP2.y
});
if (distance < minDistance) {
minDistance = distance < 0.1 ? 0 : distance; //거리가 0.1보다 작으면 0으로 처리
closestLine = roofLine;
roofLineIndex = index
interPoint = intersection;
}
});
return { line: closestLine, distance: minDistance, index: roofLineIndex, intersectionPoint: interPoint };
}
// 점에서 선분까지의 최단 거리를 계산하는 도우미 함수
function pointToLineDistance(point, lineP1, lineP2) {
const A = point.x - lineP1.x;
const B = point.y - lineP1.y;
const C = lineP2.x - lineP1.x;
const D = lineP2.y - lineP1.y;
const dot = A * C + B * D;
const lenSq = C * C + D * D;
let param = -1;
if (lenSq !== 0) {
param = dot / lenSq;
}
let xx, yy;
if (param < 0) {
xx = lineP1.x;
yy = lineP1.y;
} else if (param > 1) {
xx = lineP2.x;
yy = lineP2.y;
} else {
xx = lineP1.x + param * C;
yy = lineP1.y + param * D;
}
const dx = point.x - xx;
const dy = point.y - yy;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Moves both p1 and p2 in the specified direction by a given distance
* @param {Object} p1 - The first point {x, y}
* @param {Object} p2 - The second point {x, y}
* @param {string} direction - Direction to move ('up', 'down', 'left', 'right')
* @param {number} distance - Distance to move
* @returns {Object} Object containing the new positions of p1 and p2
*/
function moveLineInDirection(p1, p2, direction, distance) {
// Create copies to avoid mutating the original points
const newP1 = { ...p1 };
const newP2 = { ...p2 };
const move = (point) => {
switch (direction.toLowerCase()) {
case 'up':
point.y -= distance;
break;
case 'down':
point.y += distance;
break;
case 'left':
point.x -= distance;
break;
case 'right':
point.x += distance;
break;
default:
throw new Error('Invalid direction. Use "up", "down", "left", or "right"');
}
return point;
};
return {
p1: move(newP1),
p2: move(newP2)
};
}
/**
* Determines the direction and distance between original points (p1, p2) and moved points (newP1, newP2)
* @param {Object} p1 - Original first point {x, y}
* @param {Object} p2 - Original second point {x, y}
* @param {Object} newP1 - Moved first point {x, y}
* @param {Object} newP2 - Moved second point {x, y}
* @returns {Object} Object containing direction and distance of movement
*/
function getMovementInfo(p1, p2, newP1, newP2) {
// Calculate the movement vector for both points
const dx1 = newP1.x - p1.x;
const dy1 = newP1.y - p1.y;
const dx2 = newP2.x - p2.x;
const dy2 = newP2.y - p2.y;
// Verify that both points moved by the same amount
if (dx1 !== dx2 || dy1 !== dy2) {
throw new Error('Points did not move in parallel');
}
// Determine the primary direction of movement
let direction;
const absDx = Math.abs(dx1);
const absDy = Math.abs(dy1);
if (absDx > absDy) {
// Horizontal movement is dominant
direction = dx1 > 0 ? 'right' : 'left';
} else {
// Vertical movement is dominant
direction = dy1 > 0 ? 'down' : 'up';
}
// Calculate the actual distance moved
const distance = Math.sqrt(dx1 * dx1 + dy1 * dy1);
return {
direction,
distance,
dx: dx1,
dy: dy1
};
}
function getLineAngleDirection(line, isLeftSide = true) {
const dx = line.x2 - line.x1;
const dy = line.y2 - line.y1;
// 수평선인 경우 (y 좌표가 거의 같은 경우)
if (Math.abs(dy) < 0.1) {
// x 좌표 비교로 좌우 방향 결정
return line.x2 > line.x1 ? 'right' : 'left';
}
// 수직선인 경우 (x 좌표가 거의 같은 경우)
if (Math.abs(dx) < 0.1) {
// y 좌표 비교로 상하 방향 결정
return line.y2 > line.y1 ? 'down' : 'up';
}
// 대각선의 경우 기존 로직 유지
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
const normalizedAngle = (angle + 360) % 360;
if (normalizedAngle >= 45 && normalizedAngle < 135) {
return 'up';
} else if (normalizedAngle >= 135 && normalizedAngle < 225) {
return 'left';
} else if (normalizedAngle >= 225 && normalizedAngle < 315) {
return 'down';
} else {
return 'right';
}
}
function findRoofLineIndex(roof, p1, p2) {
if (!roof || !roof.lines || !Array.isArray(roof.lines)) {
console.error("Invalid roof object or lines array");
return -1;
}
// Create a tolerance for floating point comparison
const TOLERANCE = 0.1;
// Try to find a line that matches either (p1,p2) or (p2,p1)
const index = roof.lines.findIndex(line => {
// Check if points match in order
const matchOrder =
(Math.abs(line.x1 - p1.x) < TOLERANCE &&
Math.abs(line.y1 - p1.y) < TOLERANCE &&
Math.abs(line.x2 - p2.x) < TOLERANCE &&
Math.abs(line.y2 - p2.y) < TOLERANCE);
// Check if points match in reverse order
const matchReverse =
(Math.abs(line.x1 - p2.x) < TOLERANCE &&
Math.abs(line.y1 - p2.y) < TOLERANCE &&
Math.abs(line.x2 - p1.x) < TOLERANCE &&
Math.abs(line.y2 - p1.y) < TOLERANCE);
return matchOrder || matchReverse;
});
if (index === -1) {
console.warn("No matching roof line found for points:", p1, p2);
}
return index;
}
function findClosestParallelLine(roofLine, currentRoofLines) {
// Determine if the line is horizontal or vertical
const isHorizontal = Math.abs(roofLine.y2 - roofLine.y1) < 0.001; // Using a small threshold for floating point comparison
const isVertical = Math.abs(roofLine.x2 - roofLine.x1) < 0.001;
if (!isHorizontal && !isVertical) {
console.warn('Line is neither perfectly horizontal nor vertical');
return null;
}
// Calculate the reference point (midpoint of the line)
const refX = (roofLine.x1 + roofLine.x2) / 2;
const refY = (roofLine.y1 + roofLine.y2) / 2;
let closestLine = null;
let minDistance = Infinity;
currentRoofLines.forEach(line => {
// Skip the same line
if (line === roofLine) return;
// Check if the line is parallel (same orientation)
const lineIsHorizontal = Math.abs(line.y2 - line.y1) < 0.001;
const lineIsVertical = Math.abs(line.x2 - line.x1) < 0.001;
if ((isHorizontal && lineIsHorizontal) || (isVertical && lineIsVertical)) {
// Calculate midpoint of the current line
const lineMidX = (line.x1 + line.x2) / 2;
const lineMidY = (line.y1 + line.y2) / 2;
// Calculate distance between midpoints
const distance = Math.sqrt(
Math.pow(lineMidX - refX, 2) +
Math.pow(lineMidY - refY, 2)
);
// Update closest line if this one is closer
if (distance < minDistance) {
minDistance = distance;
closestLine = line;
}
}
});
return closestLine;
}
function doLinesIntersect(line1, line2) {
const x1 = line1.x1, y1 = line1.y1;
const x2 = line1.x2, y2 = line1.y2;
const x3 = line2.x1, y3 = line2.y1;
const x4 = line2.x2, y4 = line2.y2;
// Calculate the direction of the lines
const uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
const uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
// If uA and uB are between 0-1, lines are colliding
return (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1);
}
const getOrientation = (line, eps = 0.1) => {
const x1 = line.get('x1')
const y1 = line.get('y1')
const x2 = line.get('x2')
const y2 = line.get('y2')
const dx = Math.abs(x2 - x1)
const dy = Math.abs(y2 - y1)
if (dx < eps && dy >= eps) return 'vertical'
if (dy < eps && dx >= eps) return 'horizontal'
if (dx < eps && dy < eps) return 'point'
return 'diagonal'
}
/**
* 두 선분이 교차하는지 확인하는 헬퍼 함수
* (끝점이 닿아있는 경우도 교차로 간주)
*/
function checkIntersection(p1, p2, p3, p4) {
// CCW (Counter Clockwise) 알고리즘을 이용한 교차 판별
function ccw(a, b, c) {
const val = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
if (val < 0) return -1;
if (val > 0) return 1;
return 0;
}
const abc = ccw(p1, p2, p3);
const abd = ccw(p1, p2, p4);
const cda = ccw(p3, p4, p1);
const cdb = ccw(p3, p4, p2);
// 두 선분이 일직선 상에 있을 때 (겹치는지 확인)
if (abc === 0 && abd === 0) {
// x축, y축 순서대로 정렬하여 겹침 여부 확인
if (p1.x > p2.x || (p1.x === p2.x && p1.y > p2.y)) [p1, p2] = [p2, p1];
if (p3.x > p4.x || (p3.x === p4.x && p3.y > p4.y)) [p3, p4] = [p4, p3];
return p2.x >= p3.x && p2.y >= p3.y && p4.x >= p1.x && p4.y >= p1.y;
}
return abc * abd <= 0 && cda * cdb <= 0;
}
/**
* aLine의 좌표를 추출하는 함수
*/
function getACoords(line) {
return {
start: { x: line.newPStart.x, y: line.newPStart.y },
end: { x: line.newPEnd.x, y: line.newPEnd.y }
};
}
/**
* bLine의 좌표를 추출하는 함수
* (left, top을 시작점으로 보고 width, height를 더해 끝점을 계산)
*/
function getBCoords(line) {
// QLine 데이터 구조상 left/top이 시작점, width/height가 델타값으로 가정
return {
start: { x: line.left, y: line.top },
end: { x: line.left + line.width, y: line.top + line.height }
};
}
/**
* 메인 로직 함수
* 1. aLines 순회
* 2. aLine과 교차하는 bLines 찾기 (Level 1)
* 3. 찾은 bLine과 교차하는 또 다른 bLines 찾기 (Level 2)
*/
function findConnectedLines(aLines, bLines, canvas, roofId, roof) {
const results = [];
aLines.forEach(aLine => {
const aCoords = getACoords(aLine);
const intersections = [];
// 1단계: aLine과 교차하는 bLines 찾기
bLines.forEach(bLine1 => {
const bCoords1 = getBCoords(bLine1);
if (checkIntersection(aCoords.start, aCoords.end, bCoords1.start, bCoords1.end)) {
// 2단계: 위에서 찾은 bLine1과 교차하는 다른 bLines 찾기
const connectedToB1 = [];
bLines.forEach(bLine2 => {
// 자기 자신은 제외
if (bLine1 === bLine2) return;
const bCoords2 = getBCoords(bLine2);
if (checkIntersection(bCoords1.start, bCoords1.end, bCoords2.start, bCoords2.end)) {
connectedToB1.push(bLine2);
let testLine = new QLine([bLine2.x1, bLine2.y1, bLine2.x2, bLine2.y2], {
stroke: 'orange',
strokeWidth: 10,
property: 'normal',
fontSize: 14,
lineName: 'helpLine',
roofId:roofId,
parentId: roof.id,
});
canvas.add(testLine)
}
});
intersections.push({
targetBLine: bLine1, // aLine과 만난 녀석
connectedBLines: connectedToB1 // 그 녀석과 만난 다른 bLines
});
}
});
// 결과가 있는 경우에만 저장 (필요에 따라 조건 제거 가능)
if (intersections.length > 0) {
results.push({
sourceALine: aLine,
intersections: intersections
});
}
});
return results;
}
export const processEaveHelpLines = (lines) => {
if (!lines || lines.length === 0) return [];
// 수직/수평 라인 분류 (부동소수점 오차 고려)
const verticalLines = lines.filter(line => Math.abs(line.x1 - line.x2) < 0.1);
const horizontalLines = lines.filter(line => Math.abs(line.y1 - line.y2) < 0.1);
// 라인 병합 (더 엄격한 조건으로)
const mergedVertical = mergeLines(verticalLines, 'vertical');
const mergedHorizontal = mergeLines(horizontalLines, 'horizontal');
// 결과 확인용 로그
console.log('Original lines:', lines.length);
console.log('Merged vertical:', mergedVertical.length);
console.log('Merged horizontal:', mergedHorizontal.length);
return [...mergedVertical, ...mergedHorizontal];
};
const mergeLines = (lines, direction) => {
if (!lines || lines.length < 2) return lines || [];
// 방향에 따라 정렬 (수직: y1 기준, 수평: x1 기준)
lines.sort((a, b) => {
const aPos = direction === 'vertical' ? a.y1 : a.x1;
const bPos = direction === 'vertical' ? b.y1 : b.x1;
return aPos - bPos;
});
const merged = [];
let current = { ...lines[0] };
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
// 같은 선상에 있는지 확인 (부동소수점 오차 고려)
const isSameLine = direction === 'vertical'
? Math.abs(current.x1 - line.x1) < 0.1
: Math.abs(current.y1 - line.y1) < 0.1;
// 연결 가능한지 확인 (약간의 겹침 허용)
const isConnected = direction === 'vertical'
? current.y2 + 0.1 >= line.y1 // 약간의 오차 허용
: current.x2 + 0.1 >= line.x1;
if (isSameLine && isConnected) {
// 라인 병합
current.y2 = Math.max(current.y2, line.y2);
current.x2 = direction === 'vertical' ? current.x1 : current.x2;
} else {
merged.push(current);
current = { ...line };
}
}
merged.push(current);
// 병합 결과 로그
console.log(`Merged ${direction} lines:`, merged);
return merged;
};
function mergeMovedLines(movedLines) {
if (!movedLines || movedLines.length < 2) return movedLines;
const result = [...movedLines]; // Start with all original lines
const processed = new Set();
// First pass: find and merge connected lines
for (let i = 0; i < result.length; i++) {
if (processed.has(i)) continue;
for (let j = i + 1; j < result.length; j++) {
if (processed.has(j)) continue;
const line1 = result[i];
const line2 = result[j];
// Skip if lines are not the same type (vertical/horizontal)
const line1Type = getLineType(line1);
const line2Type = getLineType(line2);
if (line1Type !== line2Type) continue;
if (areLinesConnected(line1, line2, line1Type)) {
// Merge the lines
const merged = mergeTwoLines(line1, line2, line1Type);
// Replace the first line with merged result
result[i] = merged;
// Mark the second line for removal
processed.add(j);
}
}
}
// Remove processed lines and keep the order
return result.filter((_, index) => !processed.has(index));
}
function getLineType(line) {
if (Math.abs(line.p1.x - line.p2.x) < 0.1) return 'vertical';
if (Math.abs(line.p1.y - line.p2.y) < 0.1) return 'horizontal';
return 'other';
}
function areLinesConnected(line1, line2, type) {
if (type === 'vertical') {
// For vertical lines, check if x coordinates are the same and y ranges overlap
return Math.abs(line1.p1.x - line2.p1.x) < 0.1 &&
Math.min(line1.p2.y, line2.p2.y) >= Math.max(line1.p1.y, line2.p1.y) - 0.1;
} else if (type === 'horizontal') {
// For horizontal lines, check if y coordinates are the same and x ranges overlap
return Math.abs(line1.p1.y - line2.p1.y) < 0.1 &&
Math.min(line1.p2.x, line2.p2.x) >= Math.max(line1.p1.x, line2.p1.x) - 0.1;
}
return false;
}
function mergeTwoLines(line1, line2, type) {
if (type === 'vertical') {
return {
...line1, // Preserve original properties
p1: {
x: line1.p1.x,
y: Math.min(line1.p1.y, line1.p2.y, line2.p1.y, line2.p2.y)
},
p2: {
x: line1.p1.x,
y: Math.max(line1.p1.y, line1.p2.y, line2.p1.y, line2.p2.y)
}
};
} else { // horizontal
return {
...line1, // Preserve original properties
p1: {
x: Math.min(line1.p1.x, line1.p2.x, line2.p1.x, line2.p2.x),
y: line1.p1.y
},
p2: {
x: Math.max(line1.p1.x, line1.p2.x, line2.p1.x, line2.p2.x),
y: line1.p1.y
}
};
}
}
/**
* Adjusts line points based on movement type and orientation
* @param {Object} params - Configuration object
* @param {Object} params.roofLine - The original roof line
* @param {Object} params.currentRoofLine - The current roof line after movement
* @param {Object} params.wallBaseLine - The wall base line
* @param {Object} params.origin - The original position before movement
* @param {string} params.moveType - Type of movement: 'start' | 'end' | 'both'
* @returns {{newPStart: {x: number, y: number}, newPEnd: {x: number, y: number}}}
*/
function adjustLinePoints({ roofLine, currentRoofLine, wallBaseLine, origin, moveType }) {
const isHorizontal = getOrientation(roofLine) === 'horizontal';
const isVertical = !isHorizontal;
// Initialize points
let newPStart = { x: roofLine.x1, y: roofLine.y1 };
let newPEnd = { x: roofLine.x2, y: roofLine.y2 };
// Check if lines cross (same as original isCross logic)
let isCross = false;
if (isVertical) {
isCross = Math.abs(currentRoofLine.x2 - roofLine.x1) < 0.1 ||
Math.abs(currentRoofLine.x1 - roofLine.x2) < 0.1;
} else {
isCross = Math.abs(currentRoofLine.y1 - roofLine.y2) < 0.1 ||
Math.abs(currentRoofLine.y2 - roofLine.y1) < 0.1;
}
// Determine which points to adjust
const adjustStart = moveType === 'start' || moveType === 'both';
const adjustEnd = moveType === 'end' || moveType === 'both';
if (isVertical) {
// Vertical line adjustments
if (adjustStart) {
newPStart = {
x: roofLine.x1,
y: isCross ? currentRoofLine.y1 : wallBaseLine.y1
};
}
if (adjustEnd) {
newPEnd = {
x: roofLine.x2,
y: isCross ? currentRoofLine.y2 : wallBaseLine.y2
};
}
} else {
// Horizontal line adjustments
if (adjustStart) {
newPStart = {
y: roofLine.y1,
x: isCross ? currentRoofLine.x1 : wallBaseLine.x1
};
}
if (adjustEnd) {
newPEnd = {
y: roofLine.y2,
x: isCross ? currentRoofLine.x2 : wallBaseLine.x2
};
}
}
return { newPStart, newPEnd };
}