Files
Aislo/resources/tester/test_562_1_plan_draw.py
T
eomsangdonandClaude Opus 5.5 ae24e29d12 feat(562-1): D03 수동 그리기 셈 짝 — 길이 · 원지반 기울기 · 길이 고정 · 기울기 고정 · 양각기 · 선 위 거리 ↔ 점 · 측점
- logic/D03_PlanAnalysis_Draw.ts ↔ D03_PlanAnalysis_Draw.py 같은 이름 짝 · 높이는 인자 heightAt
- 거울 시험 test_562_1_plan_draw.py — 두 쪽 값 대조 + 손셈 값

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:58:56 +09:00

204 lines
7.5 KiB
Python

"""D03 수동 그리기 셈 거울(PLAN 562-1) — 파이썬 `D03_PlanAnalysis_Draw` ↔
TS `logic/D03_PlanAnalysis_Draw.ts`.
같은 높이 함수(`hill` · x > 800 = 지형 밖)로 두 쪽 값이 같은지 + 손셈 값.
./venv/Scripts/python.exe -m pytest resources/tester/test_562_1_plan_draw.py -q
"""
from __future__ import annotations
import json
import math
import re
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from D03_PlanAnalysis import D03_PlanAnalysis_Draw as draw # noqa: E402
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Draw.ts"
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
def hill(x: float, y: float) -> float:
if x > 800:
return math.nan
return 0.0005 * x * x + 3 * math.sin(y / 40)
def plane(x: float, y: float) -> float:
return 0.1 * x
LINE = [[0.0, 0.0], [30.0, 40.0], [30.0, 100.0], [90.0, 180.0]] # 50 + 60 + 100 = 210 m
# (함수 이름, 인자) — 높이 자리는 "hill" · "plane" 글자로 넘김
CASES = [
("segmentLength", [[1, 2], [4, 6]]),
("groundSlopePct", [[0, 0], [100, 30], "hill"]),
("groundSlopePct", [[100, 30], [0, 0], "hill"]),
("groundSlopePct", [[700, 0], [900, 0], "hill"]),
("groundSlopePct", [[5, 5], [5, 5], "hill"]),
("fixedLengthPoint", [[10, 10], [13, 14], 20]),
("fixedLengthPoint", [[10, 10], [10, 10], 20]),
("fixedSlopePoint", [[0, 0], [50, 0], 9, "hill"]),
("fixedSlopePoint", [[0, 0], [50, 0], -9, "hill"]),
("fixedSlopePoint", [[100, 30], [130, 70], 12, "hill"]),
("fixedSlopePoint", [[0, 0], [50, 0], 9, "hill", 100]),
("fixedSlopePoint", [[600, 0], [700, 0], 1, "hill"]),
("compassCandidates", [[0, 0], 50, 5, "plane"]),
("compassCandidates", [[200, 10], 60, 15, "hill"]),
("compassCandidates", [[0, 0], 50, 50, "plane"]),
("compassPoint", [[0, 0], [10, 40], 50, 5, "plane"]),
("compassPoint", [[0, 0], [10, 40], 50, 50, "plane"]),
("lineLength", [LINE]),
("pointAtDistance", [LINE, -5]),
("pointAtDistance", [LINE, 75]),
("pointAtDistance", [LINE, 500]),
("distanceAlong", [LINE, [40, 70]]),
("distanceAlong", [LINE, [-10, -10]]),
("sliceLine", [LINE, 25, 160]),
("sliceLine", [LINE, 160, 25]),
("stationChainages", [210]),
("stationChainages", [40, 20]),
("stationChainages", [0]),
]
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import * as draw from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Draw.js";
const heights = {
hill: (x, y) => (x > 800 ? NaN : 0.0005 * x * x + 3 * Math.sin(y / 40)),
plane: (x, y) => 0.1 * x,
};
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
const out = cases.map(([name, args]) =>
draw[name](...args.map((a) => (typeof a === "string" ? heights[a] : a))),
);
writeFileSync(process.argv[3], JSON.stringify(out));
"""
_HEIGHTS = {"hill": hill, "plane": plane}
_PY = {
"segmentLength": draw.segment_length,
"groundSlopePct": draw.ground_slope_pct,
"fixedLengthPoint": draw.fixed_length_point,
"fixedSlopePoint": draw.fixed_slope_point,
"compassCandidates": draw.compass_candidates,
"compassPoint": draw.compass_point,
"lineLength": draw.line_length,
"pointAtDistance": draw.point_at_distance,
"distanceAlong": draw.distance_along,
"sliceLine": draw.slice_line,
"stationChainages": draw.station_chainages,
}
def _py(name: str, args: list) -> object:
return _PY[name](*(_HEIGHTS[a] if isinstance(a, str) else a for a in args))
def _close(a: object, b: object) -> bool:
if a is None or b is None:
return a is b
if isinstance(a, (int, float)):
return isinstance(b, (int, float)) and abs(a - b) < 1e-6
return len(a) == len(b) and all(_close(x, y) for x, y in zip(a, b)) # type: ignore[arg-type]
@pytest.fixture(scope="module")
def ts_out(tmp_path_factory: pytest.TempPathFactory) -> list:
tmp = tmp_path_factory.mktemp("draw")
out = tmp / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(TS_FILE),
"--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")
job = tmp / "job.json"
result = tmp / "out.json"
job.write_text(json.dumps(CASES), encoding="utf-8")
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(job), str(result)],
cwd=str(ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
def test_ts_matches_python(ts_out: list) -> None:
for (name, args), ts in zip(CASES, ts_out):
assert _close(ts, _py(name, args)), (name, args, ts, _py(name, args))
def test_hand_values() -> None:
assert draw.segment_length((1, 2), (4, 6)) == 5
# 오르막 + · 내리막 − · 지형 밖 · 길이 0
up = draw.ground_slope_pct((0, 0), (100, 0), hill)
assert up == pytest.approx(5.0)
assert draw.ground_slope_pct((100, 0), (0, 0), hill) == pytest.approx(-5.0)
assert draw.ground_slope_pct((700, 0), (900, 0), hill) is None
assert draw.ground_slope_pct((5, 5), (5, 5), hill) is None
assert draw.fixed_length_point((10, 10), (13, 14), 20) == pytest.approx((22, 26))
# z = 0.0005 x² · x0 = 0 → 평균 기울기 0.0005 t = 9 % 첫 자리 t = 180
# 부호 무관 · 한도 100 m = 없음
assert draw.fixed_slope_point((0, 0), (50, 0), 9, hill) == pytest.approx((180, 0), abs=1e-6)
assert draw.fixed_slope_point((0, 0), (50, 0), -9, hill) == pytest.approx((180, 0), abs=1e-6)
assert draw.fixed_slope_point((0, 0), (50, 0), 9, hill, 100) is None
# 지형 밖에 닿음
assert draw.fixed_slope_point((600, 0), (700, 0), 1, hill) is None
# 평면 10 % · 반지름 50 · 5 % → cos θ = ±0.5 · 60 · 120 · 240 · 300°
got = draw.compass_candidates((0, 0), 50, 5, plane)
angles = sorted(round(math.degrees(math.atan2(y, x)) % 360, 6) for x, y in got)
assert angles == [60, 120, 240, 300]
assert draw.compass_candidates((0, 0), 50, 50, plane) == []
near = draw.compass_point((0, 0), (10, 40), 50, 5, plane)
assert near == pytest.approx((25, 25 * math.sqrt(3)), abs=1e-6)
assert draw.compass_point((0, 0), (10, 40), 50, 50, plane) is None
def test_line_values() -> None:
assert draw.line_length(LINE) == 210
assert draw.point_at_distance(LINE, -5) == (0, 0)
assert draw.point_at_distance(LINE, 75) == (30, 65)
assert draw.point_at_distance(LINE, 500) == (90, 180)
assert draw.distance_along(LINE, (40, 70)) == 80
assert draw.distance_along(LINE, (-10, -10)) == 0
assert draw.slice_line(LINE, 25, 160) == pytest.approx(
[(15, 20), (30, 40), (30, 100), (60, 140)]
)
assert draw.slice_line(LINE, 160, 25) == draw.slice_line(LINE, 25, 160)
assert draw.station_chainages(210) == [0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 210]
assert draw.station_chainages(40, 20) == [0, 20, 40]
assert draw.station_chainages(0) == [0]