Files
Aislo/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Engine_Areas.py
T
eomsangdonandClaude Opus 5 b0fe443269 feat(B06): 자연방토 반영 — 사토를 자연방토/사토장 운반으로 가름
판정 기준은 성토측 자연 지반 경사(사용자 확정). 지반이 임계보다 가파르면 부어 놓은 흙이
쌓이지 않고 스스로 흘러내리므로 운반비를 세지 않는다.

계상 방식은 참고 도면을 근거로 정했다. 도면의 사토 balloon은 L=(운반거리) 없이
M.N=측점만 적는다 - 그 노선의 사토가 이미 운반비 없이 처리되고 있다는 뜻이고,
자연방토라는 이름을 안 썼을 뿐 처리 방식이 자연방토다. 그래서 새 항목을 만들지 않고
사토 안에서 가른다. 판정 통과분은 도면과 똑같이 M.N만, 실패분에만 "사토장=X㎥"를 더한다.
수량은 그대로 두고 성토로 흡수하지 않아 유토곡선 모양과 누가토량은 안 바뀐다.

- config NATURAL_SPOIL_MIN_GROUND_SLOPE = 1/1.5 (rise/run, 약 33.7도) 신설.
  유일한 정의처이며 프론트는 context로 받아 쓴다. 못 받으면 자연방토 없음(보수적)
- 엔진 fill_ground_slope(): 노면 끝~성토 사면의 첫 지반 교차까지 평균 경사.
  양쪽이 다 성토면 완만한 쪽을 택한다(보수적)
- AreaSample/MassHaulPoint에 natural_spoil - 양 끝 측점이 둘 다 가능할 때만 참
- HaulResidual.natural_m3 - 잔량 구간에서 자연방토 가능한 길이 비율만큼 계상.
  장거리 상쇄로 깎이면 안분·자연방토도 같은 비율로 감소
- HaulPlan.natural_spoil_m3 + 요약 칩

700줄 초과 2건 분리: _Engine_Areas.py(면적 적분), _UI_MassHaul_Settle.ts(안분·장비·상쇄·
직렬화). 의존은 한 방향(Balance -> Settle 함수, Settle -> Balance 타입만)이라 순환 없음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:18:08 +09:00

83 lines
3.5 KiB
Python

"""B06 횡단 단면적 적분 — 절·성토 면적과 절토의 토사/암반 분리.
`_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