Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
196 lines
8.0 KiB
JavaScript
196 lines
8.0 KiB
JavaScript
/* 91 원지반 · 암 제약 삭제 · 유출측 복합경사 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림.
|
|
* 입력 = 73-4 픽스처(세월교 173.09 · 박스 352.14 · 관 180) · 결과 = 표준출력 JSON 한 벌. 짝 = `test_91_ground_free.py`.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const cache = new Map();
|
|
// 화면 전용 모듈(토스트 · 글) — 기하 계산엔 안 쓰임
|
|
const STUBS = {
|
|
"@ui/ui_template_toast": { showToast: () => undefined },
|
|
};
|
|
|
|
function loadTs(file) {
|
|
const full = path.resolve(file);
|
|
if (cache.has(full)) return cache.get(full);
|
|
if (full.endsWith(".json")) {
|
|
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
|
cache.set(full, data);
|
|
return data;
|
|
}
|
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2020,
|
|
esModuleInterop: true,
|
|
},
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
cache.set(full, box.exports);
|
|
const localRequire = (request) => {
|
|
if (STUBS[request]) return STUBS[request];
|
|
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
|
for (const [prefix, dir] of Object.entries(alias))
|
|
if (request.startsWith(prefix))
|
|
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
|
return loadTs(resolve(path.join(path.dirname(full), request)));
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
cache.set(full, box.exports);
|
|
return box.exports;
|
|
}
|
|
const resolve = (base) =>
|
|
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
|
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
|
) ?? `${base}.ts`;
|
|
|
|
const B06 = (f) => path.join(ROOT, "B06_Section", f);
|
|
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
|
|
|
|
const fixture = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
|
|
);
|
|
const sections = fixture.cross_sections;
|
|
const at = (c) => sections.find((s) => Math.abs(s.chainage_m - c) < 0.6);
|
|
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
const r3 = (v) => Math.round(v * 1000) / 1000;
|
|
const out = {};
|
|
/** 원지반 사본 — [from, to] 안 샘플을 dz 만큼 옮김. */
|
|
const shiftGround = (s, dz, from = -Infinity, to = Infinity) => {
|
|
for (const p of s.samples)
|
|
if (p.offset_m >= from && p.offset_m <= to && p.elevation_m != null) p.elevation_m += dz;
|
|
};
|
|
const layoutsOf = (s, base) =>
|
|
computeStoredLayouts(
|
|
s,
|
|
sections.map((x) => (x === base ? s : x)),
|
|
);
|
|
|
|
// ① 91-2 · 107-2 세월교 바닥 — 기본 · 손 값(측벽 높이)은 원지반을 바꿔도 그대로 · 흙막이 기본 높이도
|
|
const FORD = at(173.087);
|
|
function fordFloor(edit) {
|
|
const s = clone(FORD);
|
|
for (const role of ["inlet", "outlet"]) s.culvert[role].revet_height_m = null;
|
|
edit(s);
|
|
const { ford, culvert } = layoutsOf(s, FORD);
|
|
return ford.sides.map((side) => {
|
|
const wall = culvert.walls.find((w) => w.role === side.role);
|
|
return {
|
|
role: side.role,
|
|
slabTop: r3(side.slabTopElevation),
|
|
wallBottom: wall ? r3(Math.min(...wall.points.map((p) => p.elevation))) : null,
|
|
};
|
|
});
|
|
}
|
|
const hand = (s) => {
|
|
s.design.ford_adjust = {
|
|
inlet: { heightM: 2.0, lateralM: 0, slopeM: 0 },
|
|
outlet: { heightM: 2.4, lateralM: 0, slopeM: 0 },
|
|
};
|
|
};
|
|
out.fordAuto = fordFloor(() => undefined);
|
|
out.fordAutoBedDown = fordFloor((s) => shiftGround(s, -2, -2, 2));
|
|
out.fordHand = fordFloor(hand);
|
|
out.fordHandBedDown = fordFloor((s) => {
|
|
hand(s);
|
|
shiftGround(s, -2, -2, 2);
|
|
});
|
|
// ② 91-3 다단 자동 자리 — 관 180 유출 흙막이 + 2단 · 원지반을 옮겨도 단 자리 같음 · 사면 1.0 m
|
|
const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts"));
|
|
function tiers(dz) {
|
|
const s = clone(at(180));
|
|
shiftGround(s, dz);
|
|
s.culvert = {
|
|
type: "pipe",
|
|
chainage_m: 180,
|
|
pipe_kind: "흄관",
|
|
diameter_m: 1.0,
|
|
min_cover_m: 0.5,
|
|
inlet: { role: "inlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
|
outlet: { role: "outlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
|
};
|
|
s.design.inlet_structure = "revet";
|
|
const tier = { x: null, d: null, h: 2, m: null };
|
|
const layout = computeCulvertLayout(s, s.samples, { extras: [tier, tier] }, "revet");
|
|
return {
|
|
walls: layout.extraWalls.map((w) => [r3(w.bottomBack.offset), r3(w.bottomBack.elevation)]),
|
|
slopes: layout.outletFill.segments.slice(0, 2).map((g) => r3(g.lengthM)),
|
|
};
|
|
}
|
|
out.tiers = [tiers(0), tiers(0.5), tiers(-0.5)];
|
|
|
|
// ⑤ 91-3 전면 기울기 — 단면유형(원지반 자동판정)과 무관 · 성토/절토 칸만
|
|
const { leanOf } = loadTs(B06("B06_Section_UI_Cross_Lean.ts"));
|
|
out.lean = ["left_cut", "right_cut", "both_fill", "both_cut"].map((mode) => [
|
|
leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌" }),
|
|
leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌", face_role: "절토" }),
|
|
]);
|
|
// ③ 91-4 유출측 절토 복합경사 — 꼭짓점 사이 경사(수평/수직) · 사면 길이 · 끝 = 원지반 교점
|
|
const { groundInterpolator } = loadTs(B06("B06_Section_UI_Cross_Culvert_Solve.ts"));
|
|
function profile(points, samples) {
|
|
const groundAt = groundInterpolator(samples);
|
|
const legs = points.slice(1).map((p, i) => {
|
|
const dx = Math.abs(p.offset - points[i].offset);
|
|
const dz = p.elevation - points[i].elevation;
|
|
return { ratio: dz > 1e-9 ? r3(dx / dz) : null, slant: r3(Math.hypot(dx, dz)), run: r3(dx) };
|
|
});
|
|
const end = points[points.length - 1];
|
|
return { legs, endGap: r3(Math.abs(groundAt(end.offset) - end.elevation)) };
|
|
}
|
|
const withBerm = (s) => {
|
|
s.design.berm = { width_m: 0.5, interval_m: 3, slope_deg: 0 };
|
|
};
|
|
// 흙막이 수평공간 뒤(박스 352.14 · 세월교 173.09)
|
|
function guardTails(base, edit = () => undefined) {
|
|
const s = clone(base);
|
|
edit(s);
|
|
const { culvert } = layoutsOf(s, base);
|
|
const tagged = (segments, role) => segments.map((g) => ({ ...g, role }));
|
|
return [
|
|
...tagged(culvert.outletFill.segments, "outlet"),
|
|
...tagged(culvert.basinFill.segments, "inlet"),
|
|
]
|
|
.filter((g) => g.kind === "cut")
|
|
.map((g) => ({ role: g.role, ...profile(g.points, s.samples) }));
|
|
}
|
|
out.guardTails = [...guardTails(at(352.14)), ...guardTails(FORD)];
|
|
// 박스 끝 · 세월교 측벽 끝(흙막이 없음)
|
|
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts"));
|
|
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford_Geom.ts"));
|
|
function endCuts(base, compute) {
|
|
const s = clone(base);
|
|
delete s.culvert;
|
|
s.uphill_side = "left"; // 절토 쪽(우측)을 유출로
|
|
return compute(s, s.samples)
|
|
.sides.filter((side) => side.cutLine)
|
|
.map((side) => profile(side.cutLine, s.samples));
|
|
}
|
|
out.boxEnd = endCuts(at(352.14), computeBoxLayout);
|
|
out.fordEnd = endCuts(FORD, computeFordLayout);
|
|
// 절토 쪽 배관 유출 벽 뒤(관 340 · 유출 = 우측 절토) · 소단 있음/없음
|
|
function cutOutlet(edit = () => undefined) {
|
|
const s = clone(at(340));
|
|
s.uphill_side = "left";
|
|
s.culvert = {
|
|
type: "pipe",
|
|
chainage_m: 340,
|
|
pipe_kind: "흄관",
|
|
diameter_m: 1.0,
|
|
min_cover_m: 0.5,
|
|
inlet: { role: "inlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
|
outlet: { role: "outlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" },
|
|
};
|
|
s.design.inlet_structure = "revet";
|
|
edit(s);
|
|
const layout = computeCulvertLayout(s, s.samples, undefined, "revet");
|
|
const wall = layout.walls.find((w) => w.role === "outlet");
|
|
const pts = layout.designTrim.minSlope.points;
|
|
const from = pts.findIndex((p) => Math.abs(p.offset - wall.points[2].offset) < 1e-9);
|
|
return profile(pts.slice(from), s.samples);
|
|
}
|
|
out.cutOutlet = cutOutlet();
|
|
out.cutOutletBerm = cutOutlet(withBerm);
|
|
process.stdout.write(JSON.stringify(out));
|