From 86437ce753bd4571bc2b5752a889eaf79620cbdf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:06:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EC=B8=B5=EB=94=B0=EA=B8=B0=20?= =?UTF-8?q?=EB=B0=91=EC=88=98=EB=A5=BC=20=ED=9A=A1=EB=8B=A8=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=EC=97=90=EC=84=9C=20=EB=83=84=20=E2=80=94=20=EC=84=B1?= =?UTF-8?q?=ED=86=A0=EB=B6=80=20=EC=95=84=EB=9E=98=20=EC=9B=90=EC=A7=80?= =?UTF-8?q?=EB=B0=98=20=EC=A7=80=ED=91=9C=EB=A9=B4=20=EA=B8=B8=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 확정(2026-09-09): 층따기 단위는 ㎡. 대신 그 면적이 B06 설계에서 나와야 한다. - `_bench_cut_length`: 성토(diff<0) 구간에서 원지반 횡단기울기가 **1:4 보다 급한** 조각만 골라 지표면을 따라간 길이(빗변)를 더한다. 절·성토 경계는 영교점까지만 센다. 근거는 별표2·교본 6장 4절, 지식DB 성토_비탈면 §4 [구현]. - 결과에 `bench_cut_length_m` 을 실어 B08 이 측점 사이를 평균단면적법으로 이어 ㎡ 를 낸다. ⚠ 여기서 ㎥ 로 바꾸지 않는다 — 단의 높이·폭은 설계도서 값이라 지어낼 수 없다. - 파이썬·TS 짝을 함께 고침(`common_util_cross_design_areas.ts`). 거울 테스트 통과. - ⚠ 성토 비탈면 길이가 아니라 **원지반 표면**을 센다 — 층따기 대상 면이 그쪽이다. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_Engine_Areas.py | 46 +++++++++++++++++++ B06_Section/B06_Section_Engine_Design.py | 10 ++++ common_util/common_util_cross_design.ts | 9 +++- common_util/common_util_cross_design_areas.ts | 31 +++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/B06_Section/B06_Section_Engine_Areas.py b/B06_Section/B06_Section_Engine_Areas.py index af308b3c..a3df5142 100644 --- a/B06_Section/B06_Section_Engine_Areas.py +++ b/B06_Section/B06_Section_Engine_Areas.py @@ -85,3 +85,49 @@ def _split_cut_areas( 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 diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index 1f26d92e..c4142964 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -32,6 +32,7 @@ from collections.abc import Callable from typing import Any from B06_Section.B06_Section_Engine_Areas import ( + _bench_cut_length, _split_cut_areas, _trapezoid_areas, ) @@ -684,17 +685,22 @@ def compute_cross_design( merged = sorted(set(round(offset, 6) for offset in merged)) offsets: list[float] = [] + grounds: list[float] = [] diffs: list[float] = [] design_line: list[dict[str, float]] = [] for offset_m in merged: ground_m = ground_at(offset_m) design_z = geometry.design_z(offset_m, ground_m) offsets.append(offset_m) + grounds.append(ground_m) diffs.append(ground_m - design_z) design_line.append({"offset_m": round(offset_m, 4), "elevation_m": round(design_z, 4)}) # 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음 — 이중계상 방지). cut_area, fill_area = _trapezoid_areas(offsets, diffs) + # 층따기 밑수(길이 m) — 성토부 아래 원지반이 1:4 보다 급한 구간의 지표면 길이. + # 면적(㎡)은 측점 사이를 평균단면적법으로 이어 B08 이 낸다(2026-09-09 사용자 확정). + bench_cut_length = _bench_cut_length(offsets, grounds, diffs) fill_ground_slope = geometry.fill_ground_slope() # 사면이 샘플 범위 끝에서도 원지반과 만나지 않으면 면적이 거기서 잘린다 — 그만큼 # 절·성토량이 실제와 다르고 유토곡선도 그 값을 그대로 쌓는다. 영원히 안 만나는 @@ -807,6 +813,10 @@ 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), + # 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m). + # B08 이 측점 사이를 이어 ㎡ 로 만든다. 여기서 ㎥ 로 바꾸지 않는다 — + # 단의 높이·폭이 설계도서 값이라 지어낼 수 없다. + "bench_cut_length_m": round(bench_cut_length, 4), # 사면이 샘플 범위 끝까지 원지반을 못 만나 면적이 잘린 측점 — 경고 표기용. "slope_unclosed": slope_unclosed, # 성토측 자연 지반 경사(rise/run) — 자연방토 판정 입력. 성토측이 없으면 None. diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index 6ebb2ec1..9eee140d 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -24,7 +24,7 @@ * ========================================================================== */ import type { BermSpec } from "./common_util_cross_berm"; -import { splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas"; +import { benchCutLength, splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas"; // 단면 기하(노면·측구·사면 설계고)는 파일이 700줄을 넘어 떼어냈다(2026-09-04). import { CURVE_WIDENING_MAX_WIDTH_M, @@ -131,6 +131,8 @@ export interface CrossDesignResult { cut_rock_area_m2: number; cut_rock_kind: string | null; fill_area_m2: number; + /** 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m). */ + bench_cut_length_m: number; slope_unclosed: boolean; fill_ground_slope: number | null; ditch_area_m2: number; @@ -331,18 +333,22 @@ export function computeCrossDesign( const merged = [...mergedSet].sort((a, b) => a - b); const offsets: number[] = []; + const grounds: number[] = []; const diffs: number[] = []; const designLine: CrossDesignEdge[] = []; for (const offsetM of merged) { const groundM = groundAt(offsetM); const designZ = geometry.designZ(offsetM, groundM); offsets.push(offsetM); + grounds.push(groundM); diffs.push(groundM - designZ); designLine.push({ offset_m: round4(offsetM), elevation_m: round4(designZ) }); } // 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음). const [cutArea, fillArea] = trapezoidAreas(offsets, diffs); + // 층따기 밑수(길이 m) — 성토부 아래 원지반이 1:4 보다 급한 구간의 지표면 길이. + const benchCut = benchCutLength(offsets, grounds, diffs); const fillGroundSlope = geometry.fillGroundSlope(); const slopeUnclosed = diffs.length > 0 && @@ -447,6 +453,7 @@ export function computeCrossDesign( cut_rock_area_m2: round4(cutRockArea), cut_rock_kind: cutRockKind, fill_area_m2: round4(fillArea), + bench_cut_length_m: round4(benchCut), slope_unclosed: slopeUnclosed, fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope), ditch_area_m2: round4(ditchArea), diff --git a/common_util/common_util_cross_design_areas.ts b/common_util/common_util_cross_design_areas.ts index 081cea98..83e6fb89 100644 --- a/common_util/common_util_cross_design_areas.ts +++ b/common_util/common_util_cross_design_areas.ts @@ -95,3 +95,34 @@ export function splitCutAreas( } return [soilArea, rockArea]; } + +/** 층따기 대상 판정 기울기 — 원지반 횡단기울기 1:4(=25%)보다 급한 곳에만 한다. + * 근거: 별표2 · 임도기술교본 6장 4절(「1:4보다 급한 경사를 가진 지반 위에 성토」). + * ⚠ 파이썬 짝: `B06_Section_Engine_Areas._BENCH_CUT_MIN_GROUND_SLOPE`. */ +export const BENCH_CUT_MIN_GROUND_SLOPE = 0.25; + +/** + * 층따기 밑수 — **성토부 아래 원지반 표면의 경사길이(m)**. + * ⚠ 파이썬 짝: `B06_Section_Engine_Areas._bench_cut_length`. 한 벌로 움직인다. + * 면적(㎡)은 측점 사이를 평균단면적법으로 이어 B08 이 낸다. + */ +export function benchCutLength(offsets: number[], grounds: number[], diffs: number[]): number { + let total = 0; + for (let index = 1; index < offsets.length; index += 1) { + const run = offsets[index] - offsets[index - 1]; + if (run <= 0) continue; + const d0 = diffs[index - 1]; + const d1 = diffs[index]; + if (d0 >= 0 && d1 >= 0) continue; + let share = 1; + if (d0 * d1 < 0) { + const zeroRatio = d0 / (d0 - d1); + share = d0 > 0 ? 1 - zeroRatio : zeroRatio; + } + if (share <= 0) continue; + const rise = grounds[index] - grounds[index - 1]; + if (Math.abs(rise) / run < BENCH_CUT_MIN_GROUND_SLOPE) continue; + total += Math.sqrt(run * run + rise * rise) * share; + } + return total; +}