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:
@@ -133,6 +133,8 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag {
|
||||
};
|
||||
// 위쪽 경계를 끌어 패널 높이를 조절한다. 늘어난 만큼은 그래프만 먹고 도면 테이블은
|
||||
// 처음 잡힌 높이를 지킨다(사용자 지시) — 테이블 행이 늘었다 줄었다 하면 읽기 어려워서다.
|
||||
// 새 창으로 뗀 유토 판은 자리를 안 차지함 — 「열림 && 뿌리 안」(325-2).
|
||||
const massDocked = (): boolean => !massHaul.overlay.hidden && root.contains(massHaul.overlay);
|
||||
const heightResizer = createPanelResizer({
|
||||
axis: "vertical",
|
||||
target: root,
|
||||
@@ -143,7 +145,7 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag {
|
||||
// 셋 다 최소가 되면 여기서 더는 못 내려가게 막는다(뒤로 넘어가는 역전 차단).
|
||||
min: () => {
|
||||
const chrome = Math.max(0, root.offsetHeight - body.clientHeight);
|
||||
const massMin = massHaul.overlay.hidden ? 0 : MASSHAUL_MIN_HEIGHT;
|
||||
const massMin = massDocked() ? MASSHAUL_MIN_HEIGHT : 0;
|
||||
const tableMin = tableOverlay.isOpen() ? TABLE_OVERLAY_MIN_HEIGHT : 0;
|
||||
return Math.max(
|
||||
MIN_PANEL_HEIGHT,
|
||||
@@ -169,7 +171,7 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag {
|
||||
let mainDragRef: { chart: number; mass: number; table: number } | null = null;
|
||||
heightResizer.root.addEventListener("pointerdown", () => {
|
||||
mainPanelDragging = true;
|
||||
const massH = massHaul.overlay.hidden ? 0 : massHaul.overlay.offsetHeight;
|
||||
const massH = massDocked() ? massHaul.overlay.offsetHeight : 0;
|
||||
const tableH = tableOverlay.isOpen() ? tableOverlay.overlay.offsetHeight : 0;
|
||||
mainDragRef = {
|
||||
chart: Math.max(
|
||||
@@ -191,7 +193,7 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag {
|
||||
// 비례 연동 결과를 서브패널 저장 높이로 확정한다 — 이후 모든 redraw의 판정
|
||||
// 기준이 이 값이 되어 멱등성이 유지된다(확정 없이는 손을 떼는 순간 예전
|
||||
// 저장 높이로 튄다). 확정 후 인라인은 걷는다(변수가 같은 값을 담는다).
|
||||
if (!massHaul.overlay.hidden) massHaul.commitHeight(massHaul.overlay.offsetHeight);
|
||||
if (massDocked()) massHaul.commitHeight(massHaul.overlay.offsetHeight);
|
||||
if (tableOverlay.isOpen()) tableOverlay.commitHeight(tableOverlay.overlay.offsetHeight);
|
||||
massHaul.overlay.style.height = "";
|
||||
tableOverlay.overlay.style.height = "";
|
||||
|
||||
@@ -67,7 +67,8 @@ export function createHeightCascade(deps: HeightCascadeDeps): HeightCascade {
|
||||
// 구조물 알약 레인이 그래프 아래 한 줄을 차지한다 — 그만큼 먼저 떼어 놓고 나눈다
|
||||
// (2026-08-17 사용자 지시 6: 그래프가 밀려도 되지만 여유는 최소).
|
||||
const available = Math.max(120, deps.body.clientHeight - STRUCTURE_LANE_HEIGHT_PX);
|
||||
const massOpen = !deps.massHaul.overlay.hidden;
|
||||
// 새 창으로 뗀 유토 판은 자리를 안 차지함 — 「열림 && 뿌리 안」(325-2).
|
||||
const massOpen = !deps.massHaul.overlay.hidden && deps.root.contains(deps.massHaul.overlay);
|
||||
const tableOpen = deps.tableOverlay.isOpen();
|
||||
// 판정 기준은 항상 **저장 높이**(리사이저 변수·기본값)다 — 임시 축소가 반영된 화면
|
||||
// 높이(offsetHeight)로 재면 "줄임 → 충분해 보임 → 풀림 → 부족 → 다시 줄임"이
|
||||
|
||||
@@ -438,7 +438,12 @@ export function createRouteMassHaulPanel(
|
||||
window: undefined,
|
||||
hideStations: true,
|
||||
}
|
||||
: { ...params.axis, window: windowState };
|
||||
: {
|
||||
...params.axis,
|
||||
// 새 창으로 뗀 판은 넓어 메인 보이는 구간 세로면 잘림 — 노선 전체 세로(325-2).
|
||||
viewRange: overlay.classList.contains("is-popout") ? undefined : params.axis.viewRange,
|
||||
window: windowState,
|
||||
};
|
||||
|
||||
let massAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null;
|
||||
const chart = createMassHaulChart(
|
||||
|
||||
@@ -89,7 +89,10 @@ if (typeof document !== "undefined")
|
||||
|
||||
/** 방향키가 종단도 몫인가 — 마지막으로 누른 자리가 종단도 안이면 참. */
|
||||
export function arrowsBelongToProfile(): boolean {
|
||||
return !!lastPointer?.closest(".b05-route-profile");
|
||||
// 새 창으로 뗀 유토 · 배수 판도 종단도 몫(325-2).
|
||||
return !!lastPointer?.closest(
|
||||
".b05-route-profile, .b05-profile__masshaul-overlay, .b05-drainage",
|
||||
);
|
||||
}
|
||||
|
||||
/** 조정창 없는 고름(관 · 도수로/돌붙임 띠)이 방향키를 삼키나 — 그 동안 방향키는 아무것도 안 옮김.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user