From cb06e40f9caf2192fbce58ffbff8d3689fb4377a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 2 Sep 2026 12:06:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=9E=98=EB=A6=B0=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EC=9C=A4=EA=B3=BD=EC=9D=98=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EB=AA=A8=EC=84=9C=EB=A6=AC=20=EC=9C=A0=EC=8B=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=E2=80=94=20dropCollinear=20=EB=8B=AB=ED=9E=98?= =?UTF-8?q?=EC=A0=90=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `_Corridor_Cut.ts` `dropCollinear`: 추적 고리의 닫힘 중복점(머리=꼬리)을 떼고 순환 다듬기 뒤 다시 붙임. 그대로 두면 머리 점의 이웃이 자기 자신이 되어 `away=0` 으로 지워지고 양옆 일직선 점까지 빠져 8고리 전부 시작 모서리가 대각선으로 잘림(63셀 구멍). - 공선 판정의 앞점을 마지막으로 남긴 점 기준으로 변경 — 완만한 곡선에서 누적 편차로 현이 곡선 안쪽으로 파고들던 2셀 제거(`_Region.ts` `simplifyLoop` 과 같은 규칙). - `BUILD_VERSION` 89→90 으로 저장본 만료. - 실측: 되살릴 영역 미포함 셀 63→0(우 7886/7886·좌 7023/7023), 8고리 닫힘, `tmp/tests/test_b05_cut_merged_loop.py` 3건 포함 359 passed. - PLAN.md 해당 항목 완료 표시 + 자체검증 기록(검증 대기). Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Corridor.ts | 6 +++++- B05_Profile/B05_Profile_UI_Corridor_Cut.ts | 24 +++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 6fad466f..0541eadc 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -218,7 +218,11 @@ function fnv1a(text: string): string { // 걸려 있어 BOX 패치는 한 번도 안 잘리고 영역 밖으로 넘쳤다 — 실측에서 BOX 패치 // 리본 두 벌(198.4~203.5m)에 셀 마스크가 없었다. BOX도 `wing-box` 커브로 절취 // 영역을 내므로 같은 반대 규칙이 성립한다. -const BUILD_VERSION = 89; +// 90 = 잘린 자리 윤곽(`cut-merged`)의 **시작 모서리 유실**을 고친다 — `dropCollinear`가 +// 고리의 닫힘 중복점(머리=꼬리)을 순환 이웃으로 셈해 머리 모서리를 지우고, 그 양옆 +// 일직선 점까지 빠져 첫 행이 대각선으로 잘렸다(실측: 8고리 전부, 63셀 구멍 — +// 우 197.6~198.2m BOX 앞 쐐기 33셀이 가장 큼). 되살릴 영역이 이 커브라 저장본을 만료한다. +const BUILD_VERSION = 90; /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Cut.ts b/B05_Profile/B05_Profile_UI_Corridor_Cut.ts index 07f4fc5d..42b90c52 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Cut.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Cut.ts @@ -323,12 +323,24 @@ const COLLINEAR_TOLERANCE_M = 0.001; function dropCollinear( loop: ReadonlyArray<[number, number, number]>, ): Array<[number, number, number]> { - if (loop.length < 3) return [...loop]; + // 추적 고리는 머리 점이 꼬리에 한 번 더 실려 온다(닫힘 표시). **순환으로 다듬기 전에 + // 뗀다** — 그대로 두면 머리 점의 이웃이 자기 자신이 되어 `away = 0`으로 지워지고, 그 + // 모서리 양옆의 일직선 점들까지 함께 빠져 **시작 모서리가 대각선으로 잘려 나갔다** + // (2026-09-02 실측: 고리마다 첫 행 쐐기 63셀이 되살릴 영역에서 빠져 구멍이 됐다). + const closed = + loop.length > 1 && + loop[0][0] === loop[loop.length - 1][0] && + loop[0][1] === loop[loop.length - 1][1]; + const ring = closed ? loop.slice(0, -1) : loop; + if (ring.length < 3) return [...loop]; const kept: Array<[number, number, number]> = []; - for (let i = 0; i < loop.length; i += 1) { - const prev = loop[(i - 1 + loop.length) % loop.length]; - const at = loop[i]; - const next = loop[(i + 1) % loop.length]; + for (let i = 0; i < ring.length; i += 1) { + // 앞점은 **마지막으로 남긴 점**이다 — 바로 앞 원점으로 재면 완만한 곡선에서 점마다 + // 1mm 미만이라 전부 빠지고, 남은 긴 현이 곡선 안쪽으로 2cm 파고든다(2026-09-02 실측: + // 직선부 노견 열의 셀 중심이 현 위에 놓였다). `_Region.ts` `simplifyLoop`과 같은 규칙. + const prev = kept[kept.length - 1] ?? ring[ring.length - 1]; + const at = ring[i]; + const next = ring[(i + 1) % ring.length]; const dx = next[0] - prev[0]; const dy = next[1] - prev[1]; const span = Math.hypot(dx, dy); @@ -339,6 +351,8 @@ function dropCollinear( } kept.push(at); } + // 닫힘 표시는 그대로 돌려준다 — 스케치가 `Line`으로 그려져 꼬리 점이 있어야 닫힌다. + if (closed && kept.length) kept.push(kept[0]); return kept; }