feat(종단): 325-1 배수 판 창구 — 지도 크기 관찰을 놓인 창 것으로 바꿔 끼우는 rehome · 직전 크기 이어받아 배율 유지
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -536,8 +536,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
selectBasin: (index) => selectBasin(index),
|
||||
});
|
||||
|
||||
// 패널을 늘리면 지도를 더 보여 줄 뿐, 배율은 그대로 둔다 — 늘릴 때마다 확대되면
|
||||
// 방금 보던 자리를 다시 찾아야 한다(2026-08-02 사용자 지시).
|
||||
// 패널을 늘리면 지도를 더 보여 줄 뿐, 배율은 그대로 둔다(늘릴 때마다 확대되면 보던 자리를 잃음).
|
||||
const resizeObserver = observeViewportSize(viewport, {
|
||||
meta: () => meta,
|
||||
anchor: () => ({ width: 0, height: 0, scale, offsetX, offsetY }),
|
||||
@@ -696,5 +695,6 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
resizeObserver.disconnect();
|
||||
widthResizer.dispose();
|
||||
},
|
||||
rehome: () => resizeObserver.rehome(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface DrainagePanel {
|
||||
/** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6).
|
||||
* 종단 레인의 띠와 같은 뜻이고, 목록이 바뀔 때마다 통째로 넣어 준다. */
|
||||
setIntervalSpans: (spans: ReadonlyArray<RouteSpanBand>) => void;
|
||||
/** 판이 다른 창(새 창 · 제자리)으로 옮겨 간 뒤 — 지도 크기 관찰을 그 창 것으로 바꿔 낌(325-1). */
|
||||
rehome: () => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { MapContextMenu, MapContextMenuItem } from "@ui/ui_template_context
|
||||
import type { DetailBasin } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { PipeEditor, PipePoint } from "./B05_Profile_UI_Drainage_Pipes";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { viewOf } from "../common_util/common_util_view_window";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -454,7 +455,8 @@ export function preserveViewOnResize(
|
||||
};
|
||||
}
|
||||
|
||||
/** 뷰포트 크기 변화를 지켜보며 배율을 지킨다. 반환값은 정리용 `ResizeObserver`. */
|
||||
/** 뷰포트 크기 변화를 지켜보며 배율을 지킨다. `rehome` = 판이 다른 창으로 옮겨 간 뒤 그 창의
|
||||
* 관찰자로 바꿔 낌(직전 크기를 지켜 배율 유지 · 325-1) · `disconnect` = 정리. */
|
||||
export function observeViewportSize(
|
||||
viewport: HTMLElement,
|
||||
ports: {
|
||||
@@ -463,9 +465,9 @@ export function observeViewportSize(
|
||||
apply: (next: { scale: number; offsetX: number; offsetY: number }) => void;
|
||||
redraw: () => void;
|
||||
},
|
||||
): ResizeObserver {
|
||||
): { rehome: () => void; disconnect: () => void } {
|
||||
let last: { width: number; height: number } | null = null;
|
||||
const observer = new ResizeObserver(() => {
|
||||
const onResize = (): void => {
|
||||
const rect = viewport.getBoundingClientRect();
|
||||
const width = Math.max(1, Math.floor(rect.width));
|
||||
const height = Math.max(1, Math.floor(rect.height));
|
||||
@@ -482,9 +484,22 @@ export function observeViewportSize(
|
||||
}
|
||||
last = { width, height };
|
||||
ports.redraw();
|
||||
});
|
||||
observer.observe(viewport);
|
||||
return observer;
|
||||
};
|
||||
// 새 창 크기 변화는 그 창의 관찰자만 안다 — 판이 놓인 창의 것으로 단다.
|
||||
const watch = (): ResizeObserver => {
|
||||
const view = viewOf(viewport) as Window & typeof globalThis;
|
||||
const next = new view.ResizeObserver(onResize);
|
||||
next.observe(viewport);
|
||||
return next;
|
||||
};
|
||||
let observer = watch();
|
||||
return {
|
||||
rehome: () => {
|
||||
observer.disconnect();
|
||||
observer = watch();
|
||||
},
|
||||
disconnect: () => observer.disconnect(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 캔버스 버퍼를 뷰포트 크기·DPR에 맞춘다. 실제로 바뀔 때만 재할당한다(재할당 시 내용이 지워진다). */
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/* 224-1a 배수 판 창구(rehome) — 진짜 `observeViewportSize`(B05_Profile_UI_Drainage_Parts.ts)를
|
||||
* 가짜 창 둘 위에서 돌려 JSON 으로 냄.
|
||||
* · 처음 = 판이 놓인 창(메인)의 관찰자
|
||||
* · rehome = 옛 관찰자 끊고 지금 놓인 창(새 창)의 관찰자로 · 직전 크기 이어받아 배율 유지
|
||||
* · disconnect = 지금 관찰자 끊음 */
|
||||
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();
|
||||
const stub = new Proxy({}, { get: () => () => "" });
|
||||
|
||||
/** TS 한 장을 CommonJS 로 — 상대 경로는 진짜(공용 창 · 지도 셈만), 나머지는 빈 함수. */
|
||||
function loadTs(full) {
|
||||
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 localRequire = (request) =>
|
||||
/common_util_view_window|B04_PreProcess_UI_MapRender/.test(request)
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: stub;
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
/** 가짜 창 — 관찰자를 모아 두고 크기를 바꿔 울림. */
|
||||
function fakeWindow(name) {
|
||||
const win = { name, observers: [] };
|
||||
win.ResizeObserver = class {
|
||||
constructor(fn) {
|
||||
this.fn = fn;
|
||||
this.live = false;
|
||||
win.observers.push(this);
|
||||
}
|
||||
observe() {
|
||||
this.live = true;
|
||||
}
|
||||
disconnect() {
|
||||
this.live = false;
|
||||
}
|
||||
};
|
||||
return win;
|
||||
}
|
||||
const main = fakeWindow("main");
|
||||
const popup = fakeWindow("popup");
|
||||
global.window = main;
|
||||
|
||||
const parts = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Drainage_Parts.ts"));
|
||||
const doc = { defaultView: main };
|
||||
let size = { width: 400, height: 300 };
|
||||
const viewport = { ownerDocument: doc, getBoundingClientRect: () => ({ ...size }) };
|
||||
const fire = (win) => win.observers.filter((o) => o.live).forEach((o) => o.fn());
|
||||
|
||||
let view = { scale: 2, offsetX: 10, offsetY: -5 };
|
||||
const applied = [];
|
||||
let redraws = 0;
|
||||
const META = {
|
||||
x_min: 0,
|
||||
x_max: 4000,
|
||||
y_min: 0,
|
||||
y_max: 3000,
|
||||
width_meters: 4000,
|
||||
height_meters: 3000,
|
||||
};
|
||||
const watcher = parts.observeViewportSize(viewport, {
|
||||
meta: () => META,
|
||||
anchor: () => ({ width: 0, height: 0, ...view }),
|
||||
apply: (next) => (applied.push(next), (view = next)),
|
||||
redraw: () => (redraws += 1),
|
||||
});
|
||||
const out = { firstOwner: main.observers.length === 1 && popup.observers.length === 0 };
|
||||
fire(main); // 처음 크기 기억
|
||||
out.firstApplied = applied.length;
|
||||
|
||||
// 새 창으로 옮김 — 메인 관찰자 끊고 새 창 관찰자로
|
||||
doc.defaultView = popup;
|
||||
watcher.rehome();
|
||||
out.mainLive = main.observers.some((o) => o.live);
|
||||
out.popupLive = popup.observers.filter((o) => o.live).length;
|
||||
size = { width: 800, height: 600 }; // 새 창에서 두 배로 커짐
|
||||
fire(popup);
|
||||
out.afterGrow = { applied: applied.length, ...view };
|
||||
// 메인 창 관찰자가 울려도(이미 끊김) 아무 일 없음
|
||||
fire(main);
|
||||
out.mainIgnored = applied.length;
|
||||
|
||||
// 제자리 — 다시 메인
|
||||
doc.defaultView = main;
|
||||
watcher.rehome();
|
||||
out.backPopupLive = popup.observers.some((o) => o.live);
|
||||
out.backMainLive = main.observers.filter((o) => o.live).length;
|
||||
watcher.disconnect();
|
||||
out.afterDispose = [...main.observers, ...popup.observers].some((o) => o.live);
|
||||
out.redraws = redraws;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""224-1a(PLAN 325-1) 배수유역 판 창구 — 새 창으로 떼어도 지도 크기 관찰이 그 창을 따름.
|
||||
|
||||
- 판 지도 크기 관찰(`observeViewportSize`)은 판이 놓인 창의 ResizeObserver 를 씀.
|
||||
- `rehome()` = 옛 창 관찰자 끊고 지금 놓인 창 것으로 · 직전 크기를 이어받아 배율(화면 m 당 px) 유지.
|
||||
- 판 창구(`DrainagePanel.rehome`)로 내보내고 패널 본체는 700줄 안.
|
||||
|
||||
`helper_224_1a_drainage_rehome.cjs` 가 진짜 TS 를 가짜 창 둘 위에서 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
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_1a_drainage_rehome.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):
|
||||
assert r["firstOwner"] is True
|
||||
assert r["firstApplied"] == 0 # 처음 크기는 기억만
|
||||
|
||||
|
||||
def test_rehome_은_새_창_관찰자로(r):
|
||||
assert r["mainLive"] is False
|
||||
assert r["popupLive"] == 1
|
||||
|
||||
|
||||
def test_새_창에서_커져도_배율_유지(r):
|
||||
"""400 → 800 px · 배율 2 → 1 = 화면 m 당 px 그대로."""
|
||||
grow = r["afterGrow"]
|
||||
assert grow["applied"] == 1
|
||||
assert grow["scale"] == pytest.approx(1)
|
||||
assert r["mainIgnored"] == 1 # 끊긴 메인 관찰자는 안 울림
|
||||
|
||||
|
||||
def test_제자리와_정리(r):
|
||||
assert r["backPopupLive"] is False
|
||||
assert r["backMainLive"] == 1
|
||||
assert r["afterDispose"] is False
|
||||
assert r["redraws"] == 2
|
||||
|
||||
|
||||
def test_판_창구와_700줄():
|
||||
panel = (B05 / "B05_Profile_UI_Drainage_Panel.ts").read_text(encoding="utf-8")
|
||||
assert "rehome: () => resizeObserver.rehome()," in panel
|
||||
assert len(panel.splitlines()) <= 700
|
||||
types = (B05 / "B05_Profile_UI_Drainage_Panel_Types.ts").read_text(encoding="utf-8")
|
||||
assert "rehome: () => void;" in types
|
||||
Reference in New Issue
Block a user