Files
Aislo/resources/tester/helper_344_3_station_keys.mjs
T
eomsangdonandClaude Opus 5.5 4262038cd4 feat(설계묶음): 346-2 cross · masshaul · haul 단계 — 계약 모양 · 측점 지문 국부 연산 · 범위 밖 재사용 · 흙 배분 열쇠 기억 · AbortSignal
- cross 단계(B06_Section_Cross_Stage) = profile · ground · input.cross → 측점 열쇠마다 횡단 설계(CrossOutput.byKey) · 같은 엔진 · 측점 지문이 같으면 앞 결과 · 바뀐 범위 밖은 ctx.prev 재사용
- masshaul 단계(logic/B05_Profile_MassHaul_Stage) = cross · profile → 유토곡선 계열 + 흙 배분 입력
- haul 단계(Api_HaulPlan createHaulStage) = 서버 Node 배분 · 선반입과 같은 열쇠 기억 · 새 조작이면 끊김 · 못 받으면 앞 배분
- 시험: 옛 초안 옮김 · 쓴 자리 읽기 · 노선 바뀜 넘김(노선 밖 2 건) · cross 6 → 같은 입력 0 → 측점 하나 1 · masshaul 계열 · haul 같은 열쇠 POST 1 번

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
2026-10-16 11:45:32 +09:00

241 lines
8.6 KiB
JavaScript

