/* ============================================================================= * B06_Section_Cut_Slope_Check.ts * 절토 비탈 **법정 기울기 검사**(별표2) — 순수 판정만 둔다(그리기는 카드 몫). * * 왜 필요한가 — 평면 곡선반경도, 성토사면 길이(5m·기슭막이 의무)도 위반 표시가 있는데 * **절토 경사만 검사가 없었다**(2026-09-07). 절토 경사비는 표준 횡단면 설정에서 오는 * 입력값일 뿐이라 별표2 범위를 벗어나도 아무 표시가 없었다. * * ⚠ **실효 경사로 보면 안 된다.** 소단이 서면 사면 전체를 하나로 잰 경사가 완만해져 * **위반이 사라진 것처럼** 보인다(실측: 폭 1.0·간격 2 이면 설계 1:1 이 실효 1:1.71). * 그래서 `design.cut_slope_segments`(소단을 뺀 **구간별** 경사)를 읽는다. * * 기준값·매핑은 **서버가 준 값**을 쓴다(config 상수를 화면에 복제하지 않는다). * ========================================================================== */ import type { CrossSection, SectionContextResponse } from "./B06_Section_Api_Fetch"; /** 별표2 한 줄 — 수직 1 에 대한 수평(1:n 의 n) 범위. */ export interface CutSlopeLimit { min: number; max: number; } export interface CutSlopeCriteria { /** 별표2 범위표 — `hard_rock`·`soft_rock`·`soil`. */ limits: Record; /** 지반유형(리핑암·발파암) → 별표2 줄. 사용자가 바꿀 수 있는 **설정값**이다. */ classOf: Record; /** 절토 기울기 규정이 없는 등급(작업임도). */ exemptGrades: ReadonlyArray; } export interface CutSlopeViolation { side: string; ratio: number; /** 어느 별표2 줄로 판정했나 — 툴팁에 적는다. */ limitKey: string; limit: CutSlopeLimit; /** 급한 쪽 위반인지(`steep`) 완만한 쪽 위반인지(`gentle`). */ kind: "steep" | "gentle"; startOffsetM: number; endOffsetM: number; } /** 서버 컨텍스트에서 기준을 꺼낸다. 값이 안 왔으면 null — 검사를 하지 않는다. */ export function criteriaFrom(context: SectionContextResponse | null): CutSlopeCriteria | null { const limits = context?.cut_slope_limits; if (!limits || !Object.keys(limits).length) return null; const table: Record = {}; for (const [key, pair] of Object.entries(limits)) { if (Array.isArray(pair) && pair.length >= 2) table[key] = { min: pair[0], max: pair[1] }; } return { limits: table, classOf: context?.cut_slope_class_default ?? {}, exemptGrades: context?.cut_slope_exempt_grades ?? [], }; } /** * 이 측점의 절토 구간이 별표2 범위를 벗어났는지. * * · 등급이 면제(작업임도)면 빈 목록. * · 구간이 없으면(성토 측점 등) 빈 목록. * · 암 구간은 측점의 `cut_rock_kind` 로 별표2 줄을 고른다 — 구간에는 `rock`/`soil` 만 있다. */ export function cutSlopeViolations( section: CrossSection, criteria: CutSlopeCriteria | null, gradeClass: string | null, ): CutSlopeViolation[] { if (!criteria) return []; if (gradeClass && criteria.exemptGrades.includes(gradeClass)) return []; const design = section.design as { cut_slope_segments?: unknown; cut_rock_kind?: string | null } | undefined; const segments = design?.cut_slope_segments; if (!Array.isArray(segments) || !segments.length) return []; const rockKey = criteria.classOf[design?.cut_rock_kind ?? ""] ?? "hard_rock"; const out: CutSlopeViolation[] = []; for (const raw of segments) { const segment = raw as { side?: string; ratio?: number; material?: string | null; start_offset_m?: number; end_offset_m?: number; }; const ratio = segment.ratio; if (typeof ratio !== "number" || !Number.isFinite(ratio)) continue; const limitKey = segment.material === "rock" ? rockKey : "soil"; const limit = criteria.limits[limitKey]; if (!limit) continue; // 경계값은 통과 — 1:0.8 은 「0.3~0.8」 안이다. if (ratio >= limit.min - 1e-9 && ratio <= limit.max + 1e-9) continue; out.push({ side: segment.side ?? "", ratio, limitKey, limit, kind: ratio < limit.min ? "steep" : "gentle", startOffsetM: segment.start_offset_m ?? 0, endOffsetM: segment.end_offset_m ?? 0, }); } return out; } /** 별표2 줄 이름 — 화면·툴팁에 적는 말. */ export function limitLabel(key: string): string { if (key === "hard_rock") return "경암"; if (key === "soft_rock") return "연암"; return "토사"; } /* ── 기억해 둔 기준 ────────────────────────────────────────────────────────── * 카드는 그리는 자리마다 컨텍스트를 들고 있지 않다. 표준단면 기본값을 기억해 두는 것과 * 같은 방식으로(`rememberStandardDefaults`) 여기 한 번 담아 두고 카드가 읽는다. * ------------------------------------------------------------------------ */ let remembered: CutSlopeCriteria | null = null; let rememberedGrade: string | null = null; export function rememberCutSlopeCriteria(context: SectionContextResponse | null): void { remembered = criteriaFrom(context); rememberedGrade = context?.road_type ?? null; } /** 사용자가 고른 매핑(리핑암·발파암 → 별표2 줄)을 얹는다. 없으면 기본값 그대로. */ export function applyCutSlopeClassChoice(choice: Record | null | undefined): void { if (!remembered || !choice) return; remembered = { ...remembered, classOf: { ...remembered.classOf, ...choice } }; } /** 지금 측점의 위반 목록 — 기준이 없거나 면제 등급이면 빈 목록. */ export function violationsOf(section: CrossSection): CutSlopeViolation[] { return cutSlopeViolations(section, remembered, rememberedGrade); }