Files
Aislo/resources/tester/helper_139_6_line_shift.cjs
T
eomsangdonandClaude Opus 5.5 ba53c7726d feat(B06): 보라 선 치수 두 줄 · 사면 이동 패널(151-1 · 151-3)
- 치수 글 두 줄(사면길이 · 수평 / 높이 · 구배 · 이동) · 좁은 카드는 면적표 아래 높이로
- [◀ ▶] 지움 → 공용 조정창 꼴 이동 패널(◀ ↺ ▶ · 방향키 ←→ · 0.1 m · ✕ = 고름 풂)
- 시험 — test_137_design_pick · test_139_6_line_shift(패널 · 방향키 · ↺ · 숨김)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
2026-10-09 19:08:22 +09:00

200 lines
6.0 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 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;
}
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),
},
});
const panel = handle.panel;
const slot = (name) => panel.querySelector(`.b06-structure-panel__btn--${name}`);
const close = panel.querySelector(".b06-structure-panel__close");
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 = panel.classList.contains("is-hidden");
const log = [];
handle.setActive(pieceAt(4)); // 좌 사면
const titleLeft = panel.descendants.find((n) =>
n._cls.has("b06-structure-panel__title"),
).textContent;
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 = handle.label.textContent;
const readout = panel.descendants.find((n) => n._cls.has("b06-structure-panel__value")).textContent;
slot("reset").click(); // ↺ = 그 쪽 0
const afterReset = { ...values };
close.click(); // ✕ = 같은 조각 다시 고름(카드가 풂)
handle.setActive(null);
key("ArrowRight"); // 숨은 패널은 방향키 안 먹음
process.stdout.write(
JSON.stringify({
log,
text,
readout,
titleLeft,
afterReset,
afterHidden: { ...values },
picks,
pieceRight: pieceAt(-4),
hiddenAtStart,
hiddenAfter: panel.classList.contains("is-hidden"),
labelButtons: handle.label.children.length,
slots: ["left", "reset", "right"].map((n) => slot(n).textContent),
}),
);