feat(횡단): 성토면 소단 + 옹벽 의무 판정을 「소단 사이 구간별 최대」로 (계획서 3-9 ⑥)
한 묶음으로 처리했음 — 화면에 5m 판정이 틀린 채 서 있는 시간이 없게. 성토 사면 꼭짓점 짝 신설(`fill_profile_points` · `fillProfilePoints`). 절토와 달리 경사가 하나뿐이라 무릎이 없고, 소단 규칙은 같음. 설계선·지반 교차·꼭짓점 목록이 모두 그 선을 봄. ⚠ **옹벽 의무 판정을 함께 고쳤음** — 「성토사면 길이 5m 이내, 넘으면 옹벽·석축 의무」 (`성토_비탈면.md` §2)를 재는 `fillSlopeLengths` 가 **「성토선은 1:n 직선」을 전제로 수평거리 × 기울기**로 재고 있었음(주석에도 그 전제가 적혀 있었음). 소단이 들어가면 그 전제가 깨져 값이 틀림. 고침 — 소단이 있으면 설계선을 걸어가며 **소단으로 끊긴 한 도막**의 최대 길이를 잼. · 전체를 한 줄로 재면 소단을 넣어도 5m 를 넘어 **의무가 사라지지 않음** · 실효 경사로 재면 완만해져 **의무가 사라진 것처럼** 보임 둘 다 틀리므로 「끊긴 한 도막」이 맞는 기준임. **소단이 없는 측점은 종전 식을 그대로 탐** — `design.berm` 이 없으면 예전 계산 그대로라 값이 한 톨도 안 바뀜(구조로 보장). 화면 전후 대조는 다음 단계에서 냄. 자체검증 — 새 시험 2건(성토면에 계단이 서고 같은 거리에서 덜 내려감 · 소단이 없으면 성토선이 곧은 한 줄). 전체 542 passed · 18 skipped. TS 타입 검사·ruff 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ from B06_Section.B06_Section_Engine_Areas import (
|
||||
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 (
|
||||
@@ -196,6 +197,7 @@ class _SectionGeometry:
|
||||
# 소단 제원(없으면 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] = {}
|
||||
@@ -313,6 +315,19 @@ class _SectionGeometry:
|
||||
"""절토 사면선 표고(무릎·소단 반영). 지반 교차 클램프는 하지 않는다."""
|
||||
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]]:
|
||||
"""절토 사면을 **경사 구간별로** 쪼갠 목록 — 법정 경사 검사가 읽는 값이다.
|
||||
|
||||
@@ -445,7 +460,7 @@ class _SectionGeometry:
|
||||
max_dist = start_dist + 500.0
|
||||
while dist <= max_dist:
|
||||
signed = dist if side == "left" else -dist
|
||||
fill_line = start_z - (dist - start_dist) / self.fill_ratio
|
||||
fill_line = self._fill_slope_z(side, dist)
|
||||
if fill_line - self._ground_at(signed) <= 0:
|
||||
result = dist
|
||||
break
|
||||
@@ -477,9 +492,7 @@ class _SectionGeometry:
|
||||
return z0 + (z1 - z0) * ratio
|
||||
return points[-1][1]
|
||||
role = self.left_role if side == "left" else self.right_role
|
||||
start_dist, start_z = self._slope_start(side)
|
||||
dist = abs(offset_m)
|
||||
run = dist - start_dist
|
||||
if role == "cut":
|
||||
# 지반과 1회 교차하면 그 이후 절토는 의미 없음 → 지반 추종(N-2-4).
|
||||
cross = self.cut_cross_dist(side)
|
||||
@@ -490,8 +503,7 @@ class _SectionGeometry:
|
||||
cross = self.fill_cross_dist(side)
|
||||
if cross is not None and dist >= cross:
|
||||
return ground_m
|
||||
fill_line = start_z - run / self.fill_ratio
|
||||
return max(fill_line, ground_m)
|
||||
return max(self._fill_slope_z(side, dist), ground_m)
|
||||
|
||||
def breakpoints(self) -> list[float]:
|
||||
"""적분·설계선에 반드시 포함할 설계 꼭짓점 오프셋 목록(2단계 무릎·소단 포함)."""
|
||||
@@ -508,6 +520,17 @@ class _SectionGeometry:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user