feat(B06 횡단): 암 경계선 고름 · 방향키 · 세로 끌기 · 아래 띠 공용 칸 (370-3)
- 새 B06_Section_UI_Cross_Line_Cell.ts — 아래 띠 왼쪽 공용 칸(제목 · ◀▲↺▼▶ 한 벌 · 값 한 줄 말줄임 · 길게 누름 반복) · 숨은 버튼은 방향키 슬롯 뗌 - Design_Pick — 암 점선 위 판정 선(키 rock · ▲▼↺ · ◀▶ 막음 · 방향키 = 암만) · 떠 있는 선 조정창 · 우측 위 치수 라벨 걷음 · 안 고르면 암 칸 · 고르면 그 선(노면 · 사면 · 소단 · 중심점 · 교차점 · 암) - Center.ts bindAxisDrag(한 축 끌기 · 놓을 때 한 번 · click 한 번 막음) — 중심점 · 암 세로 끌기 같이 씀 - buildRockBoundaryControl = 공용 칸 「안 고름」 꼴(Cross_Design) · Card_Chrome 띠 왼쪽 = 공용 칸 · Detail_Pick 표 강조에 암 판정 선 뺌 - CSS(Cross.css 공용 칸 · Areas.css 암 고름 띠 · Controls.css 주석) · 시험 test_370_3 새로 · 137 · 139_6 · 179_1 · 193_1 · 194_1 · 318 · 356 · 370_2 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -288,7 +288,12 @@ const designLines = kids.filter((k) => k.classList.contains("b06-chart__design-c
|
||||
const stopped = hits[0].click();
|
||||
handle.setActive(1);
|
||||
const activeOn = hits.map((h) => h.classList.contains("is-active"));
|
||||
const labelOn = { hidden: handle.label.hidden, text: handle.label.textContent };
|
||||
// 아래 띠 공용 칸(370-3) — 빈 칸 = is-empty · 값 = 버튼 줄 끝 글
|
||||
const cellNow = () => ({
|
||||
hidden: handle.cell.classList.contains("is-empty"),
|
||||
text: handle.cell.children[1].children.at(-1).textContent,
|
||||
});
|
||||
const labelOn = cellNow();
|
||||
handle.setActive(null);
|
||||
out.real = {
|
||||
designLines,
|
||||
@@ -303,7 +308,7 @@ out.real = {
|
||||
activeCount: activeOn.filter(Boolean).length,
|
||||
activeIndex: activeOn.indexOf(true),
|
||||
labelOn,
|
||||
labelOff: { hidden: handle.label.hidden, text: handle.label.textContent },
|
||||
labelOff: cellNow(),
|
||||
zones: [...new Set(handle.pieces.map((p) => pick.pieceZone(pipeSection.design, p).zone))].sort(),
|
||||
};
|
||||
|
||||
|
||||
@@ -147,21 +147,21 @@ const handle = pick.attachDesignLinePick({
|
||||
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");
|
||||
// 아래 띠 공용 칸(370-3) — 제목 · 버튼 · 값 한 줄
|
||||
const cell = handle.cell;
|
||||
const slot = (name) => cell.querySelector(`.b06-cross-card__cell-btn--${name}`);
|
||||
const cellTitle = () => cell.children[0].textContent;
|
||||
const cellValue = () => cell.children[1].children.at(-1).textContent;
|
||||
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 hiddenAtStart = cell.classList.contains("is-empty");
|
||||
const log = [];
|
||||
handle.setActive(pieceAt(4)); // 좌 사면
|
||||
const titleLeft = panel.descendants.find((n) =>
|
||||
n._cls.has("b06-structure-panel__title"),
|
||||
).textContent;
|
||||
const titleLeft = cellTitle();
|
||||
slot("left").click(); // 화면 왼쪽 = 좌측 바깥 → +0.1
|
||||
key("ArrowLeft"); // 방향키 = 십자
|
||||
log.push({ ...values });
|
||||
@@ -174,26 +174,25 @@ 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;
|
||||
const text = cellValue();
|
||||
const slots = ["left", "up", "reset", "down", "right"]
|
||||
.map((n) => slot(n))
|
||||
.filter((b) => !b.hidden)
|
||||
.map((b) => b.textContent);
|
||||
slot("reset").click(); // ↺ = 그 쪽 0
|
||||
const afterReset = { ...values };
|
||||
close.click(); // ✕ = 같은 조각 다시 고름(카드가 풂)
|
||||
handle.setActive(null);
|
||||
key("ArrowRight"); // 숨은 패널은 방향키 안 먹음
|
||||
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),
|
||||
hiddenAfter: cell.classList.contains("is-empty"),
|
||||
slots,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -173,13 +173,10 @@ function card(sectionMode, ditch) {
|
||||
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");
|
||||
// 아래 띠 공용 칸(370-3) — 제목 · 버튼 줄(◀▲↺▼▶ 보임만 바뀜) · 값 한 줄
|
||||
const slot = (name) => handle.cell.querySelector(`.b06-cross-card__cell-btn--${name}`);
|
||||
const title = () => handle.cell.children[0].textContent;
|
||||
const rowHidden = () => ["left", "reset", "right"].every((name) => slot(name).hidden);
|
||||
return { design, handle, values, at, slot, title, rowHidden };
|
||||
}
|
||||
|
||||
@@ -236,7 +233,7 @@ const out = {};
|
||||
same: JSON.stringify(before) === JSON.stringify(c.values),
|
||||
title: c.title(),
|
||||
rowHidden: c.rowHidden(),
|
||||
label: c.handle.label.textContent,
|
||||
label: c.handle.cell.children[1].children.at(-1).textContent,
|
||||
};
|
||||
// 고름 풀면 아무 키도 안 막음
|
||||
c.handle.setActive(null);
|
||||
|
||||
@@ -176,15 +176,17 @@ function card({ dl = 1, stored = undefined, order = ["left", "road", "right"], s
|
||||
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),
|
||||
};
|
||||
// 아래 띠 공용 칸(370-3) — 버튼 다섯 칸 한 벌(보임만 바뀜) · 제목 · 값 한 줄
|
||||
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;
|
||||
handle.cell.descendants.find((n) => n._cls.has(`b06-cross-card__cell-btn--${name}`));
|
||||
const rows = () => [
|
||||
slot("left").hidden && slot("right").hidden,
|
||||
slot("up").hidden && slot("down").hidden,
|
||||
];
|
||||
const text = (what) =>
|
||||
what === "title"
|
||||
? handle.cell.children[0].textContent
|
||||
: handle.cell.children[1].children.at(-1).textContent;
|
||||
return { design, handle, values, sets, focus, at, slot, rows, text };
|
||||
}
|
||||
|
||||
@@ -212,13 +214,10 @@ const out = {};
|
||||
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();
|
||||
c.slot("reset").click();
|
||||
out.berm = {
|
||||
title: c.text("b06-structure-panel__title"),
|
||||
readout: c.text("b06-structure-panel__value"),
|
||||
title: c.text("title"),
|
||||
readout: c.text("value"),
|
||||
moveHidden,
|
||||
liftHidden,
|
||||
leftBlocked,
|
||||
@@ -232,7 +231,7 @@ const out = {};
|
||||
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.slopeTitle = c.text("title");
|
||||
out.berm.focusAfter = c.focus.at(-1);
|
||||
// 노면 = ▲▼ 는 종단도로 넘김(안 막음)
|
||||
c.handle.setActive(c.at.road);
|
||||
|
||||
@@ -321,6 +321,12 @@ const out = {};
|
||||
soil_exists: soil !== null,
|
||||
soil_children: soil?.childElementCount ?? null,
|
||||
};
|
||||
// 370-3 — 설계선 고름 공용 칸이 오면 그 칸이 왼쪽(암 제어를 따로 안 만듦)
|
||||
const cell = new FakeNode("div");
|
||||
cell.className = "linecell";
|
||||
const cellCard = new FakeNode("article");
|
||||
appendCardFooter(cellCard, { design: { geometry_preset: "rock" } }, {}, {}, cell);
|
||||
out.footer.cell_classes = cellCard.querySelector("footer").children.map((kid) => kid.className);
|
||||
}
|
||||
|
||||
// ── 3. 단추 줄(318-1) — 빈자리 누름 = 카드 고름 · 조작 누름 = 안 넘김 ───────────
|
||||
|
||||
@@ -260,18 +260,23 @@ out.toe = {
|
||||
};
|
||||
toes[1].click();
|
||||
handle.setActive(picks[0]);
|
||||
const right = handle.panel.children.find((k) =>
|
||||
k.classList?.contains("b06-structure-panel__struct"),
|
||||
);
|
||||
const buttons = right.children[1].children;
|
||||
const rightButton = x(-1) > x(0) ? buttons[2] : buttons[0]; // 화면 → 바깥(우 = −offset)
|
||||
rightButton.click();
|
||||
// 아래 띠 공용 칸(370-3) — 제목 · 버튼 줄(끝 = 값 글)
|
||||
const cellOf = (h) => ({
|
||||
title: h.cell.children[0].textContent,
|
||||
value: h.cell.children[1].children.at(-1).textContent,
|
||||
button: (slot) =>
|
||||
h.cell.children[1].children.find((k) =>
|
||||
k.classList.contains(`b06-cross-card__cell-btn--${slot}`),
|
||||
),
|
||||
});
|
||||
const cell = cellOf(handle);
|
||||
cell.button(x(-1) > x(0) ? "right" : "left").click(); // 화면 → 바깥(우 = −offset)
|
||||
out.toePick = {
|
||||
picked: picks,
|
||||
active: toes[1].classList.contains("is-active"),
|
||||
label: handle.label.textContent,
|
||||
title: handle.panel.children[0].children[0].textContent,
|
||||
panelOpen: !handle.panel.classList.contains("is-hidden"),
|
||||
value: cell.value,
|
||||
title: cell.title,
|
||||
panelOpen: !handle.cell.classList.contains("is-empty"),
|
||||
sent: changes.map((c) => c.shoulder_extend),
|
||||
};
|
||||
|
||||
@@ -295,6 +300,6 @@ out.inherit = {
|
||||
afterIndex,
|
||||
};
|
||||
after.handle.setActive(afterIndex);
|
||||
out.inherit.label = after.handle.label.textContent;
|
||||
out.inherit.label = cellOf(after.handle).title;
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/* 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) {
|
||||
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) 돌려줌. */
|
||||
/** 방향키 한 번 — 막혔나(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();
|
||||
const index = c.hits.indexOf(c.rockHit);
|
||||
c.rockHit.click();
|
||||
out.hit = {
|
||||
found: !!c.rockHit,
|
||||
tag: c.rockHit?.tagName,
|
||||
moves: (c.rockHit?.getAttribute("d").match(/M/g) ?? []).length,
|
||||
last: index === c.hits.length - 1,
|
||||
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));
|
||||
@@ -100,8 +100,9 @@ def test_카드_배선_하나만() -> None:
|
||||
assert "if (activeRevet === null) revetOffset?.select(section.chainage_m, null);" in view
|
||||
assert view.count("dropLine();") >= 4 # 본체 · 벽 · 카드 풂 · 면적
|
||||
assert "setLineActive(pickedLineAt(section.station_id));" in view # 다시 그려도 되살림
|
||||
assert "zoomBar.prepend(linePick.label);" in view # 치수 = [+ − ⤢] 왼쪽
|
||||
assert "chartWrap.append(linePick.panel);" in view # 사면 이동 패널(151-3)
|
||||
# 치수 · 이동 = 아래 띠 왼쪽 공용 칸(370-3 · 옛 [+ − ⤢] 왼쪽 치수 · 떠 있는 이동 패널 걷음)
|
||||
assert "linePick.label" not in view and "linePick.panel" not in view
|
||||
assert "appendCardFooter(card, section, rockBoundary, cutSlope, linePick?.cell);" in view
|
||||
css = _src("B06_Section/B06_Section_UI_Style_Cross_Areas.css")
|
||||
assert ".b06-chart__design-hit.is-active {" in css
|
||||
|
||||
|
||||
@@ -33,18 +33,17 @@ def test_화면_방향대로_그_쪽만_단추와_방향키(out: dict) -> None:
|
||||
assert right == {"left": 0, "right": 0.1} # 우 사면 · ← = 안쪽 0 · ▶ = 바깥 0.1
|
||||
|
||||
|
||||
def test_치수_줄과_패널에_이동_표기(out: dict) -> None:
|
||||
def test_공용_칸_값에_이동_표기(out: dict) -> None:
|
||||
"""370-3 — 치수 · 이동은 아래 띠 공용 칸 값 한 줄(옛 우측 위 치수 · 패널 읽기 줄)."""
|
||||
assert "이동 +0.1m" in out["text"] and "노견 +" not in out["text"]
|
||||
assert out["readout"] == "이동 0.1m"
|
||||
assert out["text"].startswith("길이 ") and "\n" not in out["text"]
|
||||
assert out["titleLeft"] == "사면 이동 (좌)"
|
||||
|
||||
|
||||
def test_단추는_패널로_옮김_재설정_닫기(out: dict) -> None:
|
||||
"""151-3 — 치수 칸 [◀ ▶] 지움 · 패널 ◀ ↺ ▶ · ↺ = 그 쪽 0 · ✕ = 고름 풂 · 숨으면 방향키 안 먹음."""
|
||||
assert out["labelButtons"] == 0
|
||||
def test_단추는_공용_칸_재설정(out: dict) -> None:
|
||||
assert out["slots"] == ["◀", "↺", "▶"]
|
||||
assert out["afterReset"] == {"left": 0, "right": 0}
|
||||
assert out["picks"] == [out["pieceRight"]]
|
||||
assert out["picks"] == [] # 칸 누름은 고름으로 안 번짐
|
||||
assert out["hiddenAtStart"] is True and out["hiddenAfter"] is True
|
||||
assert out["afterHidden"] == {"left": 0, "right": 0}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 179-1 — 선 길이 정보창 = 열 수 무관 늘 줌 [+ − ⤢] 왼쪽(좁은 카드 내림 규칙 지움 · 소스 시험)."""
|
||||
"""PLAN 179-1 — 선 길이 정보창 = 열 수 무관 늘 줌 [+ − ⤢] 왼쪽(좁은 카드 내림 규칙 지움 · 소스 시험).
|
||||
|
||||
370-3 — 카드 치수는 아래 띠 공용 칸으로 옮김 · 치수 칸 꼴은 상세 모달(`_Detail_Select`)이 씀.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
@@ -22,9 +25,15 @@ def test_좁은_카드_내림_규칙_없음():
|
||||
]
|
||||
|
||||
|
||||
def test_치수_칸은_줌_막대_맨_앞():
|
||||
def test_카드_치수는_아래_띠_공용_칸():
|
||||
"""370-3 — 카드의 고른 선 치수는 줌 막대 앞 라벨을 걷고 아래 띠 왼쪽 공용 칸 값 한 줄로(상세 모달 치수는 그대로)."""
|
||||
view = _src("B06_Section/B06_Section_UI_Cross_View.ts")
|
||||
assert "zoomBar.prepend(linePick.label);" in view
|
||||
assert "linePick.label" not in view and "zoomBar.prepend(" not in view
|
||||
assert "linePick?.cell" in view
|
||||
assert "b06-cross-card__line-dims" not in _src(
|
||||
"B06_Section/B06_Section_UI_Cross_Design_Pick.ts"
|
||||
)
|
||||
assert "b06-cross-card__line-dims" in _src("B06_Section/B06_Section_UI_Cross_Detail_Select.ts")
|
||||
zoom = (
|
||||
_src("B06_Section/B06_Section_UI_Style_Cross_Controls.css")
|
||||
.split(".b06-cross-card__zoom {", 1)[1]
|
||||
|
||||
@@ -298,7 +298,8 @@ def test_이동_셋_분리(ui: dict) -> None:
|
||||
@node_only
|
||||
def test_소단_고름_위아래만(ui: dict) -> None:
|
||||
berm = ui["berm"]
|
||||
assert berm["title"] == "소단 높이 (좌)" and berm["readout"] == "도로 아래 1.0m"
|
||||
assert berm["title"] == "소단 높이 (좌)"
|
||||
assert berm["readout"].startswith("도로 아래 1.0m · 길이 ") # 값 한 줄(370-3 공용 칸)
|
||||
assert berm["moveHidden"] is True and berm["liftHidden"] is False
|
||||
assert berm["leftBlocked"] and berm["rightBlocked"] # ◀▶ 막힘
|
||||
assert berm["afterSide"] == {"values": {"left": 0, "right": 0}, "sets": 0} # 가로 안 감
|
||||
@@ -332,7 +333,7 @@ def test_좌측_소단_상자_알림_카드_창구(ui: dict) -> None:
|
||||
assert chainage == 40 and change["berm_depth"] == {"side": "left", "valueM": 1.3}
|
||||
assert change["ground_type"] == "soil" # 카드 버튼 값은 그대로 실음
|
||||
assert moves["host"] == [40, None] # 한 번만 켬 · 다른 측점 끔은 무시
|
||||
assert moves["none"] == {"stationId": "S7"} # 창구 없으면 이동 패널 없음
|
||||
assert moves["none"] == {"stationId": "S7"} # 창구 없으면 이동 없음
|
||||
selection = (ROOT / "B06_Section" / "B06_Section_UI_Page_Structures_Selection.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
@@ -95,6 +95,11 @@ def test_footer_without_center_text(result: dict) -> None:
|
||||
assert footer["rock_first"].split()[-1] == "b06-cross-card__rockb" # 암 경계 = 왼쪽 첫 칸
|
||||
assert footer["rock_classes"][-1].split()[-1] == "b06-cross-card__cutslope"
|
||||
assert footer["soil_exists"] and footer["soil_children"] == 0 # 토사도 띠는 남음(빈 띠)
|
||||
# 370-3 — 선 공용 칸 = 왼쪽 첫 칸(암 꼴 클래스) · 암 절토 경사 = 오른쪽 그대로
|
||||
assert footer["cell_classes"] == [
|
||||
"linecell b06-cross-card__rockb",
|
||||
"cutslope b06-cross-card__cutslope",
|
||||
]
|
||||
|
||||
|
||||
def test_center_wired_into_card_and_modal() -> None:
|
||||
|
||||
@@ -82,11 +82,9 @@ def test_배선():
|
||||
pick = (B06 / "B06_Section_UI_Cross_Design_Pick.ts").read_text(encoding="utf-8")
|
||||
assert "center: section.chainage_m," in pick # designLineMoves → 카드 고름 끝 조각
|
||||
assert 'if (axis === "center") return centerShift?.step(dir);' in pick
|
||||
# 364-7 덧 — 중심점 ◀▶ 툴팁 = 노선 좌우 · ↺ · 소단 ▲▼ 숨김
|
||||
# 364-7 덧 — 중심점 ◀▶ 툴팁 = 노선 좌우 · ↺ 없음 · 소단 ▲▼ 없음(370-3 공용 칸 = 축에 맞는 버튼만)
|
||||
assert "`이 측점 노선을 화면 ${screen}으로 0.1 m`" in pick
|
||||
assert '[toLeft.title, toRight.title] = [stepTitle("왼쪽"), stepTitle("오른쪽")];' in pick
|
||||
assert 'move.controls.children[1]?.classList.toggle("is-hidden", axis === "center");' in pick
|
||||
assert 'lift.row.classList.toggle("is-hidden", axis !== "depth");' in pick
|
||||
assert 'title: "중심점", value: centerPreviewText(0), buttons: sideways(false)' in pick
|
||||
order = (B06 / "B06_Section_UI_Cross_Pick_Order.ts").read_text(encoding="utf-8")
|
||||
assert "layer.append(...surfaces, ...edges, ...lines, ...centers);" in order
|
||||
edit = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
||||
|
||||
@@ -63,8 +63,8 @@ def test_교차점_손잡이_쪽마다_늘_보임(out) -> None:
|
||||
def test_손잡이를_고르면_그_쪽_사면_좌우_이동(out) -> None:
|
||||
got = out["toePick"]
|
||||
assert got["active"] is True and got["panelOpen"] is True
|
||||
assert got["label"].startswith("교차점 (우) · 노선에서 ")
|
||||
assert got["title"] == "교차점 이동 (우)"
|
||||
assert got["title"] == "교차점 이동 (우)" # 아래 띠 공용 칸(370-3)
|
||||
assert got["value"].startswith("노선에서 ")
|
||||
assert len(got["sent"]) == 1 and got["sent"][0]["side"] == "right"
|
||||
low, high = out["stop"]["crossing"]
|
||||
assert abs(got["sent"][0]["valueM"] - out["stop"]["value"]) < 1e-9 # ◀▶ 도 멈춤 자리
|
||||
@@ -76,4 +76,4 @@ def test_절에서_성으로_넘어가도_고름_유지(out) -> None:
|
||||
assert inherit["cutIndex"] >= 0 and inherit["before"] == inherit["cutIndex"]
|
||||
assert inherit["farMode"] == "left_cut" # 우 = 성토로 넘어감
|
||||
assert inherit["afterIndex"] is not None
|
||||
assert inherit["label"].startswith("교차점 (우)")
|
||||
assert inherit["label"] == "교차점 이동 (우)"
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""PLAN 370-3 — B06 암 경계선 고름 · 방향키 · 세로 끌기 · 아래 띠 공용 칸(요구 240-22).
|
||||
|
||||
짝: `B06_Section/B06_Section_UI_Cross_Design_Pick.ts`(rock 축 · 판정 선) ·
|
||||
`B06_Section/B06_Section_UI_Cross_Line_Cell.ts`(공용 칸) · `B06_Section_UI_Cross_Center.ts`(`bindAxisDrag`).
|
||||
실제 TS 를 `helper_370_3_rock_cell.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).parent / "helper_370_3_rock_cell.cjs"
|
||||
B06 = ROOT / "B06_Section"
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_안_고르면_공용_칸은_암_버튼과_값(out: dict) -> None:
|
||||
idle = out["idle"]
|
||||
assert idle["title"] == "암 경계선" and idle["value"] == "-0.5m"
|
||||
assert idle["slots"] == ["up", "reset", "down"]
|
||||
assert idle["upPassed"] is True # 안 고르면 ▲▼ = 종단 계획고 그대로
|
||||
assert idle["offsets"] == [0.1, -0.1] and idle["resets"] == 1
|
||||
|
||||
|
||||
def test_암_경계선_판정_선(out: dict) -> None:
|
||||
hit = out["hit"]
|
||||
assert hit["found"] and hit["tag"] == "path" and hit["moves"] == 2 # 끊긴 두 토막 한 판정
|
||||
assert hit["last"] and hit["picked"] == [3] # 조각 셋 뒤 · 누르면 고름
|
||||
|
||||
|
||||
def test_암_고름은_위아래만_암_키는_암만(out: dict) -> None:
|
||||
got = out["picked"]
|
||||
assert got["active"] and got["key"] # 키 rock 으로 기억
|
||||
assert got["title"] == "암 경계선" and got["slots"] == ["up", "reset", "down"]
|
||||
assert got["leftBlocked"] and got["rightBlocked"] # ◀▶ 막음
|
||||
assert got["upTaken"] and got["downTaken"] # ▲▼ = 암(종단으로 안 샘)
|
||||
assert got["sideValues"] == {"left": 0, "right": 0}
|
||||
assert got["offsets"][:2] == [0.1, -0.1]
|
||||
|
||||
|
||||
def test_세로_끌기는_놓을_때_한_번(out: dict) -> None:
|
||||
drag = out["drag"]
|
||||
assert drag["stopped"] is True # 카드 팬으로 안 번짐
|
||||
assert drag["during"]["rock"] == drag["during"]["hit"] # 점선 · 판정 선 같이
|
||||
dy = float(drag["during"]["rock"].split()[1].removesuffix("px"))
|
||||
assert abs(dy - (-0.3 * 12)) < 1e-9 # 위로 0.3 m(화면 px = m × 12)
|
||||
assert drag["during"]["value"] == "-0.2m — 놓으면 반영"
|
||||
assert drag["sent"] == [0.3] # 0.34 m → 걸음 0.1 맞춤 · 한 번
|
||||
assert drag["clickSwallowed"] is True
|
||||
assert out["idleDrag"]["stopped"] is False # 안 고르면 끌기 아님
|
||||
|
||||
|
||||
def test_풀면_암_칸_사면_고르면_그_선_칸(out: dict) -> None:
|
||||
assert out["released"]["title"] == "암 경계선" and out["released"]["upPassed"]
|
||||
slope = out["slope"]
|
||||
assert slope["title"] == "사면 이동 (좌)" and slope["slots"] == ["left", "reset", "right"]
|
||||
assert slope["value"].startswith("길이 ") and "\n" not in slope["value"]
|
||||
assert slope["arrowSlots"] == slope["slots"] # 숨은 버튼은 방향키 슬롯 없음
|
||||
|
||||
|
||||
def test_토사는_빈_칸_판정_선_없음(out: dict) -> None:
|
||||
soil = out["soil"]
|
||||
assert soil["empty"] is True and soil["slots"] == [] and soil["rockHit"] is False
|
||||
|
||||
|
||||
def test_배선_떠_있는_선_조정창_걷음() -> None:
|
||||
pick = (B06 / "B06_Section_UI_Cross_Design_Pick.ts").read_text(encoding="utf-8")
|
||||
assert "buildPanelShell" not in pick and "b06-cross-card__line-dims" not in pick
|
||||
view = (B06 / "B06_Section_UI_Cross_View.ts").read_text(encoding="utf-8")
|
||||
assert "rockBoundary && rockCellSpec(section, rockBoundary)" in view
|
||||
chrome = (B06 / "B06_Section_UI_Cross_Card_Chrome.ts").read_text(encoding="utf-8")
|
||||
assert "if (lineCell) {" in chrome
|
||||
detail = (B06 / "B06_Section_UI_Cross_Detail_Pick.ts").read_text(encoding="utf-8")
|
||||
assert ".b06-chart__design-hit:not(.b06-chart__rock-hit)" in detail # 상세 표 강조에 안 섞임
|
||||
css = (B06 / "B06_Section_UI_Style_Cross.css").read_text(encoding="utf-8")
|
||||
assert ".b06-cross-card__linecell.is-empty {" in css and "text-overflow: ellipsis;" in css
|
||||
Reference in New Issue
Block a user