- 겹 표(map_layer_list) = 그룹1 22 줄 표 차례 · title = 호버제목 · group(수계망도 · 도로 · 토지·건물 · 제한·재해) · pages · 숨김 표 걷음 · 도로300m 한 줄(「도로」 묶음 · 기능 남김) - 겹 줄(map_layer_bar) = 단독 넷 | 묶음 넷(여럿 고르기 메뉴 · 켠 수 배지 · 안 닫힘) | 그래픽 카드 · sync 가 단추 · 메뉴 줄 막힘 · 글을 맞춤(why = 페이지 까닭) - 도구 표(map_toolbar) = 그룹2 17 줄 title · mapToolGroups 가 글을 표로 맞춤 · 기본 단추 생성 · 지형 칩 막힘 「수치지형도/라이다 — 까닭」 - 띠 = 한 자료 · 겹 둘(GL fillEdge 거름 · 옛 캔버스 · 범례 줄 따로) · B05 띠 받기(D03 과 같은 서버 길 · bandCollection 공용) - D03 = 겹 줄 · 도구줄 한 행(createMapToolRow) · 222px 기댐 · 줄바꿈 CSS 걷음 · 측점 · 기울기 호버 덮어쓰기 걷음 · LAS 올리기 호버 걷음 - B05 = 세로 두 줄 → 한 행 · 호버 덮어쓰기 걷음(도구줄 · 1m 등고선 · 3D · 추천) · 겹 막힘 querySelector 덮어쓰기 → 공용 sync · 임도(업로드) 보임 + 「받은 임도 자료 없음」 막힘 · 스냅 간격 날 select → 고르기 메뉴(하나) - 켬끔 기억 열쇠 · 측점 · 기울기 처음 값 · 그래픽 카드 취향 열쇠 둘 그대로 - 낡은 시험 고침(568_1 · 622_2 · 624_2 · 624_3 · 553_4 · 625_draw · b05_routeedit_initial · 626_3 · 390_4 · 346_2 · 353_1 · 581_2 · 584_1 · 584_3) — 표 꼴 row() · 호버 글 = 표 · 한 행 · 고르기 메뉴로 바뀐 글을 잡음 · 새 지킴 634_2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
8.9 KiB
Python
191 lines
8.9 KiB
Python
"""D03 길이·기울기 고정 부채꼴 셈(PLAN 625-2 · 요구 530 · 531 B1~B7) — TS `logic/D03_PlanAnalysis_Fan.ts` 순수 시험.
|
||
|
||
화면 고르기 전용(파이썬 짝 없음) — tsc 로 옮겨 Node 로 돌림.
|
||
- 경사면 65 % · 반지름 20 m · 9 % = 노드 4 · 호 2 · 호마다 + 하나 − 하나 · 부채꼴 폭 ≈ 2·arcsin(9/65).
|
||
- 꽃잎 땅 cos(nθ) = 노드 2 · 4 · 6 · 8 · 호 n.
|
||
- 정방향 = 앞 구간 쪽 호 · 첫 구간 없음 · 거의 같으면 마우스 쪽.
|
||
- 완만한 땅 = 노드 0 · 어느 방향이나 · Shift 사이 = 길이 그대로 · 기울기 = 실제 지형 · 호 밖 = 호 끝.
|
||
|
||
./venv/Scripts/python.exe -m pytest resources/tester/test_625_draw.py -q
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import math
|
||
import re
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
TS_FILES = [
|
||
ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Draw.ts",
|
||
ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Fan.ts",
|
||
]
|
||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||
|
||
_RUNNER = """
|
||
import { writeFileSync } from "node:fs";
|
||
import * as fan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Fan.js";
|
||
import { groundSlopePct, segmentLength } from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Draw.js";
|
||
const L = 20, S = 9, O = [0, 0];
|
||
const plane = (x, y) => 0.65 * x;
|
||
const gentle = (x, y) => 0.01 * x;
|
||
// 꽃잎 — 원 위 높이 차 = 10·(cos nθ − 1)/2 · 시작점 0
|
||
const petal = (n) => (x, y) => (Math.hypot(x, y) / L) * 10 * (Math.cos(n * Math.atan2(y, x)) - 1) / 2;
|
||
const at = (deg) => [Math.cos((deg * Math.PI) / 180) * 50, Math.sin((deg * Math.PI) / 180) * 50];
|
||
const deg = (p) => ((Math.atan2(p[1], p[0]) * 180) / Math.PI + 360) % 360;
|
||
const view = (f) => ({
|
||
nodes: f.nodes.map((n) => ({ deg: (n.angle * 180) / Math.PI, sign: n.sign })),
|
||
arcs: f.arcs.map((a) => ({ from: (a.from * 180) / Math.PI, to: (a.to * 180) / Math.PI, forward: a.forward })),
|
||
flat: f.flat,
|
||
});
|
||
const pick = (f, z, p) => p && { deg: deg(p), length: segmentLength(O, p), slope: groundSlopePct(O, p, z) };
|
||
const out = {};
|
||
// 경사면 — 앞 구간 북쪽으로(앞 점 = 남쪽) · 첫 구간 · 동쪽(오르막) 앞 구간 = 마우스 쪽
|
||
out.plane = view(fan.compassFan(O, L, S, plane, [0, -20]));
|
||
out.planeFirst = view(fan.compassFan(O, L, S, plane, null));
|
||
out.planeTieSouth = view(fan.compassFan(O, L, S, plane, [-20, 0], at(-80)));
|
||
out.planeTieNorth = view(fan.compassFan(O, L, S, plane, [-20, 0], at(80)));
|
||
const f = fan.compassFan(O, L, S, plane, [0, -20]);
|
||
out.between = pick(f, plane, fan.fanBetween(f, O, at(93), L));
|
||
out.betweenOut = pick(f, plane, fan.fanBetween(f, O, at(60), L));
|
||
out.pick = pick(f, plane, fan.fanPick(f, O, at(60), L));
|
||
out.pickReverse = pick(f, plane, fan.fanPick(f, O, at(-95), L));
|
||
for (const n of [1, 2, 3, 4]) out[`petal${n}`] = view(fan.compassFan(O, L, S, petal(n), null));
|
||
const g = fan.compassFan(O, L, S, gentle, [0, -20]);
|
||
out.gentle = view(g);
|
||
out.gentlePick = pick(g, gentle, fan.fanPick(g, O, at(200), L));
|
||
out.gentleBetween = pick(g, gentle, fan.fanBetween(g, O, at(200), L));
|
||
out.steep = view(fan.compassFan(O, L, S, (x, y) => 0.5 * Math.hypot(x, y), null));
|
||
out.offTerrain = view(fan.compassFan(O, L, S, () => NaN, null));
|
||
writeFileSync(process.argv[2], JSON.stringify(out));
|
||
"""
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def r(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||
tmp = tmp_path_factory.mktemp("fan")
|
||
out = tmp / "js"
|
||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||
["node", str(TSC), *map(str, TS_FILES), "--outDir", str(out), "--rootDir", str(ROOT)]
|
||
+ ["--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler"]
|
||
+ ["--ignoreConfig"],
|
||
cwd=str(ROOT),
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
for emitted in out.rglob("*.js"):
|
||
emitted.write_text(
|
||
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||
encoding="utf-8",
|
||
)
|
||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||
result = tmp / "out.json"
|
||
subprocess.run( # noqa: S603
|
||
["node", str(out / "runner.mjs"), str(result)],
|
||
cwd=str(ROOT),
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
return json.loads(result.read_text(encoding="utf-8"))
|
||
|
||
|
||
def _inside(arc: dict, deg: float) -> bool:
|
||
return (deg - arc["from"]) % 360 <= arc["to"] - arc["from"]
|
||
|
||
|
||
def test_경사면_노드_넷_호_둘_부호_짝(r):
|
||
f = r["plane"]
|
||
assert len(f["nodes"]) == 4 and len(f["arcs"]) == 2 and not f["flat"]
|
||
width = 2 * math.degrees(math.asin(9 / 65))
|
||
by_deg = {round(n["deg"], 3): n["sign"] for n in f["nodes"]}
|
||
for arc in f["arcs"]:
|
||
assert abs((arc["to"] - arc["from"]) - width) < 0.05
|
||
ends = {by_deg[round(arc["from"], 3)], by_deg[round(arc["to"] % 360, 3)]}
|
||
assert ends == {1, -1} # 호마다 + 하나 − 하나
|
||
assert _inside(arc, 90) or _inside(arc, 270) # 등고선 방향
|
||
# + = 오르막(동쪽 반) · − = 서쪽 반
|
||
for n in f["nodes"]:
|
||
assert (n["sign"] == 1) == (math.cos(math.radians(n["deg"])) > 0)
|
||
|
||
|
||
def test_정방향_앞_구간_쪽_첫_구간_없음_같으면_마우스_쪽(r):
|
||
forward = [a for a in r["plane"]["arcs"] if a["forward"]]
|
||
assert len(forward) == 1 and _inside(forward[0], 90)
|
||
assert not any(a["forward"] for a in r["planeFirst"]["arcs"])
|
||
south = [a for a in r["planeTieSouth"]["arcs"] if a["forward"]]
|
||
north = [a for a in r["planeTieNorth"]["arcs"] if a["forward"]]
|
||
assert _inside(south[0], 270) and _inside(north[0], 90)
|
||
|
||
|
||
@pytest.mark.parametrize("n", [1, 2, 3, 4])
|
||
def test_꽃잎_땅_노드_2_4_6_8(r, n):
|
||
f = r[f"petal{n}"]
|
||
assert len(f["nodes"]) == 2 * n and len(f["arcs"]) == n
|
||
for k in range(n): # 허용 호 = 꽃잎 꼭대기(cos nθ = 1)마다
|
||
assert sum(_inside(a, 360 * k / n) for a in f["arcs"]) == 1
|
||
assert all(node["sign"] == -1 for node in f["nodes"])
|
||
|
||
|
||
def test_shift_사이_길이_그대로_기울기_실제_지형(r):
|
||
b = r["between"]
|
||
assert abs(b["deg"] - 93) < 1e-6 and abs(b["length"] - 20) < 1e-9
|
||
assert abs(b["slope"] - 65 * math.cos(math.radians(93))) < 1e-6 and abs(b["slope"]) < 9
|
||
# 호 밖 = 가까운 호 끝(= ±9 % 노드)
|
||
o = r["betweenOut"]
|
||
assert abs(o["length"] - 20) < 1e-9 and abs(abs(o["slope"]) - 9) < 0.01
|
||
assert abs(o["deg"] - (90 - math.degrees(math.asin(9 / 65)))) < 0.05
|
||
|
||
|
||
def test_보통_고르기_가까운_노드_역방향도(r):
|
||
assert abs(abs(r["pick"]["slope"]) - 9) < 0.01
|
||
rev = r["pickReverse"]
|
||
assert abs(abs(rev["slope"]) - 9) < 0.01 and 260 < rev["deg"] < 280 # 역방향 호 노드도 고름
|
||
|
||
|
||
def test_완만한_땅_어느_방향이나(r):
|
||
g = r["gentle"]
|
||
assert g["flat"] and not g["nodes"] and not g["arcs"]
|
||
for key in ("gentlePick", "gentleBetween"):
|
||
p = r[key]
|
||
assert abs(p["deg"] - 200) < 1e-6 and abs(p["length"] - 20) < 1e-9
|
||
assert abs(p["slope"] - 1 * math.cos(math.radians(200))) < 1e-6
|
||
|
||
|
||
def test_가파른_땅_지형_밖_고를_자리_없음(r):
|
||
for key in ("steep", "offTerrain"):
|
||
f = r[key]
|
||
assert not f["flat"] and not f["nodes"] and not f["arcs"]
|
||
|
||
|
||
def test_놓인_노드_부호_색_선_위_고름_위에도_보임():
|
||
"""625-3 — 놓인 꺾임점 = 선 테(3 + 4 px)보다 굵은 부호 색 점 + 검은 테 · 고른 선은 노랑 강조 위에 점을 다시 그리고 손잡이 = 빈 네모."""
|
||
paint = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_DrawPaint.ts").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
edit = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Edit.ts").read_text(encoding="utf-8")
|
||
assert int(re.search(r"NODE_DOT_PX = (\d+)", paint).group(1)) * 2 > 3 + 4
|
||
body = paint[paint.index("export function paintNodes") :]
|
||
body = body[: body.index("\n}\n")]
|
||
assert "fillCased(context, slopeColor(groundSlopePct(" in body
|
||
assert "paintNodes(context, map, network.lines, z)" in paint
|
||
draw = edit[edit.index("draw(context, map) {") :]
|
||
assert draw.index("sel.paths()") < draw.index("paintNodes(") < draw.index("context.rect(")
|
||
assert "fillRect" not in draw[: draw.index("고른 노드 · 마크 테")]
|
||
|
||
|
||
def test_놓인_노드_미리보기_부채꼴_원_위에_그림():
|
||
"""625-4 — paintNetwork 는 미리보기(extra) 를 먼저 그리고 놓인 점(paintNodes)을 그 위에 · D03 도구줄은 줄바꿈."""
|
||
paint = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_DrawPaint.ts").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
body = paint[paint.index("export function paintNetwork") :]
|
||
body = body[: body.index("\n}\n")]
|
||
assert body.index("extra();") < body.index("paintNodes(")
|
||
css = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Style.css").read_text(encoding="utf-8")
|
||
# 634-3 — 줄바꿈 걷음 · 한 행(공용 `ui-map-row`) · 영역마다 가로 밀기
|
||
assert ".d03-plan__row {" in css and "flex-wrap: wrap" not in css
|