fix(B05·B06 한 페이지): 시설 옮김 = 횡단 편집 열쇠째 · [저장] 중 먹이기 멈춤 · 새 시설 측점 미리보기 걷음 · 되돌리기 뒤 다시 견줌 (415-1 · 415-2)
- H1: 관 · 구조물 밖 측점이 움직인 때만 「노선 바뀜」 넘김 규칙 · 시설만 옮기면 열쇠째 옮김 (사면 쉬프트 · 소단 깊이 · 단면 유형 · 관 측점 고침 그대로) - H1 덧: 초안 없을 때도 저장분 계획으로 열쇠 주인(관 = 저장 id · 계산 묶음 열쇠와 같음 · 첫 옮김도 열쇠째) · 묶음이 세운 열쇠 목록을 같은 측점이면 주인 규칙으로 다시 세지 않음 · 갈아끼운 옛 상세 맞춤 끝냄 - H2: [저장] 도중 관 · 구조물 먹이기 · 맞춤 고리 쉼 · 끝나면 저장 목록 다시 읽고 맞춤(design_pipeline_facilities) - M4: 새 시설 측점 서버 미리보기(preview-stations) 걷음 · 새 자리 = 계산 묶음 갈아끼움 · 카드 이름만 얹음 · 종류 바뀐 자리 = 구조물 없는 사본 - 415-2: 되돌리기 · 다시하기(세션만 되씀) 뒤 관 · 구조물을 묶음 입력과 견줘 다시 넣음 - 시험: test_415_page_split 새로 · 미리보기 전제 시험(273-2 · 298 · 317 · 318 · 322-0 · 322-3)을 새 규칙으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pipeline_facilities.ts
|
||||
* 계산 묶음 관 · 구조물 먹이기(PLAN 415) — 가게 관 · 구조물 초안이 바뀌면 묶음에 넣음(비면 저장분).
|
||||
* · [저장] 도중 = 안 넣음(H2) — 초안이 비는 사이 열 때 읽은 옛 저장분으로 돌지 않게 · 끝나면 `resync`
|
||||
* (저장 목록 다시 읽기 · 입력 맞춤) 한 번.
|
||||
* · 되돌리기 · 다시하기 = 세션만 되쓰고 가게 알림이 없음 → 묶음 입력과 견줘 다르면 넣음(415-2 · 노선 초안은
|
||||
* 노선 길이 맡음).
|
||||
* 저장 창구는 늦게 불러옴(묶음 시험이 화면 모듈을 안 끌게).
|
||||
* ========================================================================== */
|
||||
|
||||
import { onPageHistory } from "../../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import type { useDesignStore } from "./design_store";
|
||||
import type { ChangeSource } from "./logic/design_pipeline_types";
|
||||
|
||||
type Key = "pipes" | "structures";
|
||||
const FACILITIES: ReadonlyArray<{ source: ChangeSource; key: Key }> = [
|
||||
{ source: "pipe", key: "pipes" },
|
||||
{ source: "structure", key: "structures" },
|
||||
];
|
||||
|
||||
export interface FacilityFeed {
|
||||
saving(): boolean;
|
||||
/** 저장이 끝나면 `resync` 한 번 — 기다리는 동안 다시 불러도 한 번. */
|
||||
afterSave(): void;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
export function feedFacilities(o: {
|
||||
projectId: string;
|
||||
store: ReturnType<typeof useDesignStore>;
|
||||
/** 묶음이 받을 때인가(열었음 · 묶음이 가게에 쓰는 메아리 아님). */
|
||||
ready: () => boolean;
|
||||
gone: () => boolean;
|
||||
saved: (key: Key) => unknown;
|
||||
input: (key: Key) => unknown;
|
||||
/** 노선 초안이 걸렸나 — 되돌리기는 노선 길이 맡음. */
|
||||
routed: () => boolean;
|
||||
feed: (source: ChangeSource, value: unknown) => void;
|
||||
resync: () => Promise<unknown>;
|
||||
}): FacilityFeed {
|
||||
let savingOf: ((id: string) => boolean) | null = null;
|
||||
// 한 번 불러 봄 — 덜 선 모듈(시험 싣개의 고리)은 안 받음
|
||||
void import("./design_save")
|
||||
.then((m) => (m.savingDesign(o.projectId), (savingOf = m.savingDesign)))
|
||||
.catch(() => undefined);
|
||||
const saving = (): boolean => savingOf?.(o.projectId) ?? false;
|
||||
let waiting: Promise<void> | null = null;
|
||||
const afterSave = (): void => {
|
||||
waiting ??= (async () => {
|
||||
// ponytail: 100 ms 마다 물음 — 저장 끝 알림이 생기면 그것으로
|
||||
while (saving() && !o.gone()) await new Promise((done) => setTimeout(done, 100));
|
||||
if (!o.gone()) await o.resync();
|
||||
})().finally(() => (waiting = null));
|
||||
};
|
||||
const put = (source: ChangeSource, value: unknown): void =>
|
||||
saving() ? afterSave() : o.feed(source, value);
|
||||
const stops = [
|
||||
...FACILITIES.map(({ source, key }) =>
|
||||
o.store.subscribe(key, (value) => o.ready() && put(source, value ?? o.saved(key))),
|
||||
),
|
||||
onPageHistory({
|
||||
restore: () => {
|
||||
if (!o.ready() || o.routed()) return;
|
||||
for (const { source, key } of FACILITIES) {
|
||||
const now = o.store.get(key) ?? o.saved(key);
|
||||
if (JSON.stringify(now) !== JSON.stringify(o.input(key))) put(source, now);
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
return { saving, afterSave, stop: () => stops.forEach((stop) => stop()) };
|
||||
}
|
||||
@@ -66,8 +66,9 @@ import {
|
||||
} from "../../B05_Profile/B05_Profile_Api_Replan";
|
||||
import { createHaulStage } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
|
||||
import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { pagePipes, pageStructures, trackPageBusy } from "./design_page_data";
|
||||
import { forgetPageData, pagePipes, pageStructures, trackPageBusy } from "./design_page_data";
|
||||
import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types";
|
||||
import { feedFacilities } from "./design_pipeline_facilities";
|
||||
export type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types";
|
||||
import {
|
||||
groundStageDeps,
|
||||
@@ -569,6 +570,7 @@ function createPagePipeline(
|
||||
while (seen !== lastRun) await (seen = lastRun); // 기다리는 사이 뜬 차례까지
|
||||
await pipe.settled();
|
||||
if (disposed || canon?.routeId !== savedSource().routeId) return round; // 정본 바뀜 = restart 몫
|
||||
if (facilities.saving()) return (facilities.afterSave(), round); // 저장 도중 = 옛 저장분 · 끝나면 다시
|
||||
const now = inputsNow();
|
||||
const stale = (Object.entries(SOURCE_INPUT) as Array<[ChangeSource, keyof PipelineInputs]>)
|
||||
.filter(([, key]) => !same(now[key], pipe.input(key)) && !same(now[key], given[key]))
|
||||
@@ -617,13 +619,18 @@ function createPagePipeline(
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
const unsubscribe = [
|
||||
store.subscribe("pipes", (pipes) => started && !echo && void feed("pipe", pipes ?? savedPipes)),
|
||||
store.subscribe(
|
||||
"structures",
|
||||
(list) => started && !echo && void feed("structure", list ?? savedStructures),
|
||||
),
|
||||
];
|
||||
/** 관 · 구조물 먹이기 — [저장] 도중 멈춤(끝나면 저장 목록 다시 읽고 맞춤) · 되돌리기 뒤 다시 견줌(415). */
|
||||
const facilities = feedFacilities({
|
||||
projectId,
|
||||
store,
|
||||
ready: () => started && !echo,
|
||||
gone: () => disposed,
|
||||
saved: (key) => (key === "pipes" ? savedPipes : savedStructures),
|
||||
input: (key) => pipe.input(key),
|
||||
routed: () => pipe.input("route") !== null || store.get("routeDraft") !== null,
|
||||
feed: (source, value) => void feed(source, value),
|
||||
resync: async () => (forgetPageData(projectId), await loadPlan(), reconcile()),
|
||||
});
|
||||
|
||||
const bridge: RouteDraftBridge = {
|
||||
saved: () => store.get("routeDraft"),
|
||||
@@ -679,7 +686,7 @@ function createPagePipeline(
|
||||
detail: () => shown,
|
||||
dispose() {
|
||||
disposed = true;
|
||||
unsubscribe.forEach((stop) => stop());
|
||||
facilities.stop();
|
||||
pipe.dispose();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
type StateName,
|
||||
} from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { forgetPreviewAdjusts } from "./B06_Section_Section_Drafts";
|
||||
|
||||
/** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로.
|
||||
* 값은 수치가 대부분이나 날개벽 설치("있음"/"없음")처럼 문자열도 있다. */
|
||||
@@ -167,7 +166,6 @@ export function forgetStationAdjusts(
|
||||
if (hit !== undefined) delete marks[hit];
|
||||
if (next !== first) marks[chainageM.toFixed(2)] = first;
|
||||
writeState("kindreset", Object.keys(marks).length ? marks : null, projectId);
|
||||
forgetPreviewAdjusts(projectId, chainageM, STRUCTURE_ADJUST_KEYS);
|
||||
reloadStationAdjusts?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
*
|
||||
* · 옛 자리 구조물 측점(서버가 [저장] 때 다시 맞추기 전) = 숨김 — 쥐고 있다가 초안이 다시 그 자리를
|
||||
* 심거나 초안이 없어지면 돌려 놓음.
|
||||
* · 새 자리(서버 횡단 없음) = 서버가 미리 떠 준 횡단을 캐시에 얹음(`preview-stations` · 파일 · DB
|
||||
* 안 씀) — [저장] 때 서버가 같은 계산으로 정본에 씀. 한 번 뜬 자리는 기억해 다시 안 물음.
|
||||
* · 새 자리(서버 횡단 없음) = 계산 묶음 측점 갈아끼움이 얹음(브라우저 계산 줄 하나 · 415 M4) — 여기는 안 물음.
|
||||
*
|
||||
* 심는 자리 · 카드 이름 = 측점 계획 한 벌(`planStations` · 서버 `resolve_extra_stations` 짝 · 322-3) ·
|
||||
* 그림 자료 = 형상 얹기 한 벌(`withDraftShapes` → `attachShapes` · 서버 `attach_shapes` 짝 · 323-4). 일반 측점
|
||||
@@ -31,11 +30,7 @@ import {
|
||||
type PlannedStation,
|
||||
} from "../common_util/common_util_structure_station";
|
||||
import { useStationKeys, useStationOwners } from "./B06_Section_Station_Keys";
|
||||
import {
|
||||
previewStructureSections,
|
||||
type CrossSection,
|
||||
type SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { type CrossSection, type SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
|
||||
type Pipes = PendingPipes;
|
||||
|
||||
@@ -43,35 +38,19 @@ const near = (a: number, b: number): boolean => Math.abs(a - b) < SAME_STATION_M
|
||||
const byChainage = (a: { chainage_m: number }, b: { chainage_m: number }) =>
|
||||
a.chainage_m - b.chainage_m;
|
||||
|
||||
/** 미리 뜬 횡단(`프로젝트:노선:누가거리`) — 못 뜬 자리는 null(다시 안 물음) · 저장 뒤 비움. */
|
||||
const previews = new Map<string, CrossSection | null>();
|
||||
/** 초안이 빼 둔 옛 자리 측점. */
|
||||
const parked = new WeakMap<SectionDetailResponse, CrossSection[]>();
|
||||
/** 상세마다 마지막 맞춤 번호 — 늦게 끝난 옛 맞춤이 새 맞춤을 덮지 않게. */
|
||||
const turns = new WeakMap<SectionDetailResponse, number>();
|
||||
|
||||
/** 저장 · 노선 바뀜 뒤 — 페이지 저장분(자료 줄 · 386-2) · 미리 뜬 횡단을 버림. */
|
||||
export function forgetDraftStations(projectId: string): void {
|
||||
forgetPageData(projectId);
|
||||
for (const key of [...previews.keys()]) if (key.startsWith(`${projectId}:`)) previews.delete(key);
|
||||
/** 상세가 갈아끼워짐 — 옛 상세에 돌던 맞춤은 측점 열쇠 목록을 옛 측점으로 덮지 않게 끝냄(415 H1). */
|
||||
export function supersedeDraftStations(detail: SectionDetailResponse): void {
|
||||
turns.set(detail, (turns.get(detail) ?? 0) + 1);
|
||||
}
|
||||
|
||||
/** 종류 바꿈(318-4) — 그 자리 미리 뜬 횡단 설계에 얹혔던 조정값 사본을 걷음(조작이 캐시에 쓴 것). */
|
||||
export function forgetPreviewAdjusts(
|
||||
projectId: string,
|
||||
chainageM: number,
|
||||
keys: readonly string[],
|
||||
): void {
|
||||
for (const [key, section] of previews) {
|
||||
if (
|
||||
!key.startsWith(`${projectId}:`) ||
|
||||
!section?.design ||
|
||||
!near(section.chainage_m, chainageM)
|
||||
)
|
||||
continue;
|
||||
const design = section.design as unknown as Record<string, unknown>;
|
||||
for (const name of keys) delete design[name];
|
||||
}
|
||||
/** 저장 · 노선 바뀜 뒤 — 페이지 저장분(자료 줄 · 386-2)을 버림. */
|
||||
export function forgetDraftStations(projectId: string): void {
|
||||
forgetPageData(projectId);
|
||||
}
|
||||
|
||||
/** 저장된 관 · 구조물 — 한쪽 초안만 있을 때 심는 자리 셈에 씀(페이지 자료 줄 한 번 · 저장 뒤 다시). */
|
||||
@@ -81,20 +60,6 @@ const savedOf = (projectId: string): Promise<[Pipes, StructureInstance[]]> =>
|
||||
pageStructures(projectId).then((res) => res.structures),
|
||||
]);
|
||||
|
||||
async function previewOf(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
chainages: number[],
|
||||
): Promise<CrossSection[]> {
|
||||
const key = (at: number) => `${projectId}:${routeId}:${at.toFixed(3)}`;
|
||||
const ask = chainages.filter((at) => !previews.has(key(at)));
|
||||
if (ask.length) {
|
||||
const got = await previewStructureSections(projectId, ask);
|
||||
ask.forEach((at) => previews.set(key(at), got.find((s) => near(s.chainage_m, at)) ?? null));
|
||||
}
|
||||
return chainages.flatMap((at) => previews.get(key(at)) ?? []);
|
||||
}
|
||||
|
||||
/** 일반 측점 자리(grid · 272)의 「구조물 없는 꼴」(217) — 저장분에서 시설 · 이름만 뺀 사본(한 벌을 다시 씀 ·
|
||||
* 지형 쪽 고친 값은 그 측점 것 그대로) · 새 종류는 그 위에 얹음(`decorate`) · 측점은 남으므로 숨기지 않음. */
|
||||
const bares = new WeakMap<CrossSection, CrossSection>();
|
||||
@@ -109,7 +74,7 @@ function bareOf(section: CrossSection): CrossSection {
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** 미리 뜬 횡단 · 일반 측점 자리 사본에 카드 이름(그림 자료는 `withDraftShapes` 가 모든 측점에 한 번). */
|
||||
/** 구조물 없는 사본에 카드 이름(그림 자료는 `withDraftShapes` 가 모든 측점에 한 번). */
|
||||
function decorate(fresh: CrossSection[], planned: readonly PlannedStation[]): void {
|
||||
for (const section of fresh) {
|
||||
section.preview = true;
|
||||
@@ -156,9 +121,8 @@ export async function withDraftShapes(
|
||||
}
|
||||
|
||||
/**
|
||||
* 초안에 맞춰 상세의 횡단 목록을 고친다 — 옛 자리 숨김 · 새 자리 미리 뜬 횡단 얹음. 초안이 없으면
|
||||
* 서버 목록 그대로(빼 둔 것 돌려 놓음). 미리 뜨기를 못 하면(샘플링 조건 없음 · 끊김) 새 자리는
|
||||
* 종전처럼 [저장] 뒤에 섬.
|
||||
* 초안에 맞춰 상세의 횡단 목록을 고친다 — 옛 자리 숨김 · 종류 바뀐 자리 구조물 없는 사본. 초안이 없으면
|
||||
* 서버 목록 그대로(빼 둔 것 돌려 놓음). 새 자리는 계산 묶음 측점 갈아끼움이 얹음(415 M4).
|
||||
*/
|
||||
export async function withDraftStations(
|
||||
detail: SectionDetailResponse,
|
||||
@@ -175,9 +139,19 @@ export async function withDraftStations(
|
||||
].sort(byChainage);
|
||||
let live = all;
|
||||
let stale: CrossSection[] = [];
|
||||
let fresh: CrossSection[] = [];
|
||||
// 추가 측점 초안(368-2)도 측점 계획을 바꿈 — 계획은 그 초안을 스스로 읽음
|
||||
if (pipesDraft || structuresDraft || readPlusDraft(projectId)) {
|
||||
if (!(pipesDraft || structuresDraft || readPlusDraft(projectId))) {
|
||||
// 초안 없음 = 저장분 계획으로 열쇠 주인만 — 관 = 저장 id(계산 묶음 열쇠와 같음 · 첫 옮김도 열쇠째 · 415 H1)
|
||||
const inputs = await statusOf(projectId, null, null);
|
||||
if (inputs) {
|
||||
const [pipes, structures, types] = inputs;
|
||||
useStationOwners(
|
||||
projectId,
|
||||
planStations(pipes, structures, types, registryDefaults(types)),
|
||||
pipes,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const inputs = await statusOf(projectId, pipesDraft, structuresDraft);
|
||||
if (inputs) {
|
||||
const [pipes, structures, types] = inputs;
|
||||
@@ -185,7 +159,7 @@ export async function withDraftStations(
|
||||
useStationOwners(projectId, planned, pipes); // 측점 열쇠 = 덮는 구조물 id(344-3) · 관 = pipe:
|
||||
const planted = planned.map((station) => station.chainage_m);
|
||||
// 구조물 측점에서 종류를 바꿈 = 「구조물 없는 상태」부터 새 종류로(요구 213) — 저장분 횡단은 빼 두고
|
||||
// 새 자리처럼 미리 뜬 횡단을 씀 · 저장분 종류로 되돌리면 빼 둔 횡단이 돌아옴
|
||||
// 구조물 없는 사본을 씀 · 저장분 종류로 되돌리면 빼 둔 횡단이 돌아옴
|
||||
const [oldPipes, oldStructures] = await savedOf(projectId).catch(
|
||||
(): [Pipes, StructureInstance[]] => [[], []],
|
||||
);
|
||||
@@ -221,25 +195,28 @@ export async function withDraftStations(
|
||||
planned.some((station) => near(station.chainage_m, section.chainage_m)),
|
||||
);
|
||||
stale = [...all.filter(isStale), ...retitled];
|
||||
const bare = [...all.filter(isStale).filter((section) => section.grid), ...retitled].map(
|
||||
bareOf,
|
||||
);
|
||||
const bare = [
|
||||
// 종류만 바뀐 자리(측점이 남음) — 시설이 떠난 자리는 숨김만
|
||||
...all.filter(
|
||||
(section) =>
|
||||
isStale(section) &&
|
||||
replaced(section) &&
|
||||
(section.grid || planted.some((at) => near(at, section.chainage_m))),
|
||||
),
|
||||
...retitled,
|
||||
].map(bareOf);
|
||||
live = [...all.filter((section) => !stale.includes(section)), ...bare];
|
||||
const missing = planted.filter(
|
||||
(at, index) =>
|
||||
planted.findIndex((other) => near(other, at)) === index &&
|
||||
live.every((section) => !near(section.chainage_m, at)),
|
||||
);
|
||||
fresh = missing.length ? await previewOf(projectId, routeId, missing).catch(() => []) : [];
|
||||
if (fresh.length || bare.length) decorate([...fresh, ...bare], planned);
|
||||
if (bare.length) decorate(bare, planned);
|
||||
// 계산 묶음 갈아끼움이 얹은 새 자리 = 카드 이름만(415 M4)
|
||||
for (const section of live)
|
||||
if (section.kind === "irregular" && !section.structure) {
|
||||
const hit = planned.find((station) => near(station.chainage_m, section.chainage_m));
|
||||
if (hit) section.structure = hit.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (turns.get(detail) !== turn) return; // 더 새 맞춤이 돎 — 그쪽이 얹음
|
||||
detail.cross_sections.splice(
|
||||
0,
|
||||
detail.cross_sections.length,
|
||||
...[...live, ...fresh].sort(byChainage),
|
||||
);
|
||||
detail.cross_sections.splice(0, detail.cross_sections.length, ...live.sort(byChainage));
|
||||
parked.set(detail, stale);
|
||||
useStationKeys(projectId, routeId, detail); // 세션 횡단 초안 = 이 측점 목록의 열쇠(344-3)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { setGeneralStations, setPlusStations } from "../common_util/common_util_
|
||||
import { loadSavedPlusStations, plusStationsOf } from "../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import {
|
||||
forgetDraftStations,
|
||||
supersedeDraftStations,
|
||||
withDraftShapes,
|
||||
withDraftStations,
|
||||
} from "./B06_Section_Section_Drafts";
|
||||
@@ -170,6 +171,7 @@ export async function swapSectionStations(
|
||||
else useStationKeyList(projectId, routeId, nextKeys);
|
||||
// 메모리만 — 세션 상세 = 저장 정본만(387-7 · 새로고침 · B06 바로 열기 = 묶음이 다시 갈아끼움 · 365-1)
|
||||
cache.set(key, swap.detail);
|
||||
supersedeDraftStations(current); // 옛 상세에 돌던 맞춤이 새 열쇠 목록을 덮지 않게(415 H1)
|
||||
await withDraftShapes(swap.detail, projectId); // 새 측점에도 구조물 그림 자료(323-4)
|
||||
withStructureAreas(swap.detail);
|
||||
if (cache.get(key) !== swap.detail) return null; // 그 사이 다른 갈아끼움 · 새로 받음
|
||||
|
||||
@@ -150,16 +150,28 @@ export function stationIntervalOf(detail: KeyedDetail): number {
|
||||
);
|
||||
}
|
||||
|
||||
/** 그대로 세운 목록(계산 묶음 열쇠)이 있는 노선 — 같은 측점이면 주인 규칙으로 다시 세지 않음(415 H1). */
|
||||
const given = new Set<string>();
|
||||
|
||||
/** 상세가 측점을 다 얹었다 — 이 노선의 지금 측점 열쇠를 세움(읽기 · 쓰기가 이 목록으로 옮김). */
|
||||
export function useStationKeys(projectId: string, routeId: number, detail: KeyedDetail): void {
|
||||
const list = stationKeysOf(projectId, detail.cross_sections, stationIntervalOf(detail));
|
||||
lists.set(scopeOf(projectId, routeId), list);
|
||||
const scope = scopeOf(projectId, routeId);
|
||||
const now = lists.get(scope);
|
||||
const sections = detail.cross_sections;
|
||||
current = { projectId, routeId };
|
||||
// 옮긴 관의 주인 자리가 아직 옛 자리여도 묶음 열쇠(pipe:<저장 id>)를 세션 id 로 덮지 않음
|
||||
const same =
|
||||
now?.length === sections.length &&
|
||||
sections.every((section, i) => Math.abs(now[i].chainageM - section.chainage_m) < NEAR_M);
|
||||
if (given.has(scope) && same) return;
|
||||
given.delete(scope);
|
||||
lists.set(scope, stationKeysOf(projectId, sections, stationIntervalOf(detail)));
|
||||
}
|
||||
|
||||
/** 열쇠 목록을 그대로 세움 — 계산 묶음 측점(`PipelineStation.key`)이 정한 열쇠(346-2 4파). */
|
||||
export function useStationKeyList(projectId: string, routeId: number, list: Keyed[]): void {
|
||||
lists.set(scopeOf(projectId, routeId), list);
|
||||
given.add(scopeOf(projectId, routeId));
|
||||
current = { projectId, routeId };
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export interface StationSwap {
|
||||
removed: string[];
|
||||
/** 이어 받은 측점 중 누가거리가 바뀐 것 — 옛 0.01 m 글 → 새 글(조작값 맵 옮김). */
|
||||
moves: Map<string, string>;
|
||||
/** 이어 받은 측점 중 좌표가 0.01 m 넘게 움직인 것이 있나 — 노선이 바뀐 차례(「노선 밖 고침」 셈). */
|
||||
/** 이어 받은 관 · 구조물 밖 측점 중 좌표가 0.01 m 넘게 움직인 것이 있나 — 노선이 바뀐 차례(「노선 밖 고침」 셈). */
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,13 @@ export function swapCrossSections(
|
||||
const prior = priorOf(station);
|
||||
if (prior) used.add(prior);
|
||||
else added.push(station.key);
|
||||
if (prior && Math.hypot(prior.center_x - station.x, prior.center_y - station.y) > 0.01)
|
||||
// 노선이 바뀜 = 관 · 구조물 밖 측점이 움직임 — 시설만 옮기면 고침을 열쇠째 옮김(415 H1)
|
||||
if (
|
||||
prior &&
|
||||
station.kind !== "pipe" &&
|
||||
station.kind !== "structure" &&
|
||||
Math.hypot(prior.center_x - station.x, prior.center_y - station.y) > 0.01
|
||||
)
|
||||
moved = true;
|
||||
if (prior && text(prior.chainage_m) !== text(station.chainageM))
|
||||
moves.set(text(prior.chainage_m), text(station.chainageM));
|
||||
|
||||
@@ -111,14 +111,18 @@ const chainages = (detail) => detail.cross_sections.map((s) => s.chainage_m);
|
||||
draftPipes = [{ chainage_m: 95, facility: "pipe", source: "user" }]; // 95.38 → 95
|
||||
await drafts.withDraftStations(detail, "P", 1);
|
||||
await drafts.withDraftShapes(detail, "P"); // 그림 자료(323-4 · `Section_Store` 차례)
|
||||
const preview = detail.cross_sections.find((s) => s.chainage_m === 95);
|
||||
out.moved = {
|
||||
out.moved = { chainages: chainages(detail), asked: asked.splice(0) };
|
||||
// 새 자리 = 계산 묶음 갈아끼움이 얹은 측점(415 M4) — 다음 맞춤이 그대로 둠 · 관 그림
|
||||
detail.cross_sections.push({ chainage_m: 95, kind: "irregular", station_id: "s95", samples: [] });
|
||||
detail.cross_sections.sort((a, b) => a.chainage_m - b.chainage_m);
|
||||
await drafts.withDraftStations(detail, "P", 1);
|
||||
await drafts.withDraftShapes(detail, "P");
|
||||
const swapped = detail.cross_sections.find((s) => s.chainage_m === 95);
|
||||
out.again = {
|
||||
chainages: chainages(detail),
|
||||
asked: asked.splice(0),
|
||||
preview: { preview: preview.preview, structure: preview.structure, culvert: preview.culvert },
|
||||
swapped: { preview: !!swapped?.preview, culvert: !!swapped?.culvert },
|
||||
};
|
||||
await drafts.withDraftStations(detail, "P", 1);
|
||||
out.again = { chainages: chainages(detail), asked: asked.splice(0) };
|
||||
draftPipes = null;
|
||||
await drafts.withDraftStations(detail, "P", 1);
|
||||
out.cleared = { chainages: chainages(detail) };
|
||||
|
||||
@@ -472,6 +472,8 @@ const out = {};
|
||||
|
||||
// ── 5. 미리 뜬 횡단 사본 걷음(318-4) — 그 자리 조정값만 · 다른 칸 그대로 ─────────
|
||||
const state = { draftPipes: null, savedPipes: [] };
|
||||
/** 서버 미리보기 요청(415 M4 — 0 이어야 함). */
|
||||
const asked = [];
|
||||
const loadDrafts = makeLoader(
|
||||
[
|
||||
B06("B06_Section_Section_Drafts"),
|
||||
@@ -499,7 +501,7 @@ const loadDrafts = makeLoader(
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => state.draftPipes },
|
||||
"./B06_Section_Api_Fetch": {
|
||||
previewStructureSections: async (_p, chainages) =>
|
||||
chainages.map((at) => ({
|
||||
(asked.push(chainages), chainages).map((at) => ({
|
||||
chainage_m: at,
|
||||
kind: "irregular",
|
||||
design: { ground_type: "soil", ford_adjust: { w: 1 }, revet_adjust: { inlet: { x: 1 } } },
|
||||
@@ -524,15 +526,16 @@ const loadStore = makeLoader(
|
||||
);
|
||||
|
||||
(async () => {
|
||||
// 415 M4 — 새 자리 서버 미리보기 걷음 · 조정값 사본도 없음(새 자리는 계산 묶음 갈아끼움이 얹음)
|
||||
const { withDraftStations, forgetPreviewAdjusts } = loadDrafts(B06("B06_Section_Section_Drafts"));
|
||||
const detail = { cross_sections: [{ chainage_m: 0, kind: "regular", design: {} }] };
|
||||
state.draftPipes = [{ chainage_m: 50, facility: "box_culvert" }];
|
||||
await withDraftStations(detail, "P", 7);
|
||||
const at50 = () => detail.cross_sections.find((s) => Math.abs(s.chainage_m - 50) < 0.01);
|
||||
const had = Object.keys(at50()?.design ?? {}).sort();
|
||||
forgetPreviewAdjusts("P", 50, ["ford_adjust", "revet_adjust"]);
|
||||
forgetPreviewAdjusts("OTHER", 0, ["ground_type"]);
|
||||
out.preview = { had, after: Object.keys(at50()?.design ?? {}).sort() };
|
||||
out.preview = {
|
||||
asked: asked.splice(0),
|
||||
chainages: detail.cross_sections.map((s) => s.chainage_m),
|
||||
forget: typeof forgetPreviewAdjusts,
|
||||
};
|
||||
|
||||
const { crossPatchesFromCache } = loadStore(B06("B06_Section_Section_Store"));
|
||||
out.cache_patches = crossPatchesFromCache(
|
||||
|
||||
@@ -235,6 +235,26 @@ const { pickGroups } = loadTs(B05("B05_Profile_UI_Structures_Panel_Types"));
|
||||
const { structureDisplayName } = loadTs(B06("B06_Section_UI_Section_Common"));
|
||||
const { listPick } = loadTs(file("common_util", "common_util_structure_catalog.ts"));
|
||||
|
||||
/** 415 M4 — 새 자리 = 계산 묶음 측점 갈아끼움 흉내(측점 계획 자리 중 상세에 없는 것 · 서버 안 물음). */
|
||||
function swapIn(detail, types) {
|
||||
const plan = loadTs(file("common_util", "common_util_structure_station.ts"));
|
||||
const { registryDefaults } = loadTs(file("common_util", "common_util_culvert_sets.ts"));
|
||||
const structures = JSON.parse(
|
||||
window.sessionStorage.getItem(`aislo:draft:structures:${PROJECT}`) ?? "null",
|
||||
);
|
||||
const planned = plan.planStations(
|
||||
pendingPipes ?? [],
|
||||
structures ?? [],
|
||||
types,
|
||||
registryDefaults(types),
|
||||
);
|
||||
const near = (a, b) => Math.abs(a - b) < plan.SAME_STATION_M;
|
||||
for (const { chainage_m: at } of planned)
|
||||
if (detail.cross_sections.every((s) => !near(s.chainage_m, at)))
|
||||
detail.cross_sections.push({ ...plainAt(at), kind: "irregular" });
|
||||
detail.cross_sections.sort((a, b) => a.chainage_m - b.chainage_m);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const types = await fetchStructureTypes(true);
|
||||
const byId = new Map(types.map((type) => [type.type_id, type]));
|
||||
@@ -267,7 +287,8 @@ async function main() {
|
||||
forgetDraftStations(PROJECT);
|
||||
pendingPipes = sample.pipe ? [clone(sample.pipe)] : null;
|
||||
writePendingStructures(PROJECT, sample.structure ? [clone(sample.structure)] : null);
|
||||
const detail = await loadSectionDetail(PROJECT, route, { force: true });
|
||||
swapIn(await loadSectionDetail(PROJECT, route, { force: true }), types);
|
||||
const detail = await loadSectionDetail(PROJECT, route); // 갈아끼움 뒤 다시 그림 = 캐시 다시 맞춤
|
||||
const touched = detail.cross_sections.filter(
|
||||
(section) => section.structure || Object.keys(shapeOf(section)).length,
|
||||
);
|
||||
|
||||
@@ -128,10 +128,29 @@ async function run(setup, detail) {
|
||||
drafts.forgetDraftStations("P");
|
||||
route += 1;
|
||||
await drafts.withDraftStations(detail, "P", route);
|
||||
swapIn(detail);
|
||||
await drafts.withDraftStations(detail, "P", route); // 갈아끼움 뒤 다시 그림 = 다시 맞춤
|
||||
await drafts.withDraftShapes(detail, "P"); // 그림 자료 = 형상 얹기 한 벌(323-4)
|
||||
return summary(detail);
|
||||
}
|
||||
|
||||
/** 415 M4 — 새 자리 = 계산 묶음 측점 갈아끼움 흉내(측점 계획 자리 중 상세에 없는 것 · 기본 설계 · 서버 안 물음). */
|
||||
function swapIn(detail) {
|
||||
const plan = load(file("common_util", "common_util_structure_station.ts"));
|
||||
const { registryDefaults } = load(file("common_util", "common_util_culvert_sets.ts"));
|
||||
const planned = plan.planStations(
|
||||
state.draftPipes ?? state.savedPipes,
|
||||
state.draftStructures ?? state.savedStructures,
|
||||
TYPES,
|
||||
registryDefaults(TYPES),
|
||||
);
|
||||
const near = (a, b) => Math.abs(a - b) < plan.SAME_STATION_M;
|
||||
for (const { chainage_m: at } of planned)
|
||||
if (detail.cross_sections.every((s) => !near(s.chainage_m, at)))
|
||||
detail.cross_sections.push({ chainage_m: at, kind: "irregular", station_id: `p${at}` });
|
||||
detail.cross_sections.sort((a, b) => a.chainage_m - b.chainage_m);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
out.place = await run({ draftStructures: [wall("retaining_wall")] }, { cross_sections: grid() });
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
/* 415 H2 헬퍼 — [저장] 도중 계산 묶음 먹이기 멈춤 · 끝나면 저장 목록 다시 읽음. 진짜 TS(`design_pipeline_page` ·
|
||||
* 단계 파일 전부)를 트랜스파일해 Node 로 돌림 · 서버 창구 · 저장 중 표시만 가짜(틀 = `helper_365_page_pipeline.cjs`).
|
||||
* 입력 argv[2] JSON · argv[3] 격자 이진 · 결과 = 표준출력 JSON. 짝 = `test_415_page_split.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
|
||||
function memoryStorage() {
|
||||
const map = new Map();
|
||||
return {
|
||||
getItem: (key) => (map.has(key) ? map.get(key) : null),
|
||||
setItem: (key, value) => map.set(key, String(value)),
|
||||
removeItem: (key) => map.delete(key),
|
||||
key: (index) => [...map.keys()][index] ?? null,
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
globalThis.window = {
|
||||
sessionStorage: memoryStorage(),
|
||||
localStorage: memoryStorage(),
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
};
|
||||
|
||||
// 흙 배분 서버 — 늦게 답함 · 끊기면 버림
|
||||
const haul = { calls: 0, aborted: 0, answered: 0 };
|
||||
globalThis.fetch = (url, init) =>
|
||||
new Promise((resolve, reject) => {
|
||||
haul.calls += 1;
|
||||
const timer = setTimeout(() => {
|
||||
haul.answered += 1;
|
||||
resolve({ ok: true, json: async () => ({ haul_plan: { blocks: [], url } }) });
|
||||
}, job.haulDelayMs);
|
||||
init.signal.addEventListener("abort", () => {
|
||||
clearTimeout(timer);
|
||||
haul.aborted += 1;
|
||||
reject(new Error("aborted"));
|
||||
});
|
||||
});
|
||||
|
||||
const STUBS = {};
|
||||
const cache = new Map();
|
||||
function loadTs(full) {
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
||||
const key = Object.keys(STUBS).find((name) => request.endsWith(name));
|
||||
if (key) return STUBS[key];
|
||||
if (request.startsWith("@config/")) return { API_BASE_URL: "http://fake" };
|
||||
if (request.startsWith("@util/"))
|
||||
return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`));
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
const load = (...parts) => loadTs(path.join(ROOT, ...parts));
|
||||
|
||||
const sampler = load("common_util", "common_util_surface_sampler.ts");
|
||||
const bytes = fs.readFileSync(process.argv[3]);
|
||||
const grid = sampler.parseDtmGrid(
|
||||
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.length),
|
||||
);
|
||||
const local = { grid, design: "las" };
|
||||
|
||||
STUBS["B05_Profile_Api_Replan"] = {
|
||||
analysisTerrain: () => "las",
|
||||
fetchRoutePlan: async () => ({
|
||||
planned: job.line,
|
||||
curves: [],
|
||||
min_radius_m: job.minRadius,
|
||||
station_snap: false,
|
||||
}),
|
||||
};
|
||||
STUBS["B05_Profile_Api_Structures"] = {
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
fetchStructureTypes: async () => job.types,
|
||||
};
|
||||
STUBS["B05_Profile_Api_TerrainGrid"] = {
|
||||
loadDesignTerrainGrid: async () => local,
|
||||
groundStageDeps: () => ({ grid: () => local, remote: async () => [] }),
|
||||
};
|
||||
/** 서버 관 저장분 — [저장] 뒤 바뀜 · 읽은 횟수(저장 뒤 다시 읽음). */
|
||||
let serverPipes = job.pipes;
|
||||
let pipeReads = 0;
|
||||
STUBS["B04_PreProcess_Api_Fetch"] = {
|
||||
fetchDetailPipePoints: async () => {
|
||||
pipeReads += 1;
|
||||
return { pipe_points: serverPipes.map((pipe) => ({ ...pipe, lonlat: [0, 0] })) };
|
||||
},
|
||||
};
|
||||
/** [저장] 도중 — 묶음이 늦게 불러오는 저장 창구 가짜(415 H2). */
|
||||
let saving = false;
|
||||
STUBS["design_save"] = { savingDesign: () => saving };
|
||||
STUBS["B06_Section_UI_Standard_Panel"] = {
|
||||
effectiveStandardCross: () => job.standard,
|
||||
readRockBoundaryDefault: () => -0.5,
|
||||
};
|
||||
|
||||
const pipeMod = load("A00_Common", "design", "logic", "design_pipeline.ts");
|
||||
const pageMod = load("A00_Common", "design", "design_pipeline_page.ts");
|
||||
const stationsMod = load("A00_Common", "design", "logic", "design_pipeline_stations.ts");
|
||||
const routeGround = load("B05_Profile", "logic", "B05_Profile_Pipeline_RouteGround.ts");
|
||||
const grade = load("B05_Profile", "logic", "B05_Profile_Pipeline_Grade.ts");
|
||||
const gradeGround = load("B05_Profile", "logic", "B05_Profile_Grade_Ground.ts");
|
||||
const detailMod = load("B05_Profile", "logic", "B05_Profile_Pipeline_Detail.ts");
|
||||
const alignment = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
|
||||
const editMod = load("B05_Profile", "B05_Profile_UI_Profile_Edit.ts");
|
||||
const page = load("A00_Common", "b_page_state.ts");
|
||||
|
||||
const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const runs = () => globalThis.__aisloPipeline?.runs ?? [];
|
||||
async function settle(count) {
|
||||
for (let i = 0; i < 400; i += 1) {
|
||||
const last = runs()[runs().length - 1];
|
||||
if (runs().length >= count && last && last.outcome !== "running") return last;
|
||||
await tick(5);
|
||||
}
|
||||
throw new Error("묶음이 끝나지 않음");
|
||||
}
|
||||
const n = job.nodes.length;
|
||||
const draftOf = (nodes) => ({
|
||||
nodes,
|
||||
curveOn: Array(n).fill(true),
|
||||
curveRadius: Array(n).fill(null),
|
||||
curveLock: Array(n).fill(null),
|
||||
curveArc: Array(n).fill(null),
|
||||
stationSnap: false,
|
||||
});
|
||||
const shift = (index, dx, dy) =>
|
||||
job.nodes.map((node, i) => (i === index ? [node[0] + dx, node[1] + dy] : node));
|
||||
|
||||
/** 저장 기준 — 저장 노선 지반 첫 계획선(관 측점 포함 · 서버 정책 그대로). */
|
||||
async function savedBaseOf() {
|
||||
const seed = pipeMod.createDesignPipeline({
|
||||
stages: {
|
||||
route: routeGround.createRouteStage({
|
||||
minRadiusM: () => job.minRadius,
|
||||
savedLine: () => ({ line: job.line, curves: [] }),
|
||||
}),
|
||||
stations: stationsMod.createStationsStage({
|
||||
intervalM: job.interval,
|
||||
types: () => job.types,
|
||||
}),
|
||||
ground: routeGround.createGroundStage({ grid: () => local, remote: async () => [] }),
|
||||
},
|
||||
});
|
||||
await seed.start({ route: null, alignment: null, cross: null, pipes: job.pipes, structures: [] });
|
||||
const ground = seed.output("ground");
|
||||
seed.dispose();
|
||||
const samples = ground.longitudinal.chainageM.map((c, i) => ({
|
||||
chainage_m: c,
|
||||
elevation_m: ground.longitudinal.z[i],
|
||||
}));
|
||||
const profile = gradeGround.groundProfile(samples);
|
||||
return {
|
||||
policy: job.policy,
|
||||
onlyExplicitCurves: false,
|
||||
basePvi: gradeGround.groundFollowingBasePvi({
|
||||
samples,
|
||||
stations: ground.stations.map((s) => s.chainageM),
|
||||
policy: job.policy,
|
||||
}),
|
||||
chainage: profile.chainage,
|
||||
ground: profile.ground,
|
||||
stations: ground.stations.map((s) => ({
|
||||
station_id: grade.stationIdOf(s.chainageM),
|
||||
chainage_m: s.chainageM,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const store = load("A00_Common", "design", "design_store.ts");
|
||||
|
||||
const pipeAt = () =>
|
||||
pageMod
|
||||
.pagePipeline(P)
|
||||
.input("pipes")
|
||||
.map((pipe) => pipe.chainage_m);
|
||||
const stationAt = () =>
|
||||
(shop.get("stations") ?? []).filter((s) => s.kind === "pipe").map((s) => s.chainageM);
|
||||
const P = "proj-415";
|
||||
const shop = store.useDesignStore(P, null);
|
||||
|
||||
async function run() {
|
||||
window.localStorage.setItem("frd_current_project_id", P);
|
||||
const savedBase = await savedBaseOf();
|
||||
const link = {
|
||||
pipelineSource: () => ({
|
||||
base: savedBase,
|
||||
edits: alignment.emptyEdits(),
|
||||
detail: job.detail,
|
||||
routeId: 7,
|
||||
}),
|
||||
adoptPipeline: () => undefined,
|
||||
redraw: () => undefined,
|
||||
};
|
||||
pageMod.routeDraftBridge(P, link, { stationIntervalM: job.interval });
|
||||
pageMod.pipelineEarthwork(P, { conversion: job.conversion });
|
||||
await settle(runs().length + 1);
|
||||
const out = { open: pipeAt() };
|
||||
|
||||
// ① 관 5 m 옮김(저장 전 초안) — 묶음이 새 자리로
|
||||
const moved = job.pipes.map((pipe) => ({ ...pipe, chainage_m: pipe.chainage_m + 5 }));
|
||||
shop.set("pipes", moved);
|
||||
await settle(runs().length + 1);
|
||||
out.moved = { input: pipeAt(), stations: stationAt() };
|
||||
|
||||
// ② [저장] 도중 — 관 차례가 서버에 쓰고 초안을 비움 · 맞춤 고리도 불림
|
||||
saving = true;
|
||||
serverPipes = moved;
|
||||
const mark = runs().length;
|
||||
const reads = pipeReads;
|
||||
shop.set("pipes", null);
|
||||
const rounds = await pageMod.reconcilePagePipeline(P);
|
||||
await tick(150);
|
||||
out.during = {
|
||||
runs: runs().length - mark,
|
||||
rounds,
|
||||
input: pipeAt(),
|
||||
stations: stationAt(),
|
||||
reads: pipeReads - reads,
|
||||
};
|
||||
|
||||
// ③ 저장 끝 — 저장 목록 다시 읽고 맞춤(같은 자리 = 다시 안 셈)
|
||||
saving = false;
|
||||
await tick(400);
|
||||
await pageMod.pagePipeline(P).settled();
|
||||
out.after = {
|
||||
runs: runs().length - mark,
|
||||
input: pipeAt(),
|
||||
stations: stationAt(),
|
||||
reads: pipeReads - reads,
|
||||
sources: runs()
|
||||
.slice(mark)
|
||||
.map((r) => r.sources),
|
||||
};
|
||||
// ④ 저장 뒤 맞춤 고리 — 저장분(새 자리) 대 묶음 입력 같음 = 0 바퀴
|
||||
out.reconcile = await pageMod.reconcilePagePipeline(P);
|
||||
|
||||
// ⑤ 관 옮김 → 되돌리기(415-2) — 세션만 되쓰는 사진첩 · 묶음 입력 · 측점이 원래로 · 다시하기 = 옮긴 자리
|
||||
const history = load("B05_Profile", "B05_Profile_UI_Profile_History.ts");
|
||||
const book = history.createProfileHistory(() => undefined);
|
||||
const again = moved.map((pipe) => ({ ...pipe, chainage_m: pipe.chainage_m + 5 }));
|
||||
shop.set("pipes", again);
|
||||
await settle(runs().length + 1);
|
||||
book.record();
|
||||
const movedAgain = { input: pipeAt(), stations: stationAt() };
|
||||
book.undo();
|
||||
await tick(50);
|
||||
await pageMod.pagePipeline(P).settled();
|
||||
const undone = { input: pipeAt(), stations: stationAt(), draft: shop.get("pipes") };
|
||||
book.redo();
|
||||
await tick(50);
|
||||
await pageMod.pagePipeline(P).settled();
|
||||
out.undo = { movedAgain, undone, redone: { input: pipeAt(), stations: stationAt() } };
|
||||
pageMod.disposePagePipeline(P);
|
||||
return out;
|
||||
}
|
||||
|
||||
run()
|
||||
.then((out) => process.stdout.write(JSON.stringify(out)))
|
||||
.catch((error) => {
|
||||
process.stderr.write(String(error?.stack ?? error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/* 415 H1 헬퍼 — 관만 옮김 = 횡단 고침을 측점 열쇠째 옮김 · 노선이 바뀜 = 「노선 바뀜」 넘김 규칙.
|
||||
* node helper_415_swap.mjs <묶음.mjs>
|
||||
* 묶음 = 346-2 진입점(`helper_346_2_swap_entry.ts` · vite ssr 진짜 모듈) · 표준출력 JSON 한 벌. 짝 = test_415_page_split.py.
|
||||
*/
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const store = () => {
|
||||
const m = new Map();
|
||||
return {
|
||||
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||
setItem: (k, v) => m.set(k, String(v)),
|
||||
removeItem: (k) => m.delete(k),
|
||||
key: (i) => [...m.keys()][i] ?? null,
|
||||
get length() {
|
||||
return m.size;
|
||||
},
|
||||
clear: () => m.clear(),
|
||||
};
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.sessionStorage = store();
|
||||
globalThis.localStorage = store();
|
||||
const any = new Proxy(function () {}, {
|
||||
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
||||
apply: () => any,
|
||||
construct: () => any,
|
||||
});
|
||||
globalThis.document = any;
|
||||
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
globalThis.__toasts = [];
|
||||
|
||||
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||
const P = "p415";
|
||||
const R = 3;
|
||||
const out = {};
|
||||
|
||||
/* 옛 목록 — 시점 · 격자 넷 · 관 하나(45.054) · 종점(간격 20 m) */
|
||||
const section = (chainage_m, kind, extra = {}) => ({
|
||||
station_id: `station_${String(Math.round(chainage_m * 1000)).padStart(12, "0")}`,
|
||||
chainage_m,
|
||||
label: "",
|
||||
kind,
|
||||
center_z: 100,
|
||||
azimuth_deg: 90,
|
||||
center_x: chainage_m,
|
||||
center_y: 0,
|
||||
frame: { left_xy: [0, 1] },
|
||||
samples: [{ offset_m: 0, elevation_m: 100, valid: true }],
|
||||
design: { design_elevation_m: 101 },
|
||||
...extra,
|
||||
});
|
||||
const detail = () => ({
|
||||
longitudinal: {
|
||||
length_m: 100,
|
||||
stations: [],
|
||||
profile_alignment: { policy: { station_interval_m: 20 } },
|
||||
},
|
||||
cross_sections: [
|
||||
section(0, "bp"),
|
||||
section(20, "regular"),
|
||||
section(40, "regular"),
|
||||
section(45.054, "irregular", { structure: "파형강관 D800", culvert: {} }),
|
||||
section(60, "regular"),
|
||||
section(80, "regular"),
|
||||
section(100, "ep"),
|
||||
],
|
||||
});
|
||||
const station = (key, chainageM, kind, id = null, dy = 0) => ({
|
||||
key,
|
||||
chainageM,
|
||||
x: chainageM,
|
||||
y: dy,
|
||||
azimuthDeg: 90,
|
||||
left: [0, 1],
|
||||
kind,
|
||||
id,
|
||||
ground: { centerZ: 99, left: [{ offsetM: 0.5, z: 99.1 }], right: [{ offsetM: 0.5, z: 98.9 }] },
|
||||
planRadiusM: null,
|
||||
curveOuterSide: null,
|
||||
curveWideningM: null,
|
||||
});
|
||||
/** 관 자리 `pipeAt` · 노선 옆 밀림 `dy`(0 = 노선 그대로) · 종점 `endAt`. */
|
||||
const list = (pipeAt, dy = 0, endAt = 100) =>
|
||||
[
|
||||
station("end:BP", 0, "end", null, dy),
|
||||
station("grid:1", 20, "grid", null, dy),
|
||||
station("grid:2", 40, "grid", null, dy),
|
||||
station("pipe:pipe-s1", pipeAt, "pipe", "pipe-s1", dy),
|
||||
station("grid:3", 60, "grid", null, dy),
|
||||
station("grid:4", 80, "grid", null, dy),
|
||||
station("end:EP", endAt, "end", null, dy),
|
||||
].sort((a, b) => a.chainageM - b.chainageM);
|
||||
|
||||
const choice = (mode) => ({ ground_type: "soil", section_mode: mode, ditch_side: "right" });
|
||||
const snap = () => ({
|
||||
choices: [...m.crossDesignChoices(P, R).entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([at, c]) => [at, c.ground_type ?? null, c.section_mode ?? null]),
|
||||
shoulder: Object.entries(m.readState("shoulderext", P, R) ?? {})
|
||||
.filter(([k]) => k !== "@at")
|
||||
.map(([k, v]) => [k, v]),
|
||||
berm: Object.entries(m.readState("bermdepth", P, R) ?? {})
|
||||
.filter(([k]) => k !== "@at")
|
||||
.map(([k, v]) => [k, v]),
|
||||
});
|
||||
|
||||
/* 1. 순수 — 관만 옮김 = moved 아님 · 노선이 옆으로 밀림 = moved */
|
||||
{
|
||||
const keys = ["end:BP", "grid:1", "grid:2", "pipe:pipe-s1", "grid:3", "grid:4", "end:EP"];
|
||||
out.pure = {
|
||||
pipeOnly: m.swapCrossSections(detail(), list(50), keys, 20).moved,
|
||||
route: m.swapCrossSections(detail(), list(50, 0.5, 95), keys, 20).moved,
|
||||
};
|
||||
}
|
||||
|
||||
/* 2. 가게 측점 → 상세 갈아끼움 · 세션 고침 */
|
||||
{
|
||||
m.writeState("section-detail", detail(), P, R);
|
||||
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 45.054 }] }, P);
|
||||
await m.loadSectionDetail(P, R); // 세션 한 벌 → 캐시 · 측점 열쇠 세움
|
||||
m.writeCrossDesignChoice(P, R, 40, choice("right_cut")); // 단면 유형
|
||||
m.writeCrossDesignChoice(P, R, 45.054, choice("left_cut")); // 관 측점 고침
|
||||
m.writeState("shoulderext", { "@at": { "grid:1": 20 }, "grid:1": { left: 0.7 } }, P, R); // 사면 쉬프트
|
||||
m.writeState("bermdepth", { "@at": { "grid:3": 60 }, "grid:3": { right: 0.4 } }, P, R); // 소단 깊이
|
||||
out.before = snap();
|
||||
const stop = m.followSwaps(
|
||||
P,
|
||||
() => R,
|
||||
{ visible: () => true },
|
||||
async () => undefined,
|
||||
);
|
||||
// 관 5 m 옮김(노선 그대로) — 관 자리 장부도 새 자리
|
||||
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 50 }] }, P);
|
||||
m.useDesignStore(P, null).set("stations", list(50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
out.pipeMoved = { ...snap(), toasts: globalThis.__toasts.length };
|
||||
// 관 한 번 더(10 m) — 둘째 옮김도 그대로(첫 옮김 뒤 관 id 를 앎)
|
||||
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 55 }] }, P);
|
||||
m.useDesignStore(P, null).set("stations", list(55));
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
out.pipeMovedAgain = snap();
|
||||
// 노선이 바뀜(옆으로 0.5 m · 종점 95 m) = 넘김 규칙 — 단면 유형 · 사면 쉬프트 · 소단 깊이 버림 · 지반 유형 넘김
|
||||
m.useDesignStore(P, null).set("stations", list(55, 0.5, 95));
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
out.routeMoved = snap();
|
||||
stop();
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -3,8 +3,8 @@
|
||||
- 미리 뜨기(`sample_structure_sections`)는 파일을 안 쓰고, [저장] 때 정본 쓰기
|
||||
(`generate_irregular_sections`)와 같은 횡단을 낸다(같은 길 · 화면이 본 값 = 저장 값).
|
||||
- 기본 설계(`compute_default_designs`)는 파일 대신 받은 횡단으로도 셈.
|
||||
- 화면 쪽: 옛 자리 숨김 · 새 자리 얹음 · 이름 · 관 그림 · 늦게 끝난 옛 맞춤이 새 것을 안 덮음
|
||||
(`helper_273_2_preview_stations.cjs`).
|
||||
- 화면 쪽: 옛 자리 숨김 · 새 자리는 안 물음(415 M4 · 계산 묶음 갈아끼움이 얹음) · 관 그림 · 늦게 끝난 옛
|
||||
맞춤이 새 것을 안 덮음(`helper_273_2_preview_stations.cjs`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -111,22 +111,21 @@ def ts() -> dict:
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_화면_옛_자리_숨기고_새_자리_얹음(ts) -> None:
|
||||
assert ts["moved"]["chainages"] == [0, 20, 40, 95, 100] # 옛 95.38 숨김 · 새 95 미리 뜬 것
|
||||
assert ts["moved"]["asked"] == [[95]]
|
||||
card = ts["moved"]["preview"]
|
||||
assert card["preview"] is True and card["structure"] == "파형강관 D800"
|
||||
assert card["culvert"] is True # 관 그림(세트)도 얹힘
|
||||
def test_화면_옛_자리_숨기고_새_자리_안_물음(ts) -> None:
|
||||
# 415 M4 — 새 자리 서버 미리보기 걷음(브라우저 계산 줄 하나) · 옛 95.38 숨김 · 미리보기 요청 0
|
||||
assert ts["moved"]["chainages"] == [0, 20, 40, 100]
|
||||
assert ts["moved"]["asked"] == []
|
||||
|
||||
|
||||
def test_한_번_뜬_자리는_다시_안_물음(ts) -> None:
|
||||
def test_묶음이_얹은_새_자리는_그대로_관_그림(ts) -> None:
|
||||
assert ts["again"]["asked"] == []
|
||||
assert ts["again"]["chainages"] == [0, 20, 40, 95, 100]
|
||||
assert ts["again"]["swapped"] == {"preview": False, "culvert": True}
|
||||
|
||||
|
||||
def test_초안이_없으면_서버_목록_그대로(ts) -> None:
|
||||
assert ts["cleared"]["chainages"] == [0, 20, 40, 95.38, 100]
|
||||
assert ts["cleared"]["chainages"] == [0, 20, 40, 95, 95.38, 100]
|
||||
|
||||
|
||||
def test_늦게_끝난_옛_맞춤이_새_것을_안_덮음(ts) -> None:
|
||||
assert ts["race"]["chainages"] == [0, 20, 40, 100, 120]
|
||||
assert ts["race"]["chainages"] == [0, 20, 40, 100]
|
||||
|
||||
@@ -159,7 +159,7 @@ def test_추가_뒤_새_측점_횡단_미리_뜸():
|
||||
drafts, "export async function withDraftStations(", "parked.set(detail, stale);"
|
||||
)
|
||||
assert "readPendingPipes(projectId)" in stations
|
||||
assert "previewOf(projectId, routeId, missing)" in stations
|
||||
assert "previewStructureSections" not in drafts # 415 M4 — 새 자리는 계산 묶음 갈아끼움이 얹음
|
||||
|
||||
|
||||
def test_B06_알림_글_뒤에서_다시_나눔():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""PLAN 317 — 구조물 측점에서 종류를 바꾸면 횡단을 「구조물 없는 상태」부터 새로(요구 213).
|
||||
|
||||
- 저장분과 같은 종류 = 저장분 횡단 그대로
|
||||
- 다른 종류로 바꿈 = 저장분 횡단은 빼 두고 새 자리처럼 미리 뜬 횡단(옛 구조물 흔적 없음)
|
||||
- 다른 종류로 바꿈 = 저장분 횡단은 빼 두고 구조물 없는 사본(옛 구조물 흔적 없음 · 415 M4 = 서버 미리보기 안 물음)
|
||||
- 저장분 종류로 되돌림 = 빼 둔 저장분 횡단이 돌아옴
|
||||
|
||||
실제 TS(`withDraftStations`)를 `helper_317_kind_change_station.cjs` 가 Node 로 돌린다(서버 부르는 곳 흉내).
|
||||
@@ -42,7 +42,8 @@ def test_same_kind_keeps_saved_section(result: dict) -> None:
|
||||
|
||||
|
||||
def test_changed_kind_starts_from_fresh_section(result: dict) -> None:
|
||||
assert result["changed_kind"] == {"count": 2, "fresh": True, "old": False, "preview": True}
|
||||
# 415 M4 — 서버 미리보기(fresh) 대신 구조물 없는 사본 · 설계는 브라우저 다시 셈이 맡음
|
||||
assert result["changed_kind"] == {"count": 2, "fresh": False, "old": True, "preview": True}
|
||||
|
||||
|
||||
def test_back_to_saved_kind_restores_saved_section(result: dict) -> None:
|
||||
|
||||
@@ -183,7 +183,7 @@ def test_forget_session_adjusts_at_station(result: dict) -> None:
|
||||
assert forget["basinadjust"] == {} and forget["inletstruct"] == {}
|
||||
assert forget["crossw"] == {"100.00": 5} # 구조물 아닌 값(표시 반폭)은 그대로
|
||||
assert forget["reloads"] == 1 # 횡단 보기 메모리 사본 다시 읽음
|
||||
assert forget["preview_calls"][0] == ["P", 100, list(STRUCTURE_ADJUST_KEYS)]
|
||||
assert forget["preview_calls"] == [] # 415 M4 — 미리 뜬 횡단 사본 없음
|
||||
|
||||
|
||||
def test_kind_reset_marks(result: dict) -> None:
|
||||
@@ -196,8 +196,8 @@ def test_kind_reset_marks(result: dict) -> None:
|
||||
|
||||
|
||||
def test_preview_copy_stripped(result: dict) -> None:
|
||||
assert result["preview"]["had"] == ["ford_adjust", "ground_type", "revet_adjust"]
|
||||
assert result["preview"]["after"] == ["ground_type"]
|
||||
# 415 M4 — 새 자리 서버 미리보기 요청 0 · 사본 없음
|
||||
assert result["preview"] == {"asked": [], "chainages": [0], "forget": "undefined"}
|
||||
|
||||
|
||||
def test_b05_save_path_patches(result: dict) -> None:
|
||||
|
||||
@@ -39,10 +39,11 @@ def _marked(rows: list) -> list:
|
||||
|
||||
|
||||
def test_place(flows: dict) -> None:
|
||||
# 415 M4 — 새 자리 = 계산 묶음 갈아끼움(미리 뜬 카드 아님 · 서버 안 물음) · 이름 그대로
|
||||
assert _marked(flows["place"]) == [
|
||||
[93, "옹벽", True, []],
|
||||
[101, "옹벽", True, []],
|
||||
[113, "옹벽", True, []],
|
||||
[93, "옹벽", False, []],
|
||||
[101, "옹벽", False, []],
|
||||
[113, "옹벽", False, []],
|
||||
]
|
||||
|
||||
|
||||
@@ -56,7 +57,7 @@ def test_place_on_general_station_gets_name_and_shape(flows: dict) -> None:
|
||||
|
||||
|
||||
def test_move_hides_old_and_adds_new(flows: dict) -> None:
|
||||
assert _marked(flows["move"]) == [[75, "파형강관 D1000", True, ["culvert"]]]
|
||||
assert _marked(flows["move"]) == [[75, "파형강관 D1000", False, ["culvert"]]] # 415 M4
|
||||
assert all(row[0] != 61 for row in flows["move"])
|
||||
|
||||
|
||||
@@ -108,6 +109,7 @@ def test_saved_decimal_stations_follow_plan(flows: dict) -> None:
|
||||
# 소수 자리 저장분 = B05 를 먼저 연 것과 같음 — 소수 카드는 빼 두고 정수 자리(격자 자리는 사본)
|
||||
rows = flows["snapSavedDec"]
|
||||
assert [row[0] for row in _marked(rows)] == SNAPPED
|
||||
assert all(row[2] for row in _marked(rows))
|
||||
# 격자 자리 = 구조물 없는 사본(미리 뜬 카드) · 나머지 = 계산 묶음 갈아끼움(415 M4)
|
||||
assert all(row[2] == (row[0] % 20 == 0) for row in _marked(rows))
|
||||
assert all(float(row[0]).is_integer() for row in rows)
|
||||
assert len(rows) == 58
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 415 — 한 페이지 끊김 ① (분석 412 3장 H1 · H2 · M4).
|
||||
|
||||
못박는 것
|
||||
- H1 관 · 구조물만 옮김 = 「노선 바뀜」 넘김 규칙 안 탐 — 측점 열쇠째 옮김(단면 유형 · 사면 쉬프트 ·
|
||||
소단 깊이 · 관 측점 고침 그대로 · 두 번 옮겨도) · 노선 입력이 바뀐 때만 넘김 규칙(대조).
|
||||
- H2 [저장] 도중 계산 줄 먹이기 멈춤 — 관 초안이 비어도 열 때 읽은 옛 관 목록으로 안 돎 · 맞춤 고리도 쉼 ·
|
||||
저장 끝 = 저장 목록 다시 읽음 · 새 자리 그대로(다시 셈 0).
|
||||
- 415-2 관 옮김 → 되돌리기 = 묶음 입력 · 공용 측점도 원래 · 다시하기 = 옮긴 자리.
|
||||
- M4 새 시설 측점 서버 미리보기 걷음 — 미리보기 요청 0(`test_273_2` · `test_318` 이 맡음) · 여기는 코드 자리.
|
||||
|
||||
짝: `helper_415_swap.mjs`(346-2 진입점 묶음 · 진짜 상세 가게) · `helper_415_h2.cjs`(진짜 계산 묶음 · 365 틀).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(HERE) not in sys.path:
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
import test_365_page_pipeline as t365 # noqa: E402
|
||||
|
||||
pytestmark = t365.pytestmark
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def h1(tmp_path_factory) -> dict:
|
||||
work = tmp_path_factory.mktemp("b415")
|
||||
build = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_346_2_swap_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
)
|
||||
assert build.returncode == 0, build.stderr
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_346_2_swap_entry.js", bundle)
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_415_swap.mjs"), str(bundle)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def h2() -> dict:
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path, grid = t365._job(Path(folder))
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_415_h2.cjs"), str(path), str(grid)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=180,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_H1_순수_관만_옮김은_노선_바뀜_아님(h1):
|
||||
assert h1["pure"] == {"pipeOnly": False, "route": True}
|
||||
|
||||
|
||||
def test_H1_관_옮김_뒤_고침_그대로(h1):
|
||||
before = h1["before"]
|
||||
assert before["choices"] == [[40, "soil", "right_cut"], [45.05, "soil", "left_cut"]]
|
||||
assert before["shoulder"] == [["grid:1", {"left": 0.7}]]
|
||||
assert before["berm"] == [["grid:3", {"right": 0.4}]]
|
||||
moved = h1["pipeMoved"]
|
||||
# 관 측점 고침은 새 자리(50 m)로 · 격자 고침 그대로 · 「노선 밖 고침」 알림 없음
|
||||
assert moved["choices"] == [[40, "soil", "right_cut"], [50, "soil", "left_cut"]]
|
||||
assert moved["shoulder"] == before["shoulder"] and moved["berm"] == before["berm"]
|
||||
assert moved["toasts"] == 0
|
||||
again = h1["pipeMovedAgain"]
|
||||
assert again["choices"] == [[40, "soil", "right_cut"], [55, "soil", "left_cut"]]
|
||||
assert again["shoulder"] == before["shoulder"] and again["berm"] == before["berm"]
|
||||
|
||||
|
||||
def test_H1_노선이_바뀌면_넘김_규칙(h1):
|
||||
routed = h1["routeMoved"]
|
||||
# 대조 — 노선 입력이 바뀐 때 = 지반 유형만 넘기고 단면 유형 · 사면 쉬프트 · 소단 깊이 버림
|
||||
assert routed["choices"] == [[40, "soil", None], [55, "soil", None]]
|
||||
assert routed["shoulder"] == [] and routed["berm"] == []
|
||||
|
||||
|
||||
def test_H2_저장_도중_먹이기_멈춤(h2):
|
||||
opened = h2["open"]
|
||||
assert h2["moved"]["input"] == [c + 5 for c in opened]
|
||||
during = h2["during"]
|
||||
assert during["runs"] == 0 and during["rounds"] == 0 # 옛 관 목록으로 안 돎 · 고리도 쉼
|
||||
assert during["input"] == h2["moved"]["input"]
|
||||
assert during["stations"] == h2["moved"]["stations"]
|
||||
assert during["reads"] == 0
|
||||
|
||||
|
||||
def test_H2_저장_끝_저장_목록_다시_읽음(h2):
|
||||
after = h2["after"]
|
||||
assert after["reads"] >= 1 # 저장 목록 다시 읽음
|
||||
assert after["input"] == h2["moved"]["input"] # 새 자리 그대로
|
||||
assert after["stations"] == h2["moved"]["stations"]
|
||||
# 관 · 구조물 다시 먹이기 없음(옛 목록으로 안 돎) — 저장 측점 기준이 새 자리라 횡단만 한 번
|
||||
assert all(source == ["cross"] for source in after["sources"]) and after["runs"] <= 1
|
||||
assert h2["reconcile"] == 0
|
||||
|
||||
|
||||
def test_M4_새_시설_측점_미리보기_걷음():
|
||||
drafts = (ROOT / "B06_Section/B06_Section_Section_Drafts.ts").read_text(encoding="utf-8")
|
||||
assert "previewStructureSections" not in drafts
|
||||
page = (ROOT / "A00_Common/design/design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
guard = (ROOT / "A00_Common/design/design_pipeline_facilities.ts").read_text(encoding="utf-8")
|
||||
# 저장 창구는 늦게 불러옴 — 묶음(시험 · 화면)이 저장 모듈을 맨 위에서 안 끎
|
||||
assert 'from "./design_save"' not in page and 'import("./design_save")' in guard
|
||||
|
||||
|
||||
def test_415_2_관_옮김_되돌리기_측점_원래(h2):
|
||||
undo = h2["undo"]
|
||||
base = h2["moved"]["input"] # 저장 뒤 저장분 = 옮긴 자리
|
||||
assert undo["movedAgain"]["input"] == [c + 5 for c in base]
|
||||
# 되돌리기 = 세션만 되씀(가게 알림 없음) → 묶음 입력 · 공용 측점도 원래
|
||||
assert undo["undone"]["draft"] is None
|
||||
assert undo["undone"]["input"] == base and undo["undone"]["stations"] == base
|
||||
assert undo["redone"]["input"] == undo["movedAgain"]["input"]
|
||||
assert undo["redone"]["stations"] == undo["movedAgain"]["stations"]
|
||||
Reference in New Issue
Block a user