[2173] 케라바 처마→박공 마루 미생성 수정 — 잔여단계 순서무관 연결성 판정으로 일반화
작업내용: - drawRoofByAttribute otherIs-replace 블록: intersect===analyze.end 일치 시 zero-length 붕괴를 farEnd(교차점에서 먼 끝점) 보존으로 수정(Fix-A) - wall-merge-residual: baseLines.length===4 하드코딩+순서의존 point-count 게이트를 제거, 잔여라인 전체 끝점(innerLines+residualLines) 기반 순서무관 연결성(startAnchored/endAnchored) 판정으로 교체(Fix-B) — ㄱ자(6변) 등 임의 다각형 마루 네트워크 지원 - sharedCnt/dup tolerance 0.5 적용(UI drift 대응) - KERAB-GABLE-DIAG 진단 로그 전체 주석처리(재활용 가능하도록 삭제 안 함)
This commit is contained in:
parent
157828e29f
commit
8c30c3c2bb
@ -6,6 +6,7 @@ import { QPolygon } from '@/components/fabric/QPolygon'
|
|||||||
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||||
import Big from 'big.js'
|
import Big from 'big.js'
|
||||||
import * as turf from '@turf/turf'
|
import * as turf from '@turf/turf'
|
||||||
|
// import { debugCapture } from '@/util/debugCapture' // [KERAB-GABLE-DIAG 2026-06-29] 박공 마루 미생성 추적용
|
||||||
|
|
||||||
const TWO_PI = Math.PI * 2
|
const TWO_PI = Math.PI * 2
|
||||||
const EPSILON = 1e-10 //좌표계산 시 최소 차이값
|
const EPSILON = 1e-10 //좌표계산 시 최소 차이값
|
||||||
@ -3595,8 +3596,14 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
const prevIndex = baseLines.findIndex((line) => line === prevLine)
|
const prevIndex = baseLines.findIndex((line) => line === prevLine)
|
||||||
const nextIndex = baseLines.findIndex((line) => line === nextLine)
|
const nextIndex = baseLines.findIndex((line) => line === nextLine)
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ① 이벤트: 박공 변 처리 진입
|
||||||
|
// const __gcoord = `(${Math.round(currentLine.x1)},${Math.round(currentLine.y1)})-(${Math.round(currentLine.x2)},${Math.round(currentLine.y2)})`
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ①이벤트:gable-enter', { gable: __gcoord, prevType: prevLine?.attributes?.type, nextType: nextLine?.attributes?.type })
|
||||||
|
|
||||||
//양옆이 처마가 아닐때 패스
|
//양옆이 처마가 아닐때 패스
|
||||||
if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) {
|
if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ②원인→④결과: 양옆이 처마가 아님 → 마루 미생성
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ②원인:neighbor-not-eaves → ④결과:no-ridge', { gable: __gcoord })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3608,6 +3615,11 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
y: (currentLine.y1 + currentLine.y2) / 2 + Math.sign(nextLine.y2 - nextLine.y1),
|
y: (currentLine.y1 + currentLine.y2) / 2 + Math.sign(nextLine.y2 - nextLine.y1),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ②원인: 본처리 진입 게이트(vector 차이 + inPolygon) 통과 여부
|
||||||
|
// const __vecDiff = prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y
|
||||||
|
// const __inPoly = checkWallPolygon.inPolygon(inPolygonPoint)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ②원인:gate-vec+inPoly', { gable: __gcoord, vecDiff: __vecDiff, inPoly: __inPoly, inPolygonPoint })
|
||||||
|
|
||||||
//좌우 라인이 서로 다른방향일때
|
//좌우 라인이 서로 다른방향일때
|
||||||
if ((prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y) && checkWallPolygon.inPolygon(inPolygonPoint)) {
|
if ((prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y) && checkWallPolygon.inPolygon(inPolygonPoint)) {
|
||||||
const analyze = analyzeLine(currentLine)
|
const analyze = analyzeLine(currentLine)
|
||||||
@ -3921,6 +3933,13 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ②원인: 반대편 평행변(oppositeLine) 매칭 — 0이면 마루 미생성
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ②원인:oppositeLine-match', {
|
||||||
|
// gable: __gcoord,
|
||||||
|
// count: oppositeLine.length,
|
||||||
|
// lines: oppositeLine.map((o) => `(${Math.round(o.line.x1)},${Math.round(o.line.y1)})-(${Math.round(o.line.x2)},${Math.round(o.line.y2)})`),
|
||||||
|
// })
|
||||||
|
|
||||||
if (oppositeLine.length > 0) {
|
if (oppositeLine.length > 0) {
|
||||||
const ridgePoints = []
|
const ridgePoints = []
|
||||||
|
|
||||||
@ -4182,6 +4201,14 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
|
|
||||||
if (stdAnalyze.isHorizontal) {
|
if (stdAnalyze.isHorizontal) {
|
||||||
if (overlapMinX > overlapMaxX) {
|
if (overlapMinX > overlapMaxX) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 수평 박공 X구간 겹침 없음 → 이 oppositeLine skip(no-ridge)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:horizontal-no-overlap → ④결과:no-ridge', {
|
||||||
|
// gable: __gcoord,
|
||||||
|
// overlapMinX,
|
||||||
|
// overlapMaxX,
|
||||||
|
// stdRange: [stdMinX, stdMaxX],
|
||||||
|
// ridgeRange: [rMinX, rMaxX],
|
||||||
|
// })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dist1 = Math.abs(ridgePoint[0] - overlapMinX)
|
const dist1 = Math.abs(ridgePoint[0] - overlapMinX)
|
||||||
@ -4199,6 +4226,14 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
}
|
}
|
||||||
if (stdAnalyze.isVertical) {
|
if (stdAnalyze.isVertical) {
|
||||||
if (overlapMinY > overlapMaxY) {
|
if (overlapMinY > overlapMaxY) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 수직 박공 Y구간 겹침 없음 → 이 oppositeLine skip(no-ridge)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:vertical-no-overlap → ④결과:no-ridge', {
|
||||||
|
// gable: __gcoord,
|
||||||
|
// overlapMinY,
|
||||||
|
// overlapMaxY,
|
||||||
|
// stdRange: [stdMinY, stdMaxY],
|
||||||
|
// ridgeRange: [rMinY, rMaxY],
|
||||||
|
// })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dist1 = Math.abs(ridgePoint[1] - overlapMinY)
|
const dist1 = Math.abs(ridgePoint[1] - overlapMinY)
|
||||||
@ -4315,7 +4350,19 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
type: TYPES.RIDGE,
|
type: TYPES.RIDGE,
|
||||||
degree: 0,
|
degree: 0,
|
||||||
})
|
})
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ④결과: 박공 마루 가선분 생성 성공 — left/right 는 이후 교점 매칭의 핵심 단서
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ④결과:ridge-pushed', {
|
||||||
|
// gable: __gcoord,
|
||||||
|
// ridgeLength,
|
||||||
|
// left: prevIndex,
|
||||||
|
// right: nextIndex,
|
||||||
|
// start: startPoint,
|
||||||
|
// end: endPoint,
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 마루 길이 ~0 → 가선분 미생성
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:ridge-too-short → ④결과:no-ridge', { gable: __gcoord, ridgeLength, point })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -4323,6 +4370,8 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
}
|
}
|
||||||
//반절마루 생성불가이므로 지붕선 분기를 해야하는지 확인 후 처리.
|
//반절마루 생성불가이므로 지붕선 분기를 해야하는지 확인 후 처리.
|
||||||
if (prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y) {
|
if (prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기: 양옆 벡터 동일(U자) → 반절마루 불가, GABLE_LINE 분기로
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:U-shape-gableline', { gable: __gcoord })
|
||||||
const analyze = analyzeLine(currentLine)
|
const analyze = analyzeLine(currentLine)
|
||||||
const roofLine = analyze.roofLine
|
const roofLine = analyze.roofLine
|
||||||
|
|
||||||
@ -4385,6 +4434,24 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
while (linesAnalysis.length > 0 && iterations < MAX_ITERATIONS) {
|
while (linesAnalysis.length > 0 && iterations < MAX_ITERATIONS) {
|
||||||
iterations++
|
iterations++
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ①이벤트: 교점 루프 iteration 시작 — RIDGE 현재 상태(붕괴 추적)
|
||||||
|
// {
|
||||||
|
// const __r = linesAnalysis.find((l) => l.type === TYPES.RIDGE)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ①이벤트:loop-iter-start', {
|
||||||
|
// iter: iterations,
|
||||||
|
// total: linesAnalysis.length,
|
||||||
|
// ridge: __r
|
||||||
|
// ? {
|
||||||
|
// start: { x: Math.round(__r.start.x * 10) / 10, y: Math.round(__r.start.y * 10) / 10 },
|
||||||
|
// end: { x: Math.round(__r.end.x * 10) / 10, y: Math.round(__r.end.y * 10) / 10 },
|
||||||
|
// len: Math.round(Math.hypot(__r.end.x - __r.start.x, __r.end.y - __r.start.y) * 10) / 10,
|
||||||
|
// left: __r.left,
|
||||||
|
// right: __r.right,
|
||||||
|
// }
|
||||||
|
// : null,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
const intersections = []
|
const intersections = []
|
||||||
linesAnalysis.forEach((currLine, i) => {
|
linesAnalysis.forEach((currLine, i) => {
|
||||||
let minDistance = Infinity
|
let minDistance = Infinity
|
||||||
@ -4435,6 +4502,25 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
intersections.push({ index: i, intersect: intersectPoint, linePoint, partner })
|
intersections.push({ index: i, intersect: intersectPoint, linePoint, partner })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ②원인: 이번 iter 에서 각 RIDGE 가 교점쌍(partner)을 찾았는지 — 못 찾으면 루프 미처리되어 잔여로 떨어진다
|
||||||
|
// linesAnalysis.forEach((l, li) => {
|
||||||
|
// if (l.type !== TYPES.RIDGE) return
|
||||||
|
// const found = intersections.find((it) => it.index === li)
|
||||||
|
// const partnerLine = found ? linesAnalysis[found.partner] : null
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ②원인:loop-ridge-intersect-scan', {
|
||||||
|
// iter: iterations,
|
||||||
|
// ridge: {
|
||||||
|
// start: { x: Math.round(l.start.x * 10) / 10, y: Math.round(l.start.y * 10) / 10 },
|
||||||
|
// end: { x: Math.round(l.end.x * 10) / 10, y: Math.round(l.end.y * 10) / 10 },
|
||||||
|
// share: [l.left, l.right],
|
||||||
|
// },
|
||||||
|
// hasIntersect: !!found,
|
||||||
|
// partner: partnerLine ? { type: partnerLine.type, share: [partnerLine.left, partnerLine.right] } : null,
|
||||||
|
// intersect: found ? { x: Math.round(found.intersect.x * 10) / 10, y: Math.round(found.intersect.y * 10) / 10 } : null,
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
const intersectPoints = intersections
|
const intersectPoints = intersections
|
||||||
.map((item) => item.intersect)
|
.map((item) => item.intersect)
|
||||||
.filter((point, index, self) => self.findIndex((p) => almostEqual(p.x, point.x) && almostEqual(p.y, point.y)) === index)
|
.filter((point, index, self) => self.findIndex((p) => almostEqual(p.x, point.x) && almostEqual(p.y, point.y)) === index)
|
||||||
@ -4450,24 +4536,78 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
for (const { index, intersect, linePoint, partner } of intersections) {
|
for (const { index, intersect, linePoint, partner } of intersections) {
|
||||||
const cLine = linesAnalysis[index]
|
const cLine = linesAnalysis[index]
|
||||||
const pLine = linesAnalysis[partner]
|
const pLine = linesAnalysis[partner]
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] RIDGE 가 낀 교점쌍이 어느 게이트에서 탈락하는지 추적용
|
||||||
|
// const __ridgePair = cLine?.type === TYPES.RIDGE || pLine?.type === TYPES.RIDGE
|
||||||
|
|
||||||
//교점이 없거나, 이미 처리된 선분이면 처리하지 않는다.
|
//교점이 없거나, 이미 처리된 선분이면 처리하지 않는다.
|
||||||
if (!intersect || processed.has(index)) continue
|
if (!intersect || processed.has(index)) {
|
||||||
|
// if (__ridgePair)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', {
|
||||||
|
// iter: iterations,
|
||||||
|
// reason: !intersect ? 'no-intersect' : 'already-processed',
|
||||||
|
// cType: cLine?.type,
|
||||||
|
// pType: pLine?.type,
|
||||||
|
// })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
//교점이 없거나, 교점 선분이 없으면 처리하지 않는다.
|
//교점이 없거나, 교점 선분이 없으면 처리하지 않는다.
|
||||||
const pIntersection = intersections.find((p) => p.index === partner)
|
const pIntersection = intersections.find((p) => p.index === partner)
|
||||||
if (!pIntersection || !pIntersection.intersect) continue
|
if (!pIntersection || !pIntersection.intersect) {
|
||||||
|
// if (__ridgePair)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', {
|
||||||
|
// iter: iterations,
|
||||||
|
// reason: 'partner-no-intersect',
|
||||||
|
// cType: cLine?.type,
|
||||||
|
// pType: pLine?.type,
|
||||||
|
// })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
//상호 최단 교점이 아닐경우 처리하지 않는다.
|
//상호 최단 교점이 아닐경우 처리하지 않는다.
|
||||||
if (pIntersection.partner !== index) continue
|
if (pIntersection.partner !== index) {
|
||||||
|
// if (__ridgePair)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', {
|
||||||
|
// iter: iterations,
|
||||||
|
// reason: 'not-mutual-shortest',
|
||||||
|
// cType: cLine?.type,
|
||||||
|
// pType: pLine?.type,
|
||||||
|
// pPartner: pIntersection.partner,
|
||||||
|
// index,
|
||||||
|
// })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
//공통선분이 없으면 처리하지 않는다.
|
//공통선분이 없으면 처리하지 않는다.
|
||||||
const isSameLine = cLine.left === pLine.left || cLine.left === pLine.right || cLine.right === pLine.left || cLine.right === pLine.right
|
const isSameLine = cLine.left === pLine.left || cLine.left === pLine.right || cLine.right === pLine.left || cLine.right === pLine.right
|
||||||
if (!isSameLine) continue
|
if (!isSameLine) {
|
||||||
|
// if (__ridgePair)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', {
|
||||||
|
// iter: iterations,
|
||||||
|
// reason: 'no-common-line',
|
||||||
|
// cType: cLine?.type,
|
||||||
|
// pType: pLine?.type,
|
||||||
|
// cShare: [cLine.left, cLine.right],
|
||||||
|
// pShare: [pLine.left, pLine.right],
|
||||||
|
// })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
//처리 된 라인으로 설정
|
//처리 된 라인으로 설정
|
||||||
processed.add(index).add(partner)
|
processed.add(index).add(partner)
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ②원인: 이번 교점쌍에 RIDGE 포함 — 어떤 partner 와 어디서 만났나
|
||||||
|
if (cLine.type === TYPES.RIDGE || pLine.type === TYPES.RIDGE) {
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ②원인:loop-pair-has-ridge', {
|
||||||
|
// iter: iterations,
|
||||||
|
// cType: cLine.type,
|
||||||
|
// pType: pLine.type,
|
||||||
|
// intersect: { x: Math.round(intersect.x * 10) / 10, y: Math.round(intersect.y * 10) / 10 },
|
||||||
|
// cShare: [cLine.left, cLine.right],
|
||||||
|
// pShare: [pLine.left, pLine.right],
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
let point1 = linePoint
|
let point1 = linePoint
|
||||||
let point2 = pIntersection.linePoint
|
let point2 = pIntersection.linePoint
|
||||||
let length1 = Math.sqrt(Math.pow(point1[2] - point1[0], 2) + Math.pow(point1[3] - point1[1], 2))
|
let length1 = Math.sqrt(Math.pow(point1[2] - point1[0], 2) + Math.pow(point1[3] - point1[1], 2))
|
||||||
@ -4483,10 +4623,22 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
length1 = Math.sqrt((point1[2] - point1[0]) ** 2 + (point1[3] - point1[1]) ** 2)
|
length1 = Math.sqrt((point1[2] - point1[0]) ** 2 + (point1[3] - point1[1]) ** 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기: RIDGE(c) 그리기 게이트 판정(length1>0 && !중복)
|
||||||
|
if (cLine.type === TYPES.RIDGE) {
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-ridge(c)-draw-gate', {
|
||||||
|
// iter: iterations,
|
||||||
|
// length1: Math.round(length1 * 10) / 10,
|
||||||
|
// already: alreadyPoints(innerLines, point1),
|
||||||
|
// willDraw: length1 > 0 && !alreadyPoints(innerLines, point1),
|
||||||
|
// point: point1.map((v) => Math.round(v * 10) / 10),
|
||||||
|
// })
|
||||||
|
}
|
||||||
if (length1 > 0 && !alreadyPoints(innerLines, point1)) {
|
if (length1 > 0 && !alreadyPoints(innerLines, point1)) {
|
||||||
if (cLine.type === TYPES.HIP) {
|
if (cLine.type === TYPES.HIP) {
|
||||||
innerLines.push(drawHipLine(point1, canvas, roof, textMode, cLine.degree, cLine.degree))
|
innerLines.push(drawHipLine(point1, canvas, roof, textMode, cLine.degree, cLine.degree))
|
||||||
} else if (cLine.type === TYPES.RIDGE) {
|
} else if (cLine.type === TYPES.RIDGE) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ④결과: RIDGE(c) 실제로 그려짐
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ④결과:loop-ridge(c)-drawn', { iter: iterations, point: point1.map((v) => Math.round(v * 10) / 10) })
|
||||||
innerLines.push(drawRidgeLine(point1, canvas, roof, textMode))
|
innerLines.push(drawRidgeLine(point1, canvas, roof, textMode))
|
||||||
} else if (cLine.type === TYPES.NEW) {
|
} else if (cLine.type === TYPES.NEW) {
|
||||||
const isDiagonal = Math.abs(point1[0] - point1[2]) >= 1 && Math.abs(point1[1] - point1[3]) >= 1
|
const isDiagonal = Math.abs(point1[0] - point1[2]) >= 1 && Math.abs(point1[1] - point1[3]) >= 1
|
||||||
@ -4504,10 +4656,22 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기: RIDGE(p) 그리기 게이트 판정(length2>0 && !중복)
|
||||||
|
if (pLine.type === TYPES.RIDGE) {
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-ridge(p)-draw-gate', {
|
||||||
|
// iter: iterations,
|
||||||
|
// length2: Math.round(length2 * 10) / 10,
|
||||||
|
// already: alreadyPoints(innerLines, point2),
|
||||||
|
// willDraw: length2 > 0 && !alreadyPoints(innerLines, point2),
|
||||||
|
// point: point2.map((v) => Math.round(v * 10) / 10),
|
||||||
|
// })
|
||||||
|
}
|
||||||
if (length2 > 0 && !alreadyPoints(innerLines, point2)) {
|
if (length2 > 0 && !alreadyPoints(innerLines, point2)) {
|
||||||
if (pLine.type === TYPES.HIP) {
|
if (pLine.type === TYPES.HIP) {
|
||||||
innerLines.push(drawHipLine(point2, canvas, roof, textMode, pLine.degree, pLine.degree))
|
innerLines.push(drawHipLine(point2, canvas, roof, textMode, pLine.degree, pLine.degree))
|
||||||
} else if (pLine.type === TYPES.RIDGE) {
|
} else if (pLine.type === TYPES.RIDGE) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ④결과: RIDGE(p) 실제로 그려짐
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ④결과:loop-ridge(p)-drawn', { iter: iterations, point: point2.map((v) => Math.round(v * 10) / 10) })
|
||||||
innerLines.push(drawRidgeLine(point2, canvas, roof, textMode))
|
innerLines.push(drawRidgeLine(point2, canvas, roof, textMode))
|
||||||
} else if (pLine.type === TYPES.NEW) {
|
} else if (pLine.type === TYPES.NEW) {
|
||||||
const isDiagonal = Math.abs(point2[0] - point2[2]) >= 1 && Math.abs(point2[1] - point2[3]) >= 1
|
const isDiagonal = Math.abs(point2[0] - point2[2]) >= 1 && Math.abs(point2[1] - point2[3]) >= 1
|
||||||
@ -4681,9 +4845,27 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
if (otherIs.length > 0) {
|
if (otherIs.length > 0) {
|
||||||
const analyze = linesAnalysis[otherIs[0].index]
|
const analyze = linesAnalysis[otherIs[0].index]
|
||||||
processed.add(otherIs[0].index)
|
processed.add(otherIs[0].index)
|
||||||
|
// 연결점(intersect)에서 더 먼 끝점을 유지한다.
|
||||||
|
// 기존엔 항상 analyze.end 를 유지해, intersect 가 analyze.end 와 일치하면 start=end 로 zero-length 붕괴(박공 마루 미생성)가 발생했다.
|
||||||
|
const distToStart = Math.hypot(analyze.start.x - intersect.x, analyze.start.y - intersect.y)
|
||||||
|
const distToEnd = Math.hypot(analyze.end.x - intersect.x, analyze.end.y - intersect.y)
|
||||||
|
const farEnd = distToStart >= distToEnd ? analyze.start : analyze.end
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 연결점 기존 analyze 라인 교체 — far-end 보존으로 붕괴 방지 검증
|
||||||
|
// const __newLen = Math.round(Math.hypot(farEnd.x - intersect.x, farEnd.y - intersect.y) * 10) / 10
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-analyze-replace → ④결과:' + (__newLen < 0.05 ? 'COLLAPSE-len0' : 'replaced-farEnd'), {
|
||||||
|
// iter: iterations,
|
||||||
|
// replacedType: analyze.type,
|
||||||
|
// replacedLeft: analyze.left,
|
||||||
|
// replacedRight: analyze.right,
|
||||||
|
// origStart: { x: Math.round(analyze.start.x * 10) / 10, y: Math.round(analyze.start.y * 10) / 10 },
|
||||||
|
// origEnd: { x: Math.round(analyze.end.x * 10) / 10, y: Math.round(analyze.end.y * 10) / 10 },
|
||||||
|
// intersect: { x: Math.round(intersect.x * 10) / 10, y: Math.round(intersect.y * 10) / 10 },
|
||||||
|
// farEnd: { x: Math.round(farEnd.x * 10) / 10, y: Math.round(farEnd.y * 10) / 10 },
|
||||||
|
// newLen: __newLen,
|
||||||
|
// })
|
||||||
newAnalysis.push({
|
newAnalysis.push({
|
||||||
start: { x: intersect.x, y: intersect.y },
|
start: { x: intersect.x, y: intersect.y },
|
||||||
end: { x: analyze.end.x, y: analyze.end.y },
|
end: { x: farEnd.x, y: farEnd.y },
|
||||||
left: analyze.left,
|
left: analyze.left,
|
||||||
right: analyze.right,
|
right: analyze.right,
|
||||||
type: analyze.type,
|
type: analyze.type,
|
||||||
@ -4739,6 +4921,23 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
// 처리된 가선분 제외
|
// 처리된 가선분 제외
|
||||||
linesAnalysis = newAnalysis.concat(linesAnalysis.filter((_, index) => !processed.has(index)))
|
linesAnalysis = newAnalysis.concat(linesAnalysis.filter((_, index) => !processed.has(index)))
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ④결과: iteration 종료 후 RIDGE 상태(붕괴/소멸 여부)
|
||||||
|
// {
|
||||||
|
// const __r = linesAnalysis.find((l) => l.type === TYPES.RIDGE)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ④결과:loop-iter-end', {
|
||||||
|
// iter: iterations,
|
||||||
|
// newAnalysisCount: newAnalysis.length,
|
||||||
|
// processedCount: processed.size,
|
||||||
|
// ridge: __r
|
||||||
|
// ? {
|
||||||
|
// start: { x: Math.round(__r.start.x * 10) / 10, y: Math.round(__r.start.y * 10) / 10 },
|
||||||
|
// end: { x: Math.round(__r.end.x * 10) / 10, y: Math.round(__r.end.y * 10) / 10 },
|
||||||
|
// len: Math.round(Math.hypot(__r.end.x - __r.start.x, __r.end.y - __r.start.y) * 10) / 10,
|
||||||
|
// }
|
||||||
|
// : 'GONE',
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
canvas
|
canvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
.filter((object) => object.name === 'check' || object.name === 'checkAnalysis')
|
.filter((object) => object.name === 'check' || object.name === 'checkAnalysis')
|
||||||
@ -4747,6 +4946,21 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
if (newAnalysis.length === 0) break
|
if (newAnalysis.length === 0) break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ①이벤트: 교점 루프 종료 — 잔여 linesAnalysis 덤프(이 시점에 RIDGE 가 남아있는지가 핵심)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ①이벤트:after-loop-residual', {
|
||||||
|
// baseLinesLen: baseLines.length,
|
||||||
|
// iterations,
|
||||||
|
// count: linesAnalysis.length,
|
||||||
|
// lines: linesAnalysis.map((l) => ({
|
||||||
|
// type: l.type,
|
||||||
|
// degree: l.degree,
|
||||||
|
// left: l.left,
|
||||||
|
// right: l.right,
|
||||||
|
// start: { x: Math.round(l.start.x * 10) / 10, y: Math.round(l.start.y * 10) / 10 },
|
||||||
|
// end: { x: Math.round(l.end.x * 10) / 10, y: Math.round(l.end.y * 10) / 10 },
|
||||||
|
// })),
|
||||||
|
// })
|
||||||
|
|
||||||
// 가선분 중 처리가 안되어있는 붙어있는 라인에 대한 예외처리.
|
// 가선분 중 처리가 안되어있는 붙어있는 라인에 대한 예외처리.
|
||||||
const proceedAnalysis = []
|
const proceedAnalysis = []
|
||||||
linesAnalysis
|
linesAnalysis
|
||||||
@ -4837,36 +5051,77 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
|||||||
|
|
||||||
// 최종 라인중 벽에서 시작해서 벽에서 끝나는 라인이 남을 경우(벽취함)
|
// 최종 라인중 벽에서 시작해서 벽에서 끝나는 라인이 남을 경우(벽취함)
|
||||||
if (linesAnalysis.length > 0) {
|
if (linesAnalysis.length > 0) {
|
||||||
linesAnalysis
|
// 길이 0 잔여 제거
|
||||||
.filter((line) => {
|
const residualLines = linesAnalysis.filter((line) => Math.hypot(line.end.x - line.start.x, line.end.y - line.start.y) > 0)
|
||||||
const dx = line.end.x - line.start.x
|
|
||||||
const dy = line.end.y - line.start.y
|
// 순서 비의존 연결성 판정용 끝점 집합: 이미 그려진 라인(innerLines) + 아직 안 그려진 잔여 라인(residualLines) 전부.
|
||||||
const length = Math.sqrt(dx ** 2 + dy ** 2)
|
// 끝점이 지붕선 위(앵커)이거나 다른 라인 끝점과 공유(접합)되면 "연결됨". 양끝이 모두 연결된 라인만 그린다.
|
||||||
return length > 0
|
// 이렇게 하면 마루↔마루/마루↔힙 접합도 그리기 순서와 무관하게 살아남고, 자유단(고립)을 가진 라인만 소멸한다.
|
||||||
})
|
const allEndpoints = []
|
||||||
.forEach((line) => {
|
innerLines.forEach((il) => allEndpoints.push({ x: il.x1, y: il.y1 }, { x: il.x2, y: il.y2 }))
|
||||||
|
residualLines.forEach((rl) => allEndpoints.push({ x: rl.start.x, y: rl.start.y }, { x: rl.end.x, y: rl.end.y }))
|
||||||
|
// UI/Big.js drift 로 끝점 좌표가 미세하게 어긋날 수 있어 0.5 tolerance 사용 (feedback_same_point_tolerance)
|
||||||
|
const SHARED_EPS = 0.5
|
||||||
|
const sharedCnt = (pt) => allEndpoints.filter((p) => almostEqual(p.x, pt.x, SHARED_EPS) && almostEqual(p.y, pt.y, SHARED_EPS)).length
|
||||||
|
|
||||||
|
residualLines.forEach((line) => {
|
||||||
const startOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.start))
|
const startOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.start))
|
||||||
const endOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.end))
|
const endOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.end))
|
||||||
const allLinesPoints = []
|
|
||||||
innerLines.forEach((innerLine) => {
|
|
||||||
allLinesPoints.push({ x: innerLine.x1, y: innerLine.y1 }, { x: innerLine.x2, y: innerLine.y2 })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (
|
// 자기 자신이 끝점마다 1회 기여하므로 sharedCnt >= 2 이면 다른 라인과 접합한 것.
|
||||||
allLinesPoints.filter((p) => almostEqual(p.x, line.start.x) && almostEqual(p.y, line.start.y)).length < 3 &&
|
const startAnchored = !!startOnLine || sharedCnt(line.start) >= 2
|
||||||
allLinesPoints.filter((p) => almostEqual(p.x, line.end.x) && almostEqual(p.y, line.end.y)).length === 0
|
const endAnchored = !!endOnLine || sharedCnt(line.end) >= 2
|
||||||
) {
|
|
||||||
if (line.type === TYPES.RIDGE && baseLines.length === 4 && (startOnLine || endOnLine)) {
|
// 동일 양끝점으로 이미 그려진 라인이 있으면 중복 방지.
|
||||||
|
const dup = innerLines.find(
|
||||||
|
(il) =>
|
||||||
|
(almostEqual(il.x1, line.start.x, SHARED_EPS) && almostEqual(il.y1, line.start.y, SHARED_EPS) && almostEqual(il.x2, line.end.x, SHARED_EPS) && almostEqual(il.y2, line.end.y, SHARED_EPS)) ||
|
||||||
|
(almostEqual(il.x1, line.end.x, SHARED_EPS) && almostEqual(il.y1, line.end.y, SHARED_EPS) && almostEqual(il.x2, line.start.x, SHARED_EPS) && almostEqual(il.y2, line.start.y, SHARED_EPS)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ②원인: 벽취합 잔여 라인 연결성 판정값(순서 무관)
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ②원인:wall-merge-residual', {
|
||||||
|
// type: line.type,
|
||||||
|
// degree: line.degree,
|
||||||
|
// baseLinesLen: baseLines.length,
|
||||||
|
// startOnLine: !!startOnLine,
|
||||||
|
// endOnLine: !!endOnLine,
|
||||||
|
// start: { x: Math.round(line.start.x * 10) / 10, y: Math.round(line.start.y * 10) / 10 },
|
||||||
|
// end: { x: Math.round(line.end.x * 10) / 10, y: Math.round(line.end.y * 10) / 10 },
|
||||||
|
// startShared: sharedCnt(line.start),
|
||||||
|
// endShared: sharedCnt(line.end),
|
||||||
|
// startAnchored,
|
||||||
|
// endAnchored,
|
||||||
|
// dup: !!dup,
|
||||||
|
// })
|
||||||
|
|
||||||
|
if (dup) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 이미 그려진 동일 라인 → 중복 스킵
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:duplicate → ④결과:skipped', { type: line.type })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startAnchored && endAnchored) {
|
||||||
|
if (line.type === TYPES.RIDGE) {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 양끝 연결된 RIDGE → 마루 그림
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:anchored-ridge → ④결과:ridge-drawn', { type: line.type })
|
||||||
innerLines.push(drawRidgeLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode))
|
innerLines.push(drawRidgeLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode))
|
||||||
} else {
|
} else if (line.degree === 0) {
|
||||||
if (startOnLine && endOnLine) {
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 양끝 연결 + degree0 → roofLine 그림
|
||||||
if (line.degree === 0) {
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:anchored-degree0 → ④결과:roofLine-drawn', { type: line.type, degree: line.degree })
|
||||||
innerLines.push(drawRoofLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode))
|
innerLines.push(drawRoofLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode))
|
||||||
} else {
|
} else {
|
||||||
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 양끝 연결 + degree>0 → hip 그림
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:anchored-hip → ④결과:hip-drawn', { type: line.type, degree: line.degree })
|
||||||
innerLines.push(drawHipLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode, line.degree, line.degree))
|
innerLines.push(drawHipLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode, line.degree, line.degree))
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 한쪽 끝이 고립(연결 안됨) → 소멸
|
||||||
|
// debugCapture.log('KERAB-GABLE-DIAG ③분기:not-anchored → ④결과:dropped', {
|
||||||
|
// type: line.type,
|
||||||
|
// startAnchored,
|
||||||
|
// endAnchored,
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -6478,7 +6733,7 @@ export const equalizeSymmetricHips = (hipLines, canvas) => {
|
|||||||
text.set({ planeSize: plane, actualSize: actual })
|
text.set({ planeSize: plane, actualSize: actual })
|
||||||
const isPlane = !text.actualSize || text.text === String(text.planeSize)
|
const isPlane = !text.actualSize || text.text === String(text.planeSize)
|
||||||
// planeSize/actualSize 모두 저장값(정수 또는 0.5 step) 그대로 표시.
|
// planeSize/actualSize 모두 저장값(정수 또는 0.5 step) 그대로 표시.
|
||||||
const __raw = isPlane ? plane : actual
|
// const __raw = isPlane ? plane : actual
|
||||||
text.set({ text: Number(__raw).toFixed(1).replace(/\.0$/, '') })
|
text.set({ text: Number(__raw).toFixed(1).replace(/\.0$/, '') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6550,7 +6805,7 @@ export const equalizeParallelEaveLabels = (lines, canvas) => {
|
|||||||
}
|
}
|
||||||
if (text) {
|
if (text) {
|
||||||
text.set({ planeSize: plane, actualSize: actual })
|
text.set({ planeSize: plane, actualSize: actual })
|
||||||
const __raw = wasPlane ? plane : actual
|
// const __raw = wasPlane ? plane : actual
|
||||||
text.set({ text: Number(__raw).toFixed(1).replace(/\.0$/, '') })
|
text.set({ text: Number(__raw).toFixed(1).replace(/\.0$/, '') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user