/* 344-3 · 346-2 2파 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_344_3_entry.ts)을 부르고 결과를 JSON 한 벌로.
* node helper_344_3_station_keys.mjs <묶음.mjs> <입력.json>
* 입력 = { standard: 파이썬 config 표준단면, policy: 종단 정책 }.
*/
import { readFileSync } from "node:fs";
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);
const [bundlePath, inputPath] = process.argv.slice(2);
const m = await import(pathToFileURL(bundlePath).href);
const input = JSON.parse(readFileSync(inputPath, "utf8"));
const P = "p344";
const R = 7;
const R2 = 8;
const raw = (name, route) => m.readState(name, P, route) ?? {};
const keysOf = (name, route) =>
Object.keys(raw(name, route))
.filter((k) => k !== "@at")
.sort();
const section = (chainage_m, kind, extra = {}) => ({
station_id: `s${chainage_m}`,
chainage_m,
kind,
label: String(chainage_m),
samples: [],
...extra,
});
const detailOf = (sections) => ({
longitudinal: {
stations: sections,
profile_alignment: { policy: { station_interval_m: 20 } },
},
cross_sections: sections,
});
const choice = (ground) => ({
ground_type: ground,
section_mode: "right_cut",
ditch_side: "right",
ditch_type: "l_type",
paved: true,
two_stage_slope: true,
ditch_choice: false,
});
const out = {};
/* 1. 옛 초안(누가거리 열쇠) → 측점 열쇠로 옮김 · 지금 측점에 없는 것은 남겨 둠 */
const oldRoute = detailOf([
section(0, "bp"),
section(20, "regular"),
section(40, "regular"),
section(45.054, "irregular", { structure: "파형강관 D800", culvert: {} }),
section(52.5, "irregular", { structure: "대피로" }),
section(60, "regular"),
section(70, "ep"),
]);
m.useStructureIds(P, [{ structure_id: "st-1", chainage_m: 52.5 }]);
m.useStationKeys(P, R, oldRoute);
m.writeState(
"crossdesign",
{
"20.00": choice("soil"),
45.05: choice("blasting_rock"),
"52.50": choice("soil"),
"60.00": choice("soil"),
"99.00": choice("soil"),
},
P,
R,
);
out.legacyRead = [...m.crossDesignChoices(P, R).keys()].sort((a, b) => a - b);
out.legacyKeys = keysOf("crossdesign", R);
/* 2. 쓰기 = 측점 열쇠 · 누가거리로 다시 읽음 · 암 경계 저장소도 같은 열쇠 */
m.writeCrossDesignChoice(P, R, 40, choice("ripping_rock"));
m.writeShoulderExtend(P, R, 40, "left", 0.5);
out.writeKey = keysOf("crossdesign", R).includes("grid:2");
out.shoulderKeys = keysOf("shoulderext", R);
const rock = m.createRockBoundaryStore({
sessionKey: () => m.stateKey("rockb", P, R),
detail: () => oldRoute,
refreshCard: () => undefined,
recompute: () => undefined,
});
rock.control.adjust(20, -0.3);
out.rockKeys = keysOf("rockb", R);
rock.load();
out.rockLoaded = Object.fromEntries(rock.offsets);
out.rockSession = m.readRockBoundarySession(P, R);
/* 3. 구조물 id 를 아직 모를 때(첫 열기) — 쓴 때 자리(@at)로 그 측점 고침을 읽음 */
m.useStructureIds(P, []);
m.useStationKeys(P, R, oldRoute);
out.hintKeyNow = m.keyAt(P, R, 52.5);
out.hintRead = m.crossDesignChoices(P, R).has(52.5);
/* 4. 노선이 바뀜 — 측점 열쇠로 넘김 · 넘김 = 카드 선택 넷 · 암 경계 · 노견 확장 안 넘김 · 사라진 측점 = 노선 밖 */
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 47 }] }, P); // 관이 47 m 로 옮김
m.useStructureIds(P, [{ structure_id: "st-1", chainage_m: 55 }]);
m.useStationKeys(
P,
R2,
detailOf([
section(0, "bp"),
section(20, "regular"),
section(40, "regular"),
section(47, "irregular", { structure: "파형강관 D800", culvert: {} }),
section(55, "irregular", { structure: "대피로" }),
section(58, "ep"),
]),
);
// 옛 노선 목록(st-1 은 52.5 · 관 장부는 옛 자리 기준으로 이미 열쇠가 섬)
out.outside = m.carryStationDrafts(P, R, R2);
out.carriedKeys = keysOf("crossdesign", R2);
out.carriedChoice = raw("crossdesign", R2)["grid:1"];
out.carriedRead = [...m.crossDesignChoices(P, R2).keys()].sort((a, b) => a - b);
out.carriedRock = m.readRockBoundarySession(P, R2);
out.carriedShoulder = keysOf("shoulderext", R2);
/* 5. cross 단계 — 가짜 입력 · 같은 입력 0 · 측점 하나 고침 = 1 · 범위 밖 재사용 */
const grid = [0, 20, 40, 60, 80, 100];
const groundAt = (c) => 100 + 0.05 * c - 3 * Math.sin(c / 17);
const base = {
policy: input.policy,
onlyExplicitCurves: false,
basePvi: grid.map((c, i) => ({ chainage_m: c, elevation_m: groundAt(c) + (i % 2 ? 0.8 : -0.6) })),
chainage: Array.from({ length: 101 }, (_, i) => i),
ground: Array.from({ length: 101 }, (_, i) => groundAt(i)),
stations: grid.map((c) => ({ station_id: `s${c}`, chainage_m: c })),
};
const profile = m.buildAlignment(base, m.emptyEdits());
const side = (c, sign) =>
Array.from({ length: 24 }, (_, i) => ({
offsetM: (i + 1) * 0.5,
z: groundAt(c) + sign * 0.3 * (i + 1) * 0.5,
}));
const ground = {
stations: grid.map((c, i) => ({
key: i === 0 ? "end:BP" : i === grid.length - 1 ? "end:EP" : `grid:${i}`,
chainageM: c,
x: c,
y: 0,
azimuthDeg: 90,
left: [0, 1],
kind: i === 0 || i === grid.length - 1 ? "end" : "grid",
id: null,
ground: { centerZ: groundAt(c), left: side(c, 1), right: side(c, -1) },
planRadiusM: null,
curveOuterSide: null,
curveWideningM: null,
})),
longitudinal: { chainageM: [], z: [] },
};
const samples = m.groundSamples(ground.stations[1]);
out.samplesSorted = samples.every((s, i) => i === 0 || s.offset_m > samples[i - 1].offset_m);
out.samplesCount = samples.length;
const { stage, counts } = m.createCrossStage();
const ctxOf = (cross, range, prev, prof = profile) => ({
generation: 1,
range,
signal: new AbortController().signal,
inputs: { route: null, alignment: null, cross, pipes: null, structures: null },
out: { profile: prof, ground },
before: {},
prev,
});
const crossInput = { standard: input.standard, byKey: { "grid:2": { groundType: "soil" } } };
const first = stage(ctxOf(crossInput, null, null));
out.crossFirst = { ...counts(), keys: Object.keys(first.byKey).length };
out.crossHasArea = Object.values(first.byKey).every((r) => Number.isFinite(r.cutAreaM2));
stage(ctxOf(crossInput, null, first));
out.crossSame = counts();
const touched = { ...crossInput, byKey: { ...crossInput.byKey, "grid:3": { groundType: "soil" } } };
const third = stage(
ctxOf(touched, { sources: ["cross"], fromM: 60, toM: 60, keys: ["grid:3"] }, first),
);
out.crossOne = counts();
out.crossOneChanged = third.byKey["grid:3"].detail.ground_type;
const edited = m.buildAlignment(base, { station_offsets: { "40.000": 0.4 }, curve_radii: {} });
stage(ctxOf(touched, { sources: ["grade"], fromM: 20, toM: 60, keys: [] }, third, edited));
out.crossGrade = counts();
/* 6. masshaul 단계 — 계열 · 흙 배분 입력 */
const conversion = {
soil: { loose: 1.25, compacted: 0.9 },
ripping_rock: { loose: 1.5, compacted: 1.2 },
blasting_rock: { loose: 1.65, compacted: 1.35 },
};
const massStage = m.createMassHaulStage(() => ({ conversion }));
const mass = massStage({ ...ctxOf(crossInput, null, null), out: { profile, cross: first } });
out.massSeries = mass.series.map((s) => s.basis);
out.massHaulInput = Array.isArray(mass.haulInput?.points);
/* 7. haul 단계 — 같은 열쇠 안 물음 · 끊김 = 앞 배분 그대로 */
const posts = [];
globalThis.fetch = async (_url, init) => {
if (init.signal?.aborted) throw new Error("aborted");
posts.push(1);
return { ok: true, json: async () => ({ haul_plan: { n: posts.length } }) };
};
const haul = m.createHaulStage(P, () => R);
const haulCtx = (masshaul, prev, signal = new AbortController().signal) => ({
...ctxOf(crossInput, null, prev),
signal,
out: { masshaul },
});
const plan1 = await haul(haulCtx(mass, null));
const plan2 = await haul(haulCtx(mass, plan1));
const other = massStage({
...ctxOf(crossInput, null, null),
out: { profile: edited, cross: third },
});
const aborted = new AbortController();
aborted.abort();
const plan3 = await haul(haulCtx(other, plan1, aborted.signal));
out.haul = { posts: posts.length, plan1, plan2, plan3 };
process.stdout.write(JSON.stringify(out));