Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_UI_Measure.ts
T
eomsangdonandClaude Sonnet 5.5 26c5c0343d feat(D 예정노선): 지도 전용 그림 색 · 선마다 차례 색 · 검은 테 (PLAN 600-1 · 요구 463-24)
- 새 값 --draw-map-sketch · measure · warn · route-0~4 (테마 안 따름) · routeColor(index) 차례 색 함수
- planColors 가 지도 전용 색 + casing · 미리보기 · 거리재기 · 마커 · 핀 · 지점 검은 테 (strokeCased)
- 시험 551_2 에 지도 전용 색 대비(어두운 바탕 · 검은 테 ≥ 3 : 1) 덧

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:37:54 +09:00

103 lines
3.6 KiB
TypeScript

/* =============================================================================
* D03_PlanAnalysis_UI_Measure.ts
* D03 거리재기 겹(PLAN 562-3) — 두 점을 누르면 그 사이 길이(m) · 원지반 기울기(%) · 높이 차(m).
* 셋째 누름 = 새로 잼 · Esc = 지움. 지반고 = 받은 지형 `heightAt` · 셈 = 562-1 짝(`logic/D03_PlanAnalysis_Draw`).
* ========================================================================== */
import { groundSlopePct, segmentLength } from "./logic/D03_PlanAnalysis_Draw";
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
import { planColors, type PlanMapLayer, type ScreenMap } from "./D03_PlanAnalysis_UI_Map";
import { strokeCased } from "./D03_PlanAnalysis_UI_DrawPaint";
export interface PlanMeasure {
layer: PlanMapLayer;
/** 지움(Esc · 끔). */
clear: () => void;
/** 검증 창구 — 잰 값(두 점이 다 있으면). */
result: () => { lengthM: number; slopePct: number | null; riseM: number | null } | null;
}
const fmt = (value: number, digits = 1): string => value.toFixed(digits);
const signed = (value: number, digits = 1): string =>
`${value >= 0 ? "+" : ""}${fmt(value, digits)}`;
export function createPlanMeasure(heightAt: () => (x: number, y: number) => number): PlanMeasure {
let from: Vertex | null = null;
let to: Vertex | null = null;
let fixed = false;
const measure = (a: Vertex, b: Vertex) => {
const z = heightAt();
const rise = z(b[0], b[1]) - z(a[0], a[1]);
return {
lengthM: segmentLength(a, b),
slopePct: groundSlopePct(a, b, z),
riseM: Number.isFinite(rise) ? rise : null,
};
};
const layer: PlanMapLayer = {
draw(context, map: ScreenMap) {
if (!from) return;
const colors = planColors();
const [ax, ay] = map.toScreen(from);
const dot = (x: number, y: number): void => {
context.beginPath();
context.arc(x, y, 4.5, 0, Math.PI * 2);
context.fillStyle = colors.measure;
context.fill();
context.lineWidth = 1.5;
context.strokeStyle = colors.casing;
context.stroke();
};
if (to) {
const [bx, by] = map.toScreen(to);
context.beginPath();
context.moveTo(ax, ay);
context.lineTo(bx, by);
strokeCased(context, colors.measure, 2.5, [6, 4]);
dot(ax, ay);
dot(bx, by);
const { lengthM, slopePct, riseM } = measure(from, to);
const text =
`${fmt(lengthM)} m · ` +
(slopePct === null ? "지형 밖" : `${signed(slopePct)} % · Δh ${signed(riseM ?? 0)} m`);
const [mx, my] = [(ax + bx) / 2, (ay + by) / 2 - 14];
map.upright(context, mx, my, () => {
context.font = "12px system-ui, sans-serif";
context.textAlign = "center";
context.textBaseline = "middle";
context.lineWidth = 3;
context.strokeStyle = colors.halo;
context.strokeText(text, mx, my);
context.fillStyle = "#ffffff";
context.fillText(text, mx, my);
});
} else {
dot(ax, ay);
}
},
press(px, py, map, button) {
if (button !== 0) return false;
const point = map.toMetric(px, py);
if (!from || fixed) {
[from, to, fixed] = [point, null, false];
} else {
[to, fixed] = [point, true];
}
return true;
},
hover(px, py, map) {
if (from && !fixed) to = map.toMetric(px, py);
},
};
return {
layer,
clear: () => {
[from, to, fixed] = [null, null, false];
},
result: () => (from && to && fixed ? measure(from, to) : null),
};
}