Files
Aislo/resources/tester/helper_287_ford_pavement.cjs
T
eomsangdonandClaude Opus 5.5 6c86612f65 fix(b05,b06): 월류 폭 → 높이 — 세월교 · 물넘이 한 벌 overflowSync · 안내 글 · 높이 비우면 다시 채움 (287-3)
- 까닭: 폭 → 높이 계산은 설계유량(담당 유역)이 있어야 돎 — 유량 없는 지점은 아무 일도 안 일어나고 안내도 없었음 · 높이를 비우면 안 채움
- overflowSync(design_structure_ford_calc) — 폭 바꿈 = 필요 수심 · 높이 비움 · 그 아래 = 필요 수심 · 상자 끝 안내 「필요 수심 n 이상」 · 「설계유량 없음 — 수기 입력」 · 유량이 오면 안내 다시 그림
- 세월교 · 물넘이 폼이 같은 것을 씀 · Facility.ts 줄 줄임(697 → 686)
- 시험 test_287 화면 길(폼 onChange 훅) — 유량 있음 · 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-12 21:24:35 +09:00

246 lines
8.8 KiB
JavaScript

/* 287 물넘이포장 후속 헬퍼 — 진짜 TS 를 Node 로 돌려 표준출력 JSON 한 벌로.
* 상대 import · `@config/` 는 따라 옮김.
*/
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 load = (file, stubs = {}) => {
if (cache.has(file)) return cache.get(file);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(file, box.exports);
const near = (m) => {
if (stubs[m]) return stubs[m];
if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`));
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
return new Proxy({}, { get: () => () => {} });
};
new Function("exports", "module", "require", js)(box.exports, box, near);
return box.exports;
};
const D = (f) => path.join(ROOT, "A00_Common", "design", f);
const calc = load(D("design_structure_ford_calc.ts"));
/** 폼 흉내 — 상태 · setValue 만. */
const formOf = (slope, chute) => {
const state = { slope: [{ ...slope }], chute: [{ ...chute }] };
return { state: () => state, setValue: (c, i, v) => (state[c][0][i] = v) };
};
const change = (form, c, i) => {
calc.syncChuteArea(form, { id: c }, { id: i });
return form.state().chute[0].area;
};
// 287-2 · 287-4 그림 — DOM 흉내(속성 · 자식만)
class Node {
constructor(tag) {
this.tag = tag;
this.attrs = {};
this.children = [];
this.dataset = {};
this.style = {};
}
setAttribute(k, v) {
this.attrs[k] = String(v);
}
getAttribute(k) {
return this.attrs[k] ?? null;
}
hasAttribute(k) {
return k in this.attrs;
}
append(...kids) {
this.children.push(...kids);
}
insertBefore(kid, ref) {
const at = ref ? this.children.indexOf(ref) : -1;
if (at < 0) this.children.push(kid);
else this.children.splice(at, 0, kid);
}
get classList() {
const names = () => (this.attrs.class ?? "").split(" ").filter(Boolean);
return {
contains: (c) => names().includes(c),
toggle: (c, on) => {
const rest = names().filter((n) => n !== c);
this.attrs.class = (on ? [...rest, c] : rest).join(" ");
},
};
}
addEventListener(type, fn) {
(this.listeners ??= {})[type] = fn;
}
querySelectorAll(selector) {
const attr = /^\[([\w-]+)\]$/.exec(selector)?.[1];
const all = this.children.flatMap((k) => [k, ...k.querySelectorAll(selector)]);
return all.filter((k) => attr && k.hasAttribute(attr));
}
}
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
// 서버가 지대 낮은 쪽(우)으로 5 % 기울인 노면 · 노견 끝 ±3 · 차도 끝 ±2.5
const tilted = {
section_mode: "left_cut",
tilted: true,
cross_slope_pct: 5,
design_elevation_m: 100,
pavement_thickness_m: 0.2,
paved: true,
road_edges: {
left: { offset_m: 3, elevation_m: 100.15 },
right: { offset_m: -3, elevation_m: 99.85 },
},
carriageway_edges: {
left: { offset_m: 2.5, elevation_m: 100.125 },
right: { offset_m: -2.5, elevation_m: 99.875 },
},
design_line: [
{ offset_m: 6, elevation_m: 103 },
{ offset_m: 3, elevation_m: 100.15 },
{ offset_m: -3, elevation_m: 99.85 },
{ offset_m: -7, elevation_m: 97.2 },
],
};
const svg = new Node("g");
const drew = pave.appendFordPavementOverlay(
svg,
{ span_m: 8, depth_m: 0.4, slope_pct: 5 },
tilted,
(o) => o,
(e) => e,
);
const ofClass = (c) => svg.children.filter((k) => (k.attrs.class ?? "").split(" ").includes(c));
// 287-5 형상 고름 — 판정면 차례 · 누름 = 고름 · 끌기 = 고름 아님 · 노면공 포장층 안 섞임 · 강조
const shapePick = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts"));
const layer = new Node("g");
const fordShape = new Node("polygon");
fordShape.setAttribute("class", "b06-chart__pavement b06-chart__ford-pavement");
fordShape.setAttribute("data-structure-id", "pipe-230.00");
fordShape.setAttribute("points", "0,0 1,0 1,1");
const paveShape = new Node("polygon"); // 노면공 포장층 — 범위 표식이 있어 형상 고름 밖
paveShape.setAttribute("data-structure-id", "pave-1");
paveShape.setAttribute("data-pave-type", "pavement_concrete");
const structureHit = new Node("polygon");
structureHit.setAttribute("class", "b06-chart__culvert-revet-hit");
layer.append(fordShape, paveShape, structureHit);
const picked = [];
const setShape = shapePick.attachShapePick(layer, (id) => picked.push(id));
// 보라 손잡이 판정은 뒤에 붙음 — 설계선 고름과 같은 자리(구조물 판정면 앞)에 꽂음
const purple = new Node("polyline");
purple.setAttribute("class", "b06-chart__design-hit");
layer.insertBefore(
purple,
layer.children.find((k) => k.classList.contains("b06-chart__culvert-revet-hit")),
);
const shapeHit = layer.children.find((k) => k.classList.contains("b06-chart__shape-hit"));
const press = (dx) => {
const ev = {
clientX: 10,
clientY: 10,
stopped: false,
stopPropagation() {
this.stopped = true;
},
};
shapeHit.listeners.pointerdown(ev);
const up = {
...ev,
clientX: 10 + dx,
stopPropagation() {
up.stopped = true;
},
};
shapeHit.listeners.click(up);
return up.stopped;
};
const clickStops = press(0);
const dragStops = press(12);
setShape(true);
const activeAfterSet = fordShape.classList.contains("is-active");
shapePick.highlightShapes(layer, "other");
const activeAfterOther = fordShape.classList.contains("is-active");
shapePick.highlightShapes(layer, "pipe-230.00");
const activeAfterSame = fordShape.classList.contains("is-active");
// 물넘이 포장층 표식 = 관 지점 알약 id
const tagged = new Node("g");
pave.appendFordPavementOverlay(
tagged,
{ chainage_m: 230, span_m: 8, depth_m: 0.4, slope_pct: 5 },
tilted,
(o) => o,
(e) => e,
);
// 287-3 화면 길 — 폼 onChange 훅 그대로(세월교 · 물넘이 한 벌 overflowSync)
globalThis.document.createElement = (tag) => new Node(tag);
const overflowRun = (flowValue) => {
const state = { slope: [{ width: "10", height: "0.05" }] };
const form = {
state: () => state,
setValue: (c, i, v) => (state[c][0][i] = v),
refresh: () => undefined,
};
let emitted = 0;
const sync = calc.overflowSync(
() => form,
"slope",
() => flowValue,
() => (emitted += 1),
);
const edit = (item, value) => {
state.slope[0][item] = value; // 폼이 값을 적고 훅을 부름(design_structure_form onEdit)
sync.onChange({ id: "slope" }, { id: item });
return state.slope[0].height;
};
return {
afterWidth8: edit("width", "8"),
afterClear: edit("height", ""),
afterLow: edit("height", "0.01"),
afterHigh: edit("height", "0.9"),
hint: sync.fields({ id: "slope" }).map((el) => el.textContent),
otherBoxHint: sync.fields({ id: "chute" }).length,
emitted,
};
};
const out = {
overflowFlow: overflowRun(14.5776),
overflowNoFlow: overflowRun(null),
shapeOrder: layer.children.map((k) =>
(k.attrs.class ?? k.attrs["data-structure-id"]).split(" ").pop(),
),
picked,
clickStops,
dragStops,
activeAfterSet,
activeAfterOther,
activeAfterSame,
pavedShapeIsPicked: picked.includes("pave-1"),
fordTag: tagged.children
.filter((k) => (k.attrs.class ?? "").includes("ford-pavement"))
.map((k) => k.attrs["data-structure-id"]),
drew,
pavePoints: ofClass("b06-chart__ford-pavement").map((k) => k.attrs.points),
planPoints: ofClass("b06-chart__ford-deck-plan").map((k) => k.attrs.points),
inflowLeft: pave.inflowLeft(tilted),
// 287-3 월류 폭 → 높이 = 세월교와 같은 풀이(유량 1.2 ㎥/s · 폭 10 m) · 유량 없으면 null(손 입력)
heightByWidth: calc.fordHeightM(1.2, 10),
heightNoFlow: calc.fordHeightM(null, 10),
paveSlopeItems: load(D("design_structure_ford_pavement.ts"))
.FORD_PAVEMENT_SPEC.containers.find((c) => c.id === "slope")
.items.map((i) => i.id),
// 287-1 길이 넣음 = 월류 폭 × 길이 · 비움 = 기본값 · 폭 바꿈 = 길이 있을 때만 다시 셈 · 폭 비면 기본 폭
areaByLength: change(formOf({ width: "6" }, { length_m: "4", area: "30" }), "chute", "length_m"),
areaCleared: change(formOf({ width: "6" }, { length_m: "", area: "24" }), "chute", "length_m"),
areaByWidth: change(formOf({ width: "8" }, { length_m: "2.5", area: "15" }), "slope", "width"),
areaWidthNoLength: change(formOf({ width: "8" }, { length_m: "", area: "17" }), "slope", "width"),
areaNoWidth: change(formOf({ width: "" }, { length_m: "3", area: "30" }), "chute", "length_m"),
};
process.stdout.write(JSON.stringify(out));