diff --git a/B06_Section/B06_Section_Engine_Areas.py b/B06_Section/B06_Section_Engine_Areas.py index a3df5142..62220348 100644 --- a/B06_Section/B06_Section_Engine_Areas.py +++ b/B06_Section/B06_Section_Engine_Areas.py @@ -131,3 +131,38 @@ def _bench_cut_length(offsets: list[float], grounds: list[float], diffs: list[fl continue total += ((run**2 + rise**2) ** 0.5) * share return total + + +def _split_ditch_area(ditch_spec: dict, depth_to_boundary_m: float | None) -> tuple[float, float]: + """측구 단면적을 (토사, 암반)으로 가른다 — 암반 경계선까지의 깊이 기준. + + `depth_to_boundary_m` 은 **측구 상단에서 암반 경계선까지의 깊이(m)** 다. + `None` 이면 가를 근거가 없다는 뜻이라 부르는 쪽이 처리한다(여기서는 안 부른다). + + ⚠ 측구 단면은 **공칭 도형**(사다리꼴·L형 근사)이라 지반선을 따라 적분하지 않는다. + 경계선도 그 자리 한 높이로 본다 — 폭 1m 안팎에서 지반선 기울기 차이는 도형 근사보다 + 작다. 절토 면적 분리(`_split_cut_areas`)가 균일 두께를 쓰는 것과 같은 태도다. + """ + kind = str(ditch_spec.get("type") or "none") + if kind == "l_type": + width = float(ditch_spec.get("width_m") or 0.0) + depth = float(ditch_spec.get("depth_m") or 0.0) + total = width * depth / 2.0 + if depth <= 0 or width <= 0: + return 0.0, 0.0 + d0 = min(max(depth_to_boundary_m or 0.0, 0.0), depth) + # 깊이 d 에서의 가로 폭 = W(1 − d/D). 위에서 d0 까지 적분한다. + soil = width * d0 - width * d0 * d0 / (2.0 * depth) + return soil, max(total - soil, 0.0) + if kind == "standard": + top = float(ditch_spec.get("top_width_m") or 0.0) + bottom = min(float(ditch_spec.get("bottom_width_m") or 0.0), top) + depth = float(ditch_spec.get("depth_m") or 0.0) + total = (top + bottom) / 2.0 * depth + if depth <= 0 or top <= 0: + return 0.0, 0.0 + d0 = min(max(depth_to_boundary_m or 0.0, 0.0), depth) + # 깊이 d 에서의 폭 = top − (top−bottom)·d/depth. 위에서 d0 까지 적분한다. + soil = top * d0 - (top - bottom) * d0 * d0 / (2.0 * depth) + return soil, max(total - soil, 0.0) + return 0.0, 0.0 diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index c4142964..41beaaf8 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -34,6 +34,7 @@ from typing import Any from B06_Section.B06_Section_Engine_Areas import ( _bench_cut_length, _split_cut_areas, + _split_ditch_area, _trapezoid_areas, ) from common_util.common_util_cross_berm import ( @@ -749,6 +750,28 @@ def compute_cross_design( "depth_m": group["ditch_depth_m"], } + # 측구터파기 토사/암 분리 — **새 입력을 만들지 않는다.** 절토 분리와 같은 근거 + # (지반 유형 + 암반 경계선)를 그대로 쓴다. 별표2 Ⅰ.1.나.(5) 「측구터파기 단면적」이 + # 횡단도 표의 법정 칸이라 반만 채워 나가면 안 된다(2026-09-09). + # 근거가 없으면 **나누지 않고** 사유를 함께 내보낸다 — 절반을 임의로 가르지 않는다. + if not geometry.has_ditch: + ditch_soil_area, ditch_rock_area = 0.0, 0.0 + ditch_split_basis = "no_ditch" + elif preset_key != "rock": + # 토사 지반 — 암반 경계선 자체가 없다. 전량 토사(절토 분리와 같은 판정). + ditch_soil_area, ditch_rock_area = ditch_area, 0.0 + ditch_split_basis = "soil_ground" + elif rock_boundary_offset_m is None or not geometry.ditch_points: + # 암 지반인데 경계선 값이 없다(구 데이터) — 가를 근거가 없으므로 전량 암. + ditch_soil_area, ditch_rock_area = 0.0, ditch_area + ditch_split_basis = "rock_ground_no_boundary" + else: + ditch_top_z = geometry.ditch_points[0][1] + mid_offset = sum(point[0] for point in geometry.ditch_points) / len(geometry.ditch_points) + boundary_z = ground_at(mid_offset) - abs(float(rock_boundary_offset_m)) + ditch_soil_area, ditch_rock_area = _split_ditch_area(ditch_spec, ditch_top_z - boundary_z) + ditch_split_basis = "rock_boundary" + # 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo한다(D-2, 표시·저장용). if geometry.left_role == "cut" and geometry.right_role == "cut": resolved_mode = "both_cut" @@ -824,6 +847,12 @@ def compute_cross_design( round(fill_ground_slope, 4) if fill_ground_slope is not None else None ), "ditch_area_m2": round(ditch_area, 4), + # 측구터파기 내역(합=ditch_area_m2). 가른 근거는 `ditch_split_basis` 로 함께 낸다: + # rock_boundary(암반 경계선으로 가름) · soil_ground(토사 지반이라 전량 토사) · + # rock_ground_no_boundary(암 지반인데 경계선 없음 — 전량 암) · no_ditch(측구 없음). + "ditch_soil_area_m2": round(ditch_soil_area, 4), + "ditch_rock_area_m2": round(ditch_rock_area, 4), + "ditch_split_basis": ditch_split_basis, "design_line": design_line, # 절토 사면을 경사 구간별로 쪼갠 목록(소단 제외). # ⚠ **지금 이 값을 읽는 곳은 없다**(2026-09-07). 원래 임자였던 별표2 법정 경사 검사는 diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index 9eee140d..b011ac78 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -24,7 +24,12 @@ * ========================================================================== */ import type { BermSpec } from "./common_util_cross_berm"; -import { benchCutLength, splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas"; +import { + benchCutLength, + splitCutAreas, + splitDitchArea, + trapezoidAreas, +} from "./common_util_cross_design_areas"; // 단면 기하(노면·측구·사면 설계고)는 파일이 700줄을 넘어 떼어냈다(2026-09-04). import { CURVE_WIDENING_MAX_WIDTH_M, @@ -136,6 +141,10 @@ export interface CrossDesignResult { slope_unclosed: boolean; fill_ground_slope: number | null; ditch_area_m2: number; + /** 측구터파기 내역(합=ditch_area_m2)과 가른 근거. 짝: 파이썬 `ditch_split_basis`. */ + ditch_soil_area_m2: number; + ditch_rock_area_m2: number; + ditch_split_basis: string; design_line: CrossDesignEdge[]; /** 절토 사면 경사 구간(소단 제외). 짝: `cut_slope_segments`. * ⚠ **지금 읽는 곳은 없다**(2026-09-07) — 임자였던 별표2 검사는 폐기됐고 저장분에도 @@ -395,6 +404,37 @@ export function computeCrossDesign( }; } + // 측구터파기 토사/암 분리 — **새 입력을 만들지 않는다.** 절토 분리와 같은 근거 + // (지반 유형 + 암반 경계선)를 그대로 쓴다. 근거가 없으면 나누지 않고 사유를 낸다. + // ⚠ 파이썬 짝: `B06_Section_Engine_Design` 의 같은 자리. + let ditchSoilArea: number; + let ditchRockArea: number; + let ditchSplitBasis: string; + if (!geometry.hasDitch) { + ditchSoilArea = 0; + ditchRockArea = 0; + ditchSplitBasis = "no_ditch"; + } else if (presetKey !== "rock") { + ditchSoilArea = ditchArea; + ditchRockArea = 0; + ditchSplitBasis = "soil_ground"; + } else if ( + rockBoundaryOffsetM === null || + rockBoundaryOffsetM === undefined || + !geometry.ditchPoints.length + ) { + ditchSoilArea = 0; + ditchRockArea = ditchArea; + ditchSplitBasis = "rock_ground_no_boundary"; + } else { + const ditchTopZ = geometry.ditchPoints[0][1]; + const midOffset = + geometry.ditchPoints.reduce((sum, point) => sum + point[0], 0) / geometry.ditchPoints.length; + const boundaryZ = groundAt(midOffset) - Math.abs(rockBoundaryOffsetM); + [ditchSoilArea, ditchRockArea] = splitDitchArea(ditchSpec, ditchTopZ - boundaryZ); + ditchSplitBasis = "rock_boundary"; + } + // 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo 한다(D-2). let resolvedMode: string; if (geometry.leftRole === "cut" && geometry.rightRole === "cut") resolvedMode = "both_cut"; @@ -457,6 +497,9 @@ export function computeCrossDesign( slope_unclosed: slopeUnclosed, fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope), ditch_area_m2: round4(ditchArea), + ditch_soil_area_m2: round4(ditchSoilArea), + ditch_rock_area_m2: round4(ditchRockArea), + ditch_split_basis: ditchSplitBasis, design_line: designLine, cut_slope_segments: geometry.cutSlopeSegments(), }; diff --git a/common_util/common_util_cross_design_areas.ts b/common_util/common_util_cross_design_areas.ts index 83e6fb89..dfb41184 100644 --- a/common_util/common_util_cross_design_areas.ts +++ b/common_util/common_util_cross_design_areas.ts @@ -126,3 +126,36 @@ export function benchCutLength(offsets: number[], grounds: number[], diffs: numb } return total; } + +/** + * 측구 단면적을 [토사, 암반]으로 가른다 — 측구 상단에서 암반 경계선까지의 깊이(m) 기준. + * ⚠ 파이썬 짝: `B06_Section_Engine_Areas._split_ditch_area`. 한 벌로 움직인다. + * 측구 단면은 공칭 도형이라 지반선을 따라 적분하지 않는다 — 경계선도 그 자리 한 높이로 본다. + */ +export function splitDitchArea( + ditchSpec: Record, + depthToBoundaryM: number | null, +): [number, number] { + const kind = String(ditchSpec.type ?? "none"); + const clamp = (depth: number): number => Math.min(Math.max(depthToBoundaryM ?? 0, 0), depth); + if (kind === "l_type") { + const width = Number(ditchSpec.width_m ?? 0); + const depth = Number(ditchSpec.depth_m ?? 0); + if (depth <= 0 || width <= 0) return [0, 0]; + const total = (width * depth) / 2; + const d0 = clamp(depth); + const soil = width * d0 - (width * d0 * d0) / (2 * depth); + return [soil, Math.max(total - soil, 0)]; + } + if (kind === "standard") { + const top = Number(ditchSpec.top_width_m ?? 0); + const bottom = Math.min(Number(ditchSpec.bottom_width_m ?? 0), top); + const depth = Number(ditchSpec.depth_m ?? 0); + if (depth <= 0 || top <= 0) return [0, 0]; + const total = ((top + bottom) / 2) * depth; + const d0 = clamp(depth); + const soil = top * d0 - ((top - bottom) * d0 * d0) / (2 * depth); + return [soil, Math.max(total - soil, 0)]; + } + return [0, 0]; +}