fix(B05): 절단 실패 셀을 안쪽 면적 비율로 갈라 경계 잔여 메쉬를 지운다

투영 커브 경계 주변에 미세한 성토 메쉬가 남던 문제(사용자 보고). 원인은
`clipCellByChord`가 자를 직선을 못 세운 셀을 **통째로 남기던** 폴백이다.

저장 코리도 실좌표로 셀을 재현해 보니 절단 실패 셀 627개(우 314·좌 313) 중
**622개가 셀 안쪽 비율 1.00** — 속은 통째로 커브 안인데 귀퉁이가 경계에 딱 붙어
`inCount === 4` 판정을 못 받고 걸친 셀로 내려간 뒤, 바깥 귀퉁이가 없어 `keepSign`을
못 잡은 셀들이었다(no-keepsign 491 · degenerate-chord 122 · 교점<2 14).

- Cut: `coverage()` 추가(4×4 이중선형 표본) — 절단 실패 셀만 안쪽 비율로 가른다.
  0.5 이상이면 마스크, 미만이면 종전대로 남긴다. 정상 절단 경로·정점은 불변.
- Corridor: BUILD_VERSION 50 → 51(기하 변경 — 안 올리면 옛 저장본이 복원된다).

검증:
- 실빌드 데이터(공용 브라우저 `__corridorBuild.raw`): 안쪽인데 남은 셀 **0**(좌·우),
  커브 밖인데 지운 셀 **0**. 마스크 셀 우 6,611·좌 6,237, 조각 삼각형 711·728.
- 같은 카메라 전후 스크린샷: 바뀐 픽셀의 수정 전 색이 성토면 청록 5,831 → 수정 후 14.
  투영선·스케치선 픽셀은 그대로 — 지워진 건 성토 메쉬뿐이다.
- 저장본 시뮬레이션: 잔여 우 2.17㎡·좌 4.50㎡ → 0.00㎡, 과절단 합 0.0011㎡.
- npm run typecheck 통과, prettier 적용.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 07:25:39 +09:00
co-authored by Claude Opus 5
parent 753e8fcb44
commit f4a3cb7b44
2 changed files with 41 additions and 4 deletions
+2 -1
View File
@@ -99,7 +99,8 @@ function fnv1a(text: string): string {
// 투영 커브 안쪽 성토면 절단 — 커브 다각형 기준(2026-08-26).
// ※ 40에 머물러 있는 동안 기하 수정 3회가 통째로 묻혔다 — 저장본이 그대로
// 복원되어 화면·데이터가 하나도 안 바뀌었다. 기하를 고치면 **반드시** 올릴 것.
const BUILD_VERSION = 50;
// 51 = 절단 실패 셀을 안쪽 면적 비율로 가른다 — 경계 잔여 메쉬 제거(2026-08-27).
const BUILD_VERSION = 51;
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
+39 -3
View File
@@ -21,7 +21,9 @@
* 사실상 직선이고, 볼록 사각형을 직선으로 자르면 결과도 볼록이라 부채꼴 삼각분할이
* 항상 옳다. 다각형 대 다각형 클리핑을 시도했다가 되살릴 면적의 40%를 잃고 톱니가
* 났던 전례가 있다(2026-08-26 실측) — 교점 두 개로 직선을 세우는 지금 방식이 그 함정을
* 피한다. 교점이 2개가 아니면 **통째로 남긴다**(모자라게 자를지언정 더 자르지 않는다).
* 피한다. 자를 직선을 못 세운 **안쪽 면적 비율**로 가른다(2026-08-27 사용자):
* 절반 이상 잠겼으면 지우고 아니면 남긴다. 종전처럼 통째로 남기면 귀퉁이가 경계에 딱
* 붙어 안팎 판정을 못 받은 셀들이 경계에 미세 메쉬로 줄줄이 남았다.
*
* 잘린 자리는 비워 둔다 — 구조물 솔리드가 들어앉아 메운다(2026-08-25 사용자 확정).
* 절토면·도로부·원지반은 건드리지 않는다(사용자: 성토면만).
@@ -170,6 +172,32 @@ function clipCellByChord(
return kept.length >= 3 ? kept : null;
}
/**
* 셀 안쪽 면적 비율 — 4×4 격자 표본으로 어림한다(2026-08-27).
*
* 클리핑이 실패한 셀을 가르는 데만 쓴다. 실패 셀은 거의 전부 **귀퉁이가 커브 경계에
* 딱 붙어** `inCount === 4` 판정을 못 받았을 뿐 속은 통째로 안쪽이다(실측: 실패
* 627개 중 622개가 비율 1.00).
*/
function coverage(areas: ReadonlyArray<CurveArea>, ring: ReadonlyArray<Corner>): number {
const STEPS = 4;
let hits = 0;
for (let i = 0; i < STEPS; i += 1) {
const a = (i + 0.5) / STEPS;
const topX = ring[0].p[0] + (ring[1].p[0] - ring[0].p[0]) * a;
const topY = ring[0].p[1] + (ring[1].p[1] - ring[0].p[1]) * a;
const bottomX = ring[3].p[0] + (ring[2].p[0] - ring[3].p[0]) * a;
const bottomY = ring[3].p[1] + (ring[2].p[1] - ring[3].p[1]) * a;
for (let j = 0; j < STEPS; j += 1) {
const b = (j + 0.5) / STEPS;
const x = topX + (bottomX - topX) * b;
const y = topY + (bottomY - topY) * b;
if (areas.some((area) => inside(area, x, y))) hits += 1;
}
}
return hits / (STEPS * STEPS);
}
/**
* 성토 리본에 **셀 마스크**(+ 축 고정 커브가 걸친 셀의 조각 삼각형)를 달아 돌려준다.
* 원본 정점은 안 건드린다. 아무것도 안 걸리면 리본을 그대로 통과시킨다.
@@ -227,8 +255,16 @@ export function maskFillByPlanCurves(
continue;
}
const kept = clipCellByChord(areas, ring);
// 교점이 2개가 아니면 셀을 통째로 남긴다 — 모자라게 자를지언정 더 자르지 않는다.
if (!kept) continue;
// 절단 실패는 **안쪽 면적 비율**로 가른다(2026-08-27 사용자: 면적 필터).
// 절반 이상 잠긴 셀은 지운다 — 종전처럼 통째로 남기면 경계에 미세 메쉬가
// 줄줄이 남았다(실측: 좌 313·우 314셀, 4.50·2.17㎡).
if (!kept) {
if (coverage(areas, ring) >= 0.5) {
mask[row * (cols - 1) + col] = 1;
masked += 1;
}
continue;
}
mask[row * (cols - 1) + col] = 1;
masked += 1;
for (let k = 1; k < kept.length - 1; k += 1) {