From 73117fa93144f7242450c5f8d0b3b62a87d900c5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 12:12:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05=20=EB=85=B8=EC=84=A0=ED=8E=B8=EC=A7=91?= =?UTF-8?q?):=20345-5=20=EB=8D=A7=202=20=EA=B3=A1=EC=84=A0=20=EB=B0=94=20?= =?UTF-8?q?=ED=8C=90=20=EC=9C=84=20=C2=B7=20[=EB=A7=9E=EC=B6=A4]=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=20=E2=80=94=20=EA=BA=BE=EC=9E=84=EC=A0=90=20?= =?UTF-8?q?=EA=B3=A1=EC=84=A0=20=EB=B0=94=20=EC=95=84=EB=9E=98=20=ED=95=9C?= =?UTF-8?q?=EA=B3=84=20=3D=20=EB=9C=AC=20=EC=A2=85=EB=8B=A8=20=ED=8C=90=20?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=20=EC=9C=84(=ED=8C=90=20=EB=81=8C=EA=B8=B0?= =?UTF-8?q?=20=C2=B7=20=EC=A0=91=EA=B8=B0=20=EB=94=B0=EB=9D=BC=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=90=EB=A6=AC)=20=C2=B7=20=EB=8F=84=EA=B5=AC?= =?UTF-8?q?=EC=A4=84=20[=EB=A7=9E=EC=B6=A4]=20=3D=20=EB=85=B8=EC=84=A0=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=EB=A5=BC=20=ED=8C=90=20=EC=9C=84=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EB=8A=94=20=EC=98=81=EC=97=AD=EC=97=90(=ED=9A=8C?= =?UTF-8?q?=EC=A0=84=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _RouteEdit_View.fitView(처음 열기 · [맞춤] 한 벌) · _RouteEdit_Cover.onCoverChange(덮인 높이 바뀜 알림 · 떠난 편집기는 뗌) - 시험: test_345_5b_plan_visible_area 3 · 관련 81 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- B05_Profile/B05_Profile_UI_RouteEdit.ts | 8 ++++++++ .../B05_Profile_UI_RouteEdit_Chrome.ts | 2 ++ B05_Profile/B05_Profile_UI_RouteEdit_Cover.ts | 14 +++++++++++-- .../B05_Profile_UI_RouteEdit_CurveBar.ts | 7 ++++++- B05_Profile/B05_Profile_UI_RouteEdit_View.ts | 11 +++++++++- .../tester/test_345_5b_plan_visible_area.py | 20 ++++++++++++++++++- 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 68c58f846..6d916379f 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -54,6 +54,7 @@ import { captureView, createViewMemory, fitCanvas, + fitView, openingView, restoreSavedView, screenToMetric, @@ -308,6 +309,13 @@ export async function mountRouteEditor( rotation.set(opening.rotationRad); viewReady = true; } + // [맞춤] — 노선 전체를 뜬 종단 판 위 보이는 영역에(회전 0 · 345-5 덧 2). 셈은 `_View` 몫. + overlay.querySelector('[data-act="fit"]')!.addEventListener("click", () => { + if (!meta || view.width <= 0 || !planned.length) return; + view = fitView(meta, view, planned); + rotation.set(0); + draw(); + }); const toScreen = (vertex: Vertex): [number, number] => meta ? metricToScreen(meta, view, vertex[0], vertex[1]) : [0, 0]; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index cf85dcb7a..63c5000ed 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -65,6 +65,8 @@ export function createRouteEditChrome( + diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cover.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cover.ts index 4a0c83b1e..6b0563340 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cover.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cover.ts @@ -3,8 +3,9 @@ * 노선 편집 평면 아래를 **뜬 종단 판**이 덮은 높이를 잰다(345-5 덧). * * 칸 · 판 크기가 바뀔 때마다(창 · 판 높이 끌기 · 접기 · 새 창 떼기) 다시 재서 - * 칸에 `--b05-routeedit-cover`(px)를 적는다 — 상태 · 범례 · 잰 값 글판이 판 바로 위로 오름. - * 맞춤(`_View.openingView`)은 `coveredPx()` 를 읽는다. 셈은 `logic/_RouteEdit_Visible`. + * 칸에 `--b05-routeedit-cover`(px)를 적는다 — 상태 · 범례 · 잰 값 글판이 판 바로 위로 오름 · + * 곡선 바는 `onCoverChange` 로 다시 자리 잡음. + * 맞춤(`_View.fitView`)은 `coveredPx()` 를 읽는다. 셈은 `logic/_RouteEdit_Visible`. * ========================================================================== */ import { coveredBottomPx } from "./logic/B05_Profile_RouteEdit_Visible"; @@ -12,12 +13,18 @@ import { coveredBottomPx } from "./logic/B05_Profile_RouteEdit_Visible"; // ponytail: 편집기는 페이지에 하나 — 덮인 높이 한 값 · 둘이 되면 칸마다 값으로. let covered = 0; const watching = new WeakSet(); +const listeners = new Set<() => boolean>(); /** 지금 평면 아래 덮인 높이(px). */ export function coveredPx(): number { return covered; } +/** 덮인 높이가 바뀌면 부름(곡선 바 자리 · 345-5 덧 2) — `false` 를 돌려주면 뗌(떠난 편집기). */ +export function onCoverChange(listener: () => boolean): void { + listeners.add(listener); +} + /** 같은 가운데 칸의 종단 판 — 새 창으로 떼면 없음. */ function panelOf(wrap: HTMLElement): HTMLElement | null { return ( @@ -31,8 +38,11 @@ function span(element: HTMLElement): { top: number; bottom: number } { } function measure(wrap: HTMLElement, panel: HTMLElement | null): void { + const before = covered; covered = coveredBottomPx(span(wrap), panel ? span(panel) : null); wrap.style.setProperty("--b05-routeedit-cover", `${covered}px`); + if (covered !== before) + for (const listener of listeners) if (!listener()) listeners.delete(listener); } /** 칸을 지켜본다(여러 번 불러도 한 번만 붙음) · 부를 때마다 한 번 잰다. */ diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_CurveBar.ts b/B05_Profile/B05_Profile_UI_RouteEdit_CurveBar.ts index b10a00c54..06483dc4b 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_CurveBar.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_CurveBar.ts @@ -22,6 +22,7 @@ import { createCurveLabel, type CurveLabel, } from "./B05_Profile_UI_RouteEdit_Label"; +import { coveredPx, onCoverChange } from "./B05_Profile_UI_RouteEdit_Cover"; /** 패널이 만지는 편집값 한 벌 — 모달이 쥔 배열을 그대로 건네받는다. */ export interface CurveBarState { @@ -145,7 +146,8 @@ export function createCurveBar(params: CurveBarParams): CurveBar { left: rect.left + 8, top: rect.top + 8, right: rect.right - 8, - bottom: rect.bottom - 8, + // 뜬 종단 판 밑은 안 보임 — 판 바로 위까지(345-5 덧 2 · 접으면 0). + bottom: rect.bottom - 8 - coveredPx(), }; const last = planned.length - 1; if (picked === 0 || picked === last) { @@ -202,5 +204,8 @@ export function createCurveBar(params: CurveBarParams): CurveBar { }); } + // 판 끌기 · 접기 — 덮인 높이가 바뀌면 다시 자리 잡음(그리기 없이 · 떠나면 뗌). + onCoverChange(() => (params.container.isConnected ? (sync(), true) : false)); + return { label, sync }; } diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_View.ts b/B05_Profile/B05_Profile_UI_RouteEdit_View.ts index 406740ed5..9498c0d06 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_View.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_View.ts @@ -124,6 +124,15 @@ export function openingView( }); if (seen) return { view: restored, rotationRad }; } + return { view: fitView(meta, view, nodes), rotationRad: 0 }; +} + +/** 노선 전체를 뜬 종단 판 위 보이는 영역 가운데에(회전 0 기준) — 처음 열기 · [맞춤](345-5 덧). */ +export function fitView( + meta: VWorldMeta, + view: ViewState, + nodes: ReadonlyArray, +): ViewState { const xs = nodes.map((vertex) => vertex[0]); const ys = nodes.map((vertex) => vertex[1]); const [xMin, xMax, yMin, yMax] = [ @@ -145,7 +154,7 @@ export function openingView( wantWidthM: Math.max(xMax - xMin, 1) + ROUTE_VIEW_MARGIN_M * 2, wantHeightM: Math.max(yMax - yMin, 1) + ROUTE_VIEW_MARGIN_M * 2, }); - return { view: { ...view, ...fitted }, rotationRad: 0 }; + return { ...view, ...fitted }; } /** 캔버스 버퍼를 감싼 칸 크기(고해상도 배율 포함)에 맞춘다. 칸의 CSS 크기를 돌려준다. */ diff --git a/resources/tester/test_345_5b_plan_visible_area.py b/resources/tester/test_345_5b_plan_visible_area.py index 1c86636e7..4fc02a0c2 100644 --- a/resources/tester/test_345_5b_plan_visible_area.py +++ b/resources/tester/test_345_5b_plan_visible_area.py @@ -1,4 +1,4 @@ -"""345-5 덧 — 노선 편집 평면의 보이는 영역(뜬 종단 판 위) · 노선 맞춤 · 글판 자리. +"""345-5 덧 — 노선 편집 평면의 보이는 영역(뜬 종단 판 위) · 노선 맞춤 · 글판 · 곡선 바 자리. `logic/B05_Profile_RouteEdit_Visible.ts` 를 tsc 로 옮겨 node 로 돌림 · 배선 · CSS 는 글로 봄. ./venv/Scripts/python.exe -m pytest resources/tester/test_345_5b_plan_visible_area.py -q @@ -130,3 +130,21 @@ def test_배선과_글판_자리() -> None: # 접힌 판은 높이 0 — 덮인 높이도 0 이 됨 style = (B05 / "B05_Profile_UI_Style.css").read_text(encoding="utf-8") assert ".b05-route-profile.is-collapsed {\n height: 0;" in style + + +def test_곡선_바는_판_위_맞춤_단추(): + """345-5 덧 2 — 곡선 바 아래 한계 = 판 바로 위 · 판 끌기 · 접기 따라감 · [맞춤] 단추.""" + bar = (B05 / "B05_Profile_UI_RouteEdit_CurveBar.ts").read_text(encoding="utf-8") + assert "bottom: rect.bottom - 8 - coveredPx()," in bar + assert "onCoverChange(() => (params.container.isConnected ? (sync(), true) : false));" in bar + cover = (B05 / "B05_Profile_UI_RouteEdit_Cover.ts").read_text(encoding="utf-8") + assert "if (covered !== before)" in cover and "listeners.delete(listener)" in cover + chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8") + assert 'data-act="fit"' in chrome and ">맞춤" in chrome + editor = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8") + at = editor.index("""overlay.querySelector('[data-act="fit"]')!""") + handler = editor[at : at + 260] + assert "view = fitView(meta, view, planned);" in handler and "rotation.set(0);" in handler + assert len(editor.splitlines()) <= 700 + view = (B05 / "B05_Profile_UI_RouteEdit_View.ts").read_text(encoding="utf-8") + assert "return { view: fitView(meta, view, nodes), rotationRad: 0 };" in view