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>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"""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]
|
||||
Reference in New Issue
Block a user