/* 139-6 고른 설계선 사면 이동 헬퍼 — `_Cross_Design_Pick.attachDesignLinePick` 을 그 자리에서 * 트랜스파일해 흉내 DOM 으로 돌림 · 151-3 이동 패널(◀ ↺ ▶ · 방향키 ←→). 짝 = `test_139_6_line_shift.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(); 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 }), ); function loadTs(file) { const full = path.resolve(file); if (cache.has(full)) return cache.get(full); const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(full, box.exports); const req = (r) => r.startsWith("@util/") ? loadTs(path.join(ROOT, "common_util", `${r.slice(6)}.ts`)) : loadTs(path.join(path.dirname(full), `${r}.ts`)); new Function("exports", "module", "require", js)(box.exports, box, req); cache.set(full, box.exports); return box.exports; } 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; }; const pointer = (inProfile) => [...(docListeners.pointerdown ?? [])].forEach((fn) => fn({ target: Object.assign(new Node("g"), { closest: () => (inProfile ? {} : null) }) }), ); /** 단면 하나 — 좌 사면 · 노면 · 우 사면(+ 좌 구조물 트림 선). */ function card(sectionMode, ditch) { 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: ditch ? { type: "standard", top_width_m: 1 } : null, ditch_side: "left", ditch_enabled: !!ditch, section_mode: sectionMode, }; const layer = new Node("g"); layer.append( line([ [6, sectionMode === "left_cut" ? 103 : 97], [2, 100], ]), line([ [2, 100], [-2, 100], ]), line([ [-2, 100], [-6, 97], ]), line( [ [7, 96], [7, 94], ], true, ), ); const values = { left: 0, right: 0 }; const handle = pick.attachDesignLinePick({ layer, design, x, toDisplayY: y, onPick: () => undefined, shift: { valueOf: (s) => values[s], set: (s, v) => (values[s] = v) }, }); const at = (offset, structure = false) => handle.pieces.findIndex( (p) => p.structure === structure && Math.min(p.from.offset, p.to.offset) <= offset && offset <= Math.max(p.from.offset, p.to.offset), ); const slot = (name) => handle.panel.querySelector(`.b06-structure-panel__btn--${name}`); const title = () => handle.panel.descendants.find((n) => n._cls.has("b06-structure-panel__title")).textContent; const rowHidden = () => handle.panel.descendants .find((n) => n._cls.has("b06-structure-panel__struct")) ._cls.has("is-hidden"); return { design, handle, values, at, slot, title, rowHidden }; } const out = {}; // ① 성토측(both_fill) — 좌 사면: 가로만 · 바깥만 · 중심 못 넘음 · ▲▼ 막힘 { const c = card("both_fill", false); out.axis = { left: pick.pieceMoveAxis(c.design, c.handle.pieces[c.at(4)]), road: pick.pieceMoveAxis(c.design, c.handle.pieces[c.at(0)]), right: pick.pieceMoveAxis(c.design, c.handle.pieces[c.at(-4)]), structure: pick.pieceMoveAxis(c.design, c.handle.pieces[c.at(7, true)]), }; c.handle.setActive(c.at(4)); const upBlocked = press("ArrowUp"); const downBlocked = press("ArrowDown"); const afterUpDown = { ...c.values }; press("ArrowLeft"); // 화면 왼쪽 = 좌 바깥 press("ArrowLeft"); const out2 = { ...c.values }; for (let i = 0; i < 30; i += 1) press("ArrowRight"); // 안쪽 끝까지 — 0 에서 멈춤 const inward = { ...c.values }; out.fill = { upBlocked, downBlocked, afterUpDown, out2, inward, title: c.title(), rowHidden: c.rowHidden(), }; // 종단도를 마지막에 눌렀으면 방향키는 종단도 몫 — 막지 않음 pointer(true); out.fill.profileUp = press("ArrowUp"); pointer(false); // ② 구조물 트림 선(성토측) = 같은 쪽 노견째 · 반대측 고정 c.handle.setActive(c.at(7, true)); c.slot("left").click(); out.structure = { values: { ...c.values }, title: c.title(), upBlocked: press("ArrowUp") }; // 우 사면 = 우 바깥(화면 오른쪽)만 · 좌 값 그대로 c.handle.setActive(c.at(-4)); press("ArrowLeft"); // 안쪽 — 0 press("ArrowRight"); out.right = { values: { ...c.values }, title: c.title() }; // ④ 도로측 — 높이만: ▲▼ 는 종단도로 넘김(안 막음) · ◀▶ 막힘 · 값 그대로 · 이동 줄 숨김 const before = { ...c.values }; c.handle.setActive(c.at(0)); out.road = { upPassed: !press("ArrowUp"), downPassed: !press("ArrowDown"), leftBlocked: press("ArrowLeft"), rightBlocked: press("ArrowRight"), same: JSON.stringify(before) === JSON.stringify(c.values), title: c.title(), rowHidden: c.rowHidden(), label: c.handle.label.textContent, }; // 고름 풀면 아무 키도 안 막음 c.handle.setActive(null); out.released = [press("ArrowUp"), press("ArrowLeft")]; } // ③ 절토측(left_cut · 좌 = 절토 + 측구) — 같은 규칙 반대로 { const c = card("left_cut", true); out.cutZone = pick.pieceZone(c.design, c.handle.pieces[c.at(4)]).zone; c.handle.setActive(c.at(4)); const upBlocked = press("ArrowUp"); c.slot("left").click(); // 좌 바깥 const outward = { ...c.values }; c.slot("right").click(); c.slot("right").click(); // 안쪽 — 0 에서 멈춤 out.cut = { upBlocked, outward, inward: { ...c.values }, axis: pick.pieceMoveAxis(c.design, c.handle.pieces[c.at(4)]), }; } process.stdout.write(JSON.stringify(out));