diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 9f19d786..13b4d1cb 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -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 { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Cut.ts b/B05_Profile/B05_Profile_UI_Corridor_Cut.ts index ec65bb35..0f96e32c 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Cut.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Cut.ts @@ -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, ring: ReadonlyArray): 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) {