2050 lines
66 KiB
JavaScript
2050 lines
66 KiB
JavaScript
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
|
import Big from 'big.js'
|
|
import { SkeletonBuilder } from '@/lib/skeletons'
|
|
import { arePointsEqual, calcLineActualSize, calcLinePlaneSize, calculateAngle, toGeoJSON } from '@/util/qpolygon-utils'
|
|
import { QLine } from '@/components/fabric/QLine'
|
|
import { getDegreeByChon } from '@/util/canvas-util'
|
|
|
|
|
|
/**
|
|
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다.
|
|
* @param {string} roofId - 대상 지붕 객체의 ID
|
|
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
|
|
* @param {string} textMode - 텍스트 표시 모드
|
|
* @param existingSkeletonLines
|
|
*/
|
|
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode, existingSkeletonLines = []) => {
|
|
const roof = canvas?.getObjects().find((object) => object.id === roofId)
|
|
if (!roof) {
|
|
console.error(`Roof with id "${roofId}" not found.`);
|
|
return;
|
|
}
|
|
const skeletonLines = [...existingSkeletonLines];
|
|
// 1. 기존 스켈레톤 라인 제거
|
|
// const existingSkeletonLines = canvas.getObjects().filter(obj =>
|
|
// obj.parentId === roofId && obj.attributes?.type === 'skeleton'
|
|
// );
|
|
// existingSkeletonLines.forEach(line => canvas.remove(line));
|
|
|
|
// 2. 지붕 폴리곤 좌표 전처리
|
|
const coordinates = preprocessPolygonCoordinates(roof.points);
|
|
if (coordinates.length < 3) {
|
|
console.warn("Polygon has less than 3 unique points. Cannot generate skeleton.");
|
|
return;
|
|
}
|
|
|
|
|
|
const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId)
|
|
|
|
//평행선 여부
|
|
const hasNonParallelLines = roof.lines.filter((line) => Big(line.x1).minus(Big(line.x2)).gt(1) && Big(line.y1).minus(Big(line.y2)).gt(1))
|
|
if (hasNonParallelLines.length > 0) {
|
|
return
|
|
}
|
|
|
|
|
|
/** 외벽선 */
|
|
const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0)
|
|
const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 }))
|
|
|
|
skeletonBuilder(roofId, canvas, textMode, roof, skeletonLines)
|
|
|
|
}
|
|
|
|
/**
|
|
* 스켈레톤의 edge를 각도가 있는 구간으로 변형합니다.
|
|
* @param {Object} skeleton - 스켈레톤 객체
|
|
* @param {number} edgeIndex - 변형할 edge의 인덱스
|
|
* @param {number} angleOffset - 추가할 각도 (도 단위)
|
|
* @param {number} splitRatio - 분할 비율 (0-1 사이, 0.5면 중간점)
|
|
* @returns {Object} 변형된 스켈레톤 객체
|
|
*/
|
|
export const transformEdgeWithAngle = (skeleton, edgeIndex, angleOffset = 45, splitRatio = 0.5) => {
|
|
if (!skeleton || !skeleton.Edges || edgeIndex >= skeleton.Edges.length || edgeIndex < 0) {
|
|
console.warn('유효하지 않은 스켈레톤 또는 edge 인덱스입니다.')
|
|
return skeleton
|
|
}
|
|
|
|
const edgeResult = skeleton.Edges[edgeIndex]
|
|
if (!edgeResult || !edgeResult.Polygon || !Array.isArray(edgeResult.Polygon)) {
|
|
console.warn('유효하지 않은 edge 또는 Polygon 데이터입니다.')
|
|
return skeleton
|
|
}
|
|
|
|
const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y }))
|
|
|
|
// 변형할 edge 찾기 (가장 긴 내부 선분을 대상으로 함)
|
|
let longestEdge = null
|
|
let longestLength = 0
|
|
let longestEdgeIndex = -1
|
|
|
|
for (let i = 0; i < polygonPoints.length; i++) {
|
|
const p1 = polygonPoints[i]
|
|
const p2 = polygonPoints[(i + 1) % polygonPoints.length]
|
|
|
|
const length = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))
|
|
|
|
if (length > longestLength) {
|
|
longestLength = length
|
|
longestEdge = { p1, p2, index: i }
|
|
longestEdgeIndex = i
|
|
}
|
|
}
|
|
|
|
if (!longestEdge) return skeleton
|
|
|
|
// 중간점 계산
|
|
const midPoint = {
|
|
x: longestEdge.p1.x + (longestEdge.p2.x - longestEdge.p1.x) * splitRatio,
|
|
y: longestEdge.p1.y + (longestEdge.p2.y - longestEdge.p1.y) * splitRatio,
|
|
}
|
|
|
|
// 원래 선분의 방향 벡터
|
|
const originalVector = {
|
|
x: longestEdge.p2.x - longestEdge.p1.x,
|
|
y: longestEdge.p2.y - longestEdge.p1.y,
|
|
}
|
|
|
|
// 각도 변형을 위한 새로운 점 계산
|
|
const angleRad = (angleOffset * Math.PI) / 180
|
|
const perpVector = {
|
|
x: -originalVector.y,
|
|
y: originalVector.x,
|
|
}
|
|
|
|
// 정규화
|
|
const perpLength = Math.sqrt(perpVector.x * perpVector.x + perpVector.y * perpVector.y)
|
|
const normalizedPerp = {
|
|
x: perpVector.x / perpLength,
|
|
y: perpVector.y / perpLength,
|
|
}
|
|
|
|
// 각도 변형을 위한 오프셋 거리 (선분 길이의 10%)
|
|
const offsetDistance = longestLength * 0.1
|
|
|
|
// 새로운 각도 점
|
|
const anglePoint = {
|
|
x: midPoint.x + normalizedPerp.x * offsetDistance * Math.sin(angleRad),
|
|
y: midPoint.y + normalizedPerp.y * offsetDistance * Math.sin(angleRad),
|
|
}
|
|
|
|
// 새로운 폴리곤 점들 생성
|
|
const newPolygonPoints = [...polygonPoints]
|
|
|
|
// 기존 점을 제거하고 새로운 세 점으로 교체
|
|
newPolygonPoints.splice(longestEdgeIndex + 1, 0, anglePoint)
|
|
|
|
// 스켈레톤 객체 업데이트 - 순환 참조 문제를 방지하기 위해 안전한 복사 방식 사용
|
|
const newSkeleton = {
|
|
...skeleton,
|
|
Edges: skeleton.Edges.map((edge, idx) => {
|
|
if (idx === edgeIndex) {
|
|
return {
|
|
...edge,
|
|
Polygon: newPolygonPoints.map((p) => ({ X: p.x, Y: p.y })),
|
|
}
|
|
}
|
|
return edge
|
|
}),
|
|
}
|
|
|
|
return newSkeleton
|
|
}
|
|
|
|
/**
|
|
* 여러 edge를 한 번에 변형합니다.
|
|
* @param {Object} skeleton - 스켈레톤 객체
|
|
* @param {Array} edgeConfigs - 변형 설정 배열 [{edgeIndex, angleOffset, splitRatio}]
|
|
* @returns {Object} 변형된 스켈레톤 객체
|
|
*/
|
|
export const transformMultipleEdges = (skeleton, edgeConfigs) => {
|
|
let transformedSkeleton = skeleton
|
|
|
|
// 인덱스 역순으로 정렬하여 변형 시 인덱스 변화를 방지
|
|
edgeConfigs.sort((a, b) => b.edgeIndex - a.edgeIndex)
|
|
|
|
edgeConfigs.forEach((config) => {
|
|
transformedSkeleton = transformEdgeWithAngle(transformedSkeleton, config.edgeIndex, config.angleOffset || 45, config.splitRatio || 0.5)
|
|
})
|
|
|
|
return transformedSkeleton
|
|
}
|
|
|
|
/**
|
|
* 마루가 있는 지붕을 그린다.
|
|
* @param roofId
|
|
* @param canvas
|
|
* @param textMode
|
|
* @param roof
|
|
* @param edgeProperties
|
|
*/
|
|
export const skeletonBuilder = (roofId, canvas, textMode, roof, skeletonLines) => {
|
|
// 1. 다각형 좌표를 GeoJSON 형식으로 변환합니다.
|
|
const geoJSONPolygon = toGeoJSON(roof.points)
|
|
|
|
try {
|
|
// 2. SkeletonBuilder를 사용하여 스켈레톤을 생성합니다.
|
|
geoJSONPolygon.pop()
|
|
let skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
|
|
|
console.log(`지붕 형태: ${skeleton.roof_type}`) // "complex"
|
|
console.log('Edge 분석:', skeleton.edge_analysis)
|
|
|
|
// 3. 라인을 그림
|
|
const innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode, skeletonLines)
|
|
|
|
console.log("innerLines::", innerLines)
|
|
// 4. 생성된 선들을 캔버스에 추가하고 지붕 객체에 저장
|
|
// innerLines.forEach((line) => {
|
|
// canvas.add(line)
|
|
// line.bringToFront()
|
|
// canvas.renderAll()
|
|
// })
|
|
|
|
roof.innerLines = innerLines
|
|
|
|
// canvas에 skeleton 상태 저장
|
|
if (!canvas.skeletonStates) {
|
|
canvas.skeletonStates = {}
|
|
}
|
|
canvas.skeletonStates[roofId] = true
|
|
|
|
canvas.renderAll()
|
|
} catch (e) {
|
|
console.error('지붕 생성 중 오류 발생:', e)
|
|
// 오류 발생 시 기존 로직으로 대체하거나 사용자에게 알림
|
|
if (canvas.skeletonStates) {
|
|
canvas.skeletonStates[roofId] = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 스켈레톤 결과와 원본 외벽선 정보를 바탕으로 내부선(마루, 추녀)들을 생성합니다.
|
|
* @param {object} skeleton - SkeletonBuilder로부터 반환된 스켈레톤 객체
|
|
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
|
|
* @param {QPolygon} roof - 대상 지붕 QPolygon 객체
|
|
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
|
|
* @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
|
|
* @returns {Array<QLine>} 생성된 내부선(QLine) 배열
|
|
*/
|
|
// 두 선분이 같은 직선상에 있고 겹치는지 확인하는 함수
|
|
const areLinesCollinearAndOverlapping = (line1, line2) => {
|
|
// 두 선분이 같은 직선상에 있는지 확인
|
|
const areCollinear = (p1, p2, p3, p4) => {
|
|
const area1 = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
|
|
const area2 = (p2.x - p1.x) * (p4.y - p1.y) - (p2.y - p1.y) * (p4.x - p1.x)
|
|
return Math.abs(area1) < 1 && Math.abs(area2) < 1
|
|
}
|
|
|
|
// 두 선분이 겹치는지 확인
|
|
const isOverlapping = (a1, a2, b1, b2) => {
|
|
// x축에 평행한 경우
|
|
if (Math.abs(a1.y - a2.y) < 1 && Math.abs(b1.y - b2.y) < 1) {
|
|
if (Math.abs(a1.y - b1.y) > 1) return false
|
|
return !(Math.max(a1.x, a2.x) < Math.min(b1.x, b2.x) || Math.min(a1.x, a2.x) > Math.max(b1.x, b2.x))
|
|
}
|
|
// y축에 평행한 경우
|
|
if (Math.abs(a1.x - a2.x) < 1 && Math.abs(b1.x - b2.x) < 1) {
|
|
if (Math.abs(a1.x - b1.x) > 1) return false
|
|
return !(Math.max(a1.y, a2.y) < Math.min(b1.y, b2.y) || Math.min(a1.y, a2.y) > Math.max(b1.y, b2.y))
|
|
}
|
|
return false
|
|
}
|
|
|
|
return areCollinear(line1.p1, line1.p2, line2.p1, line2.p2) && isOverlapping(line1.p1, line1.p2, line2.p1, line2.p2)
|
|
}
|
|
|
|
// 겹치는 선분을 하나로 합치는 함수
|
|
const mergeCollinearLines = (lines) => {
|
|
if (lines.length <= 1) return lines
|
|
|
|
const merged = []
|
|
const processed = new Set()
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (processed.has(i)) continue
|
|
|
|
let currentLine = lines[i]
|
|
let mergedLine = { ...currentLine }
|
|
let wasMerged = false
|
|
|
|
for (let j = i + 1; j < lines.length; j++) {
|
|
if (processed.has(j)) continue
|
|
|
|
const otherLine = lines[j]
|
|
|
|
if (areLinesCollinearAndOverlapping(mergedLine, otherLine)) {
|
|
// 겹치는 선분을 하나로 합침
|
|
const allPoints = [
|
|
{ x: mergedLine.p1.x, y: mergedLine.p1.y },
|
|
{ x: mergedLine.p2.x, y: mergedLine.p2.y },
|
|
{ x: otherLine.p1.x, y: otherLine.p1.y },
|
|
{ x: otherLine.p2.x, y: otherLine.p2.y },
|
|
]
|
|
|
|
// x축에 평행한 경우 x 좌표로 정렬
|
|
if (Math.abs(mergedLine.p1.y - mergedLine.p2.y) < 1) {
|
|
allPoints.sort((a, b) => a.x - b.x)
|
|
mergedLine = {
|
|
p1: allPoints[0],
|
|
p2: allPoints[allPoints.length - 1],
|
|
attributes: mergedLine.attributes,
|
|
}
|
|
}
|
|
// y축에 평행한 경우 y 좌표로 정렬
|
|
else {
|
|
allPoints.sort((a, b) => a.y - b.y)
|
|
mergedLine = {
|
|
p1: allPoints[0],
|
|
p2: allPoints[allPoints.length - 1],
|
|
attributes: mergedLine.attributes,
|
|
}
|
|
}
|
|
|
|
wasMerged = true
|
|
processed.add(j)
|
|
}
|
|
}
|
|
|
|
merged.push(mergedLine)
|
|
processed.add(i)
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
//조건에 따른 스켈레톤을 그린다.
|
|
const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMode, skeletonLines) => {
|
|
console.log('=== Edge Properties 기반 후처리 시작 ===')
|
|
|
|
if (!skeleton || !skeleton.Edges) return []
|
|
|
|
const innerLines = []
|
|
const processedInnerEdges = new Set()
|
|
//const skeletonLines = []
|
|
|
|
// 1. 기본 skeleton에서 모든 내부 선분 수집
|
|
//edge 순서와 baseLines 순서가 같을수가 없다.
|
|
for (let edgeIndex = 0; edgeIndex < skeleton.Edges.length; edgeIndex++) {
|
|
console.log('edgeIndex:::', edgeIndex)
|
|
let changeEdgeIndex = edgeIndex
|
|
//입력 폴리곤이 왼쪽 상단에서 시작하면 시계 방향으로 진행합니다.
|
|
//오른쪽 하단에서 시작하면 그 지점에서부터 시계 방향으로 진행합니다.
|
|
//edgeIndex 대신에 실제 baseLines 선택라인을 찾아야 한다.
|
|
const edgeResult = skeleton.Edges[edgeIndex]
|
|
console.log(edgeResult)
|
|
// 방향을 고려하지 않고 같은 라인인지 확인하는 함수
|
|
|
|
let edgeType = 'eaves'
|
|
let baseLineIndex = 0
|
|
|
|
processEavesEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges)
|
|
|
|
}
|
|
|
|
|
|
for (let edgeIndex = 0; edgeIndex < skeleton.Edges.length; edgeIndex++) {
|
|
|
|
const edgeResult = skeleton.Edges[edgeIndex]
|
|
const startX = edgeResult.Edge.Begin.X
|
|
const startY = edgeResult.Edge.Begin.Y
|
|
const endX = edgeResult.Edge.End.X
|
|
const endY = edgeResult.Edge.End.Y
|
|
|
|
|
|
//외벽선 라인과 같은 edgeResult를 찾는다
|
|
for (let baseLineIndex = 0; baseLineIndex < baseLines.length; baseLineIndex++) {
|
|
|
|
if (baseLines[baseLineIndex].attributes.type === 'gable') {
|
|
// 일다 그려서 skeletonLines를 만들어
|
|
//외벽선 동일 라인이면
|
|
if (isSameLine(startX, startY, endX, endY, baseLines[baseLineIndex])) {
|
|
processGableEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges, edgeIndex, baseLineIndex) //
|
|
break // 매칭되는 라인을 찾았으므로 루프 종료
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
console.log(`처리된 skeletonLines: ${skeletonLines.length}개`)
|
|
|
|
// 2. 겹치는 선분 병합
|
|
// const mergedLines = mergeCollinearLines(skeletonLines)
|
|
// console.log('mergedLines', mergedLines)
|
|
// 3. QLine 객체로 변환
|
|
for (const line of skeletonLines) {
|
|
const { p1, p2, attributes, lineStyle } = line
|
|
const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
|
|
parentId: roof.id,
|
|
fontSize: roof.fontSize,
|
|
stroke: lineStyle.color,
|
|
strokeWidth: lineStyle?.width,
|
|
name: attributes.type,
|
|
textMode: textMode,
|
|
attributes: attributes,
|
|
})
|
|
|
|
canvas.add(innerLine)
|
|
innerLine.bringToFront()
|
|
canvas.renderAll()
|
|
|
|
innerLines.push(innerLine)
|
|
}
|
|
|
|
return innerLines
|
|
}
|
|
|
|
// ✅ EAVES (처마) 처리 - 기본 skeleton 모두 사용
|
|
function processEavesEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges) {
|
|
console.log(`processEavesEdge::`, skeletonLines)
|
|
const begin = edgeResult.Edge.Begin
|
|
const end = edgeResult.Edge.End
|
|
|
|
|
|
//내부 선분 수집 (스케레톤은 다각형)
|
|
const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y }))
|
|
|
|
for (let i = 0; i < polygonPoints.length; i++) {
|
|
//시계방향
|
|
const p1 = polygonPoints[i]
|
|
const p2 = polygonPoints[(i + 1) % polygonPoints.length]
|
|
|
|
// 외벽선 제외 후 추가
|
|
if(begin !== edgeResult.Polygon[i] && end !== edgeResult.Polygon[i] ) {
|
|
addRawLine(skeletonLines, processedInnerEdges, p1, p2, 'RIDGE', '#FF0000', 3)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ✅ WALL (벽) 처리 - 선분 개수 최소화
|
|
function processWallEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges, edgeIndex) {
|
|
console.log(`WALL Edge ${edgeIndex}: 내부 선분 최소화`)
|
|
|
|
const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y }))
|
|
|
|
// 벽면은 내부 구조를 단순화 - 주요 선분만 선택
|
|
for (let i = 0; i < polygonPoints.length; i++) {
|
|
const p1 = polygonPoints[i]
|
|
const p2 = polygonPoints[(i + 1) % polygonPoints.length]
|
|
|
|
if (!isOuterEdge(p1, p2, baseLines)) {
|
|
// 선분 길이 확인 - 긴 선분만 사용 (짧은 선분 제거)
|
|
const lineLength = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
|
|
|
|
if (lineLength > 10) {
|
|
// 최소 길이 조건
|
|
addRawLine(skeletonLines, processedInnerEdges, p1, p2, 'HIP', '#000000', 2)
|
|
} else {
|
|
console.log(`WALL: 짧은 선분 제거 (길이: ${lineLength.toFixed(1)})`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ✅ GABLE (케라바) 처리 - 직선 생성, 다른 선분 제거
|
|
function processGableEdge(edgeResult, baseLines, skeletonLines, processedInnerEdges, edgeIndex, baseLineIndex) {
|
|
console.log(`GABLE Edge ${edgeResult}: 직선 skeleton 생성`)
|
|
const diagonalLine = []; //대각선 라인
|
|
|
|
const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y }))
|
|
console.log('polygonPoints::', polygonPoints)
|
|
// ✅ 케라바는 직선 패턴으로 변경
|
|
|
|
// 1. 기존 복잡한 skeleton 선분들 무시
|
|
// 2. GABLE edge에 수직인 직선 생성
|
|
const sourceEdge = edgeResult.Edge
|
|
const gableStart = { x: sourceEdge.Begin.X, y: sourceEdge.Begin.Y }
|
|
const gableEnd = { x: sourceEdge.End.X, y: sourceEdge.End.Y }
|
|
|
|
// GABLE edge 중점
|
|
const gableMidpoint = {
|
|
x: (gableStart.x + gableEnd.x) / 2,
|
|
y: (gableStart.y + gableEnd.y) / 2,
|
|
}
|
|
|
|
// 폴리곤 중심점 (대략적)
|
|
const centerX = polygonPoints.reduce((sum, p) => sum + p.x, 0) / polygonPoints.length
|
|
const centerY = polygonPoints.reduce((sum, p) => sum + p.y, 0) / polygonPoints.length
|
|
const polygonCenter = { x: centerX, y: centerY }
|
|
|
|
|
|
|
|
const selectBaseLine = baseLines[baseLineIndex];
|
|
console.log('selectBaseLine:', selectBaseLine);
|
|
console.log('skeletonLines:', skeletonLines)
|
|
|
|
// selectBaseLine의 중간 좌표 계산
|
|
const midPoint = {
|
|
x: (selectBaseLine.x1 + selectBaseLine.x2) / 2,
|
|
y: (selectBaseLine.y1 + selectBaseLine.y2) / 2
|
|
};
|
|
console.log('midPoint of selectBaseLine:', midPoint);
|
|
|
|
// 대각선 보정(fallback) 제거: 항상 수평/수직 내부 용마루만 생성
|
|
const edgePoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y }));
|
|
|
|
//제거
|
|
for (let i = skeletonLines.length - 1; i >= 0; i--) {
|
|
const line = skeletonLines[i];
|
|
console.log('line:', line)
|
|
console.log('line.attributes.type:', line.attributes.type)
|
|
|
|
const linePoints = [line.p1, line.p2];
|
|
|
|
// Check if both points of the line are in the edgePoints
|
|
const isEdgeLine = linePoints.every(point =>
|
|
edgePoints.some(ep =>
|
|
Math.abs(ep.x - point.x) < 0.001 &&
|
|
Math.abs(ep.y - point.y) < 0.001
|
|
)
|
|
);
|
|
|
|
if (isEdgeLine) {
|
|
skeletonLines.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
//확장
|
|
const breakLinePont = findDisconnectedSkeletonLines(skeletonLines, baseLines)
|
|
console.log('breakLinePont:', breakLinePont)
|
|
|
|
if(breakLinePont.disconnectedLines.length > 0) {
|
|
|
|
|
|
for (const dLine of breakLinePont.disconnectedLines) {
|
|
const inx = dLine.index;
|
|
const exLine = dLine.extendedLine;
|
|
|
|
//확장
|
|
if (dLine.p1Connected) {
|
|
skeletonLines[inx].p2 = { ...skeletonLines[inx].p2, x: exLine.p2.x, y: exLine.p2.y };
|
|
|
|
} else if (dLine.p2Connected) {
|
|
skeletonLines[inx].p1 = { ...skeletonLines[inx].p1, x: exLine.p1.x, y: exLine.p1.y };
|
|
}
|
|
|
|
}
|
|
}
|
|
//확장(연장)
|
|
// for (let i = 0; i < skeletonLines.length; i++) {
|
|
// const line = skeletonLines[i];
|
|
// const p1 = line.p1;
|
|
// const p2 = line.p2;
|
|
// const lineP1 = { x: line.p1.x, y: line.p1.y };
|
|
// const lineP2 = { x: line.p2.x, y: line.p2.y };
|
|
//
|
|
// let hasP1 = false;
|
|
// let hasP2 = false;
|
|
// console.log('edgeResult.Edge::',edgeResult.Edge)
|
|
// //선택한 라인과 다각형을 생성하는 라인 여부
|
|
// const matchingLinePoint = findMatchingLinePoints(line, edgeResult.Polygon);
|
|
// console.log(matchingLinePoint);
|
|
//
|
|
//
|
|
// if(matchingLinePoint.hasMatch) {
|
|
//
|
|
// if (matchingLinePoint.matches[0].type === 'diagonal') {
|
|
// console.log("lineP1:", lineP1)
|
|
// console.log("lineP2:", lineP2)
|
|
// const intersectionPoint = getLineIntersectionParametric(lineP1, lineP2, gableStart, gableEnd);
|
|
// console.log('intersectionPoint:', intersectionPoint);
|
|
// console.log('gableStart:', gableStart);
|
|
// console.log('gableEnd:', gableEnd);
|
|
// // 교차점이 생겼다면 절삭(교차점 이하(이상) 삭제)
|
|
// if (!intersectionPoint) {
|
|
// console.warn('No valid intersection point found between line and gable edge');
|
|
// return; // or handle the null case appropriately
|
|
// }
|
|
//
|
|
// if (matchingLinePoint.matches[0].linePoint === 'p1') {
|
|
// skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: intersectionPoint.x, y: intersectionPoint.y };
|
|
// } else {
|
|
// skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: intersectionPoint.x, y: intersectionPoint.y };
|
|
// }
|
|
//
|
|
// } else if (matchingLinePoint.matches[0].type === 'horizontal') {
|
|
// if (matchingLinePoint.matches[0].linePoint === 'p1') {
|
|
// skeletonLines[i].p1 = { ...skeletonLines[i].p1, x: edgeResult.Edge.Begin.X };
|
|
// } else {
|
|
// skeletonLines[i].p2 = { ...skeletonLines[i].p2, x: edgeResult.Edge.Begin.X };
|
|
// }
|
|
//
|
|
// } else if (matchingLinePoint.matches[0].type === 'vertical') {
|
|
// if (matchingLinePoint.matches[0].linePoint === 'p1') {
|
|
// skeletonLines[i].p1 = { ...skeletonLines[i].p1, y: edgeResult.Edge.Begin.Y };
|
|
// } else {
|
|
// skeletonLines[i].p2 = { ...skeletonLines[i].p2, y: edgeResult.Edge.Begin.Y };
|
|
// }
|
|
// }
|
|
//
|
|
// }
|
|
//
|
|
// }
|
|
|
|
|
|
}
|
|
|
|
// ✅ 헬퍼 함수들
|
|
function isOuterEdge(p1, p2, baseLines) {
|
|
const tolerance = 0.1
|
|
return baseLines.some((line) => {
|
|
const lineStart = line.startPoint || { x: line.x1, y: line.y1 }
|
|
const lineEnd = line.endPoint || { x: line.x2, y: line.y2 }
|
|
|
|
return (
|
|
(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) ||
|
|
(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)
|
|
)
|
|
})
|
|
}
|
|
|
|
function addRawLine(skeletonLines, processedInnerEdges, p1, p2, lineType, color, width) {
|
|
const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|')
|
|
|
|
if (processedInnerEdges.has(edgeKey)) return
|
|
processedInnerEdges.add(edgeKey)
|
|
|
|
// 라인 타입을 상수로 정규화
|
|
const inputNormalizedType =
|
|
lineType === LINE_TYPE.SUBLINE.RIDGE || lineType === 'RIDGE'
|
|
? LINE_TYPE.SUBLINE.RIDGE
|
|
: lineType === LINE_TYPE.SUBLINE.HIP || lineType === 'HIP'
|
|
? LINE_TYPE.SUBLINE.HIP
|
|
: lineType
|
|
|
|
// 대각선 여부 판단 (수평/수직이 아닌 경우)
|
|
const dx = Math.abs(p2.x - p1.x)
|
|
const dy = Math.abs(p2.y - p1.y)
|
|
const tolerance = 0.1
|
|
const isHorizontal = dy < tolerance
|
|
const isVertical = dx < tolerance
|
|
const isDiagonal = !isHorizontal && !isVertical
|
|
|
|
// 대각선일 때 lineType을 HIP로 지정
|
|
const normalizedType = isDiagonal ? LINE_TYPE.SUBLINE.HIP : inputNormalizedType
|
|
|
|
skeletonLines.push({
|
|
p1: p1,
|
|
p2: p2,
|
|
attributes: {
|
|
type: normalizedType,
|
|
planeSize: calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }),
|
|
isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE,
|
|
},
|
|
lineStyle: {
|
|
color: color,
|
|
width: width,
|
|
},
|
|
})
|
|
|
|
}
|
|
|
|
/**
|
|
* 특정 roof의 edge를 캐라바로 설정하여 다시 그립니다.
|
|
* @param {string} roofId - 지붕 ID
|
|
* @param {fabric.Canvas} canvas - 캔버스 객체
|
|
* @param {string} textMode - 텍스트 모드
|
|
* @param {number} selectedEdgeIndex - 선택된 외곽선의 인덱스
|
|
*/
|
|
|
|
export const drawSkeletonWithTransformedEdges = (roofId, canvas, textMode, selectedEdgeIndex) => {
|
|
let roof = canvas?.getObjects().find((object) => object.id === roofId)
|
|
if (!roof) {
|
|
console.warn('Roof object not found')
|
|
return
|
|
}
|
|
|
|
// Clear existing inner lines if any
|
|
if (roof.innerLines) {
|
|
roof.innerLines.forEach((line) => canvas.remove(line))
|
|
roof.innerLines = []
|
|
}
|
|
|
|
// Transform the selected wall into a roof
|
|
transformWallToRoof(roof, canvas, selectedEdgeIndex)
|
|
|
|
canvas.renderAll()
|
|
}
|
|
|
|
/**
|
|
* 삼각형에 대한 캐라바 처리
|
|
* @param {QPolygon} roof - 지붕 객체
|
|
* @param {fabric.Canvas} canvas - 캔버스 객체
|
|
* @param {string} textMode - 텍스트 모드
|
|
* @param {number} selectedEdgeIndex - 선택된 외곽선의 인덱스
|
|
*/
|
|
const drawCarabaForTriangle = (roof, canvas, textMode, edgeIndex) => {
|
|
const points = roof.getCurrentPoints()
|
|
|
|
if (!points || points.length !== 3) {
|
|
console.warn('삼각형이 아니거나 유효하지 않은 점 데이터입니다.')
|
|
return
|
|
}
|
|
|
|
if (edgeIndex < 0 || edgeIndex >= 3) {
|
|
console.warn('유효하지 않은 edge 인덱스입니다.')
|
|
return
|
|
}
|
|
|
|
// 선택된 edge의 두 꼭짓점을 제외한 나머지 점 찾기
|
|
const oppositeVertexIndex = (edgeIndex + 2) % 3
|
|
const oppositeVertex = points[oppositeVertexIndex]
|
|
|
|
// 선택된 edge의 시작점과 끝점
|
|
const edgeStartIndex = edgeIndex
|
|
const edgeEndIndex = (edgeIndex + 1) % 3
|
|
const edgeStart = points[edgeStartIndex]
|
|
const edgeEnd = points[edgeEndIndex]
|
|
|
|
// 선택된 edge의 중점 계산
|
|
const edgeMidPoint = {
|
|
x: (edgeStart.x + edgeEnd.x) / 2,
|
|
y: (edgeStart.y + edgeEnd.y) / 2,
|
|
}
|
|
|
|
// 맞은편 꼭짓점에서 선택된 edge의 중점으로 가는 직선 생성
|
|
const carabaLine = new QLine([oppositeVertex.x, oppositeVertex.y, edgeMidPoint.x, edgeMidPoint.y], {
|
|
parentId: roof.id,
|
|
fontSize: roof.fontSize,
|
|
stroke: '#FF0000',
|
|
strokeWidth: 2,
|
|
name: LINE_TYPE.SUBLINE.RIDGE,
|
|
textMode: textMode,
|
|
attributes: {
|
|
type: LINE_TYPE.SUBLINE.RIDGE,
|
|
planeSize: calcLinePlaneSize({
|
|
x1: oppositeVertex.x,
|
|
y1: oppositeVertex.y,
|
|
x2: edgeMidPoint.x,
|
|
y2: edgeMidPoint.y,
|
|
}),
|
|
actualSize: calcLineActualSize(
|
|
{
|
|
x1: oppositeVertex.x,
|
|
y1: oppositeVertex.y,
|
|
x2: edgeMidPoint.x,
|
|
y2: edgeMidPoint.y,
|
|
},
|
|
getDegreeByChon(roof.lines[edgeIndex]?.attributes?.pitch || 30),
|
|
),
|
|
roofId: roof.id,
|
|
isRidge: true,
|
|
},
|
|
})
|
|
|
|
// 캔버스에 추가
|
|
canvas.add(carabaLine)
|
|
carabaLine.bringToFront()
|
|
|
|
// 지붕 객체에 저장
|
|
roof.innerLines = [carabaLine]
|
|
|
|
// canvas에 skeleton 상태 저장
|
|
if (!canvas.skeletonStates) {
|
|
canvas.skeletonStates = {}
|
|
}
|
|
canvas.skeletonStates[roof.id] = true
|
|
|
|
canvas.renderAll()
|
|
}
|
|
|
|
/**
|
|
* 다각형에 대한 캐라바 처리
|
|
* @param {QPolygon} roof - 지붕 객체
|
|
* @param {fabric.Canvas} canvas - 캔버스 객체
|
|
* @param {string} textMode - 텍스트 모드
|
|
* @param {number} selectedEdgeIndex - 선택된 외곽선의 인덱스
|
|
*/
|
|
const drawCarabaForPolygon = (roof, canvas, textMode, selectedEdgeIndex) => {
|
|
const points = roof.getCurrentPoints()
|
|
|
|
if (!points || points.length < 3) {
|
|
console.warn('유효하지 않은 다각형 점 데이터입니다.')
|
|
return
|
|
}
|
|
|
|
if (selectedEdgeIndex < 0 || selectedEdgeIndex >= points.length) {
|
|
console.warn('유효하지 않은 edge 인덱스입니다.')
|
|
return
|
|
}
|
|
|
|
// 삼각형인 경우 기존 로직 사용
|
|
if (points.length === 3) {
|
|
drawCarabaForTriangle(roof, canvas, textMode, selectedEdgeIndex)
|
|
return
|
|
}
|
|
|
|
// 먼저 스켈레톤을 생성하여 내부 구조를 파악
|
|
const geoJSONPolygon = toGeoJSON(points)
|
|
geoJSONPolygon.pop() // 마지막 좌표 제거
|
|
|
|
let skeleton
|
|
try {
|
|
skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
|
} catch (e) {
|
|
console.error('스켈레톤 생성 중 오류:', e)
|
|
return
|
|
}
|
|
|
|
if (!skeleton || !skeleton.Edges) {
|
|
console.warn('스켈레톤 생성에 실패했습니다.')
|
|
return
|
|
}
|
|
|
|
// 선택된 외곽선의 시작점과 끝점
|
|
const selectedStart = points[selectedEdgeIndex]
|
|
const selectedEnd = points[(selectedEdgeIndex + 1) % points.length]
|
|
|
|
const innerLines = []
|
|
const processedInnerEdges = new Set()
|
|
|
|
// 스켈레톤의 모든 내부선을 수집
|
|
for (const edgeResult of skeleton.Edges) {
|
|
const polygonPoints = edgeResult.Polygon.map((p) => ({ x: p.X, y: p.Y }))
|
|
|
|
for (let i = 0; i < polygonPoints.length; i++) {
|
|
const p1 = polygonPoints[i]
|
|
const p2 = polygonPoints[(i + 1) % polygonPoints.length]
|
|
|
|
// 선택된 외곽선은 제외
|
|
const isSelectedEdge =
|
|
(arePointsEqual(selectedStart, p1) && arePointsEqual(selectedEnd, p2)) ||
|
|
(arePointsEqual(selectedStart, p2) && arePointsEqual(selectedEnd, p1))
|
|
|
|
if (isSelectedEdge) continue
|
|
|
|
// 다른 외곽선들은 제외
|
|
const isOtherOuterEdge = roof.lines.some((line, idx) => {
|
|
if (idx === selectedEdgeIndex) return false
|
|
return (
|
|
(arePointsEqual(line.startPoint, p1) && arePointsEqual(line.endPoint, p2)) ||
|
|
(arePointsEqual(line.startPoint, p2) && arePointsEqual(line.endPoint, p1))
|
|
)
|
|
})
|
|
|
|
if (isOtherOuterEdge) continue
|
|
|
|
const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|')
|
|
|
|
if (processedInnerEdges.has(edgeKey)) continue
|
|
processedInnerEdges.add(edgeKey)
|
|
|
|
// 선택된 외곽선에 수직으로 연장되는 선들만 처리
|
|
const selectedLineAngle = calculateAngle(selectedStart, selectedEnd)
|
|
const innerLineAngle = calculateAngle(p1, p2)
|
|
const angleDiff = Math.abs(selectedLineAngle - innerLineAngle)
|
|
const isPerpendicular = Math.abs(angleDiff - 90) < 5 || Math.abs(angleDiff - 270) < 5
|
|
|
|
if (isPerpendicular) {
|
|
// 선택된 외곽선 방향으로 연장
|
|
const extendedLine = extendLineToOppositeEdge(p1, p2, points, selectedEdgeIndex)
|
|
|
|
if (extendedLine) {
|
|
const carabaLine = new QLine([extendedLine.start.x, extendedLine.start.y, extendedLine.end.x, extendedLine.end.y], {
|
|
parentId: roof.id,
|
|
fontSize: roof.fontSize,
|
|
stroke: '#FF0000',
|
|
strokeWidth: 2,
|
|
name: LINE_TYPE.SUBLINE.RIDGE,
|
|
textMode: textMode,
|
|
attributes: {
|
|
type: LINE_TYPE.SUBLINE.RIDGE,
|
|
planeSize: calcLinePlaneSize({
|
|
x1: extendedLine.start.x,
|
|
y1: extendedLine.start.y,
|
|
x2: extendedLine.end.x,
|
|
y2: extendedLine.end.y,
|
|
}),
|
|
actualSize: calcLineActualSize(
|
|
{
|
|
x1: extendedLine.start.x,
|
|
y1: extendedLine.start.y,
|
|
x2: extendedLine.end.x,
|
|
y2: extendedLine.end.y,
|
|
},
|
|
getDegreeByChon(roof.lines[selectedEdgeIndex]?.attributes?.pitch || 30),
|
|
),
|
|
roofId: roof.id,
|
|
isRidge: true,
|
|
},
|
|
})
|
|
|
|
innerLines.push(carabaLine)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 캔버스에 추가
|
|
innerLines.forEach((line) => {
|
|
canvas.add(line)
|
|
line.bringToFront()
|
|
})
|
|
|
|
// 지붕 객체에 저장
|
|
roof.innerLines = innerLines
|
|
|
|
// canvas에 skeleton 상태 저장
|
|
if (!canvas.skeletonStates) {
|
|
canvas.skeletonStates = {}
|
|
}
|
|
canvas.skeletonStates[roof.id] = true
|
|
|
|
canvas.renderAll()
|
|
}
|
|
|
|
/**
|
|
* 선분을 맞은편 외곽선까지 연장하는 함수
|
|
*/
|
|
const extendLineToOppositeEdge = (p1, p2, polygonPoints, selectedEdgeIndex) => {
|
|
// 선분의 방향 벡터 계산
|
|
const direction = {
|
|
x: p2.x - p1.x,
|
|
y: p2.y - p1.y,
|
|
}
|
|
|
|
// 방향 벡터 정규화
|
|
const length = Math.sqrt(direction.x * direction.x + direction.y * direction.y)
|
|
|
|
if (length === 0) return null
|
|
|
|
const normalizedDir = {
|
|
x: direction.x / length,
|
|
y: direction.y / length,
|
|
}
|
|
|
|
// 선택된 외곽선의 반대편 찾기
|
|
const oppositeEdgeIndex = (selectedEdgeIndex + Math.floor(polygonPoints.length / 2)) % polygonPoints.length
|
|
const oppositeStart = polygonPoints[oppositeEdgeIndex]
|
|
const oppositeEnd = polygonPoints[(oppositeEdgeIndex + 1) % polygonPoints.length]
|
|
|
|
// p1에서 시작해서 반대편까지 연장
|
|
const extendedStart = { x: p1.x, y: p1.y }
|
|
const extendedEnd = findIntersectionWithEdge(p1, normalizedDir, oppositeStart, oppositeEnd) || { x: p2.x, y: p2.y }
|
|
|
|
return {
|
|
start: extendedStart,
|
|
end: extendedEnd,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 선분과 외곽선의 교점을 찾는 함수
|
|
*/
|
|
const findIntersectionWithEdge = (lineStart, lineDir, edgeStart, edgeEnd) => {
|
|
const edgeDir = {
|
|
x: edgeEnd.x - edgeStart.x,
|
|
y: edgeEnd.y - edgeStart.y,
|
|
}
|
|
|
|
const denominator = lineDir.x * edgeDir.y - lineDir.y * edgeDir.x
|
|
if (Math.abs(denominator) < 1e-10) return null // 평행선
|
|
|
|
const t = ((edgeStart.x - lineStart.x) * edgeDir.y - (edgeStart.y - lineStart.y) * edgeDir.x) / denominator
|
|
const u = ((edgeStart.x - lineStart.x) * lineDir.y - (edgeStart.y - lineStart.y) * lineDir.x) / denominator
|
|
|
|
if (t >= 0 && u >= 0 && u <= 1) {
|
|
return {
|
|
x: lineStart.x + t * lineDir.x,
|
|
y: lineStart.y + t * lineDir.y,
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Transforms the selected wall line into a roof structure
|
|
* @param {QPolygon} roof - The roof object
|
|
* @param {fabric.Canvas} canvas - The canvas object
|
|
* @param {number} edgeIndex - Index of the selected edge
|
|
*/
|
|
const transformWallToRoof = (roof, canvas, edgeIndex) => {
|
|
// Get the current points
|
|
const points = roof.getCurrentPoints()
|
|
|
|
if (!points || points.length < 3) {
|
|
console.warn('Invalid polygon points')
|
|
return
|
|
}
|
|
|
|
// Get the selected edge points
|
|
const p1 = points[edgeIndex]
|
|
const p2 = points[(edgeIndex + 1) % points.length]
|
|
|
|
// Calculate mid point of the selected edge
|
|
const midX = (p1.x + p2.x) / 2
|
|
const midY = (p1.y + p2.y) / 2
|
|
|
|
// Calculate the perpendicular vector (for the roof ridge)
|
|
const dx = p2.x - p1.x
|
|
const dy = p2.y - p1.y
|
|
const length = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
// Normal vector (perpendicular to the edge)
|
|
const nx = -dy / length
|
|
const ny = dx / length
|
|
|
|
// Calculate the ridge point (extending inward from the middle of the edge)
|
|
const ridgeLength = length * 0.4 // Adjust this factor as needed
|
|
const ridgeX = midX + nx * ridgeLength
|
|
const ridgeY = midY + ny * ridgeLength
|
|
|
|
// Create the new points for the roof
|
|
const newPoints = [...points]
|
|
newPoints.splice(edgeIndex + 1, 0, { x: ridgeX, y: ridgeY })
|
|
|
|
// Update the roof with new points
|
|
roof.set({
|
|
points: newPoints,
|
|
// Ensure the polygon is re-rendered
|
|
dirty: true,
|
|
})
|
|
|
|
// Update the polygon's path
|
|
roof.setCoords()
|
|
|
|
// Force a re-render of the canvas
|
|
canvas.renderAll()
|
|
|
|
return roof
|
|
}
|
|
|
|
/**
|
|
* 사용 예제: 첫 번째 edge를 45도 각도로 변형
|
|
* drawSkeletonWithTransformedEdges(roofId, canvas, textMode, [
|
|
* { edgeIndex: 0, angleOffset: 45, splitRatio: 0.5 }
|
|
* ]);
|
|
*/
|
|
|
|
class Advanced2DRoofBuilder extends SkeletonBuilder {
|
|
static Build2DRoofFromAdvancedProperties(geoJsonPolygon, edgeProperties) {
|
|
// 입력 데이터 검증
|
|
if (!geoJsonPolygon || !Array.isArray(geoJsonPolygon) || geoJsonPolygon.length === 0) {
|
|
throw new Error('geoJsonPolygon이 유효하지 않습니다')
|
|
}
|
|
|
|
if (!edgeProperties || !Array.isArray(edgeProperties)) {
|
|
throw new Error('edgeProperties가 유효하지 않습니다')
|
|
}
|
|
|
|
console.log('입력 검증 통과')
|
|
console.log('geoJsonPolygon:', geoJsonPolygon)
|
|
console.log('edgeProperties:', edgeProperties)
|
|
|
|
// 1. 입력 폴리곤을 edgeProperties에 따라 수정
|
|
const modifiedPolygon = this.preprocessPolygonByEdgeTypes(geoJsonPolygon, edgeProperties)
|
|
|
|
// 2. 수정된 폴리곤으로 skeleton 생성
|
|
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[modifiedPolygon]])
|
|
|
|
if (!skeleton || !skeleton.Edges) {
|
|
throw new Error('Skeleton 생성 실패')
|
|
}
|
|
|
|
// 3. Edge 분석
|
|
const edgeAnalysis = this.analyzeAdvancedEdgeTypes(edgeProperties)
|
|
|
|
return {
|
|
skeleton: skeleton,
|
|
original_polygon: geoJsonPolygon,
|
|
modified_polygon: modifiedPolygon,
|
|
roof_type: edgeAnalysis.roof_type,
|
|
edge_analysis: edgeAnalysis,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ✅ 안전한 폴리곤 전처리
|
|
*/
|
|
static preprocessPolygonByEdgeTypes(geoJsonPolygon, edgeProperties) {
|
|
try {
|
|
const originalRing = geoJsonPolygon
|
|
|
|
if (!Array.isArray(originalRing) || originalRing.length < 4) {
|
|
throw new Error('외곽선이 유효하지 않습니다')
|
|
}
|
|
|
|
const modifiedRing = originalRing.map((point) => {
|
|
if (!Array.isArray(point) || point.length < 2) {
|
|
throw new Error('좌표점 형식이 잘못되었습니다')
|
|
}
|
|
return [point[0], point[1]]
|
|
})
|
|
|
|
const isClosedPolygon = this.isPolygonClosed(modifiedRing)
|
|
if (isClosedPolygon) {
|
|
modifiedRing.pop()
|
|
}
|
|
|
|
const actualEdgeCount = modifiedRing.length
|
|
const edgeCountToProcess = Math.min(edgeProperties.length, actualEdgeCount)
|
|
|
|
for (let i = 0; i < edgeCountToProcess; i++) {
|
|
const edgeProp = edgeProperties[i]
|
|
const edgeType = edgeProp?.edge_type
|
|
|
|
console.log(`Processing edge ${i}: ${edgeType}`)
|
|
|
|
try {
|
|
switch (edgeType) {
|
|
case 'EAVES':
|
|
// ✅ 수정: 처마는 기본 상태이므로 수정하지 않음
|
|
console.log(`Edge ${i}: EAVES - 기본 처마 상태 유지`)
|
|
break
|
|
|
|
case 'WALL':
|
|
// ✅ 수정: 처마를 벽으로 변경
|
|
this.transformEavesToWall(modifiedRing, i, edgeProp)
|
|
break
|
|
|
|
case 'GABLE':
|
|
// ✅ 수정: 처마를 케라바로 변경
|
|
this.transformEavesToGable(modifiedRing, i, edgeProp)
|
|
break
|
|
|
|
default:
|
|
console.warn(`알 수 없는 edge 타입: ${edgeType}, 기본 EAVES로 처리`)
|
|
}
|
|
} catch (edgeError) {
|
|
console.error(`Edge ${i} 처리 중 오류:`, edgeError)
|
|
}
|
|
}
|
|
|
|
const finalPolygon = this.prepareFinalPolygon(modifiedRing)
|
|
return finalPolygon
|
|
} catch (error) {
|
|
console.error('폴리곤 전처리 오류:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ✅ 처마를 벽으로 변경 (내부로 수축)
|
|
*/
|
|
static transformEavesToWall(ring, edgeIndex, edgeProp) {
|
|
console.log(`transformEavesToWall: edgeIndex=${edgeIndex}`)
|
|
|
|
if (!ring || !Array.isArray(ring)) {
|
|
console.error('ring이 배열이 아니거나 undefined입니다')
|
|
return
|
|
}
|
|
|
|
const totalPoints = ring.length
|
|
|
|
if (edgeIndex < 0 || edgeIndex >= totalPoints) {
|
|
console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`)
|
|
return
|
|
}
|
|
|
|
const p1 = ring[edgeIndex]
|
|
const nextIndex = (edgeIndex + 1) % totalPoints
|
|
const p2 = ring[nextIndex]
|
|
|
|
if (!Array.isArray(p1) || p1.length < 2 || !Array.isArray(p2) || p2.length < 2) {
|
|
console.error('점 형식이 잘못되었습니다')
|
|
return
|
|
}
|
|
|
|
try {
|
|
// 폴리곤 중심 계산
|
|
const centerX = ring.reduce((sum, p) => sum + p[0], 0) / totalPoints
|
|
const centerY = ring.reduce((sum, p) => sum + p[1], 0) / totalPoints
|
|
|
|
// edge 중점
|
|
const midX = (p1[0] + p2[0]) / 2
|
|
const midY = (p1[1] + p2[1]) / 2
|
|
|
|
// 내향 방향 (처마 → 벽: 안쪽으로 수축)
|
|
const dirX = centerX - midX
|
|
const dirY = centerY - midY
|
|
const length = Math.sqrt(dirX * dirX + dirY * dirY)
|
|
|
|
if (length < 0.001) {
|
|
console.warn('내향 방향 벡터 길이가 거의 0입니다')
|
|
return
|
|
}
|
|
|
|
const unitX = dirX / length
|
|
const unitY = dirY / length
|
|
const shrinkDistance = edgeProp.shrink_distance || 0.8 // 벽 수축 거리
|
|
|
|
// 점들을 내부로 이동
|
|
ring[edgeIndex] = [p1[0] + unitX * shrinkDistance, p1[1] + unitY * shrinkDistance]
|
|
|
|
ring[nextIndex] = [p2[0] + unitX * shrinkDistance, p2[1] + unitY * shrinkDistance]
|
|
|
|
console.log(`✅ WALL: Edge ${edgeIndex} 내부로 수축 완료 (${shrinkDistance})`)
|
|
} catch (calcError) {
|
|
console.error('벽 변환 계산 중 오류:', calcError)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ✅ 처마를 케라바로 변경 (특별한 형태로 변형)
|
|
*/
|
|
// static transformEavesToGable(ring, edgeIndex, edgeProp) {
|
|
// console.log(`transformEavesToGable: edgeIndex=${edgeIndex}`);
|
|
//
|
|
// // 안전성 검증
|
|
// if (!ring || !Array.isArray(ring)) {
|
|
// console.error('ring이 배열이 아니거나 undefined입니다');
|
|
// return;
|
|
// }
|
|
//
|
|
// const totalPoints = ring.length;
|
|
//
|
|
// if (edgeIndex < 0 || edgeIndex >= totalPoints) {
|
|
// console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`);
|
|
// return;
|
|
// }
|
|
//
|
|
// const p1 = ring[edgeIndex];
|
|
// const nextIndex = (edgeIndex + 1) % totalPoints;
|
|
// const p2 = ring[nextIndex];
|
|
//
|
|
// if (!Array.isArray(p1) || p1.length < 2 ||
|
|
// !Array.isArray(p2) || p2.length < 2) {
|
|
// console.error('점 형식이 잘못되었습니다');
|
|
// return;
|
|
// }
|
|
//
|
|
// try {
|
|
// // 케라바 변형: edge를 직선화하고 약간 내부로 이동
|
|
// const centerX = ring.reduce((sum, p) => sum + p[0], 0) / totalPoints;
|
|
// const centerY = ring.reduce((sum, p) => sum + p[1], 0) / totalPoints;
|
|
//
|
|
// const midX = (p1[0] + p2[0]) / 2;
|
|
// const midY = (p1[1] + p2[1]) / 2;
|
|
//
|
|
// // 내향 방향으로 약간 이동
|
|
// const dirX = centerX - midX;
|
|
// const dirY = centerY - midY;
|
|
// const length = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
//
|
|
// if (length < 0.001) {
|
|
// console.warn('내향 방향 벡터 길이가 거의 0입니다');
|
|
// return;
|
|
// }
|
|
//
|
|
// const unitX = dirX / length;
|
|
// const unitY = dirY / length;
|
|
// const gableInset = edgeProp.gable_inset || 0.3; // 케라바 안쪽 이동 거리
|
|
//
|
|
// // edge의 방향 벡터
|
|
// const edgeVecX = p2[0] - p1[0];
|
|
// const edgeVecY = p2[1] - p1[1];
|
|
//
|
|
// // 새로운 중점 (안쪽으로 이동)
|
|
// const newMidX = midX + unitX * gableInset;
|
|
// const newMidY = midY + unitY * gableInset;
|
|
//
|
|
// // 케라바를 위한 직선화된 점들
|
|
// ring[edgeIndex] = [
|
|
// newMidX - edgeVecX * 0.5,
|
|
// newMidY - edgeVecY * 0.5
|
|
// ];
|
|
//
|
|
// ring[nextIndex] = [
|
|
// newMidX + edgeVecX * 0.5,
|
|
// newMidY + edgeVecY * 0.5
|
|
// ];
|
|
//
|
|
// console.log(`✅ GABLE: Edge ${edgeIndex} 케라바 변형 완료`);
|
|
//
|
|
// } catch (calcError) {
|
|
// console.error('케라바 변환 계산 중 오류:', calcError);
|
|
// }
|
|
// }
|
|
|
|
static transformEavesToGable(ring, edgeIndex, edgeProp) {
|
|
// ✅ 캐라바면을 위한 특별 처리
|
|
// 해당 edge를 "직선 제약 조건"으로 만들어야 함
|
|
|
|
const p1 = ring[edgeIndex]
|
|
const nextIndex = (edgeIndex + 1) % ring.length
|
|
const p2 = ring[nextIndex]
|
|
|
|
// 캐라바면: edge를 완전히 직선으로 고정
|
|
// 이렇게 하면 skeleton이 이 edge에 수직으로만 생성됨
|
|
|
|
// 중간점들을 제거하여 직선화
|
|
const midX = (p1[0] + p2[0]) / 2
|
|
const midY = (p1[1] + p2[1]) / 2
|
|
|
|
// 캐라바 edge를 단순 직선으로 만들어
|
|
// SkeletonBuilder가 여기서 직선 skeleton을 생성하도록 유도
|
|
console.log(`✅ GABLE: Edge ${edgeIndex}를 직선 캐라바로 설정`)
|
|
}
|
|
|
|
// analyzeAdvancedEdgeTypes도 수정
|
|
static analyzeAdvancedEdgeTypes(edgeProperties) {
|
|
const eavesEdges = [] // 기본 처마 (수정 안함)
|
|
const wallEdges = [] // 처마→벽 변경
|
|
const gableEdges = [] // 처마→케라바 변경
|
|
|
|
edgeProperties.forEach((prop, i) => {
|
|
switch (prop?.edge_type) {
|
|
case 'EAVES':
|
|
eavesEdges.push(i)
|
|
break
|
|
case 'WALL':
|
|
wallEdges.push(i)
|
|
break
|
|
case 'GABLE':
|
|
gableEdges.push(i)
|
|
break
|
|
default:
|
|
console.warn(`Edge ${i}: 알 수 없는 타입 ${prop?.edge_type}, 기본 EAVES로 처리`)
|
|
eavesEdges.push(i)
|
|
}
|
|
})
|
|
|
|
let roofType
|
|
if (wallEdges.length === 0 && gableEdges.length === 0) {
|
|
roofType = 'pavilion' // 모든 면이 처마
|
|
} else if (wallEdges.length === 4 && gableEdges.length === 0) {
|
|
roofType = 'hipped' // 모든 면이 벽
|
|
} else if (gableEdges.length === 2 && wallEdges.length === 2) {
|
|
roofType = 'gabled' // 박공지붕
|
|
} else {
|
|
roofType = 'complex' // 복합지붕
|
|
}
|
|
|
|
return {
|
|
roof_type: roofType,
|
|
eaves_edges: eavesEdges, // 기본 처마
|
|
wall_edges: wallEdges, // 처마→벽
|
|
gable_edges: gableEdges, // 처마→케라바
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ✅ 폴리곤이 닫혀있는지 확인
|
|
*/
|
|
static isPolygonClosed(ring) {
|
|
if (!ring || ring.length < 2) return false
|
|
|
|
const firstPoint = ring[0]
|
|
const lastPoint = ring[ring.length - 1]
|
|
|
|
const tolerance = 0.0001 // 부동소수점 허용 오차
|
|
|
|
return Math.abs(firstPoint[0] - lastPoint[0]) < tolerance && Math.abs(firstPoint[1] - lastPoint[1]) < tolerance
|
|
}
|
|
|
|
/**
|
|
* ✅ BuildFromGeoJSON용 최종 polygon 준비
|
|
*/
|
|
static prepareFinalPolygon(ring) {
|
|
// 1. 최소 점 개수 확인
|
|
if (ring.length < 3) {
|
|
throw new Error(`폴리곤 점이 부족합니다: ${ring.length}개 (최소 3개 필요)`)
|
|
}
|
|
|
|
// 2. 닫힌 폴리곤인지 다시 확인
|
|
const isClosed = this.isPolygonClosed(ring)
|
|
|
|
if (isClosed) {
|
|
console.log('여전히 닫힌 폴리곤입니다. 마지막 점 제거')
|
|
return ring.slice(0, -1) // 마지막 점 제거
|
|
}
|
|
|
|
// 3. 열린 폴리곤이면 그대로 반환
|
|
console.log('열린 폴리곤 상태로 BuildFromGeoJSON에 전달')
|
|
return [...ring] // 복사본 반환
|
|
}
|
|
|
|
static expandEdgeForEaves(ring, edgeIndex, overhang) {
|
|
console.log(`expandEdgeForEaves 시작: edgeIndex=${edgeIndex}, overhang=${overhang}`)
|
|
|
|
// 안전성 검증
|
|
if (!ring || !Array.isArray(ring)) {
|
|
console.error('ring이 배열이 아니거나 undefined입니다')
|
|
return
|
|
}
|
|
|
|
const totalPoints = ring.length - 1 // 마지막 중복점 제외
|
|
console.log(`ring 길이: ${ring.length}, totalPoints: ${totalPoints}`)
|
|
|
|
if (totalPoints <= 2) {
|
|
console.error('ring 점 개수가 부족합니다')
|
|
return
|
|
}
|
|
|
|
if (edgeIndex < 0 || edgeIndex >= totalPoints) {
|
|
console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`)
|
|
return
|
|
}
|
|
|
|
// 안전한 점 접근
|
|
const p1 = ring[edgeIndex]
|
|
const nextIndex = (edgeIndex + 1) % totalPoints
|
|
const p2 = ring[nextIndex]
|
|
|
|
console.log(`p1 (index ${edgeIndex}):`, p1)
|
|
console.log(`p2 (index ${nextIndex}):`, p2)
|
|
|
|
if (!Array.isArray(p1) || p1.length < 2 || !Array.isArray(p2) || p2.length < 2) {
|
|
console.error('점 형식이 잘못되었습니다')
|
|
console.error('p1:', p1, 'p2:', p2)
|
|
return
|
|
}
|
|
|
|
if (typeof p1[0] !== 'number' || typeof p1[1] !== 'number' || typeof p2[0] !== 'number' || typeof p2[1] !== 'number') {
|
|
console.error('좌표값이 숫자가 아닙니다')
|
|
return
|
|
}
|
|
|
|
try {
|
|
// 폴리곤 중심 계산 (마지막 중복점 제외)
|
|
const validPoints = ring.slice(0, totalPoints)
|
|
const centerX = validPoints.reduce((sum, p) => sum + p[0], 0) / totalPoints
|
|
const centerY = validPoints.reduce((sum, p) => sum + p[1], 0) / totalPoints
|
|
|
|
console.log(`중심점: (${centerX}, ${centerY})`)
|
|
|
|
// edge 중점
|
|
const midX = (p1[0] + p2[0]) / 2
|
|
const midY = (p1[1] + p2[1]) / 2
|
|
|
|
// 외향 방향
|
|
const dirX = midX - centerX
|
|
const dirY = midY - centerY
|
|
const length = Math.sqrt(dirX * dirX + dirY * dirY)
|
|
|
|
if (length < 0.001) {
|
|
// 거의 0인 경우
|
|
console.warn('외향 방향 벡터 길이가 거의 0입니다, 확장하지 않습니다')
|
|
return
|
|
}
|
|
|
|
const unitX = dirX / length
|
|
const unitY = dirY / length
|
|
|
|
// 안전하게 점 수정
|
|
ring[edgeIndex] = [p1[0] + unitX * overhang, p1[1] + unitY * overhang]
|
|
|
|
ring[nextIndex] = [p2[0] + unitX * overhang, p2[1] + unitY * overhang]
|
|
|
|
console.log(`✅ EAVES: Edge ${edgeIndex} 확장 완료 (${overhang})`)
|
|
console.log('수정된 p1:', ring[edgeIndex])
|
|
console.log('수정된 p2:', ring[nextIndex])
|
|
} catch (calcError) {
|
|
console.error('계산 중 오류:', calcError)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ✅ 안전한 박공 조정
|
|
*/
|
|
static adjustEdgeForGable(ring, edgeIndex, gableHeight) {
|
|
console.log(`adjustEdgeForGable 시작: edgeIndex=${edgeIndex}`)
|
|
|
|
// 안전성 검증 (동일한 패턴)
|
|
if (!ring || !Array.isArray(ring)) {
|
|
console.error('ring이 배열이 아니거나 undefined입니다')
|
|
return
|
|
}
|
|
|
|
const totalPoints = ring.length - 1
|
|
|
|
if (totalPoints <= 2) {
|
|
console.error('ring 점 개수가 부족합니다')
|
|
return
|
|
}
|
|
|
|
if (edgeIndex < 0 || edgeIndex >= totalPoints) {
|
|
console.error(`edgeIndex ${edgeIndex}가 범위를 벗어났습니다`)
|
|
return
|
|
}
|
|
|
|
const p1 = ring[edgeIndex]
|
|
const nextIndex = (edgeIndex + 1) % totalPoints
|
|
const p2 = ring[nextIndex]
|
|
|
|
if (!Array.isArray(p1) || p1.length < 2 || !Array.isArray(p2) || p2.length < 2) {
|
|
console.error('점 형식이 잘못되었습니다')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const validPoints = ring.slice(0, totalPoints)
|
|
const centerX = validPoints.reduce((sum, p) => sum + p[0], 0) / totalPoints
|
|
const centerY = validPoints.reduce((sum, p) => sum + p[1], 0) / totalPoints
|
|
|
|
console.log(`중심점: (${centerX}, ${centerY})`)
|
|
|
|
// edge 중점
|
|
const midX = (p1[0] + p2[0]) / 2
|
|
const midY = (p1[1] + p2[1]) / 2
|
|
|
|
// 외향 방향
|
|
const dirX = centerX - midX
|
|
const dirY = centerY - midY
|
|
const length = Math.sqrt(dirX * dirX + dirY * dirY)
|
|
|
|
if (length < 0.001) {
|
|
console.warn('중심 방향 벡터 길이가 거의 0입니다')
|
|
return
|
|
}
|
|
|
|
const unitX = dirX / length
|
|
const unitY = dirY / length
|
|
const insetDistance = 0.5
|
|
|
|
const newMidX = midX + unitX * insetDistance
|
|
const newMidY = midY + unitY * insetDistance
|
|
|
|
const edgeVecX = p2[0] - p1[0]
|
|
const edgeVecY = p2[1] - p1[1]
|
|
|
|
ring[edgeIndex] = [newMidX - edgeVecX * 0.5, newMidY - edgeVecY * 0.5]
|
|
|
|
ring[nextIndex] = [newMidX + edgeVecX * 0.5, newMidY + edgeVecY * 0.5]
|
|
|
|
console.log(`✅ GABLE: Edge ${edgeIndex} 조정 완료`)
|
|
} catch (calcError) {
|
|
console.error('박공 조정 계산 중 오류:', calcError)
|
|
}
|
|
}
|
|
|
|
static processGableSkeleton(skeleton, gableEdgeIndex, originalPolygon) {
|
|
// ✅ Gable edge에 해당하는 skeleton 정점들을 찾아서
|
|
// 해당 edge의 중점으로 강제 이동
|
|
|
|
const gableEdge = originalPolygon[gableEdgeIndex]
|
|
const edgeMidpoint = calculateMidpoint(gableEdge)
|
|
|
|
// skeleton 정점들을 edge 중점으로 "압축"
|
|
skeleton.Edges.forEach((edge) => {
|
|
if (isRelatedToGableEdge(edge, gableEdgeIndex)) {
|
|
// 해당 edge 관련 skeleton 정점들을 직선으로 정렬
|
|
straightenSkeletonToEdge(edge, edgeMidpoint)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ✅ Gable edge에 제약 조건을 추가하여 skeleton 생성
|
|
static buildConstrainedSkeleton(polygon, edgeConstraints) {
|
|
const constraints = edgeConstraints
|
|
.map((constraint) => {
|
|
if (constraint.type === 'GABLE') {
|
|
return {
|
|
edgeIndex: constraint.edgeIndex,
|
|
forceLinear: true, // 직선 강제
|
|
fixToMidpoint: true, // 중점 고정
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
.filter((c) => c !== null)
|
|
|
|
// 제약 조건이 적용된 skeleton 생성
|
|
return SkeletonBuilder.build(polygon, constraints)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 지붕 폴리곤의 좌표를 스켈레톤 생성에 적합한 형태로 전처리합니다.
|
|
* - 연속된 중복 좌표를 제거합니다.
|
|
* - 폴리곤이 닫힌 형태가 되도록 마지막 좌표를 확인하고 필요시 제거합니다.
|
|
* - 좌표를 시계 방향으로 정렬합니다.
|
|
* @param {Array<object>} initialPoints - 초기 폴리곤 좌표 배열 (e.g., [{x: 10, y: 10}, ...])
|
|
* @returns {Array<Array<number>>} 전처리된 좌표 배열 (e.g., [[10, 10], ...])
|
|
*/
|
|
const preprocessPolygonCoordinates = (initialPoints) => {
|
|
// fabric.Point 객체를 [x, y] 배열로 변환
|
|
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();
|
|
}
|
|
|
|
// SkeletonBuilder 라이브러리는 시계 방향 순서의 좌표를 요구하므로 배열을 뒤집습니다.
|
|
coordinates.reverse();
|
|
|
|
return coordinates;
|
|
};
|
|
|
|
const isSameLine = (edgeStartX, edgeStartY, edgeEndX, edgeEndY, baseLine) => {
|
|
const tolerance = 0.1
|
|
|
|
// 시계방향 매칭 (edgeResult.Edge.Begin -> End와 baseLine.x1 -> x2)
|
|
const clockwiseMatch =
|
|
Math.abs(edgeStartX - baseLine.x1) < tolerance &&
|
|
Math.abs(edgeStartY - baseLine.y1) < tolerance &&
|
|
Math.abs(edgeEndX - baseLine.x2) < tolerance &&
|
|
Math.abs(edgeEndY - baseLine.y2) < tolerance
|
|
|
|
// 반시계방향 매칭 (edgeResult.Edge.Begin -> End와 baseLine.x2 -> x1)
|
|
const counterClockwiseMatch =
|
|
Math.abs(edgeStartX - baseLine.x2) < tolerance &&
|
|
Math.abs(edgeStartY - baseLine.y2) < tolerance &&
|
|
Math.abs(edgeEndX - baseLine.x1) < tolerance &&
|
|
Math.abs(edgeEndY - baseLine.y1) < tolerance
|
|
|
|
return clockwiseMatch || counterClockwiseMatch
|
|
}
|
|
|
|
/**
|
|
* skeletonLines에서 특정 점(polyPoint)을 지나는 라인을 찾는 함수
|
|
* @param {Array} skeletonLines - 검색할 라인 배열
|
|
* @param {Object} polyPoint - 검색할 점 {X, Y}
|
|
* @returns {Array} - 일치하는 라인 배열
|
|
*/
|
|
function findLinesPassingPoint(skeletonLines, polyPoint) {
|
|
return skeletonLines.filter(line => {
|
|
// 라인의 시작점이나 끝점이 polyPoint와 일치하는지 확인
|
|
const isP1Match = (Math.abs(line.p1.x - polyPoint.X) < 0.001 &&
|
|
Math.abs(line.p1.y - polyPoint.Y) < 0.001);
|
|
const isP2Match = (Math.abs(line.p2.x - polyPoint.X) < 0.001 &&
|
|
Math.abs(line.p2.y - polyPoint.Y) < 0.001);
|
|
|
|
return isP1Match || isP2Match;
|
|
});
|
|
}
|
|
|
|
// 두 선분의 교차점을 찾는 함수
|
|
// 두 선분의 교차점을 찾는 함수 (개선된 버전)
|
|
function findIntersection(p1, p2, p3, p4) {
|
|
// 선분1: p1 -> p2
|
|
// 선분2: p3 -> p4
|
|
|
|
// 선분 방향 벡터
|
|
const d1x = p2.x - p1.x;
|
|
const d1y = p2.y - p1.y;
|
|
const d2x = p4.x - p3.x;
|
|
const d2y = p4.y - p3.y;
|
|
|
|
// 분모 계산
|
|
const denominator = d1x * d2y - d1y * d2x;
|
|
|
|
// 평행한 경우 (또는 매우 가까운 경우)
|
|
// if (Math.abs(denominator) < 0.0001) {
|
|
// return null;
|
|
// }
|
|
|
|
// 매개변수 t와 u 계산
|
|
const t = ((p3.x - p1.x) * d2y - (p3.y - p1.y) * d2x) / denominator;
|
|
const u = ((p3.x - p1.x) * d1y - (p3.y - p1.y) * d1x) / denominator;
|
|
|
|
// 두 선분이 교차하는지 확인 (0 <= t <= 1, 0 <= u <= 1)
|
|
if (t >= -0.001 && t <= 1.001 && u >= -0.001 && u <= 1.001) {
|
|
// 교차점 계산
|
|
const x = p1.x + t * d1x;
|
|
const y = p1.y + t * d1y;
|
|
return { x, y };
|
|
}
|
|
|
|
// 교차하지 않는 경우
|
|
return null;
|
|
}
|
|
|
|
|
|
// baseLine 좌표 추출 헬퍼 함수
|
|
const extractBaseLineCoordinates = (baseLine) => {
|
|
const left = baseLine.left || 0;
|
|
const top = baseLine.top || 0;
|
|
const width = baseLine.width || 0;
|
|
const height = baseLine.height || 0;
|
|
|
|
// 수평선인 경우 (height가 0에 가까움)
|
|
if (Math.abs(height) < 0.1) {
|
|
return {
|
|
p1: { x: left, y: top },
|
|
p2: { x: left + width, y: top }
|
|
};
|
|
}
|
|
// 수직선인 경우 (width가 0에 가까움)
|
|
else if (Math.abs(width) < 0.1) {
|
|
return {
|
|
p1: { x: left, y: top },
|
|
p2: { x: left, y: top + height }
|
|
};
|
|
}
|
|
// 기타 경우 (기본값)
|
|
else {
|
|
return {
|
|
p1: { x: left, y: top },
|
|
p2: { x: left + width, y: top + height }
|
|
};
|
|
}
|
|
};
|
|
|
|
// 연결이 끊어진 라인들을 찾는 함수
|
|
export const findDisconnectedSkeletonLines = (skeletonLines, baseLines, options = {}) => {
|
|
const {
|
|
includeDiagonal = true,
|
|
includeStraight = true,
|
|
minLength = 0
|
|
} = options;
|
|
|
|
if (!skeletonLines?.length) {
|
|
return {
|
|
disconnectedLines: [],
|
|
diagonalLines: [],
|
|
straightLines: [],
|
|
statistics: { total: 0, diagonal: 0, straight: 0, disconnected: 0 }
|
|
};
|
|
}
|
|
|
|
const disconnectedLines = [];
|
|
const diagonalLines = [];
|
|
const straightLines = [];
|
|
|
|
// 점 일치 확인 헬퍼 함수
|
|
const pointsEqual = (p1, p2, epsilon = 0.1) => {
|
|
return Math.abs(p1.x - p2.x) < epsilon && Math.abs(p1.y - p2.y) < epsilon;
|
|
};
|
|
|
|
// baseLine 좌표 추출
|
|
const extractBaseLineCoordinates = (baseLine) => {
|
|
const left = baseLine.left || 0;
|
|
const top = baseLine.top || 0;
|
|
const width = baseLine.width || 0;
|
|
const height = baseLine.height || 0;
|
|
|
|
if (Math.abs(height) < 0.1) {
|
|
return { p1: { x: left, y: top }, p2: { x: left + width, y: top } };
|
|
} else if (Math.abs(width) < 0.1) {
|
|
return { p1: { x: left, y: top }, p2: { x: left, y: top + height } };
|
|
} else {
|
|
return { p1: { x: left, y: top }, p2: { x: left + width, y: top + height } };
|
|
}
|
|
};
|
|
|
|
// baseLine에 점이 있는지 확인
|
|
const isPointOnBase = (point) => {
|
|
return baseLines?.some(baseLine => {
|
|
const coords = extractBaseLineCoordinates(baseLine);
|
|
return pointsEqual(point, coords.p1) || pointsEqual(point, coords.p2);
|
|
}) || false;
|
|
};
|
|
|
|
// baseLine과 교차하는지 확인
|
|
const isIntersectingWithBase = (skeletonLine) => {
|
|
return baseLines?.some(baseLine => {
|
|
const coords = extractBaseLineCoordinates(baseLine);
|
|
const intersection = findIntersection(
|
|
skeletonLine.p1.x, skeletonLine.p1.y, skeletonLine.p2.x, skeletonLine.p2.y,
|
|
coords.p1.x, coords.p1.y, coords.p2.x, coords.p2.y
|
|
);
|
|
return intersection !== null;
|
|
}) || false;
|
|
};
|
|
|
|
// 라인 타입 확인
|
|
const getLineType = (p1, p2) => {
|
|
const dx = Math.abs(p2.x - p1.x);
|
|
const dy = Math.abs(p2.y - p1.y);
|
|
const tolerance = 0.1;
|
|
|
|
if (dy < tolerance) return 'horizontal';
|
|
if (dx < tolerance) return 'vertical';
|
|
return 'diagonal';
|
|
};
|
|
|
|
// 라인 길이 계산
|
|
const getLineLength = (p1, p2) => {
|
|
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
|
};
|
|
|
|
/**
|
|
* 연결 상태 확인 함수
|
|
*
|
|
* @param {Object} line - 검사할 skeletonLine (p1, p2)
|
|
* @param {number} lineIndex - 현재 라인의 인덱스
|
|
* @returns {{isConnected: boolean, p1Connected: boolean, p2Connected: boolean}} 연결되어 있으면 true, 끊어져 있으면 false
|
|
*
|
|
* 연결 판단 기준:
|
|
* 1. p1이 baseLine과 연결되어 있는지 확인
|
|
* 2. p1이 연결되어 있으면 p2가 skeletonLine과 연결되어 있는지 확인
|
|
* 3. p1이 연결되어 있지 않으면 p2가 baseLine과 연결되어 있는지 확인
|
|
* 4. p2도 연결되어 있지 않으면 p1과 p2가 skeletonLine과 연결되어 있는지 확인
|
|
*/
|
|
const isConnected = (line, lineIndex) => {
|
|
const result= {
|
|
isConnected: false,
|
|
p1Connected: false,
|
|
p2Connected: false,
|
|
extendedLine: []
|
|
}
|
|
const { p1, p2 } = line;
|
|
|
|
// 1. p1이 baseLine과 연결되어 있는지 확인
|
|
const isP1OnBase = isPointOnBase(p1);
|
|
const isP2OnBase = isPointOnBase(p2);
|
|
|
|
if (isP1OnBase || isP2OnBase) {
|
|
|
|
|
|
// 2. p1 또는 p2가 baseLine과 연결되어 있음
|
|
// -> 연결되지 않은 점이 skeletonLine과 연결되어 있는지 확인
|
|
for (let i = 0; i < skeletonLines.length; i++) {
|
|
if (i === lineIndex) continue; // 자기 자신은 제외
|
|
|
|
const otherLine = skeletonLines[i];
|
|
const otherP1 = otherLine.p1;
|
|
const otherP2 = otherLine.p2;
|
|
|
|
// p2가 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인
|
|
if (pointsEqual(p2, otherP1) || pointsEqual(p2, otherP2)) {
|
|
result.p1Connected = true;
|
|
result.p2Connected = true;
|
|
result.isConnected = true;
|
|
// p2가 연결되어 있으므로 전체 라인이 연결됨
|
|
}else{
|
|
result.p1Connected = true;
|
|
result.p2Connected = false;
|
|
result.isConnected = false;
|
|
}
|
|
|
|
if (pointsEqual(p1, otherP1) || pointsEqual(p1, otherP2)) {
|
|
result.p1Connected = true;
|
|
result.p2Connected = true;
|
|
// p1이 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인
|
|
result.isConnected = true;
|
|
// p1이 연결되어 있으므로 전체 라인이 연결됨
|
|
}else{
|
|
result.p1Connected = true;
|
|
result.p2Connected = false;
|
|
result.isConnected = false;
|
|
}
|
|
|
|
}
|
|
return result;
|
|
|
|
} else {
|
|
// 3. p1과 p2 모두 baseLine과 연결되어 있지 않음
|
|
// -> p1과 p2가 skeletonLine과 연결되어 있는지 확인
|
|
let p1Connected = false; // p1이 skeletonLine과 연결되어 있는지
|
|
let p2Connected = false; // p2가 skeletonLine과 연결되어 있는지
|
|
|
|
|
|
for (let i = 0; i < skeletonLines.length; i++) {
|
|
if (i === lineIndex) continue; // 자기 자신은 제외
|
|
|
|
const otherLine = skeletonLines[i];
|
|
const otherP1 = otherLine.p1;
|
|
const otherP2 = otherLine.p2;
|
|
|
|
// p1이 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인
|
|
if (!p1Connected && (pointsEqual(p1, otherP1) || pointsEqual(p1, otherP2))) {
|
|
p1Connected = true;
|
|
result.p1Connected = true;
|
|
}else{
|
|
// p1이 skeletonLine과 연결되지 않음 - baseLine까지 연장
|
|
result.extendedLine = extendToBaseLine(p1, baseLines);
|
|
}
|
|
|
|
// p2가 다른 skeletonLine의 p1 또는 p2와 일치하는지 확인
|
|
if (!p2Connected && (pointsEqual(p2, otherP1) || pointsEqual(p2, otherP2))) {
|
|
p2Connected = true;
|
|
result.p2Connected = true;
|
|
}else{
|
|
// p2가 skeletonLine과 연결되지 않음 - baseLine까지 연장
|
|
result.extendedLine = extendToBaseLine(p1, baseLines);
|
|
}
|
|
|
|
// p1과 p2가 모두 연결되어 있으면 전체 라인이 연결됨
|
|
if (p1Connected && p2Connected) {
|
|
result.isConnected = true;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
return result
|
|
}
|
|
};
|
|
|
|
// 각 라인 분석
|
|
skeletonLines.forEach((line, index) => {
|
|
const { p1, p2 } = line;
|
|
const length = getLineLength(p1, p2);
|
|
const type = getLineType(p1, p2);
|
|
|
|
if (length < minLength) return;
|
|
|
|
const connected = isConnected(line, index);
|
|
const extendedLine = connected.extendedLine;
|
|
const p1Connected = connected.p1Connected;
|
|
const p2Connected = connected.p2Connected;
|
|
|
|
if (type === 'diagonal') {
|
|
diagonalLines.push({ line, index, length, type, connected});
|
|
} else {
|
|
straightLines.push({ line, index, length, type, connected });
|
|
}
|
|
|
|
if (!connected.isConnected) {
|
|
disconnectedLines.push({
|
|
line, index, length, type,
|
|
isDiagonal: type === 'diagonal',
|
|
isHorizontal: type === 'horizontal',
|
|
isVertical: type === 'vertical',
|
|
p1Connected: p1Connected,
|
|
p2Connected: p2Connected,
|
|
extendedLine: extendedLine,
|
|
|
|
|
|
|
|
});
|
|
}
|
|
});
|
|
|
|
const filteredDisconnected = includeDiagonal && includeStraight
|
|
? disconnectedLines
|
|
: disconnectedLines.filter(item =>
|
|
(includeDiagonal && item.isDiagonal) ||
|
|
(includeStraight && (item.isHorizontal || item.isVertical))
|
|
);
|
|
|
|
return {
|
|
disconnectedLines: filteredDisconnected,
|
|
diagonalLines: includeDiagonal ? diagonalLines : [],
|
|
straightLines: includeStraight ? straightLines : [],
|
|
statistics: {
|
|
total: skeletonLines.length,
|
|
diagonal: diagonalLines.length,
|
|
straight: straightLines.length,
|
|
disconnected: filteredDisconnected.length
|
|
}
|
|
};
|
|
};
|
|
const extendToBaseLine = (point, baseLines) => {
|
|
// point에서 가장 가까운 baseLine을 찾아서 연장
|
|
let closestBaseLine = null;
|
|
let minDistance = Infinity;
|
|
|
|
for (const baseLine of baseLines) {
|
|
// point와 baseLine 사이의 거리 계산
|
|
const distance = getDistanceToLine(point, baseLine);
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
closestBaseLine = baseLine;
|
|
}
|
|
}
|
|
|
|
if (closestBaseLine) {
|
|
// point에서 closestBaseLine으로 연장하는 라인 생성
|
|
// 연장된 라인을 skeletonLines에 추가
|
|
return {
|
|
p1: point,
|
|
p2: getProjectionPoint(point, closestBaseLine)
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* 점과 선분 사이의 거리를 계산하는 함수
|
|
* @param {Object} point - 거리를 계산할 점 {x, y}
|
|
* @param {Object} line - 선분 {x1, y1, x2, y2}
|
|
* @returns {number} 점과 선분 사이의 최단 거리
|
|
*/
|
|
const getDistanceToLine = (point, line) => {
|
|
const { x: px, y: py } = point;
|
|
const { x1, y1, x2, y2 } = line;
|
|
|
|
// 선분의 방향 벡터
|
|
const dx = x2 - x1;
|
|
const dy = y2 - y1;
|
|
const lineLength = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (lineLength === 0) {
|
|
// 선분이 점인 경우
|
|
return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
|
|
}
|
|
|
|
// 선분의 단위 방향 벡터
|
|
const ux = dx / lineLength;
|
|
const uy = dy / lineLength;
|
|
|
|
// 점에서 선분 시작점으로의 벡터
|
|
const vx = px - x1;
|
|
const vy = py - y1;
|
|
|
|
// 투영된 길이 (스칼라 투영)
|
|
const projectionLength = vx * ux + vy * uy;
|
|
|
|
// 투영점이 선분 범위 내에 있는지 확인
|
|
if (projectionLength >= 0 && projectionLength <= lineLength) {
|
|
// 투영점이 선분 내에 있음
|
|
const distance = Math.sqrt(vx * vx + vy * vy - projectionLength * projectionLength);
|
|
return distance;
|
|
} else {
|
|
// 투영점이 선분 밖에 있음 - 끝점까지의 거리 중 작은 값
|
|
const distToStart = Math.sqrt(vx * vx + vy * vy);
|
|
const distToEnd = Math.sqrt((px - x2) * (px - x2) + (py - y2) * (py - y2));
|
|
return Math.min(distToStart, distToEnd);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 점을 선분에 투영한 점을 반환하는 함수
|
|
* @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 lineLength = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (lineLength === 0) {
|
|
// 선분이 점인 경우
|
|
return { x: x1, y: y1 };
|
|
}
|
|
|
|
// 선분의 단위 방향 벡터
|
|
const ux = dx / lineLength;
|
|
const uy = dy / lineLength;
|
|
|
|
// 점에서 선분 시작점으로의 벡터
|
|
const vx = px - x1;
|
|
const vy = py - y1;
|
|
|
|
// 투영된 길이 (스칼라 투영)
|
|
const projectionLength = vx * ux + vy * uy;
|
|
|
|
// 투영점이 선분 범위 내에 있는지 확인
|
|
if (projectionLength >= 0 && projectionLength <= lineLength) {
|
|
// 투영점이 선분 내에 있음
|
|
const projX = x1 + ux * projectionLength;
|
|
const projY = y1 + uy * projectionLength;
|
|
return { x: projX, y: projY };
|
|
} else if (projectionLength < 0) {
|
|
// 투영점이 시작점 앞에 있음
|
|
return { x: x1, y: y1 };
|
|
} else {
|
|
// 투영점이 끝점 뒤에 있음
|
|
return { x: x2, y: y2 };
|
|
}
|
|
}; |