From e14ed81db4a9fb346a5541f32b050c30c9cfcd5c Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 17 Jun 2026 16:25:09 +0900 Subject: [PATCH] =?UTF-8?q?innerLines=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20?= =?UTF-8?q?=EB=B6=84=ED=95=A0=20=EC=95=88=EB=90=98=EB=8A=94=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/usePolygon.js | 115 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index cd2c9b3f..f769f9e7 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -1543,6 +1543,121 @@ export const usePolygon = () => { } } + // [MOVED-DIVIDER 2026-06-17] 내부 분할선(예: A타입 중앙 3868)을 옮기면 경계선만 새 위치로 재분할되고 + // 스켈레톤 hip 은 옛 위치에서 만나, 옮긴 보조선 끝점이 hip 에 노드로 안 붙어 분할이 안 되고 면이 + // 겹쳐 생긴다(ROOF-FACE-DIAG: 거의 동일한 면 2개). 해결: + // (1) 내부 보조선 끝점이 hip 몸통 위(strict)에 있으면 그 hip 을 끝점에서 split → 보조선이 연결됨. + // (2) split 이 일어난 경우에만, 옛 접합점이 degree-2 공선(共線) 잉여 노드가 되면 merge → 옛 분할점 제거. + // 게이트: 실제 split 이 일어난 경우(=내부분할선 이동)에만 (2) 수행 → 외곽선 띠(SUPERSEDED) 케이스는 불변. + { + const innerAux = allLines.filter((l) => l.name === 'auxiliaryLine') + const isInner = (l) => l.name === 'hip' || l.name === 'ridge' + const EPS = 2 + const mkSeg = (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 onBodyStrict = (L, P) => { + const ax = L.startPoint.x, + ay = L.startPoint.y + const dx = L.endPoint.x - ax, + 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, + py = ay + t * dy + return (P.x - px) ** 2 + (P.y - py) ** 2 < EPS * EPS + } + if (innerAux.length > 0) { + const auxPts = [] + innerAux.forEach((a) => { + auxPts.push(a.startPoint, a.endPoint) + }) + // (1) 내부선을 보조선 끝점에서 split + let didSplit = false + const afterSplit = [] + allLines.forEach((L) => { + if (!isInner(L)) { + afterSplit.push(L) + return + } + const ax = L.startPoint.x, + ay = L.startPoint.y + const dx = L.endPoint.x - ax, + 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(auxPts.filter((P) => onBodyStrict(L, P)).map((P) => [Math.round(tOf(P) * 1000), P])).values()].sort( + (p, q) => tOf(p) - tOf(q), + ) + if (cuts.length === 0) { + afterSplit.push(L) + return + } + didSplit = true + let cur = L.startPoint + cuts.forEach((P) => { + afterSplit.push(mkSeg(L, cur, P)) + cur = P + }) + afterSplit.push(mkSeg(L, cur, L.endPoint)) + }) + if (didSplit) { + let work = afterSplit + const key = (p) => `${Math.round(p.x)},${Math.round(p.y)}` + // (2) degree-2 공선 내부선 접합점 merge (잉여 옛 분할점 제거) + let merged = true + let guard = 0 + while (merged && guard++ < 200) { + merged = false + const incident = {} + work.forEach((L) => { + ;[L.startPoint, L.endPoint].forEach((p) => { + ;(incident[key(p)] || (incident[key(p)] = [])).push(L) + }) + }) + for (const k in incident) { + const inc = incident[k] + if (inc.length !== 2) continue + const [la, lb] = inc + if (la === lb || !isInner(la) || !isInner(lb)) continue + const [kx, ky] = k.split(',').map(Number) + const farA = Math.abs(la.startPoint.x - kx) < 1 && Math.abs(la.startPoint.y - ky) < 1 ? la.endPoint : la.startPoint + const farB = Math.abs(lb.startPoint.x - kx) < 1 && Math.abs(lb.startPoint.y - ky) < 1 ? lb.endPoint : lb.startPoint + const cross = (kx - farA.x) * (farB.y - ky) - (ky - farA.y) * (farB.x - kx) + const norm = Math.hypot(kx - farA.x, ky - farA.y) * Math.hypot(farB.x - kx, farB.y - ky) + if (norm < 1 || Math.abs(cross) / norm > 0.02) continue + work = work.filter((L) => L !== la && L !== lb) + work.push(mkSeg(la, farA, farB)) + merged = true + break + } + } + // 동일 좌표 변 중복 제거 + work = work.filter( + (line, idx, self) => + idx === + self.findIndex( + (o) => + (isSamePoint(o.startPoint, line.startPoint) && isSamePoint(o.endPoint, line.endPoint)) || + (isSamePoint(o.startPoint, line.endPoint) && isSamePoint(o.endPoint, line.startPoint)), + ), + ) + logger.log(`[MOVED-DIVIDER] 내부 분할선 이동 감지 — hip 재분할 적용 (allLines ${allLines.length}→${work.length})`) + allLines = work + } + } + } + // 나눠서 중복 제거된 roof return let newRoofs = getSplitRoofsPoints(allLines)