fix(472 덧 B05 관 지움 ↶): 관 초안 없는 걸음 = 저장분 — 판이 마지막 저장분 답과 견줘 다르면 조용히 다시 읽음 · 옛 「처음 본 관 목록」(판이 읽기 전 빈 목록 · 관 하나 지움 ↶ 이 같은 목록으로 멈춤 → 배수판 · 시설 줄 · 알약 · 목록 관 없음 · −17,385.4 · B06 보기 배수유역도 관 · 세부유역 빠짐) 걷음 · 종단 ↶ ↷ 켜짐 = 사진첩 change 알림으로 바로(삭제 직후 꺼져 있던 흠) · 시험 test_472_d_b05_pipe_undo(옛 코드 빨강 2) · 옛 단언 215_1 이름 · 219_1 흉내 · 위키 operations_common · _b05 · rules 줄
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -333,6 +333,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
const fordFilled = fillFordHeights(response.pipe_points, response.basins, fordWaits());
|
||||
facilityStore.replaceFromResponse(response.pipe_points);
|
||||
if (fordFilled) rememberPipes();
|
||||
if (saved?.response === response) saved.request = JSON.stringify(pipeRequest());
|
||||
// 재계산 뒤에는 선택을 다시 알려야 한다 — 관을 새로 넣은 직후에는 목록이 아직
|
||||
// 비어 폼이 열리지 못했고, 중복 억제 가드에 걸려 두 번째 기회를 잃는다.
|
||||
lastNotifiedPipeChainage = null;
|
||||
@@ -368,6 +369,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
})),
|
||||
);
|
||||
|
||||
/** 마지막 저장분 답 · 그 답을 실은 뒤 요청 몸통 — 초안 없는 걸음으로 되돌릴 때 견줌(472 덧). */
|
||||
let saved: { response: DetailBasinResponse; request: string | null } | null = null;
|
||||
|
||||
/** 지금 화면의 관 목록을 세션 초안에 담는다 — 정본 쓰기는 [저장]·[확정] 몫이다. */
|
||||
function rememberPipes(): void {
|
||||
if (!projectId) return;
|
||||
@@ -380,7 +384,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
const pending = readPendingPipes(projectId as string);
|
||||
return pending !== null // 빈 목록 = 다 지움(관 0개 · 312) — 저장분으로 안 넘어감
|
||||
? computeDetailBasins(projectId as string, pending)
|
||||
: fetchDetailPipePoints(projectId as string);
|
||||
: fetchDetailPipePoints(projectId as string).then(
|
||||
(response) => ((saved = { response, request: null }), response),
|
||||
);
|
||||
};
|
||||
|
||||
/** 세부유역을 다시 나눈다(격자 해석 없음) — 같은 틱의 여러 부름은 한 번(342-3 · 옛 소수 관 옮김).
|
||||
@@ -529,6 +535,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
root,
|
||||
load(nextProjectId: string) {
|
||||
if (projectId === nextProjectId && meta) return;
|
||||
if (projectId !== nextProjectId) saved = null;
|
||||
projectId = nextProjectId;
|
||||
void loadLayers();
|
||||
},
|
||||
@@ -563,6 +570,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
selectBasin(picked ? picked.index : null, false);
|
||||
},
|
||||
setPipeChainages(chainages) {
|
||||
// 초안 없는 걸음 = 저장분 — 마지막 저장분 그대로면 끝 · 아니면 조용히 다시 읽음(472 덧)
|
||||
if (chainages === null)
|
||||
return void (JSON.stringify(pipeRequest()) !== saved?.request && run(loadSaved, true));
|
||||
const before = pipeEditor.pipes().map((pipe) => pipe.chainage_m);
|
||||
const next = reconcilePipes(pipeEditor.pipes(), chainages);
|
||||
if (!next) return; // 같은 목록 — 되돌아온 것이므로 여기서 끊는다
|
||||
|
||||
@@ -37,8 +37,9 @@ export interface DrainagePanel {
|
||||
/** 종단 테이블에서 배관 라인을 끌었을 때 — 그 자리로 옮기고 세부유역을 다시 나눈다. */
|
||||
movePipe: (fromChainage: number, toChainage: number) => void;
|
||||
/** 관 목록을 통째로 맞춘다(사이드바 구조물 폼 편집 등 밖에서 바뀐 경우).
|
||||
* 현재 목록과 같으면 아무 일도 하지 않는다 — 되먹임 고리를 끊는 지점이다. */
|
||||
setPipeChainages: (chainages: ReadonlyArray<number>) => void;
|
||||
* 현재 목록과 같으면 아무 일도 하지 않는다 — 되먹임 고리를 끊는 지점이다.
|
||||
* null = 초안 없는 걸음(되돌리기) — 저장분으로(472 덧). */
|
||||
setPipeChainages: (chainages: ReadonlyArray<number> | null) => void;
|
||||
/** 종단 테이블 우클릭·통합 목록에서 계곡 통과 시설을 넣을 때.
|
||||
* attributes에 시설 종류·구간·부속 옵션이 담긴다(생략 = 맨 배관). */
|
||||
addPipe: (chainageM: number, attributes?: FacilityAttributes) => void;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { adjustStation } from "./logic/B05_Profile_UI_Profile_Alignment";
|
||||
import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit";
|
||||
import {
|
||||
createProfileHistory,
|
||||
onPageHistory,
|
||||
setPageHistory,
|
||||
type ProfileHistory,
|
||||
} from "./B05_Profile_UI_Profile_History";
|
||||
@@ -63,8 +64,7 @@ export interface PanelToolsContext {
|
||||
moveStation: (station: IrregularStation, toChainageM: number) => void;
|
||||
/** 관 목록 정본 — 되돌리기가 구조물 위치까지 되돌리려면 이력이 이 값을 봐야 한다. */
|
||||
drainage: {
|
||||
pipeChainages: () => number[];
|
||||
setPipeChainages: (chainages: number[]) => void;
|
||||
setPipeChainages: (chainages: number[] | null) => void;
|
||||
};
|
||||
/** 세션 복원 후 화면을 다시 세운다(편집 초안 재적재 포함). */
|
||||
restore: () => void;
|
||||
@@ -83,38 +83,26 @@ export interface PanelTools {
|
||||
handleToolPick: (chainageM: number | null) => boolean;
|
||||
/** 창 키 듣기를 잇고 뗀다 — 종단도가 숨거나 떠나면 뗀다(계획서 24-3a). */
|
||||
setListening: (on: boolean) => void;
|
||||
/** 사진첩 알림을 뗀다 — 종단도를 버릴 때. */
|
||||
disposeTools: () => void;
|
||||
}
|
||||
|
||||
export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
/** 이력이 처음 본 관 목록 — 최초 스냅샷에는 아직 키가 없어 여기로 되돌린다. */
|
||||
let initialPipes: number[] | null = null;
|
||||
|
||||
/** 관 자리는 가게 관 초안(`pipes` · 사진에 듦)이 정본 — 찍기 직전엔 처음 본 목록만 잡아 둔다. */
|
||||
function syncPipes(): void {
|
||||
if (initialPipes === null) initialPipes = ctx.drainage.pipeChainages();
|
||||
}
|
||||
|
||||
/** 스냅샷을 되돌린 뒤 관 위치도 그 시점 초안으로 맞춘다(초안 없는 걸음 = 처음 목록). 같은 목록이면 조용히 끝난다. */
|
||||
/** 스냅샷을 되돌린 뒤 관 위치도 그 시점 초안으로 맞춘다 — 관 자리는 가게 관 초안(`pipes` · 사진에 듦)이
|
||||
* 정본 · 초안 없는 걸음 = 저장분(null · 옛 「처음 본 목록」은 판이 읽기 전 빈 목록이라 관 하나 지움 ↶ 이
|
||||
* 같은 목록으로 멈췄음 · 472 덧). 같은 목록이면 조용히 끝난다. */
|
||||
function restorePipes(): void {
|
||||
const draft = useDesignStore(draftProjectId(), null).get("pipes");
|
||||
const chainages = draft
|
||||
? draft.map((pipe) => Math.round(pipe.chainage_m * 100) / 100)
|
||||
: initialPipes;
|
||||
if (chainages) ctx.drainage.setPipeChainages(chainages);
|
||||
ctx.drainage.setPipeChainages(
|
||||
draft ? draft.map((pipe) => Math.round(pipe.chainage_m * 100) / 100) : null,
|
||||
);
|
||||
}
|
||||
|
||||
// 계획선 편집만 되돌리면 구조물 위치가 어긋난다(사용자 확정 ⑥ — 범위는 B05 조작 전부).
|
||||
/** 찍기 직전(세울 때 · 쉬는 동안 온 값 덮을 때 · 연속 조작 멈출 때) 처음 본 관 목록을 잡는다 —
|
||||
* 그래야 그 목록이 조작 **전** 값으로 잡혀 되돌리기가 시작 자리로 돌아온다(2026-09-04 실측). */
|
||||
const inner = createProfileHistory(
|
||||
() => {
|
||||
ctx.restore();
|
||||
restorePipes();
|
||||
},
|
||||
100,
|
||||
syncPipes,
|
||||
);
|
||||
const history: ProfileHistory = { ...inner, pause: () => (syncPipes(), inner.pause()) };
|
||||
const history = createProfileHistory(() => {
|
||||
ctx.restore();
|
||||
restorePipes();
|
||||
}, 100);
|
||||
setPageHistory(history); // 페이지 사진첩 하나 — 노선 편집기도 이것으로 쌓고 되돌림(346-4)
|
||||
|
||||
// [자유지정] — 적용은 직선화 결과와 같은 길(applyEdits · 이력 · 막기 규칙)(215-1).
|
||||
@@ -173,10 +161,10 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
},
|
||||
canResetToSaved: ctx.canRestoreSaved,
|
||||
// 자유지정 동안 ↶ ↷ = 자유지정 안 한 걸음(367-3) — 큰 이력은 안 건드림.
|
||||
onUndo: () => (tools.mode() === "free" ? (free.undo(), freeView.redraw()) : inner.undo()),
|
||||
onRedo: () => (tools.mode() === "free" ? (free.redo(), freeView.redraw()) : inner.redo()),
|
||||
canUndo: () => (tools.mode() === "free" ? free.canUndo() : inner.canUndo()),
|
||||
canRedo: () => (tools.mode() === "free" ? free.canRedo() : inner.canRedo()),
|
||||
onUndo: () => (tools.mode() === "free" ? (free.undo(), freeView.redraw()) : history.undo()),
|
||||
onRedo: () => (tools.mode() === "free" ? (free.redo(), freeView.redraw()) : history.redo()),
|
||||
canUndo: () => (tools.mode() === "free" ? free.canUndo() : history.canUndo()),
|
||||
canRedo: () => (tools.mode() === "free" ? free.canRedo() : history.canRedo()),
|
||||
hasStraightRuns: () => {
|
||||
const alignment = ctx.alignment();
|
||||
return alignment ? hasStraightRuns(alignment) : false;
|
||||
@@ -400,5 +388,7 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
window.addEventListener("keydown", onEscape, true);
|
||||
} else stopHold();
|
||||
};
|
||||
return { tools, history, handleToolPick, setListening };
|
||||
// 삭제 직후 등 다시 그리기 전에도 ↶ ↷ 켜짐을 맞춤(472 덧 — 종단 ↶ 가 꺼진 채 남던 흠)
|
||||
const disposeTools = onPageHistory({ change: () => tools.syncHistory() });
|
||||
return { tools, history, handleToolPick, setListening, disposeTools };
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface ProfileToolsCallbacks {
|
||||
export interface ProfileTools {
|
||||
/** 요약줄 맨 앞에 넣을 도구 묶음. 그릴 때마다 새로 만든다. */
|
||||
render: () => HTMLElement;
|
||||
/** 사진첩이 쌓이거나 되돌린 뒤 ↶ ↷ 켜짐만 맞춤 — 다시 그리기를 안 기다림(472 덧). */
|
||||
syncHistory: () => void;
|
||||
mode: () => ProfileToolMode;
|
||||
/** 그래프에서 측점을 눌렀을 때 — 도구가 삼켰으면 true. */
|
||||
handleStationPick: (chainageM: number) => boolean;
|
||||
@@ -88,6 +90,8 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
let mode: ProfileToolMode = "none";
|
||||
let pending: number | null = null;
|
||||
let runs: StraightRun[] = [];
|
||||
/** 마지막에 그린 ↶ ↷ — 다시 그리기 사이에도 켜짐을 맞춤. */
|
||||
let historyButtons: [HTMLButtonElement, HTMLButtonElement] | null = null;
|
||||
|
||||
function reset(): void {
|
||||
mode = "none";
|
||||
@@ -172,6 +176,11 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
callbacks.onChanged();
|
||||
return true;
|
||||
},
|
||||
syncHistory() {
|
||||
if (!historyButtons) return;
|
||||
historyButtons[0].disabled = !callbacks.canUndo();
|
||||
historyButtons[1].disabled = !callbacks.canRedo();
|
||||
},
|
||||
render() {
|
||||
const wrap = document.createElement("span");
|
||||
wrap.className = "b05-route-profile__tools";
|
||||
@@ -193,6 +202,7 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
undo.setAttribute("aria-label", "되돌리기");
|
||||
redo.innerHTML = HISTORY_ICON.redo;
|
||||
redo.setAttribute("aria-label", "다시하기");
|
||||
historyButtons = [undo, redo];
|
||||
|
||||
const straighten = toolButton(
|
||||
"직선화",
|
||||
|
||||
Reference in New Issue
Block a user