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)