Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/* 281-1 배수유역 지도 줌아웃 한계 헬퍼 — `B05_Profile_UI_Drainage_ZoomRange.ts` 를
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌리고 값을 JSON 으로 낸다.
|
||||
* `.cjs` 까닭은 `helper_b05_curve_radius_floor.cjs` 와 같다.
|
||||
*/
|
||||
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();
|
||||
|
||||
/** TS 한 장을 CommonJS 로 옮겨 실행한다. `@ui/` 는 색 함수만 쓰므로 빈 값으로 갈음한다. */
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
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) => {
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
if (request.startsWith("@ui/")) return { themeColor: () => "" };
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const zoom = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Drainage_ZoomRange.ts"));
|
||||
const render = loadTs(path.join(ROOT, "B04_PreProcess", "B04_PreProcess_UI_MapRender.ts"));
|
||||
const { computeZoomOutLimit, clampWheelScale, ZOOM_OUT_ROUTE_FACTOR, ZOOM_OUT_FALLBACK_SCALE } =
|
||||
zoom;
|
||||
|
||||
// 도엽 4000 × 3000 m — 경위도는 1 도 = 1000 m 로 단순하게 둔다.
|
||||
const META = {
|
||||
x_min: 0,
|
||||
x_max: 4000,
|
||||
y_min: 0,
|
||||
y_max: 3000,
|
||||
width_meters: 4000,
|
||||
height_meters: 3000,
|
||||
lon_min: 127,
|
||||
lon_max: 131,
|
||||
lat_min: 36,
|
||||
lat_max: 39,
|
||||
};
|
||||
const W = 800;
|
||||
const H = 600; // 도엽과 같은 비율 — 배율 1 = 화면 폭 4000 m
|
||||
const toLonLat = (x, y) => [127 + x / 1000, 36 + y / 1000];
|
||||
/** 배율 s 에서 화면 폭에 드는 실폭(m). */
|
||||
const viewWidthM = (s) => 4000 / s;
|
||||
|
||||
// 노선 1000 × 500 m (가운데 2000, 1500) — 2 배 = 2000 × 1000 m.
|
||||
const ROUTE = [
|
||||
{ x: 1500, y: 1250 },
|
||||
{ x: 2500, y: 1750 },
|
||||
];
|
||||
const routeOnly = computeZoomOutLimit(META, ROUTE, [], W, H);
|
||||
|
||||
// 유역이 노선 2 배 범위 밖(서쪽 400 m 까지)으로 나감 → 폭 2600 m.
|
||||
const basinOut = computeZoomOutLimit(
|
||||
META,
|
||||
ROUTE,
|
||||
[{ polygon_lonlat: [toLonLat(400, 1500), toLonLat(1500, 1400), toLonLat(1500, 1600)] }],
|
||||
W,
|
||||
H,
|
||||
);
|
||||
// 유역이 2 배 범위 안 → 한계 그대로.
|
||||
const basinIn = computeZoomOutLimit(
|
||||
META,
|
||||
ROUTE,
|
||||
[{ polygon_lonlat: [toLonLat(1800, 1400), toLonLat(2200, 1400), toLonLat(2000, 1600)] }],
|
||||
W,
|
||||
H,
|
||||
);
|
||||
|
||||
// 노선이 도엽만큼 길면 2 배가 도엽보다 큼 → 도엽 전체(배율 1)에서 멈춤.
|
||||
const wide = computeZoomOutLimit(
|
||||
META,
|
||||
[
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 3900, y: 2900 },
|
||||
],
|
||||
[],
|
||||
W,
|
||||
H,
|
||||
);
|
||||
|
||||
// 짧은 노선(100 m) — 2 배(200 m)가 처음 보기(노선 + 200 m 여유 양쪽)보다 좁음 → 처음 보기에 맞춤.
|
||||
const SHORT = [
|
||||
{ x: 1950, y: 1500 },
|
||||
{ x: 2050, y: 1500 },
|
||||
];
|
||||
const short = computeZoomOutLimit(META, SHORT, [], W, H);
|
||||
const shortInitial = render.computeRouteView(
|
||||
META,
|
||||
{ x_min: 1950, x_max: 2050, y_min: 1500, y_max: 1500 },
|
||||
W,
|
||||
H,
|
||||
).scale;
|
||||
|
||||
// 휠 — 밀기(×0.87)를 계속하면 한계에서 멈춤 · 당기기(×1.15)는 상한에서 멈춤.
|
||||
let scale = 3;
|
||||
for (let i = 0; i < 40; i += 1) scale = clampWheelScale(scale * 0.87, scale, routeOnly, 200);
|
||||
const wheelOutStop = scale;
|
||||
for (let i = 0; i < 80; i += 1) scale = clampWheelScale(scale * 1.15, scale, routeOnly, 200);
|
||||
const wheelInStop = scale;
|
||||
// 이미 한계 밖(더 멀리)에 있으면 밀어도 더 안 빠지고 튀지도 않음.
|
||||
const outside = clampWheelScale(1.2 * 0.87, 1.2, routeOnly, 200);
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
factor: ZOOM_OUT_ROUTE_FACTOR,
|
||||
fallbackConst: ZOOM_OUT_FALLBACK_SCALE,
|
||||
routeOnly,
|
||||
routeOnlyViewM: viewWidthM(routeOnly),
|
||||
basinOutViewM: viewWidthM(basinOut),
|
||||
basinIn,
|
||||
noRoute: computeZoomOutLimit(META, [], [], W, H),
|
||||
noMeta: computeZoomOutLimit(null, ROUTE, [], W, H),
|
||||
wide,
|
||||
short,
|
||||
shortInitial,
|
||||
wheelOutStop,
|
||||
wheelInStop,
|
||||
outside,
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""281 B05 · B06 아래 종단 패널 배수유역 지도 — 줌아웃 한계 = 노선 기준.
|
||||
|
||||
- 한계 = 노선 범위 2 배가 화면에 드는 배율 · 유역이 밖이면 유역까지 · 도엽 전체(배율 1)보다 멀리 안 감
|
||||
- 노선이 없으면 종전 0.5 · 처음 보기(노선 + 여유)보다 좁히지 않음
|
||||
- 휠은 한계 · 상한에서 멈춤 · 이미 한계 밖이면 더 안 빠지고 튀지도 않음
|
||||
|
||||
Node 헬퍼(`helper_281_1_drainage_zoom_range.cjs`)가 TS 를 그 자리에서 돌린다.
|
||||
도엽 4000 × 3000 m · 화면 800 × 600 — 배율 1 = 화면 폭 4000 m.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = Path(HERE).parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_281_1_drainage_zoom_range.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_상수는_한_곳(r):
|
||||
assert r["factor"] == 2
|
||||
assert r["fallbackConst"] == 0.5
|
||||
|
||||
|
||||
def test_노선만_2배가_화면에(r):
|
||||
"""노선 1000 × 500 m → 2 배 2000 × 1000 m · 폭이 이김 → 화면 폭 2000 m(배율 2)."""
|
||||
assert r["routeOnly"] == pytest.approx(2.0)
|
||||
assert r["routeOnlyViewM"] == pytest.approx(2000)
|
||||
|
||||
|
||||
def test_유역이_밖이면_유역까지(r):
|
||||
"""유역 서쪽 끝 400 m → 범위 400 ~ 3000 m = 폭 2600 m."""
|
||||
assert r["basinOutViewM"] == pytest.approx(2600)
|
||||
|
||||
|
||||
def test_유역이_안이면_그대로(r):
|
||||
assert r["basinIn"] == pytest.approx(r["routeOnly"])
|
||||
|
||||
|
||||
def test_노선_없음은_종전(r):
|
||||
assert r["noRoute"] == 0.5
|
||||
assert r["noMeta"] == 0.5
|
||||
|
||||
|
||||
def test_도엽보다_큰_범위는_도엽_전체에서_멈춤(r):
|
||||
assert r["wide"] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_짧은_노선은_처음_보기보다_좁히지_않음(r):
|
||||
assert r["short"] == pytest.approx(r["shortInitial"])
|
||||
|
||||
|
||||
def test_휠이_한계_안에서_멈춤(r):
|
||||
assert r["wheelOutStop"] == pytest.approx(r["routeOnly"])
|
||||
assert r["wheelInStop"] == pytest.approx(200)
|
||||
assert r["outside"] == pytest.approx(1.2)
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_휠과_창_크기가_한계를_씀():
|
||||
interact = _read("B05_Profile/B05_Profile_UI_Drainage_Interact.ts")
|
||||
assert "Math.max(0.5" not in interact
|
||||
assert "params.getMinScale()" in interact
|
||||
panel = _read("B05_Profile/B05_Profile_UI_Drainage_Panel.ts")
|
||||
assert "getMinScale: zoomOutLimit" in panel
|
||||
assert "scale < zoomOutLimit() * 0.999) fitToRoute()" in panel
|
||||
assert len(panel.splitlines()) <= 700
|
||||
|
||||
|
||||
def test_B04_공용은_그대로():
|
||||
"""B04 지도 · 노선 편집 지도는 0.5 하한을 그대로 씀(공용 함수 안 바꿈)."""
|
||||
render = _read("B04_PreProcess/B04_PreProcess_UI_MapRender.ts")
|
||||
assert "ZoomRange" not in render
|
||||
Reference in New Issue
Block a user