3494 lines
134 KiB
JavaScript
3494 lines
134 KiB
JavaScript
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
|
import { SkeletonBuilder } from '@/lib/skeletons'
|
|
import { calcLineActualSize, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils'
|
|
import { QLine } from '@/components/fabric/QLine'
|
|
import { getDegreeByChon } from '@/util/canvas-util'
|
|
import Big from 'big.js'
|
|
import { QPolygon } from '@/components/fabric/QPolygon'
|
|
import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine'
|
|
|
|
/**
|
|
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다.
|
|
* @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 moveUpDownLength = 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}
|
|
// 원본 라인 업데이트
|
|
// newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트
|
|
console.log('absMove::', moveUpDownLength);
|
|
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(moveUpDownLength).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(moveUpDownLength).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(moveUpDownLength).toNumber();
|
|
}
|
|
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
|
|
point.y = Big(point.y).plus(moveUpDownLength).toNumber();
|
|
}
|
|
}else if(moveDirection === 'out'){
|
|
if(isSamePoint(roof.basePoints[index], originalStartPoint)) {
|
|
point.y = Big(point.y).minus(moveUpDownLength).toNumber();
|
|
}
|
|
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
|
|
point.y = Big(point.y).minus(moveUpDownLength).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(moveUpDownLength).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(moveUpDownLength).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(moveUpDownLength).toNumber();
|
|
}
|
|
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
|
|
point.x = Big(point.x).minus(moveUpDownLength).toNumber();
|
|
}
|
|
}else if(moveDirection === 'out'){
|
|
if(isSamePoint(roof.basePoints[index], originalStartPoint)) {
|
|
point.x = Big(point.x).plus(moveUpDownLength).toNumber();
|
|
}
|
|
if (isSamePoint(roof.basePoints[index], originalEndPoint)) {
|
|
point.x = Big(point.x).plus(moveUpDownLength).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)
|
|
|
|
// [추가] wall 객체를 찾아 roof.lines에 wallId를 직접 주입 (초기화)
|
|
// 지붕은 벽을 기반으로 생성되므로 라인의 순서(Index)가 동일합니다.
|
|
const wallObj = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId)
|
|
|
|
if (roof && wallObj && roof.lines && wallObj.lines) {
|
|
// 개선된 코드 (기하학적 매칭)
|
|
// or use some other unique properties
|
|
|
|
|
|
roof.lines.forEach((rLine, index) => {
|
|
// 벽 라인 중에서 시작점과 끝점이 일치하는 라인 찾기
|
|
const wLine = wallObj.lines[index]
|
|
if (wLine) {
|
|
// 안정적인 ID 생성
|
|
rLine.attributes.wallLine = wLine.id; // Use the stable ID
|
|
|
|
// ...
|
|
}
|
|
})
|
|
}
|
|
|
|
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 []
|
|
const roof = canvas?.getObjects().find((object) => object.id === roofId)
|
|
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
|
|
let skeletonLines = []
|
|
let findPoints = [];
|
|
|
|
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: 'magenta',
|
|
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. 연결이 끊어진 라인이 있을경우 찾아서 추가한다(동 이동일때)
|
|
|
|
// 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 }
|
|
);
|
|
|
|
|
|
|
|
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 (Math.abs(roof.moveUpDown ?? 0) > 0 || Math.abs(roof.moveFlowLine ?? 0) > 0) {
|
|
const getMoveUpDownLine = () => {
|
|
// 같은 라인이 없으므로 새 다각형 라인 생성
|
|
//라인 편집
|
|
// 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
|
|
})
|
|
}
|
|
|
|
// 각 라인 집합 정렬
|
|
const sortWallLines = ensureCounterClockwiseLines(wallLines)
|
|
const sortWallBaseLines = ensureCounterClockwiseLines(wall.baseLines)
|
|
const sortRoofLines = ensureCounterClockwiseLines(roofLines)
|
|
|
|
// 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
|
|
})
|
|
}
|
|
|
|
console.log('wallBaseLines', wall.baseLines)
|
|
|
|
//wall.baseLine은 움직인라인
|
|
let movedLines = []
|
|
|
|
// 조건에 맞는 라인들만 필터링
|
|
const validWallLines = [...wallLines].sort((a, b) => a.idx - b.idx).filter((wallLine, index) => wallLine.idx - 1 === index)
|
|
|
|
console.log('', sortRoofLines, sortWallLines, sortWallBaseLines)
|
|
sortWallLines.length > 3 &&
|
|
sortWallLines.forEach((wallLine, index) => {
|
|
|
|
const roofLine = sortRoofLines[index]
|
|
const wallBaseLine = sortWallBaseLines[index]
|
|
|
|
//roofline 외곽선 설정
|
|
|
|
console.log('index::::', index)
|
|
console.log('roofLine:', roofLine.x1, roofLine.y1, roofLine.x2, roofLine.y2)
|
|
console.log('wallLine:', wallLine.x1, wallLine.y1, wallLine.x2, wallLine.y2)
|
|
console.log('wallBaseLine:', wallBaseLine.x1, wallBaseLine.y1, wallBaseLine.x2, wallBaseLine.y2)
|
|
console.log('isSamePoint result:', isSameLine2(wallBaseLine, wallLine))
|
|
|
|
if (isSameLine2(wallBaseLine, wallLine)) {
|
|
return
|
|
}
|
|
|
|
const movedStart = Math.abs(wallBaseLine.x1 - wallLine.x1) > EPSILON || Math.abs(wallBaseLine.y1 - wallLine.y1) > EPSILON
|
|
const movedEnd = Math.abs(wallBaseLine.x2 - wallLine.x2) > EPSILON || Math.abs(wallBaseLine.y2 - wallLine.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 = '') => {
|
|
movedLines.push({ index, p1, p2 })
|
|
|
|
//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',
|
|
visible: true,
|
|
roofId: roofId,
|
|
selectable: true,
|
|
hoverCursor: 'pointer',
|
|
attributes: {
|
|
type: 'eaveHelpLine',
|
|
isStart: true,
|
|
pitch: wallLine.attributes.pitch,
|
|
},
|
|
})
|
|
|
|
coordinateText(line)
|
|
canvas.add(line)
|
|
line.bringToFront()
|
|
canvas.renderAll()
|
|
return line
|
|
}
|
|
|
|
//getAddLine(roofLine.startPoint, roofLine.endPoint, ) //외곽선을 그린다
|
|
|
|
newPStart = { x: roofLine.x1, y: roofLine.y1 }
|
|
newPEnd = { x: roofLine.x2, y: roofLine.y2 }
|
|
|
|
const getInnerLines = (lines, point) => {}
|
|
let isIn = false
|
|
let isOut = false
|
|
|
|
//두 포인트가 변경된 라인인
|
|
if (fullyMoved) {
|
|
//반시계방향향
|
|
|
|
const mLine = getSelectLinePosition(wall, wallBaseLine)
|
|
|
|
if (getOrientation(roofLine) === 'vertical') {
|
|
if (['left', 'right'].includes(mLine.position)) {
|
|
if (Math.abs(wallLine.x1 - wallBaseLine.x1) < 0.1 || Math.abs(wallLine.x2 - wallBaseLine.x2) < 0.1) {
|
|
return false
|
|
}
|
|
const isLeftPosition = mLine.position === 'left'
|
|
const isRightPosition = mLine.position === 'right'
|
|
const isInPosition =
|
|
(isLeftPosition && wallLine.x1 < wallBaseLine.x1) ||
|
|
(isRightPosition && wallLine.x1 > wallBaseLine.x1) ||
|
|
(isLeftPosition && wallLine.x2 < wallBaseLine.x2) ||
|
|
(isRightPosition && wallLine.x2 > wallBaseLine.x2)
|
|
|
|
const positionType = isInPosition ? 'in' : 'out'
|
|
|
|
const condition = `${mLine.position}_${positionType}`
|
|
let isStartEnd = findInteriorPoint(wallBaseLine, sortWallBaseLines)
|
|
let sPoint, ePoint
|
|
if (condition === 'left_in') {
|
|
isIn = true
|
|
|
|
if (isStartEnd.start) {
|
|
newPEnd.y = roofLine.y2
|
|
newPEnd.x = roofLine.x2
|
|
|
|
const moveDist = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber()
|
|
ePoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }
|
|
newPStart.y = wallBaseLine.y1
|
|
|
|
findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_start' })
|
|
const newPointX = Big(roofLine.x1).plus(moveDist).abs().toNumber()
|
|
const pDist = Big(wallLine.x1).minus(roofLine.x1).toNumber()
|
|
const pLineY = Big(roofLine.y1).minus(0).abs().toNumber()
|
|
// let idx = 0 > index - 1 ? sortRoofLines.length : index
|
|
// const pLineX = sortRoofLines[idx - 1].x1
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineX = sortRoofLines[prevIndex].x1
|
|
|
|
getAddLine({ x: newPStart.x, y: newPStart.y }, { x: ePoint.x, y: ePoint.y }, 'blue')
|
|
getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange')
|
|
|
|
if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green')
|
|
getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink')
|
|
}
|
|
}
|
|
|
|
if (isStartEnd.end) {
|
|
newPStart.y = roofLine.y1
|
|
newPStart.x = roofLine.x1
|
|
|
|
const moveDist = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber()
|
|
ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 }
|
|
newPEnd.y = wallBaseLine.y2
|
|
|
|
findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_end' })
|
|
const newPointX = Big(roofLine.x1).plus(moveDist).toNumber()
|
|
const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber()
|
|
const pLineY = Big(roofLine.y2).minus(0).toNumber()
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const pLineX = sortRoofLines[idx + 1].x2
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineX = sortRoofLines[nextIndex].x2
|
|
|
|
getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue')
|
|
getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange')
|
|
|
|
if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green')
|
|
getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink')
|
|
}
|
|
//getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange')
|
|
}
|
|
} else if (condition === 'left_out') {
|
|
console.log('left_out::::isStartEnd:::::', isStartEnd)
|
|
if (isStartEnd.start) {
|
|
const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
|
|
const aStartY = Big(roofLine.y1).minus(moveDist).abs().toNumber()
|
|
const bStartY = Big(wallLine.y1).minus(moveDist).abs().toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x2 })
|
|
|
|
const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber()
|
|
newPStart.y = aStartY
|
|
newPEnd.y = roofLine.y2 //Big(roofLine.y2).minus(eLineY).toNumber()
|
|
// let idx = 0 >= index - 1 ? sortRoofLines.length : index
|
|
// const newLine = sortRoofLines[idx - 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const newLine = sortRoofLines[nextIndex]
|
|
|
|
if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) {
|
|
if (inLine) {
|
|
if (inLine.x1 < inLine.x2) {
|
|
getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink')
|
|
} else {
|
|
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: bStartY, x: wallLine.x2 }, 'pink')
|
|
}
|
|
}
|
|
getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta')
|
|
getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray')
|
|
findPoints.push({ y: aStartY, x: newPStart.x, position: 'left_out_start' })
|
|
} else {
|
|
const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber()
|
|
newPStart.y = Big(newPStart.y).minus(cLineY).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.x1 < inLine.x2) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple')
|
|
}
|
|
} else {
|
|
//newPStart.y = wallLine.y1;
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber()
|
|
newPStart.y = Big(wallBaseLine.y1).minus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.x2 > inLine.x1) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isStartEnd.end) {
|
|
const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
|
|
const aStartY = Big(roofLine.y2).plus(moveDist).toNumber()
|
|
const bStartY = Big(wallLine.y2).plus(moveDist).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 })
|
|
console.log('startLines:::::::', inLine)
|
|
const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber()
|
|
newPEnd.y = aStartY
|
|
newPStart.y = roofLine.y1 //Big(roofLine.y1).plus(eLineY).toNumber()
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const newLine = sortRoofLines[idx + 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length;
|
|
const nextIndex = (index + 1) % sortRoofLines.length;
|
|
const newLine = sortRoofLines[prevIndex]
|
|
|
|
|
|
if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) {
|
|
if (inLine) {
|
|
if (inLine.x1 < inLine.x2) {
|
|
getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: bStartY, x: wallLine.x1 }, 'pink')
|
|
}
|
|
}
|
|
getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta')
|
|
getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray')
|
|
findPoints.push({ y: aStartY, x: newPEnd.x, position: 'left_out_end' })
|
|
} else {
|
|
const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber()
|
|
newPEnd.y = Big(newPEnd.y).plus(cLineY).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.x1 < inLine.x2) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
} else {
|
|
// newPEnd.y = wallLine.y2
|
|
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber()
|
|
newPEnd.y = Big(wallBaseLine.y2).plus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.x2 > inLine.x1) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
findPoints.push({ y: newPStart.y, x: newPEnd.x, position: 'left_out_end' })
|
|
}
|
|
} else if (condition === 'right_in') {
|
|
if (isStartEnd.start) {
|
|
newPEnd.y = roofLine.y2
|
|
newPEnd.x = roofLine.x2
|
|
|
|
const moveDist = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber()
|
|
ePoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }
|
|
newPStart.y = wallBaseLine.y1
|
|
|
|
findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_start' })
|
|
const newPointX = Big(roofLine.x1).minus(moveDist).abs().toNumber()
|
|
const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber()
|
|
const pLineY = Big(roofLine.y1).minus(0).abs().toNumber()
|
|
// let idx = 0 >= index - 1 ? sortRoofLines.length : index
|
|
// const pLineX = sortRoofLines[idx - 1].x1
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineX = sortRoofLines[prevIndex].x1
|
|
|
|
getAddLine({ x: newPStart.x, y: newPStart.y }, { x: ePoint.x, y: ePoint.y }, 'blue')
|
|
//getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange')
|
|
|
|
if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green')
|
|
getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink')
|
|
}
|
|
}
|
|
|
|
if (isStartEnd.end) {
|
|
newPStart.y = roofLine.y1
|
|
newPStart.x = roofLine.x1
|
|
|
|
const moveDist = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber()
|
|
ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 }
|
|
newPEnd.y = wallBaseLine.y2
|
|
|
|
findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_end' })
|
|
const newPointX = Big(roofLine.x1).minus(moveDist).toNumber()
|
|
const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber()
|
|
const pLineY = Big(roofLine.y2).minus(0).abs().toNumber()
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const pLineX = sortRoofLines[idx + 1].x2
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineX = sortRoofLines[nextIndex].x2
|
|
|
|
getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue')
|
|
getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange')
|
|
|
|
if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green')
|
|
getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink')
|
|
}
|
|
getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange')
|
|
}
|
|
} else if (condition === 'right_out') {
|
|
console.log('right_out::::isStartEnd:::::', isStartEnd)
|
|
if (isStartEnd.start) {
|
|
//x1 inside
|
|
const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
|
|
const aStartY = Big(roofLine.y1).plus(moveDist).abs().toNumber()
|
|
const bStartY = Big(wallLine.y1).plus(moveDist).abs().toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 })
|
|
console.log('startLines:::::::', inLine)
|
|
const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber()
|
|
newPStart.y = aStartY
|
|
newPEnd.y = roofLine.y2 //Big(roofLine.y2).plus(eLineY).toNumber()
|
|
// let idx = 0 >= index - 1 ? sortRoofLines.length : index
|
|
// const newLine = sortRoofLines[idx - 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const newLine = sortRoofLines[nextIndex]
|
|
|
|
if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) {
|
|
if (inLine) {
|
|
if (inLine.x2 < inLine.x1) {
|
|
getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: bStartY, x: wallLine.x2 }, 'pink')
|
|
}
|
|
}
|
|
getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta')
|
|
getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray')
|
|
findPoints.push({ y: aStartY, x: newPEnd.x, position: 'right_out_start' })
|
|
} else {
|
|
const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber()
|
|
newPStart.y = Big(newPStart.y).plus(cLineY).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.x2 < inLine.x1) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple')
|
|
}
|
|
} else {
|
|
//newPStart.y = wallLine.y1;
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.x1).minus(roofLine.x1).abs().toNumber()
|
|
newPStart.y = Big(wallBaseLine.y1).plus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.x2 > inLine.x1) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPStart.y, x: newPStart.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isStartEnd.end) {
|
|
const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
|
|
const aStartY = Big(roofLine.y2).minus(moveDist).abs().toNumber()
|
|
const bStartY = Big(wallLine.y2).minus(moveDist).abs().toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 })
|
|
console.log('startLines:::::::', inLine)
|
|
const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber()
|
|
newPEnd.y = aStartY
|
|
newPStart.y = roofLine.y1 //Big(roofLine.y1).minus(eLineY).toNumber()
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const newLine = sortRoofLines[idx + 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length;
|
|
const nextIndex = (index + 1) % sortRoofLines.length;
|
|
const newLine = sortRoofLines[prevIndex]
|
|
|
|
if (inLine) {
|
|
if (inLine.x2 < inLine.x1) {
|
|
getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: bStartY, x: wallLine.x1 }, 'pink')
|
|
}
|
|
}
|
|
if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) {
|
|
getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta')
|
|
getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray')
|
|
findPoints.push({ y: aStartY, x: newPEnd.x, position: 'right_out_end' })
|
|
} else {
|
|
const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber()
|
|
newPEnd.y = Big(newPEnd.y).minus(cLineY).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.x2 < inLine.x1) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
} else {
|
|
//newPEnd.y = wallLine.y2;
|
|
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber()
|
|
newPEnd.y = Big(wallBaseLine.y2).minus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.x2 > inLine.x1) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (getOrientation(roofLine) === 'horizontal') {
|
|
//red
|
|
|
|
if (['top', 'bottom'].includes(mLine.position)) {
|
|
if (Math.abs(wallLine.y1 - wallBaseLine.y1) < 0.1 || Math.abs(wallLine.y2 - wallBaseLine.y2) < 0.1) {
|
|
return false
|
|
}
|
|
const isTopPosition = mLine.position === 'top'
|
|
const isBottomPosition = mLine.position === 'bottom'
|
|
const isInPosition =
|
|
(isTopPosition && wallLine.y1 < wallBaseLine.y1) ||
|
|
(isBottomPosition && wallLine.y1 > wallBaseLine.y1) ||
|
|
(isTopPosition && wallLine.y2 < wallBaseLine.y2) ||
|
|
(isBottomPosition && wallLine.y2 > wallBaseLine.y2)
|
|
|
|
const positionType = isInPosition ? 'in' : 'out'
|
|
const condition = `${mLine.position}_${positionType}`
|
|
let isStartEnd = findInteriorPoint(wallBaseLine, sortWallBaseLines)
|
|
|
|
let sPoint, ePoint
|
|
|
|
if (condition === 'top_in') {
|
|
if (isStartEnd.start) {
|
|
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
|
sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }
|
|
newPStart.x = wallBaseLine.x1
|
|
|
|
const newPointY = Big(roofLine.y2).plus(moveDist).toNumber()
|
|
|
|
const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber()
|
|
const pLineX = Big(roofLine.x1).minus(0).toNumber()
|
|
// let idx = 0 >= index - 1 ? sortRoofLines.length : index
|
|
// const pLineY = sortRoofLines[idx - 1].y1
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineY = sortRoofLines[prevIndex].y1
|
|
|
|
|
|
getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue')
|
|
findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_start' })
|
|
|
|
if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green')
|
|
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
|
|
}
|
|
//getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange')
|
|
}
|
|
|
|
if (isStartEnd.end) {
|
|
const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber()
|
|
sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 }
|
|
newPEnd.x = wallBaseLine.x2
|
|
|
|
const newPointY = Big(roofLine.y1).plus(moveDist).toNumber()
|
|
|
|
const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber()
|
|
const pLineX = Big(roofLine.x2).minus(0).abs().toNumber()
|
|
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const pLineY = sortRoofLines[idx + 1].y2
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineY = sortRoofLines[nextIndex].y2
|
|
|
|
getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue')
|
|
findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_end' })
|
|
|
|
if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green')
|
|
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
|
|
}
|
|
|
|
//getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange')
|
|
}
|
|
} else if (condition === 'top_out') {
|
|
console.log('top_out isStartEnd:::::::', isStartEnd)
|
|
|
|
if (isStartEnd.start) {
|
|
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
|
const aStartX = Big(roofLine.x1).plus(moveDist).toNumber()
|
|
const bStartX = Big(wallLine.x1).plus(moveDist).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y })
|
|
|
|
const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber()
|
|
newPEnd.x = roofLine.x2 //Big(newPEnd.x).plus(eLineX).toNumber()
|
|
newPStart.x = aStartX
|
|
// let idx = 0 > index - 1 ? sortRoofLines.length : index
|
|
// const newLine = sortRoofLines[idx - 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length;
|
|
const nextIndex = (index + 1) % sortRoofLines.length;
|
|
const newLine = sortRoofLines[nextIndex]
|
|
|
|
if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) {
|
|
if (inLine) {
|
|
if (inLine.y2 > inLine.y1) {
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink')
|
|
} else {
|
|
getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink')
|
|
}
|
|
}
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta')
|
|
getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray')
|
|
findPoints.push({ x: aStartX, y: newPEnd.y, position: 'top_out_start' })
|
|
} else {
|
|
const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber()
|
|
newPStart.x = Big(newPStart.x).plus(cLineX).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.y2 > inLine.y1) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple')
|
|
}
|
|
} else {
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.y1).minus(roofLine.y1).abs().toNumber()
|
|
newPStart.x = Big(wallBaseLine.x1).plus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.y2 > inLine.y1) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (isStartEnd.end) {
|
|
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
|
const aStartX = Big(roofLine.x2).minus(moveDist).abs().toNumber()
|
|
const bStartX = Big(wallLine.x2).minus(moveDist).abs().toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y })
|
|
console.log('startLines:::::::', inLine)
|
|
const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber()
|
|
newPStart.x = roofLine.x1 //Big(newPStart.x).minus(eLineX).abs().toNumber()
|
|
newPEnd.x = aStartX
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const newLine = sortRoofLines[idx + 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length;
|
|
const nextIndex = (index + 1) % sortRoofLines.length;
|
|
const newLine = sortRoofLines[prevIndex]
|
|
|
|
if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) {
|
|
if (inLine) {
|
|
if (inLine.y2 > inLine.y1) {
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink')
|
|
} else {
|
|
getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink')
|
|
}
|
|
}
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta')
|
|
getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray')
|
|
findPoints.push({ x: aStartX, y: newPEnd.y, position: 'top_out_end' })
|
|
} else {
|
|
const cLineX = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber()
|
|
newPEnd.x = Big(newPEnd.x).minus(cLineX).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.y2 > inLine.y1) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
} else {
|
|
//newPEnd.x = wallLine.x2;
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.y2).minus(roofLine.y2).abs().toNumber()
|
|
newPEnd.x = Big(wallBaseLine.x2).minus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.y1 > inLine.y2) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (condition === 'bottom_in') {
|
|
if (isStartEnd.start) {
|
|
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
|
sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }
|
|
newPStart.x = wallBaseLine.x1
|
|
|
|
const newPointY = Big(roofLine.y2).minus(moveDist).toNumber()
|
|
|
|
const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber()
|
|
const pLineX = Big(roofLine.x1).minus(0).abs().toNumber()
|
|
|
|
// let idx = 0 > index - 1 ? sortRoofLines.length : index
|
|
// const pLineY = sortRoofLines[idx - 1].y1
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineY = sortRoofLines[prevIndex].y1
|
|
|
|
getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue')
|
|
findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_start' })
|
|
|
|
if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green')
|
|
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
|
|
}
|
|
getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange')
|
|
}
|
|
|
|
if (isStartEnd.end) {
|
|
const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber()
|
|
sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 }
|
|
newPEnd.x = wallBaseLine.x2
|
|
|
|
const newPointY = Big(roofLine.y1).minus(moveDist).toNumber()
|
|
|
|
const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber()
|
|
const pLineX = Big(roofLine.x2).minus(0).abs().toNumber()
|
|
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const pLineY = sortRoofLines[idx + 1].y2
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length
|
|
const nextIndex = (index + 1) % sortRoofLines.length
|
|
const pLineY = sortRoofLines[nextIndex].y2
|
|
|
|
|
|
getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue')
|
|
findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_end' })
|
|
|
|
if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) {
|
|
getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green')
|
|
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
|
|
}
|
|
getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange')
|
|
}
|
|
} else if (condition === 'bottom_out') {
|
|
console.log('bottom_out isStartEnd:::::::', isStartEnd)
|
|
if (isStartEnd.start) {
|
|
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
|
const aStartX = Big(roofLine.x1).minus(moveDist).abs().toNumber()
|
|
const bStartX = Big(wallLine.x1).minus(moveDist).abs().toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 })
|
|
console.log('startLines:::::::', inLine)
|
|
const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber()
|
|
newPEnd.x = roofLine.x2 //Big(roofLine.x2).minus(eLineX).toNumber()
|
|
newPStart.x = aStartX
|
|
// let idx = 0 > index - 1 ? sortRoofLines.length : index
|
|
// const newLine = sortRoofLines[idx - 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length;
|
|
const nextIndex = (index + 1) % sortRoofLines.length;
|
|
const newLine = sortRoofLines[nextIndex]
|
|
|
|
if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) {
|
|
if (inLine) {
|
|
if (inLine.y2 < inLine.y1) {
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink')
|
|
} else {
|
|
getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink')
|
|
}
|
|
}
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta')
|
|
getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray')
|
|
findPoints.push({ x: aStartX, y: newPEnd.y, position: 'bottom_out_start' })
|
|
} else {
|
|
const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber()
|
|
newPStart.x = Big(newPStart.x).minus(cLineX).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.y2 < inLine.y1) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple')
|
|
}
|
|
} else {
|
|
//newPStart.x = wallLine.x1;
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.y1).minus(roofLine.y1).abs().toNumber()
|
|
newPStart.x = Big(wallBaseLine.x1).minus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
|
|
if (inLine) {
|
|
if (inLine.y2 > inLine.y1) {
|
|
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPStart.y, x: newPStart.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isStartEnd.end) {
|
|
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
|
const aStartX = Big(roofLine.x2).plus(moveDist).toNumber()
|
|
const bStartX = Big(wallLine.x2).plus(moveDist).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 })
|
|
console.log('startLines:::::::', inLine)
|
|
const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber()
|
|
newPEnd.x = aStartX
|
|
newPStart.x = roofLine.x1 //Big(roofLine.x1).plus(eLineX).toNumber()
|
|
// let idx = sortRoofLines.length < index + 1 ? 0 : index
|
|
// const newLine = sortRoofLines[idx + 1]
|
|
|
|
const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length;
|
|
const nextIndex = (index + 1) % sortRoofLines.length;
|
|
const newLine = sortRoofLines[prevIndex]
|
|
|
|
if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) {
|
|
if (inLine) {
|
|
if (inLine.y2 < inLine.y1) {
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink')
|
|
} else {
|
|
getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink')
|
|
}
|
|
}
|
|
getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta')
|
|
getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray')
|
|
findPoints.push({ x: aStartX, y: newPEnd.y, position: 'bottom_out_end' })
|
|
} else {
|
|
const cLineX = Big(wallBaseLine.y2).minus(wallLine.y2).abs().toNumber()
|
|
newPEnd.x = Big(newPEnd.x).plus(cLineX).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.y2 < inLine.y1) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
} else {
|
|
//newPEnd.x = wallLine.x2;
|
|
//외곽 라인 그리기
|
|
const rLineM = Big(wallBaseLine.y2).minus(roofLine.y2).abs().toNumber()
|
|
newPEnd.x = Big(wallBaseLine.x2).plus(rLineM).toNumber()
|
|
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
|
|
if (inLine) {
|
|
if (inLine.y1 > inLine.y2) {
|
|
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
|
|
} else {
|
|
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
getAddLine(newPStart, newPEnd, 'red')
|
|
//canvas.remove(roofLine)
|
|
} else {
|
|
getAddLine(roofLine.startPoint, roofLine.endPoint)
|
|
}
|
|
|
|
canvas.renderAll()
|
|
})
|
|
}
|
|
getMoveUpDownLine()
|
|
|
|
}
|
|
|
|
if (findPoints.length > 0) {
|
|
// 모든 점에 대해 라인 업데이트를 누적
|
|
return findPoints.reduce((innerLines, point) => {
|
|
return updateAndAddLine(innerLines, point);
|
|
}, [...innerLines]);
|
|
|
|
}
|
|
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)
|
|
// [1] 벽 객체를 가져옵니다.
|
|
let wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId);
|
|
|
|
const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y }));
|
|
|
|
//처마선인지 확인하고 pitch 대입 각 처마선마다 pitch가 다를수 있음
|
|
const { Begin, End } = edgeResult.Edge;
|
|
// [2] 현재 처리 중인 엣지가 roof.lines의 몇 번째 인덱스인지 찾습니다.
|
|
const roofLineIndex = roof.lines.findIndex(line =>
|
|
line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
|
|
);
|
|
|
|
let outerLine = null;
|
|
let targetWallId = null;
|
|
|
|
// [3] 인덱스를 통해 매칭되는 벽 라인의 불변 ID(wallId)를 가져옵니다.
|
|
if (roofLineIndex !== -1) {
|
|
outerLine = roof.lines[roofLineIndex];
|
|
if (wall && wall.lines && wall.lines[roofLineIndex]) {
|
|
targetWallId = wall.lines[roofLineIndex].attributes.wallId;
|
|
}
|
|
targetWallId = outerLine.attributes.wallId;
|
|
}
|
|
|
|
if(!outerLine) {
|
|
outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points);
|
|
console.log('Has matching line:', outerLine);
|
|
//if(outerLine === null) return
|
|
}
|
|
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, targetWallId);
|
|
// }
|
|
}
|
|
}
|
|
|
|
|
|
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, wallLineId) {
|
|
// 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,
|
|
wallLineId: wallLineId, // [5] attributes에 wallId 저장 (이 정보가 최종 roofLines에 들어갑니다)
|
|
...(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 {Array} lines - x1, y1, x2, y2 속성을 가진 라인 객체 배열
|
|
* @returns {Array} 반시계 방향으로 정렬된 새 라인 배열
|
|
*/
|
|
export function ensureCounterClockwiseLines(lines) {
|
|
if (!lines || lines.length < 3) return [...(lines || [])];
|
|
|
|
// 1. 모든 점을 연결 그래프로 구성
|
|
const graph = new Map();
|
|
|
|
// 각 점에서 연결된 점들을 저장
|
|
lines.forEach(line => {
|
|
const p1 = `${line.x1},${line.y1}`;
|
|
const p2 = `${line.x2},${line.y2}`;
|
|
|
|
if (!graph.has(p1)) graph.set(p1, []);
|
|
if (!graph.has(p2)) graph.set(p2, []);
|
|
|
|
// 양방향 연결
|
|
graph.get(p1).push({ x: line.x2, y: line.y2, line });
|
|
graph.get(p2).push({ x: line.x1, y: line.y1, line });
|
|
});
|
|
|
|
// 2. 왼쪽 상단 점 찾기
|
|
let startPoint = null;
|
|
let minY = Infinity;
|
|
let minX = Infinity;
|
|
|
|
for (const [pointStr] of graph) {
|
|
const [x, y] = pointStr.split(',').map(Number);
|
|
if (y < minY || (y === minY && x < minX)) {
|
|
minY = y;
|
|
minX = x;
|
|
startPoint = { x, y };
|
|
}
|
|
}
|
|
|
|
if (!startPoint) return [...lines];
|
|
|
|
// 3. 점들을 순회하며 라인 구성
|
|
const visited = new Set();
|
|
const result = [];
|
|
let current = `${startPoint.x},${startPoint.y}`;
|
|
let prev = null;
|
|
|
|
while (true) {
|
|
if (visited.has(current)) break;
|
|
visited.add(current);
|
|
|
|
const neighbors = graph.get(current) || [];
|
|
if (neighbors.length === 0) break;
|
|
|
|
// 이전 점 제외
|
|
const nextPoints = neighbors.filter(n =>
|
|
!prev || `${n.x},${n.y}` !== `${prev.x},${prev.y}`
|
|
);
|
|
|
|
if (nextPoints.length === 0) break;
|
|
|
|
// 각도가 가장 작은(반시계 방향) 이웃 선택
|
|
const [cx, cy] = current.split(',').map(Number);
|
|
const next = nextPoints.reduce((best, curr) => {
|
|
const angleBest = Math.atan2(best.y - cy, best.x - cx);
|
|
const angleCurr = Math.atan2(curr.y - cy, curr.x - cx);
|
|
return angleCurr > angleBest ? curr : best;
|
|
}, nextPoints[0]);
|
|
|
|
// 라인 추가 (방향 유지)
|
|
const line = next.line;
|
|
const isReversed = (line.x1 !== next.x || line.y1 !== next.y);
|
|
|
|
result.push({
|
|
...line,
|
|
x1: isReversed ? line.x2 : line.x1,
|
|
y1: isReversed ? line.y2 : line.y1,
|
|
x2: isReversed ? line.x1 : line.x2,
|
|
y2: isReversed ? line.y1 : line.y2,
|
|
idx: result.length
|
|
});
|
|
|
|
prev = { x: cx, y: cy };
|
|
current = `${next.x},${next.y}`;
|
|
}
|
|
|
|
// 4. 시계 방향이면 뒤집기
|
|
let area = 0;
|
|
for (let i = 0; i < result.length; i++) {
|
|
const current = result[i];
|
|
const next = result[(i + 1) % result.length];
|
|
area += (next.x1 - current.x1) * (next.y1 + current.y1);
|
|
}
|
|
|
|
if (area > 0) {
|
|
return result.reverse().map((line, idx) => ({
|
|
...line,
|
|
x1: line.x2,
|
|
y1: line.y2,
|
|
x2: line.x1,
|
|
y2: line.y1,
|
|
idx
|
|
}));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 점을 선분에 투영한 점의 좌표를 반환합니다.
|
|
* @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,
|
|
preprocessPolygonCoordinates,
|
|
findOppositeLine,
|
|
createOrderedBasePoints,
|
|
createInnerLinesFromSkeleton
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* 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
|
|
});
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
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 isSameLine2(line1, line2, tolerance = 0.1) {
|
|
return (
|
|
Math.abs(line1.x1 - line2.x1) < tolerance &&
|
|
Math.abs(line1.y1 - line2.y1) < tolerance &&
|
|
Math.abs(line1.x2 - line2.x2) < tolerance &&
|
|
Math.abs(line1.y2 - line2.y2) < tolerance
|
|
);
|
|
}
|
|
// 두 점을 지나는 직선의 기울기 계산
|
|
function calculateSlope(p1, p2) {
|
|
// 수직선인 경우 (기울기 무한대)
|
|
if (p1.x === p2.x) return Infinity;
|
|
return (p2.y - p1.y) / (p2.x - p1.x);
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 };
|
|
}
|
|
|
|
|
|
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)
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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; // 일치하는 라인이 없는 경우
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 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);
|
|
}
|
|
|
|
|
|
const getOrientation = (line, eps = 0.1) => {
|
|
if (!line) {
|
|
console.error('line 객체가 유효하지 않습니다:', line);
|
|
return null; // 또는 적절한 기본값 반환
|
|
}
|
|
|
|
// get 메서드가 있으면 사용하고, 없으면 직접 프로퍼티에 접근
|
|
const getValue = (obj, key) =>
|
|
obj && typeof obj.get === 'function' ? obj.get(key) : obj[key];
|
|
|
|
try {
|
|
const x1 = getValue(line, 'x1');
|
|
const y1 = getValue(line, 'y1');
|
|
const x2 = getValue(line, 'x2');
|
|
const y2 = getValue(line, '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';
|
|
} catch (e) {
|
|
console.error('방향 계산 중 오류 발생:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* 주어진 점을 포함하는 라인을 찾는 함수
|
|
* @param {Array} lines - 검색할 라인 배열 (각 라인은 x1, y1, x2, y2 속성을 가져야 함)
|
|
* @param {Object} point - 찾고자 하는 점 {x, y}
|
|
* @param {number} [tolerance=0.1] - 점이 선분 위에 있는지 판단할 때의 허용 오차
|
|
* @returns {Object|null} 점을 포함하는 첫 번째 라인 또는 null
|
|
*/
|
|
function findLineContainingPoint(lines, point, tolerance = 0.1) {
|
|
if (!point || !lines || !lines.length) return null;
|
|
|
|
return lines.find(line => {
|
|
const { x1, y1, x2, y2 } = line;
|
|
return isPointOnLineSegment(point, {x: x1, y: y1}, {x: x2, y: y2}, tolerance);
|
|
}) || null;
|
|
}
|
|
|
|
/**
|
|
* 점이 선분 위에 있는지 확인하는 함수
|
|
* @param {Object} point - 확인할 점 {x, y}
|
|
* @param {Object} lineStart - 선분의 시작점 {x, y}
|
|
* @param {Object} lineEnd - 선분의 끝점 {x, y}
|
|
* @param {number} tolerance - 허용 오차
|
|
* @returns {boolean}
|
|
*/
|
|
function isPointOnLineSegment(point, lineStart, lineEnd, tolerance = 0.1) {
|
|
const { x: px, y: py } = point;
|
|
const { x: x1, y: y1 } = lineStart;
|
|
const { x: x2, y: y2 } = lineEnd;
|
|
|
|
// 선분의 길이
|
|
const lineLength = Math.hypot(x2 - x1, y2 - y1);
|
|
|
|
// 점에서 선분의 양 끝점까지의 거리 합
|
|
const dist1 = Math.hypot(px - x1, py - y1);
|
|
const dist2 = Math.hypot(px - x2, py - y2);
|
|
|
|
// 점이 선분 위에 있는지 확인 (허용 오차 범위 내에서)
|
|
return Math.abs(dist1 + dist2 - lineLength) <= tolerance;
|
|
}
|
|
|
|
/**
|
|
* Updates a line in the innerLines array and returns the updated array
|
|
* @param {Array} innerLines - Array of line objects to update
|
|
* @param {Object} targetPoint - The point to find the line {x, y}
|
|
* @param {Object} wallBaseLine - The base line containing new coordinates
|
|
* @param {Function} getAddLine - Function to add a new line
|
|
* @returns {Array} Updated array of lines
|
|
*/
|
|
function updateAndAddLine(innerLines, targetPoint) {
|
|
|
|
// 1. Find the line containing the target point
|
|
const foundLine = findLineContainingPoint(innerLines, targetPoint);
|
|
if (!foundLine) {
|
|
console.warn('No line found containing the target point');
|
|
return [...innerLines];
|
|
}
|
|
|
|
// 2. Create a new array without the found line
|
|
const updatedLines = innerLines.filter(line =>
|
|
line !== foundLine &&
|
|
!(line.x1 === foundLine.x1 &&
|
|
line.y1 === foundLine.y1 &&
|
|
line.x2 === foundLine.x2 &&
|
|
line.y2 === foundLine.y2)
|
|
);
|
|
|
|
// Calculate distances to both endpoints
|
|
const distanceToStart = Math.hypot(
|
|
targetPoint.x - foundLine.x1,
|
|
targetPoint.y - foundLine.y1
|
|
);
|
|
const distanceToEnd = Math.hypot(
|
|
targetPoint.x - foundLine.x2,
|
|
targetPoint.y - foundLine.y2
|
|
);
|
|
|
|
// 단순 거리 비교: 타겟 포인트가 시작점에 더 가까우면 시작점을 수정(isUpdatingStart = true)
|
|
//무조건 start
|
|
let isUpdatingStart = false //distanceToStart < distanceToEnd;
|
|
if(targetPoint.position === "top_in_start"){
|
|
if(foundLine.y2 >= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "top_in_end"){
|
|
if(foundLine.y2 >= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
|
|
}else if(targetPoint.position === "bottom_in_start"){
|
|
if(foundLine.y2 <= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "bottom_in_end"){
|
|
if(foundLine.y2 <= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "left_in_start"){
|
|
if(foundLine.x2 >= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "left_in_end"){
|
|
if(foundLine.x2 >= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "right_in_start"){
|
|
if(foundLine.x2 <= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "right_in_end"){
|
|
if(foundLine.x2 <= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "top_out_start"){
|
|
if(foundLine.y2 >= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "top_out_end"){
|
|
if(foundLine.y2 >= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "bottom_out_start"){
|
|
if(foundLine.y2 <= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "bottom_out_end"){
|
|
if(foundLine.y2 <= foundLine.y1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "left_out_start"){
|
|
if(foundLine.x2 >= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "left_out_end"){
|
|
if(foundLine.x2 >= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "right_out_start"){
|
|
if(foundLine.x2 <= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}else if(targetPoint.position === "right_out_end"){
|
|
if(foundLine.x2 <= foundLine.x1){
|
|
isUpdatingStart = true;
|
|
}
|
|
}
|
|
|
|
const updatedLine = {
|
|
...foundLine,
|
|
left: isUpdatingStart ? targetPoint.x : foundLine.x1,
|
|
top: isUpdatingStart ? targetPoint.y : foundLine.y1,
|
|
x1: isUpdatingStart ? targetPoint.x : foundLine.x1,
|
|
y1: isUpdatingStart ? targetPoint.y : foundLine.y1,
|
|
x2: isUpdatingStart ? foundLine.x2 : targetPoint.x,
|
|
y2: isUpdatingStart ? foundLine.y2 : targetPoint.y,
|
|
startPoint: {
|
|
x: isUpdatingStart ? targetPoint.x : foundLine.x1,
|
|
y: isUpdatingStart ? targetPoint.y : foundLine.y1
|
|
},
|
|
endPoint: {
|
|
x: isUpdatingStart ? foundLine.x2 : targetPoint.x,
|
|
y: isUpdatingStart ? foundLine.y2 : targetPoint.y
|
|
}
|
|
};
|
|
|
|
// 4. If it's a Fabric.js object, use set method if available
|
|
if (typeof foundLine.set === 'function') {
|
|
foundLine.set({
|
|
x1: isUpdatingStart ? targetPoint.x : foundLine.x1,
|
|
y1: isUpdatingStart ? targetPoint.y : foundLine.y1,
|
|
x2: isUpdatingStart ? foundLine.x2 : targetPoint.x,
|
|
y2: isUpdatingStart ? foundLine.y2 : targetPoint.y
|
|
});
|
|
updatedLines.push(foundLine);
|
|
} else {
|
|
updatedLines.push(updatedLine);
|
|
}
|
|
|
|
return updatedLines;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 점이 선분 위에 있는지 확인
|
|
* @param {Object} point - 확인할 점 {x, y}
|
|
* @param {Object} lineStart - 선분의 시작점 {x, y}
|
|
* @param {Object} lineEnd - 선분의 끝점 {x, y}
|
|
* @param {number} tolerance - 오차 허용 범위
|
|
* @returns {boolean} - 점이 선분 위에 있으면 true, 아니면 false
|
|
*/
|
|
function isPointOnLineSegment2(point, lineStart, lineEnd, tolerance = 0.1) {
|
|
const { x: px, y: py } = point;
|
|
const { x: x1, y: y1 } = lineStart;
|
|
const { x: x2, y: y2 } = lineEnd;
|
|
|
|
// 선분의 길이
|
|
const lineLength = Math.hypot(x2 - x1, y2 - y1);
|
|
|
|
// 점에서 선분의 양 끝점까지의 거리
|
|
const dist1 = Math.hypot(px - x1, py - y1);
|
|
const dist2 = Math.hypot(px - x2, py - y2);
|
|
|
|
// 점이 선분 위에 있는지 확인 (오차 허용 범위 내에서)
|
|
const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance;
|
|
|
|
if (isOnSegment) {
|
|
console.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`);
|
|
}
|
|
|
|
return isOnSegment;
|
|
}
|
|
|
|
/**
|
|
* 세 점(p1 -> p2 -> p3)의 방향성을 계산합니다. (2D 외적)
|
|
* 반시계 방향(CCW)으로 그려진 폴리곤(Y축 Down) 기준:
|
|
* - 결과 > 0 : 오른쪽 턴 (Right Turn) -> 골짜기 (Valley/Reflex Vertex)
|
|
* - 결과 < 0 : 왼쪽 턴 (Left Turn) -> 외곽 모서리 (Convex Vertex)
|
|
* - 결과 = 0 : 직선
|
|
*/
|
|
function getTurnDirection(p1, p2, p3) {
|
|
// 벡터 a: p1 -> p2
|
|
// 벡터 b: p2 -> p3
|
|
const val = (p2.x - p1.x) * (p3.y - p2.y) - (p2.y - p1.y) * (p3.x - p2.x);
|
|
return val;
|
|
}
|
|
|
|
/**
|
|
* 현재 점(point)을 기준으로 연결된 이전 라인과 다음 라인을 찾아 골짜기 여부 판단
|
|
*/
|
|
function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
|
|
const tolerance = 0.1;
|
|
|
|
// 1. 연결된 '다른' 라인을 찾습니다.
|
|
// isStartVertex가 true면 : 이 점으로 '들어오는' 라인(Previous Line)을 찾아야 함
|
|
// isStartVertex가 false면 : 이 점에서 '나가는' 라인(Next Line)을 찾아야 함
|
|
|
|
let neighborLine = null;
|
|
|
|
if (isStartVertex) {
|
|
// targetPoint가 Start이므로, 어떤 라인의 End가 targetPoint와 같아야 함 (Previous Line)
|
|
neighborLine = allLines.find(l =>
|
|
l !== connectedLine &&
|
|
isSamePoint(l.endPoint || {x:l.x2, y:l.y2}, targetPoint, tolerance)
|
|
);
|
|
} else {
|
|
// targetPoint가 End이므로, 어떤 라인의 Start가 targetPoint와 같아야 함 (Next Line)
|
|
neighborLine = allLines.find(l =>
|
|
l !== connectedLine &&
|
|
isSamePoint(l.startPoint || {x:l.x1, y:l.y1}, targetPoint, tolerance)
|
|
);
|
|
}
|
|
|
|
// 연결된 라인을 못 찾았거나 끊겨있으면 판단 불가 (일단 false)
|
|
if (!neighborLine) return false;
|
|
|
|
// 2. 세 점을 구성하여 회전 방향(Turn) 계산
|
|
// 순서: PrevLine.Start -> [TargetVertex] -> NextLine.End
|
|
let p1, p2, p3;
|
|
|
|
if (isStartVertex) {
|
|
// neighbor(Prev) -> connected(Current)
|
|
p1 = neighborLine.startPoint || {x: neighborLine.x1, y: neighborLine.y1};
|
|
p2 = targetPoint; // 접점
|
|
p3 = connectedLine.endPoint || {x: connectedLine.x2, y: connectedLine.y2};
|
|
} else {
|
|
// connected(Current) -> neighbor(Next)
|
|
p1 = connectedLine.startPoint || {x: connectedLine.x1, y: connectedLine.y1};
|
|
p2 = targetPoint; // 접점
|
|
p3 = neighborLine.endPoint || {x: neighborLine.x2, y: neighborLine.y2};
|
|
}
|
|
|
|
// 3. 외적 계산 (Y축이 아래로 증가하는 캔버스 좌표계 + CCW 진행 기준)
|
|
// 값이 양수(+)면 오른쪽 턴 = 골짜기
|
|
const crossProduct = getTurnDirection(p1, p2, p3);
|
|
|
|
return crossProduct > 0;
|
|
}
|
|
|
|
function findInteriorPoint(line, polygonLines) {
|
|
const { x1, y1, x2, y2 } = line;
|
|
|
|
// line 객체 포맷 통일
|
|
const currentLine = {
|
|
...line,
|
|
startPoint: { x: x1, y: y1 },
|
|
endPoint: { x: x2, y: y2 }
|
|
};
|
|
|
|
// 1. 시작점이 골짜기인지 확인 (들어오는 라인과 나가는 라인의 각도)
|
|
const startIsValley = isValleyVertex(currentLine.startPoint, currentLine, polygonLines, true);
|
|
|
|
// 2. 끝점이 골짜기인지 확인
|
|
const endIsValley = isValleyVertex(currentLine.endPoint, currentLine, polygonLines, false);
|
|
|
|
return {
|
|
start: startIsValley,
|
|
end: endIsValley
|
|
};
|
|
}
|
|
|