feat(D 예정노선): 거리재기 첫 점이 노선 위면 노선 따라 길이 (PLAN 600-2 · 요구 463-23)
- 새 순수 셈 logic/D03_PlanAnalysis_Measure — 같은 선 누가 차 · 갈래 고리 따라 올라가 만남 · 이어지지 않으면 곧은 거리 - 재기 겹 — 첫 누름 선 8 px · 꺾임점 12 px 안이면 노선 따라 · 굵게 그림 · 글 「노선 따라 L m · 측점 · Δh」 - Page.ts 한 줄(노선 넘김) · 새 시험 test_600_2_plan_measure Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,40 +2,72 @@
|
||||
* D03_PlanAnalysis_UI_Measure.ts
|
||||
* D03 거리재기 겹(PLAN 562-3) — 두 점을 누르면 그 사이 길이(m) · 원지반 기울기(%) · 높이 차(m).
|
||||
* 셋째 누름 = 새로 잼 · Esc = 지움. 지반고 = 받은 지형 `heightAt` · 셈 = 562-1 짝(`logic/D03_PlanAnalysis_Draw`).
|
||||
* 600-2 — 첫 누름이 노선 위면 「노선 따라」: 둘째 점 = 이어진 노선 위 가장 가까운 자리 · 길이 = 노선 따라
|
||||
* (`logic/D03_PlanAnalysis_Measure`) · 첫 누름이 선 밖이면 곧은 거리 그대로.
|
||||
* ========================================================================== */
|
||||
|
||||
import { groundSlopePct, segmentLength } from "./logic/D03_PlanAnalysis_Draw";
|
||||
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import {
|
||||
nearestOnRoute,
|
||||
routeSpan,
|
||||
type RoutePos,
|
||||
type RouteSpan,
|
||||
} from "./logic/D03_PlanAnalysis_Measure";
|
||||
import type { PlanNetwork, Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { planColors, type PlanMapLayer, type ScreenMap } from "./D03_PlanAnalysis_UI_Map";
|
||||
import { strokeCased } from "./D03_PlanAnalysis_UI_DrawPaint";
|
||||
import { lineAt, strokeCased } from "./D03_PlanAnalysis_UI_DrawPaint";
|
||||
|
||||
export interface PlanMeasure {
|
||||
layer: PlanMapLayer;
|
||||
/** 지움(Esc · 끔). */
|
||||
clear: () => void;
|
||||
/** 검증 창구 — 잰 값(두 점이 다 있으면). */
|
||||
result: () => { lengthM: number; slopePct: number | null; riseM: number | null } | null;
|
||||
/** 검증 창구 — 잰 값(두 점이 다 있으면) · `alongRoute` = 노선 따라 쟀나. */
|
||||
result: () => {
|
||||
lengthM: number;
|
||||
slopePct: number | null;
|
||||
riseM: number | null;
|
||||
alongRoute: boolean;
|
||||
} | 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 {
|
||||
export function createPlanMeasure(
|
||||
heightAt: () => (x: number, y: number) => number,
|
||||
network: () => PlanNetwork,
|
||||
): PlanMeasure {
|
||||
let from: Vertex | null = null;
|
||||
let to: Vertex | null = null;
|
||||
let fixed = false;
|
||||
/** 첫 누름이 노선 위였던 자리 · 둘째 점이 노선 위에 놓인 길. */
|
||||
let fromPos: RoutePos | null = null;
|
||||
let span: RouteSpan | null = null;
|
||||
|
||||
const measure = (a: Vertex, b: Vertex) => {
|
||||
const z = heightAt();
|
||||
const rise = z(b[0], b[1]) - z(a[0], a[1]);
|
||||
const lengthM = span ? span.lengthM : segmentLength(a, b);
|
||||
return {
|
||||
lengthM: segmentLength(a, b),
|
||||
slopePct: groundSlopePct(a, b, z),
|
||||
lengthM,
|
||||
slopePct: span
|
||||
? Number.isFinite(rise) && lengthM > 0
|
||||
? (rise / lengthM) * 100
|
||||
: null
|
||||
: groundSlopePct(a, b, z),
|
||||
riseM: Number.isFinite(rise) ? rise : null,
|
||||
alongRoute: !!span,
|
||||
};
|
||||
};
|
||||
|
||||
/** 둘째 점 놓기 — 노선 위 첫 점이면 이어진 노선 위 가까운 자리 · 아니면 누른 자리. */
|
||||
const place = (point: Vertex): void => {
|
||||
const near = fromPos ? nearestOnRoute(network(), point, fromPos) : null;
|
||||
span = near && fromPos ? routeSpan(network(), fromPos, near) : null;
|
||||
to = near && span ? near.point : point;
|
||||
};
|
||||
|
||||
const layer: PlanMapLayer = {
|
||||
draw(context, map: ScreenMap) {
|
||||
if (!from) return;
|
||||
@@ -50,18 +82,33 @@ export function createPlanMeasure(heightAt: () => (x: number, y: number) => numb
|
||||
context.strokeStyle = colors.casing;
|
||||
context.stroke();
|
||||
};
|
||||
context.lineCap = "round";
|
||||
context.lineJoin = "round";
|
||||
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]);
|
||||
if (span) {
|
||||
span.path.forEach((point, i) => {
|
||||
const [sx, sy] = map.toScreen(point);
|
||||
if (i) context.lineTo(sx, sy);
|
||||
else context.moveTo(sx, sy);
|
||||
});
|
||||
strokeCased(context, colors.measure, 4);
|
||||
} else {
|
||||
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 rise = riseM === null ? "지형 밖" : `Δh ${signed(riseM)} m`;
|
||||
const text = span
|
||||
? `노선 따라 ${fmt(lengthM)} m · ` +
|
||||
(span.stations ? `측점 ${fmt(span.stations[0])} ~ ${fmt(span.stations[1])} m · ` : "") +
|
||||
rise
|
||||
: `${fmt(lengthM)} m · ` +
|
||||
(slopePct === null ? "지형 밖" : `${signed(slopePct)} % · ${rise}`);
|
||||
const [mx, my] = [(ax + bx) / 2, (ay + by) / 2 - 14];
|
||||
map.upright(context, mx, my, () => {
|
||||
context.font = "12px system-ui, sans-serif";
|
||||
@@ -81,21 +128,26 @@ export function createPlanMeasure(heightAt: () => (x: number, y: number) => numb
|
||||
if (button !== 0) return false;
|
||||
const point = map.toMetric(px, py);
|
||||
if (!from || fixed) {
|
||||
[from, to, fixed] = [point, null, false];
|
||||
// 첫 누름 — 노선 위(선 8 px · 꺾임점 12 px)면 노선 따라 · 아니면 곧은 거리
|
||||
const net = network();
|
||||
const hit = lineAt(net, px, py, map);
|
||||
fromPos = hit ? { line: net.lines[hit.line].id, atM: hit.atM } : null;
|
||||
[from, to, fixed, span] = [hit ? hit.point : point, null, false, null];
|
||||
} else {
|
||||
[to, fixed] = [point, true];
|
||||
place(point);
|
||||
fixed = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
hover(px, py, map) {
|
||||
if (from && !fixed) to = map.toMetric(px, py);
|
||||
if (from && !fixed) place(map.toMetric(px, py));
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
layer,
|
||||
clear: () => {
|
||||
[from, to, fixed] = [null, null, false];
|
||||
[from, to, fixed, fromPos, span] = [null, null, false, null, null];
|
||||
},
|
||||
result: () => (from && to && fixed ? measure(from, to) : null),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user