Files
Aislo/A00_Common/design/design_profile_range_band.ts
T
eomsangdonandClaude Opus 5.5 182de3b2e3 fix(461 B05 종단 직선화): 클릭 = 늘 측점 고름 · 끝 자동 R 지움 기본 L · 이미 직선 범위 부분 구간 · 실시간 범위 띠
- 직선화 첫 클릭이 직선 안이면 직선 통째 고름(붉은 강조)으로 새던 갈림 지움 · 빈 곳 클릭 측점선에 붙음
- 이미 직선인 범위 = 부분 구간 고름(보라 띠 · 안내) · ▲▼ = 가운데 + 양 끝 기본 L 곡선
- 직선화 · 자유지정 끝 R 이 저장 자동 R 이면 지움 = 끝 곡선 L 20 · 사용자 R 은 둠
- 법정 미달 판정 오차 1e-3 짝(TS · 파이썬) — L 19.99998 거짓 미달 없앰
- 새 도우미 design_profile_range_band — 직선화 · 자유지정 범위 단계 실시간 띠 · 시작–끝 · 길이 글
- Esc = 시작만 취소 · 모드 유지 · 고름 풀기로 안 샘
- 시험 test_271_straighten_pick (거울 포함)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:49:09 +09:00

150 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<string, string | number>): 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 };
}