- 도구줄 「예정노선 그리기」 칸(공용 표 b05 · 연필 아이콘) → 떠 있는 판(단추 셋 + 공용 방식 칸) · 고치는 예정노선 맨 위 · 판 켠 동안 캔버스 누름을 먼저 받음(계획선 · 구조물 · 등고선 고르기 · Shift 재기 · 노드 넣기 · 지움 막음 · 가운데 끌기 · 휠 그대로) - 긋기 = D03 과 같은 방식 칸 · 부채꼴 · +/− 색 · Shift 사이 기울기 · Tab · Enter · Esc · Backspace(셈 `logic/B05_Profile_ExpectedDraw` · 그림 D03 `paintPreview` 그대로) - 끝 → 확인창(직접 고친 평면 버림) → `POST /route/follow`(저장 0) → `restoreSnapshot(…, true)` 한 걸음(되돌리기 됨) · 지움 = 파일입력 예정노선으로 다시 추적 - 걸음 사진 · 노선 초안에 `expected`(같은 선 판정 · 세션 되살림 · 페이지 되돌리기) · [저장] = 손 편집과 같은 길 + 초안 `expected`(null 안 실음 · [] 지움) · 다시 열면 `expected_edited` - `/route/replan` · `as_initial` · `expected_route.csv` · 초기값 · 추천 저장본 안 건드림 · 시험 test_626_3_expected_edit(7) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_ExpectedDraw.ts
|
|
* B05 평면 예정노선 그리기 셈(PLAN 626-3 · 요구 530-11 · 결정 D10) — 다음 점 고르기 · 구간 다시 긋기 갈음 · 선 위 자리.
|
|
* 다음 점 = D03 긋기와 같은 방식 넷(자유지정 · 길이 고정 · 기울기 고정 · 길이·기울기 고정 부채꼴 · Shift 사이 기울기) —
|
|
* 셈은 D03 `logic/D03_PlanAnalysis_Draw` · `logic/D03_PlanAnalysis_Fan` 그대로(파이썬 짝 없음 · 화면 고르기 전용).
|
|
* 구간 다시 긋기 = 선 위 두 자리(m) 사이를 그은 점으로 갈음(거꾸로 그었으면 뒤집어 끼움).
|
|
* 서버는 고친 예정노선을 다시 추적(`POST /route/follow`) · [저장] 때 `expected_route_edit.csv`.
|
|
* DOM · fetch · sessionStorage 없음.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
distanceAlong,
|
|
fixedLengthPoint,
|
|
fixedSlopePoint,
|
|
lineLength,
|
|
pointAtDistance,
|
|
segmentLength,
|
|
sliceLine,
|
|
type HeightAt,
|
|
type Vertex,
|
|
} from "../../D03_PlanAnalysis/logic/D03_PlanAnalysis_Draw";
|
|
import {
|
|
compassFan,
|
|
fanBetween,
|
|
fanPick,
|
|
type Fan,
|
|
} from "../../D03_PlanAnalysis/logic/D03_PlanAnalysis_Fan";
|
|
|
|
/** 긋기 방식 — 공용 방식 칸(`A00_Common/map/map_draw_kind` `DrawConstraint`)과 같은 꼴(logic 은 화면 파일을 안 부름). */
|
|
export type DrawConstraint = "free" | "length" | "slope" | "both";
|
|
|
|
/** 같은 점으로 볼 거리(m). */
|
|
const SAME_M = 1e-6;
|
|
/** 구간 다시 긋기 첫 구간 정방향 — 시작 자리 앞 이만큼(m) 선 위 점을 앞 구간 시작으로 봄. */
|
|
const LEAD_M = 5;
|
|
|
|
export interface ExpectedPick {
|
|
/** 찍을 점 — 없으면 null(자리 없음). */
|
|
point: Vertex | null;
|
|
/** 길이·기울기 고정 부채꼴(그 방식일 때만). */
|
|
fan: Fan | null;
|
|
note: string | null;
|
|
/** 길이만 지킨 점(Shift 사이 고르기 · 완만한 땅). */
|
|
lengthOnly: boolean;
|
|
}
|
|
|
|
const one = (value: number): string => value.toFixed(1);
|
|
|
|
/** 다음 점 — `prev` = 앞 구간 시작(정방향 · 없으면 null) · `shift` = 부채꼴 사이 고르기. */
|
|
export function pickNext(
|
|
kind: DrawConstraint,
|
|
from: Vertex,
|
|
cursor: Vertex,
|
|
lengthM: number,
|
|
slopePct: number,
|
|
heightAt: HeightAt,
|
|
prev: Vertex | null,
|
|
shift = false,
|
|
): ExpectedPick {
|
|
if (kind === "length")
|
|
return {
|
|
point: fixedLengthPoint(from, cursor, lengthM),
|
|
fan: null,
|
|
note: null,
|
|
lengthOnly: false,
|
|
};
|
|
if (kind === "slope") {
|
|
const point = fixedSlopePoint(from, cursor, slopePct, heightAt);
|
|
const note = point ? null : `이 방향엔 ±${one(Math.abs(slopePct))} % 자리 없음`;
|
|
return { point, fan: null, note, lengthOnly: false };
|
|
}
|
|
if (kind === "both") {
|
|
const fan = compassFan(from, lengthM, slopePct, heightAt, prev, cursor);
|
|
const point = shift
|
|
? fanBetween(fan, from, cursor, lengthM)
|
|
: fanPick(fan, from, cursor, lengthM);
|
|
const note = point
|
|
? null
|
|
: `반지름 ${one(lengthM)} m 원 위에 ±${one(Math.abs(slopePct))} % 자리 없음`;
|
|
return { point, fan, note, lengthOnly: shift || fan.flat };
|
|
}
|
|
return { point: cursor, fan: null, note: null, lengthOnly: false };
|
|
}
|
|
|
|
/** 선 위 가장 가까운 자리(누가 m · 점). */
|
|
export function nearestOnLine(
|
|
line: ReadonlyArray<Vertex>,
|
|
point: Vertex,
|
|
): { atM: number; point: Vertex } {
|
|
const atM = distanceAlong(line, point);
|
|
return { atM, point: pointAtDistance(line, atM) };
|
|
}
|
|
|
|
/** 구간 다시 긋기 첫 구간의 앞 구간 시작 — 선 위 시작 자리 조금 앞(시점이면 null). */
|
|
export function leadBefore(line: ReadonlyArray<Vertex>, atM: number): Vertex | null {
|
|
return atM > SAME_M ? pointAtDistance(line, Math.max(0, atM - LEAD_M)) : null;
|
|
}
|
|
|
|
/** 이웃한 같은 점 뺌. */
|
|
function dedupe(points: Vertex[]): Vertex[] {
|
|
return points.filter((point, i) => !i || segmentLength(points[i - 1], point) > SAME_M);
|
|
}
|
|
|
|
/** 구간 다시 긋기 — 선 `fromM` ~ `toM` 사이를 `through`(그은 속 점 · 시작 · 끝 자리 뺌)로 갈음. */
|
|
export function respliceLine(
|
|
line: ReadonlyArray<Vertex>,
|
|
fromM: number,
|
|
toM: number,
|
|
through: ReadonlyArray<Vertex>,
|
|
): Vertex[] {
|
|
const total = lineLength(line);
|
|
const forward = fromM <= toM;
|
|
const [lo, hi] = forward ? [fromM, toM] : [toM, fromM];
|
|
const middle = forward ? [...through] : [...through].reverse();
|
|
return dedupe([...sliceLine(line, 0, lo), ...middle, ...sliceLine(line, hi, total)]);
|
|
}
|