/* 194-1 · 194-2 소단 고름 헬퍼 — `_Cross_Design_Pick` 을 그 자리에서 트랜스파일해 흉내 DOM 으로 돌림. * 소단 = 높이만(▲▼ · ◀▶ 막힘 · 노면 끝 위 금지) · 조각 키로 다시 그려도 같은 조각 · 좌측 상자 알림. 짝 = `test_194_1_berm_depth.py`. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); class Node { constructor(tag) { this.tagName = tag; this.children = []; this.attrs = {}; this.listeners = {}; this.hidden = false; this.isConnected = true; this._text = ""; this._cls = new Set(); const self = this; this.classList = { add: (...n) => n.forEach((c) => self._cls.add(c)), toggle: (c, on) => ((on ?? !self._cls.has(c)) ? self._cls.add(c) : self._cls.delete(c)), contains: (c) => self._cls.has(c), }; } get descendants() { return this.children.flatMap((c) => [c, ...c.descendants]); } /** `.이름` 한 개만 — 패널 공용 부품이 슬롯 단추를 찾는 꼴. */ querySelector(sel) { return this.descendants.find((n) => n._cls.has(sel.slice(1))) ?? null; } getClientRects() { return this._cls.has("is-hidden") ? [] : [{}]; } set className(v) { this._cls = new Set(String(v).split(/\s+/).filter(Boolean)); } set textContent(v) { this._text = String(v); } get textContent() { return this._text + this.children.map((c) => c.textContent).join(""); } append(...kids) { this.children.push(...kids); } insertBefore(kid) { this.children.push(kid); } setAttribute(n, v) { this.attrs[n] = String(v); if (n === "class") this.className = v; } getAttribute(n) { return this.attrs[n] ?? null; } hasAttribute(n) { return n in this.attrs; } addEventListener(t, fn) { (this.listeners[t] ??= []).push(fn); } click() { (this.listeners.click ?? []).forEach((fn) => fn({ stopPropagation: () => undefined })); } } const docListeners = {}; globalThis.document = { createElement: (t) => new Node(t), createElementNS: (_n, t) => new Node(t), addEventListener: (t, fn) => (docListeners[t] ??= []).push(fn), removeEventListener: (t, fn) => { docListeners[t] = (docListeners[t] ?? []).filter((f) => f !== fn); }, }; const key = (name) => [...(docListeners.keydown ?? [])].forEach((fn) => fn({ key: name, target: null, preventDefault: () => undefined }), ); const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (r) => r.startsWith("@util/") ? loadTs(path.join(ROOT, "common_util", `${r.slice(6)}.ts`)) : loadTs(path.join(path.dirname(full), `${r}.ts`)), }); globalThis.Element = Node; const pick = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Design_Pick.ts")); const x = (o) => 100 - o * 12; // +offset = 화면 왼쪽 const y = (e) => 900 - (e - 100) * 12; const line = (pts, structure = false) => { const el = new Node("polyline"); el.setAttribute("class", "b06-chart__design-cross"); el.setAttribute("points", pts.map(([o, e]) => `${x(o)},${y(e)}`).join(" ")); if (!structure) el.setAttribute(pick.DESIGN_LINE_ATTR, ""); return el; }; /** 방향키 한 번 — 막혔나(preventDefault) 돌려줌. */ const press = (name) => { let prevented = false; [...(docListeners.keydown ?? [])].forEach((fn) => fn({ key: name, target: null, preventDefault: () => (prevented = true) }), ); return prevented; }; /** 양성토 단면(노면 끝 표고 100) — 좌 소단 깊이 `dl`(노면 끝 아래) · 우 1.0 · 사면 1:1.5 · 끝 표고 97. `order` = 그리는 차례. */ function card({ dl = 1, stored = undefined, order = ["left", "road", "right"], stationId } = {}) { for (const k of Object.keys(docListeners)) if (k === "keydown") docListeners[k] = []; const design = { road_edges: { left: { offset_m: 2, elevation_m: 100 }, right: { offset_m: -2, elevation_m: 100 }, }, ditch: null, ditch_side: null, ditch_enabled: false, section_mode: "both_fill", berm: { width_m: 0.5, interval_m: 3, slope_deg: 0, depth_m: 1 }, }; if (stored !== undefined) design.berm_depth_left_m = stored; const slope = (s, d) => { const k = s === "left" ? 1 : -1; const knee = 2 + 1.5 * d; const toe = knee + 0.5 + 1.5 * (100 - d - 97); return [ [k * toe, 97], [k * (knee + 0.5), 100 - d], [k * knee, 100 - d], [k * 2, 100], ]; }; const lines = { left: line(slope("left", dl)), road: line([ [2, 100], [0, 100.04], // 노면 횡단경사 2 % — 깊이 0 소단이 노면과 한 직선으로 안 합쳐짐 [-2, 100], ]), right: line(slope("right", 1)), }; const layer = new Node("g"); layer.append(...order.map((name) => lines[name])); const values = { left: 0, right: 0 }; const sets = []; const focus = []; const handle = pick.attachDesignLinePick({ layer, design, x, toDisplayY: y, onPick: () => undefined, shift: { valueOf: (s) => values[s], set: (s, v) => (values[s] = v) }, berm: { set: (s, v) => sets.push([s, v]), focus: (on) => focus.push(on) }, stationId, }); const find = (test) => handle.pieces.findIndex(test); const mid = (p) => (p.from.offset + p.to.offset) / 2; const at = { leftBerm: find((p) => pick.isBermPiece(design, p) && mid(p) > 0), rightBerm: find((p) => pick.isBermPiece(design, p) && mid(p) < 0), leftUpper: find((p) => !pick.isBermPiece(design, p) && mid(p) > 2 && mid(p) < 2 + 1.5 * dl), road: find((p) => mid(p) > 0 && mid(p) < 2), }; // 아래 띠 공용 칸(370-3) — 버튼 다섯 칸 한 벌(보임만 바뀜) · 제목 · 값 한 줄 const slot = (name) => handle.cell.descendants.find((n) => n._cls.has(`b06-cross-card__cell-btn--${name}`)); const rows = () => [ slot("left").hidden && slot("right").hidden, slot("up").hidden && slot("down").hidden, ]; const text = (what) => what === "title" ? handle.cell.children[0].textContent : handle.cell.children[1].children.at(-1).textContent; return { design, handle, values, sets, focus, at, slot, rows, text }; } const out = {}; // ① 갈래 · 축 — 소단 = depth · 사면 = x · 노면 = y · 키 { const c = card(); out.axis = Object.fromEntries( Object.entries(c.at).map(([name, i]) => [ name, pick.pieceMoveAxis(c.design, c.handle.pieces[i]), ]), ); out.keys = pick.pieceKeys(c.design, c.handle.pieces).sort(); } // ② 소단 고름 — ▲▼ 만 · ◀▶ 막힘 · ↺ = 구간 기본 · 좌측 상자 켬 알림 { const c = card(); c.handle.setActive(c.at.leftBerm); const [moveHidden, liftHidden] = c.rows(); const leftBlocked = press("ArrowLeft"); const rightBlocked = press("ArrowRight"); const afterSide = { values: { ...c.values }, sets: c.sets.length }; const upTaken = press("ArrowUp"); press("ArrowDown"); c.slot("reset").click(); out.berm = { title: c.text("title"), readout: c.text("value"), moveHidden, liftHidden, leftBlocked, rightBlocked, afterSide, upTaken, sets: c.sets, focus: c.focus, }; c.handle.setActive(c.at.leftUpper); // 사면으로 바꿈 — 소단 상자 끔 · ▲▼ 막힘 · 깊이 안 바뀜 const before = c.sets.length; out.berm.slopeUpBlocked = press("ArrowUp"); out.berm.slopeSets = c.sets.length - before; out.berm.slopeTitle = c.text("title"); out.berm.focusAfter = c.focus.at(-1); // 노면 = ▲▼ 는 종단도로 넘김(안 막음) c.handle.setActive(c.at.road); out.berm.roadUpPassed = !press("ArrowUp"); out.berm.roadSets = c.sets.length - before; } // ③ 도로 위 금지 = 0 에서 멈춤 · 사면 끝 앞에서 멈춤 { const top = card({ dl: 0, stored: 0 }); top.handle.setActive(top.at.leftBerm); press("ArrowUp"); const deep = card({ dl: 2.9, stored: 2.9 }); deep.handle.setActive(deep.at.leftBerm); press("ArrowDown"); // 2.9 → 3.0 = 사면 끝(97) — 막힘 press("ArrowUp"); out.limit = { topSets: top.sets, topFound: top.at.leftBerm >= 0, deepSets: deep.sets }; } // ④ 옮긴 뒤 다시 그려도 같은 조각(194-2) — 그리는 차례 · 깊이가 바뀌어도 { const first = card({ stationId: "S1" }); pick.setPickedLine("S1", first.at.leftBerm); const again = card({ dl: 1.1, stored: 1.1, order: ["right", "road", "left"], stationId: "S1", }); const restored = pick.pickedLineAt("S1"); pick.setPickedLine("S1", again.at.leftUpper); const third = card({ dl: 1.2, stored: 1.2, stationId: "S1" }); out.stable = { firstIndex: first.at.leftBerm, againIndex: again.at.leftBerm, restored, upperRestored: pick.pickedLineAt("S1") === third.at.leftUpper, upperIndexMoved: again.at.leftUpper !== third.at.leftUpper, }; pick.setPickedLine("S1", null); } // ⑤ 카드 창구 — 소단 깊이 변경 · 좌측 상자 알림(한 번만 · 이 측점이 켠 때만 끔) { const changes = []; const design = { ground_type: "soil", section_mode: "both_fill", shoulder_extend_left_m: 0.3 }; const moves = pick.designLineMoves({ station_id: "S9", chainage_m: 40, design }, (ch, change) => changes.push([ch, change]), ); moves.berm.set("left", 1.3); const host = []; pick.setBermPickHost((ch) => host.push(ch)); moves.berm.focus(true); moves.berm.focus(true); // 다시 그린 카드 — 안 부름 pick .designLineMoves({ station_id: "S8", chainage_m: 20, design }, () => undefined) .berm.focus(false); moves.berm.focus(false); out.moves = { stationId: moves.stationId, shiftValue: moves.shift.valueOf("left"), change: changes[0], host, none: pick.designLineMoves({ station_id: "S7", chainage_m: 0, design }), }; pick.setBermPickHost(null); } process.stdout.write(JSON.stringify(out));