Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_Draft.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

125 lines
5.3 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_RouteEdit_Draft.ts
* 노선 편집기 ↔ 계산 묶음 다리(PLAN 346-1) — 노선을 한 번 고칠 때마다(놓기 · 더함 · 지움 · 곡선 ·
* 되돌리기 · 다시하기 · 추천 [적용]) 노선 초안을 넘김. 끄는 동안은 넘기지 않음
* (사진첩에 쌓이는 때 = 손을 놓은 때).
*
* 편집기는 사진첩(`_History`)을 `watch` 로 감싸기만 함 — 고친 줄 하나. 저장 안 한 초안은
* 다리가 캐시 칸(`route-draft`)에 두고, 편집기가 자료를 읽은 뒤 `restore` 로 한 번 되살림
* (새로고침 · 보기 전환에도 남음). 계산 · 가게는 `A00_Common/design/design_pipeline_page.ts` 몫.
*
* 되돌리기 줄(346-4) — 페이지 사진첩(`_Profile_History`)이 있으면 쌓기 ·
* [되돌리기] · [다시하기]는 사진첩이 함(평면 · 종단 · 횡단 · 관 · 구조물 한 줄) · 사진첩이 되쓴 노선
* 초안은 `fromPage` 로 편집기 새 걸음이 됨. 사진첩이 없으면 편집기 사진 그대로.
* ========================================================================== */
import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types";
import { pageHistory } from "./B05_Profile_UI_Profile_History";
import {
lineChanged,
sameLine,
type RouteEditHistory,
type RouteEditSnapshot,
} from "./B05_Profile_UI_RouteEdit_History";
import type { SnapSetting } from "./B05_Profile_UI_RouteEdit_SnapStep";
/** 편집기가 넘기는 노선 — 판 번호 · 지형은 다리가 붙임. */
export type RouteDraftEdit = Omit<RouteDraft, "rev" | "terrain">;
export interface RouteDraftBridge {
/** 노선이 바뀜 — 열 때(저장분)와 같으면 null. */
edited(draft: RouteDraftEdit | null): void;
/** 저장 안 한 초안(캐시 칸) — 없으면 null. */
saved(): RouteDraft | null;
}
/** 사진 → 노선 초안(고정 · 고른 점은 노선이 아님) · 스냅 켬끔 · 간격(390-5)은 `_SnapStep` 몫. */
export function draftOfSnapshot(snapshot: RouteEditSnapshot, snap: SnapSetting): RouteDraftEdit {
return {
nodes: snapshot.planned.map(([x, y]): [number, number] => [x, y]),
curveOn: [...snapshot.curveOn],
curveRadius: [...snapshot.curveRadius],
curveLock: [...snapshot.curveLock],
curveArc: [...snapshot.curveArc],
...snap,
...(snapshot.expected
? { expected: snapshot.expected.map(([x, y]): [number, number] => [x, y]) }
: {}),
};
}
/** 노선 초안 → 사진 — 고정은 편집기가 좌표로 다시 짚음(`restoreSnapshot` 새 걸음). */
export function snapshotOfDraft(draft: RouteDraft): RouteEditSnapshot {
return {
planned: draft.nodes.map(([x, y]): [number, number] => [x, y]),
curveOn: [...draft.curveOn],
curveRadius: [...draft.curveRadius],
curveLock: [...draft.curveLock],
curveArc: [...draft.curveArc],
apexLock: draft.nodes.map(() => false),
picked: -1,
expected: draft.expected ?? null,
};
}
/** 사진첩을 감싸 노선이 바뀔 때마다 다리에 알림 — 쌓기 · 되돌리기 · 다시하기 · 열 때로.
* `changed` = 노선 밖 바뀜(스냅 · 분석 지형) — 노선이 처음으로 돌아가도 이것이면 초안(433-14 ②). */
export function watch(
history: RouteEditHistory,
bridge: RouteDraftBridge | undefined,
snap: () => SnapSetting,
changed: () => boolean = () => false,
): RouteEditHistory {
if (!bridge) return history;
const tell = (snapshot: RouteEditSnapshot | null): RouteEditSnapshot | null => {
if (snapshot)
bridge.edited(lineChanged(history) || changed() ? draftOfSnapshot(snapshot, snap()) : null);
return snapshot;
};
/** 사진첩이 되쓴 노선 초안 → 편집기 사진(같은 선의 옛 걸음이 있으면 고정 · 꼬리표째). */
const fromPage = (): RouteEditSnapshot | null => {
const kept = bridge.saved();
const opened = history.first?.();
const line = kept?.nodes.length ? snapshotOfDraft(kept) : opened;
const now = history.current?.();
if (!line || (now && sameLine(now, line))) return null;
const snapshot = history.match?.(line) ?? line;
history.commit(snapshot);
tell(snapshot);
return snapshot;
};
return {
...history,
commit: (snapshot) => {
history.commit(snapshot);
tell(snapshot);
pageHistory()?.record();
},
undo: () => {
const album = pageHistory();
if (!album) return tell(history.undo());
album.undo();
return null; // 편집기 되살림은 사진첩 restore → fromPage
},
redo: () => {
const album = pageHistory();
if (!album) return tell(history.redo());
album.redo();
return null;
},
canUndo: () => pageHistory()?.canUndo() ?? history.canUndo(),
canRedo: () => pageHistory()?.canRedo() ?? history.canRedo(),
fromPage,
};
}
/** 저장 안 한 초안을 편집기에 한 번 되살림 — 사진첩에 새 걸음으로(되돌리면 저장분). */
export function restore(
bridge: RouteDraftBridge | undefined,
apply: (snapshot: RouteEditSnapshot, message: string, record: boolean) => void,
): void {
const kept = bridge?.saved();
if (kept?.nodes.length)
apply(snapshotOfDraft(kept), "저장 안 한 노선 초안을 되살렸습니다.", true);
}