Files
Aislo/resources/tester/test_625_draw.py
T
eomsangdonandClaude Sonnet 5.5 306a88e964 fix(D03 긋기): 긋는 중 놓인 점이 부채꼴 · 점선 원 위에 보임 · 좁은 폭에서 도구줄 줄바꿈으로 [LAS 올리기] 안 잘림 (PLAN 625-4 · 점검 535-3)
- DrawPaint `paintNetwork` 미리보기(부채꼴 · 원) 먼저 · 놓인 점 그 위
- D03 도구줄 CSS 줄바꿈(공용 한 줄 밀기 · B05 그대로)
- 시험 test_625_draw 순서 · 줄바꿈 지킴 하나 덧

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uTVHDYKk4dyt7bEAQKcVU
2026-10-31 22:45:14 +09:00

190 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")
assert re.search(r"\.d03-plan__tools > \.ui-toolbar \{[^}]*flex-wrap: wrap", css)