/* ============================================================================= * B06_Section_UI_Cross_Culvert_Wire.ts * 횡단 카드의 배수관 세트 **계산 호출 + 이동량 되받기(토스트)** — Cross_View에서 * 분리(700줄 제한). 카드가 다시 그려질 때마다 한 번 불린다. * ========================================================================== */ import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch"; import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert"; import type { CulvertLayout, InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert"; import { materialLabel, materialLimit } from "./B06_Section_UI_Cross_Culvert_Const"; import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types"; import { designInterpolator, groundInterpolator, slopeToeOffset, } from "./B06_Section_UI_Cross_Culvert_Solve"; /** * 기슭막이 4축 조작 제어(2026-08-22 확정) — 벽을 눌러 고르고 조정창에서 * 좌우(x)·상하(d, 성토선 대각)·높이(h)·재질(m)을 만진다. 고른 벽은 여기 담아 두어 * 카드가 다시 그려져도 되살아난다. `select`는 값만 담고 다시 그리지 않는다. */ export interface RevetOffsetControl { adjustFor: (section: CrossSection, role: RevetKey) => WallAdjust; selectedFor: (section: CrossSection) => RevetKey | null; select: (chainageM: number, key: RevetKey | null) => void; /** 기하가 실제로 적용한 조작값을 되받아 담는다(한계에 걸린 요청값을 잘라 낸다). */ syncApplied: (chainageM: number, role: RevetKey, applied: WallAdjust) => void; /** 일부 축만 바꾼다 — 나머지는 유지. h: null = 기본값 복귀. */ update: (chainageM: number, role: RevetKey, patch: Partial) => void; reset: (chainageM: number, role: RevetKey) => void; } /** 유입측 구조물 형식 선택(드롭다운 — 2026-08-22 사용자). 세션 보관은 Page가 한다. */ export interface InletStructureControl { valueFor: (section: CrossSection) => InletStructureChoice; set: (chainageM: number, value: InletStructureChoice) => void; } /** 유출측 다단 기슭막이 단 수 제어(2026-08-22 사용자 — 유출 벽 기준 숫자 입력). */ export interface ExtraWallControl { countFor: (section: CrossSection) => number; /** 단 수 지정 — 줄이면 사라지는 단의 이동량도 함께 지운다. */ setCount: (chainageM: number, count: number) => void; /** 다단 등간격 배치 요청(2026-08-22 ①) — 다음 계산 1회에 적용된다. */ equalize: (chainageM: number) => void; /** 등간격 요청을 소비(1회성) — 계산 직전에 Wire가 부른다. */ consumeEqualize: (section: CrossSection) => boolean; /** 기하가 실제로 세운 단 수로 잘라 동기화(지형상 못 세운 단 정리). */ syncCount: (chainageM: number, built: number) => void; } import { showToast } from "@ui/ui_template_elements"; import { L } from "./B06_Section_UI_Section_Common"; /** * 배수관 세트 기하 계산 + 제어 상태 동기화. * * ① 세션의 이동량(기슭막이·추가 벽)·유입 구조물 선택을 모아 기하를 계산하고, * ② 성토부가 짧아져 못 세운 추가 벽은 개수를 잘라 동기화하며(고아 이동량 방지), * ③ 요청 이동량이 한계에 잘렸으면 실제 적용값으로 되돌려 담는다 — 안 그러면 눌러도 * 안 움직이는데 창의 숫자만 계속 커진다(2026-08-21 사용자 지적). 잘린 순간에는 * **왜 안 움직였는지 토스트**로 알린다(2026-08-22 사용자 — 13+15.7처럼 자동 * 자리가 이미 안쪽 한계라 ◀가 그대로 먹히지 않는 경우). 조작 중(그 벽이 선택된 * 상태)일 때만 띄운다 — 리로드로 복원된 옛 세션 값에는 침묵. */ export function computeCardCulvert( section: CrossSection, sourceSamples: SectionSample[], activeRevet: RevetKey | null, revetOffset?: RevetOffsetControl, inletStructure?: InletStructureControl, extraWalls?: ExtraWallControl, ): CulvertLayout | null { const equalizeExtras = extraWalls?.consumeEqualize(section) ?? false; const layout = computeCulvertLayout( section, sourceSamples, adjustsInput(section, revetOffset, extraWalls), inletStructure?.valueFor(section), equalizeExtras, ); if (!layout) return null; if (extraWalls && extraWalls.countFor(section) > layout.extraWalls.length) { // 요청 단 수보다 지형이 허락하는 단이 적다(벽이 원지반에 0.5m 이상 묻히면 성토 // 불필요 — 그 아래 단은 못 세운다). 조작 중일 때만 가능한 단 수를 토스트로 알린다. if (activeRevet === "outlet" || activeRevet?.startsWith("extra")) { showToast( L("B06_Cross_Extra_Limit").replace("{n}", String(layout.extraWalls.length)), "info", ); } extraWalls.syncCount(section.chainage_m, layout.extraWalls.length); } if (revetOffset) { const roles: Array<[RevetKey, WallAdjust]> = [ ["inlet", layout.revetShift.inlet], ["outlet", layout.revetShift.outlet], ...layout.revetShift.extras.map((applied, i): [RevetKey, WallAdjust] => [ `extra${i}`, applied, ]), ]; const wallOf = (role: RevetKey) => role.startsWith("extra") ? layout.extraWalls[Number(role.slice(5))] : layout.walls.find((wall) => wall.role === role); for (const [role, applied] of roles) { const requested = revetOffset.adjustFor(section, role); // 축별로 요청이 잘렸으면 왜 안 됐는지 토스트로 알린다(조작 중인 벽만). if (activeRevet === role) { if (Math.abs(requested.x - applied.x) > 0.05) { showToast( L( requested.x < applied.x ? "B06_Cross_Revet_Limit_Inward" : "B06_Cross_Revet_Limit_Outward", ), "info", ); } if (Math.abs(requested.d - applied.d) > 0.05) { showToast( L(requested.d < applied.d ? "B06_Cross_Revet_Limit_Up" : "B06_Cross_Revet_Limit_Down"), "info", ); } // 높이가 재질 한계에 잘렸으면 재질 변경을 안내(2026-08-22 사용자 — 경고와 // 높이 해제는 재질 선택으로). const wall = wallOf(role); if (requested.h != null && applied.h != null && requested.h > applied.h + 0.05 && wall) { showToast( L("B06_Cross_Height_Limit") .replace("{mat}", materialLabel(wall.material)) .replace("{limit}", materialLimit(wall.material).toFixed(1)), "info", ); } if (requested.h != null && applied.h != null && requested.h < applied.h - 0.05) { showToast(L("B06_Cross_Height_Floor"), "info"); } } revetOffset.syncApplied(section.chainage_m, role, applied); } } return layout; } function adjustsInput( section: CrossSection, revetOffset?: RevetOffsetControl, extraWalls?: ExtraWallControl, ): { inlet: WallAdjust; outlet: WallAdjust; extras: WallAdjust[] } | undefined { if (!revetOffset) return undefined; return { inlet: revetOffset.adjustFor(section, "inlet"), outlet: revetOffset.adjustFor(section, "outlet"), extras: Array.from({ length: extraWalls?.countFor(section) ?? 0 }, (_, i) => revetOffset.adjustFor(section, `extra${i}`), ), }; } /** * **구조물 없는 상태**의 설계 절·성토선이 원지반과 만나는 지점이 들어오는 데 * 필요한 표시 반폭(m, 여유 0.5 포함) — 2026-08-22 사용자 확정 줌 기준. * 구조물(기슭막이·집수정)이 어디로 가든 화면 기준은 지형·설계선이 정한다. */ export function culvertRequiredHalfWidth(section: CrossSection): number | null { const design = section.design; if (!design) return null; const groundAt = groundInterpolator(section.samples); const designAt = designInterpolator(design.design_line); const edges = design.road_edges; if (!groundAt || !designAt || !edges) return null; const offsets = section.samples.map((sample) => sample.offset_m ?? 0); const limits = { left: Math.max(...offsets), right: Math.min(...offsets) }; let extent = 0; for (const side of ["left", "right"] as const) { const edge = side === "left" ? edges.left : edges.right; const toe = slopeToeOffset(designAt, groundAt, edge.offset_m, limits[side]); extent = Math.max(extent, Math.abs(toe)); } return extent > 0 ? extent + 0.5 : null; }