- 정의 「상세 내용」 = 폭만 · depth_m 표시 끔(옛 구간 값 = 기본값으로 읽음 · 새 기본 1 m)
- 짝: BermSpec 쪽마다 깊이 · side_berm ↔ sideBerm · 설계 결과 berm_depth_{side}_m 되싣기(파이썬 ↔ TS)
- 저장: patch berm_depth_{side}_m(≥ 0) · 재계산 살림 목록 짝(RD ↔ CR) · 세션 bermdepth
- 서버 [저장] 다시 셈 = 구조물 정본 소단 구간(load_berm_spans) + 측점 깊이 · 구간 지우면 소단 없음
- 고름: 소단 수평 단 = 높이만(▲ 0 에서 멈춤 · ▼ 사면 끝 앞 멈춤 · ◀▶ 막힘) · 사면 = 가로 · 노면 = 종단
- 고름을 조각 키(갈래 + 쪽 + 차례)로 기억 — 옮겨 다시 그려도 같은 조각
- 새 시험 test_194_1_berm_depth + helper · test_178_2 · test_54_26 고침
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
305 lines
10 KiB
JavaScript
305 lines
10 KiB
JavaScript
/* 194-1 · 194-2 소단 고름 헬퍼 — `_Cross_Design_Pick` 을 그 자리에서 트랜스파일해 흉내 DOM 으로 돌림.
|
|
* 소단 = 높이만(▲▼ · ◀▶ 막힘 · 노면 끝 위 금지) · 조각 키로 다시 그려도 같은 조각 · 좌측 상자 알림. 짝 = `test_194_1_berm_depth.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;
|
|
};
|
|
|
|
/** 양성토 단면(노면 끝 표고 100) — 좌 소단 깊이 `dl`(노면 끝 아래) · 우 1.0 · 사면 1:1.5 · 끝 표고 97. `order` = 그리는 차례. */
|
|
function card({ dl = 1, stored = undefined, order = ["left", "road", "right"], stationId } = {}) {
|
|
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: null,
|
|
ditch_side: null,
|
|
ditch_enabled: false,
|
|
section_mode: "both_fill",
|
|
berm: { width_m: 0.5, interval_m: 3, slope_deg: 0, depth_m: 1 },
|
|
};
|
|
if (stored !== undefined) design.berm_depth_left_m = stored;
|
|
const slope = (s, d) => {
|
|
const k = s === "left" ? 1 : -1;
|
|
const knee = 2 + 1.5 * d;
|
|
const toe = knee + 0.5 + 1.5 * (100 - d - 97);
|
|
return [
|
|
[k * toe, 97],
|
|
[k * (knee + 0.5), 100 - d],
|
|
[k * knee, 100 - d],
|
|
[k * 2, 100],
|
|
];
|
|
};
|
|
const lines = {
|
|
left: line(slope("left", dl)),
|
|
road: line([
|
|
[2, 100],
|
|
[0, 100.04], // 노면 횡단경사 2 % — 깊이 0 소단이 노면과 한 직선으로 안 합쳐짐
|
|
[-2, 100],
|
|
]),
|
|
right: line(slope("right", 1)),
|
|
};
|
|
const layer = new Node("g");
|
|
layer.append(...order.map((name) => lines[name]));
|
|
const values = { left: 0, right: 0 };
|
|
const sets = [];
|
|
const focus = [];
|
|
const handle = pick.attachDesignLinePick({
|
|
layer,
|
|
design,
|
|
x,
|
|
toDisplayY: y,
|
|
onPick: () => undefined,
|
|
shift: { valueOf: (s) => values[s], set: (s, v) => (values[s] = v) },
|
|
berm: { set: (s, v) => sets.push([s, v]), focus: (on) => focus.push(on) },
|
|
stationId,
|
|
});
|
|
const find = (test) => handle.pieces.findIndex(test);
|
|
const mid = (p) => (p.from.offset + p.to.offset) / 2;
|
|
const at = {
|
|
leftBerm: find((p) => pick.isBermPiece(design, p) && mid(p) > 0),
|
|
rightBerm: find((p) => pick.isBermPiece(design, p) && mid(p) < 0),
|
|
leftUpper: find((p) => !pick.isBermPiece(design, p) && mid(p) > 2 && mid(p) < 2 + 1.5 * dl),
|
|
road: find((p) => mid(p) > 0 && mid(p) < 2),
|
|
};
|
|
const slot = (name) =>
|
|
handle.panel.descendants.find(
|
|
(n) => n._cls.has(`b06-structure-panel__btn--${name}`) && !n.parentHidden,
|
|
);
|
|
const rows = () =>
|
|
handle.panel.descendants
|
|
.filter((n) => n._cls.has("b06-structure-panel__struct"))
|
|
.map((n) => n._cls.has("is-hidden"));
|
|
const text = (cls) => handle.panel.descendants.find((n) => n._cls.has(cls)).textContent;
|
|
return { design, handle, values, sets, focus, at, slot, rows, text };
|
|
}
|
|
|
|
const out = {};
|
|
|
|
// ① 갈래 · 축 — 소단 = depth · 사면 = x · 노면 = y · 키
|
|
{
|
|
const c = card();
|
|
out.axis = Object.fromEntries(
|
|
Object.entries(c.at).map(([name, i]) => [
|
|
name,
|
|
pick.pieceMoveAxis(c.design, c.handle.pieces[i]),
|
|
]),
|
|
);
|
|
out.keys = pick.pieceKeys(c.design, c.handle.pieces).sort();
|
|
}
|
|
|
|
// ② 소단 고름 — ▲▼ 만 · ◀▶ 막힘 · ↺ = 구간 기본 · 좌측 상자 켬 알림
|
|
{
|
|
const c = card();
|
|
c.handle.setActive(c.at.leftBerm);
|
|
const [moveHidden, liftHidden] = c.rows();
|
|
const leftBlocked = press("ArrowLeft");
|
|
const rightBlocked = press("ArrowRight");
|
|
const afterSide = { values: { ...c.values }, sets: c.sets.length };
|
|
const upTaken = press("ArrowUp");
|
|
press("ArrowDown");
|
|
c.handle.panel.descendants
|
|
.filter((n) => n._cls.has("b06-structure-panel__btn--reset"))
|
|
.at(-1)
|
|
.click();
|
|
out.berm = {
|
|
title: c.text("b06-structure-panel__title"),
|
|
readout: c.text("b06-structure-panel__value"),
|
|
moveHidden,
|
|
liftHidden,
|
|
leftBlocked,
|
|
rightBlocked,
|
|
afterSide,
|
|
upTaken,
|
|
sets: c.sets,
|
|
focus: c.focus,
|
|
};
|
|
c.handle.setActive(c.at.leftUpper); // 사면으로 바꿈 — 소단 상자 끔 · ▲▼ 막힘 · 깊이 안 바뀜
|
|
const before = c.sets.length;
|
|
out.berm.slopeUpBlocked = press("ArrowUp");
|
|
out.berm.slopeSets = c.sets.length - before;
|
|
out.berm.slopeTitle = c.text("b06-structure-panel__title");
|
|
out.berm.focusAfter = c.focus.at(-1);
|
|
// 노면 = ▲▼ 는 종단도로 넘김(안 막음)
|
|
c.handle.setActive(c.at.road);
|
|
out.berm.roadUpPassed = !press("ArrowUp");
|
|
out.berm.roadSets = c.sets.length - before;
|
|
}
|
|
|
|
// ③ 도로 위 금지 = 0 에서 멈춤 · 사면 끝 앞에서 멈춤
|
|
{
|
|
const top = card({ dl: 0, stored: 0 });
|
|
top.handle.setActive(top.at.leftBerm);
|
|
press("ArrowUp");
|
|
const deep = card({ dl: 2.9, stored: 2.9 });
|
|
deep.handle.setActive(deep.at.leftBerm);
|
|
press("ArrowDown"); // 2.9 → 3.0 = 사면 끝(97) — 막힘
|
|
press("ArrowUp");
|
|
out.limit = { topSets: top.sets, topFound: top.at.leftBerm >= 0, deepSets: deep.sets };
|
|
}
|
|
|
|
// ④ 옮긴 뒤 다시 그려도 같은 조각(194-2) — 그리는 차례 · 깊이가 바뀌어도
|
|
{
|
|
const first = card({ stationId: "S1" });
|
|
pick.setPickedLine("S1", first.at.leftBerm);
|
|
const again = card({
|
|
dl: 1.1,
|
|
stored: 1.1,
|
|
order: ["right", "road", "left"],
|
|
stationId: "S1",
|
|
});
|
|
const restored = pick.pickedLineAt("S1");
|
|
pick.setPickedLine("S1", again.at.leftUpper);
|
|
const third = card({ dl: 1.2, stored: 1.2, stationId: "S1" });
|
|
out.stable = {
|
|
firstIndex: first.at.leftBerm,
|
|
againIndex: again.at.leftBerm,
|
|
restored,
|
|
upperRestored: pick.pickedLineAt("S1") === third.at.leftUpper,
|
|
upperIndexMoved: again.at.leftUpper !== third.at.leftUpper,
|
|
};
|
|
pick.setPickedLine("S1", null);
|
|
}
|
|
|
|
// ⑤ 카드 창구 — 소단 깊이 변경 · 좌측 상자 알림(한 번만 · 이 측점이 켠 때만 끔)
|
|
{
|
|
const changes = [];
|
|
const design = { ground_type: "soil", section_mode: "both_fill", shoulder_extend_left_m: 0.3 };
|
|
const moves = pick.designLineMoves({ station_id: "S9", chainage_m: 40, design }, (ch, change) =>
|
|
changes.push([ch, change]),
|
|
);
|
|
moves.berm.set("left", 1.3);
|
|
const host = [];
|
|
pick.setBermPickHost((ch) => host.push(ch));
|
|
moves.berm.focus(true);
|
|
moves.berm.focus(true); // 다시 그린 카드 — 안 부름
|
|
pick
|
|
.designLineMoves({ station_id: "S8", chainage_m: 20, design }, () => undefined)
|
|
.berm.focus(false);
|
|
moves.berm.focus(false);
|
|
out.moves = {
|
|
stationId: moves.stationId,
|
|
shiftValue: moves.shift.valueOf("left"),
|
|
change: changes[0],
|
|
host,
|
|
none: pick.designLineMoves({ station_id: "S7", chainage_m: 0, design }),
|
|
};
|
|
pick.setBermPickHost(null);
|
|
}
|
|
|
|
process.stdout.write(JSON.stringify(out));
|