- 까닭: 폭 → 높이 계산은 설계유량(담당 유역)이 있어야 돎 — 유량 없는 지점은 아무 일도 안 일어나고 안내도 없었음 · 높이를 비우면 안 채움 - 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
246 lines
8.8 KiB
JavaScript
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));
|