From 87ffe800ed2646c1334ec344f5f235d95590f904 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 14:56:48 +0900 Subject: [PATCH] =?UTF-8?q?refactor(b05):=20=EC=A7=80=EA=B8=88=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EA=BC=AC=EB=A6=AC=20=C2=B7=20=ED=8F=89=EB=A9=B4=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=B9=98=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20Old?= =?UTF-8?q?=20=EB=A1=9C(135-1=20=EB=82=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 통째 이동 = _Plan · _Plan_Footprint · _Cut · _Carve · _Carve_Wing · _Union · _Skirt - 잘라 냄(원본은 Old 같은 이름) = _Build(스케치 · 자르기 · 패치 꼬리 · 깃발 · 측벽 지반 표고) · _Build_Types · _Envelope · _Mesh(patch · cellMask · trimTris · cutWalls · planCurves) · 뷰어 디버그 · 뷰어코리도(__corridorPlanCurves) - BUILD_VERSION · 행 자리 그대로 — 노면 · 구조물 · 외곽 같음(옛 저장본 남는 몫은 무시) - 시험 2개 + 도우미 2 = resources/tester/old/ Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt --- B05_Profile/B05_Profile_UI_Corridor.ts | 2 +- B05_Profile/B05_Profile_UI_Corridor_Build.ts | 77 +-- .../B05_Profile_UI_Corridor_Build_Types.ts | 61 +- B05_Profile/B05_Profile_UI_Corridor_Deform.ts | 2 +- .../B05_Profile_UI_Corridor_Envelope.ts | 66 --- B05_Profile/B05_Profile_UI_Corridor_Mesh.ts | 140 +---- B05_Profile/B05_Profile_UI_Corridor_Tin.ts | 2 +- B05_Profile/B05_Profile_UI_Viewer_Corridor.ts | 26 +- B05_Profile/B05_Profile_UI_Viewer_Debug.ts | 16 +- .../B05_Profile_UI_Corridor_Build_Types.ts | 102 ++++ .../B05_Profile_UI_Corridor_Carve.ts | 0 .../B05_Profile_UI_Corridor_Carve_Wing.ts | 0 .../{ => Old}/B05_Profile_UI_Corridor_Cut.ts | 0 .../Old/B05_Profile_UI_Corridor_Envelope.ts | 331 +++++++++++ .../Old/B05_Profile_UI_Corridor_Mesh.ts | 538 ++++++++++++++++++ .../{ => Old}/B05_Profile_UI_Corridor_Plan.ts | 0 .../B05_Profile_UI_Corridor_Plan_Footprint.ts | 0 .../B05_Profile_UI_Corridor_Skirt.ts | 0 .../B05_Profile_UI_Corridor_Union.ts | 0 .../Old/B05_Profile_UI_Viewer_Debug.ts | 55 ++ .../{ => old}/helper_b05_cut_merged_loop.cjs | 0 .../{ => old}/helper_b05_patch_skirt.cjs | 0 .../{ => old}/test_b05_cut_merged_loop.py | 0 .../tester/{ => old}/test_b05_patch_skirt.py | 0 24 files changed, 1039 insertions(+), 379 deletions(-) create mode 100644 B05_Profile/Old/B05_Profile_UI_Corridor_Build_Types.ts rename B05_Profile/{ => Old}/B05_Profile_UI_Corridor_Carve.ts (100%) rename B05_Profile/{ => Old}/B05_Profile_UI_Corridor_Carve_Wing.ts (100%) rename B05_Profile/{ => Old}/B05_Profile_UI_Corridor_Cut.ts (100%) create mode 100644 B05_Profile/Old/B05_Profile_UI_Corridor_Envelope.ts create mode 100644 B05_Profile/Old/B05_Profile_UI_Corridor_Mesh.ts rename B05_Profile/{ => Old}/B05_Profile_UI_Corridor_Plan.ts (100%) rename B05_Profile/{ => Old}/B05_Profile_UI_Corridor_Plan_Footprint.ts (100%) rename B05_Profile/{ => Old}/B05_Profile_UI_Corridor_Skirt.ts (100%) rename B05_Profile/{ => Old}/B05_Profile_UI_Corridor_Union.ts (100%) create mode 100644 B05_Profile/Old/B05_Profile_UI_Viewer_Debug.ts rename resources/tester/{ => old}/helper_b05_cut_merged_loop.cjs (100%) rename resources/tester/{ => old}/helper_b05_patch_skirt.cjs (100%) rename resources/tester/{ => old}/test_b05_cut_merged_loop.py (100%) rename resources/tester/{ => old}/test_b05_patch_skirt.py (100%) diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 492940b75..e9c39a6b3 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -145,7 +145,7 @@ export type ProfileSamples = Array<{ /** 마지막 코리도가 **어디서 왔는지** — 저장본을 그대로 썼나, 브라우저가 다시 만들었나. * 서버 사전 생성(2026-09-04)이 실제로 먹었는지 화면 밖에서 수치로 확인하는 창구다 - * (`__corridorScene`·`__corridorSkirt`와 같은 디버그 훅). */ + * (`__corridorScene`·`__corridorClip`와 같은 디버그 훅). */ declare global { interface Window { __corridorSource?: { source: "stored" | "built"; hash: string; at: number }; diff --git a/B05_Profile/B05_Profile_UI_Corridor_Build.ts b/B05_Profile/B05_Profile_UI_Corridor_Build.ts index a4f3384f2..518edba60 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Build.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Build.ts @@ -50,26 +50,9 @@ import type { export type { CorridorBuildResult, CorridorCap, - CorridorCutWall, CorridorRibbon, } from "./B05_Profile_UI_Corridor_Build_Types"; -import { buildStructurePatchRibbons } from "./B05_Profile_UI_Corridor_Carve"; import { structureSilhouettes } from "./B05_Profile_UI_Corridor_Station_Structure"; -import { buildPlanCurves } from "./B05_Profile_UI_Corridor_Plan"; -import { maskFillByPlanCurves } from "./B05_Profile_UI_Corridor_Cut"; -export type { PlanCurve } from "./B05_Profile_UI_Corridor_Plan"; - -/** 리본 하나 — rows[i]는 프레임 i의 단면 점(colCount개, 모델 x/y/z 평탄 배열). */ -/** - * 구조물 구간 **성토 패치 리본** 적용 여부(2026-08-27 사용자). B06 횡단이 내는 - * 변형 성토선을 3D 포켓에 되메운다 — 다단이면 `성토 > 구조물 > 성토`라 성토선이 - * 하나가 아니므로 **구간 1줄 = 리본 1장**으로 낸다(`_Carve.ts` `fillRuns`). - * - * 절취(`FILL_CUT_APPLY`)와 별개다: 패치는 잘린 자리를 채우기만 하고, 절취·측벽· - * 경계 트림(0-2~0-6)은 그대로 둔다. 패치 리본은 절취 대상이 아니다 - * (`_Cut.ts` `ribbon.patch` 제외). - */ -const FILL_PATCH_APPLY: boolean = true; /** 종방향 세분 간격(m) — 곡선 각짐 방지(2026-08-23 사용자: 노선 폴리라인 따라 세분). * 평면 궤적을 스플라인으로 잇게 되어(2026-08-23) 0.1m까지 쪼갤 이유가 없어졌다 — @@ -84,16 +67,8 @@ const SUBDIVIDE_STEP_M = 0.2; const STRUCTURE_SPAN_STEP_M = SUBDIVIDE_STEP_M; /** - * 투영 커브 안쪽 **성토면 도려내기** 적용 여부(2026-08-26 사용자). 끄면 커브만 얹고 - * 서피스는 그대로 둔다. 절토면은 대상이 아니다(사용자: 성토면만). - */ -const FILL_CUT_APPLY: boolean = true; - -/** - * 구조물 점유 구간의 시작·끝 누가거리 — 여기를 **행 경계로 강제**해야 탑뷰 투영 - * 커브가 격자 모서리 위에 앉는다. 값은 `_Corridor_Plan.ts`의 비탈 커브 구간과 같은 - * 출처(`structureSilhouettes`의 전/후 몫)를 쓴다 — 두 곳이 어긋나면 커브 끝이 다시 - * 셀 중간에 뜬다. + * 구조물 점유 구간의 시작·끝 누가거리 — 여기를 **행 경계로 강제**한다(옛 탑뷰 투영 + * 커브용 · 행 자리가 저장본 판번호에 묶여 그대로 둠). 출처 = `structureSilhouettes`의 전/후 몫. */ interface StructureSpanRange { side: CorridorSide; @@ -200,8 +175,6 @@ export function buildCorridor( sections: Map; outerLeft: OffsetPoint; outerRight: OffsetPoint; - /** 그 행 지반선 — 성토 리본에 원지반 표고를 함께 실어 절단면 측벽의 밑선으로 쓴다. */ - ground: ((offset: number) => number) | null; }> = []; const toModel = (row: (typeof rows)[number], point: OffsetPoint): [number, number, number] => [ @@ -497,7 +470,6 @@ export function buildCorridor( sections, outerLeft: outerOf("left"), outerRight: outerOf("right"), - ground: groundRow, }); } } @@ -535,10 +507,7 @@ export function buildCorridor( } const chainages: number[] = []; const positions = new Float64Array((endExclusive - start) * colCount * 3); - // 성토 리본만 원지반 표고를 같이 싣는다 — 잘린 자리 측벽의 밑선(2026-08-27 사용자). - const groundZ = kind === "fill" ? new Float64Array((endExclusive - start) * colCount) : null; let cursor = 0; - let groundCursor = 0; for (let row = start; row < endExclusive; row += 1) { chainages.push(rows[row].chainage_m); rows[row].sections.get(key)!.forEach((point) => { @@ -547,14 +516,9 @@ export function buildCorridor( positions[cursor + 1] = y; positions[cursor + 2] = z; cursor += 3; - if (groundZ) { - // 지반선을 못 읽는 행은 서피스 표고를 그대로 둔다 — 높이 0짜리 벽이 된다. - groundZ[groundCursor] = rows[row].ground?.(point.offset_m) ?? z; - groundCursor += 1; - } }); } - ribbons.push({ kind, side, colCount, chainages, positions, ...(groundZ ? { groundZ } : {}) }); + ribbons.push({ kind, side, colCount, chainages, positions }); // 측구 리본이 노선 중간에서 시작·종료하면 그 자리를 벽으로 막는다. if (kind === "ditch") { if (start > 0) pushDitchCap(rows[start], key, side); @@ -590,38 +554,5 @@ export function buildCorridor( }); if (points.length >= 2) caps.push({ points }); }); - // 탑뷰 투영 커브(2026-08-26 사용자) — 세트별 평면(구조물 상단 +5m)에 눕힌 - // 구조물·비탈 커브. 원본 비탈선은 코리도 바깥 윤곽(outline)을 그대로 읽는다. - // 리본을 함께 넘겨 파란 스케치 커브를 절·성토면에 얹은 빨간 투영선도 낸다. - // - // **자르기보다 먼저** 내야 한다 — 성토면을 먼저 도려내면 투영선이 z를 읽을 면이 - // 없어져 커브가 통째로 사라진다(2026-08-26). - const planCurves = buildPlanCurves(crossSections, structures, frameAt, outline, ribbons); - - // 커브 안쪽 성토면은 **셀 마스크**로 지운다 — 행을 잘라 내면 결국 노선 기준이 - // 되어 비스듬한 커브에서 어긋난다(2026-08-26 사용자 지적, `_Cut.ts` 머리말). - // 잘린 자리 윤곽(`cut-merged`)은 새 데이텀에 얹어 기존 커브 **뒤에 덧붙인다**. - const cut = FILL_CUT_APPLY ? maskFillByPlanCurves(ribbons, planCurves) : null; - - // 성토 패치는 **투영 커브·절취를 다 낸 뒤** 붙인다 — 먼저 넣으면 투영선이 패치 - // 표면에서 z를 읽어 절취 경계가 달라지고, 절취가 패치를 다시 갉는다. - const patchRibbons = FILL_PATCH_APPLY - ? buildStructurePatchRibbons(crossSections, structures, frameAt, profileZ ?? null, planCurves) - : []; - // 패치도 같은 커브로 한 번 더 훑는다 — 단 **반대 규칙**이다(`patchClip` 표식이 붙은 - // 세월교 패치만: 절취 영역 **안쪽만** 남긴다). 지우는 면과 채우는 면이 같은 경계를 - // 공유해야 삐져나오지도 벌어지지도 않는다(2026-08-27 사용자). 병합 커브·측벽은 - // 여기서 안 나온다 — 기본 성토면 몫이다. - const clippedPatches = - FILL_CUT_APPLY && patchRibbons.length - ? maskFillByPlanCurves(patchRibbons, planCurves).ribbons - : patchRibbons; - return { - ribbons: [...(cut ? cut.ribbons : ribbons), ...clippedPatches], - outline, - caps, - structures, - planCurves: cut ? [...planCurves, ...cut.curves] : planCurves, - cutWalls: cut ? cut.walls : [], - }; + return { ribbons, outline, caps, structures }; } diff --git a/B05_Profile/B05_Profile_UI_Corridor_Build_Types.ts b/B05_Profile/B05_Profile_UI_Corridor_Build_Types.ts index 9dcf752d7..d984d4c9d 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Build_Types.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Build_Types.ts @@ -1,6 +1,6 @@ /* ============================================================================= * B05_Profile_UI_Corridor_Build_Types.ts - * 코리도 빌더가 내는 **결과 자료형** — 리본·절취 측벽·캡·빌드 결과. + * 코리도 빌더가 내는 **결과 자료형** — 리본·캡·빌드 결과. * * `B05_Profile_UI_Corridor_Build` 에서 떼어냈다(700줄 제한, 2026-09-02). 계산은 전혀 * 없고 모양과 그 뜻(주석)만 있다. 빌더가 그대로 다시 내보내므로 기존 import 경로 @@ -10,25 +10,12 @@ import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; import type { CorridorKind, CorridorSide } from "./B05_Profile_UI_Corridor_Station"; import type { CorridorStructure } from "./B05_Profile_UI_Corridor_Structures"; -import type { PlanCurve } from "./B05_Profile_UI_Corridor_Plan"; export interface CorridorRibbon { kind: CorridorKind; side: CorridorSide; colCount: number; chainages: number[]; - /** - * 구조물 **패치 리본**(2026-08-25) — 투영커브로 잘린 자리에 다시 그리는 변형 성토면. - * 절단 대상에서 빠진다(패치까지 자르면 구멍이 도로 뚫린다). - */ - patch?: boolean; - /** - * 패치를 **절취 영역 안쪽으로 클립**한다(2026-08-27 사용자: 삐져나오지도 벌어지지도 - * 않게). `maskFillByPlanCurves`가 기본 성토면을 지우는 규칙의 **정확한 반대**를 건다 — - * 영역 밖 셀은 지우고, 걸친 셀은 안쪽 조각만 남긴다. 두 면이 같은 경계를 공유하니 - * 겹침도 구멍도 안 생긴다. **세월교 전용** — 구조물별로 로직이 다르다. - */ - patchClip?: boolean; /** * rowCount × colCount × 3 (모델 좌표). * @@ -37,42 +24,6 @@ export interface CorridorRibbon { * 생겼다(2026-08-23 실측). 화면에 올릴 때 원점을 빼면서 Float32로 줄인다. */ positions: Float64Array; - /** - * 셀 마스크 — `(rowCount-1) × (colCount-1)`, 1이면 **그 셀은 안 그린다** - * (2026-08-26 투영 커브 안쪽 지우기). 정점은 그대로 두고 삼각형만 뺀다. - */ - cellMask?: Uint8Array; - /** - * 축 고정 커브(BOX암거)가 **걸친 셀**을 커브에서 잘라 되살린 조각 삼각형 - * (모델 좌표, 삼각형당 9). 격자로는 못 담는 비스듬한 경계를 여기로 메운다 - * (2026-08-26 사용자: 이원화하여 관리). - */ - trimTris?: Float64Array; - /** `trimTris`와 짝인 UV(삼각형당 6) — 빗금이 실척으로 이어지게 한다. */ - trimUvs?: Float32Array; - /** - * 격자점별 **원지반 표고**(rowCount × colCount) — 성토 리본에만 싣는다. - * 잘린 자리 측벽을 성토면에서 원지반까지 세우는 밑선이다(2026-08-27 사용자). - */ - groundZ?: Float64Array; -} - -/** - * 잘린 자리 **측벽 서피스**(2026-08-27 사용자) — 리본 메시에 섞지 않고 구조물처럼 - * 따로 그린다. 삼각형 수프(정점당 3, 삼각형당 9). 빗금이 없어 UV는 담지 않는다. - */ -export interface CorridorCutWall { - side: "left" | "right"; - positions: Float64Array; - /** 패치 바깥 끝 스커트인가(2026-08-27) — 뷰어가 지형 색인으로 뒤늦게 붙이므로, - * 다시 붙일 때 옛 것을 걷어내려면 표식이 있어야 한다(`_Corridor_Skirt.ts`). */ - patchSkirt?: boolean; - /** - * 정점별 UV(정점당 2) — **빗금 있는 성토 서피스**로 그릴 때만 싣는다. - * 절취 측벽은 일반 서피스(빗금 없음)이고, 패치 스커트는 성토면의 연장이라 빗금을 - * 넣는다(2026-08-27 사용자: "변형 성토면 하단 끝단부 서피스는 메시 형태"). - */ - uvs?: Float32Array; } /** 시·종점 마구리(캡) — 설계선과 지반선 사이를 세로로 봉인하는 스트립(모델 좌표). */ @@ -89,14 +40,6 @@ export interface CorridorBuildResult { caps: CorridorCap[]; /** 배수관 세트 구조물(기슭막이·집수정·배관) 솔리드(2026-08-23). 없으면 빈 배열. */ structures: CorridorStructure[]; - /** - * 구조물 세트별 **탑뷰 투영 커브**(2026-08-26 사용자) — 세트 구조물 상단 5m - * 평면에 눕힌 닫힌 고리들. 구조물·비탈(변형/원본)을 유입·유출로 나눠 담는다. - * 저장 코리도엔 없을 수 있다. - */ - planCurves?: PlanCurve[]; - /** 절취 측벽 서피스(2026-08-27) — 저장본에도 담는다. 없으면 빈 배열. */ - cutWalls?: CorridorCutWall[]; - /** 측점 단면 — 새 방식 지형 변형이 비탈 단면을 읽는다(115-1). 메모리만 · 저장본에 안 담는다. */ + /** 측점 단면 — 제안 ③ 사면이 비탈 단면을 읽는다(115-1). 메모리만 · 저장본에 안 담는다. */ sections?: CrossSection[]; } diff --git a/B05_Profile/B05_Profile_UI_Corridor_Deform.ts b/B05_Profile/B05_Profile_UI_Corridor_Deform.ts index c5580ef2a..f29b941ad 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Deform.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Deform.ts @@ -41,7 +41,7 @@ export function roadRows(build: CorridorBuildResult): DeformRow[] { >(); for (const ribbon of build.ribbons) { const rank = RANK[ribbon.kind]; - if (!rank || ribbon.patch) continue; + if (!rank) continue; const cols = ribbon.colCount; ribbon.chainages.forEach((chainage, row) => { const key = Math.round(chainage * 1000); diff --git a/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts b/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts index 960302b92..771f51404 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts @@ -37,34 +37,12 @@ export interface CorridorEnvelope { colCount: number; chainages: number[]; positionsBase64: string; - /** - * 구조물 패치 리본 표식(2026-09-02) — 빌드 때만 쓰는 값이 아니라 **뷰어가 그릴 때도 - * 본다**(`B05_Profile_UI_Viewer.ts` 지형 스냅 제외). 안 담으면 저장본으로 다시 연 - * 코리도가 패치인 줄 몰라 바깥 끝이 원지반까지 끌려 내려간다. - */ - patch?: boolean; - patchClip?: boolean; - /** 셀 마스크(2026-08-26 투영 커브 안쪽 지우기) — 없으면 구멍 없는 리본. */ - cellMaskBase64?: string; - /** 축 고정 커브가 걸친 셀의 조각 삼각형·UV(2026-08-26). */ - trimTrisBase64?: string; - trimUvsBase64?: string; }>; outline: CorridorBuildResult["outline"]; /** 시·종점 마구리(구 저장본엔 없음 — 없으면 빈 배열로 복원). */ caps?: CorridorBuildResult["caps"]; /** 배수관 세트 구조물(2026-08-23 — 구 저장본엔 없음, 없으면 빈 배열). */ structures?: CorridorBuildResult["structures"]; - /** - * 구조물 세트별 탑뷰 투영 커브(2026-08-26) — 저장본에도 담는다. 안 담으면 - * 저장본으로 복원한 코리도에서는 커브가 통째로 사라진다. - */ - planCurves?: CorridorBuildResult["planCurves"]; - /** 절취 측벽 서피스(2026-08-27) — 구 저장본엔 없다. 없으면 빈 배열로 복원. */ - cutWalls?: Array<{ - side: "left" | "right"; - positionsBase64: string; - }>; } /** FNV-1a 32bit — 설계 입력 요약 문자열의 버전 해시. */ @@ -247,22 +225,6 @@ function base64FromFloats(values: Float64Array): string { return btoa(binary); } -function base64FromBytes(bytes: Uint8Array): string { - let binary = ""; - const CHUNK = 0x8000; - for (let i = 0; i < bytes.length; i += CHUNK) { - binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK)); - } - return btoa(binary); -} - -function bytesFromBase64(encoded: string): Uint8Array { - const binary = atob(encoded); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i); - return bytes; -} - function floatsFromBase64(encoded: string): Float64Array { const binary = atob(encoded); const bytes = new Uint8Array(binary.length); @@ -280,24 +242,10 @@ export function serialize(build: CorridorBuildResult, hash: string): CorridorEnv colCount: ribbon.colCount, chainages: ribbon.chainages, positionsBase64: base64FromFloats(ribbon.positions), - ...(ribbon.patch ? { patch: true } : {}), - ...(ribbon.patchClip ? { patchClip: true } : {}), - ...(ribbon.cellMask ? { cellMaskBase64: base64FromBytes(ribbon.cellMask) } : {}), - ...(ribbon.trimTris && ribbon.trimUvs - ? { - trimTrisBase64: base64FromFloats(ribbon.trimTris), - trimUvsBase64: base64FromBytes(new Uint8Array(ribbon.trimUvs.buffer.slice(0))), - } - : {}), })), outline: build.outline, caps: build.caps, structures: build.structures, - planCurves: build.planCurves, - cutWalls: (build.cutWalls ?? []).map((wall) => ({ - side: wall.side, - positionsBase64: base64FromFloats(wall.positions), - })), }; } @@ -309,23 +257,9 @@ export function deserialize(envelope: CorridorEnvelope): CorridorBuildResult { colCount: ribbon.colCount, chainages: ribbon.chainages, positions: floatsFromBase64(ribbon.positionsBase64), - ...(ribbon.patch ? { patch: true } : {}), - ...(ribbon.patchClip ? { patchClip: true } : {}), - ...(ribbon.cellMaskBase64 ? { cellMask: bytesFromBase64(ribbon.cellMaskBase64) } : {}), - ...(ribbon.trimTrisBase64 && ribbon.trimUvsBase64 - ? { - trimTris: floatsFromBase64(ribbon.trimTrisBase64), - trimUvs: new Float32Array(bytesFromBase64(ribbon.trimUvsBase64).buffer), - } - : {}), })), outline: envelope.outline, caps: envelope.caps ?? [], structures: envelope.structures ?? [], - planCurves: envelope.planCurves ?? [], - cutWalls: (envelope.cutWalls ?? []).map((wall) => ({ - side: wall.side, - positions: floatsFromBase64(wall.positionsBase64), - })), }; } diff --git a/B05_Profile/B05_Profile_UI_Corridor_Mesh.ts b/B05_Profile/B05_Profile_UI_Corridor_Mesh.ts index 855fd5899..425d36170 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Mesh.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Mesh.ts @@ -19,7 +19,6 @@ import type { CorridorKind, CorridorRibbon, CorridorStructure, - PlanCurve, } from "./B05_Profile_UI_Corridor_Build"; /** @@ -35,44 +34,6 @@ const KIND_COLORS: Record = { fill: 0x06b6d4, }; -/** 탑뷰 투영 커브 색(2026-08-26 사용자) — 종류별. 지형·코리도 색과 안 겹친다. */ -const PLAN_CURVE_COLORS: Record = { - structure: 0xffd93d, - "slope-deformed": 0xff2d78, - "slope-original": 0x38bdf8, - // 날개벽 성토끝 박스 — 구조물 노랑과 구분되게 주황(2026-08-26 사용자). - "wing-box": 0xff8c1a, - // 스케치 파란 커브를 절·성토면에 얹은 투영선 — 빨강(2026-08-26 사용자 지정). - "slope-projected": 0xff3b30, - // 실제로 잘린 자리 윤곽(2026-08-27 사용자) — 새 데이텀(+3m)에 얹어 라임으로 낸다. - "cut-merged": 0x84cc16, - // 측벽이 설 자리 — 벽 서피스로만 쓰고 선으로는 안 그린다(아래에서 걸러진다). - "cut-wall": 0x000000, -}; - -/** - * 평면 스케치(투영 커브) 묶음의 이름 — 뷰어가 이 이름으로 찾아 켜고 끈다. - * - * **최종 결과물에서는 숨긴다**(2026-09-02 사용자). 이 커브들은 구조물이 어디를 지우고 - * 어디를 다시 채우는지 눈으로 좇으려고 만든 것이라 설계 산출물이 아니다. 다만 절취·패치 - * 기하를 다시 들여다볼 때 쓸모가 있어 **지우지 않고 감춰만 둔다**. - */ -export const PLAN_CURVE_GROUP = "corridor-plan-sketch"; - -/** 평면 스케치를 켜 둔 브라우저인가 — 기본은 꺼짐. 디버깅용 스위치다. */ -export const PLAN_CURVE_FLAG = "frd_debug_plan_curves"; - -export function planCurvesVisible(): boolean { - try { - return window.localStorage.getItem(PLAN_CURVE_FLAG) === "1"; - } catch { - return false; // 저장소가 막힌 환경(사생활 보호 모드 등) — 꺼진 것으로 본다. - } -} - -/** 유출측 파선 눈금(m) — 유입은 실선이다. */ -const PLAN_DASH_M = { dashSize: 0.6, gapSize: 0.4 }; - /** 마구리(시·종점 절단면) 색 — 절단면임이 드러나게 어둡게. */ const CAP_COLOR = 0x2a2f3a; @@ -157,12 +118,8 @@ function ribbonGeometry(ribbon: CorridorRibbon, origin: SceneOrigin): THREE.Buff } } const indices: number[] = []; - // 셀 마스크가 선 자리는 삼각형을 빼 그 자리를 비운다 — 투영 커브 안쪽 지우기 - // (2026-08-26). 정점은 그대로 두므로 남는 면의 형상은 하나도 안 바뀐다. - const mask = ribbon.cellMask; for (let row = 0; row < rowCount - 1; row += 1) { for (let col = 0; col < cols - 1; col += 1) { - if (mask?.[row * (cols - 1) + col]) continue; const a = row * cols + col; const b = a + 1; const c = a + cols; @@ -170,27 +127,9 @@ function ribbonGeometry(ribbon: CorridorRibbon, origin: SceneOrigin): THREE.Buff indices.push(a, c, b, b, c, d); } } - // 축 고정 커브가 걸친 셀은 격자로 못 담는다 — 잘라 되살린 조각 삼각형을 뒤에 붙인다. - const trim = ribbon.trimTris; - const trimCount = trim ? trim.length / 3 : 0; - const allPositions = new Float32Array((rowCount * cols + trimCount) * 3); - const allUvs = new Float32Array((rowCount * cols + trimCount) * 2); - allPositions.set(positions); - allUvs.set(uvs); - if (trim) { - const base = rowCount * cols; - for (let v = 0; v < trimCount; v += 1) { - allPositions[(base + v) * 3] = trim[v * 3] - origin.cx; - allPositions[(base + v) * 3 + 1] = trim[v * 3 + 2] - origin.cz; - allPositions[(base + v) * 3 + 2] = -(trim[v * 3 + 1] - origin.cy); - allUvs[(base + v) * 2] = ribbon.trimUvs?.[v * 2] ?? 0; - allUvs[(base + v) * 2 + 1] = ribbon.trimUvs?.[v * 2 + 1] ?? 0; - if (v % 3 === 2) indices.push(base + v - 2, base + v - 1, base + v); - } - } const geometry = new THREE.BufferGeometry(); - geometry.setAttribute("position", new THREE.BufferAttribute(allPositions, 3)); - geometry.setAttribute("uv", new THREE.BufferAttribute(allUvs, 2)); + geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2)); geometry.setIndex(indices); geometry.computeVertexNormals(); return geometry; @@ -408,81 +347,6 @@ export function createCorridorGroup(build: CorridorBuildResult, bounds: ModelBou group.add(line); } }); - // 탑뷰 투영 커브(2026-08-26 사용자) — 구조물 세트마다 **그 세트 구조물 상단 +5m** - // 평면에 눕힌 닫힌 고리다. 종류는 색(구조물 노랑 / 변형 성토선 분홍 / 원본 하늘), - // 유입·유출은 실선·파선으로 나눈다. 표기 전용 — 서피스는 안 건드린다. - // - // **최종 결과물에서는 숨긴다**(2026-09-02 사용자). 지우지는 않는다 — 절취·패치 기하를 - // 다시 들여다볼 때 쓰므로 묶음째 감춰 두고 `__corridorPlanCurves(true)` 로 되켠다. - const planGroup = new THREE.Group(); - planGroup.name = PLAN_CURVE_GROUP; - planGroup.visible = planCurvesVisible(); - group.add(planGroup); - (build.planCurves ?? []).forEach((curve) => { - // 벽 자리 커브는 표시용이 아니다 — 벽 서피스로만 나간다(2026-08-27 사용자). - if (curve.source === "cut-wall") return; - const color = PLAN_CURVE_COLORS[curve.source]; - const dashed = curve.role === "outlet"; - curve.loops.forEach((loop, index) => { - if (loop.length < 2) return; - const positions: number[] = []; - // 점의 z를 그대로 쓴다 — 평면 커브는 z에 planZ가 들어 있어 동작이 같고, - // 투영선(`slope-projected`)만 점마다 절·성토면 표고를 들고 온다(2026-08-26). - for (const [x, y, z] of loop) { - positions.push(x - origin.cx, z - origin.cz, -(y - origin.cy)); - } - const geometry = new THREE.BufferGeometry().setAttribute( - "position", - new THREE.Float32BufferAttribute(positions, 3), - ); - const line = new THREE.Line( - geometry, - dashed - ? new THREE.LineDashedMaterial({ color, ...PLAN_DASH_M }) - : new THREE.LineBasicMaterial({ color }), - ); - if (dashed) line.computeLineDistances(); - const station = curve.setChainageM.toFixed(2); - line.name = `corridor-plan:${curve.source}:${curve.role}:${curve.side}:${station}:${index}`; - planGroup.add(line); - }); - }); - // 절취 측벽 — 리본과 분리된 **별도 서피스**(2026-08-27 사용자). 성토면과 같은 색·빗금이라 - // 한 몸으로 보이지만 객체가 달라 다음 단계(바닥면·트림)에서 따로 다룰 수 있다. - (build.cutWalls ?? []).forEach((wall, index) => { - const count = wall.positions.length / 3; - if (!count) return; - const positions = new Float32Array(count * 3); - for (let v = 0; v < count; v += 1) { - positions[v * 3] = wall.positions[v * 3] - origin.cx; - positions[v * 3 + 1] = wall.positions[v * 3 + 2] - origin.cz; - positions[v * 3 + 2] = -(wall.positions[v * 3 + 1] - origin.cy); - } - const geometry = new THREE.BufferGeometry(); - geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); - if (wall.uvs && wall.uvs.length === count * 2) { - geometry.setAttribute("uv", new THREE.BufferAttribute(wall.uvs, 2)); - } - geometry.computeVertexNormals(); - const mesh = new THREE.Mesh( - geometry, - new THREE.MeshLambertMaterial({ - // 절취 측벽은 **일반 서피스**(빗금 없음) — 벽은 방향성을 표현할 면이 아니다 - // (2026-08-27 사용자). 패치 스커트는 성토면의 연장이라 **빗금 있는 메시 - // 서피스**로 그린다(2026-08-27 사용자) — UV를 싣고 온 판만 해당된다. - color: KIND_COLORS.fill, - map: wall.uvs ? hatchTexture(HATCH_DIRECTION.fill ?? -1) : null, - side: THREE.DoubleSide, - polygonOffset: true, - polygonOffsetFactor: POLYGON_OFFSET_FACTOR, - polygonOffsetUnits: POLYGON_OFFSET_FACTOR, - }), - ); - mesh.name = wall.patchSkirt - ? `corridor-patch-skirt:${wall.side}:${index}` - : `corridor-cut-wall:${wall.side}:${index}`; - group.add(mesh); - }); build.caps.forEach((cap, index) => { const geometry = capGeometry(cap, origin); if (!geometry) return; diff --git a/B05_Profile/B05_Profile_UI_Corridor_Tin.ts b/B05_Profile/B05_Profile_UI_Corridor_Tin.ts index 98e73c6f8..b48e6fd37 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Tin.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Tin.ts @@ -30,7 +30,7 @@ import { TriGrid, } from "./B05_Profile_UI_Corridor_Tin_Pslg"; -/** 절토 · 성토 색 — 새 방식(`_Deform_Terrain.ts`) · 리본 색과 같은 값. */ +/** 절토 · 성토 색 — 리본 색과 같은 값. */ const CUT_COLOR = new THREE.Color(0xb5179e); const FILL_COLOR = new THREE.Color(0x06b6d4); /** 원지반 색을 못 찾은 점(지형 밖) — 회색. */ diff --git a/B05_Profile/B05_Profile_UI_Viewer_Corridor.ts b/B05_Profile/B05_Profile_UI_Viewer_Corridor.ts index 1ddec6c09..7f5f2e899 100644 --- a/B05_Profile/B05_Profile_UI_Viewer_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Viewer_Corridor.ts @@ -11,11 +11,7 @@ import * as THREE from "three"; import type { ModelBounds } from "./B05_Profile_UI_Markers"; import type { CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build"; -import { - createCorridorGroup, - PLAN_CURVE_FLAG, - PLAN_CURVE_GROUP, -} from "./B05_Profile_UI_Corridor_Mesh"; +import { createCorridorGroup } from "./B05_Profile_UI_Corridor_Mesh"; import { setCorridorBuildSummary } from "./B05_Profile_UI_Viewer_Debug"; import type { TerrainHeightIndex } from "./B05_Profile_UI_Corridor_Terrain"; import { BAND_MARGIN_M, TerrainBandSplit, type SceneBox } from "./B05_Profile_UI_Corridor_Split"; @@ -101,8 +97,6 @@ function roadOnly(build: CorridorBuildResult): CorridorBuildResult { return { ...build, ribbons: build.ribbons.filter((ribbon) => ribbon.kind !== "cut" && ribbon.kind !== "fill"), - planCurves: [], - cutWalls: [], }; } @@ -238,24 +232,6 @@ export function createViewerCorridor(deps: ViewerCorridorDeps): ViewerCorridor { scene.add(corridorGroup); deps.onGroup(); } - // 평면 스케치 되켜기 — 최종 결과물에서는 숨기지만 절취·패치 기하를 다시 볼 때 쓴다 - // (2026-09-02 사용자: "나중에 디버깅을 위해 재사용 가능성 있음"). - // `__corridorPlanCurves(true)` 로 켜고 `(false)` 로 끈다. 선택은 이 브라우저에 남아 - // 다음에 열 때도 그대로다. 인자 없이 부르면 지금 상태를 돌려준다. - ( - window as unknown as { __corridorPlanCurves?: (on?: boolean) => boolean } - ).__corridorPlanCurves = (on?: boolean): boolean => { - const sketch = corridorGroup?.getObjectByName(PLAN_CURVE_GROUP); - if (on !== undefined) { - try { - window.localStorage.setItem(PLAN_CURVE_FLAG, on ? "1" : "0"); - } catch { - // 저장소가 막힌 환경 — 이번 화면에만 적용한다. - } - if (sketch) sketch.visible = on; - } - return sketch?.visible ?? false; - }; scheduleTerrainClip(); } diff --git a/B05_Profile/B05_Profile_UI_Viewer_Debug.ts b/B05_Profile/B05_Profile_UI_Viewer_Debug.ts index b5964869a..127163ad8 100644 --- a/B05_Profile/B05_Profile_UI_Viewer_Debug.ts +++ b/B05_Profile/B05_Profile_UI_Viewer_Debug.ts @@ -35,21 +35,7 @@ export function setCorridorBuildSummary(build: CorridorBuildResult | null): void rings: solid.rings?.length ?? 0, points: solid.polygon?.length ?? 0, })), - raw: { - ribbons: build.ribbons, - planCurves: build.planCurves ?? [], - cutWalls: build.cutWalls ?? [], - }, - planCurves: (build.planCurves ?? []).map((curve) => ({ - source: curve.source, - role: curve.role, - side: curve.side, - at: curve.setChainageM, - loops: curve.loops.length, - points: curve.loops.reduce((sum, loop) => sum + loop.length, 0), - z0: curve.loops[0]?.[0]?.[2] ?? null, - planZ: curve.planZ, - })), + raw: { ribbons: build.ribbons }, } : null; } diff --git a/B05_Profile/Old/B05_Profile_UI_Corridor_Build_Types.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Build_Types.ts new file mode 100644 index 000000000..9dcf752d7 --- /dev/null +++ b/B05_Profile/Old/B05_Profile_UI_Corridor_Build_Types.ts @@ -0,0 +1,102 @@ +/* ============================================================================= + * B05_Profile_UI_Corridor_Build_Types.ts + * 코리도 빌더가 내는 **결과 자료형** — 리본·절취 측벽·캡·빌드 결과. + * + * `B05_Profile_UI_Corridor_Build` 에서 떼어냈다(700줄 제한, 2026-09-02). 계산은 전혀 + * 없고 모양과 그 뜻(주석)만 있다. 빌더가 그대로 다시 내보내므로 기존 import 경로 + * (`... from "./B05_Profile_UI_Corridor_Build"`)는 전부 유효하다. + * ========================================================================== */ + +import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; +import type { CorridorKind, CorridorSide } from "./B05_Profile_UI_Corridor_Station"; +import type { CorridorStructure } from "./B05_Profile_UI_Corridor_Structures"; +import type { PlanCurve } from "./B05_Profile_UI_Corridor_Plan"; + +export interface CorridorRibbon { + kind: CorridorKind; + side: CorridorSide; + colCount: number; + chainages: number[]; + /** + * 구조물 **패치 리본**(2026-08-25) — 투영커브로 잘린 자리에 다시 그리는 변형 성토면. + * 절단 대상에서 빠진다(패치까지 자르면 구멍이 도로 뚫린다). + */ + patch?: boolean; + /** + * 패치를 **절취 영역 안쪽으로 클립**한다(2026-08-27 사용자: 삐져나오지도 벌어지지도 + * 않게). `maskFillByPlanCurves`가 기본 성토면을 지우는 규칙의 **정확한 반대**를 건다 — + * 영역 밖 셀은 지우고, 걸친 셀은 안쪽 조각만 남긴다. 두 면이 같은 경계를 공유하니 + * 겹침도 구멍도 안 생긴다. **세월교 전용** — 구조물별로 로직이 다르다. + */ + patchClip?: boolean; + /** + * rowCount × colCount × 3 (모델 좌표). + * + * **Float64**다 — 모델 좌표는 EPSG 절대값(십만 m대)이라 Float32에 담으면 해상도가 + * 0.14m까지 떨어져, 0.2m로 세분한 점들이 양자화 계단을 타며 최대 35°짜리 지그재그가 + * 생겼다(2026-08-23 실측). 화면에 올릴 때 원점을 빼면서 Float32로 줄인다. + */ + positions: Float64Array; + /** + * 셀 마스크 — `(rowCount-1) × (colCount-1)`, 1이면 **그 셀은 안 그린다** + * (2026-08-26 투영 커브 안쪽 지우기). 정점은 그대로 두고 삼각형만 뺀다. + */ + cellMask?: Uint8Array; + /** + * 축 고정 커브(BOX암거)가 **걸친 셀**을 커브에서 잘라 되살린 조각 삼각형 + * (모델 좌표, 삼각형당 9). 격자로는 못 담는 비스듬한 경계를 여기로 메운다 + * (2026-08-26 사용자: 이원화하여 관리). + */ + trimTris?: Float64Array; + /** `trimTris`와 짝인 UV(삼각형당 6) — 빗금이 실척으로 이어지게 한다. */ + trimUvs?: Float32Array; + /** + * 격자점별 **원지반 표고**(rowCount × colCount) — 성토 리본에만 싣는다. + * 잘린 자리 측벽을 성토면에서 원지반까지 세우는 밑선이다(2026-08-27 사용자). + */ + groundZ?: Float64Array; +} + +/** + * 잘린 자리 **측벽 서피스**(2026-08-27 사용자) — 리본 메시에 섞지 않고 구조물처럼 + * 따로 그린다. 삼각형 수프(정점당 3, 삼각형당 9). 빗금이 없어 UV는 담지 않는다. + */ +export interface CorridorCutWall { + side: "left" | "right"; + positions: Float64Array; + /** 패치 바깥 끝 스커트인가(2026-08-27) — 뷰어가 지형 색인으로 뒤늦게 붙이므로, + * 다시 붙일 때 옛 것을 걷어내려면 표식이 있어야 한다(`_Corridor_Skirt.ts`). */ + patchSkirt?: boolean; + /** + * 정점별 UV(정점당 2) — **빗금 있는 성토 서피스**로 그릴 때만 싣는다. + * 절취 측벽은 일반 서피스(빗금 없음)이고, 패치 스커트는 성토면의 연장이라 빗금을 + * 넣는다(2026-08-27 사용자: "변형 성토면 하단 끝단부 서피스는 메시 형태"). + */ + uvs?: Float32Array; +} + +/** 시·종점 마구리(캡) — 설계선과 지반선 사이를 세로로 봉인하는 스트립(모델 좌표). */ +export interface CorridorCap { + /** [x, y, z설계, z지반] × N — offset 오름차순. */ + points: Array<[number, number, number, number]>; +} + +export interface CorridorBuildResult { + ribbons: CorridorRibbon[]; + /** 클리핑 경계 — 프레임별 좌/우 최외곽(catch) XY(모델). 좌우 같은 길이. */ + outline: { chainages: number[]; left: Array<[number, number]>; right: Array<[number, number]> }; + /** 노선 시·종점 단면 봉인(2026-08-23 품질 개선). 캡이 없으면 빈 배열. */ + caps: CorridorCap[]; + /** 배수관 세트 구조물(기슭막이·집수정·배관) 솔리드(2026-08-23). 없으면 빈 배열. */ + structures: CorridorStructure[]; + /** + * 구조물 세트별 **탑뷰 투영 커브**(2026-08-26 사용자) — 세트 구조물 상단 5m + * 평면에 눕힌 닫힌 고리들. 구조물·비탈(변형/원본)을 유입·유출로 나눠 담는다. + * 저장 코리도엔 없을 수 있다. + */ + planCurves?: PlanCurve[]; + /** 절취 측벽 서피스(2026-08-27) — 저장본에도 담는다. 없으면 빈 배열. */ + cutWalls?: CorridorCutWall[]; + /** 측점 단면 — 새 방식 지형 변형이 비탈 단면을 읽는다(115-1). 메모리만 · 저장본에 안 담는다. */ + sections?: CrossSection[]; +} diff --git a/B05_Profile/B05_Profile_UI_Corridor_Carve.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Carve.ts similarity index 100% rename from B05_Profile/B05_Profile_UI_Corridor_Carve.ts rename to B05_Profile/Old/B05_Profile_UI_Corridor_Carve.ts diff --git a/B05_Profile/B05_Profile_UI_Corridor_Carve_Wing.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Carve_Wing.ts similarity index 100% rename from B05_Profile/B05_Profile_UI_Corridor_Carve_Wing.ts rename to B05_Profile/Old/B05_Profile_UI_Corridor_Carve_Wing.ts diff --git a/B05_Profile/B05_Profile_UI_Corridor_Cut.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Cut.ts similarity index 100% rename from B05_Profile/B05_Profile_UI_Corridor_Cut.ts rename to B05_Profile/Old/B05_Profile_UI_Corridor_Cut.ts diff --git a/B05_Profile/Old/B05_Profile_UI_Corridor_Envelope.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Envelope.ts new file mode 100644 index 000000000..960302b92 --- /dev/null +++ b/B05_Profile/Old/B05_Profile_UI_Corridor_Envelope.ts @@ -0,0 +1,331 @@ +/* ============================================================================= + * B05_Profile_UI_Corridor_Envelope.ts + * 코리도 저장본 형식 — 버전·해시·직렬화 한 벌. **브라우저와 서버가 같은 것을 쓴다.** + * + * 서버(전처리 체인 끝)가 같은 빌더로 코리도를 미리 만들어 저장하므로(2026-09-04 사용자 + * 확정), 저장 형식과 버전 해시가 두 벌이 되면 브라우저가 서버 저장본을 남으로 보고 + * 통째로 다시 만든다. 그래서 이 파일 하나만 둔다 — 화면 API 를 쓰지 않아 Node 에서도 + * 그대로 돈다(`btoa`·`atob` 는 Node 16+ 전역). + * + * 원래 `_UI_Corridor.ts` 안에 있던 것을 그대로 옮겼다(2026-09-04 분리, 내용 불변). + * ========================================================================== */ + +import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch"; +import type { RoutePoint } from "./B05_Profile_Api_Fetch"; +import type { CorridorBuildResult, CorridorRibbon } from "./B05_Profile_UI_Corridor_Build_Types"; +import { structureHashParts } from "./B05_Profile_UI_Corridor_Structures"; + +/** + * 저장본 **형식(스키마)** 버전 — 담는 필드가 바뀔 때만 올린다. + * + * 기하 계산이 바뀐 것은 여기가 아니라 아래 `BUILD_VERSION`으로 만료시킨다. + * + * 3 = 리본에 셀 마스크·축 고정 커브 조각 삼각형 추가 — 2026-08-26. + * 4 = 절취 측벽 서피스(`cutWalls`) 추가 — 2026-08-27. + * 5 = 측벽에서 빗금(UV)을 뺀다 — 벽은 방향성을 표현하지 않는다(2026-08-27 사용자). + * 6 = 리본의 `patch`·`patchClip` 표식을 담는다 — 안 담아서 저장본으로 다시 연 코리도가 + * 패치인 줄 모르고 지형 스냅에 끌려갔다(2026-09-02). + */ +export const ENVELOPE_VERSION = 6; + +export interface CorridorEnvelope { + version: number; + hash: string; + ribbons: Array<{ + kind: CorridorRibbon["kind"]; + side: CorridorRibbon["side"]; + colCount: number; + chainages: number[]; + positionsBase64: string; + /** + * 구조물 패치 리본 표식(2026-09-02) — 빌드 때만 쓰는 값이 아니라 **뷰어가 그릴 때도 + * 본다**(`B05_Profile_UI_Viewer.ts` 지형 스냅 제외). 안 담으면 저장본으로 다시 연 + * 코리도가 패치인 줄 몰라 바깥 끝이 원지반까지 끌려 내려간다. + */ + patch?: boolean; + patchClip?: boolean; + /** 셀 마스크(2026-08-26 투영 커브 안쪽 지우기) — 없으면 구멍 없는 리본. */ + cellMaskBase64?: string; + /** 축 고정 커브가 걸친 셀의 조각 삼각형·UV(2026-08-26). */ + trimTrisBase64?: string; + trimUvsBase64?: string; + }>; + outline: CorridorBuildResult["outline"]; + /** 시·종점 마구리(구 저장본엔 없음 — 없으면 빈 배열로 복원). */ + caps?: CorridorBuildResult["caps"]; + /** 배수관 세트 구조물(2026-08-23 — 구 저장본엔 없음, 없으면 빈 배열). */ + structures?: CorridorBuildResult["structures"]; + /** + * 구조물 세트별 탑뷰 투영 커브(2026-08-26) — 저장본에도 담는다. 안 담으면 + * 저장본으로 복원한 코리도에서는 커브가 통째로 사라진다. + */ + planCurves?: CorridorBuildResult["planCurves"]; + /** 절취 측벽 서피스(2026-08-27) — 구 저장본엔 없다. 없으면 빈 배열로 복원. */ + cutWalls?: Array<{ + side: "left" | "right"; + positionsBase64: string; + }>; +} + +/** FNV-1a 32bit — 설계 입력 요약 문자열의 버전 해시. */ +function fnv1a(text: string): string { + let hash = 0x811c9dc5; + for (let i = 0; i < text.length; i += 1) { + hash ^= text.charCodeAt(i); + hash = Math.imul(hash, 0x01000193); + } + return (hash >>> 0).toString(16).padStart(8, "0"); +} + +/** + * 빌드 로직 판번호 — **기하를 바꾸는 수정을 하면 반드시 올린다.** + * + * 해시는 종횡단 입력만 요약하므로, 빌드 코드를 고쳐도 입력이 그대로면 해시가 같아 + * 옛 저장본을 그대로 다시 쓴다 — 코드를 고쳤는데 화면이 안 바뀌는 일이 생긴다 + * (2026-08-23 사용자 보고: "원복이 안 된 것 같다가 갑자기 반영됨"). 판번호를 해시에 + * 섞어 두면 배포와 동시에 저장본이 만료된다. + */ +// 42 = 탑뷰 투영선(빨간 선)·구조물 구간 경계 행·파란 스케치 안쪽 기준 정정· +// 투영 커브 안쪽 성토면 절단 — 커브 다각형 기준(2026-08-26). +// ※ 40에 머물러 있는 동안 기하 수정 3회가 통째로 묻혔다 — 저장본이 그대로 +// 복원되어 화면·데이터가 하나도 안 바뀌었다. 기하를 고치면 **반드시** 올릴 것. +// 56 = 측벽에서 **노견 쪽·성토 끝단 쪽 리본 경계 벽을 뺀다** — 거기서 잘린 게 +// 아니라 리본이 끝난 자리다(2026-08-27 사용자). +// 64 = 변형 성토면(패치) 마감 3종 — ① 행별 지반 절단(빨간 교차선에서 끝냄) + +// 바깥 끝 수직 스커트, ② 날개 사다리꼴 몫만큼 패치 행 범위 확장(BOX는 축 고정), +// ③ 구조물 안착 자리 원지반 재노출(원본 성토 스케치 − 구조물 ∪ 변형 성토) +// (2026-08-27 사용자). +// 65 = 날개벽 평면 스케치까지 변형 성토면에 포함 — 늘린 행에 벽 트림을 걸지 않는다 +// (2026-08-27 사용자). +// 66 = 패치 행을 그 행 지반선과 직접 교차시켜 **자르고 연장**한다 — 날개 사다리꼴 +// 바깥변(원본 성토 끝선)까지 성토면이 이어진다(2026-08-27 사용자). +// 67 = (주석만 앞서 나갔던 번호 — 실제로 들어간 것은 66까지다. 2026-08-27 확인) +// 68 = 패치 스윕 범위를 **주황 날개 평면 스케치(`wing-box`)까지** 넓힌다 — 행별 지반 +// 핀(절단·연장)과 스커트는 그대로 두고, 스케치가 더 바깥일 때만 그 행을 설계 +// 성토 물매로 스케치 경계까지 덧이어 낸다(2026-08-27 사용자: 그 자리에 변형 +// 성토면이 있어야 한다 — 삭제가 아니다). +// 69 = 날개 스케치 확장에서 **BOX암거는 제외**한다 — 날개벽은 세월교·BOX 두 곳에서 +// 쓰이지만 로직이 분리돼 있고, BOX는 이미 채워져 있어(구멍 5~12%) 손대면 행 폭이 +// 0.74 → 4.53m로 뛴다(2026-08-27 사용자: "박스 암거는 로직 원복"). +// 70 = 세월교 날개 사다리꼴 A안 — ① 종방향 범위를 **노선 실투영**으로 잰다 +// (`wingSpanOnRoute`, 직선 투영은 곡선+먼 편거리에서 3m 어긋났다), +// ② 행 편거리를 스케치의 **안쪽·바깥쪽 끝**까지 늘린다(`wingRangeAt`, +// 좌측 고리는 패치보다 1.5m 안쪽에 있었다). BOX암거는 둘 다 제외 — 종전 그대로. +// 71 = 안쪽 확장을 **세월교 바닥판 상단 Z(`slabTopElevation`)로 트림**한다 — 성토 +// 물매 역연장이 유로 위로 솟아 바닥 상단과 안 만나는 자리로 튀었다(2026-08-27 사용자). +// 72 = 날개 스윕 범위를 **날개벽 끝점 횡단선**까지로 바꾼다 — 고리 전체가 아니라 +// 날개벽 선 두 끝을 노선에 실투영해 재고, 구조물 점유 구간 ±5m 여유 안에서 +// 트림한다. 고리와 안 만나는 중간 행도 더는 끊지 않는다(2026-08-27 사용자: +// "5m 늘려 서피스 만들고 날개벽 끝점 횡단선으로 트림"). +// 73 = ① 패치 점 중 **세월교 바닥판 상단 Z를 넘는 것은 지운다**(`clipRunBelow`, +// 종단 shift 반영·천장 교차점 삽입), ② 날개 스팬을 다시 **고리 전체 점**으로 +// 잰다 — 벽 끝점만 쓰면 좌측이 143.2~157.3 → 144.4~155.9로 줄었다 +// (2026-08-27 사용자: "좌측 스윕 더 가야 함"). +// 74 = (날개 몫 행 트림 시도 — 사용자 지시로 **원복**. 안쪽까지 자르면 구조물 구간과 +// 날개 사이가 벌어졌고, 바깥선만 자르는 판은 버전을 안 올려 화면에 못 올라갔다.) +// 75 = 73 상태로 원복 — 트림 없음. 날개 몫 행은 지반 핀 + 스케치 확장 + 바닥 상단 +// Z 초과 삭제까지만 한다(2026-08-27 사용자: "2번 전으로"). +// (76~78의 트림 도구 `wingOuterLines`·`wingTrimAt`·`clipRunToRange` 는 2026-09-02에 +// 제거했다 — 원복 뒤 다시 배선되지 않아 참조가 0이었다. 아래는 이력으로만 남긴다.) +// 76 = 날개 몫 행을 **주황 고리의 바깥선**에서 끝낸다(`wingOuterLines`+`wingTrimAt`). +// 고리가 그 행까지 모자라면 **끝 마디를 복사해 20m 연장**해서 만나게 한다 +// (2026-08-27 사용자). 안쪽은 안 자른다. +// 77 = 주황 바깥선 트림을 **모든 행**에 건다 — 구조물 점유 구간 안 행을 빼놨더니 +// 그 구간이 커브 밖으로 삐져나왔다. 자르는 게 아니라 커브를 새 바깥 끝으로 +// 삼아 줄을 다시 잡고 `resample`이 열을 그 위에 다시 깔아 **메시가 재배치**된다 +// (2026-08-27 사용자: "메시의 재배치가 필요하잖아, 그래야 잘리지"). +// 78 = 날개 몫 로직을 **구조물 종류로 가른다**(2026-08-27 사용자: "세월교, 박스암거, +// 기슭막이") — `section.ford`일 때만 실투영 스팬·스케치 확장·바깥선 트림을 태우고 +// BOX암거·기슭막이는 종전 경로 그대로. 트림선은 **날개 하나 단위**로 나눠 행마다 +// 누가거리가 가장 가까운 날개 것만 쓴다(20m 연장선이 서로 먹던 것을 막는다). +// 79 = 세월교 패치를 **절취 영역 셀 마스크의 반대 규칙**으로 클립한다 — 영역 밖 셀은 +// 지우고 걸친 셀은 안쪽 조각만 남긴다(`patchClip`). 행 단위 바깥선 트림은 폐기 — +// 비스듬한 사다리꼴 경계를 행 끝 하나로는 못 맞춰 74셀이 삐져나왔다(실측). +// 지우는 면과 채우는 면이 같은 경계를 공유한다(2026-08-27 사용자). +// 80 = 배수관 세트(기슭막이) **노견 → 벽 이음선 상단 성토 사면**을 되메움 리본 배열 +// (`fillRuns`)에도 담는다 — 78/79의 포켓은 노견까지 파는데 79까지의 배열에는 +// 벽 아래 다단·집수정 성토선만 있어 **기슭막이 상단부가 통째로 비었다** +// (2026-08-27 사용자). 새 면이 아니라 실루엣이 이미 쓰던 `designTrim` 사면선 +// 그대로다. 벽 루프·집수정 분기가 같은 선을 담으므로 측당 한 번만 등록한다. +// 81 = `slope-deformed` 커브를 **패치 리본과 같은 규칙**으로 끝낸다 — 실루엣 바깥 +// 끝이 그 측점 지반에 안 닿으면(BOX암거: 구체 최상단) 외삽하지 않고 실루엣이 +// 끝나는 자리를 바깥 끝으로 쓴다. 종전에는 이 커브만 무조건 최대 30m 외삽해 +// 성토 끝선까지 갔고, BOX는 기본값에서 변형 성토선이 원본 성토선과 정확히 +// collinear가 되어 `원본 − (구조물 ∪ 변형)` = ∅ — **구체 밖 원지반 재노출이 +// 통째로 미실행**이었다(2026-08-27 사용자 확정: "구체 밖은 원지반이어야 한다"). +// 실루엣이 구조물에서 끝나는지는 기하로 재지 않고 `endsAtStructure` 표식으로 +// 가른다 — 낙차로 재면 기슭막이 유출측처럼 지반보다 바깥으로 나간 실루엣까지 +// 걸려 원래 짧게 잘리던 것이 안 잘린다(실측 84.3 우측 5.10 → 5.69m). +// 아울러 BOX 날개 사다리꼴(`wing-box`)을 원지반 재노출 후보에 넣는다 — 절취 +// 소스에는 있고 되살릴 목록에는 없어 그 자리가 성토면도 지반도 없는 구멍이었다 +// (2026-08-27 사용자: "날개부의 평면 스케치 영역까지 원지반이 같이 노출"). +// 세월교는 제외 — 그 자리는 되메움 패치가 덮는다(`wingRangeAt`, isFord 전용). +// 82 = BOX암거 `structure` 탑뷰 커브를 부재별 7고리가 아니라 **아령 외곽선 한 줄**로 +// 낸다(2026-08-27 사용자: "바닥을 위에서 보면 아령형상이고 제일 외곽에 있는 선 +// 성분을 평면 투영하면 경계"). 상판+에이프런에 `planOutline` 표식을 달고 +// `_Plan_Footprint.ts`가 링 자리마다 편거리 창을 합친다 — 기하 불리언 없음 +// (BOX 부재는 전부 같은 고정축 위 평행 링이다). 종전 부재별 고리에는 날개벽이 +// **패널 두께 슬리버**로 섞여 있었고(실측 0.40㎡ / bbox 1.69×1.40 ×2), 그 슬리버가 +// "덮인 자리"로 빠져 그 자리만 성토면도 지형도 없는 검은 줄이 됐다. 에이프런은 +// 날개벽 발치를 얹으려고 넓혀 둔 부재라 외곽선이 날개를 이미 품는다. +// 83 = 원지반 재노출 영역을 **`cut-merged`**(성토면에서 실제로 도려낸 셀의 윤곽)로 +// 바꾼다. 종전에는 `원본 성토 스케치 − (구조물 ∪ 변형 성토 스케치)`로 영역을 +// **다시 조립**해, 지우는 쪽과 되살리는 쪽이 다른 커브를 쓰며 경계가 어긋났다 +// (날개벽 패널 두께 슬리버 자리에 검은 줄). 이제 지우는 쪽이 남긴 자국을 그대로 +// 쓰므로 **원본 지형 메시가 그 자리에 그대로 남는다** — 새 서피스를 만들지 않는다 +// (2026-08-27 사용자: "원지반 오리지널 데이터에서 가져오라니까"). +// 84 = 잘린 자리 윤곽(`cut-merged`) 추적을 고친다 — 갈림목에서 아무 변이나 집어 +// walk가 남의 고리로 새 나가고, 막다른 길에서 끊긴 **열린 사슬이 고리로 담겼다**. +// 표시용일 땐 안 보였지만 이제 이 커브가 되살릴 영역의 근거라 그대로 구멍이 된다 +// (실측: 도려낸 셀의 18% — 성토 끝단 쪽 col 40~46 — 이 영역에서 빠졌다. +// 2026-08-27 사용자: "유출구에 일부 성토부에 터짐 있음"). 오른손 규칙으로 갈림목을 +// 가르고 **닫힌 고리만** 담는다. +// 85 = 물넘이포장 자리 노면을 판다 — 횡단도와 같은 산식(fordDeckElevationAt)으로 노면·노견 +// 조각 표고를 내린다. 노면 밖 비탈은 그대로라 노견 끝에 수직 단차가 선다(2026-08-28 사용자). +// 86 = 독립 기슭막이(구조물 정본 D군)를 3D에 세운다 — 횡단 카드와 같은 폴리곤 +// (computeRevetmentLayout)을 기준측점 전/후 길이만큼 스윕한다(2026-08-28 사용자). +// 87 = 독립 기슭막이 **다단** — 단마다 솔리드를 세운다. 단 사이 성토는 1:1.2, +// 다음 단 시작점은 윗단 하단 수평선 +근입과 전면 경사선의 교차점(2026-08-22 규칙 승계). +// 88 = 독립 기슭막이 기준을 **맨 위 단**으로 되돌리고 추가 단은 아래로 붙인다(배관 세트와 +// 같은 규칙). 기준 자리는 사용자 이동값(기준 올림·좌우)을 탄다(2026-08-28 사용자). +// 89 = BOX암거 패치도 **절취 영역 안쪽으로 자른다**(2026-09-02). 그 규칙이 세월교에만 +// 걸려 있어 BOX 패치는 한 번도 안 잘리고 영역 밖으로 넘쳤다 — 실측에서 BOX 패치 +// 리본 두 벌(198.4~203.5m)에 셀 마스크가 없었다. BOX도 `wing-box` 커브로 절취 +// 영역을 내므로 같은 반대 규칙이 성립한다. +// 90 = 잘린 자리 윤곽(`cut-merged`)의 **시작 모서리 유실**을 고친다 — `dropCollinear`가 +// 고리의 닫힘 중복점(머리=꼬리)을 순환 이웃으로 셈해 머리 모서리를 지우고, 그 양옆 +// 일직선 점까지 빠져 첫 행이 대각선으로 잘렸다(실측: 8고리 전부, 63셀 구멍 — +// 우 197.6~198.2m BOX 앞 쐐기 33셀이 가장 큼). 되살릴 영역이 이 커브라 저장본을 만료한다. +// 91 = 구조물 솔리드에 **부재키**(`key`)를 싣는다 — 3D 개별 선택이 그 표로 부재를 +// 가려낸다(2026-09-04). 옛 저장본엔 키가 없어 3D에서 골라도 B06 조정창으로 넘길 +// 부재를 못 정하므로 만료한다. +// 92 = 세월교·BOX암거 부재까지 **부재키를 일원화**한다 — 횡단도가 부르는 이름 그대로 +// (세월교 유입·유출, BOX 좌·우, 본체는 `body`). 91 저장본엔 배수관 세트 키만 있다. +// 93 = 측점 사이 행 자리를 **노선 길이 직선 보간**으로(115-5 P0-2) — 균일 Catmull-Rom 이 +// 행을 최대 7.2 m 미끄러뜨리고 되접었다. 모든 행 자리가 바뀌어 저장본을 만료한다. +const BUILD_VERSION = 93; + +/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ +export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { + const parts: Array = [`v${BUILD_VERSION}`, routePoints.length]; + routePoints.forEach((p) => parts.push(p.x.toFixed(2), p.y.toFixed(2))); + detail.cross_sections.forEach((section) => { + const design = section.design; + parts.push( + section.chainage_m.toFixed(3), + section.center_x.toFixed(2), + section.center_y.toFixed(2), + ); + if (!design) { + parts.push("nodesign"); + return; + } + parts.push( + design.design_elevation_m.toFixed(3), + design.section_mode, + design.ditch_side ?? "", + design.ditch_type ?? "", + String(design.ditch_enabled ?? ""), + design.design_line.length, + ); + design.design_line.forEach((point) => + parts.push(point.offset_m.toFixed(3), point.elevation_m.toFixed(3)), + ); + // 구조물(배수관 세트) 입력 — 길이·전후·형식이 바뀌면 재빌드되어야 한다. + structureHashParts(section).forEach((part) => parts.push(part)); + }); + return fnv1a(parts.join("|")); +} + +function base64FromFloats(values: Float64Array): string { + const bytes = new Uint8Array(values.buffer, values.byteOffset, values.byteLength); + let binary = ""; + const CHUNK = 0x8000; + for (let i = 0; i < bytes.length; i += CHUNK) { + binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK)); + } + return btoa(binary); +} + +function base64FromBytes(bytes: Uint8Array): string { + let binary = ""; + const CHUNK = 0x8000; + for (let i = 0; i < bytes.length; i += CHUNK) { + binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK)); + } + return btoa(binary); +} + +function bytesFromBase64(encoded: string): Uint8Array { + const binary = atob(encoded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i); + return bytes; +} + +function floatsFromBase64(encoded: string): Float64Array { + const binary = atob(encoded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i); + return new Float64Array(bytes.buffer); +} + +export function serialize(build: CorridorBuildResult, hash: string): CorridorEnvelope { + return { + version: ENVELOPE_VERSION, + hash, + ribbons: build.ribbons.map((ribbon) => ({ + kind: ribbon.kind, + side: ribbon.side, + colCount: ribbon.colCount, + chainages: ribbon.chainages, + positionsBase64: base64FromFloats(ribbon.positions), + ...(ribbon.patch ? { patch: true } : {}), + ...(ribbon.patchClip ? { patchClip: true } : {}), + ...(ribbon.cellMask ? { cellMaskBase64: base64FromBytes(ribbon.cellMask) } : {}), + ...(ribbon.trimTris && ribbon.trimUvs + ? { + trimTrisBase64: base64FromFloats(ribbon.trimTris), + trimUvsBase64: base64FromBytes(new Uint8Array(ribbon.trimUvs.buffer.slice(0))), + } + : {}), + })), + outline: build.outline, + caps: build.caps, + structures: build.structures, + planCurves: build.planCurves, + cutWalls: (build.cutWalls ?? []).map((wall) => ({ + side: wall.side, + positionsBase64: base64FromFloats(wall.positions), + })), + }; +} + +export function deserialize(envelope: CorridorEnvelope): CorridorBuildResult { + return { + ribbons: envelope.ribbons.map((ribbon) => ({ + kind: ribbon.kind, + side: ribbon.side, + colCount: ribbon.colCount, + chainages: ribbon.chainages, + positions: floatsFromBase64(ribbon.positionsBase64), + ...(ribbon.patch ? { patch: true } : {}), + ...(ribbon.patchClip ? { patchClip: true } : {}), + ...(ribbon.cellMaskBase64 ? { cellMask: bytesFromBase64(ribbon.cellMaskBase64) } : {}), + ...(ribbon.trimTrisBase64 && ribbon.trimUvsBase64 + ? { + trimTris: floatsFromBase64(ribbon.trimTrisBase64), + trimUvs: new Float32Array(bytesFromBase64(ribbon.trimUvsBase64).buffer), + } + : {}), + })), + outline: envelope.outline, + caps: envelope.caps ?? [], + structures: envelope.structures ?? [], + planCurves: envelope.planCurves ?? [], + cutWalls: (envelope.cutWalls ?? []).map((wall) => ({ + side: wall.side, + positions: floatsFromBase64(wall.positionsBase64), + })), + }; +} diff --git a/B05_Profile/Old/B05_Profile_UI_Corridor_Mesh.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Mesh.ts new file mode 100644 index 000000000..855fd5899 --- /dev/null +++ b/B05_Profile/Old/B05_Profile_UI_Corridor_Mesh.ts @@ -0,0 +1,538 @@ +/* ============================================================================= + * B05_Profile_UI_Corridor_Mesh.ts + * 코리도 리본(모델 좌표 격자) → Three.js 메쉬 그룹. + * + * 종류별 색으로 구분된 멀티 서피스 조합(2026-08-23 계획) — 하나의 서피스가 + * 아니라 차도/노견/측구/절토비탈/성토비탈 리본을 개별 Mesh로 만든다. + * 정점은 뷰어 공통 규약(modelToScene)으로 씬 좌표(Y-up, bounds 중심 원점)로 + * 변환해 담는다. + * + * 품질 4종(2026-08-23 사용자 확정): ① 지형 고도색(주황~녹)과 안 겹치는 색 + * 재설계 ② 종류 경계 윤곽선 ③ 절·성토 빗금(해칭) 텍스처 ④ 시·종점 마구리 봉인. + * ========================================================================== */ + +import * as THREE from "three"; +import type { ModelBounds } from "./B05_Profile_UI_Markers"; +import type { + CorridorBuildResult, + CorridorCap, + CorridorKind, + CorridorRibbon, + CorridorStructure, + PlanCurve, +} from "./B05_Profile_UI_Corridor_Build"; + +/** + * 종류별 색 — 지형은 주황~황~녹 고도 램프라 그 대역을 피한다. + * 도로계는 무채색(회), 측구는 청, 절토는 자주(마젠타 계열), 성토는 청록. + * 절·성토를 적갈/초록으로 두면 지형색에 묻혀 구분이 안 됐다(2026-08-23 보고). + */ +const KIND_COLORS: Record = { + carriageway: 0x3f4451, + shoulder: 0x9aa2b1, + ditch: 0x38bdf8, + cut: 0xb5179e, + fill: 0x06b6d4, +}; + +/** 탑뷰 투영 커브 색(2026-08-26 사용자) — 종류별. 지형·코리도 색과 안 겹친다. */ +const PLAN_CURVE_COLORS: Record = { + structure: 0xffd93d, + "slope-deformed": 0xff2d78, + "slope-original": 0x38bdf8, + // 날개벽 성토끝 박스 — 구조물 노랑과 구분되게 주황(2026-08-26 사용자). + "wing-box": 0xff8c1a, + // 스케치 파란 커브를 절·성토면에 얹은 투영선 — 빨강(2026-08-26 사용자 지정). + "slope-projected": 0xff3b30, + // 실제로 잘린 자리 윤곽(2026-08-27 사용자) — 새 데이텀(+3m)에 얹어 라임으로 낸다. + "cut-merged": 0x84cc16, + // 측벽이 설 자리 — 벽 서피스로만 쓰고 선으로는 안 그린다(아래에서 걸러진다). + "cut-wall": 0x000000, +}; + +/** + * 평면 스케치(투영 커브) 묶음의 이름 — 뷰어가 이 이름으로 찾아 켜고 끈다. + * + * **최종 결과물에서는 숨긴다**(2026-09-02 사용자). 이 커브들은 구조물이 어디를 지우고 + * 어디를 다시 채우는지 눈으로 좇으려고 만든 것이라 설계 산출물이 아니다. 다만 절취·패치 + * 기하를 다시 들여다볼 때 쓸모가 있어 **지우지 않고 감춰만 둔다**. + */ +export const PLAN_CURVE_GROUP = "corridor-plan-sketch"; + +/** 평면 스케치를 켜 둔 브라우저인가 — 기본은 꺼짐. 디버깅용 스위치다. */ +export const PLAN_CURVE_FLAG = "frd_debug_plan_curves"; + +export function planCurvesVisible(): boolean { + try { + return window.localStorage.getItem(PLAN_CURVE_FLAG) === "1"; + } catch { + return false; // 저장소가 막힌 환경(사생활 보호 모드 등) — 꺼진 것으로 본다. + } +} + +/** 유출측 파선 눈금(m) — 유입은 실선이다. */ +const PLAN_DASH_M = { dashSize: 0.6, gapSize: 0.4 }; + +/** 마구리(시·종점 절단면) 색 — 절단면임이 드러나게 어둡게. */ +const CAP_COLOR = 0x2a2f3a; + +/** 종류 경계 윤곽선 색·굵기 — 도면처럼 종류 경계를 또렷하게. */ +const OUTLINE_COLOR = 0x0f172a; + +/** 빗금(해칭) 방향 — 절토는 우상향, 성토는 좌상향으로 방향까지 구분한다. */ +const HATCH_DIRECTION: Partial> = { cut: 1, fill: -1 }; + +/** 지형과의 z-fight 방지용 폴리곤 오프셋 — 코리도가 항상 살짝 앞에 그려진다. */ +const POLYGON_OFFSET_FACTOR = -1; + +/** 빗금 텍스처 캐시 — 리본마다 캔버스를 새로 그리면 편집 반복 시 낭비다. */ +const hatchCache = new Map(); + +/** 대각 빗금 캔버스 텍스처(반복). direction=1 우상향, -1 좌상향. */ +function hatchTexture(direction: 1 | -1): THREE.Texture { + const cached = hatchCache.get(direction); + if (cached) return cached; + const size = 32; + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const context = canvas.getContext("2d"); + if (context) { + context.fillStyle = "#ffffff"; + context.fillRect(0, 0, size, size); + context.strokeStyle = "rgba(0,0,0,0.42)"; + context.lineWidth = 3; + // 타일 경계에서 끊기지 않도록 3줄(−size, 0, +size)을 겹쳐 긋는다. + for (const shift of [-size, 0, size]) { + context.beginPath(); + if (direction > 0) { + context.moveTo(shift, size); + context.lineTo(shift + size, 0); + } else { + context.moveTo(shift, 0); + context.lineTo(shift + size, size); + } + context.stroke(); + } + } + const texture = new THREE.CanvasTexture(canvas); + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + hatchCache.set(direction, texture); + return texture; +} + +interface SceneOrigin { + cx: number; + cy: number; + cz: number; +} + +function originOf(bounds: ModelBounds): SceneOrigin { + return { + cx: (bounds.x[0] + bounds.x[1]) / 2, + cy: (bounds.y[0] + bounds.y[1]) / 2, + cz: (bounds.z[0] + bounds.z[1]) / 2, + }; +} + +function ribbonGeometry(ribbon: CorridorRibbon, origin: SceneOrigin): THREE.BufferGeometry { + const rowCount = ribbon.chainages.length; + const cols = ribbon.colCount; + const positions = new Float32Array(rowCount * cols * 3); + // UV — u는 종방향 누가거리(m), v는 횡방향 열 비율. 빗금이 실척으로 반복된다. + const uvs = new Float32Array(rowCount * cols * 2); + const startChainage = ribbon.chainages[0]; + for (let row = 0; row < rowCount; row += 1) { + for (let col = 0; col < cols; col += 1) { + const i = row * cols + col; + const x = ribbon.positions[i * 3]; + const y = ribbon.positions[i * 3 + 1]; + const z = ribbon.positions[i * 3 + 2]; + positions[i * 3] = x - origin.cx; + positions[i * 3 + 1] = z - origin.cz; + positions[i * 3 + 2] = -(y - origin.cy); + uvs[i * 2] = (ribbon.chainages[row] - startChainage) / 2; // 2m마다 1타일. + uvs[i * 2 + 1] = col / Math.max(1, cols - 1); + } + } + const indices: number[] = []; + // 셀 마스크가 선 자리는 삼각형을 빼 그 자리를 비운다 — 투영 커브 안쪽 지우기 + // (2026-08-26). 정점은 그대로 두므로 남는 면의 형상은 하나도 안 바뀐다. + const mask = ribbon.cellMask; + for (let row = 0; row < rowCount - 1; row += 1) { + for (let col = 0; col < cols - 1; col += 1) { + if (mask?.[row * (cols - 1) + col]) continue; + const a = row * cols + col; + const b = a + 1; + const c = a + cols; + const d = c + 1; + indices.push(a, c, b, b, c, d); + } + } + // 축 고정 커브가 걸친 셀은 격자로 못 담는다 — 잘라 되살린 조각 삼각형을 뒤에 붙인다. + const trim = ribbon.trimTris; + const trimCount = trim ? trim.length / 3 : 0; + const allPositions = new Float32Array((rowCount * cols + trimCount) * 3); + const allUvs = new Float32Array((rowCount * cols + trimCount) * 2); + allPositions.set(positions); + allUvs.set(uvs); + if (trim) { + const base = rowCount * cols; + for (let v = 0; v < trimCount; v += 1) { + allPositions[(base + v) * 3] = trim[v * 3] - origin.cx; + allPositions[(base + v) * 3 + 1] = trim[v * 3 + 2] - origin.cz; + allPositions[(base + v) * 3 + 2] = -(trim[v * 3 + 1] - origin.cy); + allUvs[(base + v) * 2] = ribbon.trimUvs?.[v * 2] ?? 0; + allUvs[(base + v) * 2 + 1] = ribbon.trimUvs?.[v * 2 + 1] ?? 0; + if (v % 3 === 2) indices.push(base + v - 2, base + v - 1, base + v); + } + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(allPositions, 3)); + geometry.setAttribute("uv", new THREE.BufferAttribute(allUvs, 2)); + geometry.setIndex(indices); + geometry.computeVertexNormals(); + return geometry; +} + +/** 리본 양 가장자리(종류 경계) 선 — 도면처럼 경계를 또렷하게 한다. */ +function ribbonOutline(ribbon: CorridorRibbon, origin: SceneOrigin): THREE.BufferGeometry | null { + const rowCount = ribbon.chainages.length; + const cols = ribbon.colCount; + if (rowCount < 2) return null; + const points: number[] = []; + const sceneAt = (row: number, col: number): [number, number, number] => { + const i = (row * cols + col) * 3; + return [ + ribbon.positions[i] - origin.cx, + // 지형·서피스 위로 살짝 띄워 z-fight로 점선처럼 깜빡이는 것을 막는다. + ribbon.positions[i + 2] - origin.cz + 0.02, + -(ribbon.positions[i + 1] - origin.cy), + ]; + }; + const push = (row: number, col: number): void => { + points.push(...sceneAt(row, col)); + }; + for (const col of [0, cols - 1]) { + for (let row = 0; row < rowCount - 1; row += 1) { + push(row, col); + push(row + 1, col); + } + } + if (points.length < 6) return null; + return new THREE.BufferGeometry().setAttribute( + "position", + new THREE.Float32BufferAttribute(points, 3), + ); +} + +/** 마구리(시·종점 절단면) — 설계선과 지반선 사이를 세로로 잇는 띠. */ +function capGeometry(cap: CorridorCap, origin: SceneOrigin): THREE.BufferGeometry | null { + if (cap.points.length < 2) return null; + const count = cap.points.length; + const positions = new Float32Array(count * 2 * 3); + cap.points.forEach(([x, y, zDesign, zGround], index) => { + const sceneX = x - origin.cx; + const sceneZ = -(y - origin.cy); + positions[index * 6] = sceneX; + positions[index * 6 + 1] = zDesign - origin.cz; + positions[index * 6 + 2] = sceneZ; + positions[index * 6 + 3] = sceneX; + positions[index * 6 + 4] = zGround - origin.cz; + positions[index * 6 + 5] = sceneZ; + }); + const indices: number[] = []; + for (let i = 0; i < count - 1; i += 1) { + const a = i * 2; + indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2); + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); + geometry.setIndex(indices); + geometry.computeVertexNormals(); + return geometry; +} + +/** 구조물 모서리 추출 임계각(도) — 이보다 꺾인 모서리만 검은선으로 긋는다. + * 스윕 분할선(거의 평평한 이음)은 걸러지고 단면 모서리·마구리만 남는다. */ +const STRUCTURE_EDGE_ANGLE_DEG = 18; + +/** 배수관 세트 구조물 색 — 기슭막이(돌빛 황갈), 집수정(콘크리트 회청), 배관(강관 청). */ +const STRUCTURE_COLORS: Record = { + revet: 0xc9a86a, + basin: 0x8d93a3, + pipe: 0x4f83cc, +}; + +/** 단면 폴리곤(offset/표고)을 스윕 프레임 열에 앉혀 노선을 따라 로프트한 솔리드 + * (2026-08-23 사용자 — 직선 압출이 아니라 노선 스윕). */ +function structureLoftGeometry( + structure: CorridorStructure, + origin: SceneOrigin, +): THREE.BufferGeometry | null { + const rings = structure.rings; + // 링별 폴리곤이 오면 그것을, 아니면 공통 폴리곤 하나를 모든 링에 쓴다(2026-08-25). + const perRing = structure.polygons; + const polygon = perRing?.[0] ?? structure.polygon; + if (!polygon || polygon.length < 3 || !rings || rings.length < 2) return null; + if (perRing && perRing.length !== rings.length) return null; + const count = polygon.length; + const ringCount = rings.length; + const positions = new Float32Array(ringCount * count * 3); + rings.forEach((frame, ring) => { + (perRing?.[ring] ?? polygon).forEach(([offset, elevation], index) => { + const x = frame.cx + frame.leftX * offset; + const y = frame.cy + frame.leftY * offset; + const i = (ring * count + index) * 3; + positions[i] = x - origin.cx; + // 종단 경사 — 프레임마다 계획고 차이를 얹는다(2026-08-23 사용자). + positions[i + 1] = elevation + frame.dz - origin.cz; + positions[i + 2] = -(y - origin.cy); + }); + }); + const indices: number[] = []; + const lastBase = (ringCount - 1) * count; + for (let i = 1; i < count - 1; i += 1) { + indices.push(0, i, i + 1); // 앞 마구리(부채꼴 — 단면 폴리곤은 볼록 사각형이다) + indices.push(lastBase, lastBase + i + 1, lastBase + i); // 뒷 마구리(반대 감김) + } + for (let ring = 0; ring < ringCount - 1; ring += 1) { + const a0 = ring * count; + const b0 = a0 + count; + for (let i = 0; i < count; i += 1) { + const j = (i + 1) % count; + indices.push(a0 + i, b0 + i, a0 + j, a0 + j, b0 + i, b0 + j); + } + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); + geometry.setIndex(indices); + // 면마다 자기 법선(flat) — 인덱스를 공유한 채 평균 법선을 쓰면 모서리가 뭉개져 + // 구조물이 찌그러져 보인다(2026-08-23 사용자 지적: 엣지를 살릴 것). + const flat = geometry.toNonIndexed(); + geometry.dispose(); + flat.computeVertexNormals(); + return flat; +} + +/** + * 배관 — 관 하단 시·끝점(횡단면 안)을 축으로 하는 **중공 튜브**(2026-08-25 사용자: + * 내부 유로 반영). 바깥·안쪽 원통을 열린 채로 겹치고 양 끝을 고리로 막는다. + */ +function structurePipeMesh( + structure: CorridorStructure, + origin: SceneOrigin, +): THREE.Object3D | null { + const pipe = structure.pipe; + const frame = structure.frame; + if (!pipe || !frame) return null; + const { cx, cy, leftX, leftY } = frame; + const toScene = ([offset, elevation]: [number, number]): THREE.Vector3 => + new THREE.Vector3( + cx + leftX * offset - origin.cx, + elevation - origin.cz, + -(cy + leftY * offset - origin.cy), + ); + const start = toScene(pipe.start); + const end = toScene(pipe.end); + const axis = end.clone().sub(start); + const length = axis.length(); + if (!(length > 0.1)) return null; + // 관 하단선이 축이므로 반지름만큼 위로 올려 관 중심을 맞춘다. + const radius = pipe.diameterM / 2; + // 관벽 두께가 없으면 관경의 2%를 임시로 준다 — 면이 겹쳐 z-파이팅이 나지 않을 정도. + const wall = Math.min(Math.max(pipe.wallThicknessM ?? pipe.diameterM * 0.02, 0.01), radius * 0.9); + const inner = radius - wall; + const material = (side: THREE.Side): THREE.Material => + new THREE.MeshLambertMaterial({ color: STRUCTURE_COLORS.pipe, side }); + const group = new THREE.Group(); + group.add( + new THREE.Mesh( + new THREE.CylinderGeometry(radius, radius, length, 20, 1, true), + material(THREE.FrontSide), + ), + // 안쪽 벽은 관 속에서 보이므로 법선을 뒤집는다(내부 유로 면). + new THREE.Mesh( + new THREE.CylinderGeometry(inner, inner, length, 20, 1, true), + material(THREE.BackSide), + ), + ); + // 양 끝 관벽 단면 — 고리로 막아 튜브가 열린 파이프로 보이게 한다. + for (const sign of [1, -1]) { + const ring = new THREE.Mesh( + new THREE.RingGeometry(inner, radius, 20), + material(THREE.DoubleSide), + ); + ring.rotation.x = Math.PI / 2; + ring.position.y = (sign * length) / 2; + group.add(ring); + } + group.position.copy(start.clone().add(end).multiplyScalar(0.5)); + group.position.y += radius; + group.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), axis.normalize()); + return group; +} + +/** 빌드 결과 → 씬에 넣을 그룹. 호출부가 dispose(disposeObject)를 책임진다. */ +export function createCorridorGroup(build: CorridorBuildResult, bounds: ModelBounds): THREE.Group { + const group = new THREE.Group(); + group.name = "corridor"; + const origin = originOf(bounds); + build.ribbons.forEach((ribbon) => { + if (ribbon.chainages.length < 2) return; + const direction = HATCH_DIRECTION[ribbon.kind]; + const material = new THREE.MeshLambertMaterial({ + color: KIND_COLORS[ribbon.kind], + // 절·성토만 빗금 — 도면 관례대로 방향으로도 구분된다. + map: direction ? hatchTexture(direction) : null, + side: THREE.DoubleSide, + polygonOffset: true, + polygonOffsetFactor: POLYGON_OFFSET_FACTOR, + polygonOffsetUnits: POLYGON_OFFSET_FACTOR, + }); + // 도로부(차도·노견·측구)는 구조물 위를 지나므로 도려내지 않는다 — 노면은 복토 + // 위를 덮는 게 맞다. 절·성토 비탈만 구조물에 자리를 내준다. + const raw = ribbonGeometry(ribbon, origin); + // 절단은 Build가 행 단면 트림으로 이미 끝냈다(2026-08-25) — 여기서는 그대로 그린다. + const mesh = new THREE.Mesh(raw, material); + mesh.name = `corridor:${ribbon.kind}:${ribbon.side}`; + group.add(mesh); + const outline = ribbonOutline(ribbon, origin); + if (outline) { + const line = new THREE.LineSegments( + outline, + new THREE.LineBasicMaterial({ color: OUTLINE_COLOR, transparent: true, opacity: 0.8 }), + ); + line.name = `corridor-outline:${ribbon.kind}:${ribbon.side}`; + group.add(line); + } + }); + // 탑뷰 투영 커브(2026-08-26 사용자) — 구조물 세트마다 **그 세트 구조물 상단 +5m** + // 평면에 눕힌 닫힌 고리다. 종류는 색(구조물 노랑 / 변형 성토선 분홍 / 원본 하늘), + // 유입·유출은 실선·파선으로 나눈다. 표기 전용 — 서피스는 안 건드린다. + // + // **최종 결과물에서는 숨긴다**(2026-09-02 사용자). 지우지는 않는다 — 절취·패치 기하를 + // 다시 들여다볼 때 쓰므로 묶음째 감춰 두고 `__corridorPlanCurves(true)` 로 되켠다. + const planGroup = new THREE.Group(); + planGroup.name = PLAN_CURVE_GROUP; + planGroup.visible = planCurvesVisible(); + group.add(planGroup); + (build.planCurves ?? []).forEach((curve) => { + // 벽 자리 커브는 표시용이 아니다 — 벽 서피스로만 나간다(2026-08-27 사용자). + if (curve.source === "cut-wall") return; + const color = PLAN_CURVE_COLORS[curve.source]; + const dashed = curve.role === "outlet"; + curve.loops.forEach((loop, index) => { + if (loop.length < 2) return; + const positions: number[] = []; + // 점의 z를 그대로 쓴다 — 평면 커브는 z에 planZ가 들어 있어 동작이 같고, + // 투영선(`slope-projected`)만 점마다 절·성토면 표고를 들고 온다(2026-08-26). + for (const [x, y, z] of loop) { + positions.push(x - origin.cx, z - origin.cz, -(y - origin.cy)); + } + const geometry = new THREE.BufferGeometry().setAttribute( + "position", + new THREE.Float32BufferAttribute(positions, 3), + ); + const line = new THREE.Line( + geometry, + dashed + ? new THREE.LineDashedMaterial({ color, ...PLAN_DASH_M }) + : new THREE.LineBasicMaterial({ color }), + ); + if (dashed) line.computeLineDistances(); + const station = curve.setChainageM.toFixed(2); + line.name = `corridor-plan:${curve.source}:${curve.role}:${curve.side}:${station}:${index}`; + planGroup.add(line); + }); + }); + // 절취 측벽 — 리본과 분리된 **별도 서피스**(2026-08-27 사용자). 성토면과 같은 색·빗금이라 + // 한 몸으로 보이지만 객체가 달라 다음 단계(바닥면·트림)에서 따로 다룰 수 있다. + (build.cutWalls ?? []).forEach((wall, index) => { + const count = wall.positions.length / 3; + if (!count) return; + const positions = new Float32Array(count * 3); + for (let v = 0; v < count; v += 1) { + positions[v * 3] = wall.positions[v * 3] - origin.cx; + positions[v * 3 + 1] = wall.positions[v * 3 + 2] - origin.cz; + positions[v * 3 + 2] = -(wall.positions[v * 3 + 1] - origin.cy); + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); + if (wall.uvs && wall.uvs.length === count * 2) { + geometry.setAttribute("uv", new THREE.BufferAttribute(wall.uvs, 2)); + } + geometry.computeVertexNormals(); + const mesh = new THREE.Mesh( + geometry, + new THREE.MeshLambertMaterial({ + // 절취 측벽은 **일반 서피스**(빗금 없음) — 벽은 방향성을 표현할 면이 아니다 + // (2026-08-27 사용자). 패치 스커트는 성토면의 연장이라 **빗금 있는 메시 + // 서피스**로 그린다(2026-08-27 사용자) — UV를 싣고 온 판만 해당된다. + color: KIND_COLORS.fill, + map: wall.uvs ? hatchTexture(HATCH_DIRECTION.fill ?? -1) : null, + side: THREE.DoubleSide, + polygonOffset: true, + polygonOffsetFactor: POLYGON_OFFSET_FACTOR, + polygonOffsetUnits: POLYGON_OFFSET_FACTOR, + }), + ); + mesh.name = wall.patchSkirt + ? `corridor-patch-skirt:${wall.side}:${index}` + : `corridor-cut-wall:${wall.side}:${index}`; + group.add(mesh); + }); + build.caps.forEach((cap, index) => { + const geometry = capGeometry(cap, origin); + if (!geometry) return; + const mesh = new THREE.Mesh( + geometry, + new THREE.MeshLambertMaterial({ color: CAP_COLOR, side: THREE.DoubleSide }), + ); + mesh.name = `corridor-cap:${index}`; + group.add(mesh); + }); + // 배수관 세트 구조물(기슭막이·집수정·배관) — B06 횡단 계산 그대로의 솔리드 + // (2026-08-23 사용자). 리본 격자와 무관한 독립 물체라 개별 Mesh로 얹는다. + (build.structures ?? []).forEach((structure, index) => { + // 신원표 — 3D 클릭이 이 값으로 부재를 가려낸다(2026-09-04). 이름 규칙은 그대로 둔다 + // (다른 코드가 이름으로 찾는다). 배관은 겉·속 두 겹이라 자식까지 같은 표를 단다. + const tag = { chainageM: structure.chainage_m, kind: structure.kind, key: structure.key }; + if (structure.kind === "pipe") { + const mesh = structurePipeMesh(structure, origin); + if (mesh) { + mesh.name = `corridor-structure:pipe:${index}`; + mesh.traverse((object) => { + object.userData = tag; + }); + group.add(mesh); + } + return; + } + const geometry = structureLoftGeometry(structure, origin); + if (!geometry) return; + const mesh = new THREE.Mesh( + geometry, + new THREE.MeshLambertMaterial({ + color: STRUCTURE_COLORS[structure.kind], + side: THREE.DoubleSide, + polygonOffset: true, + polygonOffsetFactor: POLYGON_OFFSET_FACTOR, + polygonOffsetUnits: POLYGON_OFFSET_FACTOR, + }), + ); + mesh.name = `corridor-structure:${structure.kind}:${index}`; + mesh.userData = tag; + group.add(mesh); + // 모서리 검은선 — 성토부·노폭 리본과 같은 규칙으로 경계를 또렷하게 + // (2026-08-23 사용자). 임계각 위 모서리만 뽑아 스윕 분할선은 남기지 않는다. + const edges = new THREE.LineSegments( + new THREE.EdgesGeometry(geometry, STRUCTURE_EDGE_ANGLE_DEG), + new THREE.LineBasicMaterial({ color: OUTLINE_COLOR, transparent: true, opacity: 0.9 }), + ); + edges.name = `corridor-structure-edge:${structure.kind}:${index}`; + group.add(edges); + }); + return group; +} diff --git a/B05_Profile/B05_Profile_UI_Corridor_Plan.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Plan.ts similarity index 100% rename from B05_Profile/B05_Profile_UI_Corridor_Plan.ts rename to B05_Profile/Old/B05_Profile_UI_Corridor_Plan.ts diff --git a/B05_Profile/B05_Profile_UI_Corridor_Plan_Footprint.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Plan_Footprint.ts similarity index 100% rename from B05_Profile/B05_Profile_UI_Corridor_Plan_Footprint.ts rename to B05_Profile/Old/B05_Profile_UI_Corridor_Plan_Footprint.ts diff --git a/B05_Profile/B05_Profile_UI_Corridor_Skirt.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Skirt.ts similarity index 100% rename from B05_Profile/B05_Profile_UI_Corridor_Skirt.ts rename to B05_Profile/Old/B05_Profile_UI_Corridor_Skirt.ts diff --git a/B05_Profile/B05_Profile_UI_Corridor_Union.ts b/B05_Profile/Old/B05_Profile_UI_Corridor_Union.ts similarity index 100% rename from B05_Profile/B05_Profile_UI_Corridor_Union.ts rename to B05_Profile/Old/B05_Profile_UI_Corridor_Union.ts diff --git a/B05_Profile/Old/B05_Profile_UI_Viewer_Debug.ts b/B05_Profile/Old/B05_Profile_UI_Viewer_Debug.ts new file mode 100644 index 000000000..b5964869a --- /dev/null +++ b/B05_Profile/Old/B05_Profile_UI_Viewer_Debug.ts @@ -0,0 +1,55 @@ +/* ============================================================================= + * B05_Profile_UI_Viewer_Debug.ts + * 뷰어 **검증용 요약** — 코리도 빌드 결과를 `window.__corridorBuild` 에 얹는다 + * (`__corridorClip`·`__corridorScene` 과 같은 용도). + * + * `B05_Profile_UI_Viewer` 에서 그대로 떼어냈다(700줄 제한, 2026-09-04). 계산은 없고 + * 요약 형태만 있으므로 화면 동작에는 영향이 없다. + * ========================================================================== */ + +import type { CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build"; + +/** + * 격자 재배치·투영선이 리본을 흔들었는지 화면 밖에서 수치로 확인한다(2026-08-26). + * `raw` 는 원본 참조라 투영선이 실제로 서피스에 얹혔는지 좌표로 대조할 때 쓴다. + */ +export function setCorridorBuildSummary(build: CorridorBuildResult | null): void { + (window as unknown as { __corridorBuild?: unknown }).__corridorBuild = build + ? { + ribbons: build.ribbons.map((ribbon) => ({ + kind: ribbon.kind, + side: ribbon.side, + rows: ribbon.chainages.length, + cols: ribbon.colCount, + first: ribbon.chainages[0], + last: ribbon.chainages[ribbon.chainages.length - 1], + })), + outlineRows: build.outline.chainages.length, + outline: build.outline, + // 구조물 솔리드 요약 — 어떤 시설이 몇 개 섰는지 화면 밖에서 센다(2026-08-28). + // 부재키(2026-09-04)도 함께 낸다 — 3D 개별 선택이 무엇을 집었는지 대조한다. + structures: (build.structures ?? []).map((solid) => ({ + kind: solid.kind, + key: solid.key ?? null, + at: solid.chainage_m, + rings: solid.rings?.length ?? 0, + points: solid.polygon?.length ?? 0, + })), + raw: { + ribbons: build.ribbons, + planCurves: build.planCurves ?? [], + cutWalls: build.cutWalls ?? [], + }, + planCurves: (build.planCurves ?? []).map((curve) => ({ + source: curve.source, + role: curve.role, + side: curve.side, + at: curve.setChainageM, + loops: curve.loops.length, + points: curve.loops.reduce((sum, loop) => sum + loop.length, 0), + z0: curve.loops[0]?.[0]?.[2] ?? null, + planZ: curve.planZ, + })), + } + : null; +} diff --git a/resources/tester/helper_b05_cut_merged_loop.cjs b/resources/tester/old/helper_b05_cut_merged_loop.cjs similarity index 100% rename from resources/tester/helper_b05_cut_merged_loop.cjs rename to resources/tester/old/helper_b05_cut_merged_loop.cjs diff --git a/resources/tester/helper_b05_patch_skirt.cjs b/resources/tester/old/helper_b05_patch_skirt.cjs similarity index 100% rename from resources/tester/helper_b05_patch_skirt.cjs rename to resources/tester/old/helper_b05_patch_skirt.cjs diff --git a/resources/tester/test_b05_cut_merged_loop.py b/resources/tester/old/test_b05_cut_merged_loop.py similarity index 100% rename from resources/tester/test_b05_cut_merged_loop.py rename to resources/tester/old/test_b05_cut_merged_loop.py diff --git a/resources/tester/test_b05_patch_skirt.py b/resources/tester/old/test_b05_patch_skirt.py similarity index 100% rename from resources/tester/test_b05_patch_skirt.py rename to resources/tester/old/test_b05_patch_skirt.py