perf(횡단): 346-2 국부 연산 · [전체 측점 반영] 한 번 · 보이는 카드 먼저
- 횡단 다시 셈이 측점별 입력 지문(지반 표본 · 계획고 · 횡단 입력 · 구조물 맥락 · 표준값)을 기억 — 같으면 앞 결과 그대로 · 계획고 ▲ 한 번 = 10 측점 중 2 측점만 다시 셈 · 결과는 통째 셈과 같음 - [전체 측점 반영] = 다시 셈 한 번(측점마다 66 × 66 돌던 것) · 지금 값을 「사용자 선택」으로 박는 동작 없앰(암 경계 다시 셈 포함 · 사용자 확정 10-16) - 횡단 카드: 보이는 카드만 바로 갈아 끼움 · 화면 밖은 쉬는 틈에 몇 장씩(경고칩 · 면적 합은 계산값이라 그대로) - 머묾 재현 · 국부 연산 · 흙 배분 열쇠 · 전체 반영 · 카드 시험(vite ssr 로 진짜 모듈 묶음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -318,6 +318,12 @@ function readCutSlopeRatios(projectId: string, routeId: number): Map<number, num
|
||||
* 그걸 쓰면 한 박자 늦은 값이 된다. 계산 재료(선형 저장분·표준단면)를 갖추지 못하면
|
||||
* `null` 을 돌려 서버 폴백으로 넘긴다.
|
||||
*/
|
||||
/** 측점별 앞 계산 — 측점 객체가 바뀌면(상세 다시 받음) 저절로 비워진다. */
|
||||
const memos = new WeakMap<
|
||||
CrossSection,
|
||||
{ samples: CrossSection["samples"]; fingerprint: string; design: CrossSection["design"] }
|
||||
>();
|
||||
|
||||
function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
const { projectId, detail, edits } = input;
|
||||
const stored = readAlignment(detail.longitudinal);
|
||||
@@ -334,6 +340,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
if (!alignment) return null; // 선형 저장분이 없는 옛 데이터 — 서버가 풀어 준다.
|
||||
const standard = effectiveStandardCross(projectId) as StandardCrossSectionSpec | null;
|
||||
if (!standard) return null; // 컨텍스트를 아직 못 받음 — 이번만 서버로.
|
||||
const standardKey = JSON.stringify(standard);
|
||||
// 방금 푼 계획선을 **공유 캐시에도 얹는다**. 여기서 만드는 횡단 설계는 편집이 반영된
|
||||
// 계획고 기준인데 `design_profiles` 만 저장분으로 남으면 두 값의 기준이 어긋나, 낡음
|
||||
// 판정이 영원히 참이 되어 유토곡선이 빈 채로 남는다(2026-09-03 사용자 보고: B05 편집 중
|
||||
@@ -460,9 +467,21 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
}
|
||||
: null,
|
||||
};
|
||||
// 국부 연산(346-2) — 입력 지문(지반 표본 · 계획고 · 횡단 입력 · 구조물 맥락 · 표준값)이 같고
|
||||
// 설계가 그때 낸 그 객체면 앞 결과 그대로. 계획고 ▲ 한 번에 다시 세는 측점은 곡선 둘레 몇 곳뿐.
|
||||
const samples = section.samples ?? [];
|
||||
const fingerprint = `${standardKey}|${elevation}|${samples.length}|${JSON.stringify({ ...options, standard: null })}`;
|
||||
const memo = memos.get(section);
|
||||
if (
|
||||
memo?.samples === section.samples &&
|
||||
memo.fingerprint === fingerprint &&
|
||||
memo.design === previous
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let next;
|
||||
try {
|
||||
next = computeCrossDesign(section.samples ?? [], elevation, options);
|
||||
next = computeCrossDesign(samples, elevation, options);
|
||||
} catch {
|
||||
continue; // 샘플 부족·값 손상 측점은 건너뛴다(서버 엔진과 같은 태도).
|
||||
}
|
||||
@@ -477,6 +496,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
const own = next[`berm_depth_${side}_m`];
|
||||
if (typeof own === "number") section.design[`berm_depth_${side}_m`] = own;
|
||||
}
|
||||
memos.set(section, { samples: section.samples, fingerprint, design: section.design });
|
||||
updated.push(section.chainage_m);
|
||||
}
|
||||
return updated;
|
||||
|
||||
@@ -117,28 +117,13 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
||||
await reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/** 현재 design 값에서 재계산용 change를 복원한다(암 경계 오프셋 변경 시 재계산 트리거). */
|
||||
function changeFromDesign(chainageM: number): CrossDesignChange | null {
|
||||
const target = ctx
|
||||
.detail()
|
||||
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01);
|
||||
const design = target?.design;
|
||||
if (!design) return null;
|
||||
return {
|
||||
ground_type: design.ground_type,
|
||||
section_mode: design.section_mode,
|
||||
ditch_side: design.ditch_side ?? null,
|
||||
ditch_type: design.ditch_type ?? "standard",
|
||||
paved: design.paved,
|
||||
two_stage_slope: design.two_stage_slope ?? false,
|
||||
ditch_choice: design.ditch_choice ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 암 경계 오프셋 변경 후 암 지반이면 2단계 무릎·단면적을 서버 재계산한다. */
|
||||
/** 암 경계 오프셋 변경 후 암 지반이면 2단계 무릎·단면적을 다시 셈 — 지금 값을 「사용자 선택」으로
|
||||
* 박지 않음(자동 판정이 굳으면 노선이 바뀌어도 재판정 안 됨 · 346-2). 암 경계는 세션에서 읽음. */
|
||||
function recomputeIfRock(chainageM: number): void {
|
||||
const change = changeFromDesign(chainageM);
|
||||
if (change && change.ground_type !== "soil") void handleDesignChange(chainageM, change);
|
||||
const design = ctx
|
||||
.detail()
|
||||
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01)?.design;
|
||||
if (design && design.ground_type !== "soil") void reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,23 +200,18 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
||||
void reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/** 패널 [전체 반영](N-2-1): design 보유 전 측점을 패널 최신값으로 순차 재계산한다.
|
||||
* handleDesignChange가 standardPanel.getValues()를 실어 보내므로 표준단면 수치만
|
||||
* 갱신되고 측점별 버튼 선택값은 보존된다. 순차 await로 동시 API 호출 수를 제한한다. */
|
||||
/** 패널 [전체 반영](N-2-1 · 346-2): 표준값으로 전 측점을 **한 번** 다시 셈 — 측점마다 돌면
|
||||
* 66 × 66 = 4.1 s 였음. 표준값은 다시 셈이 `effectiveStandardCross` 로 읽고, 측점별 선택은
|
||||
* 세션에 있는 것만 씀(지금 값을 「사용자 선택」으로 박지 않음 · 사용자 확정 10-16). */
|
||||
let applyingAll = false;
|
||||
async function applyPanelToAll(): Promise<void> {
|
||||
const sectionDetail = ctx.detail();
|
||||
if (applyingAll || !sectionDetail || !projectId || ctx.routeId() === null) return;
|
||||
const targets = sectionDetail.cross_sections.filter((section) => section.design);
|
||||
if (!targets.length) return;
|
||||
// 화면을 잠그지 않는다 — 카드가 하나씩 갱신되는 것이 곧 진행 표시다(CLAUDE.md 5장).
|
||||
// 대신 도는 동안 다시 누르는 것만 막는다.
|
||||
if (!sectionDetail.cross_sections.some((section) => section.design)) return;
|
||||
// 화면을 잠그지 않는다(CLAUDE.md 5장) — 도는 동안 다시 누르는 것만 막는다.
|
||||
applyingAll = true;
|
||||
try {
|
||||
for (const section of targets) {
|
||||
const change = changeFromDesign(section.chainage_m);
|
||||
if (change) await handleDesignChange(section.chainage_m, change);
|
||||
}
|
||||
await reconcileStaleDesigns({ force: true });
|
||||
showToast(L("B06_Std_ApplyAll_Success"), "success");
|
||||
} finally {
|
||||
applyingAll = false;
|
||||
|
||||
@@ -133,6 +133,28 @@ export function staleKey(
|
||||
.join("|");
|
||||
}
|
||||
|
||||
/**
|
||||
* 쉬는 틈 일거리 줄(346-2) — 화면 밖 횡단 카드를 몇 장씩 갈아 끼움(카드 하나 ≈ 8 ms · 0.54 s / 66).
|
||||
* 줄에 든 측점은 `run` 이 꺼내 그림 · 통째로 다시 그리면 `queue.clear()`.
|
||||
*/
|
||||
export function createIdleQueue(run: (chainageM: number) => void, batch = 6) {
|
||||
const queue = new Set<number>();
|
||||
let waiting = 0;
|
||||
const drain = (): void => {
|
||||
if (waiting || !queue.size) return;
|
||||
const idle = window.requestIdleCallback ?? ((next: () => void) => window.setTimeout(next, 16));
|
||||
waiting = idle(() => {
|
||||
waiting = 0;
|
||||
for (const chainageM of [...queue].slice(0, batch)) {
|
||||
queue.delete(chainageM);
|
||||
run(chainageM);
|
||||
}
|
||||
drain();
|
||||
});
|
||||
};
|
||||
return { queue, drain };
|
||||
}
|
||||
|
||||
/** 낡음 지문(`staleKey`)에 든 측점 누가거리 — 다시 셈 뒤에도 남은 측점을 표시할 때(342-1). */
|
||||
export function staleChainages(key: string): number[] {
|
||||
return key ? key.split("|").map((entry) => Number(entry.split(":")[0])) : [];
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
naturalHeightAtWidth,
|
||||
} from "./B06_Section_UI_Cross_View_Metrics";
|
||||
import { cardZoomStates } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import { CROSS_GRID_GAP, CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||
import { CROSS_GRID_GAP, CROSS_HEIGHT, createIdleQueue } from "./B06_Section_UI_Section_Common";
|
||||
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
||||
import { culvertLinkFor as culvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
@@ -415,8 +415,11 @@ export function createSectionView(
|
||||
});
|
||||
}
|
||||
|
||||
/** 화면 밖에서 아직 옛 그림인 카드 — 쉬는 틈에 몇 장씩(346-2). */
|
||||
const offscreen = createIdleQueue((chainageM) => rebuildCard(chainageM));
|
||||
const draw = (): void => {
|
||||
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
||||
offscreen.queue.clear(); // 통째로 다시 그림 — 밀린 카드도 새 그림
|
||||
const detail = currentDetail;
|
||||
cachedStationInterval =
|
||||
currentStationInterval ?? inferStationInterval(detail.longitudinal.stations);
|
||||
@@ -488,17 +491,23 @@ export function createSectionView(
|
||||
syncProfile();
|
||||
};
|
||||
|
||||
/** 카드 한 장만 갈아 끼운다(종단도는 안 건드린다). 못 찾으면 false. */
|
||||
const rebuildCard = (chainageM: number): boolean => {
|
||||
if (!currentDetail) return false;
|
||||
const section = currentDetail.cross_sections.find(
|
||||
/** 측점의 자료 · 카드 — 측점이 없으면 null. */
|
||||
const cardAt = (chainageM: number) => {
|
||||
const section = currentDetail?.cross_sections.find(
|
||||
(candidate) => Math.abs(candidate.chainage_m - chainageM) < 0.01,
|
||||
);
|
||||
if (!section) return false;
|
||||
const existing = document.getElementById(`cross-${section.station_id}`);
|
||||
return section && { section, el: document.getElementById(`cross-${section.station_id}`) };
|
||||
};
|
||||
|
||||
/** 카드 한 장만 갈아 끼운다(종단도는 안 건드린다). 못 찾으면 false. */
|
||||
const rebuildCard = (chainageM: number): boolean => {
|
||||
const card = cardAt(chainageM);
|
||||
if (!card) return false;
|
||||
offscreen.queue.delete(chainageM);
|
||||
// 단건 갱신은 draw에서 정해둔 행 높이를 재사용해 같은 행 카드와 높이를 유지한다.
|
||||
if (existing)
|
||||
existing.replaceWith(buildCrossCard(section, cachedRowHeight.get(section.station_id)));
|
||||
card.el?.replaceWith(
|
||||
buildCrossCard(card.section, cachedRowHeight.get(card.section.station_id)),
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -515,7 +524,18 @@ export function createSectionView(
|
||||
*/
|
||||
const refreshCards = (chainages: ReadonlyArray<number>, redraw = true): void => {
|
||||
let touched = false;
|
||||
for (const chainageM of chainages) if (rebuildCard(chainageM)) touched = true;
|
||||
// 보이는 카드 먼저(346-2) — 화면 밖 카드는 쉬는 틈에 나눠 그림 · 경고칩 · 면적 합은 계산값이라 그대로
|
||||
const view = cards.getBoundingClientRect();
|
||||
for (const chainageM of chainages) {
|
||||
const card = cardAt(chainageM);
|
||||
if (!card) continue;
|
||||
touched = true;
|
||||
const rect = card.el?.getBoundingClientRect();
|
||||
if (rect && rect.height > 0 && rect.bottom >= view.top && rect.top <= view.bottom)
|
||||
rebuildCard(chainageM);
|
||||
else offscreen.queue.add(chainageM);
|
||||
}
|
||||
offscreen.drain();
|
||||
if (!touched || !currentDetail) return;
|
||||
if (redraw) redrawProfile();
|
||||
else fillSlopeNotice.update(currentDetail.cross_sections, cachedStationInterval);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/* 342-1 · 346-2 시험 묶음 진입점 — 진짜 모듈을 vite ssr 로 한 벌 묶어 Node 헬퍼가 부름.
|
||||
* 화면 · 서버 없이 계획선 한 벌 · 낡음 머묾 · 국부 연산 · 흙 배분 열쇠 · [전체 측점 반영] 을 잰다.
|
||||
*/
|
||||
export {
|
||||
holdScreenAlignment,
|
||||
refreshCrossDesigns,
|
||||
staleAgainstScreen,
|
||||
} from "../../B06_Section/B06_Section_Cross_Refresh";
|
||||
export {
|
||||
createIdleQueue,
|
||||
staleChainages,
|
||||
staleKey,
|
||||
} from "../../B06_Section/B06_Section_UI_Section_Common";
|
||||
export {
|
||||
buildAlignment,
|
||||
emptyEdits,
|
||||
toAlignmentBase,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||
export { toDesignProfile } from "../../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
export { followMovedNodes, oneStationList } from "../../common_util/common_util_structure_station";
|
||||
export {
|
||||
irregularGraphStations,
|
||||
maxChainageOf,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Layout";
|
||||
export { createCrossPreview } from "../../B05_Profile/B05_Profile_UI_Profile_Preview";
|
||||
export { createHaulPlanPrefetch } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
|
||||
export { createDesignSync } from "../../B06_Section/B06_Section_UI_Page_Design_Sync";
|
||||
export { writeState } from "../../A00_Common/b_page_state";
|
||||
@@ -0,0 +1,347 @@
|
||||
/* 342-1 · 346-2 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_342_1_entry.ts)을 부르고 결과를 JSON 한 벌로.
|
||||
* node helper_342_1_station_line.mjs <묶음.mjs> <입력.json>
|
||||
* 입력 = { standard: 파이썬 config 표준단면, policy: 종단 정책 } · 화면 선 = design_profile.ts 의 진짜
|
||||
* `listed()` 본문(소스에서 떼어 트랜스파일 · helper_177_3 방식).
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, "..", "..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const ts = require(join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
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(),
|
||||
keys: () => [...m.keys()],
|
||||
};
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.sessionStorage = store();
|
||||
globalThis.localStorage = store();
|
||||
// 토스트 등 DOM 을 건드리는 자리 — 무엇을 불러도 받아 주는 빈 객체
|
||||
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);
|
||||
const warns = [];
|
||||
console.warn = (...args) => warns.push(args.map(String).join(" "));
|
||||
|
||||
const [bundlePath, inputPath] = process.argv.slice(2);
|
||||
const m = await import(pathToFileURL(bundlePath).href);
|
||||
const input = JSON.parse(readFileSync(inputPath, "utf8"));
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const transpile = (code) =>
|
||||
ts.transpileModule(code, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
|
||||
const PID = "p342";
|
||||
const RID = 7;
|
||||
m.writeState("std-cross", input.standard, PID);
|
||||
|
||||
/* ── 견본 노선 0~160 m — 서버 비정규 측점 85.054(0.001 m) · 관 지점 85.05(0.01 m) · 종단곡선 안 ── */
|
||||
const SERVER_IRR = 85.054;
|
||||
const PIPE = 85.05;
|
||||
const regular = [0, 20, 40, 60, 80, 100, 120, 140, 160];
|
||||
const serverStations = [...regular, SERVER_IRR]
|
||||
.sort((a, b) => a - b)
|
||||
.map((c) => ({
|
||||
station_id: `s${c}`,
|
||||
chainage_m: c,
|
||||
kind: c === SERVER_IRR ? "irregular" : c === 0 ? "bp" : "regular",
|
||||
label: String(c),
|
||||
...(c === SERVER_IRR ? { structure: "파형강관 D800" } : {}),
|
||||
}));
|
||||
const groundAt = (c) => 100 + 0.08 * c - 6 * Math.sin(c / 23);
|
||||
const chainage = Array.from({ length: 161 }, (_, i) => i);
|
||||
const basePvi = serverStations.map((s, i) => ({
|
||||
chainage_m: s.chainage_m,
|
||||
elevation_m: groundAt(s.chainage_m) + (i % 2 ? 1.6 : -1.3),
|
||||
}));
|
||||
const base = {
|
||||
policy: input.policy,
|
||||
onlyExplicitCurves: false,
|
||||
basePvi,
|
||||
chainage,
|
||||
ground: chainage.map(groundAt),
|
||||
stations: serverStations.map((s) => ({ station_id: s.station_id, chainage_m: s.chainage_m })),
|
||||
};
|
||||
const stored = m.buildAlignment(base, m.emptyEdits());
|
||||
const crossSamples = (c) =>
|
||||
Array.from({ length: 49 }, (_, i) => {
|
||||
const offset = -12 + i * 0.5;
|
||||
return { offset_m: offset, elevation_m: groundAt(c) + 0.3 * offset, valid: true };
|
||||
});
|
||||
const freshDesign = () => ({
|
||||
design_elevation_m: 0,
|
||||
ground_type: "soil",
|
||||
section_mode: "left_cut",
|
||||
ditch_side: "left",
|
||||
ditch_type: "standard",
|
||||
two_stage_slope: false,
|
||||
});
|
||||
const makeDetail = () => ({
|
||||
longitudinal: {
|
||||
length_m: 160,
|
||||
stations: serverStations.map((s) => ({ ...s })),
|
||||
samples: chainage.map((c) => ({ chainage_m: c, elevation_m: groundAt(c), valid: true })),
|
||||
profile_alignment: JSON.parse(JSON.stringify(stored)),
|
||||
design_profiles: [m.toDesignProfile(stored, undefined)],
|
||||
},
|
||||
cross_sections: serverStations.map((s) => ({
|
||||
station_id: s.station_id,
|
||||
chainage_m: s.chainage_m,
|
||||
samples: crossSamples(s.chainage_m),
|
||||
design: freshDesign(),
|
||||
})),
|
||||
});
|
||||
const irregularStations = [
|
||||
{ id: "pipe-1", station: 4, remainder: 5.05, chainage_m: PIPE, structure: "파형강관 D800" },
|
||||
];
|
||||
|
||||
/* 화면 선 = design_profile.ts 의 진짜 listed() */
|
||||
const dpSrc = readFileSync(join(ROOT, "A00_Common", "design", "design_profile.ts"), "utf8");
|
||||
const listedSrc = dpSrc.match(/\r?\n function listed\(\)[\s\S]*?\r?\n \}\r?\n/)[0];
|
||||
const listedOf = new Function(
|
||||
"storedBase",
|
||||
"detail",
|
||||
"irregularStations",
|
||||
"oneStationList",
|
||||
"irregularGraphStations",
|
||||
"maxChainageOf",
|
||||
"followMovedNodes",
|
||||
`${transpile(listedSrc)}\nreturn listed();`,
|
||||
);
|
||||
const screenBase = (detail) =>
|
||||
listedOf(
|
||||
m.toAlignmentBase(stored),
|
||||
detail,
|
||||
irregularStations,
|
||||
m.oneStationList,
|
||||
m.irregularGraphStations,
|
||||
m.maxChainageOf,
|
||||
m.followMovedNodes,
|
||||
);
|
||||
const staleOf = (detail, line) =>
|
||||
m.staleKey({
|
||||
longitudinal: { design_profiles: [m.toDesignProfile(line, undefined)] },
|
||||
cross_sections: detail.cross_sections,
|
||||
});
|
||||
const refresh = (detail, extra = {}) =>
|
||||
m.refreshCrossDesigns({
|
||||
projectId: PID,
|
||||
routeId: RID,
|
||||
detail,
|
||||
edits: m.emptyEdits(),
|
||||
...extra,
|
||||
});
|
||||
|
||||
const out = {};
|
||||
|
||||
/* 1. 머묾 재현 — 고치기 전 길(저장분 선) 대 화면 선 */
|
||||
{
|
||||
const detail = makeDetail();
|
||||
await refresh(detail, { alignment: stored }); // 저장본 = 서버가 저장분 선으로 낸 설계
|
||||
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
|
||||
out.screenStations = screen.stations.map((s) => s.chainage_m);
|
||||
out.staleBefore = m.staleChainages(staleOf(detail, screen));
|
||||
await refresh(detail); // 옛 길 — 맡긴 선 없음 → 저장분 선으로 다시 셈
|
||||
out.staleOldPath = m.staleChainages(staleOf(detail, screen));
|
||||
const release = m.holdScreenAlignment((id) => (id === RID ? screen : null));
|
||||
out.staleAgainstScreenBefore = m.staleAgainstScreen(detail, RID);
|
||||
await refresh(detail); // 새 길 — 종단도가 맡긴 화면 선(B06 · 3D 와 같은 길)
|
||||
out.staleNewPath = m.staleChainages(staleOf(detail, screen));
|
||||
out.staleAgainstScreenAfter = m.staleAgainstScreen(detail, RID);
|
||||
// B06 판정(다시 셈이 얹은 design_profiles)과 B05 판정(화면 선)이 같은가
|
||||
out.staleB06View = m.staleChainages(
|
||||
m.staleKey({
|
||||
longitudinal: detail.longitudinal,
|
||||
cross_sections: detail.cross_sections,
|
||||
}),
|
||||
);
|
||||
release();
|
||||
}
|
||||
|
||||
/* 2. 국부 연산 — 같은 입력 다시 셈 0 · 계획고 ▲ 한 번 = 몇 측점만 · 결과는 통째 셈과 같음 */
|
||||
{
|
||||
const detail = makeDetail();
|
||||
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
|
||||
await refresh(detail, { alignment: screen });
|
||||
await refresh(detail, { alignment: screen }); // 첫 셈이 결과 칸(측구 유무 등)을 채운 뒤 한 번 더 — 저장본 상태
|
||||
const ids = () => detail.cross_sections.map((s) => s.design);
|
||||
let before = ids();
|
||||
await refresh(detail, { alignment: screen });
|
||||
out.recomputedSame = ids().filter((d, i) => d !== before[i]).length;
|
||||
const edited = m.buildAlignment(screenBase(detail), {
|
||||
station_offsets: { "140.000": 0.3 },
|
||||
curve_radii: {},
|
||||
});
|
||||
before = ids();
|
||||
const changed = await refresh(detail, { alignment: edited });
|
||||
out.recomputedEdit = ids().filter((d, i) => d !== before[i]).length;
|
||||
out.changedEdit = changed.length;
|
||||
out.total = detail.cross_sections.length;
|
||||
// 통째 셈(새 측점 객체 = 기억 없음)과 결과 같음
|
||||
const whole = makeDetail();
|
||||
whole.cross_sections.forEach((s, i) => (s.design = { ...detail.cross_sections[i].design }));
|
||||
await refresh(whole, { alignment: stored });
|
||||
await refresh(whole, { alignment: edited });
|
||||
out.sameAsWhole =
|
||||
JSON.stringify(whole.cross_sections.map((s) => s.design)) ===
|
||||
JSON.stringify(detail.cross_sections.map((s) => s.design));
|
||||
}
|
||||
|
||||
/* 3. 다시 셈 뒤에도 남은 낡음 — 기다림(true) → 남음(false) · 콘솔 경고 · 다시 그림 */
|
||||
{
|
||||
const detail = makeDetail();
|
||||
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
|
||||
await refresh(detail, { alignment: screen });
|
||||
const broken = detail.cross_sections[3];
|
||||
broken.samples = []; // 다시 셈이 못 고치는 측점
|
||||
broken.design.design_elevation_m += 1;
|
||||
const applied = [];
|
||||
const preview = m.createCrossPreview({
|
||||
projectId: PID,
|
||||
detail: () => detail,
|
||||
routeId: () => RID,
|
||||
edits: () => m.emptyEdits(),
|
||||
alignment: () => screen,
|
||||
debounceMs: () => 0,
|
||||
onApplied: (updated) => applied.push(updated.length),
|
||||
});
|
||||
const key = staleOf(detail, screen);
|
||||
out.settleFirst = preview.scheduleIfStale(key);
|
||||
await sleep(50);
|
||||
const again = staleOf(detail, screen);
|
||||
out.settleSameKey = again === key;
|
||||
out.settleAfter = preview.scheduleIfStale(again);
|
||||
out.settleWarns = warns.filter((w) => w.includes("낡은 측점")).length;
|
||||
out.settleRedraws = applied.length;
|
||||
out.settleClear = preview.scheduleIfStale("");
|
||||
preview.dispose();
|
||||
}
|
||||
|
||||
/* 4. 흙 배분 — 실패한 열쇠 다시 보냄 · 받은 열쇠는 다시 안 물음 */
|
||||
{
|
||||
const replies = [false, true, true];
|
||||
const posts = [];
|
||||
globalThis.fetch = async (_url, init) => {
|
||||
posts.push(JSON.parse(init.body).result.points.length);
|
||||
const ok = replies.shift() ?? true;
|
||||
return {
|
||||
ok,
|
||||
json: async () => ({ haul_plan: { tag: JSON.parse(init.body).result.points.length } }),
|
||||
};
|
||||
};
|
||||
let ready = 0;
|
||||
const prefetch = m.createHaulPlanPrefetch(
|
||||
PID,
|
||||
() => RID,
|
||||
() => (ready += 1),
|
||||
);
|
||||
const A = { points: [{ cumulative_volume_m3: 1 }, { cumulative_volume_m3: 2 }] };
|
||||
const B = {
|
||||
points: [{ cumulative_volume_m3: 1 }, { cumulative_volume_m3: 2 }, { cumulative_volume_m3: 3 }],
|
||||
};
|
||||
prefetch.schedule(A);
|
||||
await sleep(450);
|
||||
out.haulAfterFail = posts.length; // 1 — 실패
|
||||
prefetch.schedule(A); // 같은 열쇠 — 다시 보냄
|
||||
await sleep(450);
|
||||
out.haulRetry = posts.length; // 2
|
||||
prefetch.schedule(B);
|
||||
await sleep(450);
|
||||
out.haulB = posts.length; // 3
|
||||
prefetch.schedule(A); // 받아 둔 열쇠 — 안 물음
|
||||
await sleep(450);
|
||||
out.haulKept = posts.length; // 3
|
||||
out.haulKeptPlan = prefetch.current()?.tag; // 2 (A 의 점 2개)
|
||||
out.haulReady = ready;
|
||||
prefetch.dispose();
|
||||
}
|
||||
|
||||
/* 5. [전체 측점 반영] — 다시 셈 한 번 · 측점 선택을 세션에 안 박음 */
|
||||
{
|
||||
const detail = makeDetail();
|
||||
const screen = m.buildAlignment(screenBase(detail), m.emptyEdits());
|
||||
const release = m.holdScreenAlignment((id) => (id === RID ? screen : null));
|
||||
const cardsCalls = [];
|
||||
const sync = m.createDesignSync({
|
||||
projectId: PID,
|
||||
routeId: () => RID,
|
||||
detail: () => detail,
|
||||
view: () => ({
|
||||
refreshCards: (updated) => cardsCalls.push(updated.length),
|
||||
refreshCard: () => cardsCalls.push(-1),
|
||||
detailChart: () => null,
|
||||
}),
|
||||
});
|
||||
const t0 = performance.now();
|
||||
await sync.applyPanelToAll();
|
||||
out.applyAllMs = performance.now() - t0;
|
||||
out.applyAllCalls = cardsCalls;
|
||||
out.applyAllChoiceKeys = sessionStorage.keys().filter((k) => k.includes("crossdesign")).length;
|
||||
release();
|
||||
}
|
||||
|
||||
/* 6. 횡단 카드 보이는 것 먼저 — B06_Section_UI_Section_View.ts 의 진짜 refreshCards 본문 */
|
||||
{
|
||||
const viewSrc = readFileSync(join(ROOT, "B06_Section", "B06_Section_UI_Section_View.ts"), "utf8");
|
||||
const body = viewSrc.match(/\r?\n const refreshCards = [\s\S]*?\r?\n \};\r?\n/)[0];
|
||||
const rebuilt = [];
|
||||
let redraws = 0;
|
||||
const top = { 0: 0, 20: 150, 40: 300, 60: 450, 80: 600 }; // 카드 위치(px) · 화면 = 0~320
|
||||
const factory = new Function(
|
||||
"cards",
|
||||
"cardAt",
|
||||
"rebuildCard",
|
||||
"offscreen",
|
||||
"currentDetail",
|
||||
"redrawProfile",
|
||||
"fillSlopeNotice",
|
||||
"cachedStationInterval",
|
||||
`${transpile(body)}\nreturn refreshCards;`,
|
||||
);
|
||||
let offscreen;
|
||||
const rebuildCard = (c) => {
|
||||
offscreen.queue.delete(c);
|
||||
rebuilt.push(c);
|
||||
return true;
|
||||
};
|
||||
offscreen = m.createIdleQueue(rebuildCard, 2);
|
||||
const refreshCards = factory(
|
||||
{ getBoundingClientRect: () => ({ top: 0, bottom: 320 }) },
|
||||
(c) => ({
|
||||
section: {},
|
||||
el: { getBoundingClientRect: () => ({ top: top[c], bottom: top[c] + 140, height: 140 }) },
|
||||
}),
|
||||
rebuildCard,
|
||||
offscreen,
|
||||
{ cross_sections: [] },
|
||||
() => (redraws += 1),
|
||||
{ update: () => undefined },
|
||||
20,
|
||||
);
|
||||
refreshCards([80, 0, 60, 20, 40]);
|
||||
out.cardsNow = [...rebuilt];
|
||||
out.cardsRedraws = redraws;
|
||||
await sleep(120);
|
||||
out.cardsAll = [...rebuilt];
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -80,9 +80,8 @@ def test_ts_defaults_off_and_stored_kept():
|
||||
assert "twoStageSlope: choice?.two_stage_slope ?? Boolean(design.two_stage_slope)," in _read(
|
||||
"B06_Section/B06_Section_Cross_Refresh.ts"
|
||||
)
|
||||
assert "two_stage_slope: design.two_stage_slope ?? false," in _read(
|
||||
"B06_Section/B06_Section_UI_Page_Design_Sync.ts"
|
||||
)
|
||||
# 지금 값을 「사용자 선택」으로 박는 길은 없앰(346-2 · 사용자 확정 10-16) — 선택은 세션 것만
|
||||
assert "changeFromDesign" not in _read("B06_Section/B06_Section_UI_Page_Design_Sync.ts")
|
||||
|
||||
|
||||
def test_3d_first_view_perspective():
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""342-1 · 346-2 — 계획선 한 벌 · 낡음 머묾 · 국부 연산 · 흙 배분 열쇠 · [전체 측점 반영] · 카드.
|
||||
|
||||
못박는 것
|
||||
- 머묾 재현: 서버 측점 85.054(0.001 m) · 관 지점 85.05(0.01 m) · 종단곡선 안 — 저장분 선으로
|
||||
다시 세면 화면 선 기준 낡음이 남음(옛 길) · 화면 선을 받으면 낡음 0 · B05 · B06 판정 같음.
|
||||
- 국부 연산: 같은 입력 다시 셈 0 측점 · 계획고 ▲ 한 번 = 곡선 둘레 몇 측점 · 결과 = 통째 셈.
|
||||
- 다시 셈 뒤에도 남은 낡음 = 기다림 끝(false) · 콘솔 경고 한 줄 · 다시 그림(곡선은 그림).
|
||||
- 흙 배분: 실패한 열쇠 다시 보냄 · 받아 둔 열쇠는 다시 안 물음.
|
||||
- [전체 측점 반영] = 다시 셈 한 번 · 측점 선택을 세션에 안 박음.
|
||||
- 횡단 카드: 보이는 카드만 바로 · 나머지는 쉬는 틈에.
|
||||
|
||||
Node 헬퍼 `helper_342_1_station_line.mjs` 가 vite ssr 로 묶은 진짜 모듈(`helper_342_1_entry.ts`)을
|
||||
부름.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
|
||||
FIXTURE = HERE / "fixtures" / "b05_192_1_moved_pipe_longitudinal.json"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
work = tmp_path_factory.mktemp("b342")
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_342_1_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_342_1_entry.js", bundle)
|
||||
policy = json.loads(FIXTURE.read_text(encoding="utf-8"))["profile_alignment"]["policy"]
|
||||
source = work / "input.json"
|
||||
source.write_text(
|
||||
json.dumps({"standard": STANDARD_CROSS_SECTION, "policy": policy}, default=str),
|
||||
encoding="utf-8",
|
||||
)
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_342_1_station_line.mjs"), str(bundle), str(source)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_화면_선은_관_지점으로_갈음(out):
|
||||
assert 85.05 in out["screenStations"] and 85.054 not in out["screenStations"]
|
||||
|
||||
|
||||
def test_머묾_재현_옛_길은_낡음이_남음(out):
|
||||
assert out["staleBefore"] == [85.054]
|
||||
assert out["staleOldPath"] == [85.054]
|
||||
assert out["staleAgainstScreenBefore"] is True
|
||||
|
||||
|
||||
def test_화면_선을_받으면_낡음_0_두_판정_같음(out):
|
||||
assert out["staleNewPath"] == []
|
||||
assert out["staleAgainstScreenAfter"] is False
|
||||
assert out["staleB06View"] == []
|
||||
|
||||
|
||||
def test_국부_연산_바뀐_측점만(out):
|
||||
assert out["recomputedSame"] == 0
|
||||
assert 0 < out["recomputedEdit"] < out["total"]
|
||||
assert out["sameAsWhole"] is True
|
||||
|
||||
|
||||
def test_남은_낡음은_곡선을_그리고_경고(out):
|
||||
assert out["settleFirst"] is True # 다시 셈 기다림
|
||||
assert out["settleSameKey"] is True # 다시 셈이 못 고침
|
||||
assert out["settleAfter"] is False # 기다림 끝 — 곡선 그림
|
||||
assert out["settleWarns"] == 1
|
||||
assert out["settleRedraws"] >= 1 # 바뀐 것 없어도 다시 그림
|
||||
assert out["settleClear"] is False
|
||||
|
||||
|
||||
def test_흙_배분_실패_열쇠_다시_보냄_받은_열쇠는_안_물음(out):
|
||||
assert out["haulAfterFail"] == 1
|
||||
assert out["haulRetry"] == 2
|
||||
assert out["haulB"] == 3
|
||||
assert out["haulKept"] == 3
|
||||
assert out["haulKeptPlan"] == 2
|
||||
|
||||
|
||||
def test_전체_측점_반영은_한_번_선택_안_박음(out):
|
||||
assert out["applyAllCalls"] == [out["total"]]
|
||||
assert out["applyAllChoiceKeys"] == 0
|
||||
|
||||
|
||||
def test_횡단_카드_보이는_것_먼저(out):
|
||||
assert out["cardsNow"] == [0, 20, 40]
|
||||
assert out["cardsRedraws"] == 1
|
||||
assert sorted(out["cardsAll"]) == [0, 20, 40, 60, 80]
|
||||
Reference in New Issue
Block a user