fix(B05): 잘린 자리 윤곽의 시작 모서리 유실 수정 — dropCollinear 닫힘점 처리

- `_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) <noreply@anthropic.com>
This commit is contained in:
2026-09-02 12:06:54 +09:00
co-authored by Claude Opus 5
parent 0ea0fe2ba8
commit cb06e40f9c
2 changed files with 24 additions and 6 deletions
+5 -1
View File
@@ -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 {
+19 -5
View File
@@ -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;
}