qcast-front/src/util/skeleton-utils.js

3749 lines
144 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
import { SkeletonBuilder } from '@/lib/skeletons'
import { calcLineActualSize, calcLineActualSize2, 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) || isSamePoint(roof.basePoints[index], originalEndPoint)) {
point.y = Big(point.y).minus(moveUpDownLength).toNumber();
// console.log('roof.basePoints[index]', roof.basePoints[index])
// console.log('point.x::::', point)
// console.log('originalStartPoint', originalStartPoint)
// console.log('originalEndPoint', originalEndPoint)
}
}
}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;
});
/**
* 직선다각형을 이루지 못하는 좌표를 삭제합니다.
* @param {Array<object>} points - 폴리곤 좌표 배열
* @returns {Array<object>} 정리된 좌표 배열
*/
function removeNonOrthogonalPoints(points) {
if (!points || points.length < 3) return points;
const EPSILON = 1.0;
const isOrthogonal = (p1, p2) =>
Math.abs(p1.x - p2.x) < EPSILON || Math.abs(p1.y - p2.y) < EPSILON;
let current = [...points];
let changed = true;
// 1. 대각선을 만드는 점 제거
while (changed && current.length >= 3) {
changed = false;
for (let i = 0; i < current.length; i++) {
const pPrev = current[(i - 1 + current.length) % current.length];
const pCurr = current[i];
const pNext = current[(i + 1) % current.length];
// 현재 점(pCurr)을 기준으로 앞뒤 연결이 모두 직교하지 않거나,
// 현재 점을 제거했을 때 앞뒤 점(pPrev, pNext)이 직교하게 된다면 현재 점이 불필요한 "꺾임"일 수 있음.
if (!isOrthogonal(pPrev, pCurr) || !isOrthogonal(pCurr, pNext)) {
if (isOrthogonal(pPrev, pNext)) {
current.splice(i, 1);
changed = true;
break;
}
}
}
}
// 2. 일직선상의 중간 점 제거 (수평 또는 수직선상에 세 점이 있는 경우)
changed = true;
while (changed && current.length >= 3) {
changed = false;
for (let i = 0; i < current.length; i++) {
const p1 = current[i];
const p2 = current[(i + 1) % current.length];
const p3 = current[(i + 2) % current.length];
if ((Math.abs(p1.x - p2.x) < EPSILON && Math.abs(p2.x - p3.x) < EPSILON) ||
(Math.abs(p1.y - p2.y) < EPSILON && Math.abs(p2.y - p3.y) < EPSILON)) {
current.splice((i + 1) % current.length, 1);
changed = true;
break;
}
}
}
return current;
}
const cleaned = removeNonOrthogonalPoints(newPoints);
// console.log(cleaned)
return cleaned;
}
}
/**
* 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)
// roof.points 순서와 맞춰야 offset 매핑이 정확함
const baseLinePoints = createOrderedBasePoints(roof.points, baseLines)
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) || []
// 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
const moveUpDown = roof.moveUpDown || 0
// 닫힌 폴리곤(첫점 = 끝점)인 경우 마지막 중복 점 제거
const isClosedPolygon = (points) =>
points.length > 1 && isSamePoint(points[0], points[points.length - 1])
// roofLinePoints : drawRoofPolygon에서 baseLine + 각 변의 offset으로 계산된 실제 처마 외곽선
// orderedBaseLinePoints: 외벽선(baseLine) 꼭짓점을 연결 순서대로 정렬
const roofLinePoints = isClosedPolygon(roof.points) ? roof.points.slice(0, -1) : roof.points
const orderedBaseLinePoints = isClosedPolygon(baseLinePoints) ? baseLinePoints.slice(0, -1) : baseLinePoints
// 2. 각 baseLine 꼭짓점 → 대응하는 roofLine 꼭짓점 방향벡터 계산
// 각 변의 offset이 달라도 방향은 항상 baseLine → roofLine (바깥쪽)
const contactData = orderedBaseLinePoints.map((point, index) => {
const roofPoint = roofLinePoints[index]
if (!roofPoint) return { dx: 0, dy: 0, signDx: 0, signDy: 0 }
const dx = roofPoint.x - point.x
const dy = roofPoint.y - point.y
return { dx, dy, signDx: Math.sign(dx), signDy: Math.sign(dy) }
})
// 3. maxStep: 모든 꼭짓점 중 가장 큰 45도 대각 step 값
// - 각 꼭짓점의 min(|dx|, |dy|) = 해당 꼭짓점에서 45도 방향으로 확장 가능한 거리
// - 전체에서 max를 취해 SkeletonBuilder 입력 다각형이 충분히 확장되도록 보장
// - 모든 꼭짓점에 동일한 maxStep을 적용하므로 각 변의 offset이 달라도
// SkeletonBuilder 입력은 항상 대칭 다각형이 되어 스켈레톤이 안정적으로 생성됨
const maxStep = contactData.reduce((max, data) => {
return Math.max(max, Math.min(Math.abs(data.dx), Math.abs(data.dy)))
}, 0)
// baseLine 폴리곤 중심점 (dx 또는 dy가 0인 꼭짓점의 확장 방향 보정에 사용)
const centroid = orderedBaseLinePoints.reduce((acc, p) => {
acc.x += p.x / orderedBaseLinePoints.length
acc.y += p.y / orderedBaseLinePoints.length
return acc
}, { x: 0, y: 0 })
// 4. 모든 꼭짓점을 동일한 maxStep으로 45도 대각 방향 확장
// - dx 또는 dy가 0인 경우(한 축 이동만 있는 꼭짓점): centroid 기준 바깥 방향으로 보정
let roofLineContactPoints = orderedBaseLinePoints.map((point, index) => {
const data = contactData[index]
let signDx = data.signDx
let signDy = data.signDy
if (signDx === 0) signDx = point.x >= centroid.x ? 1 : -1
if (signDy === 0) signDy = point.y >= centroid.y ? 1 : -1
return {
x: point.x + signDx * maxStep,
y: point.y + signDy * maxStep
}
})
// 5. changRoofLinePoints: SkeletonBuilder에 입력할 최종 다각형
// - roofLineContactPoints 방향으로 maxContactDistance(= √2 × maxStep) 만큼 확장
// - 45도 대각 방향(signDx, signDy 각 ±1)이므로 실제 x·y 이동량은 각 축으로 maxStep
const maxContactDistance = Math.hypot(maxStep, maxStep)
let changRoofLinePoints = orderedBaseLinePoints.map((point, index) => {
const contactPoint = roofLineContactPoints[index]
if (!contactPoint) return point
const dx = contactPoint.x - point.x
const dy = contactPoint.y - point.y
const len = Math.hypot(dx, dy)
if (len === 0 || maxContactDistance === 0) return point
return {
x: point.x + (dx / len) * maxContactDistance,
y: point.y + (dy / len) * maxContactDistance
}
})
// 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체
if (moveFlowLine !== 0 || moveUpDown !== 0) {
const movedPoints = movingLineFromSkeleton(roofId, canvas)
roofLineContactPoints = movedPoints
changRoofLinePoints = movedPoints
}
// changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지)
roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y }))
const geoJSONPolygon = toGeoJSON(changRoofLinePoints)
try {
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
geoJSONPolygon.pop()
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
// 스켈레톤 데이터를 기반으로 내부선 생성
roof.innerLines = roof.innerLines || []
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
//console.log("roofInnerLines:::", roof.innerLines);
// 캔버스에 스켈레톤 상태 저장
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 = roofLineContactPoints
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 - avoid circular refs by only picking needed data
const beforeGableProcessing = skeletonLines.map(line => ({
p1: { x: line.p1.x, y: line.p1.y },
p2: { x: line.p2.x, y: line.p2.y },
attributes: { ...line.attributes },
lineStyle: { ...line.lineStyle }
}));
// 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 === sortWallBaseLines.length && sortWallBaseLines.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))
const isCollinear = (l1, l2, tolerance = 0.1) => {
const slope1 = Math.abs(l1.x2 - l1.x1) < tolerance ? Infinity : (l1.y2 - l1.y1) / (l1.x2 - l1.x1)
const slope2 = Math.abs(l2.x2 - l2.x1) < tolerance ? Infinity : (l2.y2 - l2.y1) / (l2.x2 - l2.x1)
if (slope1 === Infinity && slope2 === Infinity) {
return Math.abs(l1.x1 - l2.x1) < tolerance
}
if (Math.abs(slope1 - slope2) > tolerance) return false
const yIntercept1 = l1.y1 - slope1 * l1.x1
const yIntercept2 = l2.y1 - slope2 * l2.x1
return Math.abs(yIntercept1 - yIntercept2) < tolerance
}
if (isCollinear(wallBaseLine, wallLine)) {
return
}
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 })
const dx = Math.abs(p2.x - p1.x);
const dy = Math.abs(p2.y - p1.y);
const isDiagonal = dx > 0.5 && dy > 0.5; // x, y 변화가 모두 있으면 대각선
//console.log("mergeLines:::::::", mergeLines);
const line = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId: roof.id,
fontSize: roof.fontSize,
stroke: 'black',
strokeWidth: 4,
name: 'eaveHelpLine',
lineName: 'eaveHelpLine',
visible: true,
roofId: roofId,
selectable: true,
hoverCursor: 'pointer',
attributes: {
type: 'eaveHelpLine',
isStart: true,
pitch: wallLine.attributes.pitch,
planeSize: calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }),
actualSize: (isDiagonal) ? calcLineActualSize2(
{
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y
},
getDegreeByChon(wallLine.attributes.pitch)
) : calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }),
},
})
//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')
}
getAddLine(newPStart, newPEnd, 'red')
}
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')
getAddLine(newPStart, newPEnd, 'red')
}
} 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{
newPStart.y = roofLine.y1
}
} 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')
}
}
}
}
getAddLine(newPStart, newPEnd, 'red')
}
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{
newPEnd.y = roofLine.y2
}
} 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' })
getAddLine(newPStart, newPEnd, 'red')
}
} 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')
}
getAddLine(newPStart, newPEnd, 'red')
}
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')
getAddLine(newPStart, newPEnd, 'red')
}
} 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).toNumber()
const bStartY = Big(wallLine.y1).plus(moveDist).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{
newPStart.y = roofLine.y1
}
} 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')
}
}
}
}
getAddLine(newPStart, newPEnd, 'red')
}
if (isStartEnd.end) {
const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
const aStartY = Big(roofLine.y2).minus(moveDist).toNumber()
const bStartY = Big(wallLine.y2).minus(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).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')
}
}
}
}
getAddLine(newPStart, newPEnd, 'red')
}
}
}
} 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')
getAddLine(newPStart, newPEnd, 'red')
}
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).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')
getAddLine(newPStart, newPEnd, 'red')
}
} 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{ //라인머지
newPStart.x = roofLine.x1
}
} 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')
}
}
}
}
getAddLine(newPStart, newPEnd, 'red')
}
if (isStartEnd.end) {
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
const aStartX = Big(roofLine.x2).minus(moveDist).toNumber()
const bStartX = Big(wallLine.x2).minus(moveDist).toNumber()
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y })
console.log('startLines:::::::', inLine)
const eLineX = Big(bStartX).minus(wallLine.x2).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{
newPEnd.x = roofLine.x2
}
} 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')
}
}
}
}
getAddLine(newPStart, newPEnd, 'red')
}
} 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).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')
getAddLine(newPStart, newPEnd, 'red')
}
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).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')
getAddLine(newPStart, newPEnd, 'red')
}
} 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).toNumber()
const bStartX = Big(wallLine.x1).minus(moveDist).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 {
newPStart.x = roofLine.x1
}
} 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')
}
}
}
}
getAddLine(newPStart, newPEnd, 'red')
}
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{
newPEnd.x = roofLine.x2
}
} 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')
}
}
}
}
//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 = []
// 확장된 외곽선 판별용
const skPts = roof.skeletonPoints || []
const isSkeletonOuterEdge = (p1, p2, tolerance = 0.5) => {
for (let si = 0; si < skPts.length; si++) {
const sp1 = skPts[si]
const sp2 = skPts[(si + 1) % skPts.length]
if ((Math.abs(p1.x - sp1.x) < tolerance && Math.abs(p1.y - sp1.y) < tolerance &&
Math.abs(p2.x - sp2.x) < tolerance && Math.abs(p2.y - sp2.y) < tolerance) ||
(Math.abs(p1.x - sp2.x) < tolerance && Math.abs(p1.y - sp2.y) < tolerance &&
Math.abs(p2.x - sp1.x) < tolerance && Math.abs(p2.y - sp1.y) < tolerance)) {
return true
}
}
return false
}
for (let i = 0; i < polygonPoints.length; i++) {
const p1 = polygonPoints[i];
const p2 = polygonPoints[(i + 1) % polygonPoints.length];
// 확장된 외곽선에 해당하는 edge는 스킵
if (skPts.length > 0 && isSkeletonOuterEdge(p1, p2)) continue
// 지붕 경계선과 교차 확인 및 클리핑
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) ? calcLineActualSize2(
{
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;
const connectedLineData = {
x1: connectedLine.x1 ?? connectedLine.get?.('x1'),
y1: connectedLine.y1 ?? connectedLine.get?.('y1'),
x2: connectedLine.x2 ?? connectedLine.get?.('x2'),
y2: connectedLine.y2 ?? connectedLine.get?.('y2'),
startPoint: connectedLine.startPoint,
endPoint: connectedLine.endPoint
};
let neighborLine = null;
if (isStartVertex) {
neighborLine = allLines.find(l => {
if (l === connectedLine) return false;
const lx1 = l.x1 ?? l.get?.('x1');
const ly1 = l.y1 ?? l.get?.('y1');
const lx2 = l.x2 ?? l.get?.('x2');
const ly2 = l.y2 ?? l.get?.('y2');
const end = l.endPoint || { x: lx2, y: ly2 };
return isSamePoint(end, targetPoint, tolerance);
});
} else {
neighborLine = allLines.find(l => {
if (l === connectedLine) return false;
const lx1 = l.x1 ?? l.get?.('x1');
const ly1 = l.y1 ?? l.get?.('y1');
const lx2 = l.x2 ?? l.get?.('x2');
const ly2 = l.y2 ?? l.get?.('y2');
const start = l.startPoint || { x: lx1, y: ly1 };
return isSamePoint(start, targetPoint, tolerance);
});
}
if (!neighborLine) return false;
const nlx1 = neighborLine.x1 ?? neighborLine.get?.('x1');
const nly1 = neighborLine.y1 ?? neighborLine.get?.('y1');
const nlx2 = neighborLine.x2 ?? neighborLine.get?.('x2');
const nly2 = neighborLine.y2 ?? neighborLine.get?.('y2');
const clx1 = connectedLineData.x1;
const cly1 = connectedLineData.y1;
const clx2 = connectedLineData.x2;
const cly2 = connectedLineData.y2;
let p1, p2, p3;
if (isStartVertex) {
p1 = neighborLine.startPoint || { x: nlx1, y: nly1 };
p2 = targetPoint;
p3 = connectedLineData.endPoint || { x: clx2, y: cly2 };
} else {
p1 = connectedLineData.startPoint || { x: clx1, y: cly1 };
p2 = targetPoint;
p3 = neighborLine.endPoint || { x: nlx2, y: nly2 };
}
const crossProduct = getTurnDirection(p1, p2, p3);
console.log('crossProduct:', crossProduct);
return crossProduct > 0;
}
function findInteriorPoint(line, polygonLines) {
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');
// line 객체 포맷 통일 (함수 내부용)
const currentLine = {
...line,
x1, y1, x2, y2,
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
};
}