Files
Aislo/B05_Profile/logic/B05_Profile_ExpectedDraw.ts
T
eomsangdonandClaude Opus 5.5 84e9151b7c feat(B05 평면): 예정노선 그리기 — 새로 긋기 · 구간 다시 긋기 · 지움 → 평면선 다시 추적 = 편집기 한 걸음 → [저장] (PLAN 626-3 · 요구 530 · 531 · D3 · D4 · D9 · D10)
- 도구줄 「예정노선 그리기」 칸(공용 표 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>
2026-10-31 21:58:15 +09:00

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)]);
}