feat(종단): 325-2 유토 판 붙어 있음 판정 — 높이 나눔 · 끌기는 열림 && 종단 뿌리 안만 · 뗀 유토는 노선 전체 세로 · 방향키 판정에 뗀 유토 · 배수 판

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
2026-10-15 12:26:31 +09:00
co-authored by Claude Opus 5.5
parent 6d5e5f911e
commit 2d239f780e
7 changed files with 200 additions and 7 deletions
@@ -102,7 +102,8 @@ tools.createPanelTools({
/** 누른 자리 — 종단도 안이면 closest 가 그 판을 줌(새 창 요소처럼 Element 가 아닌 것도). */
const spot = (inProfile) => ({
closest: (sel) => (inProfile && sel === ".b05-route-profile" ? {} : null),
closest: (sel) =>
inProfile && sel.split(",").some((s) => s.trim() === ".b05-route-profile") ? {} : null,
});
const press = (target) => listeners.document.pointerdown?.forEach((fn) => fn({ target }));
@@ -0,0 +1,112 @@
/* 224-1b 유토 판 「붙어 있음」 판정 — 진짜 높이 나눔(`createHeightCascade`)과 방향키 판정
* (`arrowsBelongToProfile` 본문)을 가짜 DOM 위에서 돌려 JSON 으로 냄.
* · 열림 && 뿌리 안 = 자리 차지 · 열림 && 새 창(뿌리 밖) = 자리 0 · 인라인 높이 안 씀
* · 방향키 = 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫 */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8");
const transpile = (code) =>
ts.transpileModule(code, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
// ---- 가짜 요소(부모 사슬 · contains · closest) ----
function el(className = "") {
const node = { className, parent: null, hidden: false, clientHeight: 0, offsetHeight: 0 };
node.style = {
height: "",
setProperty: (k, v) => (node.style[k] = v),
};
node.append = (kid) => ((kid.parent = node), kid);
node.contains = (other) => {
for (let at = other; at; at = at.parent) if (at === node) return true;
return false;
};
node.closest = (selector) => {
const wanted = selector.split(",").map((s) => s.trim().replace(/^\./, ""));
for (let at = node; at; at = at.parent)
if (wanted.some((c) => at.className.split(/\s+/).includes(c))) return at;
return null;
};
return node;
}
// ---- 진짜 높이 나눔 ----
const heightsExports = {};
new Function(
"exports",
"require",
"window",
transpile(read("B05_Profile", "B05_Profile_UI_Profile_Heights.ts")),
)(
heightsExports,
(id) =>
id.includes("MassHaul")
? { MASSHAUL_MIN_HEIGHT: 160 }
: id.includes("TableOverlay")
? { TABLE_OVERLAY_MIN_HEIGHT: 120 }
: id.includes("Structures_Marks")
? { STRUCTURE_LANE_HEIGHT_PX: 40 }
: { frameOf: () => 0 },
{ innerHeight: 900 },
);
function run(docked) {
const root = el("b05-route-profile");
root.offsetHeight = 500;
const body = root.append(el());
body.clientHeight = 400;
const overlay = el("b05-profile__masshaul-overlay");
if (docked) body.append(overlay);
else el("popup-body").append(overlay); // 새 창 body
const cascade = heightsExports.createHeightCascade({
root,
body,
massHaul: { overlay, desiredHeight: () => 300, syncHandle() {} },
tableOverlay: {
overlay: el(),
isOpen: () => false,
desiredHeight: () => 0,
setBottomOffset() {},
},
isMainDragging: () => false,
isSubDragging: () => false,
mainDragRef: () => null,
isMainDragCooldown: () => true, // 자동 키우기 끔 — 나눔만 봄
hasDetail: () => true,
});
const { chartHeight } = cascade.apply(false);
return { chartHeight, inline: overlay.style.height };
}
// ---- 진짜 방향키 판정 본문 ----
const base = read("B06_Section", "B06_Section_UI_Cross_Panel_Base.ts");
const at = base.indexOf("export function arrowsBelongToProfile");
const fnSrc = base.slice(at, base.indexOf("\n}", at) + 2).replace("export ", "");
const arrows = new Function(
`let lastPointer; ${transpile(fnSrc)}; return (p) => ((lastPointer = p), arrowsBelongToProfile());`,
)();
const profileRoot = el("b05-route-profile");
const profileChild = profileRoot.append(el("b05-profile__chart"));
const massPopped = el("b05-profile__masshaul-overlay is-popout");
const massChild = massPopped.append(el("b05-profile__masshaul-scroll"));
const drainPopped = el("b05-drainage is-popout");
const drainChild = drainPopped.append(el("b05-drainage__canvas"));
const card = el("b06-cross-card");
process.stdout.write(
JSON.stringify({
docked: run(true),
popped: run(false),
arrows: {
profile: arrows(profileChild),
mass: arrows(massChild),
drainage: arrows(drainChild),
card: arrows(card),
none: arrows(null),
},
}),
);
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""224-1b(PLAN 325-2) 유토곡선 판 「붙어 있음」 판정 · 방향키.
- 높이 나눔 · 끌기는 「열림 && 종단 뿌리 안」만 자리를 줌 — 새 창으로 뗀 유토 판은 메인 자리 0 ·
임시 축소 인라인 높이도 안 씀(뗀 창 높이가 굳지 않게).
- 뗀 유토 판(`is-popout`)은 메인 보이는 구간 세로 범위(viewRange)를 안 씀 — 노선 전체 세로.
- 방향키 ←→ 판정은 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫.
`helper_224_1b_masshaul_docked.cjs` 가 진짜 TS 를 가짜 DOM 위에서 돌림.
"""
import json
import os
import re
import subprocess
from pathlib import Path
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
B05 = Path(HERE).parents[1] / "B05_Profile"
@pytest.fixture(scope="module")
def r():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_224_1b_masshaul_docked.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_붙은_유토는_자리를_차지(r):
"""본문 400 − 알약 40 = 360 · 유토 300 은 예산 247 넘어 247 로 줄고 그래프 100."""
assert r["docked"] == {"chartHeight": 100, "inline": "247px"}
def test_뗀_유토는_자리_0_인라인_없음(r):
assert r["popped"] == {"chartHeight": 347, "inline": ""}
def test_방향키_판정(r):
assert r["arrows"] == {
"profile": True,
"mass": True,
"drainage": True,
"card": False,
"none": False,
}
def test_끌기도_같은_판정():
drag = (B05 / "B05_Profile_UI_Profile_Drag.ts").read_text(encoding="utf-8")
assert "massHaul.overlay.hidden" in drag # 판정 함수 한 곳에만
assert drag.count("massHaul.overlay.hidden") == 1
assert drag.count("massDocked()") == 3
assert "root.contains(massHaul.overlay)" in drag
def test_뗀_유토는_노선_전체_세로():
mass = (B05 / "B05_Profile_UI_Profile_MassHaul.ts").read_text(encoding="utf-8")
assert re.search(
r'viewRange: overlay\.classList\.contains\("is-popout"\) \? undefined : params\.axis\.viewRange',
mass,
)