diff --git a/B06_Section/B06_Section_UI_Cross_Fit.ts b/B06_Section/B06_Section_UI_Cross_Fit.ts index af03913e..b5d09f6c 100644 --- a/B06_Section/B06_Section_UI_Cross_Fit.ts +++ b/B06_Section/B06_Section_UI_Cross_Fit.ts @@ -134,7 +134,12 @@ function longestFillRun( if (!berm) return Math.abs(endOffset - startOffset) * slant; const low = Math.min(startOffset, endOffset); const high = Math.max(startOffset, endOffset); - const bermRise = Math.tan((berm.slope_deg * Math.PI) / 180) * berm.width_m; + // 소단인지는 **기울기**로 가른다 — 폭으로 찾으면 못 찾는다. 설계선 꼭짓점에는 지반 + // 샘플(0.5m 격자)이 섞여 있어 폭 0.5m 짜리 소단이 두 도막으로 **쪼개지기** 때문이다 + // (2026-09-07 실측: 폭으로 찾으니 평탄부 0개). 소단 기울기는 2°(0.035)이고 성토 기울기는 + // 1:1.2~2.0(0.5~0.83)이라 절반만 잡아도 둘이 확실히 갈린다. + const fillGradient = 1 / Math.max(design.fill_slope_ratio, 1e-6); + const bermMaxGradient = fillGradient * 0.5; let longest = 0; let current = 0; const line = design.design_line; @@ -145,10 +150,10 @@ function longestFillRun( const to = Math.min(Math.max(a.offset_m, b.offset_m), high); if (to - from <= 1e-9) continue; // 이 도막은 사면 밖이다 const run = Math.abs(b.offset_m - a.offset_m); - const rise = b.elevation_m - a.elevation_m; - const isBerm = Math.abs(run - berm.width_m) < 1e-6 && Math.abs(rise - bermRise) < 1e-6; - if (isBerm) { - longest = Math.max(longest, current); + if (run <= 1e-9) continue; + const gradient = Math.abs(b.elevation_m - a.elevation_m) / run; + if (gradient < bermMaxGradient) { + longest = Math.max(longest, current); // 소단에서 도막이 끊긴다 current = 0; continue; } diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index 83f060b0..a3c5c58a 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -134,6 +134,8 @@ export interface CrossDesignResult { design_line: CrossDesignEdge[]; /** 절토 사면 경사 구간(소단 제외) — 법정 경사 검사가 읽는다. 짝: `cut_slope_segments`. */ cut_slope_segments: CutSlopeSegment[]; + /** 이 측점에 놓인 소단 제원 — 옹벽 의무 판정이 사면을 도막으로 끊는 데 쓴다. */ + berm?: { width_m: number; interval_m: number; slope_deg: number }; surface_drop_m?: number; pavement_thickness_m?: number; rock_boundary_offset_m?: number; @@ -440,6 +442,15 @@ export function computeCrossDesign( cut_slope_segments: geometry.cutSlopeSegments(), }; if (drop > 0) result.surface_drop_m = round4(drop); + if (options.berm) { + // 소단 제원을 설계에 되싣는다 — 짝 파이썬과 같은 까닭이고, **옹벽 의무 판정 + // (`fillSlopeLengths`)이 이 값을 보고** 사면을 도막으로 끊어 잰다(계획서 3-9). + result.berm = { + width_m: round4(options.berm.widthM), + interval_m: round4(options.berm.intervalM), + slope_deg: round4(options.berm.slopeDeg), + }; + } if (paved) result.pavement_thickness_m = round4(pavedGroup.pavement_thickness_m); if (presetKey === "rock" && rockBoundaryOffsetM !== null) { result.rock_boundary_offset_m = round4(rockBoundaryOffsetM);