Files
Aislo/resources/tester/helper_370_3_rock_cell.cjs
T
eomsangdonandClaude Sonnet 5.5 8bbdbb9f69 test(508-1b 옛 헬퍼 공용 불러오기 4/7): 헬퍼 28 곳 loadTs 베낌 걷고 makeLoader 로
- 헬퍼 동작 그대로(풀이 함수 본문은 옮겨 담음) · 제품 코드 안 고침
- 확인: 이 묶음 헬퍼를 쓰는 시험 236 개 전 230 통과 → 후 230 통과 (시험별 junit 대조 · 어긋남 0)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 17:11:59 +09:00

304 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* PLAN 370-3 단위검증 헬퍼 — B06 암 경계선 고름 · 방향키 · 세로 끌기 · 아래 띠 공용 칸.
* `_Cross_Design_Pick.attachDesignLinePick` 을 그 자리에서 트랜스파일해 흉내 DOM 으로 돌림. 짝 = `test_370_3_rock_cell.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, ref) {
this.children = this.children.filter((k) => k !== kid);
const at = ref ? this.children.indexOf(ref) : -1;
if (at < 0) this.children.push(kid);
else this.children.splice(at, 0, 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) 돌려줌. */
/** 방향키 한 번 — 막혔나(preventDefault) 돌려줌. */
const press = (name) => {
let prevented = false;
[...(docListeners.keydown ?? [])].forEach((fn) =>
fn({ key: name, target: null, preventDefault: () => (prevented = true) }),
);
return prevented;
};
// 끌기 — window 듣개(놓을 때 click 한 번 막음 포함)
const winListeners = {};
globalThis.window = {
addEventListener: (t, fn) => (winListeners[t] ??= []).push(fn),
removeEventListener: (t, fn) => {
winListeners[t] = (winListeners[t] ?? []).filter((f) => f !== fn);
},
setTimeout: () => 0,
clearTimeout: () => undefined,
setInterval: () => 0,
clearInterval: () => undefined,
};
globalThis.setTimeout = () => 0;
const fire = (type, event) => [...(winListeners[type] ?? [])].forEach((fn) => fn(event));
/** 암 측점 카드 — 좌 사면 · 노면 · 우 사면 + 암 경계선 점선 두 토막(결측으로 끊김). */
function card(rockOn = true) {
docListeners.keydown = [];
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 layer = new Node("g");
layer.getScreenCTM = () => ({ a: 1, d: 1 });
layer.append(
line([
[6, 97],
[2, 100],
]),
line([
[2, 100],
[-2, 100],
]),
line([
[-2, 100],
[-6, 97],
]),
);
const rockLine = (pts) => {
const el = new Node("polyline");
el.setAttribute("class", "b06-chart__rock-boundary");
el.setAttribute("points", pts.map(([o, e]) => `${x(o)},${y(e)}`).join(" "));
el.style = { setProperty: (k, v) => (el.styleSet = v) };
return el;
};
const rocks = [
rockLine([
[8, 96],
[1, 98],
]),
rockLine([
[-1, 98],
[-8, 96],
]),
];
layer.append(...rocks);
const offsets = [];
const resets = [];
const rock = {
legend: "암 경계선",
offsetM: -0.5,
stepM: 0.1,
upTitle: "위로",
downTitle: "아래로",
resetTitle: "기본",
adjust: (d) => offsets.push(Math.round(d * 100) / 100),
reset: () => resets.push(true),
};
const values = { left: 0, right: 0 };
const picks = [];
const handle = pick.attachDesignLinePick({
layer,
design,
x,
toDisplayY: y,
onPick: (i) => picks.push(i),
shift: { valueOf: (s) => values[s], set: (s, v) => (values[s] = v) },
stationId: "R1",
...(rockOn && { rock }),
});
const cell = handle.cell;
const slot = (name) => cell.querySelector(`.b06-cross-card__cell-btn--${name}`);
const state = () => ({
empty: cell.classList.contains("is-empty"),
title: cell.children[0].textContent,
value: cell.children[1].children.at(-1).textContent,
slots: ["left", "up", "reset", "down", "right"].filter((n) => !slot(n).hidden),
arrowSlots: ["left", "up", "reset", "down", "right"].filter((n) =>
slot(n).classList.contains(`b06-structure-panel__btn--${n}`),
),
});
const hits = layer.children.filter((k) => k.classList.contains("b06-chart__design-hit"));
const rockHit = hits.find((k) => k.classList.contains("b06-chart__rock-hit"));
if (rockHit) rockHit.style = { setProperty: (k, v) => (rockHit.styleSet = v) };
return { handle, cell, slot, state, hits, rockHit, rocks, offsets, resets, values, picks };
}
const out = {};
// ① 안 고름 — 공용 칸 = 빈 칸(479) · 숨은 단추 눌러도 암 안 움직임 · 방향키는 종단 계획고로 그대로
{
const c = card();
c.handle.setActive(null);
out.idle = { ...c.state(), upPassed: !press("ArrowUp") };
c.slot("up").click();
c.slot("down").click();
c.slot("reset").click();
out.idle.offsets = c.offsets;
out.idle.resets = c.resets.length;
}
// ② 판정 선 — 점선 위 path 하나(끊긴 두 토막 · M 둘) · 보라 선 판정 클래스 · 키 rock
{
const c = card();
c.rockHit.click();
const index = c.picks[0];
out.hit = {
found: !!c.rockHit,
tag: c.rockHit?.tagName,
moves: (c.rockHit?.getAttribute("d").match(/M/g) ?? []).length,
below: c.hits.indexOf(c.rockHit) === 0, // 그리는 차례 = 보라 선 판정 아래(겹친 자리는 설계선)
picked: c.picks,
};
// ③ 고름 — 제목 · ▲▼↺ · ◀▶ 막음 · ▲▼ = 암만(종단으로 안 샘)
c.handle.setActive(index);
pick.setPickedLine("R1", index);
out.picked = {
...c.state(),
active: c.rockHit.classList.contains("is-active"),
leftBlocked: press("ArrowLeft"),
rightBlocked: press("ArrowRight"),
upTaken: press("ArrowUp"),
downTaken: press("ArrowDown"),
sideValues: { ...c.values },
key: pick.pickedLineAt("R1") === index,
};
out.picked.offsets = c.offsets;
// ④ 세로 끌기 — 위로 0.34 m(화면 px = m × 12) → 0.3 m 한 번 · 끄는 동안 점선 · 판정 선만 옮겨 보임
let stopped = false;
c.rockHit.listeners.pointerdown[0]({
button: 0,
clientX: 0,
clientY: 500,
preventDefault: () => undefined,
stopPropagation: () => (stopped = true),
});
fire("pointermove", { clientX: 0, clientY: 500 - 0.34 * 12 });
const during = { rock: c.rocks[0].styleSet, hit: c.rockHit.styleSet, value: c.state().value };
const before = c.offsets.length;
fire("pointerup", {});
out.drag = {
stopped,
during,
sent: c.offsets.slice(before),
clickSwallowed: (winListeners.click ?? []).length === 1,
};
// 풀면 빈 칸(479) · 방향키 안 먹음
c.handle.setActive(null);
out.released = { ...c.state(), upPassed: !press("ArrowUp") };
// 사면을 고르면 그 선 칸(제목 · ◀↺▶ · 값 한 줄)
const slope = c.handle.pieces.findIndex((p) => Math.max(p.from.offset, p.to.offset) > 3);
c.handle.setActive(slope);
out.slope = c.state();
pick.setPickedLine("R1", null);
}
// ⑤ 안 고른 끌기는 안 감(카드 팬) · 토사(암 없음) = 빈 칸 · 판정 선 없음
{
const c = card();
c.handle.setActive(null);
let stopped = false;
c.rockHit.listeners.pointerdown[0]({
button: 0,
clientY: 0,
preventDefault: () => undefined,
stopPropagation: () => (stopped = true),
});
out.idleDrag = { stopped };
const soil = card(false);
soil.handle.setActive(null);
out.soil = { ...soil.state(), rockHit: !!soil.rockHit };
}
process.stdout.write(JSON.stringify(out));