Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -15,6 +15,7 @@ import aiomysql
|
||||
import numpy as np
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import (
|
||||
AlignmentPolicy,
|
||||
build_curves,
|
||||
@@ -23,6 +24,7 @@ from B05_Profile.B05_Profile_Engine_Grade_Alignment import (
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
_annotate_pavement_suggestions,
|
||||
_load_pipe_points,
|
||||
_load_route_polyline,
|
||||
find_cross_file,
|
||||
@@ -421,6 +423,47 @@ def _follow_moved_stations(
|
||||
return {"id": profile["id"], **profile["summary"]}
|
||||
|
||||
|
||||
#: 구조물 측점을 새로 떠 갈음할 때 같은 자리 옛 줄에서 들고 가는 칸(472 덧) — 상단측 출처
|
||||
#: (`user` = 손 지정 · 값도 옛 줄) · 포장 제안 표기(`_annotate_pavement_suggestions` · 새 줄 없음).
|
||||
STATION_CARRY_KEYS = (
|
||||
"uphill_side_source",
|
||||
"pavement_suggested",
|
||||
"pavement_grade_pct",
|
||||
"pavement_grade_limit_pct",
|
||||
)
|
||||
|
||||
|
||||
def _carry_station_fields(
|
||||
old: dict[float, dict[str, Any]], fresh: dict[float, dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""같은 자리 옛 줄의 `STATION_CARRY_KEYS` 를 새 줄에 얹음 · 사용자 상단측은 새 자동값을 이김.
|
||||
반환 = 옛 줄 없는 새 자리(포장 표기를 지금 계획선으로 셀 줄)."""
|
||||
born = []
|
||||
for key, station in fresh.items():
|
||||
before = old.get(key)
|
||||
if before is None:
|
||||
born.append(station)
|
||||
continue
|
||||
for name in STATION_CARRY_KEYS:
|
||||
if name in before and name not in station:
|
||||
station[name] = before[name]
|
||||
if before.get("uphill_side_source") == "user" and before.get("uphill_side"):
|
||||
station["uphill_side"] = before["uphill_side"]
|
||||
return born
|
||||
|
||||
|
||||
def _pavement_for(data: dict[str, Any], stations: list[dict[str, Any]]) -> None:
|
||||
"""새 자리 측점의 포장 제안 표기 — 종단 생성과 같은 셈 · 기준 = 계획선 `criteria`."""
|
||||
profiles = data.get("design_profiles") or []
|
||||
criteria = (profiles[0].get("criteria") if profiles else None) or {}
|
||||
names = GradeDesignOptions.__dataclass_fields__.keys() - {"warnings"}
|
||||
try:
|
||||
options = GradeDesignOptions(**{k: v for k, v in criteria.items() if k in names})
|
||||
except TypeError:
|
||||
return # 옛 종단 — 기준 칸이 모자람
|
||||
_annotate_pavement_suggestions({"design_profiles": profiles, "stations": stations}, options)
|
||||
|
||||
|
||||
def _merge_irregular_into_longitudinal(
|
||||
project_root: Path, longitudinal_file_path: str, irregular_stations: list[dict[str, Any]]
|
||||
) -> dict[str, Any] | None:
|
||||
@@ -451,6 +494,9 @@ def _merge_irregular_into_longitudinal(
|
||||
if station.get("kind") in ("irregular", "curve")
|
||||
]
|
||||
fresh = {round(float(s.get("chainage_m", 0.0)), 6): s for s in irregular_stations}
|
||||
born = _carry_station_fields(
|
||||
{round(float(s.get("chainage_m", 0.0)), 6): s for s in stations}, fresh
|
||||
)
|
||||
kept: dict[float, dict[str, Any]] = {}
|
||||
left: list[dict[str, Any]] = []
|
||||
for station in stations:
|
||||
@@ -470,6 +516,7 @@ def _merge_irregular_into_longitudinal(
|
||||
data["stations"] = sorted(kept.values(), key=lambda s: float(s.get("chainage_m", 0.0)))
|
||||
previous = [round(float(station.get("chainage_m", 0.0)), 6) for station in stations]
|
||||
summary = _follow_moved_stations(data, before, previous)
|
||||
_pavement_for(data, born) # 계획선을 옮긴 뒤 — 새 자리 경사는 지금 계획선으로
|
||||
atomic_write_json(path, data)
|
||||
_relabel_cross_files(path.parent.parent / "cross_sections", left)
|
||||
return summary
|
||||
|
||||
@@ -23,6 +23,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
|
||||
| 두 보기가 보는 파생값은 페이지 한 벌이 숨은 동안도 셈 · 보기는 그리기만 · 접힘 = 그리기만 건너뜀 | `design_facility_lane.ts:1-12` · `B05_Profile_UI_Profile_Render.ts:211-226` | `test_272_hidden_summary` · `test_271_facility_lane` |
|
||||
| 되돌리기 걸음 = 사용자 조작으로 값이 실제로 바뀐 때만 · 빈 칸(「""」「{}」「null」) = 칸 없음 · 빈 맵 persist = 칸 지움 | `B05_Profile_UI_Profile_History.ts:50 · 235-236` · `B06_Section_UI_Page_Link_Session.ts:97-105 · 153` | `test_272_history_empty_cells` |
|
||||
| 짝 없는 새 측점(구조물 · 추가 측점 · 새 관) = 계획선 꼭짓점 안 세움 · 짝 있는 옮긴 측점 = 꼭짓점 따라감 | `common_util_structure_station.ts:356` · `B05_Profile_Router_Confirm.py:306-400` | `test_427_1_follow_seat` · `test_459_route_seat_mirror` |
|
||||
| 구조물 측점 다시 뜸([저장] 상단측 차례 · 측점 맞춤) = 같은 자리 옛 줄의 상단측 출처(`user` 면 값도) · 포장 제안 표기 들고 감 · 새 자리 포장 표기 = 지금 계획선 | `B05_Profile_Router_Confirm.py:426-465` | `test_472_station_user_fields` |
|
||||
| [저장] · [확정] = 서버가 입력만으로 정본 다시 셈 · 브라우저 결과(면적 · 표본 · 측점) 받아 적지 않음 | `design_save.ts:58-68` · `B06_Section_Router_Confirm.py:203-215` | `test_b06_save_recompute` |
|
||||
| 자동저장(디바운스 PUT) 없음 · 화면 잠금 = [저장] · [확정] · [초기화] · 추천 [적용]만 | `design_save.ts:249` | — |
|
||||
| 측점 열쇠 = `common_util_station_key` 한 벌 · 노선 바뀜 = 관 · 구조물 좌표(x · y) 따라감 · 누가거리 다시 매김 | `common_util_station_key.ts:32` · `common_util_route_follow.ts:291` | `test_344_3_station_key_mirror` · `test_410_follow_xy` |
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""PLAN 472 덧 (요구 272) — 구조물 측점을 다시 떠도 사용자 상단측 · 포장 제안 표기가 남음.
|
||||
|
||||
증상 — [저장] 상단측 차례(`POST /route/confirm` → `_append_irregular_cross_sections`) · 측점 맞춤
|
||||
(`ensure_structure_stations`)이 `_merge_irregular_into_longitudinal` 로 구조물 측점 줄을 새 줄로
|
||||
통째 갈음 → 같은 자리 옛 줄의 `uphill_side_source: user`(상단측 손 지정 · 값은 자동으로 돌아감) ·
|
||||
`pavement_*`(포장 제안 표기)가 사라짐(검증_sub8 측점 351).
|
||||
고침 — 같은 자리(누가거리) 옛 줄에서 `STATION_CARRY_KEYS` 를 들고 감 · 사용자 상단측은 새 자동값을
|
||||
이김 · 옛 줄 없는 새 자리는 포장 표기를 지금 계획선으로 셈.
|
||||
X6 — 옮긴 측점이 들고 가는 횡단 입력 TS `ROW_CARRY_KEYS` = 파이썬 `CARRY_INPUT_KEYS`(거울).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_station_user_fields.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile.B05_Profile_Router_Confirm import ( # noqa: E402
|
||||
_merge_irregular_into_longitudinal,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Design import CARRY_INPUT_KEYS # noqa: E402
|
||||
|
||||
RELATIVE = "B06_Section/longitudinal/longitudinal.json"
|
||||
CRITERIA = {
|
||||
"max_grade_pct": 14.0,
|
||||
"max_reverse_grade_pct": 5.0,
|
||||
"min_vertical_radius_m": 100.0,
|
||||
"min_curve_length_m": 20.0,
|
||||
"min_tangent_length_m": 20.0,
|
||||
"vertical_curve_skip_delta_pct": 5.0,
|
||||
"design_speed_kph": 20,
|
||||
"terrain_type": "special",
|
||||
"paved": False,
|
||||
"main_direction": "auto",
|
||||
"balance_segment_length_m": None,
|
||||
"start_elevation_offset_m": 0.0,
|
||||
"end_elevation_offset_m": 0.0,
|
||||
"enforce_pipe_clearance": False,
|
||||
"resolved_main_direction": "ascending",
|
||||
}
|
||||
|
||||
|
||||
def _row(at: float, kind: str = "regular", **extra) -> dict:
|
||||
return {"chainage_m": at, "kind": kind, "station_id": f"s{at}", "uphill_side": "left", **extra}
|
||||
|
||||
|
||||
def _pave(grade: float) -> dict:
|
||||
return {
|
||||
"pavement_suggested": grade > 14.0,
|
||||
"pavement_grade_pct": grade,
|
||||
"pavement_grade_limit_pct": 14.0,
|
||||
}
|
||||
|
||||
|
||||
def _write(tmp_path: Path) -> Path:
|
||||
# 계획선 0 → 100 m 고르게 20 % 오르막(포장 상한 14 % 넘음)
|
||||
samples = [{"chainage_m": float(s), "elevation_m": 0.2 * s} for s in range(0, 101, 5)]
|
||||
stations = [
|
||||
_row(0.0, "bp", uphill_side_source="user", **_pave(20.0)),
|
||||
_row(20.0, uphill_side_source="user", **_pave(20.0)),
|
||||
_row(
|
||||
35.0,
|
||||
"irregular",
|
||||
structure="관 D1000",
|
||||
uphill_side="right",
|
||||
uphill_side_source="user",
|
||||
**_pave(20.0),
|
||||
), # fmt: skip
|
||||
_row(40.0, uphill_side="right", uphill_side_source="user", **_pave(20.0)),
|
||||
_row(100.0, "ep", uphill_side_source="user", **_pave(20.0)),
|
||||
]
|
||||
path = tmp_path / RELATIVE
|
||||
path.parent.mkdir(parents=True)
|
||||
longitudinal = {
|
||||
"stations": stations,
|
||||
"samples": samples,
|
||||
"design_profiles": [{"criteria": CRITERIA, "samples": samples}],
|
||||
}
|
||||
path.write_text(json.dumps(longitudinal), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _fresh(at: float, **extra) -> dict:
|
||||
"""새로 뜬 구조물 측점 — 상단측은 지형 자동값 · 포장 표기 · 출처 칸 없음."""
|
||||
return {"chainage_m": at, "kind": "irregular", "station_id": f"n{at}", "uphill_side": "left",
|
||||
"structure": "관 D1000", **extra} # fmt: skip
|
||||
|
||||
|
||||
def _merged(tmp_path: Path, fresh: list[dict]) -> dict[float, dict]:
|
||||
path = _write(tmp_path)
|
||||
_merge_irregular_into_longitudinal(tmp_path, RELATIVE, fresh)
|
||||
rows = json.loads(path.read_text(encoding="utf-8"))["stations"]
|
||||
return {row["chainage_m"]: row for row in rows}
|
||||
|
||||
|
||||
def test_같은_자리_다시_떠도_사용자_상단측_포장_표기_남음(tmp_path) -> None:
|
||||
rows = _merged(tmp_path, [_fresh(35.0)])
|
||||
row = rows[35.0]
|
||||
assert row["station_id"] == "n35.0" # 기하 · 글은 새 줄
|
||||
assert row["uphill_side"] == "right" and row["uphill_side_source"] == "user"
|
||||
assert row["pavement_suggested"] is True and row["pavement_grade_pct"] == 20.0
|
||||
|
||||
|
||||
def test_격자_자리에_구조물이_얹혀도_사용자_상단측_남음(tmp_path) -> None:
|
||||
rows = _merged(tmp_path, [_fresh(35.0), _fresh(40.0, grid=True)])
|
||||
assert rows[40.0]["uphill_side"] == "right" and rows[40.0]["uphill_side_source"] == "user"
|
||||
assert rows[40.0]["pavement_grade_limit_pct"] == 14.0
|
||||
|
||||
|
||||
def test_새_자리는_포장_표기를_지금_계획선으로(tmp_path) -> None:
|
||||
rows = _merged(tmp_path, [_fresh(55.0)])
|
||||
assert 35.0 not in rows # 옛 구조물 측점은 걷힘
|
||||
row = rows[55.0]
|
||||
assert row["uphill_side"] == "left" and "uphill_side_source" not in row # 새 자리 = 지형 자동
|
||||
assert row["pavement_suggested"] is True
|
||||
assert row["pavement_grade_pct"] == 20.0 and row["pavement_grade_limit_pct"] == 14.0
|
||||
|
||||
|
||||
def test_X6_옮긴_측점_횡단_입력_TS_파이썬_한_벌() -> None:
|
||||
source = (ROOT / "B06_Section/logic/B06_Section_Station_Swap.ts").read_text(encoding="utf-8")
|
||||
body = re.search(r"export const ROW_CARRY_KEYS = \[(.*?)\] as const;", source, re.S)
|
||||
assert body is not None
|
||||
assert tuple(re.findall(r'"([a-z_0-9]+)"', body.group(1))) == CARRY_INPUT_KEYS
|
||||
Reference in New Issue
Block a user