- helper_ts_stub_loader: 옛 헬퍼가 저마다 다르게 쓰던 것(풀이 함수 직접 · esModuleInterop · 캐시 끔 · 파일 통째 대역)을 옵션으로 · 기본 풀이는 그대로 - 헬퍼 동작 그대로(풀이 함수 본문은 옮겨 담음) · 제품 코드 안 고침 - 확인: 이 묶음 헬퍼를 쓰는 시험 241 개 전 241 통과 → 후 241 통과 (비교 = 시험별 junit) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
189 lines
5.4 KiB
JavaScript
189 lines
5.4 KiB
JavaScript
/* 139-6 고른 설계선 사면 이동 헬퍼 — `_Cross_Design_Pick.attachDesignLinePick` 을 그 자리에서
|
|
* 트랜스파일해 흉내 DOM 으로 돌림 · 151-3 이동 패널(◀ ↺ ▶ · 방향키 ←→). 짝 = `test_139_6_line_shift.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`)),
|
|
});
|
|
|
|
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 design = {
|
|
road_edges: {
|
|
left: { offset_m: 2, elevation_m: 100 },
|
|
right: { offset_m: -2, elevation_m: 100 },
|
|
},
|
|
ditch: null,
|
|
ditch_side: "left",
|
|
ditch_enabled: false,
|
|
section_mode: "both_fill",
|
|
};
|
|
const line = (pts) => {
|
|
const el = new Node("polyline");
|
|
el.setAttribute("class", "b06-chart__design-cross");
|
|
el.setAttribute("points", pts.map(([o, e]) => `${x(o)},${y(e)}`).join(" "));
|
|
el.setAttribute(pick.DESIGN_LINE_ATTR, "");
|
|
return el;
|
|
};
|
|
const layer = new Node("g");
|
|
// 좌 성토 사면 · 노면 · 우 성토 사면
|
|
layer.append(
|
|
line([
|
|
[6, 97],
|
|
[2, 100],
|
|
]),
|
|
line([
|
|
[2, 100],
|
|
[-2, 100],
|
|
]),
|
|
line([
|
|
[-2, 100],
|
|
[-6, 97],
|
|
]),
|
|
);
|
|
const values = { left: 0, right: 0 };
|
|
const picks = [];
|
|
const handle = pick.attachDesignLinePick({
|
|
layer,
|
|
design,
|
|
x,
|
|
toDisplayY: y,
|
|
onPick: (i) => picks.push(i),
|
|
shift: {
|
|
valueOf: (side) => values[side],
|
|
set: (side, v) => (values[side] = v),
|
|
},
|
|
});
|
|
// 아래 띠 공용 칸(370-3) — 제목 · 버튼 · 값 한 줄
|
|
const cell = handle.cell;
|
|
const slot = (name) => cell.querySelector(`.b06-cross-card__cell-btn--${name}`);
|
|
const cellTitle = () => cell.children[0].textContent;
|
|
const cellValue = () => cell.children[1].children.at(-1).textContent;
|
|
const pieceAt = (offset) =>
|
|
handle.pieces.findIndex(
|
|
(p) =>
|
|
Math.min(p.from.offset, p.to.offset) <= offset &&
|
|
offset <= Math.max(p.from.offset, p.to.offset),
|
|
);
|
|
const hiddenAtStart = cell.classList.contains("is-empty");
|
|
const log = [];
|
|
handle.setActive(pieceAt(4)); // 좌 사면
|
|
const titleLeft = cellTitle();
|
|
slot("left").click(); // 화면 왼쪽 = 좌측 바깥 → +0.1
|
|
key("ArrowLeft"); // 방향키 = 십자
|
|
log.push({ ...values });
|
|
key("ArrowRight"); // 안쪽 → 0.1
|
|
slot("right").click();
|
|
key("ArrowRight"); // 0 아래 금지
|
|
log.push({ ...values });
|
|
handle.setActive(pieceAt(-4)); // 우 사면
|
|
key("ArrowLeft"); // 화면 왼쪽 = 우측 안쪽 → 0 에서 멈춤
|
|
slot("right").click(); // 우측 바깥 → +0.1
|
|
log.push({ ...values });
|
|
handle.setActive(pieceAt(-4));
|
|
const text = cellValue();
|
|
const slots = ["left", "up", "reset", "down", "right"]
|
|
.map((n) => slot(n))
|
|
.filter((b) => !b.hidden)
|
|
.map((b) => b.textContent);
|
|
slot("reset").click(); // ↺ = 그 쪽 0
|
|
const afterReset = { ...values };
|
|
handle.setActive(null);
|
|
key("ArrowRight"); // 빈 칸은 방향키 안 먹음
|
|
process.stdout.write(
|
|
JSON.stringify({
|
|
log,
|
|
text,
|
|
titleLeft,
|
|
afterReset,
|
|
afterHidden: { ...values },
|
|
picks,
|
|
hiddenAtStart,
|
|
hiddenAfter: cell.classList.contains("is-empty"),
|
|
slots,
|
|
}),
|
|
);
|