- 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
241 lines
8.6 KiB
JavaScript
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));
|