Files
Aislo/B05_Profile/B05_Profile_PageSettings.py
T
eomsangdonandClaude Opus 5.5 4f2c7b7a0b fix(448 B05 페이지 설정): 계획고 저장이 평면 최소 R 지우던 흠 · 시종점 칸 되돌리기 뒤 다시 비춤
- AlignmentPolicy.from_dict 가 min_plan_radius_m · hairpin_min_radius_m 을 읽음 — 계획고 저장마다 0 으로 굳던 것(브레인 지시)
- 「기준값 직접 지정」을 편 동안 누름 · 키 뒤마다 시 · 종점 계획고 조정 칸을 종단 편집으로 다시 비춤(되돌리기 · 끝점 ▲▼)
- 시험 test_442_2_page_settings 평면 R 유지 한 건

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:01:27 +09:00

126 lines
5.7 KiB
Python

"""B05 페이지 설정 저장(448) — stage 2 params 에 얹을 값 · 종단 기준 갈아 끼우기 · 옛 스냅샷 걷기.
설계속도 · 지형 · 기준값은 **경고 기준만** 바꾼다 — 기하(base_pvi · 편집)는 그대로 · 종횡단 다시
만들기 없음. params 에는 사용자가 넣은 원값만 둔다(해석값 저장 금지 — `resolve_grade_options`).
시작 측점은 B05 표시 전용 — 저장만 하고 측점 열쇠 · 다른 페이지 라벨에 쓰지 않는다.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from B05_Profile.B05_Profile_Engine_Grade import (
GradeDesignOptions,
legal_plan_radius_min_m,
resolve_grade_options,
)
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
from common_util.common_util_json import atomic_write_json
#: params["options"] 안의 페이지 설정 칸
PAGE_OPTION_KEYS = ("design_speed_kph", "terrain_type")
#: params 맨 위의 페이지 설정 칸(빈 값 = 법정값)
PAGE_TOP_KEYS = (
"max_grade_pct",
"min_vertical_radius_m",
"min_tangent_length_m",
"enforce_pipe_clearance",
"station_display",
)
_STORED_KEYS = ("max_grade_pct", "min_vertical_radius_m", "min_tangent_length_m")
def params_patch(request: Any) -> dict[str, Any]:
"""요청 → stage 2 params 에 얹을 값(원값 그대로)."""
return {
"options": {key: getattr(request, key) for key in PAGE_OPTION_KEYS},
**{key: getattr(request, key) for key in PAGE_TOP_KEYS},
}
def strip_page_settings(params: dict[str, Any] | None) -> dict[str, Any] | None:
"""페이지 설정 칸만 걷어 기본값으로 — stage 2 params 를 안 뜬 옛 스냅샷의 [초기화]."""
if not isinstance(params, dict):
return params
stripped = {key: value for key, value in params.items() if key not in PAGE_TOP_KEYS}
options = params.get("options")
if isinstance(options, dict):
stripped["options"] = {k: v for k, v in options.items() if k not in PAGE_OPTION_KEYS}
return stripped
def grade_class_of(road_type: str | None, params: dict[str, Any]) -> str:
"""기준 등급 — 노선 풀기가 남긴 값 · 없으면 B02 임도 종류(main → trunk · 그 밖 = work)."""
stored = (params.get("options") or {}).get("grade_class")
if stored:
return str(stored)
if road_type in ("main", "trunk"):
return "trunk"
return "fire" if road_type == "fire" else "work"
def grade_options_of(road_type: str | None, params: dict[str, Any]) -> GradeDesignOptions:
"""params(사용자 원값) → 해석된 기준(작업임도 30 → 20 · 빈 칸 = 법정값)."""
options = params.get("options") or {}
speed = options.get("design_speed_kph")
return resolve_grade_options(
grade_class_of(road_type, params),
terrain_type=str(options.get("terrain_type") or "special"),
paved=bool(options.get("paved", False)),
main_direction=str(options.get("main_direction") or "auto"),
design_speed_kph=int(speed) if speed is not None else None,
stored={key: params[key] for key in _STORED_KEYS if params.get(key) is not None}
| {"enforce_pipe_clearance": params.get("enforce_pipe_clearance")},
)
def retarget_longitudinal(
longitudinal: dict[str, Any], options: GradeDesignOptions
) -> tuple[dict[str, Any], dict[str, Any]]:
"""종단 정본의 정책 · 기준을 새 기준으로 — 저장 편집으로 다시 얹어 위반 · 요약만 새로.
정책 칸 = 최대 종단기울기 · 종단곡선 최소 R · 평면 최소 R(+ 있던 최소 L). 기하 칸은 그대로.
반환 = (선형, 요약 `{id, ...}`) · `longitudinal` 은 제자리에서 바뀐다.
"""
stored = longitudinal.get("profile_alignment")
if not isinstance(stored, dict) or not stored.get("base_pvi"):
raise ValueError("저장된 계획선 선형이 없어 기준을 바꿀 수 없습니다.")
policy = dict(stored.get("policy") or {})
policy["max_grade_pct"] = options.max_grade_pct
policy["min_vertical_radius_m"] = options.min_vertical_radius_m
policy["min_plan_radius_m"] = legal_plan_radius_min_m(
options.design_speed_kph, options.terrain_type
)
if policy.get("min_curve_length_m"):
policy["min_curve_length_m"] = options.min_curve_length_m
stored["policy"] = policy
profiles = longitudinal.get("design_profiles")
previous = profiles[0] if isinstance(profiles, list) and profiles else {}
direction = (previous.get("criteria") or {}).get("resolved_main_direction", "none")
previous["criteria"] = {**options.as_dict(), "resolved_main_direction": direction}
longitudinal["design_profiles"] = [previous, *(profiles or [])[1:]]
alignment, profile = rebuild_alignment_profile(longitudinal, stored.get("edits"))
longitudinal["profile_alignment"] = alignment
longitudinal["design_profiles"][0] = profile
return alignment, {"id": profile["id"], **profile["summary"]}
def retarget_file(
project_root: Path, longitudinal_file_path: str, options: GradeDesignOptions
) -> dict[str, Any] | None:
"""종단 정본 파일에 새 기준 — 요약을 돌려줌 · 계획선 선형이 없으면 None(파일 그대로)."""
root = Path(project_root).resolve()
path = (root / longitudinal_file_path).resolve()
if root not in path.parents:
raise ValueError("종단 데이터 경로가 프로젝트 밖을 가리킵니다.")
if not path.is_file():
return None
longitudinal = json.loads(path.read_text(encoding="utf-8"))
if not (longitudinal.get("profile_alignment") or {}).get("base_pvi"):
return None
_, summary = retarget_longitudinal(longitudinal, options)
atomic_write_json(path, longitudinal)
return summary