From ff18ee2f2c2a4fb07901f4f56ebe5dcdfca86dd6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 15:38:45 +0900 Subject: [PATCH] =?UTF-8?q?feat(425=20B06=20=ED=8F=89=EB=A9=B4=20=EA=B2=B9?= =?UTF-8?q?=EC=B9=A8):=20=ED=9A=A1=EB=8B=A8=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=20=ED=8F=89=EB=A9=B4=20=EA=B2=B9=EC=B9=A8=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=E2=80=94=20=ED=8F=89=EB=A9=B4=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=20=EC=98=AE=EA=B2=A8=20=EB=B6=99=EC=9E=84=20=C2=B7=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=ED=8C=AC=20=C2=B7=20=ED=9C=A0=EB=A7=8C=20?= =?UTF-8?q?=C2=B7=20=ED=8F=89=EB=A9=B4=20=EB=B0=A9=ED=96=A5=ED=82=A4=20B05?= =?UTF-8?q?=20=ED=8F=89=EB=A9=B4=20=EC=95=88=EB=A7=8C=20(=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20258)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- A00_Common/design/design_page.css | 52 +++++++++++ A00_Common/design/design_plan_peek.ts | 87 +++++++++++++++++++ A00_Common/design/design_view_cross.ts | 2 + B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts | 2 +- ...B05_Profile_UI_RouteEdit_StructureMarks.ts | 6 +- resources/tester/test_258_b06_plan_peek.py | 68 +++++++++++++++ 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 A00_Common/design/design_plan_peek.ts create mode 100644 resources/tester/test_258_b06_plan_peek.py diff --git a/A00_Common/design/design_page.css b/A00_Common/design/design_page.css index 14c44dba2..2f352ef7f 100644 --- a/A00_Common/design/design_page.css +++ b/A00_Common/design/design_page.css @@ -91,3 +91,55 @@ .b05-route__panel [class*="b06-structure-panel__struct--"] { display: none; } + +/* 횡단 보기 위 평면 겹침 패널(요구 258 · 425 · `design_plan_peek`) — 카드 칸 위에 뜸(자리 안 차지) · + 닫히면 손잡이만 위 가운데 · 열면 가운데 칸 높이 45 % · 보기 · 팬 · 휠만이라 편집 도구는 감춤. */ +.design-plan-peek { + position: absolute; + z-index: 4; + top: 0; + right: 0; + left: 0; + height: 0; + pointer-events: none; +} + +.design-plan-peek.is-open { + height: 45%; +} + +.design-plan-peek__body { + display: none; + height: 100%; + border-bottom: 1px solid var(--panel-handle-border, var(--color-border)); + background: var(--color-surface); + box-shadow: var(--shadow-lg); + pointer-events: auto; +} + +.design-plan-peek.is-open > .design-plan-peek__body { + display: flex; +} + +.design-plan-peek__note { + margin: auto; + color: var(--color-text-secondary); +} + +/* 손잡이 = 패널 아래 가장자리 가운데(위로 접힘) — 공용 bottom 손잡이를 위아래 뒤집음. */ +.design-plan-peek > .ui-workflow-overlay__handle--bottom { + top: 100%; + border-top: 0; + border-bottom: 1px solid var(--panel-handle-border, var(--color-border)); + border-radius: 0 0 var(--radius-buttons) var(--radius-buttons); +} + +.design-plan-peek .b05-routeedit__tools, +.design-plan-peek .b05-routeedit__hint, +.design-plan-peek .b05-routeedit__info, +.design-plan-peek .b05-routeedit__measure, +.design-plan-peek .b05-routeedit__side-wrap, +.design-plan-peek .b05-routeedit__label, +.design-plan-peek .b05-contourlock { + display: none; +} diff --git a/A00_Common/design/design_plan_peek.ts b/A00_Common/design/design_plan_peek.ts new file mode 100644 index 000000000..c7d13941a --- /dev/null +++ b/A00_Common/design/design_plan_peek.ts @@ -0,0 +1,87 @@ +/* ============================================================================= + * design_plan_peek.ts + * 횡단 보기 위 **평면 겹침 패널**(요구 258 · 계획서 425) — 종단 보기의 평면(노선 편집) 노드를 + * 그대로 옮겨 붙여 보여 줌(둘째 캔버스 없음 · 편집기는 페이지에 하나). 열 때 옮기고 · 닫을 때 · + * 횡단 보기가 숨을 때 제자리(자리표 주석)로 돌림. 누름 · 휠은 캔버스에 붙어 함께 옴. + * + * 보기 · 가운데 버튼 팬 · 휠 확대만 — 왼쪽 · 오른쪽 누름(끌기 · 끼우기 · 지우기 · 재기)은 패널이 + * 캡처에서 먼저 받아 캔버스로 안 보냄(편집은 B05 평면). 단추 줄 · 상태줄 등은 `design_page.css`. + * ========================================================================== */ + +import { createWorkflowPanelHandle } from "@ui/ui_template_overlay"; +import type { DesignPageHost } from "./design_page"; +import type { DesignPageShared } from "./design_shared"; + +/** 횡단 보기 가운데(`center`)에 겹침 패널 · 손잡이를 세운다. */ +export function mountPlanPeek( + center: HTMLElement, + host: DesignPageHost, + shared: DesignPageShared, +): void { + const root = document.createElement("div"); + root.className = "design-plan-peek"; + const body = document.createElement("div"); + body.className = "design-plan-peek__body"; + const note = document.createElement("p"); + note.className = "design-plan-peek__note"; + body.append(note); + const handle = createWorkflowPanelHandle("bottom", "up", "평면"); + root.append(body, handle.root); + center.append(root); + + // 편집 몫 누름 막음 — 가운데 버튼(팬)만 통과 · 두 번 클릭(끼우기) · 오른쪽 클릭(지우기)도 막음. + // ponytail: 손가락 팬도 막힘(왼쪽 누름과 같은 단추 0) — 터치 기기에서 필요하면 pointerType 으로 가름. + body.addEventListener( + "pointerdown", + (event) => { + if (event.button !== 1) event.stopPropagation(); + }, + true, + ); + for (const type of ["dblclick", "contextmenu"]) + body.addEventListener( + type, + (event) => { + event.preventDefault(); + event.stopPropagation(); + }, + true, + ); + + let opened = false; + let plan: HTMLElement | null = null; + let home: Comment | null = null; + + const setOpen = (open: boolean): void => { + opened = open; + root.classList.toggle("is-open", open); + handle.setOpen(open); + }; + + const close = (): void => { + setOpen(false); + if (plan && home) home.replaceWith(plan); + home = null; + }; + + const open = async (): Promise => { + setOpen(true); + note.hidden = false; + note.textContent = "평면을 여는 중…"; + await shared.ensureView("profile"); // B06 바로 열기 = 종단 보기를 숨긴 채 세움 + if (!opened || !host.visible() || home) return; + plan = + center + .closest(".design-page__center") + ?.querySelector(".b05-route__main > .b05-routeedit") ?? null; + if (!plan) return void (note.textContent = "평면을 불러오지 못했습니다."); + note.hidden = true; + home = document.createComment("design-plan-peek"); + plan.replaceWith(home); + body.append(plan); // 크기 0 → 첫 크기 알림에 편집기가 다시 그림 + }; + + handle.root.addEventListener("click", () => (opened ? close() : void open())); + host.onHide(close); + setOpen(false); +} diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 2d383e2b5..93ed81554 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -93,6 +93,7 @@ import { type DesignPageShared, } from "./design_shared"; import "@util/common_util_mass_haul.css"; +import { mountPlanPeek } from "./design_plan_peek"; export async function mountCrossView( center: HTMLElement, @@ -409,6 +410,7 @@ export async function mountCrossView( mainArea.className = "b06-profile__main"; mainArea.append(sectionView.root); center.append(mainArea); + mountPlanPeek(center, host, shared); // 위 평면 겹침 패널(요구 258 · 425) function showSectionView(): void { if (!mainArea.contains(sectionView.root)) mainArea.replaceChildren(sectionView.root); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts index 42d4ea83d..ac7626873 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts @@ -105,7 +105,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () = function onKeyDown(event: KeyboardEvent): void { if (!canvas.isConnected) return detach(); - if (!isArrowKey(event.key) || !lastPointerWithin(".b05-routeedit")) return; + if (!isArrowKey(event.key) || !lastPointerWithin(".b05-route__main .b05-routeedit")) return; // B06 겹침 평면 = 키 안 먹음(425) const target = event.target as HTMLElement | null; if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return; // 평면이 삼킴 — 종단도(▲▼ 계획고 · `defaultPrevented` 면 비킴) · 횡단 조정창으로 안 감 diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts index 6c82835d8..18410a3db 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts @@ -370,7 +370,11 @@ export function bindStructureMarks( function onKeyDown(event: KeyboardEvent): void { if (!canvas.isConnected) return detach(); const escape = event.key === "Escape"; - if ((!escape && !isArrowKey(event.key)) || !lastPointerWithin(".b05-routeedit")) return; + if ( + (!escape && !isArrowKey(event.key)) || + !lastPointerWithin(".b05-route__main .b05-routeedit") + ) + return; // 겹침 평면 = 안 먹음(425) const target = event.target as HTMLElement | null; if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return; // Esc = 마커 고름만 풂(공용 Esc 풀기는 오른쪽 판까지 비우므로 앞에서 받음) diff --git a/resources/tester/test_258_b06_plan_peek.py b/resources/tester/test_258_b06_plan_peek.py new file mode 100644 index 000000000..79486e66a --- /dev/null +++ b/resources/tester/test_258_b06_plan_peek.py @@ -0,0 +1,68 @@ +"""258 · 425 — B06 위 평면 겹침 패널. + +종단 보기 평면 노드를 옮겨 붙임(둘째 캔버스 없음) · 닫을 때 · 횡단 숨김 때 제자리 · 손잡이 = +공용 bottom 위로 접힘 · z 4 · 높이 45 % · 편집 도구 감춤 · 가운데 버튼 팬 · 휠만(편집 누름 막음) · +평면 방향키는 B05 평면 안에서만. 배선은 글로 봄. +./venv/Scripts/python.exe -m pytest resources/tester/test_258_b06_plan_peek.py -q +""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +DESIGN = ROOT / "A00_Common" / "design" +PEEK = (DESIGN / "design_plan_peek.ts").read_text(encoding="utf-8") +CROSS = (DESIGN / "design_view_cross.ts").read_text(encoding="utf-8") +CSS = (DESIGN / "design_page.css").read_text(encoding="utf-8") +B05 = ROOT / "B05_Profile" + + +def _rule(selector: str) -> str: + body = CSS[CSS.index(selector + " {") :] + return body[: body.index("}")] + + +def test_횡단_보기가_패널을_세움() -> None: + assert 'import { mountPlanPeek } from "./design_plan_peek";' in CROSS + assert CROSS.index("center.append(mainArea);") < CROSS.index( + "mountPlanPeek(center, host, shared);" + ) + + +def test_평면_노드를_옮기고_제자리로() -> None: + assert 'createWorkflowPanelHandle("bottom", "up", "평면")' in PEEK + assert 'await shared.ensureView("profile");' in PEEK # B06 바로 열기 = 종단 보기 숨긴 채 + assert '".b05-route__main > .b05-routeedit"' in PEEK + assert "plan.replaceWith(home);" in PEEK and "body.append(plan);" in PEEK + assert "home.replaceWith(plan);" in PEEK # 닫기 = 자리표 자리로 + assert "host.onHide(close);" in PEEK # 횡단 숨김(B05 로 감) = 제자리 + assert 'createElement("canvas")' not in PEEK + assert len(PEEK.splitlines()) <= 700 + + +def test_편집_누름은_막고_팬_휠은_통과() -> None: + down = PEEK[PEEK.index('"pointerdown"') :] + down = down[: down.index("true,")] + assert "if (event.button !== 1) event.stopPropagation();" in down + assert 'for (const type of ["dblclick", "contextmenu"])' in PEEK + assert '"wheel"' not in PEEK # 휠 확대는 캔버스 그대로 + + +def test_겹침_자리와_감춤() -> None: + root = _rule(".design-plan-peek") + assert "position: absolute;" in root and "z-index: 4;" in root + assert "pointer-events: none;" in root # 닫힌 동안 카드 누름 통과 + assert "height: 45%;" in _rule(".design-plan-peek.is-open") + hidden = CSS[CSS.index(".design-plan-peek .b05-routeedit__tools,") :] + hidden = hidden[: hidden.index("}")] + for part in ("__tools", "__info", "__measure", "__side-wrap", "b05-contourlock"): + assert part in hidden + assert "display: none;" in hidden + + +def test_평면_방향키는_B05_평면_안만() -> None: + for name in ("B05_Profile_UI_RouteEdit_Keys.ts", "B05_Profile_UI_RouteEdit_StructureMarks.ts"): + text = (B05 / name).read_text(encoding="utf-8") + assert 'lastPointerWithin(".b05-route__main .b05-routeedit")' in text + assert 'lastPointerWithin(".b05-routeedit")' not in text