사용자 확정(2026-09-09): 층따기 단위는 ㎡. 대신 그 면적이 B06 설계에서 나와야 한다. - `_bench_cut_length`: 성토(diff<0) 구간에서 원지반 횡단기울기가 **1:4 보다 급한** 조각만 골라 지표면을 따라간 길이(빗변)를 더한다. 절·성토 경계는 영교점까지만 센다. 근거는 별표2·교본 6장 4절, 지식DB 성토_비탈면 §4 [구현]. - 결과에 `bench_cut_length_m` 을 실어 B08 이 측점 사이를 평균단면적법으로 이어 ㎡ 를 낸다. ⚠ 여기서 ㎥ 로 바꾸지 않는다 — 단의 높이·폭은 설계도서 값이라 지어낼 수 없다. - 파이썬·TS 짝을 함께 고침(`common_util_cross_design_areas.ts`). 거울 테스트 통과. - ⚠ 성토 비탈면 길이가 아니라 **원지반 표면**을 센다 — 층따기 대상 면이 그쪽이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
853 lines
45 KiB
Python
853 lines
45 KiB
Python
"""B06 측점 표준횡단 설계 계산 엔진.
|
|
|
|
⚠⚠ TS 짝 파일과 **한 벌**이다 — 한쪽만 고치면 화면과 저장본이 갈린다 ⚠⚠
|
|
짝: `common_util/common_util_cross_design.ts` (`computeCrossDesign`)
|
|
면적 적분은 `B06_Section_Engine_Areas.py` ↔ `common_util_cross_design_areas.ts`.
|
|
회귀 테스트가 두 구현을 같은 입력으로 실제 비교한다:
|
|
`tmp/tests/test_b06_cross_design_mirror.py` — 어느 쪽을 고치든 반드시 같이 돌릴 것.
|
|
|
|
왜 두 벌인가: 사용자가 계획선을 만지는 동안의 계산은 **브라우저 안에서 끝나야
|
|
한다**(2026-09-03 사용자 확정). 계획고가 바뀔 때마다 서버에 전 측점 횡단을 물으면
|
|
왕복이 조작 속도를 지배했다. 이 파이썬 엔진은 자동설계 체인·[저장]·[확정]·도면
|
|
산출의 **정본**이고, TS 미러는 조작 중 화면용이다. 값이 갈리면 화면과 납품물이
|
|
달라지므로 새 필드·상수·판정을 더할 때 양쪽을 함께 고칠 것.
|
|
|
|
지반유형(토사/리핑암/발파암)과 단면유형(좌절/우절/양절/양성)에 따라 표준횡단
|
|
설계선을 구성하고, 지반선과의 차이로 절·성토 단면적을 산출한다. B06에서 사용자가
|
|
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B07 상세설계에서
|
|
확정치로 대체된다.
|
|
|
|
표준단면 기하는 config STANDARD_CROSS_SECTION(단일 진실 원천)을 읽고, B06 설정
|
|
패널 편집값(standard 인자)이 오면 요청값 → config 기본값 순으로 우선한다.
|
|
설계선은 도면 표준대로 횡단경사(측구 방향), 노견, 측구(사다리꼴/L형), 절·성토
|
|
사면을 모두 포함하며, 면적은 지반 샘플과 설계 꼭짓점을 합친 오프셋 격자에서
|
|
사다리꼴 적분한다(측구 굴착이 절토 면적에 자연 포함).
|
|
|
|
좌표 규약(generate_sections cad_exchange 준수): offset_m 양수=좌, 음수=우.
|
|
경사비는 수평:수직 = ratio:1 (예: 1:1.2 → ratio=1.2).
|
|
"""
|
|
|
|
import math
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from B06_Section.B06_Section_Engine_Areas import (
|
|
_bench_cut_length,
|
|
_split_cut_areas,
|
|
_trapezoid_areas,
|
|
)
|
|
from common_util.common_util_cross_berm import (
|
|
BermSpec,
|
|
cut_profile_points,
|
|
fill_profile_points,
|
|
)
|
|
from common_util.common_util_cross_berm import elevation_at as berm_elevation_at
|
|
from config.config_system import (
|
|
CURVE_WIDENING_MAX_WIDTH_M,
|
|
SECTION_DITCH_SIDES,
|
|
SECTION_DITCH_TYPES,
|
|
SECTION_GROUND_TYPE_PRESET,
|
|
SECTION_MODES,
|
|
STANDARD_CROSS_SECTION,
|
|
)
|
|
from config.config_system import (
|
|
curve_widening_m as _curve_widening_m,
|
|
)
|
|
|
|
# 사면이 원지반과 만났다고 볼 높이차(m). 이보다 크면 샘플 끝에서 잘린 것으로 본다.
|
|
_SLOPE_CLOSE_TOLERANCE_M = 0.01
|
|
|
|
|
|
def _side_role(section_mode: str) -> tuple[str, str]:
|
|
"""단면유형 → (좌측 역할, 우측 역할). 역할은 'cut' 또는 'fill'."""
|
|
if section_mode == "left_cut":
|
|
return "cut", "fill"
|
|
if section_mode == "right_cut":
|
|
return "fill", "cut"
|
|
if section_mode == "both_cut":
|
|
return "cut", "cut"
|
|
if section_mode == "both_fill":
|
|
return "fill", "fill"
|
|
raise ValueError(f"지원하지 않는 단면유형입니다: {section_mode}")
|
|
|
|
|
|
def _resolve_ditch_side(section_mode: str, ditch_side: str | None) -> str:
|
|
"""측구(배수) 배치 측을 결정한다.
|
|
|
|
편절편성은 절토측이 곧 측구측이라 자동 결정하고, 양절·양성은 배수 방향을
|
|
사용자 지정(ditch_side)에 맡긴다(미지정 시 좌측 기본).
|
|
"""
|
|
if section_mode == "left_cut":
|
|
return "left"
|
|
if section_mode == "right_cut":
|
|
return "right"
|
|
if ditch_side in SECTION_DITCH_SIDES:
|
|
return ditch_side
|
|
return "left"
|
|
|
|
|
|
def _as_float(value: Any, fallback: float) -> float:
|
|
"""패널 편집값을 안전하게 float으로 읽는다(손상 시 config 기본값)."""
|
|
try:
|
|
parsed = float(value)
|
|
except (TypeError, ValueError):
|
|
return fallback
|
|
return parsed if parsed >= 0 else fallback
|
|
|
|
|
|
def _resolve_group(preset_key: str, standard: dict[str, Any] | None) -> dict[str, float]:
|
|
"""config 기본값 위에 패널 편집값(standard[preset_key])을 덮어 평탄화한다."""
|
|
base = STANDARD_CROSS_SECTION[preset_key]
|
|
override = standard.get(preset_key) if isinstance(standard, dict) else None
|
|
override = override if isinstance(override, dict) else {}
|
|
base_ditch = base["ditch"]
|
|
over_ditch = override.get("ditch")
|
|
over_ditch = over_ditch if isinstance(over_ditch, dict) else {}
|
|
base_l = base.get("ditch_l_type", {})
|
|
over_l = override.get("ditch_l_type")
|
|
over_l = over_l if isinstance(over_l, dict) else {}
|
|
base_slope = base["cross_slope_pct"]
|
|
over_slope = override.get("cross_slope_pct")
|
|
over_slope = over_slope if isinstance(over_slope, dict) else {}
|
|
return {
|
|
"road_width_m": _as_float(override.get("road_width_m"), base["road_width_m"]),
|
|
"shoulder_left_m": _as_float(override.get("shoulder_left_m"), base["shoulder_left_m"]),
|
|
"shoulder_right_m": _as_float(override.get("shoulder_right_m"), base["shoulder_right_m"]),
|
|
"ditch_top_width_m": _as_float(over_ditch.get("top_width_m"), base_ditch["top_width_m"]),
|
|
"ditch_bottom_width_m": _as_float(
|
|
over_ditch.get("bottom_width_m"), base_ditch["bottom_width_m"]
|
|
),
|
|
"ditch_depth_m": _as_float(over_ditch.get("depth_m"), base_ditch["depth_m"]),
|
|
"l_ditch_width_m": _as_float(over_l.get("width_m"), base_l.get("width_m", 0.5)),
|
|
"l_ditch_depth_m": _as_float(over_l.get("depth_m"), base_l.get("depth_m", 0.1)),
|
|
# 횡단경사는 범위(min~max) 중 하한을 기본 채택한다(도면 표기 앞값).
|
|
"cross_slope_pct": _as_float(over_slope.get("min"), base_slope["min"]),
|
|
"fill_slope_ratio": _as_float(override.get("fill_slope_ratio"), base["fill_slope_ratio"]),
|
|
"cut_slope_ratio": _as_float(override.get("cut_slope_ratio"), base["cut_slope_ratio"]),
|
|
"pavement_thickness_m": _as_float(
|
|
override.get("pavement_thickness_m"), base.get("pavement_thickness_m", 0.2)
|
|
),
|
|
}
|
|
|
|
|
|
def _ground_interpolator(valid: list[tuple[float, float]]):
|
|
"""정렬된 (offset, 지반고) 샘플의 선형 보간 함수를 만든다(범위 밖 끝값 클램프)."""
|
|
|
|
def ground_at(offset_m: float) -> float:
|
|
if offset_m <= valid[0][0]:
|
|
return valid[0][1]
|
|
if offset_m >= valid[-1][0]:
|
|
return valid[-1][1]
|
|
for index in range(1, len(valid)):
|
|
x1, z1 = valid[index]
|
|
if offset_m > x1:
|
|
continue
|
|
x0, z0 = valid[index - 1]
|
|
span = x1 - x0
|
|
if span <= 0:
|
|
return z1
|
|
ratio = (offset_m - x0) / span
|
|
return z0 + (z1 - z0) * ratio
|
|
return valid[-1][1]
|
|
|
|
return ground_at
|
|
|
|
|
|
class _SectionGeometry:
|
|
"""설계선 피스와이즈 평가기. 노면 → 측구 → 사면 순으로 offset의 설계고를 계산한다."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
design_elevation_m: float,
|
|
group: dict[str, float],
|
|
section_mode: str,
|
|
ditch_side: str,
|
|
ditch_type: str,
|
|
cross_slope_pct: float,
|
|
ground_at: Callable[[float], float] | None = None,
|
|
soil_cut_ratio: float | None = None,
|
|
rock_boundary_offset_m: float | None = None,
|
|
two_stage_slope: bool = False,
|
|
ditch_enabled: bool | None = None,
|
|
widening_left_m: float = 0.0,
|
|
widening_right_m: float = 0.0,
|
|
berm: BermSpec | None = None,
|
|
) -> None:
|
|
half_road = group["road_width_m"] / 2.0
|
|
# 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽).
|
|
# 그래서 반폭을 좌·우로 나눠 든다 — 확폭이 0이면 예전과 똑같은 대칭 단면이다.
|
|
self.half_road_left = half_road + max(widening_left_m, 0.0)
|
|
self.half_road_right = half_road + max(widening_right_m, 0.0)
|
|
self.half_road = half_road # 규격 차도 반폭(확폭 전) — 수량·표기 기준
|
|
self.left_extent = self.half_road_left + group["shoulder_left_m"] # 좌(+) 노면 끝
|
|
self.right_extent = self.half_road_right + group["shoulder_right_m"] # 우(-) 노면 끝
|
|
self.z_center = design_elevation_m
|
|
self.cut_ratio = max(group["cut_slope_ratio"], 1e-6) # 암 구간(하단) 절토 경사
|
|
self.fill_ratio = max(group["fill_slope_ratio"], 1e-6)
|
|
self.left_role, self.right_role = _side_role(section_mode)
|
|
self.ditch_side = ditch_side
|
|
# 2단계 절토: 암반 경계선(지반선 + rock_boundary_offset) 아래는 암 경사(cut_ratio),
|
|
# 위는 토사 경사(soil_cut_ratio)를 쓴다. 경계 아래→위 전환점(무릎)을 측별로 미리 구한다.
|
|
self.soil_cut_ratio = max(soil_cut_ratio or group["cut_slope_ratio"], 1e-6)
|
|
self.two_stage = bool(
|
|
two_stage_slope and ground_at is not None and rock_boundary_offset_m is not None
|
|
)
|
|
self._ground_at = ground_at
|
|
self._rock_offset = rock_boundary_offset_m or 0.0
|
|
# 소단 제원(없으면 None) — 절토 사면 꼭짓점 셈에 그대로 넘어간다.
|
|
self.berm = berm
|
|
self._cut_points_cache: dict[str, list[tuple[float, float]]] = {}
|
|
self._fill_points_cache: dict[str, list[tuple[float, float]]] = {}
|
|
# 절토 사면·지반 최초 교차거리(측별 캐시) — 교차 후 절토 종료용(N-2-4).
|
|
self._cut_cross: dict[str, float | None] = {}
|
|
self._fill_cross: dict[str, float | None] = {}
|
|
self.ditch_type = ditch_type
|
|
# 횡단경사: 측구 방향으로 내려가는 단일 사면 (좌=+offset 규약).
|
|
slope = cross_slope_pct / 100.0
|
|
self.slope_per_offset = -slope if ditch_side == "left" else slope
|
|
|
|
# 단면 유형 자동 판정(D-2): 각 측 절/성토 역할을 노면 끝 지반이 설계면보다
|
|
# 높은지(절토)/낮은지(성토)로 결정한다. 좌절/우절/양절/양성이 모두 지형에서
|
|
# 자연 도출된다. 사용자 입력 section_mode는 측구 방향(ditch_side) 기본값에만 쓰고
|
|
# 절/성토 역할은 손대지 않는다. ground_at이 없으면 section_mode 기반 역할을 쓴다.
|
|
if ground_at is not None:
|
|
self.left_role = (
|
|
"cut"
|
|
if ground_at(self.left_extent) > self.road_z(self.left_extent) + 1e-3
|
|
else "fill"
|
|
)
|
|
self.right_role = (
|
|
"cut"
|
|
if ground_at(-self.right_extent) > self.road_z(-self.right_extent) + 1e-3
|
|
else "fill"
|
|
)
|
|
|
|
# 측구 생성 여부(D-1): 양성은 항상 미생성. ditch_enabled가 오면 그 값을 따르고(수동
|
|
# override), None이면 자동 판정 — 측구측 노면 끝에서 지반이 설계면보다 높으면(절토
|
|
# 상황) 생성, 낮으면(성토 상황, 자연 배수) 미생성. ground_at 없으면 보수적으로 생성.
|
|
if section_mode == "both_fill":
|
|
self.has_ditch = False
|
|
elif ditch_enabled is not None:
|
|
self.has_ditch = ditch_enabled
|
|
elif ground_at is not None:
|
|
ditch_edge = self.left_extent if ditch_side == "left" else -self.right_extent
|
|
self.has_ditch = ground_at(ditch_edge) > self.road_z(ditch_edge) + 1e-3
|
|
else:
|
|
self.has_ditch = True
|
|
|
|
# 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용).
|
|
self.ditch_points: list[tuple[float, float]] = []
|
|
edge_offset = self.left_extent if ditch_side == "left" else -self.right_extent
|
|
outward = 1.0 if ditch_side == "left" else -1.0
|
|
edge_z = self.road_z(edge_offset)
|
|
if self.has_ditch:
|
|
if ditch_type == "l_type":
|
|
# L형: 노면 끝에서 폭 W 동안 깊이 D로 내려가는 경사 바닥 + 바깥 수직벽.
|
|
# 바깥(노견 반대측) 상단은 **노견과 같은 표고**로 닫는다 — 사면 시작점이
|
|
# 측구 바닥 높이로 내려가면 안 된다(2026-08-23 사용자 지시).
|
|
width = group["l_ditch_width_m"]
|
|
depth = group["l_ditch_depth_m"]
|
|
self.ditch_points = [
|
|
(edge_offset, edge_z),
|
|
(edge_offset + outward * width, edge_z - depth),
|
|
(edge_offset + outward * width, edge_z),
|
|
]
|
|
else:
|
|
# 일반: 상단폭/저폭/깊이 사다리꼴.
|
|
top = group["ditch_top_width_m"]
|
|
bottom = min(group["ditch_bottom_width_m"], top)
|
|
depth = group["ditch_depth_m"]
|
|
inset = (top - bottom) / 2.0
|
|
self.ditch_points = [
|
|
(edge_offset, edge_z),
|
|
(edge_offset + outward * inset, edge_z - depth),
|
|
(edge_offset + outward * (inset + bottom), edge_z - depth),
|
|
(edge_offset + outward * top, edge_z),
|
|
]
|
|
|
|
def road_z(self, offset_m: float) -> float:
|
|
"""노면(노견 포함) 설계고 — 중심 계획고에서 횡단경사로 기운 단일 평면."""
|
|
return self.z_center + self.slope_per_offset * offset_m
|
|
|
|
def _slope_start(self, side: str) -> tuple[float, float]:
|
|
"""사면 시작점(오프셋 절대값 기준 거리, 표고)을 계산한다."""
|
|
if side == "left":
|
|
edge_offset, edge_z = self.left_extent, self.road_z(self.left_extent)
|
|
else:
|
|
edge_offset, edge_z = self.right_extent, self.road_z(-self.right_extent)
|
|
if side == self.ditch_side and self.ditch_points:
|
|
outer = self.ditch_points[-1]
|
|
return abs(outer[0]), outer[1]
|
|
return edge_offset, edge_z
|
|
|
|
def _rock_boundary_z(self, side: str, dist: float) -> float:
|
|
"""측·거리(절대 오프셋)에서 암반 경계선 표고 = 지반선 + 오프셋(음수=하향)."""
|
|
signed = dist if side == "left" else -dist
|
|
assert self._ground_at is not None # two_stage일 때만 호출
|
|
return self._ground_at(signed) + self._rock_offset
|
|
|
|
def cut_points(self, side: str) -> list[tuple[float, float]]:
|
|
"""절토 사면 꼭짓점 `[(거리, 표고), ...]` — 무릎과 소단이 모두 여기 들어 있다.
|
|
|
|
셈은 짝 모듈 `common_util_cross_berm` 한 벌이 한다(TS 도 같은 것을 부른다).
|
|
소단이 없으면 종전 무릎 방식과 **같은 값**이다(동치 시험으로 지킨다).
|
|
"""
|
|
if side in self._cut_points_cache:
|
|
return self._cut_points_cache[side]
|
|
start_dist, start_z = self._slope_start(side)
|
|
boundary = (lambda dist: self._rock_boundary_z(side, dist)) if self.two_stage else None
|
|
points = cut_profile_points(
|
|
start_dist,
|
|
start_z,
|
|
self.cut_ratio,
|
|
self.soil_cut_ratio,
|
|
boundary,
|
|
self.berm,
|
|
# 소단이 있으면 경계를 오갈 때마다 꺾는다 — 소단은 평탄한데 경계선은 지반을
|
|
# 따라 올라가서 되돌아 들어가는 일이 흔하다. 한 번만 꺾으면 그 구간을 암인데
|
|
# 토사 경사로 그려 절토가 조용히 커진다(2026-09-07).
|
|
multi_knee=self.berm is not None,
|
|
)
|
|
self._cut_points_cache[side] = points
|
|
return points
|
|
|
|
def _cut_slope_z(self, side: str, dist: float) -> float:
|
|
"""절토 사면선 표고(무릎·소단 반영). 지반 교차 클램프는 하지 않는다."""
|
|
return berm_elevation_at(self.cut_points(side), dist)
|
|
|
|
def fill_points(self, side: str) -> list[tuple[float, float]]:
|
|
"""성토 사면 꼭짓점 — 소단이 들어 있다. 절토와 달리 무릎은 없다."""
|
|
if side in self._fill_points_cache:
|
|
return self._fill_points_cache[side]
|
|
start_dist, start_z = self._slope_start(side)
|
|
points = fill_profile_points(start_dist, start_z, self.fill_ratio, self.berm)
|
|
self._fill_points_cache[side] = points
|
|
return points
|
|
|
|
def _fill_slope_z(self, side: str, dist: float) -> float:
|
|
"""성토 사면선 표고(소단 반영). 지반 교차 클램프는 하지 않는다."""
|
|
return berm_elevation_at(self.fill_points(side), dist)
|
|
|
|
def cut_slope_segments(self) -> list[dict[str, Any]]:
|
|
"""절토 사면을 **경사 구간별로** 쪼갠 목록.
|
|
|
|
⚠ **지금 이 값을 읽는 곳은 없다**(2026-09-07). 임자였던 별표2 법정 경사 검사가 폐기됐고
|
|
(암질을 횡단도에서 안 고르기로 사용자 확정), 저장분에도 안 들어간다. 소단 기하가 이 셈
|
|
위에 서 있어 남겨 둔다 — **되살릴 때는 저장분에서 읽지 말고 계산해서 쓸 것.**
|
|
|
|
원래 필요했던 까닭(되살릴 때 그대로 유효) — 소단이 서면 사면 전체를 하나로 재는
|
|
「실효 경사」가 완만해져 **위반이 사라진 것처럼** 보인다(폭 1.0·간격 2 이면 설계 1:1 이
|
|
실효 1:1.71). 검사는 소단을 뺀 **사면 구간 자체의 경사**를 봐야 한다.
|
|
|
|
· 평탄부(소단)는 싣지 않는다 — 경사 구간이 아니고 경사비가 무한대가 된다.
|
|
· 지반과 만난 뒤 구간도 싣지 않는다 — 절토가 아니다.
|
|
· `material` 은 암반 경계 기준 `rock`/`soil`. 경계를 모르면(2단계 아님) None.
|
|
암을 다시 가르는 값은 측점의 `cut_rock_kind` 를 읽는다(구간에 싣지 않는다).
|
|
"""
|
|
segments: list[dict[str, Any]] = []
|
|
for side in ("left", "right"):
|
|
role = self.left_role if side == "left" else self.right_role
|
|
if role != "cut":
|
|
continue
|
|
cross = self.cut_cross_dist(side)
|
|
points = self.cut_points(side)
|
|
sign = 1.0 if side == "left" else -1.0
|
|
for index in range(1, len(points)):
|
|
start_d, start_z = points[index - 1]
|
|
end_d, end_z = points[index]
|
|
if cross is not None and start_d >= cross - 1e-9:
|
|
break # 지반과 만난 뒤는 절토가 없다
|
|
if cross is not None and end_d > cross:
|
|
# 지반과 만나는 점에서 구간을 자른다.
|
|
end_z = berm_elevation_at(points, cross)
|
|
end_d = cross
|
|
run = end_d - start_d
|
|
rise = end_z - start_z
|
|
if run <= 1e-9 or rise <= 1e-6:
|
|
continue # 길이 0·역방향은 검사 대상이 아니다
|
|
if self.berm is not None and abs(run - self.berm.width_m) < 1e-6:
|
|
# 소단(평탄부) — 폭이 딱 맞고 오름이 기울기(2°)만큼이면 그것이다.
|
|
berm_rise = math.tan(math.radians(self.berm.slope_deg)) * self.berm.width_m
|
|
if abs(rise - berm_rise) < 1e-9:
|
|
continue
|
|
# 재료는 **그 구간을 실제로 그린 경사비**로 가른다 — 경계선을 다시 재면
|
|
# 안 된다. 무릎을 지난 뒤에도 경계선은 지반을 따라 계속 오르므로, 토사
|
|
# 경사로 그린 구간이 경계 아래로 되돌아가 있는 일이 흔하다. 그것을 경계로
|
|
# 재면 「경사비는 토사인데 재료는 암」인 구간이 생긴다(2026-09-07 다른 창
|
|
# 실측: 용화 63측점에서 13구간). 그린 대로 적는 것이 맞다.
|
|
material: str | None = None
|
|
if self.two_stage and abs(self.soil_cut_ratio - self.cut_ratio) > 1e-9:
|
|
drawn = run / rise
|
|
material = (
|
|
"soil"
|
|
if abs(drawn - self.soil_cut_ratio) < abs(drawn - self.cut_ratio)
|
|
else "rock"
|
|
)
|
|
segments.append(
|
|
{
|
|
"side": side,
|
|
"ratio": round(run / rise, 4),
|
|
"rise_m": round(rise, 4),
|
|
"run_m": round(run, 4),
|
|
"start_offset_m": round(sign * start_d, 4),
|
|
"end_offset_m": round(sign * end_d, 4),
|
|
"material": material,
|
|
}
|
|
)
|
|
return segments
|
|
|
|
def cut_cross_dist(self, side: str) -> float | None:
|
|
"""절토 사면이 지반선과 처음 만나는 거리(절대 오프셋). 이후는 절토 없음(N-2-4).
|
|
|
|
지면과 1회 교차하면 그다음 경사(2단계 전환 포함)는 의미가 없으므로 교차점에서
|
|
절토를 종료한다. 시작(노면 끝)부터 사면이 지반 위면 교차거리=시작(절토 없음),
|
|
끝까지 못 만나면 None.
|
|
"""
|
|
if side in self._cut_cross:
|
|
return self._cut_cross[side]
|
|
result: float | None = None
|
|
if self._ground_at is not None:
|
|
start_dist, _start_z = self._slope_start(side)
|
|
step = 0.05
|
|
dist = start_dist
|
|
max_dist = start_dist + 500.0
|
|
while dist <= max_dist:
|
|
signed = dist if side == "left" else -dist
|
|
if self._cut_slope_z(side, dist) - self._ground_at(signed) >= 0:
|
|
result = dist
|
|
break
|
|
dist += step
|
|
self._cut_cross[side] = result
|
|
return result
|
|
|
|
def fill_ground_slope(self) -> float | None:
|
|
"""성토측 **자연 지반**의 평균 경사(rise/run, 무차원). 성토측이 없으면 None.
|
|
|
|
자연방토 판정에 쓴다 — 지반이 가파르면 부어 놓은 흙이 쌓이지 않고 흘러내린다.
|
|
구간은 노면 끝(사면 시작)부터 성토 사면이 지반과 처음 만나는 곳까지이며, 끝까지
|
|
만나지 못하면 10m를 본다. 양쪽이 다 성토면 **완만한 쪽**을 택한다(보수적 판정).
|
|
"""
|
|
if self._ground_at is None:
|
|
return None
|
|
slopes: list[float] = []
|
|
for side in ("left", "right"):
|
|
role = self.left_role if side == "left" else self.right_role
|
|
if role != "fill":
|
|
continue
|
|
start_dist, _start_z = self._slope_start(side)
|
|
end_dist = self.fill_cross_dist(side) or (start_dist + 10.0)
|
|
run = end_dist - start_dist
|
|
if run <= 1e-6:
|
|
continue
|
|
sign = 1.0 if side == "left" else -1.0
|
|
rise = abs(self._ground_at(sign * end_dist) - self._ground_at(sign * start_dist))
|
|
slopes.append(rise / run)
|
|
return min(slopes) if slopes else None
|
|
|
|
def fill_cross_dist(self, side: str) -> float | None:
|
|
"""성토 사면이 지반선과 **처음** 만나는 거리(절대 오프셋). 이후는 성토 없음.
|
|
|
|
절토(cut_cross_dist)와 같은 규칙이다. 실제 지면은 울퉁불퉁해서 성토 사면이 지반과
|
|
여러 번 만날 수 있는데, **첫 교차점이 성토사면의 끝**이고 그 바깥은 손대지 않은
|
|
지반이다(2026-08-02 사용자 지시). `max(fill_line, ground)`만 쓰면 지반이 다시 꺼졌을 때
|
|
성토 사면이 되살아나 사면이 끊겼다 이어지는 그림이 나온다.
|
|
|
|
시작(노면 끝)부터 사면이 지반 아래면 교차거리=시작(성토 없음), 끝까지 못 만나면 None.
|
|
"""
|
|
if side in self._fill_cross:
|
|
return self._fill_cross[side]
|
|
result: float | None = None
|
|
if self._ground_at is not None:
|
|
start_dist, start_z = self._slope_start(side)
|
|
step = 0.05
|
|
dist = start_dist
|
|
max_dist = start_dist + 500.0
|
|
while dist <= max_dist:
|
|
signed = dist if side == "left" else -dist
|
|
fill_line = self._fill_slope_z(side, dist)
|
|
if fill_line - self._ground_at(signed) <= 0:
|
|
result = dist
|
|
break
|
|
dist += step
|
|
self._fill_cross[side] = result
|
|
return result
|
|
|
|
def design_z(self, offset_m: float, ground_m: float) -> float:
|
|
"""offset 하나의 설계 표고(사면은 지반 교차점 이후 지반 추종)."""
|
|
side = "left" if offset_m >= 0 else "right"
|
|
extent = self.left_extent if side == "left" else self.right_extent
|
|
if abs(offset_m) <= extent + 1e-9:
|
|
return self.road_z(offset_m)
|
|
# 측구 구간: 꼭짓점 사이 선형 보간(지반 무관 강제 굴착).
|
|
if side == self.ditch_side and self.ditch_points:
|
|
inner = abs(self.ditch_points[0][0])
|
|
outer = abs(self.ditch_points[-1][0])
|
|
if inner - 1e-9 <= abs(offset_m) <= outer + 1e-9:
|
|
points = self.ditch_points
|
|
for index in range(1, len(points)):
|
|
x0, z0 = abs(points[index - 1][0]), points[index - 1][1]
|
|
x1, z1 = abs(points[index][0]), points[index][1]
|
|
if abs(offset_m) > x1 + 1e-9:
|
|
continue
|
|
span = x1 - x0
|
|
if span <= 1e-9:
|
|
return z1
|
|
ratio = (abs(offset_m) - x0) / span
|
|
return z0 + (z1 - z0) * ratio
|
|
return points[-1][1]
|
|
role = self.left_role if side == "left" else self.right_role
|
|
dist = abs(offset_m)
|
|
if role == "cut":
|
|
# 지반과 1회 교차하면 그 이후 절토는 의미 없음 → 지반 추종(N-2-4).
|
|
cross = self.cut_cross_dist(side)
|
|
if cross is not None and dist >= cross:
|
|
return ground_m
|
|
return min(self._cut_slope_z(side, dist), ground_m)
|
|
# 지반과 1회 교차하면 그 바깥은 성토가 아니라 원지반이다(절토와 같은 규칙).
|
|
cross = self.fill_cross_dist(side)
|
|
if cross is not None and dist >= cross:
|
|
return ground_m
|
|
return max(self._fill_slope_z(side, dist), ground_m)
|
|
|
|
def breakpoints(self) -> list[float]:
|
|
"""적분·설계선에 반드시 포함할 설계 꼭짓점 오프셋 목록(2단계 무릎·소단 포함)."""
|
|
points = [0.0, self.left_extent, -self.right_extent]
|
|
points.extend(offset for offset, _z in self.ditch_points)
|
|
# 절토 사면 꼭짓점(무릎·소단 모서리) — 빠뜨리면 계단이 설계선에 안 실린다.
|
|
if self.two_stage or self.berm is not None:
|
|
for side in ("left", "right"):
|
|
role = self.left_role if side == "left" else self.right_role
|
|
if role != "cut":
|
|
continue
|
|
cross = self.cut_cross_dist(side)
|
|
for offset, _z in self.cut_points(side):
|
|
if cross is not None and offset > cross + 1e-9:
|
|
break # 지반과 만난 뒤는 절토가 없다
|
|
points.append(offset if side == "left" else -offset)
|
|
# 성토 사면 소단 모서리 — 절토와 같은 까닭으로 설계선에 실어야 계단이 그려진다.
|
|
if self.berm is not None:
|
|
for side in ("left", "right"):
|
|
role = self.left_role if side == "left" else self.right_role
|
|
if role != "fill":
|
|
continue
|
|
cross = self.fill_cross_dist(side)
|
|
for offset, _z in self.fill_points(side):
|
|
if cross is not None and offset > cross + 1e-9:
|
|
break
|
|
points.append(offset if side == "left" else -offset)
|
|
# 절·성토 사면과 지반의 **첫** 교차점을 꼭짓점에 넣어 면적 절단을 정확히 한다(N-2-4).
|
|
for side in ("left", "right"):
|
|
role = self.left_role if side == "left" else self.right_role
|
|
cross = self.cut_cross_dist(side) if role == "cut" else self.fill_cross_dist(side)
|
|
if cross is not None:
|
|
points.append(cross if side == "left" else -cross)
|
|
return points
|
|
|
|
|
|
def curve_widening_args(section: dict[str, Any] | None) -> dict[str, Any]:
|
|
"""측점 기록에서 곡선부 확폭 입력을 뽑는다 — `compute_cross_design(**...)` 로 넘긴다.
|
|
|
|
측점마다 실려 오는 값이라 호출자마다 따로 꺼내 쓰면 빠뜨리기 쉽다(2026-09-06).
|
|
옛 저장분에는 두 값이 없어 확폭 없이 예전과 같은 단면이 나온다.
|
|
"""
|
|
if not isinstance(section, dict):
|
|
return {"plan_radius_m": None, "curve_outer_side": None, "curve_widening_m": None}
|
|
return {
|
|
"plan_radius_m": section.get("plan_radius_m"),
|
|
"curve_outer_side": section.get("curve_outer_side"),
|
|
# 곡선 앞뒤 테이퍼가 얹힌 값 — 있으면 반경 표값 대신 이걸 쓴다(2026-09-06).
|
|
"curve_widening_m": section.get("curve_widening_m"),
|
|
}
|
|
|
|
|
|
def compute_cross_design(
|
|
samples: list[dict[str, Any]],
|
|
design_elevation_m: float | None,
|
|
*,
|
|
ground_type: str,
|
|
section_mode: str,
|
|
ditch_side: str | None = None,
|
|
ditch_type: str = "standard",
|
|
paved: bool = False,
|
|
standard: dict[str, Any] | None = None,
|
|
rock_boundary_offset_m: float | None = None,
|
|
two_stage_slope: bool = True,
|
|
cut_slope_ratio: float | None = None,
|
|
ditch_enabled: bool | None = None,
|
|
surface_drop_m: float = 0.0,
|
|
plan_radius_m: float | None = None,
|
|
curve_outer_side: str | None = None,
|
|
curve_widening_m: float | None = None,
|
|
berm: BermSpec | None = None,
|
|
) -> dict[str, Any]:
|
|
"""측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다.
|
|
|
|
samples: [{offset_m, elevation_m, valid}] 지반선 원시 샘플.
|
|
design_elevation_m: 중심선 계획고(노면고). None이면 계산 불가.
|
|
ditch_type: 일반(standard)/L형(l_type, 암 구간 전용).
|
|
paved: 포장 중첩 여부 — 횡단경사·포장층만 포장 그룹 값으로 바꾼다(기하는 지반유형).
|
|
standard: B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 순.
|
|
rock_boundary_offset_m: 암반 경계선 오프셋(지반선 기준, 음수=하향). 암 지반 2단계 절토용.
|
|
two_stage_slope: 암 지반에서 암반 경계 기준 2단계 경사 적용 여부(기본 True, 토글로 해제).
|
|
cut_slope_ratio: 이 측점만 쓰는 절토 경사비(1:n 의 n). 사용자가 카드에서 넣은 값이며
|
|
None 이면 표준 횡단면 설정값을 그대로 쓴다(2026-09-07 사용자 지시).
|
|
plan_radius_m: 이 측점의 평면 곡선반경(m). 곡선부 확폭(별표2 Ⅰ.2.나.(4))을 정하는
|
|
입력이며, None·45m 이상이면 확폭이 없다.
|
|
curve_outer_side: 곡선 **바깥쪽**("left"/"right"). 확폭은 그쪽으로만 붙는다
|
|
(2026-09-06 사용자 확정). 값이 없으면 확폭을 넣지 않는다.
|
|
surface_drop_m: 노면을 통째로 내리는 양(m) — 세월교 월류 높이. 구체 위 노면은 월류
|
|
높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로
|
|
횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자).
|
|
"""
|
|
if ground_type not in SECTION_GROUND_TYPE_PRESET:
|
|
raise ValueError(f"지원하지 않는 지반유형입니다: {ground_type}")
|
|
if section_mode not in SECTION_MODES:
|
|
raise ValueError(f"지원하지 않는 단면유형입니다: {section_mode}")
|
|
if ditch_type not in SECTION_DITCH_TYPES:
|
|
raise ValueError(f"지원하지 않는 측구 형식입니다: {ditch_type}")
|
|
if design_elevation_m is None:
|
|
raise ValueError("계획고(design_elevation_m)가 없어 횡단 설계를 계산할 수 없습니다.")
|
|
drop = max(float(surface_drop_m), 0.0)
|
|
design_elevation_m = float(design_elevation_m) - drop
|
|
|
|
preset_key = SECTION_GROUND_TYPE_PRESET[ground_type]
|
|
if ditch_type == "l_type" and preset_key != "rock":
|
|
raise ValueError("L형 측구는 암(리핑암/발파암) 구간에서만 선택할 수 있습니다.")
|
|
group = _resolve_group(preset_key, standard)
|
|
# 측점 하나만 다른 절토 경사(2026-09-07 사용자 지시) — 표준 그룹의 경사비만 갈아 끼운다.
|
|
# ⚠ 여기서 갈아야 아래 기하·소단이 전부 새 경사를 따른다. 계산이 끝난 뒤 결과에 값만
|
|
# 베껴 붙이면 설계선은 옛 경사로 그려지고 숫자만 새것이 되어 어긋난다.
|
|
if cut_slope_ratio is not None and float(cut_slope_ratio) > 0:
|
|
group = {**group, "cut_slope_ratio": float(cut_slope_ratio)}
|
|
paved_group = _resolve_group("paved", standard)
|
|
# 포장 중첩: 횡단경사와 포장층 두께만 포장 그룹을 따른다.
|
|
cross_slope_pct = paved_group["cross_slope_pct"] if paved else group["cross_slope_pct"]
|
|
resolved_ditch_side = _resolve_ditch_side(section_mode, ditch_side)
|
|
|
|
valid = sorted(
|
|
(
|
|
(float(s["offset_m"]), float(s["elevation_m"]))
|
|
for s in samples
|
|
if s.get("valid") is not False
|
|
and s.get("offset_m") is not None
|
|
and s.get("elevation_m") is not None
|
|
),
|
|
key=lambda pair: pair[0],
|
|
)
|
|
if len(valid) < 2:
|
|
raise ValueError("유효한 지반 샘플이 부족해 횡단 설계를 계산할 수 없습니다.")
|
|
|
|
ground_at = _ground_interpolator(valid)
|
|
# 2단계 절토는 암 프리셋에서만, 암반 경계 오프셋이 있을 때만 켠다.
|
|
enable_two_stage = (
|
|
preset_key == "rock" and two_stage_slope and rock_boundary_offset_m is not None
|
|
)
|
|
soil_cut_ratio = _resolve_group("soil", standard)["cut_slope_ratio"]
|
|
# 곡선부 확폭 — 표는 하한이고, 확폭을 더한 유효너비가 법정 상한(5m)을 넘지 않게 자른다.
|
|
# 저장된 확폭량(테이퍼 포함)이 있으면 그것을 쓰고, 없으면 반경 표값으로 되돌아간다.
|
|
if curve_outer_side in ("left", "right"):
|
|
widening = (
|
|
float(curve_widening_m)
|
|
if isinstance(curve_widening_m, (int, float))
|
|
else _curve_widening_m(plan_radius_m)
|
|
)
|
|
else:
|
|
widening = 0.0
|
|
if widening > 0.0:
|
|
room = max(CURVE_WIDENING_MAX_WIDTH_M - group["road_width_m"], 0.0)
|
|
widening = min(widening, room)
|
|
widening_left = widening if curve_outer_side == "left" else 0.0
|
|
widening_right = widening if curve_outer_side == "right" else 0.0
|
|
|
|
geometry = _SectionGeometry(
|
|
design_elevation_m=design_elevation_m,
|
|
group=group,
|
|
section_mode=section_mode,
|
|
ditch_side=resolved_ditch_side,
|
|
ditch_type=ditch_type,
|
|
cross_slope_pct=cross_slope_pct,
|
|
ground_at=ground_at,
|
|
soil_cut_ratio=soil_cut_ratio,
|
|
rock_boundary_offset_m=rock_boundary_offset_m,
|
|
two_stage_slope=enable_two_stage,
|
|
ditch_enabled=ditch_enabled,
|
|
widening_left_m=widening_left,
|
|
widening_right_m=widening_right,
|
|
berm=berm,
|
|
)
|
|
|
|
# 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야
|
|
# 측구 모서리·노면 끝이 잘리지 않아 면적과 설계선이 정확해진다.
|
|
min_offset, max_offset = valid[0][0], valid[-1][0]
|
|
merged: list[float] = [offset for offset, _z in valid]
|
|
merged.extend(point for point in geometry.breakpoints() if min_offset <= point <= max_offset)
|
|
merged = sorted(set(round(offset, 6) for offset in merged))
|
|
|
|
offsets: list[float] = []
|
|
grounds: list[float] = []
|
|
diffs: list[float] = []
|
|
design_line: list[dict[str, float]] = []
|
|
for offset_m in merged:
|
|
ground_m = ground_at(offset_m)
|
|
design_z = geometry.design_z(offset_m, ground_m)
|
|
offsets.append(offset_m)
|
|
grounds.append(ground_m)
|
|
diffs.append(ground_m - design_z)
|
|
design_line.append({"offset_m": round(offset_m, 4), "elevation_m": round(design_z, 4)})
|
|
|
|
# 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음 — 이중계상 방지).
|
|
cut_area, fill_area = _trapezoid_areas(offsets, diffs)
|
|
# 층따기 밑수(길이 m) — 성토부 아래 원지반이 1:4 보다 급한 구간의 지표면 길이.
|
|
# 면적(㎡)은 측점 사이를 평균단면적법으로 이어 B08 이 낸다(2026-09-09 사용자 확정).
|
|
bench_cut_length = _bench_cut_length(offsets, grounds, diffs)
|
|
fill_ground_slope = geometry.fill_ground_slope()
|
|
# 사면이 샘플 범위 끝에서도 원지반과 만나지 않으면 면적이 거기서 잘린다 — 그만큼
|
|
# 절·성토량이 실제와 다르고 유토곡선도 그 값을 그대로 쌓는다. 영원히 안 만나는
|
|
# 지형이 있을 수 있으므로 계산은 손대지 않고 **경고만** 낸다(2026-09-03 사용자 확정).
|
|
slope_unclosed = bool(diffs) and (
|
|
abs(diffs[0]) > _SLOPE_CLOSE_TOLERANCE_M or abs(diffs[-1]) > _SLOPE_CLOSE_TOLERANCE_M
|
|
)
|
|
|
|
# 절토면적 토사/암반 분리 — 지표면~암반 경계선이 토사, 그 아래가 암이다. 경계선 위치가
|
|
# 곧 유토곡선 EA/RR/BR 비율을 만들므로, 사용자가 경계선을 올리내리면 이 값이 함께 바뀐다.
|
|
# 토사 지반은 암반 경계선 자체가 없어 전량 토사, 암 지반인데 경계선 값이 없으면(구 데이터)
|
|
# 분리 근거가 없으므로 전량 암으로 둔다(기존 단일 지반유형 환산과 같은 결과).
|
|
if preset_key != "rock":
|
|
cut_soil_area, cut_rock_area = cut_area, 0.0
|
|
cut_rock_kind: str | None = None
|
|
elif rock_boundary_offset_m is None:
|
|
cut_soil_area, cut_rock_area = 0.0, cut_area
|
|
cut_rock_kind = ground_type
|
|
else:
|
|
cut_soil_area, cut_rock_area = _split_cut_areas(
|
|
offsets, diffs, abs(float(rock_boundary_offset_m))
|
|
)
|
|
cut_rock_kind = ground_type
|
|
|
|
# 측구 공칭 단면적(수량 산출 참고용): 일반=사다리꼴, L형=직각삼각형 근사.
|
|
if not geometry.has_ditch:
|
|
ditch_area = 0.0
|
|
ditch_spec: dict[str, Any] = {"type": "none"}
|
|
elif ditch_type == "l_type":
|
|
ditch_area = group["l_ditch_width_m"] * group["l_ditch_depth_m"] / 2.0
|
|
ditch_spec = {
|
|
"type": "l_type",
|
|
"width_m": group["l_ditch_width_m"],
|
|
"depth_m": group["l_ditch_depth_m"],
|
|
}
|
|
else:
|
|
ditch_area = (
|
|
(group["ditch_top_width_m"] + group["ditch_bottom_width_m"])
|
|
/ 2.0
|
|
* group["ditch_depth_m"]
|
|
)
|
|
ditch_spec = {
|
|
"type": "standard",
|
|
"top_width_m": group["ditch_top_width_m"],
|
|
"bottom_width_m": group["ditch_bottom_width_m"],
|
|
"depth_m": group["ditch_depth_m"],
|
|
}
|
|
|
|
# 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo한다(D-2, 표시·저장용).
|
|
if geometry.left_role == "cut" and geometry.right_role == "cut":
|
|
resolved_mode = "both_cut"
|
|
elif geometry.left_role == "fill" and geometry.right_role == "fill":
|
|
resolved_mode = "both_fill"
|
|
elif geometry.left_role == "cut":
|
|
resolved_mode = "left_cut"
|
|
else:
|
|
resolved_mode = "right_cut"
|
|
|
|
result: dict[str, Any] = {
|
|
"ground_type": ground_type,
|
|
"geometry_preset": preset_key,
|
|
"section_mode": resolved_mode,
|
|
"ditch_side": resolved_ditch_side,
|
|
"ditch_type": ditch_type if geometry.has_ditch else None,
|
|
"cut_slope_ratio": round(geometry.cut_ratio, 4),
|
|
"soil_cut_slope_ratio": round(geometry.soil_cut_ratio, 4),
|
|
# 사용자가 **켠 값**을 그대로 돌려준다 — 엔진이 실제로 적용했는지(`geometry.two_stage`)가
|
|
# 아니다(2026-09-07). 적용 결과를 저장하면, 토사 측점처럼 못 쓰는 자리에서 false 가
|
|
# 저장되고 그 false 가 다음 재계산 인자로 되먹여져 **사용자의 「켬」이 영구히 사라졌다.**
|
|
# 「실제로 적용됐나」를 읽는 곳은 코드 전체에 하나도 없다(2026-09-07 전수 확인) —
|
|
# 읽는 쪽은 모두 사용자 설정으로 쓴다. TS 짝: `common_util_cross_design.ts`.
|
|
"two_stage_slope": bool(two_stage_slope),
|
|
"fill_slope_ratio": round(geometry.fill_ratio, 4),
|
|
"roadbed_width_m": round(geometry.left_extent + geometry.right_extent, 4),
|
|
"carriageway_width_m": round(geometry.half_road_left + geometry.half_road_right, 4),
|
|
# 규격 폭과 확폭을 따로 남긴다 — 횡단도 라벨·수량 산출이 둘을 나눠 쓴다.
|
|
"carriageway_standard_width_m": round(group["road_width_m"], 4),
|
|
"widening_left_m": round(geometry.half_road_left - geometry.half_road, 4),
|
|
"widening_right_m": round(geometry.half_road_right - geometry.half_road, 4),
|
|
"cross_slope_pct": round(cross_slope_pct, 4),
|
|
"ditch": ditch_spec,
|
|
"ditch_enabled": bool(geometry.has_ditch),
|
|
"paved": bool(paved),
|
|
# 노면(노견 포함) 양 끝점 — 노면 렌더링 기준.
|
|
"road_edges": {
|
|
"left": {
|
|
"offset_m": round(geometry.left_extent, 4),
|
|
"elevation_m": round(geometry.road_z(geometry.left_extent), 4),
|
|
},
|
|
"right": {
|
|
"offset_m": round(-geometry.right_extent, 4),
|
|
"elevation_m": round(geometry.road_z(-geometry.right_extent), 4),
|
|
},
|
|
},
|
|
# 차도(노견 제외) 양 끝점 — 포장 범위 기준(D-5).
|
|
"carriageway_edges": {
|
|
"left": {
|
|
"offset_m": round(geometry.half_road_left, 4),
|
|
"elevation_m": round(geometry.road_z(geometry.half_road_left), 4),
|
|
},
|
|
"right": {
|
|
"offset_m": round(-geometry.half_road_right, 4),
|
|
"elevation_m": round(geometry.road_z(-geometry.half_road_right), 4),
|
|
},
|
|
},
|
|
"design_elevation_m": round(float(design_elevation_m), 4),
|
|
"cut_area_m2": round(cut_area, 4),
|
|
# 절토 내역(합=cut_area_m2). 유토곡선이 지반유형별 환산계수를 물리는 단위다.
|
|
"cut_soil_area_m2": round(cut_soil_area, 4),
|
|
"cut_rock_area_m2": round(cut_rock_area, 4),
|
|
"cut_rock_kind": cut_rock_kind,
|
|
"fill_area_m2": round(fill_area, 4),
|
|
# 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m).
|
|
# B08 이 측점 사이를 이어 ㎡ 로 만든다. 여기서 ㎥ 로 바꾸지 않는다 —
|
|
# 단의 높이·폭이 설계도서 값이라 지어낼 수 없다.
|
|
"bench_cut_length_m": round(bench_cut_length, 4),
|
|
# 사면이 샘플 범위 끝까지 원지반을 못 만나 면적이 잘린 측점 — 경고 표기용.
|
|
"slope_unclosed": slope_unclosed,
|
|
# 성토측 자연 지반 경사(rise/run) — 자연방토 판정 입력. 성토측이 없으면 None.
|
|
"fill_ground_slope": (
|
|
round(fill_ground_slope, 4) if fill_ground_slope is not None else None
|
|
),
|
|
"ditch_area_m2": round(ditch_area, 4),
|
|
"design_line": design_line,
|
|
# 절토 사면을 경사 구간별로 쪼갠 목록(소단 제외).
|
|
# ⚠ **지금 이 값을 읽는 곳은 없다**(2026-09-07). 원래 임자였던 별표2 법정 경사 검사는
|
|
# 폐기됐고(암질을 횡단도에서 안 고르기로 사용자 확정), 저장분(`cross_sections.data.design`
|
|
# 33키)에도 안 들어간다 — 저장되는 것은 화면이 보낸 설계 지정값이다.
|
|
# 그래도 남겨 두는 까닭: 소단 기하가 이 셈 위에 서 있고, 구간 경사를 아는 유일한 값이라
|
|
# 뒤에 쓸 자리가 있다. **되살릴 때는 저장분에서 읽지 말고 계산해서 쓸 것.**
|
|
"cut_slope_segments": geometry.cut_slope_segments(),
|
|
}
|
|
if drop > 0:
|
|
# 내려 앉힌 양 — 프론트가 "월류가 없었다면" 노면을 점선으로 되그리는 데 쓴다.
|
|
result["surface_drop_m"] = round(drop, 4)
|
|
if berm is not None:
|
|
# 소단 제원을 설계에 되싣는다 — 세션이 비어도(확정 뒤·다른 PC) 저장분만으로
|
|
# 계단이 다시 서야 한다. 암 경계선 오프셋을 echo 하는 것과 같은 까닭이다.
|
|
result["berm"] = {
|
|
"width_m": round(float(berm.width_m), 4),
|
|
"interval_m": round(float(berm.interval_m), 4),
|
|
"slope_deg": round(float(berm.slope_deg), 4),
|
|
}
|
|
if paved:
|
|
result["pavement_thickness_m"] = round(paved_group["pavement_thickness_m"], 4)
|
|
# 암 지반은 경계선 오프셋을 echo해 프론트가 세션값 없이도 오버레이·재계산에 쓰게 한다.
|
|
if preset_key == "rock" and rock_boundary_offset_m is not None:
|
|
result["rock_boundary_offset_m"] = round(float(rock_boundary_offset_m), 4)
|
|
return result
|