feat(b06): 사면길이 3m 초과 소단 검토 정보 한 줄 — 성토사면 경고 밑 흐린 색 · 절·성토 둘 다 소단 사이 가장 긴 도막 · 벽 선 쪽 뺌 · 경고 아님(별표2 차.(5) 「붕괴 우려 지역」은 우리가 판정 못 함 · 브레인 ③)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-15 06:27:28 +09:00
co-authored by Claude Opus 5
parent 35c0a8203a
commit b1bdcb643d
5 changed files with 267 additions and 12 deletions
+63
View File
@@ -146,6 +146,69 @@ export function fillSlopeLengths(section: CrossSection): {
return lengths;
}
/**
* 좌·우 사면(**절토·성토 둘 다**)에서 소단 사이 **가장 긴 도막**의 경사길이(m) — 소단 검토 정보용
* (2026-09-15 브레인 ③). 구간·교차점은 `fillSlopeLengths` 와 같고, 절토는 2단 경사라 설계선
* 도막마다 경사길이를 더한다. 평탄한 도막(소단)에서 끊는다. 사면이 없는 측은 null.
*/
export function slopeRunLengths(section: CrossSection): {
left: FillSlopeLength | null;
right: FillSlopeLength | null;
} {
const lengths: { left: FillSlopeLength | null; right: FillSlopeLength | null } = {
left: null,
right: null,
};
const design = section.design;
if (!design) return lengths;
const groundAt = groundInterpolator(section.samples);
const designAt = designInterpolator(design.design_line);
const edges = design.road_edges;
if (!groundAt || !designAt || !edges) return lengths;
const lineOffsets = design.design_line.map((point) => point.offset_m);
if (lineOffsets.length < 2) return lengths;
const { protectMax, protectMin } = protectedSpan(design, edges);
// 소단(2°≈0.035)과 사면(1:0.3~2.0)을 가르는 기울기 — 가장 누운 사면 기울기의 절반.
const gradients = [design.fill_slope_ratio, design.cut_slope_ratio, design.soil_cut_slope_ratio]
.filter((ratio): ratio is number => typeof ratio === "number" && ratio > 0)
.map((ratio) => 1 / ratio);
const flatBelow = (gradients.length ? Math.min(...gradients) : 0.5) * 0.5;
for (const side of ["left", "right"] as const) {
const outward = side === "left" ? 1 : -1;
const start = side === "left" ? protectMax : protectMin;
const limit = side === "left" ? Math.max(...lineOffsets) : Math.min(...lineOffsets);
const meet = meetOffset(designAt, groundAt, start, limit, outward);
const end = Math.abs(meet) > Math.abs(limit) ? limit : meet;
const low = Math.min(start, end);
const high = Math.max(start, end);
let longest = 0;
let current = 0;
for (let index = 1; index < design.design_line.length; index += 1) {
const a = design.design_line[index - 1];
const b = design.design_line[index];
const run = Math.abs(b.offset_m - a.offset_m);
const from = Math.max(Math.min(a.offset_m, b.offset_m), low);
const to = Math.min(Math.max(a.offset_m, b.offset_m), high);
if (run <= 1e-9 || to - from <= 1e-9) continue;
const rise = Math.abs(b.elevation_m - a.elevation_m);
if (rise / run < flatBelow) {
longest = Math.max(longest, current);
current = 0;
continue;
}
current += ((to - from) / run) * Math.hypot(run, rise);
}
const lengthM = Math.max(longest, current);
if (lengthM > 0) {
lengths[side] = {
lengthM,
open: Math.abs(designAt(end) - groundAt(end)) > MEET_TOLERANCE_M,
};
}
}
return lengths;
}
/**
* 소단으로 끊긴 성토사면에서 **가장 긴 한 구간**의 경사길이(m).
*