/* 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")); // 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, }; }; // 301-1 폼 훅 그대로(B05 물넘이 paveForm · 세월교 fordForm onChange = overflowSync onChange) const areaRun = (widthBox, chuteBox) => { const state = { [widthBox]: [{ width: "6" }], [chuteBox]: [{ length_m: "", area: "17" }] }; const form = { state: () => state, setValue: (c, i, v) => (state[c][0][i] = v), refresh: () => undefined, }; const sync = calc.overflowSync( () => form, widthBox, () => 14.5776, () => undefined, ); const edit = (box, item, value) => { state[box][0][item] = value; sync.onChange({ id: box }, { id: item }); return state[chuteBox][0].area; }; return [ edit(chuteBox, "length_m", "4"), edit(widthBox, "width", "8"), edit(chuteBox, "length_m", ""), ]; }; 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), // 301-1 면적 = 늘 손 입력 — 길이 · 월류 폭을 바꾸거나 길이를 비워도 면적 그대로(물넘이 · 세월교 폼 훅) areaKeptPave: areaRun("slope", "chute"), areaKeptFord: areaRun("ford", "chute_add"), }; process.stdout.write(JSON.stringify(out));