fix(횡단): 옹벽 의무 판정이 소단을 실제로 보게 함 — 부호·쪼개짐 두 곳 (계획서 3-9 ⑥)

앞 커밋으로는 화면 값이 안 바뀌어 실화면에서 두 가지를 더 찾았음.

① **TS 결과에 소단을 안 실었음** — 파이썬만 되싣고 브라우저 판을 빠뜨렸음.
   판정(`fillSlopeLengths`)은 브라우저에서 도는데 `design.berm` 이 없어 종전 식으로 갔음.

② **소단을 폭으로 찾으면 못 찾음** — 설계선 꼭짓점에는 지반 샘플(0.5m 격자)이 섞여 있어
   폭 0.5m 짜리 소단이 **두 도막으로 쪼개짐**(실측: 평탄부 0개). 게다가 설계선은 오프셋
   오름차순이라 **우측(음수) 성토면은 같은 소단이 `-기울기`로 나와** 부호를 그대로 대면
   역시 못 찾음.
   ⇒ **기울기로 가름.** 소단은 2°(0.035), 성토는 1:1.2~2.0(0.5~0.83)이라 성토 기울기의
   절반만 잡아도 확실히 갈림. 쪼개져도 부호가 뒤집혀도 걸림.

실화면 확인(8001·5174, `stale:false`) — 측점 2820.0m, 성토사면이 긴 자리:
· 소단 없음 → **≥32.54m** (옹벽·석축 의무 대상)
· 폭 0.5m·3m마다 놓음 → **≥3.00m** (5m 이내 — 의무 해소). 3.00m 은 소단 간격 그대로임
· 옆 측점(2840.0m, 안 놓음) → **≥32.54m 그대로**

**소단 없는 측점은 값이 안 바뀜 — 272측점 전수 대조, 달라진 곳 0.**
구조로도 보장됨(`design.berm` 이 없으면 종전 식을 그대로 탐).

전체 542 passed · 18 skipped. TS 타입 검사 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 16:59:48 +09:00
co-authored by Claude Opus 5
parent db4f17fb1c
commit c7770b7529
2 changed files with 21 additions and 5 deletions
+10 -5
View File
@@ -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;
}
+11
View File
@@ -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);