feat(B06): 측구터파기 단면적을 토사/암으로 가름 — 새 입력 없이 암반 경계선으로

별표2 Ⅰ.1.나.(5) 「측구터파기 단면적」이 횡단도 표의 법정 칸인데 한 값뿐이라
「측구 토사 / 측구 암석」 두 칸이 반만 채워졌다.

- 가르는 근거는 **절토 분리와 같은 것**(지반 유형 + 암반 경계선). 새 입력을 만들지 않았다.
- 측구 상단에서 암반 경계선까지의 깊이로 공칭 도형(사다리꼴·L형)을 가로로 가른다.
- ⚠ 근거가 없으면 **나누지 않는다.** 사유를 `ditch_split_basis` 로 함께 냄:
  rock_boundary / soil_ground / rock_ground_no_boundary / no_ditch.
- 기존 `ditch_area_m2` 는 **합계로 그대로** 두고 갈래를 덧붙였다 — B08 이 순서대로 옮겨 갈 수 있게.
- 파이썬·TS 짝을 함께 고침. 거울 테스트 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 19:22:58 +09:00
co-authored by Claude Opus 5
parent f60fb9c355
commit 6a7c339f8a
4 changed files with 141 additions and 1 deletions
+44 -1
View File
@@ -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(),
};
@@ -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<string, unknown>,
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];
}