/* ============================================================================= * D03_PlanAnalysis_UI_DrawPanel.ts * D03 좌측 「그리기」 칸 몸(PLAN 599-1 · 599-4 · 요구 463-10 · 22) — [긋기] · 연도 구역 마커 · 긋기 방식 칸. * * 방식 칸 `createDrawKindFields` = 마디 고름 2 × 2(자유 · 길이 고정 · 기울기 고정 · 길이·기울기 고정) + * 길이 · 기울기 두 열(기울기 아래 ≈ °) — 그리기 칸 · 노드 판(598-4)이 같은 부품을 씀. * 연도 구역 마커 = 끌어 선 위에 놓기(`YEAR_MARK_DRAG` · 받음 = `D03_PlanAnalysis_UI_Edit` drop). * 설명 글 없음(요구 463-6) — 칸 오류 · ≈ ° 값만. * ========================================================================== */ import type { UiSize } from "@ui/ui_template_button"; import { createButton } from "@ui/ui_template_elements"; import { createInputField } from "@ui/ui_template_input"; import { createSegment } from "@ui/ui_template_segment"; import { createText } from "@ui/ui_template_text"; import { fmt, slopeDeg } from "./D03_PlanAnalysis_UI_DrawPaint"; import { YEAR_MARK_DRAG } from "./D03_PlanAnalysis_UI_Map"; /** 제약 칸 처음 값 — 길이 = 측점 간격(지식DB 설계제원_총괄) · 기울기 = 간선 20 km/h 일반지형 상한(종단선형). */ export const DRAW_DEFAULT_LENGTH_M = 20; export const DRAW_DEFAULT_SLOPE_PCT = 9; export type DrawConstraint = "free" | "length" | "slope" | "both"; export interface DrawKindValue { kind: DrawConstraint; lengthM: number; slopePct: number; } export const DRAW_KIND_LABELS: Record = { free: "자유", length: "길이 고정", slope: "기울기 고정", both: "길이·기울기 고정", }; export interface DrawKindFields { root: HTMLElement; get: () => DrawKindValue; /** 값을 바꿈(onChange 는 안 부름). */ set: (value: Partial) => void; setDisabled: (disabled: boolean) => void; } /** 긋기 방식 칸 — 고름 · 길이 · 기울기가 바뀔 때마다 `onChange(지금 값)`(빈 · 틀린 수는 칸 오류만). */ export function createDrawKindFields(opts: { value: DrawKindValue; onChange: (value: DrawKindValue) => void; size?: UiSize; }): DrawKindFields { const size = opts.size ?? 24; let value = { ...opts.value }; let disabled = false; const changed = (): void => opts.onChange({ ...value }); const segment = createSegment({ options: (Object.keys(DRAW_KIND_LABELS) as DrawConstraint[]).map((kind) => ({ value: kind, label: DRAW_KIND_LABELS[kind], })), value: value.kind, size, columns: 2, ariaLabel: "긋기 방식", onChange: (next) => { value.kind = next as DrawConstraint; fieldsState(); changed(); }, }); segment.root.dataset.role = "draw-constraint"; const lengthField = createInputField({ label: "길이", type: "number", value: String(value.lengthM), min: 1, unit: "m", size, onInput: (text) => { const next = Number(text); lengthField.setError(next > 0 ? undefined : "0 보다 큰 수"); if (!(next > 0)) return; value.lengthM = next; changed(); }, }); lengthField.input.dataset.role = "draw-length"; const slopeField = createInputField({ label: "기울기", type: "number", value: String(value.slopePct), unit: "%", size, onInput: (text) => { const next = Number(text); const ok = text.trim() !== "" && Number.isFinite(next); slopeField.setError(ok ? undefined : "숫자"); if (!ok) return; value.slopePct = next; degText(); changed(); }, }); slopeField.input.dataset.role = "draw-slope"; const slopeDegText = createText("", "muted"); slopeDegText.dataset.role = "draw-slope-deg"; const degText = (): void => { slopeDegText.textContent = `≈ ${fmt(slopeDeg(Math.abs(value.slopePct)))}°`; }; const slopeColumn = document.createElement("div"); slopeColumn.append(slopeField.root, slopeDegText); const fields = document.createElement("div"); fields.className = "d03-plan__draw-fields"; fields.append(lengthField.root, slopeColumn); const root = document.createElement("div"); root.className = "d03-plan__draw-kind"; root.append(segment.root, fields); function fieldsState(): void { for (const button of segment.root.querySelectorAll("button")) button.disabled = disabled; lengthField.input.disabled = disabled || value.kind === "free" || value.kind === "slope"; slopeField.input.disabled = disabled || value.kind === "free" || value.kind === "length"; } degText(); fieldsState(); return { root, get: () => ({ ...value }), set(next) { value = { ...value, ...next }; segment.set(value.kind); lengthField.input.value = String(value.lengthM); slopeField.input.value = String(value.slopePct); lengthField.setError(undefined); slopeField.setError(undefined); degText(); fieldsState(); }, setDisabled(next) { disabled = next; fieldsState(); }, }; } export interface DrawPanel { root: HTMLElement; /** 단추 글 · 막힘 — 긋는 중 · 고칠 수 있음 · 마커 놓을 선 있음. */ state: (state: { drawing: boolean; editable: boolean; canMark: boolean }) => void; } /** 좌측 「그리기」 칸 — [긋기] · 연도 구역 마커(끌기) · 방식 칸. */ export function createDrawPanel(opts: { kind: DrawKindFields; onDraw: () => void }): DrawPanel { const drawButton = createButton({ label: "긋기", variant: "filled", size: 28, onClick: opts.onDraw, }); const markButton = createButton({ label: "연도 구역 마커", variant: "ghost", size: 28, iconStart: "flag", }); markButton.title = "선 위로 끌어 놓으면 구역을 나눕니다"; markButton.dataset.role = "draw-year-mark"; markButton.draggable = true; markButton.addEventListener("dragstart", (event) => { event.dataTransfer?.setData(YEAR_MARK_DRAG, "1"); if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"; }); const row = document.createElement("div"); row.className = "d03-plan__row"; row.append(drawButton, markButton); const root = document.createElement("div"); root.className = "d03-plan__draw"; root.append(row, opts.kind.root); return { root, state({ drawing, editable, canMark }) { drawButton.querySelector(".ui-btn__label")!.textContent = drawing ? "긋기 끝" : "긋기"; drawButton.disabled = !editable; drawButton.setAttribute("aria-pressed", String(drawing)); markButton.disabled = !editable || drawing || !canMark; root.dataset.mode = drawing ? "draw" : "idle"; }, }; }