Files
Aislo/resources/tester/helper_272_swap_canon.mjs
T
eomsangdonandClaude Opus 5.5 ff21ef21b7 fix(472 덧 관 지움 ↶): 갈아끼움 — 지금 상세에 없던 측점이 정본 자리로 돌아오면 정본 설계(빈 기본 설계 아님)
- 관 지움 ↶ = 351 측점이 새로 서며 freshDesign(잠정 status · 포장 권장 pavement_suggested 빠짐 → 성토 경고 안내가 달라짐) → 같은 자리 정본 행이 있으면 그 설계 사본
- 지금 상세에 같은 자리 측점이 있으면(격자에 구조물 놓기 등) 옛 규칙 그대로 · 노선 초안 차례 그대로
- 시험 test_272_swap_keeps_canon 관 지움 되돌림(옛 코드 실패)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:34:51 +09:00

281 lines
11 KiB
JavaScript

/* 467 헬퍼 — 노선 초안 없는 차례(관만 옮김)의 갈아끼움이 정본(서버) 횡단 기하 · 표본을 지키는지.
* node helper_272_swap_canon.mjs <묶음.mjs>
* 정본 = 서버 틀(표본 A) · 묶음 측점 = 브라우저 틀(중심 0.5 mm · R 12 곡선 축 3.3e-4 차 · 표본 B).
* 짝 = `test_272_swap_keeps_canon.py` · 표준출력 JSON 한 벌.
*/
import { pathToFileURL } from "node:url";
const memory = () => {
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 = memory();
globalThis.localStorage = memory();
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.fetch = async () => new Response("{}", { status: 404 });
const m = await import(pathToFileURL(process.argv[2]).href);
const idOf = (c) => `station_${String(Math.round(c * 1000)).padStart(12, "0")}`;
const OFFSETS = [-10, -6, -3, 0, 3, 6, 10];
const groundA = (c, o) => 100 + 0.03 * c + 0.4 * o; // 서버 표본(정본)
const groundB = (c, o) => groundA(c, o) + 0.0004 * Math.sin(c + o) + 0.0001; // 브라우저 지형(mm 차)
/* 측점 — [열쇠 · 누가거리 · 묶음 종류 · 상세 종류 · 축 차(rad)] · 곡선 셋 = R 12 */
const LIST = [
["end:BP", 0, "end", "bp", 1e-5],
["grid:1", 20, "grid", "regular", 1e-5],
["curve:27.50", 27.5, "curve", "curve", 3.3e-4],
["curve:33.20", 33.2, "curve", "curve", -3.3e-4],
["curve:38.90", 38.9, "curve", "curve", 2.1e-4],
["grid:2", 40, "grid", "regular", 1e-5],
["pipe:p1", 45, "pipe", "irregular", 1e-5],
["grid:3", 60, "grid", "regular", 1e-5],
["end:EP", 70, "end", "ep", 1e-5],
];
const azOf = (c) => 0.6 + c / 40; // 진행 방향(rad · 곡선 둘레가 휨)
const leftOf = (a) => [-Math.sin(a), Math.cos(a)];
const xyOf = (c) => [1000 + 12 * Math.sin(azOf(c)), 2000 - 12 * Math.cos(azOf(c))];
const canonSection = ([, c, , kind]) => {
const [x, y] = xyOf(c);
const samples = OFFSETS.map((o) => ({ offset_m: o, elevation_m: groundA(c, o), valid: true }));
return {
station_id: idOf(c),
chainage_m: c,
label: "",
kind,
center_z: groundA(c, 0),
azimuth_deg: (azOf(c) * 180) / Math.PI,
plan_radius_m: kind === "curve" ? 12 : null,
curve_outer_side: kind === "curve" ? "left" : null,
curve_widening_m: kind === "curve" ? 1.25 : null,
center_x: x,
center_y: y,
frame: { left_xy: leftOf(azOf(c)) },
uphill_side: "left",
samples,
design: { design_elevation_m: groundA(c, 0) + 0.3, mark: `canon@${c}` },
...(kind === "irregular" ? { structure: "관", culvert: {} } : {}),
};
};
const canonDetail = () => ({
longitudinal: {
length_m: 70,
stations: [],
profile_alignment: { policy: { station_interval_m: 20 } },
},
cross_sections: LIST.map(canonSection),
});
/** 묶음 측점 — 브라우저 틀(중심 0.5 mm · 축 차) · 표본 B · `shift` = 노선 초안이 옮긴 자리(m · y). */
const station = ([key, c0, kind, , tilt], pipeAt, shift = () => 0) => {
const c = key === "pipe:p1" ? pipeAt : c0;
const [x, y] = xyOf(c);
const a = azOf(c) + tilt;
const side = (sign) =>
OFFSETS.filter((o) => o * sign > 0).map((o) => ({ offsetM: Math.abs(o), z: groundB(c, o) }));
return {
key,
chainageM: c,
x: x + 0.0004,
y: y - 0.0003 + shift(c),
azimuthDeg: (a * 180) / Math.PI,
left: leftOf(a),
kind,
id: key === "pipe:p1" ? "p1" : null,
ground: { centerZ: groundB(c, 0), left: side(1), right: side(-1) },
planRadiusM: kind === "curve" ? 12.0004 : null,
curveOuterSide: kind === "curve" ? "left" : null,
curveWideningM: kind === "curve" ? 1.2501 : null,
};
};
const stationsAt = (pipeAt, shift) =>
LIST.map((row) => station(row, pipeAt, shift)).sort((p, q) => p.chainageM - q.chainageM);
const KEYS = LIST.map(([key]) => key);
const keysOf = (list) => list.map((s) => s.key);
const json = (v) => JSON.stringify(v);
const canonOf = new Map(canonDetail().cross_sections.map((s) => [s.chainage_m, s]));
/** 상세 측점마다 [정본과 같은가(기하 · 표본 · 중심고) · 표본이 브라우저 B 인가 · 설계 표시]. */
const GEOM = ["center_x", "center_y", "frame", "azimuth_deg", "plan_radius_m", "curve_widening_m"];
const report = (detail) =>
Object.fromEntries(
detail.cross_sections.map((s) => {
const c = canonOf.get(s.chainage_m);
const same =
!!c &&
GEOM.every((k) => json(s[k]) === json(c[k])) &&
json(s.samples) === json(c.samples) &&
s.center_z === c.center_z;
const browser = s.samples.every(
(p) => Math.abs(p.elevation_m - groundB(s.chainage_m, p.offset_m)) < 1e-12,
);
return [s.chainage_m.toFixed(2), { same, browser, mark: s.design?.mark ?? null }];
}),
);
/** `skip` 밖 횡단이 정본과 바이트 같은가(이름 글은 갈아끼움이 새로 씀 · 뺌). */
const bytesSame = (detail, skip) =>
detail.cross_sections
.filter((s) => !skip.includes(s.chainage_m))
.every((s) => {
const c = canonOf.get(s.chainage_m);
if (!c) return false;
const { label: _a, ...got } = s;
const { label: _b, ...want } = c;
return (
json(
Object.keys(got)
.sort()
.map((k) => [k, got[k]]),
) ===
json(
Object.keys(want)
.sort()
.map((k) => [k, want[k]]),
)
);
});
const out = {};
/* 1. 순수 — 관만 옮김(45 → 55) · 노선 초안 없음 대 있음 · 되돌림 */
{
const moved = stationsAt(55);
const keep = m.swapCrossSections(canonDetail(), moved, KEYS, 20, false);
const draft = m.swapCrossSections(canonDetail(), moved, KEYS, 20, true);
const back = m.swapCrossSections(keep.detail, stationsAt(45), keysOf(moved), 20, false);
out.pure = {
keep: report(keep.detail),
draft: report(draft.detail),
keepMoved: keep.moved,
back: report(back.detail),
backBytes: bytesSame(back.detail, [45]),
defaultIsKeep:
json(m.swapCrossSections(canonDetail(), moved, KEYS, 20).detail) === json(keep.detail),
};
}
/* 1-2. 격자 측점(40 m)에 구조물을 놓고 뺌 — 열쇠가 바뀌어도 같은 자리 = 정본 표본 · 기하 */
{
const onGrid = stationsAt(45).map((s) =>
s.key === "grid:2" ? { ...s, key: "structure:s1", kind: "structure", id: "s1" } : s,
);
const placed = m.swapCrossSections(canonDetail(), onGrid, KEYS, 20, false);
const removed = m.swapCrossSections(placed.detail, stationsAt(45), keysOf(onGrid), 20, false);
out.gridStructure = {
placed: report(placed.detail)["40.00"],
removed: report(removed.detail)["40.00"],
removedBytes: bytesSame(removed.detail, [45, 40]),
};
}
/* 1-3. 관을 옮겼다 제자리로 — 저장 정본(saved)이 있으면 관 측점도 정본 표본 · 기하 */
{
const moved = stationsAt(55);
const keep = m.swapCrossSections(canonDetail(), moved, KEYS, 20, false);
const saved = canonDetail().cross_sections;
const back = m.swapCrossSections(keep.detail, stationsAt(45), keysOf(moved), 20, false, saved);
out.pipeBack = { pipe: report(back.detail)["45.00"], bytes: bytesSame(back.detail, [45]) };
}
/* 1-4. 일반 측점 자리(40)에 선 관을 지움 — 자리 · 수 같아도 다른 목록(관 카드가 안 남음) */
{
const onGrid = stationsAt(45).map((s) =>
s.key === "grid:2" ? { ...s, key: "pipe:p9", kind: "pipe", id: "p9" } : s,
);
const placed = m.swapCrossSections(canonDetail(), onGrid, KEYS, 20, false).detail;
placed.cross_sections.find((s) => s.chainage_m === 40).culvert = { pipe: "p9" };
const gone = stationsAt(45);
out.pipeGone = {
placedKind: placed.cross_sections.find((s) => s.chainage_m === 40).kind,
same: m.sameStationList(placed.cross_sections, keysOf(onGrid), gone),
sameUntouched: m.sameStationList(canonDetail().cross_sections, KEYS, gone),
};
const after = m.swapCrossSections(placed, gone, keysOf(onGrid), 20, false).detail;
const at40 = after.cross_sections.find((s) => s.chainage_m === 40);
out.pipeGone.culvertLeft = "culvert" in at40;
out.pipeGone.kind = at40.kind;
out.pipeGone.canon = report(after)["40.00"].same;
}
/* 1-5. 관 지움 → 되돌림(472 덧) — 지금 상세에 없던 측점이 정본 자리로 = 정본 설계(빈 기본 설계 아님) */
{
const all = stationsAt(45);
const without = all.filter((s) => s.key !== "pipe:p1");
const gone = m.swapCrossSections(canonDetail(), without, KEYS, 20, false);
const saved = canonDetail().cross_sections;
const back = m.swapCrossSections(gone.detail, all, keysOf(without), 20, false, saved);
const at45 = back.detail.cross_sections.find((s) => s.chainage_m === 45);
const bare = m
.swapCrossSections(gone.detail, all, keysOf(without), 20, false)
.detail.cross_sections.find((s) => s.chainage_m === 45);
out.pipeRestore = {
design: json(at45.design) === json(canonOf.get(45).design),
copy: at45.design !== saved.find((s) => s.chainage_m === 45).design,
place: report(back.detail)["45.00"].same,
bytes: bytesSame(back.detail, [45]),
bareFresh: bare.design?.mark === undefined, // 정본 없음 = 기본 설계 그대로
};
}
/* 2. 가게 길(swapSectionStations) — 가게 routeDraft 로 가름: 없음 · 있음 · 초안 버린 직후 */
const seed = (P, R) => {
m.writeState("section-detail", canonDetail(), P, R);
m.writeState("pipe-ids", { next: 2, ids: [{ id: "p1", at: 45 }] }, P);
};
const pipeAt = (P, at) => m.writeState("pipe-ids", { next: 2, ids: [{ id: "p1", at }] }, P);
const keyList = () => KEYS.map((key, i) => ({ key, chainageM: LIST[i][1] }));
{
const P = "p272a";
const R = 3;
seed(P, R);
await m.loadSectionDetail(P, R);
m.useStationKeyList(P, R, keyList());
pipeAt(P, 55);
const moved = await m.swapSectionStations(P, R, stationsAt(55));
pipeAt(P, 45);
const back = await m.swapSectionStations(P, R, stationsAt(45));
out.storeNoDraft = {
moved: moved && report(moved.detail),
back: back && report(back.detail),
backBytes: !!back && bytesSame(back.detail, [45]),
};
}
{
const P = "p272b";
const R = 4;
seed(P, R);
await m.loadSectionDetail(P, R);
m.useStationKeyList(P, R, keyList());
const shop = m.useDesignStore(P, null);
shop.set("routeDraft", { nodes: [], rev: 1 });
pipeAt(P, 55);
// 초안 = 곡선 둘레만 3 cm 옮김(나머지 자리 그대로)
const bend = (c) => (c > 25 && c < 39 ? 0.03 : 0);
const draft = await m.swapSectionStations(P, R, stationsAt(55, bend));
shop.set("routeDraft", null); // 초안 버림 → 저장 노선 측점(묶음 「no-saved」 길)
pipeAt(P, 45);
const dropped = await m.swapSectionStations(P, R, stationsAt(45));
out.storeDraft = {
routeDraftSet: draft !== null,
draft: draft && report(draft.detail),
dropped: dropped && report(dropped.detail),
};
}
process.stdout.write(JSON.stringify(out));