From 9c6cabe911e3ccbb578e2ee5b1c7306bcbdae16a Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 29 Jun 2026 13:09:06 +0900 Subject: [PATCH] =?UTF-8?q?[=EC=A7=80=EB=B6=95=EB=A9=B4]=20=EC=A7=80?= =?UTF-8?q?=EB=B6=95=EB=A9=B4=20=ED=95=A0=EB=8B=B9=20inner-inner=20T-?= =?UTF-8?q?=EC=A0=91=ED=95=A9=20=EB=B6=84=ED=95=A0=20=EB=B3=B4=EC=99=84=20?= =?UTF-8?q?=E2=80=94=20A/B=20TYPE=20=EB=B0=95=EA=B3=B5=E2=86=92=EC=B2=98?= =?UTF-8?q?=EB=A7=88=20=EC=9A=B0=EC=B8=A1=20=EB=A9=B4=20=EB=AF=B8=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4 --- src/hooks/usePolygon.js | 75 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 7582a390..87769390 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -1664,6 +1664,81 @@ export const usePolygon = () => { } } + // [INNER-TJUNCTION-SPLIT 2026-06-29] 내부선(추녀 hip / 용마루 ridge)의 끝점이 다른 내부선의 몸통(strict + // interior)에 닿는 T-접합인데 그 내부선에 노드가 없으면, getPath 가 그 지점에서 꺾지 못해 한쪽 면이 + // 통째로 누락된다. (A/B TYPE 박공→처마: ridge x=747(64.2→266.4) 의 내부 (747,236.4) 에 hip + // (662.3,236.4)→(747,236.4) 끝점이 닿는데 ridge 가 안 잘려 우측 6각형 면이 안 생김 — GETSPLIT-IO 진단.) + // MOVED-DIVIDER 는 auxiliaryLine 끝점만, 외곽 split 루프는 외곽선만 자른다 → inner-inner T-접합은 둘 다 미커버. + // 여기서 다른 내부선 끝점 위치에서 내부선을 split(노드 삽입)만 한다. merge 없음 — 정상 평면분할 노드를 추가만 하므로 + // 기존에 닫히던 면은 불변(노드가 명시될 뿐). + { + const isInnerT = (l) => l.name === 'hip' || l.name === 'ridge' + const EPS_T = 2 + const mkSegT = (proto, sp, ep) => ({ + name: proto.name, + lineName: proto.lineName, + attributes: { ...proto.attributes }, + startPoint: { x: sp.x, y: sp.y }, + endPoint: { x: ep.x, y: ep.y }, + x1: sp.x, + y1: sp.y, + x2: ep.x, + y2: ep.y, + }) + const onBodyStrictT = (L, P) => { + const ax = L.startPoint.x + const ay = L.startPoint.y + const dx = L.endPoint.x - ax + const dy = L.endPoint.y - ay + const lenSq = dx * dx + dy * dy + if (lenSq < 1) return false + const t = ((P.x - ax) * dx + (P.y - ay) * dy) / lenSq + if (t <= 0.03 || t >= 0.97) return false + const px = ax + t * dx + const py = ay + t * dy + return (P.x - px) ** 2 + (P.y - py) ** 2 < EPS_T * EPS_T + } + const innerLinesT = allLines.filter(isInnerT) + if (innerLinesT.length > 1) { + const innerPts = [] + innerLinesT.forEach((l) => { + innerPts.push(l.startPoint, l.endPoint) + }) + let didTSplit = false + const afterT = [] + allLines.forEach((L) => { + if (!isInnerT(L)) { + afterT.push(L) + return + } + const ax = L.startPoint.x + const ay = L.startPoint.y + const dx = L.endPoint.x - ax + const dy = L.endPoint.y - ay + const lenSq = dx * dx + dy * dy || 1 + const tOf = (P) => ((P.x - ax) * dx + (P.y - ay) * dy) / lenSq + const cuts = [...new Map(innerPts.filter((P) => onBodyStrictT(L, P)).map((P) => [Math.round(tOf(P) * 1000), P])).values()].sort( + (p, q) => tOf(p) - tOf(q), + ) + if (cuts.length === 0) { + afterT.push(L) + return + } + didTSplit = true + let cur = L.startPoint + cuts.forEach((P) => { + afterT.push(mkSegT(L, cur, P)) + cur = P + }) + afterT.push(mkSegT(L, cur, L.endPoint)) + }) + if (didTSplit) { + logger.log(`[INNER-TJUNCTION-SPLIT] inner-inner T-접합 노드 삽입 (allLines ${allLines.length}→${afterT.length})`) + allLines = afterT + } + } + } + // 나눠서 중복 제거된 roof return let newRoofs = getSplitRoofsPoints(allLines)