- 자리 칸 목록 한 벌 = TS `SEAT_KEYS`(Station_Swap · 파이썬 Router_Design 이 그 글을 읽음) — two_stage_slope · rock_boundary_offset_m · cut_slope_ratio_user · section_mode · ditch_side - 자리 판정 한 벌 = `sameSeat` ↔ 파이썬 `same_seat`(중심 0.01 m · 왼쪽 축 1e-3 · 모르면 같은 자리 = 옛 동작) - 끌기 세션 넘김 · Node stations.carry = `carryKeyedStates`(목록 측점 자리 · 값마다 쓴 때 자리 `@seat` — 되돌리기로 되살린 값도 제 자리로) - 미리보기 = `pipelineCrossSource` 가 저장분 · 세션 자리 칸에 자리째 실음 → `crossAtSeat` 가 새 자리면 뺌 - B06 갈아끼움(노선 초안) = 자리 바뀐 측점 설계의 자리 칸 걷고 기본값(`unseated` · 브레인 덧) - 서버 노선 저장 = `carry_cross_inputs` 자리 칸 같은 자리만 · 손 단면 유형 · 측구 방향 = `compute_default_designs` 입력 - 옛 단언 시험 고침(자리 모름 목록 = 같은 자리 = 손 상단측도 넘김 · 「@seat」 칸 거름) · 새 시험 test_475_3_seat_carry - 위키 rules 넘김 규칙 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
621 lines
29 KiB
Python
621 lines
29 KiB
Python
"""B06 라우터의 횡단 설계 파일 읽기와 프리뷰 계산.
|
|
|
|
포장 · 세월교 구간 읽기 · 판정(짝 349-0n) = `B06_Section_Router_Design_Ranges.py` · 다시 내보냄.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from B05_Profile.B05_Profile_Engine_Sections import find_cross_file
|
|
from B06_Section.B06_Section_Engine_Design import (
|
|
ROUTE_CARRY_KEYS,
|
|
compute_cross_design,
|
|
curve_widening_args,
|
|
stored_shoulder_extend,
|
|
stored_tilt,
|
|
)
|
|
from B06_Section.B06_Section_Engine_Design_Node import (
|
|
DesignJob,
|
|
berm_specs_at,
|
|
compute_cross_designs,
|
|
)
|
|
from B06_Section.B06_Section_Engine_Tilt import tilt_args, tilt_ranges
|
|
|
|
# 포장 · 세월교 구간(나눈 파일) — 이 모듈 이름으로 다시 내보냄(부르는 곳 · 시험 갈아 끼움 그대로).
|
|
from B06_Section.B06_Section_Router_Design_Ranges import ( # noqa: F401
|
|
ford_drop_at,
|
|
ford_pavement_ranges,
|
|
ford_surface_drops,
|
|
paved_at,
|
|
pavement_ranges,
|
|
pavement_suggestions,
|
|
)
|
|
from common_util.common_util_cross_berm import (
|
|
BERM_DEFAULT_DEPTH_M,
|
|
BERM_DEFAULT_INTERVAL_M,
|
|
BERM_DEFAULT_SLOPE_DEG,
|
|
BERM_DEFAULT_WIDTH_M,
|
|
BermSpec,
|
|
)
|
|
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
|
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
|
|
|
PREVIEW_DESIGN_FIELDS = (
|
|
"ground_type",
|
|
"roadbed_width_m",
|
|
"cut_area_m2",
|
|
"cut_soil_area_m2",
|
|
"cut_rock_area_m2",
|
|
"cut_rock_kind",
|
|
"fill_area_m2",
|
|
"fill_ground_slope",
|
|
"design_elevation_m",
|
|
"slope_unclosed",
|
|
)
|
|
|
|
|
|
# 사용자 조작값 — 다시 계산해도 그대로 승계한다. **이 목록이 서버의 유일한 한 벌이다.**
|
|
#
|
|
# 브라우저 쪽 짝은 `B06_Section_Cross_Refresh.ts` 의 `PRESERVED_KEYS` 다(+ `status`·
|
|
# `pavement_suggested`, 이 둘은 부르는 쪽이 따로 붙인다). 새 사용자 조작값을 만들면
|
|
# **두 곳 모두**에 넣을 것 — 빠뜨리면 재계산이 조용히 지운다.
|
|
#
|
|
# ⚠ `extra_spans`(다단 구간값)가 실제로 그렇게 빠져 있었다(2026-09-07 발견). 브라우저는
|
|
# 살렸는데 서버 두 경로(포장 강제·세월교 노면 하강)와 단측점 갱신은 안 살려, 그 측점이
|
|
# 다시 계산되면 사용자가 넣은 단별 구간값이 사라졌다.
|
|
USER_TOUCHED_KEYS = (
|
|
"display_half_width_m",
|
|
"inlet_structure",
|
|
"basin_adjust",
|
|
"revet_adjust",
|
|
"ford_adjust",
|
|
"box_adjust",
|
|
"extra_wall_counts",
|
|
"extra_spans",
|
|
"revet_link_detached",
|
|
"revet_follow_grade",
|
|
# 소단 제원 — 사용자가 구간에 놓은 값이라 다시 계산해도 살려 둔다(계획서 3-9).
|
|
"berm",
|
|
# 측점별 암 절토 경사 — 표준 횡단면 설정을 바꿔도 개별로 고친 측점은 그대로 둔다
|
|
# (2026-09-07 사용자 확정: 「사용자가 기본값을 사용하지 않는 값들은 변경되면 안됨」).
|
|
"cut_slope_ratio_user",
|
|
# 측점 구조물 글(138-4 · 횡단 [상세] 모달 맨 위 칸) — 계산 입력이 아님.
|
|
"structure_note",
|
|
# 표 위 CAD 칸(260-1 · [상세] 모달 빈 도면) — 계산 입력이 아님.
|
|
"structure_note_cad",
|
|
# [상세] 표 손질(179-4) — 고른 칸 · 더한 행 · 열(값은 그릴 때 서버가 다시 셈)
|
|
"detail_table",
|
|
# 설계선 쉬프트 = 쪽마다 노견 확장(139-6) — 기하 입력(`stored_shoulder_extend`)
|
|
"shoulder_extend_left_m",
|
|
"shoulder_extend_right_m",
|
|
# 소단 쪽마다 옮긴 깊이(194-1 · 횡단도 ▲▼) — 기하 입력(`stored_berm`)
|
|
"berm_depth_left_m",
|
|
"berm_depth_right_m",
|
|
)
|
|
|
|
# 구조물(관 · 박스 · 세월교) 조정값 — 측점의 관 종류를 바꾸면 버리는 몫(318-4 · `structure_reset`).
|
|
# 브라우저 짝 = `B06_Section_Api_Culvert_Options.ts` 의 `STRUCTURE_ADJUST_KEYS`
|
|
# (거울 시험 test_318_kind_reset).
|
|
STRUCTURE_ADJUST_KEYS = (
|
|
"inlet_structure",
|
|
"basin_adjust",
|
|
"revet_adjust",
|
|
"ford_adjust",
|
|
"box_adjust",
|
|
"extra_wall_counts",
|
|
"extra_spans",
|
|
"revet_link_detached",
|
|
"revet_follow_grade",
|
|
)
|
|
|
|
# 노선 [확인] 체인이 옛 노선에서 누가거리로 넘기는 **사용자 입력**(PLAN 74-3) — 계획고 · 면적 ·
|
|
# design_line · road_edges 같은 결과 칸과 단면 유형 · 측구 방향(새 노선 지형이 정함) · 구조물
|
|
# 조작값은 넘기지 않는다. 받는 쪽은 `compute_default_designs(carried=…)`.
|
|
# 칸 목록 정본 = TS `ROW_CARRY_KEYS`(logic/B06_Section_Station_Swap.ts) — 그 글을 읽음(472-4 ① X6).
|
|
_SWAP_TS = Path(__file__).resolve().parent / "logic" / "B06_Section_Station_Swap.ts"
|
|
|
|
|
|
def _ts_keys(name: str) -> tuple[str, ...]:
|
|
"""TS 목록 글(`export const NAME = [ "…" ] as const;`)의 칸 이름."""
|
|
body = re.search(
|
|
rf"export const {name} = \[(.*?)\] as const;", _SWAP_TS.read_text(encoding="utf-8"), re.S
|
|
)
|
|
return tuple(re.findall(r'"([a-z_0-9]+)"', body.group(1)))
|
|
|
|
|
|
CARRY_INPUT_KEYS: tuple[str, ...] = _ts_keys("ROW_CARRY_KEYS")
|
|
# 자리 칸(요구 275-3 · 암 경계 · 2단 경사 · 손 상단측) — 노선이 바뀌면 같은 자리 측점만 넘김 ·
|
|
# 정본 = TS `SEAT_KEYS`(같은 파일).
|
|
SEAT_KEYS: tuple[str, ...] = _ts_keys("SEAT_KEYS")
|
|
|
|
|
|
def stored_standard_cross_section(longitudinal_row: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
"""확정 때 저장해 둔 표준횡단면 설정값(없으면 None = config 기본값).
|
|
|
|
사용자가 「표준 횡단면 설정」에서 고친 값은 확정 시 종단 정본 options에 실린다.
|
|
포장 강제 재계산도 같은 값을 써야 카드가 패널과 어긋나지 않는다(2026-08-28).
|
|
"""
|
|
options = (longitudinal_row or {}).get("data") or {}
|
|
options = options.get("options") if isinstance(options, dict) else None
|
|
standard = options.get("standard_cross_section") if isinstance(options, dict) else None
|
|
return standard if isinstance(standard, dict) else None
|
|
|
|
|
|
def apply_designs(
|
|
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]], standard: dict[str, Any] | None
|
|
) -> int:
|
|
"""모은 측점을 Node 한 번(TS 짝 · 472-4 ②)으로 다시 세워 자리에서 갈아 끼움 · 바꾼 수.
|
|
|
|
`design` = 승계 원천(사용자 조작값 · 상태) · 못 세운 측점은 저장분 그대로(옛 ValueError 자리).
|
|
"""
|
|
results = compute_cross_designs([job for _section, _design, job in jobs], standard)
|
|
changed = 0
|
|
for (section, design, _job), recomputed in zip(jobs, results, strict=True):
|
|
if recomputed is None:
|
|
continue
|
|
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
|
if design.get(key) is not None:
|
|
recomputed[key] = design[key]
|
|
section["design"] = recomputed
|
|
changed += 1
|
|
return changed
|
|
|
|
|
|
def enforce_pavement_ranges(
|
|
longitudinal: dict[str, Any],
|
|
cross_sections: list[dict[str, Any]],
|
|
project_root: Path,
|
|
standard: dict[str, Any] | None = None,
|
|
) -> int:
|
|
"""포장 = 노면공 · 물넘이 범위 안만 — 저장분과 다르면 켬 · 끔 양쪽으로 맞춘다(138-1).
|
|
|
|
사용자가 구간을 지정하면 그 안 횡단도에 **자동 반영**된다(2026-08-28 사용자 확정).
|
|
범위 밖 옛 `paved=true`(지운 노면공 · 옛 카드 단추)는 비포장으로 되돌린다.
|
|
포장 여부는 횡단경사·포장층을 바꾸므로 플래그만 갈아 끼우지 않고 다시 계산한다.
|
|
사용자 조작값(반폭·구조물 조정)은 그대로 승계한다.
|
|
"""
|
|
ranges = pavement_ranges(project_root)
|
|
tilts = tilt_ranges(project_root)
|
|
ford_drops = ford_surface_drops(project_root)
|
|
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
|
for section in cross_sections:
|
|
design = section.get("design")
|
|
if not isinstance(design, dict):
|
|
continue
|
|
chainage = float(section.get("chainage_m", 0.0))
|
|
wanted = paved_at(chainage, ranges)
|
|
tilt = tilt_args(chainage, tilts) # 물넘이 자리 = 측구 없음 · 기울기(286-2 · 287-4)
|
|
if bool(design.get("paved")) == wanted and stored_tilt(design) == tilt:
|
|
continue
|
|
try:
|
|
options = dict(
|
|
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
|
section_mode=str(design.get("section_mode") or "left_cut"),
|
|
ditch_side=design.get("ditch_side"),
|
|
ditch_type=str(design.get("ditch_type") or "standard"),
|
|
paved=wanted,
|
|
rock_boundary_offset_m=design.get(
|
|
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
|
),
|
|
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
|
cut_slope_ratio=stored_cut_slope(design),
|
|
ditch_enabled=design.get("ditch_enabled"),
|
|
ditch_choice=design.get("ditch_choice"),
|
|
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
|
berm=stored_berm(design),
|
|
**curve_widening_args(section),
|
|
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
|
**tilt,
|
|
)
|
|
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
|
except (ValueError, KeyError):
|
|
continue
|
|
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
|
return apply_designs(jobs, standard)
|
|
|
|
|
|
def enforce_design_lines(
|
|
longitudinal: dict[str, Any],
|
|
cross_sections: list[dict[str, Any]],
|
|
project_root: Path,
|
|
standard: dict[str, Any] | None = None,
|
|
) -> int:
|
|
"""[저장]·[확정] 때 **모든 측점** 설계선을 저장된 입력으로 서버가 다시 낸다(139-6).
|
|
|
|
그전에는 포장 · 세월교 · 사토장 측점만 다시 냈고 보통 측점은 브라우저가 보낸 설계를 그대로
|
|
받아 적었다(CLAUDE.md 5장 「[저장]은 서버가 정본으로 다시 계산」 어긋남 · B07 이 저장된 선을
|
|
그림). 입력 = 저장분(지반유형 · 단면유형 · 측구 · 포장 · 암 경계 · 암 절토 경사 · 소단 · 노견
|
|
확장) + 지금 종단 계획고 · 세월교 월류 하강 · 곡선부 확폭. 사용자 조작값은 승계.
|
|
소단 제원은 구조물 정본(`structures.json`)의 구간이 원천 — 쪽마다 옮긴 깊이는 측점 patch(194-1).
|
|
"""
|
|
ford_drops = ford_surface_drops(project_root)
|
|
tilts = tilt_ranges(project_root)
|
|
targets = [section for section in cross_sections if isinstance(section.get("design"), dict)]
|
|
chainages = [float(section.get("chainage_m", 0.0)) for section in targets]
|
|
berms = berm_specs_at(project_root, chainages) # Node 소단 구간(C4) · None = 못 읽음
|
|
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
|
for index, (section, chainage) in enumerate(zip(targets, chainages, strict=True)):
|
|
design = section["design"]
|
|
if berms is not None: # 구간을 지웠으면 소단 없음 · 못 읽으면 저장분 그대로
|
|
design = {**design, "berm": berms[index]}
|
|
try:
|
|
options = dict(
|
|
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
|
section_mode=str(design.get("section_mode") or "left_cut"),
|
|
ditch_side=design.get("ditch_side"),
|
|
ditch_type=str(design.get("ditch_type") or "standard"),
|
|
paved=bool(design.get("paved", False)),
|
|
rock_boundary_offset_m=design.get(
|
|
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
|
),
|
|
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
|
cut_slope_ratio=stored_cut_slope(design),
|
|
ditch_enabled=design.get("ditch_enabled"),
|
|
ditch_choice=design.get("ditch_choice"),
|
|
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
|
berm=stored_berm(design),
|
|
**curve_widening_args(section),
|
|
**stored_shoulder_extend(design),
|
|
**tilt_args(chainage, tilts),
|
|
)
|
|
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
|
except (ValueError, KeyError):
|
|
continue
|
|
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
|
return apply_designs(jobs, standard)
|
|
|
|
|
|
def enforce_ford_surface_drops(
|
|
longitudinal: dict[str, Any],
|
|
cross_sections: list[dict[str, Any]],
|
|
project_root: Path,
|
|
standard: dict[str, Any] | None = None,
|
|
) -> int:
|
|
"""세월교가 앉은 측점의 계획고를 월류 높이만큼 내려 다시 계산한다.
|
|
|
|
저장분은 월류 높이를 넣기 전에 계산된 것이라 노면이 그대로다 — 포장 구간과 같은
|
|
방식으로 **저장분과 지금 값이 다를 때만** 다시 계산한다(2026-08-30 사용자 확정).
|
|
월류 높이를 지우면 같은 경로로 원래 계획고로 되돌아온다. 사용자 조작값은 승계한다.
|
|
"""
|
|
drops = ford_surface_drops(project_root)
|
|
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
|
for section in cross_sections:
|
|
design = section.get("design")
|
|
if not isinstance(design, dict):
|
|
continue
|
|
chainage = float(section.get("chainage_m", 0.0))
|
|
wanted = ford_drop_at(chainage, drops)
|
|
stored = float(design.get("surface_drop_m") or 0.0)
|
|
if abs(wanted - stored) < 1e-6:
|
|
continue
|
|
try:
|
|
options = dict(
|
|
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
|
section_mode=str(design.get("section_mode") or "left_cut"),
|
|
ditch_side=design.get("ditch_side"),
|
|
ditch_type=str(design.get("ditch_type") or "standard"),
|
|
paved=bool(design.get("paved", False)),
|
|
rock_boundary_offset_m=design.get(
|
|
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
|
),
|
|
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
|
cut_slope_ratio=stored_cut_slope(design),
|
|
ditch_enabled=design.get("ditch_enabled"),
|
|
ditch_choice=design.get("ditch_choice"),
|
|
surface_drop_m=wanted,
|
|
berm=stored_berm(design),
|
|
**curve_widening_args(section),
|
|
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
|
**stored_tilt(design),
|
|
)
|
|
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
|
except (ValueError, KeyError):
|
|
continue
|
|
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
|
return apply_designs(jobs, standard)
|
|
|
|
|
|
def default_section_modes(longitudinal: dict[str, Any]) -> dict[float, str]:
|
|
mapping: dict[float, str] = {}
|
|
stations = longitudinal.get("stations")
|
|
for station in stations if isinstance(stations, list) else []:
|
|
side = station.get("uphill_side")
|
|
if side in ("left", "right"):
|
|
mapping[round(float(station.get("chainage_m", 0.0)), 3)] = f"{side}_cut"
|
|
return mapping
|
|
|
|
|
|
def resolve_longitudinal_path(project_root: Path, longitudinal_file_path: str) -> Path:
|
|
"""종단 정본 파일 경로를 검증해 돌려준다 — 저장소 밖 경로를 막는다."""
|
|
root = project_root.resolve()
|
|
path = (root / longitudinal_file_path).resolve()
|
|
if root not in path.parents:
|
|
raise ValueError("종단면 파일 경로가 프로젝트 저장소를 벗어났습니다.")
|
|
if not path.is_file():
|
|
raise FileNotFoundError("종단면 상세 파일을 찾을 수 없습니다.")
|
|
return path
|
|
|
|
|
|
def read_cross_design_inputs(
|
|
project_root: Path,
|
|
longitudinal_file_path: str,
|
|
chainage_m: float,
|
|
preloaded: tuple[Path, dict[str, Any], dict[float, bool]] | None = None,
|
|
) -> tuple[list[dict], float | None, bool, dict[str, Any]]:
|
|
"""(지반 샘플, 계획고, 포장 제안, 측점 기록) — 측점 기록은 곡선부 확폭 입력을 담고 있다.
|
|
|
|
`preloaded` 는 (종단 경로, 종단 내용, 포장 제안표)다. 여러 측점을 잇달아 볼 때
|
|
종단 정본을 측점마다 다시 읽지 않게 넘긴다 — 그 재읽기가 측점당 13.8ms 였다
|
|
(2026-09-06 실측, 측구 방향 역반영 루프).
|
|
"""
|
|
if preloaded is not None:
|
|
longitudinal_path, longitudinal, pavement = preloaded
|
|
else:
|
|
longitudinal_path = resolve_longitudinal_path(project_root, longitudinal_file_path)
|
|
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
|
|
pavement = pavement_suggestions(longitudinal)
|
|
design_elevation = design_elevation_from_longitudinal(longitudinal, chainage_m)
|
|
suggested = pavement.get(round(chainage_m, 3), False)
|
|
cross_dir = longitudinal_path.parent.parent / "cross_sections"
|
|
cross_path = find_cross_file(cross_dir, chainage_m).resolve()
|
|
if cross_dir.resolve() not in cross_path.parents or not cross_path.is_file():
|
|
raise FileNotFoundError("해당 측점의 횡단 상세 파일을 찾을 수 없습니다.")
|
|
cross = json.loads(cross_path.read_text(encoding="utf-8"))
|
|
samples = cross.get("samples") if isinstance(cross, dict) else None
|
|
if not isinstance(samples, list):
|
|
raise ValueError("횡단 상세 파일 형식이 올바르지 않습니다.")
|
|
return samples, design_elevation, suggested, cross
|
|
|
|
|
|
def attach_default_designs(
|
|
longitudinal: dict[str, Any],
|
|
cross_sections: list[dict[str, Any]],
|
|
project_root: Path | None = None,
|
|
standard: dict[str, Any] | None = None,
|
|
) -> None:
|
|
modes = default_section_modes(longitudinal)
|
|
pavement = pavement_suggestions(longitudinal)
|
|
paved_ranges = pavement_ranges(project_root) if project_root else []
|
|
tilts = tilt_ranges(project_root)
|
|
ford_drops = ford_surface_drops(project_root)
|
|
for section in cross_sections:
|
|
if section.get("design"):
|
|
continue
|
|
try:
|
|
chainage = float(section.get("chainage_m", 0.0))
|
|
suggested = pavement.get(round(chainage, 3), False)
|
|
design = compute_cross_design(
|
|
section.get("samples", []),
|
|
design_elevation_from_longitudinal(longitudinal, chainage),
|
|
ground_type="ripping_rock",
|
|
section_mode=modes.get(round(chainage, 3), "left_cut"),
|
|
# 포장은 사용자 구간 지정만 켠다 — 경사 제안은 경고로만 남는다(2026-08-28).
|
|
paved=paved_at(chainage, paved_ranges),
|
|
standard=standard,
|
|
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
|
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
|
**curve_widening_args(section),
|
|
**tilt_args(chainage, tilts),
|
|
)
|
|
design.update(status="provisional", pavement_suggested=suggested)
|
|
section["design"] = design
|
|
except (ValueError, KeyError):
|
|
continue
|
|
|
|
|
|
def stored_cut_slope(stored: dict[str, Any]) -> float | None:
|
|
"""저장분에 남은 측점별 암 절토 경사비 — 없거나 0(표준값으로 되돌림)이면 None.
|
|
|
|
⚠ 소단과 같은 성격이다 — **경사는 나르는 값이 아니라 기하 입력**이라 계산에 넣어야 한다.
|
|
다시 계산한 뒤 키만 베껴 붙이면 설계선은 옛 경사로 나오고 값만 새것이 되어 어긋난다.
|
|
"""
|
|
value = stored.get("cut_slope_ratio_user")
|
|
if isinstance(value, (int, float)) and float(value) > 0:
|
|
return float(value)
|
|
return None
|
|
|
|
|
|
def berm_from(spec: Any, sides: dict[str, Any]) -> BermSpec | None:
|
|
"""소단 제원 dict + 쪽마다 옮긴 깊이(`berm_depth_{side}_m` · 194-1) → BermSpec."""
|
|
if not isinstance(spec, dict):
|
|
return None
|
|
|
|
def own(side: str) -> float | None:
|
|
value = sides.get(f"berm_depth_{side}_m")
|
|
ok = isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
return max(float(value), 0.0) if ok else None
|
|
|
|
try:
|
|
return BermSpec(
|
|
width_m=float(spec.get("width_m", BERM_DEFAULT_WIDTH_M)),
|
|
interval_m=float(spec.get("interval_m", BERM_DEFAULT_INTERVAL_M)),
|
|
slope_deg=float(spec.get("slope_deg", BERM_DEFAULT_SLOPE_DEG)),
|
|
depth_m=max(float(spec.get("depth_m", BERM_DEFAULT_DEPTH_M)), 0.0),
|
|
depth_left_m=own("left"),
|
|
depth_right_m=own("right"),
|
|
)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def stored_berm(stored: dict[str, Any]) -> BermSpec | None:
|
|
"""저장분에 남은 소단 제원 + 쪽마다 옮긴 깊이 — 세션값이 없을 때 쓴다(확정 뒤·다른 PC)."""
|
|
return berm_from(stored.get("berm"), stored)
|
|
|
|
|
|
def recompute_designs_for_alignment(
|
|
longitudinal: dict[str, Any],
|
|
cross_sections: list[dict[str, Any]],
|
|
stored_designs: list[dict[str, Any]],
|
|
standard: dict[str, Any] | None,
|
|
rock_boundary_offsets: dict[str, float] | None = None,
|
|
project_root: Path | None = None,
|
|
berms: dict[str, dict[str, float]] | None = None,
|
|
cut_slope_ratios: dict[str, float] | None = None,
|
|
) -> None:
|
|
modes = default_section_modes(longitudinal)
|
|
pavement = pavement_suggestions(longitudinal)
|
|
paved_ranges = pavement_ranges(project_root) if project_root else []
|
|
tilts = tilt_ranges(project_root)
|
|
ford_drops = ford_surface_drops(project_root)
|
|
stored_by_chainage = {
|
|
round(float(record["chainage_m"]), 3): (record.get("design") or {})
|
|
for record in stored_designs
|
|
}
|
|
# 측점별 암 절토 경사(2026-09-07) — 0 은 「표준값을 씀」이라 저장분을 덮어 지운다.
|
|
session_cut_slopes: dict[float, float] = {}
|
|
for raw_key, ratio in (cut_slope_ratios or {}).items():
|
|
try:
|
|
session_cut_slopes[round(float(raw_key), 3)] = float(ratio)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
session_offsets: dict[float, float] = {}
|
|
for raw_key, offset in (rock_boundary_offsets or {}).items():
|
|
try:
|
|
session_offsets[round(float(raw_key), 3)] = float(offset)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
# 측점별 소단 제원 — 값이 없는 측점은 소단 없음(종전 설계 그대로).
|
|
session_berms: dict[float, BermSpec] = {}
|
|
for raw_key, spec in (berms or {}).items():
|
|
try:
|
|
parsed = berm_from(spec, spec if isinstance(spec, dict) else {}) # 쪽 깊이도(194-1)
|
|
if parsed is not None:
|
|
session_berms[round(float(raw_key), 3)] = parsed
|
|
except (TypeError, ValueError):
|
|
continue
|
|
for section in cross_sections:
|
|
chainage = float(section.get("chainage_m", 0.0))
|
|
key = round(chainage, 3)
|
|
stored = stored_by_chainage.get(key) or {}
|
|
suggested = pavement.get(key, False)
|
|
try:
|
|
design = compute_cross_design(
|
|
section.get("samples", []),
|
|
design_elevation_from_longitudinal(longitudinal, chainage),
|
|
ground_type=str(stored.get("ground_type") or "ripping_rock"),
|
|
section_mode=str(stored.get("section_mode") or modes.get(key, "left_cut")),
|
|
ditch_side=stored.get("ditch_side"),
|
|
ditch_type=str(stored.get("ditch_type") or "standard"),
|
|
paved=paved_at(chainage, paved_ranges),
|
|
standard=standard,
|
|
rock_boundary_offset_m=session_offsets.get(
|
|
key,
|
|
stored.get("rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M),
|
|
),
|
|
two_stage_slope=bool(stored.get("two_stage_slope", False)),
|
|
cut_slope_ratio=(
|
|
(session_cut_slopes[key] or None)
|
|
if key in session_cut_slopes
|
|
else stored_cut_slope(stored)
|
|
),
|
|
ditch_enabled=stored.get("ditch_enabled"),
|
|
ditch_choice=stored.get("ditch_choice"),
|
|
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
|
berm=session_berms.get(key) or stored_berm(stored),
|
|
**curve_widening_args(section),
|
|
**stored_shoulder_extend(stored), # 노견 확장(139-6)
|
|
**tilt_args(chainage, tilts),
|
|
)
|
|
except (ValueError, KeyError):
|
|
continue
|
|
design.update(status="provisional", pavement_suggested=suggested)
|
|
if stored.get("display_half_width_m") is not None:
|
|
design["display_half_width_m"] = stored["display_half_width_m"]
|
|
if stored.get("inlet_structure") is not None:
|
|
design["inlet_structure"] = stored["inlet_structure"]
|
|
if stored.get("basin_adjust") is not None:
|
|
design["basin_adjust"] = stored["basin_adjust"]
|
|
if stored.get("ford_adjust") is not None:
|
|
design["ford_adjust"] = stored["ford_adjust"]
|
|
if stored.get("box_adjust") is not None:
|
|
design["box_adjust"] = stored["box_adjust"]
|
|
section["design"] = design
|
|
|
|
|
|
def compute_default_designs(
|
|
project_root: Path,
|
|
longitudinal_file_path: str,
|
|
chainages: list[float],
|
|
standard: dict[str, Any] | None = None,
|
|
carried: dict[float, dict[str, Any]] | None = None,
|
|
crosses: dict[float, dict[str, Any]] | None = None,
|
|
) -> list[tuple[float, dict[str, Any]]]:
|
|
"""미지정 측점들을 기본값(리핑암 + 암반 경계 0.5m/상단측 절토)으로 계산한 목록을 만든다.
|
|
|
|
`crosses`(누가거리 round 3 → 횡단 dict)가 오면 파일 대신 그 횡단으로 계산한다 — 아직 정본에
|
|
없는 B06 미리 뜨기 자리(273-2) · 상단측도 그 횡단 값(종단 정본에 측점이 없음).
|
|
|
|
standard가 오면(확정 요청의 패널 편집값) 그 값으로 표준단면 기하를 계산한다.
|
|
`carried`(누가거리 round 3 → `CARRY_INPUT_KEYS` · `ROUTE_CARRY_KEYS` 값)가 오면 그 측점은
|
|
기본값 대신 그 입력으로 계산한다 — 노선 [확인] 체인의 옛 설계 이월(PLAN 74-3 · 417). 결과 칸은
|
|
늘 새 계획고로 다시 낸다.
|
|
계획고 부재 등으로 계산 불가한 측점은 조용히 건너뛴다(확정을 막지 않기 위함).
|
|
"""
|
|
root = project_root.resolve()
|
|
longitudinal_path = (root / longitudinal_file_path).resolve()
|
|
if root not in longitudinal_path.parents or not longitudinal_path.is_file():
|
|
return []
|
|
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
|
|
cross_dir = longitudinal_path.parent.parent / "cross_sections"
|
|
default_modes = default_section_modes(longitudinal)
|
|
pavement = pavement_suggestions(longitudinal)
|
|
paved_ranges = pavement_ranges(root)
|
|
tilts = tilt_ranges(root)
|
|
ford_drops = ford_surface_drops(root)
|
|
results: list[tuple[float, dict[str, Any]]] = []
|
|
for chainage_m in chainages:
|
|
cross = (crosses or {}).get(round(chainage_m, 3))
|
|
cross_path = find_cross_file(cross_dir, chainage_m)
|
|
if cross is None and not cross_path.is_file():
|
|
continue
|
|
try:
|
|
if cross is None:
|
|
cross = json.loads(cross_path.read_text(encoding="utf-8"))
|
|
samples = cross.get("samples")
|
|
if not isinstance(samples, list):
|
|
continue
|
|
suggested = pavement.get(round(chainage_m, 3), False)
|
|
user = (carried or {}).get(round(chainage_m, 3)) or {}
|
|
rock_offset = user.get("rock_boundary_offset_m")
|
|
design = compute_cross_design(
|
|
samples,
|
|
design_elevation_from_longitudinal(longitudinal, chainage_m),
|
|
ground_type=str(user.get("ground_type") or "ripping_rock"),
|
|
# 손 상단측(같은 자리로 넘긴 단면 유형 · 측구 방향 · 요구 275-3) · 없으면 상단측
|
|
section_mode=user.get("section_mode")
|
|
or default_modes.get(
|
|
round(chainage_m, 3),
|
|
f"{cross['uphill_side']}_cut"
|
|
if cross.get("uphill_side") in ("left", "right")
|
|
else "left_cut",
|
|
),
|
|
ditch_side=user.get("ditch_side"),
|
|
ditch_type=str(user.get("ditch_type") or "standard"),
|
|
# 포장은 사용자 구간 지정만 켠다 — 경사 제안은 경고로만 남는다(2026-08-28).
|
|
paved=paved_at(chainage_m, paved_ranges),
|
|
standard=standard,
|
|
rock_boundary_offset_m=(
|
|
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M if rock_offset is None else rock_offset
|
|
),
|
|
two_stage_slope=bool(user.get("two_stage_slope", False)),
|
|
cut_slope_ratio=stored_cut_slope(user),
|
|
ditch_choice=user.get("ditch_choice"),
|
|
surface_drop_m=ford_drop_at(chainage_m, ford_drops),
|
|
berm=stored_berm(user),
|
|
**curve_widening_args(cross),
|
|
**stored_shoulder_extend(user), # 노선 저장이 넘긴 사면 쉬프트(417)
|
|
**tilt_args(chainage_m, tilts),
|
|
)
|
|
for key in ("berm", "cut_slope_ratio_user", *ROUTE_CARRY_KEYS):
|
|
if user.get(key) is not None:
|
|
design[key] = user[key]
|
|
design["status"] = "provisional"
|
|
design["pavement_suggested"] = suggested
|
|
results.append((chainage_m, design))
|
|
except (ValueError, KeyError, OSError, json.JSONDecodeError):
|
|
continue
|
|
return results
|