/* ============================================================================= * B05_Profile_UI_RouteEdit_SnapStep.ts * 노선 편집 [측점 스냅] 단추 · 스냅 간격 고르기(390-2 · 요구 242-11). * * 간격 = 0.1 m · 1 m · 10 m · 측점 No.(편집 창 측점 간격 · 기본) — 곡선 MC 를 그 간격 배수에 맞춤. * 고른 값은 프로젝트별 화면 기억(pref `routeedit-snap-step`) · 스냅 켬이면 저장 노선에도 같이 * (`station_snap_step` · 390-5 · 서버 측점 셈이 MC 측점 값 자릿수로 씀 · 옛 노선 = 측점 No.). * 저장 노선이 스냅 켬이면 열 때 그 간격으로 고르기를 맞춤. 추천 · 서버 초기 노선은 측점 No. 그대로. * 스냅 끔이면 고르기는 흐림(CSS) · 고를 수는 있음. * 상태줄 글은 고른 간격 따라(390-4) — 측점 No. = 「일반측점에」 · 그 밖 = 「1 m 배수에」 꼴. * 평면 눈금 = 일반측점(스냅 켬이면 옮긴 자리) + 곡선 측점(MC 에 측점 없을 때 · 252-2 · `ticks`). * ========================================================================== */ import { readState, writeState } from "../A00_Common/b_page_state"; import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types"; import type { RouteEditChrome } from "./B05_Profile_UI_RouteEdit_Chrome"; import type { EditedCurve, Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve"; import { curveSpans, curveStations, snapCurveStations, stationsAlong, } from "./logic/B05_Profile_UI_RouteEdit_Frame"; const STEPS = ["0.1", "1", "10", "station"]; /** 저장 · 초안에 실리는 스냅 두 칸. */ export type SnapSetting = Pick; type SavedSnap = { station_snap?: boolean; station_snap_step?: number | null }; /** [측점 스냅] 단추 켬끔 모양(`is-active` · `aria-pressed`). */ export function paintStationSnap(button: HTMLButtonElement, on: boolean): void { button.classList.toggle("is-active", on); button.setAttribute("aria-pressed", String(on)); } /** 스냅 뒤 상태줄 글 — `step` = 고르기 값("0.1" · "1" · "10" · "station"). */ export function snapFitText(step: string): string { const where = step === "station" ? "일반측점" : `${step} m 배수`; return `곡선 가운데(MC)를 ${where}에 맞췄습니다.`; } export type SnapStep = { /** 지금 스냅 간격(m) — 측점 No. 면 측점 간격. */ (): number; /** 스냅 뒤 상태줄 글. */ text: () => string; /** 저장 · 초안 두 칸 — 끔이면 간격 null. */ draft: () => SnapSetting; /** 저장 노선을 읽음 — 켬이면 고르기를 그 간격으로 · 돌려줌 = 켬끔. */ load: (plan: SavedSnap) => boolean; /** 읽은 저장 노선과 켬끔 · 간격이 다른가([확인] 보낼지). */ changed: () => boolean; /** 편집 창 측점 — 끔이면 null · 켬이면 호 안 하나를 MC 로(자릿수 = 간격 · 셈은 서버 짝 `_Frame`). */ stations: (line: ReadonlyArray, curves: ReadonlyArray) => number[] | null; /** 평면 눈금 — `stations` + 곡선 측점(252-2) · 곡선 측점이 없으면 `stations` 그대로. */ ticks: ( line: ReadonlyArray, curves: ReadonlyArray, ) => { stations: number[] | null; curveStations: number[] }; }; /** 단추 · 고르기 배선 — `onChange(true)` = 단추 누름(켬끔 바꿈) · `onChange(false)` = 간격 바꿈. */ export function bindStationSnap( chrome: Pick, projectId: string, stationIntervalM: number, onChange: (toggle: boolean) => void, ): SnapStep { const select = chrome.overlay.querySelector('[data-act="snap-step"]')!; const saved = readState("routeedit-snap-step", projectId); select.value = saved && STEPS.includes(saved) ? saved : "station"; select.addEventListener("change", () => { writeState("routeedit-snap-step", select.value, projectId); onChange(false); }); chrome.stationSnapButton.addEventListener("click", () => onChange(true)); const on = () => chrome.stationSnapButton.getAttribute("aria-pressed") === "true"; const step = (): number | null => (select.value === "station" ? null : Number(select.value)); const draft = (): SnapSetting => ({ stationSnap: on(), stationSnapStep: on() ? step() : null }); let loaded: SnapSetting = { stationSnap: false, stationSnapStep: null }; const stations = (line: ReadonlyArray, curves: ReadonlyArray) => on() ? snapCurveStations( stationsAlong(line, stationIntervalM, false), curveSpans(line, curves), step(), ) : null; const stepM = () => step() ?? stationIntervalM; return Object.assign(stepM, { text: () => snapFitText(select.value), draft, load: (plan: SavedSnap) => { const snapOn = plan.station_snap === true; const kept = String(plan.station_snap_step ?? "station"); if (snapOn) select.value = STEPS.includes(kept) ? kept : "station"; loaded = { stationSnap: snapOn, stationSnapStep: snapOn ? step() : null }; return snapOn; }, changed: () => { const now = draft(); return ( now.stationSnap !== loaded.stationSnap || now.stationSnapStep !== loaded.stationSnapStep ); }, stations, ticks: (line: ReadonlyArray, curves: ReadonlyArray) => { const base = stations(line, curves) ?? stationsAlong(line, stationIntervalM, false); // 노선 길이 = 무한 — 호(BC~EC)는 늘 노선 안 const curve = curveStations(base, curveSpans(line, curves), Infinity, on() ? step() : null); return { stations: curve.length ? [...base, ...curve].sort((a, b) => a - b) : stations(line, curves), curveStations: curve, }; }, }); }