feat(B06): 횡단 설계가 사토장 단면을 냄 — 노선 성토와 갈라서
사토장이 선 측점은 **노면 끝 바깥이 사토장 몫**이라 노선 성토에서 빼야 함.
안 빼면 같은 흙을 두 번 셈(2026-09-09 확정 ㉠).
- `compute_cross_design(spoil_fill=…)` / `computeCrossDesign({spoilFill})` 신설(짝)
- 새 칸: `spoil_fill_area_m2` · `spoil_fill_side` · `spoil_fill_width_m` ·
`spoil_fill_max_width_m` · `spoil_fill_line` · `spoil_fill_unclosed` ·
`spoil_fill_replaced_fill_m2`(노선 성토에서 뺀 몫 — 되짚기용)
- **합쳐서 하나로 내지 않음** — 받는 쪽이 갈라 볼 수 있어야 함
- 기울기가 비면 그 측점의 노선 성토 기울기를 그대로 씀(새 값 안 만듦)
- `fillAreaBeyond`/`_fill_area_beyond` — 경계 종거를 보간해 자름(한 칸도 안 흘림)
- 거울 시험에 사토장 사례 둘 추가 + 값이 0 이면 잡히는 가드
⚠ `B06_Section_Engine_Design.py` 가 924줄 — 700줄 제한 초과 상태임(이번 전에도 881줄).
기능이 다 선 뒤 분리할 자리.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -166,3 +166,37 @@ def _split_ditch_area(ditch_spec: dict, depth_to_boundary_m: float | None) -> tu
|
||||
soil = top * d0 - (top - bottom) * d0 * d0 / (2.0 * depth)
|
||||
return soil, max(total - soil, 0.0)
|
||||
return 0.0, 0.0
|
||||
|
||||
|
||||
def _fill_area_beyond(offsets: list[float], diffs: list[float], x0: float, side: str) -> float:
|
||||
"""`x0` **바깥쪽**(사토장이 서는 쪽)의 성토 면적(㎡)만 따로 낸다.
|
||||
|
||||
⚠ 왜 있나 — 사토장이 선 측점에서는 노면 끝 바깥이 **사토장 몫**이라 노선 성토
|
||||
(`fill_area_m2`)에서 빼야 한다. 안 빼면 **같은 흙을 두 번 센다**(2026-09-09 확정 ㉠).
|
||||
⚠ 경계(`x0`)의 종거는 **보간해서** 넣는다 — 그냥 버리면 경계 한 칸이 통째로 빠져
|
||||
값이 작아진다. 좌는 `x0` 위쪽, 우는 `x0` 아래쪽이며 **둘 다 오름차순**으로 넘긴다.
|
||||
|
||||
짝: TS `fillAreaBeyond`.
|
||||
"""
|
||||
if len(offsets) < 2:
|
||||
return 0.0
|
||||
inside = (lambda x: x >= x0) if side == "left" else (lambda x: x <= x0)
|
||||
sub_offsets: list[float] = []
|
||||
sub_diffs: list[float] = []
|
||||
for index, x in enumerate(offsets):
|
||||
if index > 0:
|
||||
x_prev = offsets[index - 1]
|
||||
crosses = (x_prev < x0 < x) or (x < x0 < x_prev)
|
||||
if crosses:
|
||||
ratio = (x0 - x_prev) / (x - x_prev)
|
||||
sub_offsets.append(x0)
|
||||
sub_diffs.append(diffs[index - 1] + (diffs[index] - diffs[index - 1]) * ratio)
|
||||
if inside(x):
|
||||
sub_offsets.append(x)
|
||||
sub_diffs.append(diffs[index])
|
||||
order = sorted(range(len(sub_offsets)), key=lambda i: sub_offsets[i])
|
||||
sub_offsets = [sub_offsets[i] for i in order]
|
||||
sub_diffs = [sub_diffs[i] for i in order]
|
||||
if len(sub_offsets) < 2:
|
||||
return 0.0
|
||||
return _trapezoid_areas(sub_offsets, sub_diffs)[1]
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import Any
|
||||
|
||||
from B06_Section.B06_Section_Engine_Areas import (
|
||||
_bench_cut_length,
|
||||
_fill_area_beyond,
|
||||
_split_cut_areas,
|
||||
_split_ditch_area,
|
||||
_trapezoid_areas,
|
||||
@@ -43,6 +44,7 @@ from common_util.common_util_cross_berm import (
|
||||
fill_profile_points,
|
||||
)
|
||||
from common_util.common_util_cross_berm import elevation_at as berm_elevation_at
|
||||
from common_util.common_util_spoil_fill import spoil_fill_section
|
||||
from config.config_system import (
|
||||
CURVE_WIDENING_MAX_WIDTH_M,
|
||||
SECTION_DITCH_SIDES,
|
||||
@@ -581,6 +583,7 @@ def compute_cross_design(
|
||||
curve_outer_side: str | None = None,
|
||||
curve_widening_m: float | None = None,
|
||||
berm: BermSpec | None = None,
|
||||
spoil_fill: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다.
|
||||
|
||||
@@ -600,6 +603,10 @@ def compute_cross_design(
|
||||
surface_drop_m: 노면을 통째로 내리는 양(m) — 세월교 월류 높이. 구체 위 노면은 월류
|
||||
높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로
|
||||
횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자).
|
||||
spoil_fill: 이 측점에 선 유용토운반작업장(구 사토장) — `{"side", "width_m", "slope_ratio_n"}`.
|
||||
폭은 **노면 끝**(노견이 시작하는 자리)에서 재고, 그 바깥 성토는 **노선 몫이 아니라
|
||||
사토장 몫**이라 `fill_area_m2` 에서 뺀다(2026-09-09 확정 ㉠ — 두 번 세지 않기).
|
||||
`slope_ratio_n` 이 비면 그 측점의 **노선 성토 기울기**를 그대로 쓴다.
|
||||
"""
|
||||
if ground_type not in SECTION_GROUND_TYPE_PRESET:
|
||||
raise ValueError(f"지원하지 않는 지반유형입니다: {ground_type}")
|
||||
@@ -710,6 +717,26 @@ def compute_cross_design(
|
||||
abs(diffs[0]) > _SLOPE_CLOSE_TOLERANCE_M or abs(diffs[-1]) > _SLOPE_CLOSE_TOLERANCE_M
|
||||
)
|
||||
|
||||
# 사토장(유용토운반작업장) — 노면 끝 바깥에 쌓는 성토. 짝: TS `computeCrossDesign`.
|
||||
# ⚠ 그 바깥 성토는 **노선 몫이 아니다** — 빼지 않으면 같은 흙을 두 번 센다(확정 ㉠).
|
||||
spoil_section = None
|
||||
spoil_replaced = 0.0
|
||||
spoil_side = str((spoil_fill or {}).get("side") or "")
|
||||
spoil_width = _as_float((spoil_fill or {}).get("width_m"), 0.0)
|
||||
if spoil_side in ("left", "right") and spoil_width > 0:
|
||||
spoil_x0 = geometry.half_road_left if spoil_side == "left" else -geometry.half_road_right
|
||||
spoil_ratio = _as_float((spoil_fill or {}).get("slope_ratio_n"), 0.0) or geometry.fill_ratio
|
||||
spoil_section = spoil_fill_section(
|
||||
valid,
|
||||
spoil_x0,
|
||||
geometry.road_z(spoil_x0),
|
||||
spoil_side,
|
||||
spoil_width,
|
||||
spoil_ratio,
|
||||
)
|
||||
spoil_replaced = _fill_area_beyond(offsets, diffs, spoil_x0, spoil_side)
|
||||
fill_area = max(fill_area - spoil_replaced, 0.0)
|
||||
|
||||
# 절토면적 토사/암반 분리 — 지표면~암반 경계선이 토사, 그 아래가 암이다. 경계선 위치가
|
||||
# 곧 유토곡선 EA/RR/BR 비율을 만들므로, 사용자가 경계선을 올리내리면 이 값이 함께 바뀐다.
|
||||
# 토사 지반은 암반 경계선 자체가 없어 전량 토사, 암 지반인데 경계선 값이 없으면(구 데이터)
|
||||
@@ -836,6 +863,22 @@ def compute_cross_design(
|
||||
"cut_rock_area_m2": round(cut_rock_area, 4),
|
||||
"cut_rock_kind": cut_rock_kind,
|
||||
"fill_area_m2": round(fill_area, 4),
|
||||
# 사토장 몫 — **`fill_area_m2` 와 합치지 않는다**(받는 쪽이 갈라 볼 수 있어야 한다).
|
||||
"spoil_fill_area_m2": round(spoil_section.area_m2, 4) if spoil_section else 0.0,
|
||||
"spoil_fill_side": spoil_side if spoil_section else None,
|
||||
"spoil_fill_width_m": round(spoil_width, 4) if spoil_section else 0.0,
|
||||
"spoil_fill_max_width_m": round(spoil_section.max_width_m, 4) if spoil_section else 0.0,
|
||||
"spoil_fill_line": (
|
||||
[
|
||||
{"offset_m": offset, "elevation_m": elevation}
|
||||
for offset, elevation in spoil_section.line
|
||||
]
|
||||
if spoil_section
|
||||
else []
|
||||
),
|
||||
"spoil_fill_unclosed": bool(spoil_section.unclosed) if spoil_section else False,
|
||||
# 사토장이 대신 차지해 노선 성토에서 뺀 몫(㎡) — 되짚기용. 합계에 또 넣지 말 것.
|
||||
"spoil_fill_replaced_fill_m2": round(spoil_replaced, 4),
|
||||
# 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m).
|
||||
# B08 이 측점 사이를 이어 ㎡ 로 만든다. 여기서 ㎥ 로 바꾸지 않는다 —
|
||||
# 단의 높이·폭이 설계도서 값이라 지어낼 수 없다.
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { BermSpec } from "./common_util_cross_berm";
|
||||
import type { SpoilFillSection } from "./common_util_spoil_fill";
|
||||
import { spoilFillSection } from "./common_util_spoil_fill";
|
||||
import {
|
||||
benchCutLength,
|
||||
fillAreaBeyond,
|
||||
splitCutAreas,
|
||||
splitDitchArea,
|
||||
trapezoidAreas,
|
||||
@@ -100,6 +103,13 @@ export interface CrossDesignOptions {
|
||||
curveWideningM?: number | null;
|
||||
/** 이 측점의 소단 제원 — 없으면 계단 없이 종전 사면 그대로(계획서 3-9). */
|
||||
berm?: BermSpec | null;
|
||||
/**
|
||||
* 이 측점에 선 유용토운반작업장(구 사토장). 폭은 **노면 끝**(노견이 시작하는 자리)에서
|
||||
* 재고, 그 바깥 성토는 **노선 몫이 아니라 사토장 몫**이라 `fill_area_m2` 에서 뺀다
|
||||
* (2026-09-09 확정 ㉠ — 두 번 세지 않기). `slopeRatioN` 이 비면 노선 성토 기울기를 쓴다.
|
||||
* 짝: 파이썬 `compute_cross_design(spoil_fill=…)`.
|
||||
*/
|
||||
spoilFill?: { side: "left" | "right"; widthM: number; slopeRatioN?: number | null } | null;
|
||||
}
|
||||
|
||||
export interface CrossDesignEdge {
|
||||
@@ -138,6 +148,15 @@ export interface CrossDesignResult {
|
||||
fill_area_m2: number;
|
||||
/** 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m). */
|
||||
bench_cut_length_m: number;
|
||||
/** 사토장 몫 — **`fill_area_m2` 와 합치지 않는다**(받는 쪽이 갈라 볼 수 있어야 한다). */
|
||||
spoil_fill_area_m2: number;
|
||||
spoil_fill_side: "left" | "right" | null;
|
||||
spoil_fill_width_m: number;
|
||||
spoil_fill_max_width_m: number;
|
||||
spoil_fill_line: CrossDesignEdge[];
|
||||
spoil_fill_unclosed: boolean;
|
||||
/** 사토장이 대신 차지해 노선 성토에서 뺀 몫(㎡) — 되짚기용. 합계에 또 넣지 말 것. */
|
||||
spoil_fill_replaced_fill_m2: number;
|
||||
slope_unclosed: boolean;
|
||||
fill_ground_slope: number | null;
|
||||
ditch_area_m2: number;
|
||||
@@ -355,7 +374,8 @@ export function computeCrossDesign(
|
||||
}
|
||||
|
||||
// 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음).
|
||||
const [cutArea, fillArea] = trapezoidAreas(offsets, diffs);
|
||||
const [cutArea, baseFillArea] = trapezoidAreas(offsets, diffs);
|
||||
let fillArea = baseFillArea;
|
||||
// 층따기 밑수(길이 m) — 성토부 아래 원지반이 1:4 보다 급한 구간의 지표면 길이.
|
||||
const benchCut = benchCutLength(offsets, grounds, diffs);
|
||||
const fillGroundSlope = geometry.fillGroundSlope();
|
||||
@@ -364,6 +384,28 @@ export function computeCrossDesign(
|
||||
(Math.abs(diffs[0]) > SLOPE_CLOSE_TOLERANCE_M ||
|
||||
Math.abs(diffs[diffs.length - 1]) > SLOPE_CLOSE_TOLERANCE_M);
|
||||
|
||||
// 사토장(유용토운반작업장) — 노면 끝 바깥에 쌓는 성토. 짝: 파이썬 `compute_cross_design`.
|
||||
// ⚠ 그 바깥 성토는 **노선 몫이 아니다** — 빼지 않으면 같은 흙을 두 번 센다(확정 ㉠).
|
||||
const spoilSide = options.spoilFill?.side ?? null;
|
||||
const spoilWidth = Math.max(Number(options.spoilFill?.widthM ?? 0), 0);
|
||||
let spoilSection: SpoilFillSection | null = null;
|
||||
let spoilReplaced = 0;
|
||||
if ((spoilSide === "left" || spoilSide === "right") && spoilWidth > 0) {
|
||||
const spoilX0 = spoilSide === "left" ? geometry.halfRoadLeft : -geometry.halfRoadRight;
|
||||
const askedRatio = Number(options.spoilFill?.slopeRatioN ?? 0);
|
||||
const spoilRatio = askedRatio > 0 ? askedRatio : geometry.fillRatio;
|
||||
spoilSection = spoilFillSection({
|
||||
ground: valid.map(([offset_m, elevation_m]) => ({ offset_m, elevation_m })),
|
||||
startOffsetM: spoilX0,
|
||||
startElevationM: geometry.roadZ(spoilX0),
|
||||
side: spoilSide,
|
||||
widthM: spoilWidth,
|
||||
slopeRatioN: spoilRatio,
|
||||
});
|
||||
spoilReplaced = fillAreaBeyond(offsets, diffs, spoilX0, spoilSide);
|
||||
fillArea = Math.max(fillArea - spoilReplaced, 0);
|
||||
}
|
||||
|
||||
// 절토면적 토사/암반 분리 — 지표면~암반 경계선이 토사, 그 아래가 암.
|
||||
let cutSoilArea: number;
|
||||
let cutRockArea: number;
|
||||
@@ -493,6 +535,18 @@ export function computeCrossDesign(
|
||||
cut_rock_area_m2: round4(cutRockArea),
|
||||
cut_rock_kind: cutRockKind,
|
||||
fill_area_m2: round4(fillArea),
|
||||
spoil_fill_area_m2: spoilSection ? round4(spoilSection.area_m2) : 0,
|
||||
spoil_fill_side: spoilSection ? spoilSide : null,
|
||||
spoil_fill_width_m: spoilSection ? round4(spoilWidth) : 0,
|
||||
spoil_fill_max_width_m: spoilSection ? round4(spoilSection.maxWidthM) : 0,
|
||||
spoil_fill_line: spoilSection
|
||||
? spoilSection.line.map((point) => ({
|
||||
offset_m: point.offset_m,
|
||||
elevation_m: point.elevation_m,
|
||||
}))
|
||||
: [],
|
||||
spoil_fill_unclosed: spoilSection ? spoilSection.unclosed : false,
|
||||
spoil_fill_replaced_fill_m2: round4(spoilReplaced),
|
||||
bench_cut_length_m: round4(benchCut),
|
||||
slope_unclosed: slopeUnclosed,
|
||||
fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope),
|
||||
|
||||
@@ -159,3 +159,46 @@ export function splitDitchArea(
|
||||
}
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* `x0` **바깥쪽**(사토장이 서는 쪽)의 성토 면적(㎡)만 따로 낸다.
|
||||
*
|
||||
* ⚠ 왜 있나 — 사토장이 선 측점에서는 노면 끝 바깥이 **사토장 몫**이라 노선 성토
|
||||
* (`fill_area_m2`)에서 빼야 한다. 안 빼면 **같은 흙을 두 번 센다**(2026-09-09 확정 ㉠).
|
||||
* ⚠ 경계(`x0`)에서 잘라 쓰므로 그 자리의 종거를 **보간해서** 넣는다 — 그냥 버리면
|
||||
* 경계 한 칸이 통째로 빠져 값이 작아진다.
|
||||
*
|
||||
* 짝: 파이썬 `_fill_area_beyond`.
|
||||
*/
|
||||
export function fillAreaBeyond(
|
||||
offsets: number[],
|
||||
diffs: number[],
|
||||
x0: number,
|
||||
side: "left" | "right",
|
||||
): number {
|
||||
if (offsets.length < 2) return 0;
|
||||
const inside = side === "left" ? (x: number) => x >= x0 : (x: number) => x <= x0;
|
||||
const subOffsets: number[] = [];
|
||||
const subDiffs: number[] = [];
|
||||
for (let index = 0; index < offsets.length; index += 1) {
|
||||
const x = offsets[index];
|
||||
if (index > 0) {
|
||||
const xPrev = offsets[index - 1];
|
||||
const crosses = (xPrev < x0 && x0 < x) || (x < x0 && x0 < xPrev);
|
||||
if (crosses) {
|
||||
const ratio = (x0 - xPrev) / (x - xPrev);
|
||||
subOffsets.push(x0);
|
||||
subDiffs.push(diffs[index - 1] + (diffs[index] - diffs[index - 1]) * ratio);
|
||||
}
|
||||
}
|
||||
if (inside(x)) {
|
||||
subOffsets.push(x);
|
||||
subDiffs.push(diffs[index]);
|
||||
}
|
||||
}
|
||||
const order = subOffsets.map((_, index) => index).sort((a, b) => subOffsets[a] - subOffsets[b]);
|
||||
const sortedOffsets = order.map((index) => subOffsets[index]);
|
||||
const sortedDiffs = order.map((index) => subDiffs[index]);
|
||||
if (sortedOffsets.length < 2) return 0;
|
||||
return trapezoidAreas(sortedOffsets, sortedDiffs)[1];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user