/* ============================================================================= * B05_Profile_UI_Structures_Fields.ts * 「구조물 배치」 폼 입력 조각 — 라벨 필드·숫자칸·셀렉트·측점번호+잔여거리 두 칸. * * 패널 본체(B05_Profile_UI_Structures_Panel)가 700줄 한계에 닿아 분리했다. * 측점 두 칸은 구 비정규 측점 UI와 같은 입력 방식이다(2026-08-17 사용자 확정) — * 저장은 누가거리(m), 화면 입출력만 측점번호+잔여거리. * ========================================================================== */ import { chainageToStation } from "./B05_Profile_Util_Station"; export function field(labelText: string, input: HTMLElement): HTMLLabelElement { const wrapper = document.createElement("label"); wrapper.className = "b05-route__field"; const caption = document.createElement("span"); caption.textContent = labelText; wrapper.append(caption, input); return wrapper; } export function numberInput(step = "0.1", min = "0"): HTMLInputElement { const input = document.createElement("input"); input.type = "number"; input.step = step; input.min = min; return input; } export function select(options: ReadonlyArray<[string, string]>): HTMLSelectElement { const element = document.createElement("select"); element.replaceChildren(...options.map(([value, label]) => new Option(label, value))); return element; } /** 측점번호 + 잔여거리 두 칸 묶음. */ export interface StationFields { wrap: HTMLElement; station: HTMLInputElement; remainder: HTMLInputElement; /** 두 칸을 누가거리로 읽는다. 비었으면 null, 형식 오류면 붉히고 null. */ read: (intervalM: number, required: boolean) => number | null; /** 누가거리를 두 칸에 나눠 싣는다. null이면 비운다. */ write: (chainageM: number | null, intervalM: number) => void; clearInvalid: () => void; } export function stationFields(labelText: string): StationFields { const station = numberInput("1", "0"); station.placeholder = "측점"; const remainder = numberInput("0.1", "0"); remainder.placeholder = "+m"; const pair = document.createElement("div"); pair.className = "b05-structure__station-pair"; pair.append(station, remainder); const wrap = field(labelText, pair); const markInvalid = (bad: boolean): void => { station.classList.toggle("is-invalid", bad); remainder.classList.toggle("is-invalid", bad); }; return { wrap, station, remainder, read(intervalM, required) { const stationText = station.value.trim(); const remainderText = remainder.value.trim(); if (!stationText && !remainderText) { markInvalid(required); return null; } const stationNo = stationText ? Number(stationText) : 0; const rest = remainderText ? Number(remainderText) : 0; const valid = Number.isFinite(stationNo) && stationNo >= 0 && Number.isFinite(rest) && rest >= 0; markInvalid(!valid); return valid ? stationNo * intervalM + rest : null; }, write(chainageM, intervalM) { if (chainageM === null) { station.value = ""; remainder.value = ""; return; } const parts = chainageToStation(chainageM, intervalM); station.value = String(parts.station); remainder.value = parts.remainder.toFixed(1); }, clearInvalid: () => markInvalid(false), }; }