"""626-3 B05 평면 예정노선 그리기 → 다시 추적 → 편집기 한 걸음 → [저장](요구 530 · 531 · 결정 D3 · D4 · D9 · D10). - 셈(TS `logic/B05_Profile_ExpectedDraw.ts` · tsc 로 옮겨 Node) — 구간 다시 긋기 갈음(앞뒤 그대로 · 거꾸로 그음 뒤집음 · 시점 · 종점 자리) · 다음 점 방식 넷(D03 셈 그대로) · 선 위 자리 · 첫 구간 정방향 앞 점. - 지킴(글 대조) — 다시 추적 = `/route/follow` 만(저장 0) · `/route/replan` · `as_initial` · `expected_route.csv` 안 부름 · 결과 = 편집기 `restoreSnapshot(…, true)` 한 걸음 · [저장] = 손 편집 같은 길(`bindRouteApply`) + 초안 `expected` · 걸음 사진 · 노선 초안에 고친 예정노선 실림(되돌리기 · 세션 되살림) · 같은 선 판정에 예정노선 · 긋기 방식 칸 공용 · 판 켜면 캔버스 누름을 먼저 받음(재기 · 계획선 고르기 막음) · RouteEdit 700 줄 안. ./venv/Scripts/python.exe -m pytest resources/tester/test_626_3_expected_edit.py -q """ from __future__ import annotations import json import re import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] B05 = ROOT / "B05_Profile" TS_FILES = [ ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Draw.ts", ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Fan.ts", B05 / "logic" / "B05_Profile_ExpectedDraw.ts", ] TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" _RUNNER = """ import { writeFileSync } from "node:fs"; import * as d from "./B05_Profile/logic/B05_Profile_ExpectedDraw.js"; const line = [[0, 0], [10, 0], [20, 0], [30, 0], [40, 0]]; const plane = (x, y) => 0.65 * x; const out = {}; out.forward = d.respliceLine(line, 12, 28, [[15, 5], [25, 5]]); out.backward = d.respliceLine(line, 28, 12, [[25, 5], [15, 5]]); out.head = d.respliceLine(line, 0, 20, [[10, 3]]); out.tail = d.respliceLine(line, 30, 40, []); out.near = d.nearestOnLine(line, [17, 4]); out.leadStart = d.leadBefore(line, 0); out.lead = d.leadBefore(line, 12); out.free = d.pickNext("free", [0, 0], [3, 4], 20, 9, plane, null); out.length = d.pickNext("length", [0, 0], [3, 4], 20, 9, plane, null); const both = d.pickNext("both", [0, 0], [0, 50], 20, 9, plane, [0, -20]); out.both = { point: both.point, nodes: both.fan.nodes.length, arcs: both.fan.arcs.length, lengthOnly: both.lengthOnly }; const between = d.pickNext("both", [0, 0], [-3, 50], 20, 9, plane, [0, -20], true); out.between = { point: between.point, lengthOnly: between.lengthOnly }; out.none = d.pickNext("both", [0, 0], [0, 50], 20, 9, () => NaN, null); out.slopeNone = d.pickNext("slope", [0, 0], [0, 50], 20, 9, () => 0, 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("expected_draw") 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 _src(path: Path) -> str: return path.read_text(encoding="utf-8") def test_구간_다시_긋기_앞뒤_그대로_가운데만_갈음(r): assert r["forward"] == [[0, 0], [10, 0], [12, 0], [15, 5], [25, 5], [28, 0], [30, 0], [40, 0]] assert r["backward"] == r["forward"] # 거꾸로 그어도 같은 선 assert r["head"] == [[0, 0], [10, 3], [20, 0], [30, 0], [40, 0]] # 시점부터 · 같은 점 하나 assert r["tail"] == [[0, 0], [10, 0], [20, 0], [30, 0], [40, 0]] # 속 점 없음 = 곧은 갈음 def test_선_위_자리_첫_구간_앞_점(r): assert r["near"] == {"atM": 17, "point": [17, 0]} assert r["leadStart"] is None and r["lead"] == [7, 0] def test_다음_점_방식_넷_D03_셈_그대로(r): assert r["free"]["point"] == [3, 4] and r["free"]["fan"] is None assert [round(v, 6) for v in r["length"]["point"]] == [12, 16] both = r["both"] assert both["nodes"] == 4 and both["arcs"] == 2 and not both["lengthOnly"] assert abs((both["point"][0] ** 2 + both["point"][1] ** 2) ** 0.5 - 20) < 1e-6 between = r["between"] assert between["lengthOnly"] and abs(sum(v * v for v in between["point"]) ** 0.5 - 20) < 1e-6 assert r["none"]["point"] is None and "자리 없음" in r["none"]["note"] assert r["slopeNone"]["point"] is None and "자리 없음" in r["slopeNone"]["note"] def test_다시_추적_follow_만_저장_0_초기값_추천_안_건드림(): ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts") assert "/route/follow" in ui and "askConfirm(" in ui and "flattenServerPlan(" in ui for banned in ( "/route/replan", "as_initial", "expected_route.csv", "/route/initial", "replanRoute", ): assert banned not in ui, banned # B05 화면 어디서도 as_initial: true 를 안 보냄(D8) for path in B05.glob("B05_Profile_UI_*.ts"): assert not re.search(r"as_initial:\s*true", _src(path)), path.name def test_결과는_편집기_한_걸음_저장은_손_편집_같은_길(): main = _src(B05 / "B05_Profile_UI_RouteEdit.ts") assert "createExpectedDraw({" in main and "restoreSnapshot(" in main assert re.search(r"onFollow: \(flat, line\) =>\s+restoreSnapshot\(", main) assert ( '"예정노선을 따라 평면을 다시 추적했습니다.",\n true,' in main ) # 새 걸음(되돌리기 됨) assert "expected: expectedDraw.edit()," in main # 걸음 사진 칸 assert "expectedDraw.setEdit(snapshot.expected ?? null);" in main # 되돌리기 = 예정노선도 assert "expected: () => expectedDraw.edit()," in main # [저장] 초안 칸 assert "plan.expected_edited ?? []" in main # 다시 열면 고친 예정노선 assert len(main.splitlines()) <= 700 apply = _src(B05 / "B05_Profile_UI_RouteEdit_Apply.ts") assert "...(expected ? { expected } : {})" in apply # null = 안 실음(그대로) · [] = 지움 def test_걸음_사진_초안_같은_선에_예정노선(): history = _src(B05 / "B05_Profile_UI_RouteEdit_History.ts") assert "expected?: Vertex[] | null;" in history and "!sameExpected(a, b)" in history draft = _src(B05 / "B05_Profile_UI_RouteEdit_Draft.ts") assert "...(snapshot.expected" in draft and "expected: draft.expected ?? null," in draft types = _src(ROOT / "A00_Common" / "design" / "logic" / "design_pipeline_types.ts") assert "expected?: Vertex[] | null;" in types server = _src(B05 / "B05_Profile_Route_Save.py") assert "expected: list[tuple[float, float]] | None = None" in server def test_방식_칸_공용_판_켜면_캔버스_누름_먼저_받음(): ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts") assert 'from "../A00_Common/map/map_draw_kind"' in ui and "createDrawKindFields(" in ui assert "paintPreview(" in ui and "pickNext(" in ui # D03 그림 · 셈 그대로 assert '"pointerdown",' in ui and "event.stopImmediatePropagation()" in ui assert re.search( r"\n true,\n \);", ui ) # 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저 assert 'keyIs(event, "plan.drawKind")' in ui and 'keyIs(event, "plan.drawFinish")' in ui assert "createFloatPanel(" in ui and 'createElement("button")' not in ui tools = _src(ROOT / "A00_Common" / "map" / "map_toolbar.ts") assert re.search(r'key: "expected-draw",\s+does: "[^"]+",\s+pages: B05', tools) render = _src(B05 / "B05_Profile_UI_RouteEdit_Render.ts") # 맨 위 = 구조물 마커 뒤 · 회전 끝 앞 assert render.index("scene.overAll?.(context);") < render.index("context.restore(); // 회전 끝")