Files
Aislo/resources/tester/test_625_draw.py
T
eomsangdonandClaude Opus 5.5 85ab474320 feat(D03 긋기): 길이·기울기 고정 부채꼴 · 정방향 채움 · +/− 노드 색 · Shift 사이 기울기 · 완만한 땅 흠 (PLAN 625-2 · 요구 530 · 531)
- 새 셈 logic/D03_PlanAnalysis_Fan.ts(화면 고르기 전용 · 파이썬 짝 없음 · B05 626-3 이 그대로 씀) — 이웃 노드 사이 허용 호 · 정방향 = 앞 구간 쪽(첫 구간 없음 · 갈래 = 붙은 선 · 거의 같으면 마우스 쪽) · 완만한 땅 = 어느 방향이나 · Shift = 호 위 마우스 방위(길이 그대로 · 기울기 실제 지형 · 호 밖 = 호 끝)
- 미리보기 paintFan — 정방향 반투명 채움 · 역방향 테두리 · 노드 + 따뜻한 색 · − 찬 색 · 고리 = 고른 점 부호 색
- 놓인 꺾임점 = 들어온 구간 기울기 부호 색(저장 없이 셈)
- Shift · 완만한 땅으로 찍은 노드 기록 = 길이 고정(저장 꼴 그대로)
- 색 토큰 --draw-map-slope-up · --draw-map-slope-down · test_551_2 토큰 8 → 10
- 시험 test_625_draw(부채꼴 · 정방향 · +/− · 노드 2/4/6/8 · 완만한 땅 · Shift 실제 지형)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:30:53 +09:00

162 lines
7.1 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"]