diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 99ba887b..4125a77a 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -183,7 +183,13 @@ function fnv1a(text: string): string { // **패널 두께 슬리버**로 섞여 있었고(실측 0.40㎡ / bbox 1.69×1.40 ×2), 그 슬리버가 // "덮인 자리"로 빠져 그 자리만 성토면도 지형도 없는 검은 줄이 됐다. 에이프런은 // 날개벽 발치를 얹으려고 넓혀 둔 부재라 외곽선이 날개를 이미 품는다. -const BUILD_VERSION = 82; +// 83 = 원지반 재노출 영역을 **`cut-merged`**(성토면에서 실제로 도려낸 셀의 윤곽)로 +// 바꾼다. 종전에는 `원본 성토 스케치 − (구조물 ∪ 변형 성토 스케치)`로 영역을 +// **다시 조립**해, 지우는 쪽과 되살리는 쪽이 다른 커브를 쓰며 경계가 어긋났다 +// (날개벽 패널 두께 슬리버 자리에 검은 줄). 이제 지우는 쪽이 남긴 자국을 그대로 +// 쓰므로 **원본 지형 메시가 그 자리에 그대로 남는다** — 새 서피스를 만들지 않는다 +// (2026-08-27 사용자: "원지반 오리지널 데이터에서 가져오라니까"). +const BUILD_VERSION = 83; /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Clip.ts b/B05_Profile/B05_Profile_UI_Corridor_Clip.ts index 5315b9f3..4038a879 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Clip.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Clip.ts @@ -22,7 +22,6 @@ import * as THREE from "three"; import type { ModelBounds } from "./B05_Profile_UI_Markers"; import type { CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build"; -import type { PlanCurve } from "./B05_Profile_UI_Corridor_Plan"; import type { TerrainBandSplit } from "./B05_Profile_UI_Corridor_Split"; import type { P2, PlanRegion, RegionSegment } from "./B05_Profile_UI_Corridor_Region"; import { @@ -371,76 +370,51 @@ function clipPoints( } /** - * 구조물 안착 자리에서 **원지반을 되살린다**(2026-08-27 사용자 확정 공식) — + * 구조물 안착 자리에서 **원지반을 되살린다** — 그 자리 지형을 도려내지 않고 **원본 + * 지형 메시 그대로** 남긴다(2026-08-27 사용자: "원지반 오리지널 데이터에서 가져오라니까", + * "회색 서피스는 있는데 원지반과 같은 메시가 아니잖아. 너가 그냥 만든 거잖아"). * - * 되살릴 영역 = 원본 성토사면 스케치(`slope-original`, 파란 커브) - * − (구조물 평면 투영 스케치(`structure`) ∪ 변형 성토사면 스케치(`slope-deformed`)) + * 되살릴 영역 = **`cut-merged`** — 성토면에서 **실제로 도려낸 셀**을 되짚어 낸 윤곽이다 + * (`_Corridor_Cut.ts` `traceMaskLoops`). 지우는 쪽이 남긴 자국을 그대로 쓰므로, 구조물 + * 풋프린트로 영역을 다시 조립할 필요가 없다. * - * 원래 성토면이 덮던 자리에서 지금도 무언가(구조물 또는 변형 성토면)가 덮는 자리를 - * 빼면, 성토가 사라져 원지반이 드러나야 하는 자리만 남는다. **변형 성토사면이 없는 - * 세트도 있다**(사용자) — 그때는 구조물 스케치만 뺀다. 원본 커브가 없으면 되살릴 - * 것도 없다(스트립 그대로). + * 종전에는 `원본 성토 스케치 − (구조물 ∪ 변형 성토 스케치)`로 영역을 **다시 만들었다**. + * 지우는 쪽(성토 셀 마스크)과 되살리는 쪽(구조물 고리)이 서로 다른 커브를 써 경계가 + * 어긋났고, 날개벽이 패널 두께 슬리버로 남아 그 자리만 성토면도 지형도 없는 검은 줄이 + * 됐다. BOX암거에서는 변형 성토선이 원본과 collinear가 되어 영역이 통째로 공집합이 됐다. + * + * `cut-merged`가 없으면(절취를 안 했으면) 되살릴 것도 없다 — 스트립 그대로. */ function withStructureHoles( strip: CorridorStrip, build: CorridorBuildResult, bounds: ModelBounds, ): PlanRegion { - const loopsOf = ( - sources: ReadonlyArray, - only?: (curve: PlanCurve) => boolean, - ): Array> => { - const loops: Array> = []; - for (const curve of build.planCurves ?? []) { - if (!sources.includes(curve.source)) continue; - if (only && !only(curve)) continue; - for (const loop of curve.loops) { - if (loop.length >= 4) loops.push(loop.map(([x, y]) => [x, y] as [number, number])); - } + const loops: Array> = []; + for (const curve of build.planCurves ?? []) { + if (curve.source !== "cut-merged") continue; + for (const loop of curve.loops) { + if (loop.length >= 4) loops.push(loop.map(([x, y]) => [x, y] as [number, number])); } - return loops; - }; - /** - * BOX암거 **날개 사다리꼴**(`wing-box`)도 되살릴 후보에 넣는다(2026-08-27 사용자: - * "박스암거의 날개부의 평면 스케치 영역까지 원지반이 같이 노출되어야 함"). - * - * 그 자리는 절취 소스(`_Corridor_Cut.ts` `CUT_SOURCES`)에 들어 있어 성토면이 - * 파이는데, 되살릴 후보 목록(`slope-original`)에는 없어 **성토면도 원지반도 없는 - * 구멍**으로 남았다. 지우는 이름 목록과 되살리는 이름 목록이 어긋난 것이다. - * - * 세월교는 넣지 않는다 — 그 측 날개 자리는 되메움 패치가 덮는다(`_Corridor_Carve.ts` - * `wingRangeAt`, `isFord` 전용). 지형까지 올리면 패치면과 같은 높이에서 겹쳐 - * z-fight가 난다. `axisLocked`가 곧 BOX 표식이다(`_Corridor_Plan.ts` `boxAxis`). - */ - const originalLoops = [ - ...loopsOf(["slope-original"]), - ...loopsOf(["wing-box"], (curve) => curve.axisLocked === true), - ]; - if (!originalLoops.length) return strip; - const original = new PlanLoopRegion(originalLoops, bounds); - if (original.isEmpty) return strip; - // 지금도 덮여 있는 자리 — 구조물 ∪ 변형 성토사면(없을 수 있다). - const coveredLoops = loopsOf(["structure", "slope-deformed"]); - const covered = coveredLoops.length ? new PlanLoopRegion(coveredLoops, bounds) : null; - const expose: PlanRegion = - covered && !covered.isEmpty ? new SubtractedRegion(original, [covered]) : original; - // 검증용 요약(화면 밖 수치 확인) — `__corridorClip`과 같은 용도. **고리 수만으로는 - // 되살릴 영역이 실제로 남았는지 알 수 없다**: BOX암거에서 `covered`가 `original`을 - // 통째로 삼켜 공집합이 됐는데도 이 훅은 정상값을 찍었다(2026-08-27). 되살릴 영역을 - // 격자로 찍어 실적을 함께 담는다 — 코리도 빌드당 한 번이라 비용은 무시할 만하다. + } + if (!loops.length) return strip; + const expose = new PlanLoopRegion(loops, bounds); + if (expose.isEmpty) return strip; + // 검증용 요약(화면 밖 수치 확인) — `__corridorClip`과 같은 용도. 고리 수만으로는 + // 되살릴 영역이 실제로 남았는지 알 수 없어(BOX에서 공집합인데도 정상값이 찍혔다) + // 영역을 격자로 찍은 실적을 함께 담는다. 코리도 빌드당 한 번이라 비용은 무시할 만하다. const SAMPLES = 80; let inside = 0; for (let i = 0; i < SAMPLES; i += 1) { - const x = original.minX + ((original.maxX - original.minX) * (i + 0.5)) / SAMPLES; + const x = expose.minX + ((expose.maxX - expose.minX) * (i + 0.5)) / SAMPLES; for (let j = 0; j < SAMPLES; j += 1) { - const z = original.minZ + ((original.maxZ - original.minZ) * (j + 0.5)) / SAMPLES; + const z = expose.minZ + ((expose.maxZ - expose.minZ) * (j + 0.5)) / SAMPLES; if (expose.contains({ x, z })) inside += 1; } } (window as unknown as { __corridorHoles?: unknown }).__corridorHoles = { - original: originalLoops.length, - covered: coveredLoops.length, - /** 되살릴 영역에 든 표본 점 수 / 전체 표본(원본 bbox 격자 80×80). 0이면 공집합. */ + /** 되살릴 영역 고리 수 = 실제로 도려낸 자리 윤곽(`cut-merged`) 개수. */ + exposeLoops: loops.length, exposeCells: inside, exposeRatio: Number((inside / (SAMPLES * SAMPLES)).toFixed(4)), };