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:
2026-09-07 16:38:33 +09:00
co-authored by Claude Opus 5
parent a066023da9
commit db4f17fb1c
6 changed files with 207 additions and 12 deletions
+46
View File
@@ -132,6 +132,52 @@ def cut_profile_points(
return _dedupe(points)
def fill_profile_points(
start_dist: float,
start_z: float,
fill_ratio: float,
berm: BermSpec | None,
max_reach_m: float = _MAX_REACH_M,
) -> list[tuple[float, float]]:
"""성토 사면 꼭짓점 `[(거리, 표고), ...]` — 사면 시작에서 바깥으로 **내려간다**.
절토와 달리 경사가 하나뿐이라 무릎이 없다(암 경계는 절토만 본다). 소단은 같은 규칙 —
사면길이가 `interval_m` 에 닿을 때마다 폭 `width_m` 의 평탄부를 넣고, 그 평탄부는
안쪽이 낮도록 `slope_deg` 만큼 기울어 **바깥으로 갈수록 조금 올라간다**(물이 노면 쪽으로).
"""
points: list[tuple[float, float]] = [(start_dist, start_z)]
dist, elevation = start_dist, start_z
slant_since_berm = 0.0
limit = start_dist + max_reach_m
berm_rise = (
math.tan(math.radians(berm.slope_deg)) * berm.width_m
if berm is not None and berm.width_m > 0
else 0.0
)
ratio = max(fill_ratio, 1e-6)
while dist < limit:
drop = _STEP_M / ratio
slant = math.hypot(_STEP_M, drop)
if berm is not None and berm.interval_m > 0 and slant_since_berm + slant >= berm.interval_m:
remain = max(berm.interval_m - slant_since_berm, 0.0)
run = remain / math.hypot(1.0, 1.0 / ratio)
dist += run
elevation -= run / ratio
points.append((dist, elevation)) # 소단 안쪽 모서리
dist += berm.width_m
elevation += berm_rise
points.append((dist, elevation)) # 소단 바깥 모서리
slant_since_berm = 0.0
continue
dist += _STEP_M
elevation -= drop
slant_since_berm += slant
points.append((dist, elevation))
return _dedupe(points)
def _dedupe(points: list[tuple[float, float]]) -> list[tuple[float, float]]:
"""같은 자리 꼭짓점을 지운다 — 보간이 0 나눗셈을 만나지 않게."""
out: list[tuple[float, float]] = []