- 박스 흙막이 높이 빈 칸 = 박스 높이 값(2 m 박스 → 2 m) · 윗면 = 본체 윗면 · 바닥 = 윗면 − 높이 · 세월교는 그대로 - x 원점(사면 경사길이 4 m 지점) 되돌림 — 손대지 않은 벽(x · d 빈 칸)만 성토사면 제일 위 · 손으로 x 를 넣은 독립 기슭막이 · 옹벽 · 관 보호공은 종전 자리 - 세월교 · 박스 흙막이 조정창 제목 「흙막이(유입 · 유출)」 · 박스 에이프런 툴팁 유입 · 유출 날개벽 이름 바로잡음 - 시험: test_79_guard_shape — 박스 높이 값 · 저장 x 있는 관 보호공 · 독립 기슭막이 = 종전 자리 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
233 lines
8.6 KiB
JavaScript
233 lines
8.6 KiB
JavaScript
/* 79 흙막이 · 세월교 · 박스 형상 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림.
|
|
* 입력 = 73-4 픽스처(검증_sub7 상세 일부) · 결과 = 표준출력 JSON 한 벌. 짝 = `test_79_guard_shape.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, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
|
|
const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts"));
|
|
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts"));
|
|
const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts"));
|
|
const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment.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 = {};
|
|
|
|
/** 벽 윗면 · 바닥 · 전면(관 끝 자리) ↔ 본체 끝 윗면 · 바닥 · 자리. */
|
|
function guardFit(section, list = sections) {
|
|
const layouts = computeStoredLayouts(section, list);
|
|
const base = bodyGuardBase(section, section.samples);
|
|
return (layouts.culvert?.walls ?? []).map((w) => {
|
|
const end = base.ends[w.role];
|
|
const top = Math.max(...w.points.map((p) => p.elevation));
|
|
const bottom = Math.min(...w.points.map((p) => p.elevation));
|
|
return {
|
|
role: w.role,
|
|
outward: w.outward,
|
|
topGap: r3(top - end.top),
|
|
bottomGap: r3(bottom - end.bottom),
|
|
pureHeight: r3(w.height + 0.5),
|
|
endHeight: r3(end.top - end.bottom),
|
|
autoHeight: r3(end.height),
|
|
};
|
|
});
|
|
}
|
|
|
|
// ① 높이 빈 칸 = 본체 끝 높이 · 윗면 = 본체 윗면(세월교 · 박스)
|
|
const blank = (s) => {
|
|
const c = clone(s);
|
|
for (const role of ["inlet", "outlet"]) c.culvert[role].revet_height_m = null;
|
|
return c;
|
|
};
|
|
const boxBlank = blank(at(352.14));
|
|
const fordBlank = blank(at(173.087));
|
|
out.boxAuto = guardFit(
|
|
boxBlank,
|
|
sections.map((s) => (s === at(352.14) ? boxBlank : s)),
|
|
);
|
|
out.fordAuto = guardFit(
|
|
fordBlank,
|
|
sections.map((s) => (s === at(173.087) ? fordBlank : s)),
|
|
);
|
|
// 높이를 적으면 윗면은 그대로 · 바닥이 따라 움직임
|
|
out.boxSet = guardFit(at(352.14));
|
|
|
|
// ② 세월교 흙막이 쪽 ㄴ형 측벽 · 선 걷음
|
|
const fordLayouts = computeStoredLayouts(at(173.087), sections);
|
|
out.fordParts = fordLayouts.ford.sides.map((s) => ({ role: s.role, parts: s.parts.length }));
|
|
const fordOneGuard = clone(at(173.087));
|
|
fordOneGuard.culvert.outlet = { role: "outlet", structure: "" };
|
|
const oneList = sections.map((s) => (s === at(173.087) ? fordOneGuard : s));
|
|
out.fordOneGuardParts = computeStoredLayouts(fordOneGuard, oneList).ford.sides.map((s) => ({
|
|
role: s.role,
|
|
parts: s.parts.length,
|
|
}));
|
|
|
|
// ③ 박스 본체 수평 — 좌 · 우 표고 조정이 달라도
|
|
const box = at(352.14);
|
|
const tilted = computeBoxLayout(box, box.samples, {
|
|
left: { lengthM: 0, riseM: 0.6 },
|
|
right: { lengthM: 0, riseM: 0 },
|
|
});
|
|
out.boxLevel = {
|
|
left: r3(tilted.sides[0].topElevation),
|
|
right: r3(tilted.sides[1].topElevation),
|
|
slope: tilted.slopeRatio,
|
|
};
|
|
|
|
// ④ 박스 날개벽 쪽 끝 절 · 성토선(흙막이 없는 박스)
|
|
const boxWing = clone(box);
|
|
delete boxWing.culvert;
|
|
const wingLayout = computeBoxLayout(boxWing, boxWing.samples);
|
|
out.boxWing = wingLayout.sides.map((s) => ({
|
|
role: s.role,
|
|
cut: s.cutLine ? 1 : 0,
|
|
fill: s.fillSegments.length,
|
|
}));
|
|
|
|
// ⑤ 흙막이 끝 절토 = 수평공간 뒤 절토 경사 — 수평공간 칸 값을 따름
|
|
function cutTails(section, list) {
|
|
const culvert = computeStoredLayouts(section, list).culvert;
|
|
return [...culvert.outletFill.segments, ...culvert.basinFill.segments]
|
|
.filter((g) => g.kind === "cut")
|
|
.map((g) => ({
|
|
points: g.points.length,
|
|
berm: r3(Math.abs(g.points[1].offset - g.points[0].offset)),
|
|
flat: r3(g.points[1].elevation - g.points[0].elevation),
|
|
}));
|
|
}
|
|
out.cutDefault = [cutTails(at(352.14), sections), cutTails(at(173.087), sections)];
|
|
const berm2 = clone(at(352.14));
|
|
berm2.culvert.inlet.revet_berm_m = 2;
|
|
berm2.culvert.outlet.revet_berm_m = 2;
|
|
out.cutBerm2 = cutTails(
|
|
berm2,
|
|
sections.map((s) => (s === at(352.14) ? berm2 : s)),
|
|
);
|
|
|
|
// ⑥ 관 보호공 기본 자리 = 제일 위(성토선 0 지점 · d 0) · 관 길이 m 단위
|
|
const pipe = clone(at(180));
|
|
pipe.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: "돌쌓기(찰)" },
|
|
};
|
|
pipe.design.inlet_structure = "revet";
|
|
const pipeLayout = computeCulvertLayout(pipe, pipe.samples, undefined, "revet");
|
|
out.pipe = pipeLayout
|
|
? {
|
|
d: { inlet: pipeLayout.revetShift.inlet.d, outlet: pipeLayout.revetShift.outlet.d },
|
|
walls: pipeLayout.walls.map((w) => ({
|
|
role: w.role,
|
|
topBelowEdge: r3(
|
|
(w.outward > 0 ? pipe.design.road_edges.left : pipe.design.road_edges.right).elevation_m -
|
|
w.topJoint.elevation,
|
|
),
|
|
})),
|
|
lengthM: pipeLayout.pipe.lengthM,
|
|
}
|
|
: null;
|
|
|
|
// ⑥-2 손으로 x 를 넣은 관 보호공 = 종전 자리(d 기본 = 경사길이 4m 지점 · x 원점 그대로 · 79b)
|
|
const pipeX = computeCulvertLayout(
|
|
pipe,
|
|
pipe.samples,
|
|
{ outlet: { x: 1, d: null, h: null, m: null } },
|
|
"revet",
|
|
);
|
|
out.pipeStoredX = pipeX ? { x: pipeX.revetShift.outlet.x, d: r3(pipeX.revetShift.outlet.d) } : null;
|
|
|
|
// ⑦ 독립 기슭막이 · 옹벽 기본 자리 = 제일 위(d 0)
|
|
const revet = clone(at(180));
|
|
revet.revetment = {
|
|
structure_id: "t",
|
|
type_id: "masonry_wet",
|
|
name: "t",
|
|
start_m: 170,
|
|
end_m: 190,
|
|
anchor_m: 180,
|
|
form: "돌쌓기(찰)",
|
|
height_m: 2,
|
|
side: "우",
|
|
};
|
|
const own = computeRevetmentLayout(revet, null);
|
|
out.revet = own
|
|
? {
|
|
d: own.appliedAdjust.d,
|
|
topBelowEdge: r3(revet.design.road_edges.right.elevation_m - own.top.elevation),
|
|
linked: own.linked,
|
|
}
|
|
: null;
|
|
// 저장 x 가 있는 벽 = 종전 자리 그대로(x 원점 = 경사길이 4m 지점 · d 빈 칸 = 종전 기본 · 79b)
|
|
const autoX = computeRevetmentLayout(revet, null).walls[0].bottomBack.offset;
|
|
const storedX = computeRevetmentLayout(revet, { x: 1, d: null });
|
|
const storedXD = computeRevetmentLayout(revet, { x: 1, d: 1 });
|
|
out.revetStoredX = {
|
|
x: storedX.appliedAdjust.x,
|
|
d: storedX.appliedAdjust.d,
|
|
// 노견 → 4m 지점(3.07) + x 1 = 제일 위 자리에서 바깥 4.07 m
|
|
shift: r3(Math.abs(storedX.walls[0].bottomBack.offset - autoX)),
|
|
shiftXD: r3(Math.abs(storedXD.walls[0].bottomBack.offset - autoX)),
|
|
};
|
|
const neighbour = clone(revet);
|
|
neighbour.chainage_m = 170;
|
|
out.revetLinked = computeRevetmentLayout(neighbour, null)?.linked ?? null;
|
|
|
|
out.areas = structureAreaRows(sections).filter((r) =>
|
|
[173.087, 352.14].some((c) => Math.abs(r.chainage_m - c) < 0.6),
|
|
);
|
|
process.stdout.write(JSON.stringify(out, null, 1));
|