/* ============================================================================= * B06_Section_UI_Cross_CutSlope.ts * 측점 하나의 **암 절토 경사** 입력칸(2026-09-07 사용자 지시). * * 사용자 원문 — 「개별 횡단도에는 암 절토 각도의 개별 수정 가능해야함 / 전체 변경을 위해서는 * 좌측 패널의 표준 횡단면 설정을 이용」. 그래서 **전체 기본값은 좌측 패널**, **이 칸은 그 측점 * 하나**만 바꾼다. 표준을 바꿔도 여기서 만진 측점은 그대로다(사용자 값이라 재계산이 살려 둔다). * * 화면에는 **각도(°)**로 보이고 속으로는 경사비(1:n 의 n)로 다닌다 — 소단 폼의 * 「안쪽 기울기 (°)」와 같은 말법이다. 1:0.4 = 68.2° · 1:1.0 = 45° · 1:1.5 = 33.7°. * * ⚠ 경사는 **나르는 값이 아니라 기하 입력**이다. 계산이 끝난 결과에 값만 베껴 붙이면 설계선은 * 옛 경사로 그려지고 숫자만 새것이 된다 — 값은 반드시 **계산 전에** 넘어가야 한다 * (`computeCrossDesign(cutSlopeRatio)` · `compute_cross_design(cut_slope_ratio=…)`). * ========================================================================== */ import type { CrossSection } from "./B06_Section_Api_Fetch"; /** 측점별 암 절토 경사 제어기 — Page 가 세션 저장소와 연결해 구현한다. */ export interface CutSlopeControl { /** 세션 → design 저장값 → 표준값 순으로 지금 경사비를 돌려준다. */ ratioFor: (section: CrossSection) => number; /** 이 측점의 표준값(되돌릴 자리) — 사용자가 안 만진 상태의 값. */ standardRatioFor: (section: CrossSection) => number; /** 경사비를 넣는다. null 이면 표준값으로 되돌린다. */ set: (chainageM: number, ratio: number | null) => void; } /** 경사비(1:n 의 n) → 수평에서 잰 각도(°). n 이 작을수록 급하다. */ export function ratioToDegrees(ratio: number): number { return (Math.atan(1 / Math.max(ratio, 1e-6)) * 180) / Math.PI; } /** 각도(°) → 경사비(1:n 의 n). 1~89° 밖은 사면이 서지 않아 잘라 받는다. */ export function degreesToRatio(degrees: number): number { const clamped = Math.min(Math.max(degrees, 1), 89); return 1 / Math.tan((clamped * Math.PI) / 180); } /** 0.1° 단위로 같은 값인가 — 표준값으로 되돌아왔는지 가리는 데 쓴다. */ function sameDegrees(a: number, b: number): boolean { return Math.abs(a - b) < 0.05; } /** * 카드 하단에 서는 「암 절토 68.2° ↺」 칸. 암 경계선 제어와 같은 자리·같은 모양이다. * * 되돌리기(↺)는 **표준 횡단면 설정값**으로 돌려놓는다 — 좌측 패널을 그 뒤에 바꾸면 이 측점도 * 따라간다(사용자가 만진 흔적이 지워지므로). */ export function buildCutSlopeControl(section: CrossSection, control: CutSlopeControl): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b06-design__seg b06-design__cutslope"; const legend = document.createElement("span"); legend.className = "b06-design__seg-legend"; legend.textContent = "암 절토"; wrap.append(legend); const group = document.createElement("div"); group.className = "b06-design__seg-buttons"; const ratio = control.ratioFor(section); const standard = control.standardRatioFor(section); const degrees = ratioToDegrees(ratio); const input = document.createElement("input"); input.type = "number"; input.className = "b06-design__cutslope-input"; input.step = "0.5"; input.min = "1"; input.max = "89"; input.value = degrees.toFixed(1); input.title = `이 측점의 암 절토 경사 — 각도(°)로 넣는다.\n` + `지금 1:${ratio.toFixed(2)} (${degrees.toFixed(1)}°) · 표준 1:${standard.toFixed(2)} (${ratioToDegrees(standard).toFixed(1)}°)\n` + `전체를 바꾸려면 좌측 [표준 횡단면 설정]을 쓴다.`; const commit = (): void => { const entered = Number(input.value); if (!Number.isFinite(entered)) { input.value = degrees.toFixed(1); return; } // 표준값으로 되돌아온 입력은 사용자 값을 남기지 않는다 — 그래야 표준을 바꿀 때 따라간다. const next = sameDegrees(entered, ratioToDegrees(standard)) ? null : degreesToRatio(entered); control.set(section.chainage_m, next); }; input.addEventListener("change", commit); input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); commit(); } }); // 카드 클릭(선택·줌)이 입력을 뺏지 않게 한다 — 암 경계선 버튼과 같은 태도. input.addEventListener("pointerdown", (event) => event.stopPropagation()); const unit = document.createElement("span"); unit.className = "b06-design__cutslope-unit"; unit.textContent = "°"; const reset = document.createElement("button"); reset.type = "button"; reset.className = "b06-design__rockb-btn is-reset"; reset.textContent = "↺"; reset.title = `표준값으로 되돌리기 (1:${standard.toFixed(2)} · ${ratioToDegrees(standard).toFixed(1)}°)`; reset.disabled = sameDegrees(degrees, ratioToDegrees(standard)); reset.addEventListener("click", (event) => { event.stopPropagation(); control.set(section.chainage_m, null); }); group.append(input, unit, reset); wrap.append(group); return wrap; }