Files
Aislo/resources/tester/helper_370_3_rock_cell.cjs
T
eomsangdonandClaude Opus 5.5 75f4cc982f fix(B06 횡단): 암 경계선 판정 선을 설계선 판정 아래로 — 사면 발치 겹친 자리는 사면이 먼저 골라짐 (370-3)
- 화면 검증에서 1+0 우 절토 사면 누름이 암 판정 선에 먹힘 · 그리는 차례만 아래로(키 차례 그대로)
- test_370_3 판정 선 「설계선 판정 아래」 확인 · helper insertBefore 참조 자리 따름

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-17 13:16:57 +09:00

314 lines
10 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 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, 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 }),
);
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) 돌려줌. */
/** 방향키 한 번 — 막혔나(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 = {};
// ① 안 고름 — 공용 칸 = 암 ▲▼↺ · 값 · 방향키는 안 먹음(종단 계획고로 그대로)
{
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,
};
// 풀면 다시 암 칸 · 방향키 안 먹음
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));