"""B06 횡단 단면적 적분 — 절·성토 면적과 절토의 토사/암반 분리. ⚠⚠ TS 짝 파일과 **한 벌**이다 — 한쪽만 고치면 화면과 저장본이 갈린다 ⚠⚠ 짝: `common_util/common_util_cross_design_areas.ts` 회귀 테스트: `tmp/tests/test_b06_cross_design_mirror.py` (고칠 때 같이 돌릴 것). 두 벌인 이유는 `B06_Section_Engine_Design.py` 머리 참조. `_Engine_Design.py`가 700줄을 넘겨, 「설계선을 어떻게 세우나」(그쪽)와 「그 선과 지반 사이 넓이를 어떻게 재나」(여기)로 갈랐다. 두 함수 모두 지반선과 설계선의 **차이 배열**만 받으므로 설계 로직을 전혀 모른다 — 그래서 따로 떼어 검산하기도 쉽다. """ def _trapezoid_areas(offsets: list[float], diffs: list[float]) -> tuple[float, float]: """오프셋 순 (지반-설계) 차이를 사다리꼴 적분해 (절토, 성토) 면적을 반환한다. diff>0(지반이 설계보다 높음)=절토, diff<0=성토. 부호가 바뀌는 구간은 영교점에서 나눠 절·성토가 섞이지 않게 한다. """ cut_area = 0.0 fill_area = 0.0 for index in range(1, len(offsets)): x0, x1 = offsets[index - 1], offsets[index] d0, d1 = diffs[index - 1], diffs[index] width = x1 - x0 if width <= 0: continue if d0 == 0 and d1 == 0: continue if d0 * d1 < 0: # 부호 변화: 영교점에서 두 삼각형으로 분리 zero_ratio = d0 / (d0 - d1) x_zero = x0 + width * zero_ratio left_area = 0.5 * (x_zero - x0) * abs(d0) right_area = 0.5 * (x1 - x_zero) * abs(d1) if d0 > 0: cut_area += left_area fill_area += right_area else: fill_area += left_area cut_area += right_area continue area = 0.5 * (d0 + d1) * width if area >= 0: cut_area += area else: fill_area += -area return cut_area, fill_area def _split_cut_areas( offsets: list[float], diffs: list[float], soil_depth_m: float ) -> tuple[float, float]: """절토 면적을 암반 경계선 기준으로 (토사, 암반)으로 나눈다. 암반 경계선은 지반선 평행 복사(`지반고 + rock_boundary_offset_m`)이므로 토사층 두께 `t0`가 절토 구간 전체에서 균일하다. 따라서 오프셋별 절토 종거 `d = 지반고 - 설계고`에 대해 토사분은 `min(max(d, 0), t0)`, 암반분은 `max(d - t0, 0)`이며 두 값의 합은 항상 `max(d, 0)`이라 `_trapezoid_areas`의 절토 면적과 정확히 일치한다. 두 함수 모두 `d = 0`과 `d = t0`에서 꺾이므로 그 교차점을 구간 분할점으로 넣어야 사다리꼴 적분이 근사가 아닌 정확값이 된다. """ t0 = max(float(soil_depth_m), 0.0) soil_area = 0.0 rock_area = 0.0 for index in range(1, len(offsets)): x0, x1 = offsets[index - 1], offsets[index] d0, d1 = diffs[index - 1], diffs[index] width = x1 - x0 if width <= 0: continue ratios = [0.0, 1.0] for level in (0.0, t0): if (d0 - level) * (d1 - level) < 0: ratios.append((level - d0) / (d1 - d0)) ratios.sort() for step in range(1, len(ratios)): ratio_a, ratio_b = ratios[step - 1], ratios[step] span = width * (ratio_b - ratio_a) if span <= 0: continue d_a = d0 + (d1 - d0) * ratio_a d_b = d0 + (d1 - d0) * ratio_b soil_area += (min(max(d_a, 0.0), t0) + min(max(d_b, 0.0), t0)) / 2.0 * span rock_area += (max(d_a - t0, 0.0) + max(d_b - t0, 0.0)) / 2.0 * span return soil_area, rock_area # 층따기 대상 판정 기울기 — 원지반 횡단기울기 1:4(=25%)보다 급한 곳에만 한다. # 근거: 임도설치 및 관리 등에 관한 규정 별표2 · 임도기술교본 6장 4절 「경사지의 층따기에 # 있어 그 경사가 1:4보다 급한 경사를 가진 지반 위에 성토를 하는 경우 … 층따기를 설치」. # 지식DB `01_임도/02_상세설계/성토_비탈면.md` §4 [구현] 「원지반 횡단경사 > 25% 구간의 성토부」. _BENCH_CUT_MIN_GROUND_SLOPE = 0.25 def _bench_cut_length(offsets: list[float], grounds: list[float], diffs: list[float]) -> float: """층따기 밑수 — **성토부 아래 원지반 표면의 경사길이(m)**. 무엇을 재나 성토(diff<0)가 원지반에 얹히는 구간에서, 원지반 횡단기울기가 1:4 보다 급한 조각만 골라 **지표면을 따라간 길이**를 더한다. 수평 폭이 아니라 빗변이다 — 층따기는 그 경사면을 계단으로 깎는 일이라 대상 면이 곧 지표면이다. 왜 성토면이 아니라 원지반인가 층따기는 **원지반 표면**에 하는 것이다(교본 6장 4절). 성토 비탈면 길이로 재면 대상이 아닌 면을 세는 것이 된다. 단위 여기서 나오는 것은 **길이(m)** 다. 면적(㎡)은 측점 사이를 평균단면적법으로 이어 B08 이 낸다 — 사면 4계열과 같은 방식이라 계산을 두 벌로 짜지 않는다. (2026-09-09 사용자 확정: 층따기 단위는 ㎡.) """ total = 0.0 for index in range(1, len(offsets)): run = offsets[index] - offsets[index - 1] if run <= 0: continue d0, d1 = diffs[index - 1], diffs[index] # 성토 조각만 — 부호가 바뀌면 영교점까지만 성토다. if d0 >= 0 and d1 >= 0: continue share = 1.0 if d0 * d1 < 0: zero_ratio = d0 / (d0 - d1) share = (1.0 - zero_ratio) if d0 > 0 else zero_ratio if share <= 0: continue rise = grounds[index] - grounds[index - 1] if abs(rise) / run < _BENCH_CUT_MIN_GROUND_SLOPE: continue total += ((run**2 + rise**2) ** 0.5) * share return total