- 절토 · 성토측(구조물 트림 선 포함) 고름 = ◀▶ 그 쪽 노견째 · ▲▼ 막음(종단 계획고로 새지 않음) - 도로측(노면 · 노견) 고름 = ◀▶ 막음 · 이동 줄 숨김 · ▲▼ 는 종단도 계획고로 그대로 넘김 - 새 시험 test_193_1_line_axis(갈래 · 바깥만 · 0 에서 멈춤 · 반대측 고정 · 절토 대칭 · 도로 Y 만) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
265 lines
8.5 KiB
JavaScript
265 lines
8.5 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;
|
|
}
|
|
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));
|