/* ============================================================================= * A00_Common/design/design_profile_range_band.ts * 종단 범위 실시간 띠 — 시작 세로선을 고른 뒤 마우스 측점까지 띠 · 세로선 · 「시작–끝 · 길이」 글(271-2 · 3). * * [직선화]는 `createRangeBandView` 로 그래프에 붙이고, [자유지정]은 `design_profile_free_view` 가 * 같은 `appendRangeBand` 로 그린다 — 띠 한 벌. 마우스 자리는 가장 가까운 측점선에 붙음 · 그래프 밖 = * 시작선만 · 다시 들어오면 이어짐. 색 · 굵기는 자유지정 범위 띠(`b05-profile-freemark__*`)와 같음. * ========================================================================== */ import { snapStation } from "./logic/design_profile_free"; import type { FreeChartGeom } from "./design_profile_free_view"; const SVG_NS = "http://www.w3.org/2000/svg"; function svgEl(tag: string, attrs: Record): SVGElement { const element = document.createElementNS(SVG_NS, tag); Object.entries(attrs).forEach(([name, value]) => element.setAttribute(name, String(value))); return element; } /** 범위 띠를 붙인다 — 끝이 없거나 시작과 같으면 시작선만 · 반대 방향도 같은 띠. */ export function appendRangeBand( svg: SVGElement, x: (chainageM: number) => number, heightPx: number, startM: number, endM: number | null, ): void { const ends = endM === null || Math.abs(endM - startM) < 1e-6 ? [startM] : [Math.min(startM, endM), Math.max(startM, endM)]; if (ends.length === 2) { const [left, right] = ends.map(x); svg.append( svgEl("rect", { class: "b05-profile-freemark__band", x: left, y: 0, width: Math.max(0, right - left), height: heightPx, }), ); const label = svgEl("text", { class: "b05-profile-freemark__span", x: (left + right) / 2, y: 14, "text-anchor": "middle", }); label.textContent = `${ends[0].toFixed(1)}–${ends[1].toFixed(1)}m · ${(ends[1] - ends[0]).toFixed(1)}m`; svg.append(label); } ends.forEach((chainage) => svg.append( svgEl("line", { class: "b05-profile-freemark__edge", x1: x(chainage), x2: x(chainage), y1: 0, y2: heightPx, }), ), ); } export interface RangeBandView { /** 그래프를 새로 그렸거나 모드가 바뀜 — 켜져 있으면 붙이고, 꺼져 있으면 뗀다. */ mount: (geom: FreeChartGeom | null, active: boolean) => void; redraw: () => void; } /** 범위 띠 화면 — `source(마우스 측점)` 가 [시작, 끝 | null] 을 주면 그림 · null 이면 지움. */ export function createRangeBandView( source: (hoverM: number | null) => [number, number | null] | null, ): RangeBandView { let geom: FreeChartGeom | null = null; let active = false; let hover: number | null = null; /** 마지막 마우스 x(화면) — 스크롤 · 다시 그림 뒤에도 커서 밑 측점을 다시 잡는다. */ let clientX: number | null = null; let overlay: SVGElement | null = null; let listeners: AbortController | null = null; function redraw(): void { overlay?.remove(); overlay = null; const range = geom && active ? source(hover) : null; if (!geom || !range) return; const { widthPx, heightPx } = geom; overlay = svgEl("svg", { class: "b05-profile-freemark", width: widthPx, height: heightPx, viewBox: `0 0 ${widthPx} ${heightPx}`, }); appendRangeBand(overlay, geom.x, heightPx, range[0], range[1]); geom.chart.append(overlay); } /** 커서 밑 측점을 다시 잡는다 — 바뀌었을 때만 다시 그림. */ function track(): void { let next: number | null = null; if (geom && clientX !== null) { const at = geom.toChainage(clientX - geom.chart.getBoundingClientRect().left); next = Number.isFinite(at) ? snapStation(geom.stations, at) : null; } if (next === hover) return; hover = next; redraw(); } function mount(next: FreeChartGeom | null, on: boolean): void { listeners?.abort(); listeners = null; if (next) geom = next; active = on; if (!on) [hover, clientX] = [null, null]; if (!geom || !on || !geom.chart.isConnected) { overlay?.remove(); overlay = null; return; } listeners = new AbortController(); const { signal } = listeners; const chart = geom.chart; chart.addEventListener( "mousemove", (event) => { clientX = event.clientX; track(); }, { signal }, ); chart.addEventListener( "mouseleave", () => { clientX = null; track(); }, { signal }, ); geom.scroller.addEventListener("scroll", track, { signal, passive: true }); hover = null; track(); redraw(); } return { mount, redraw }; }