From 8c30c3c2bb9dfc9e5936508524c3c5f8154dcb64 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 29 Jun 2026 16:11:53 +0900 Subject: [PATCH] =?UTF-8?q?[2173]=20=EC=BC=80=EB=9D=BC=EB=B0=94=20?= =?UTF-8?q?=EC=B2=98=EB=A7=88=E2=86=92=EB=B0=95=EA=B3=B5=20=EB=A7=88?= =?UTF-8?q?=EB=A3=A8=20=EB=AF=B8=EC=83=9D=EC=84=B1=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=E2=80=94=20=EC=9E=94=EC=97=AC=EB=8B=A8=EA=B3=84=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=EB=AC=B4=EA=B4=80=20=EC=97=B0=EA=B2=B0=EC=84=B1=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EC=9D=BC=EB=B0=98?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 작업내용: - 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 진단 로그 전체 주석처리(재활용 가능하도록 삭제 안 함) --- src/util/qpolygon-utils.js | 329 ++++++++++++++++++++++++++++++++----- 1 file changed, 292 insertions(+), 37 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 842c8eba..668efc2c 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -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$/, '') }) } }