[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:
ysCha 2026-06-29 16:11:53 +09:00
parent 157828e29f
commit 8c30c3c2bb

View File

@ -6,6 +6,7 @@ import { QPolygon } from '@/components/fabric/QPolygon'
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
import Big from 'big.js'
import * as turf from '@turf/turf'
// import { debugCapture } from '@/util/debugCapture' // [KERAB-GABLE-DIAG 2026-06-29] 박공 마루 미생성 추적용
const TWO_PI = Math.PI * 2
const EPSILON = 1e-10 //좌표계산 시 최소 차이값
@ -3595,8 +3596,14 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
const prevIndex = baseLines.findIndex((line) => line === prevLine)
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) {
// [KERAB-GABLE-DIAG 2026-06-29] ②원인→④결과: 양옆이 처마가 아님 → 마루 미생성
// debugCapture.log('KERAB-GABLE-DIAG ②원인:neighbor-not-eaves → ④결과:no-ridge', { gable: __gcoord })
return
}
@ -3608,6 +3615,11 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
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)) {
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) {
const ridgePoints = []
@ -4182,6 +4201,14 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
if (stdAnalyze.isHorizontal) {
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
}
const dist1 = Math.abs(ridgePoint[0] - overlapMinX)
@ -4199,6 +4226,14 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
}
if (stdAnalyze.isVertical) {
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
}
const dist1 = Math.abs(ridgePoint[1] - overlapMinY)
@ -4315,7 +4350,19 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
type: TYPES.RIDGE,
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) {
// [KERAB-GABLE-DIAG 2026-06-29] ③분기: 양옆 벡터 동일(U자) → 반절마루 불가, GABLE_LINE 분기로
// debugCapture.log('KERAB-GABLE-DIAG ③분기:U-shape-gableline', { gable: __gcoord })
const analyze = analyzeLine(currentLine)
const roofLine = analyze.roofLine
@ -4385,6 +4434,24 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
while (linesAnalysis.length > 0 && iterations < MAX_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 = []
linesAnalysis.forEach((currLine, i) => {
let minDistance = Infinity
@ -4435,6 +4502,25 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
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
.map((item) => item.intersect)
.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) {
const cLine = linesAnalysis[index]
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)
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
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)
// [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 point2 = pIntersection.linePoint
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)
}
// [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 (cLine.type === TYPES.HIP) {
innerLines.push(drawHipLine(point1, canvas, roof, textMode, cLine.degree, cLine.degree))
} 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))
} else if (cLine.type === TYPES.NEW) {
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 (pLine.type === TYPES.HIP) {
innerLines.push(drawHipLine(point2, canvas, roof, textMode, pLine.degree, pLine.degree))
} 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))
} else if (pLine.type === TYPES.NEW) {
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) {
const analyze = linesAnalysis[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({
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,
right: analyze.right,
type: analyze.type,
@ -4739,6 +4921,23 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
// 처리된 가선분 제외
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
.getObjects()
.filter((object) => object.name === 'check' || object.name === 'checkAnalysis')
@ -4747,6 +4946,21 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
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 = []
linesAnalysis
@ -4837,38 +5051,79 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
// 최종 라인중 벽에서 시작해서 벽에서 끝나는 라인이 남을 경우(벽취함)
if (linesAnalysis.length > 0) {
linesAnalysis
.filter((line) => {
const dx = line.end.x - line.start.x
const dy = line.end.y - line.start.y
const length = Math.sqrt(dx ** 2 + dy ** 2)
return length > 0
})
.forEach((line) => {
const startOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.start))
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 })
})
// 길이 0 잔여 제거
const residualLines = linesAnalysis.filter((line) => Math.hypot(line.end.x - line.start.x, line.end.y - line.start.y) > 0)
if (
allLinesPoints.filter((p) => almostEqual(p.x, line.start.x) && almostEqual(p.y, line.start.y)).length < 3 &&
allLinesPoints.filter((p) => almostEqual(p.x, line.end.x) && almostEqual(p.y, line.end.y)).length === 0
) {
if (line.type === TYPES.RIDGE && baseLines.length === 4 && (startOnLine || endOnLine)) {
innerLines.push(drawRidgeLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode))
} else {
if (startOnLine && endOnLine) {
if (line.degree === 0) {
innerLines.push(drawRoofLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode))
} else {
innerLines.push(drawHipLine([line.start.x, line.start.y, line.end.x, line.end.y], canvas, roof, textMode, line.degree, line.degree))
}
}
}
// 순서 비의존 연결성 판정용 끝점 집합: 이미 그려진 라인(innerLines) + 아직 안 그려진 잔여 라인(residualLines) 전부.
// 끝점이 지붕선 위(앵커)이거나 다른 라인 끝점과 공유(접합)되면 "연결됨". 양끝이 모두 연결된 라인만 그린다.
// 이렇게 하면 마루↔마루/마루↔힙 접합도 그리기 순서와 무관하게 살아남고, 자유단(고립)을 가진 라인만 소멸한다.
const allEndpoints = []
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 endOnLine = roof.lines.find((l) => isPointOnLineNew(l, line.end))
// 자기 자신이 끝점마다 1회 기여하므로 sharedCnt >= 2 이면 다른 라인과 접합한 것.
const startAnchored = !!startOnLine || sharedCnt(line.start) >= 2
const endAnchored = !!endOnLine || sharedCnt(line.end) >= 2
// 동일 양끝점으로 이미 그려진 라인이 있으면 중복 방지.
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))
} else if (line.degree === 0) {
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 양끝 연결 + degree0 → roofLine 그림
// 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))
} 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))
}
})
} else {
// [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 한쪽 끝이 고립(연결 안됨) → 소멸
// debugCapture.log('KERAB-GABLE-DIAG ③분기:not-anchored → ④결과:dropped', {
// type: line.type,
// startAnchored,
// endAnchored,
// })
}
})
}
//케라바에서 파생된 하단 지붕 라인처리
const downRoofGable = []
@ -6478,7 +6733,7 @@ export const equalizeSymmetricHips = (hipLines, canvas) => {
text.set({ planeSize: plane, actualSize: actual })
const isPlane = !text.actualSize || text.text === String(text.planeSize)
// planeSize/actualSize 모두 저장값(정수 또는 0.5 step) 그대로 표시.
const __raw = isPlane ? plane : actual
// const __raw = isPlane ? plane : actual
text.set({ text: Number(__raw).toFixed(1).replace(/\.0$/, '') })
}
}
@ -6550,7 +6805,7 @@ export const equalizeParallelEaveLabels = (lines, canvas) => {
}
if (text) {
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$/, '') })
}
}