- 관 지움 ↶ = 351 측점이 새로 서며 freshDesign(잠정 status · 포장 권장 pavement_suggested 빠짐 → 성토 경고 안내가 달라짐) → 같은 자리 정본 행이 있으면 그 설계 사본 - 지금 상세에 같은 자리 측점이 있으면(격자에 구조물 놓기 등) 옛 규칙 그대로 · 노선 초안 차례 그대로 - 시험 test_272_swap_keeps_canon 관 지움 되돌림(옛 코드 실패) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
281 lines
11 KiB
JavaScript
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));
|