From f4a3cb7b44df8e96150dd3b207cd5352bb2ccfcf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 27 Aug 2026 07:25:39 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=A0=88=EB=8B=A8=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=85=80=EC=9D=84=20=EC=95=88=EC=AA=BD=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=81=20=EB=B9=84=EC=9C=A8=EB=A1=9C=20=EA=B0=88=EB=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20=EC=9E=94=EC=97=AC=20=EB=A9=94=EC=89=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=A7=80=EC=9A=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 투영 커브 경계 주변에 미세한 성토 메쉬가 남던 문제(사용자 보고). 원인은 `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) --- B05_Profile/B05_Profile_UI_Corridor.ts | 3 +- B05_Profile/B05_Profile_UI_Corridor_Cut.ts | 42 ++++++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) 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) {