/* ============================================================================= * E03_StabSection_Support.ts * E03 보강 검토 네일 · 앵커 화면 계산(PLAN 536-3 뒷부분) — 화면 · fetch · sessionStorage 없음 · 순수 함수만. * 배치(행 수 · 첫 행 높이 · 수직 · 수평 간격 · 제원) → supports 목록(서버 POST whatif 입력 꼴 그대로) · * 네일 선 그림 좌표(머리 · 끝 · 정착 구간 · 임계 활동면과 만나는 점). * 좌표 = 서버 결과 그대로(x = B06 offset · 좌 + · z = 표고). 수평 방향 = 그 비탈 활동 방향 반대(비탈 속으로). * 높이 = 비탈 밑에서 위로 — 깎기 밑 = 노면(중심 표고) · 쌓기 밑 = 노면 − 비탈 높이. * ========================================================================== */ import type { Point } from "./E03_StabSection_Logic"; export type SupportType = "nail" | "anchor"; export type SupportMode = "tensile" | "pullout" | "stripping" | "not_crossing" | "prestress"; /** 서버 · 엔진 입력 꼴(브레인 정함 · sub4 엔진 같은 이름) */ export interface SupportIn { type: SupportType; side: string; x: number; z: number; /** 수평 아래로 + */ angle_deg: number; length_m: number; /** 종방향 간격 — 단위 폭 힘 = 힘 / 간격 */ spacing_m: number; tensile_kN: number; /** 그라우트 · 지반 부착(인발 안전율 넣은 값 · 엔진이 안 나눔) — 네일 = 전 길이 · 앵커 = 정착장만 */ bond_kPa: number; /** 앵커 정착장 · 네일은 무시 */ bond_length_m: number; hole_dia_mm: number; /** 앵커만 · 0 = 수동 */ prestress_kN: number; } export interface SupportForce { index: number; mobilized_kN_per_m: number; mode: SupportMode; } /** 화면 배치 입력 */ export interface SupportLayout { type: SupportType; rows: number; first_m: number; v_spacing_m: number; h_spacing_m: number; length_m: number; angle_deg: number; tensile_kN: number; bond_kPa: number; bond_length_m: number; hole_dia_mm: number; prestress_kN: number; } /** 기본값 후보(사용자 확정 전) — 지식DB 안정성평가 02 §4(천공 φ100 · 풍화토 주면마찰 50~100 kPa · * SD400 D25 허용 0.6 fy·As = 121.6 kN · 부착 = 극한 주면마찰 ÷ 인발 안전율 2.0) · * 앵커 = 강연선 φ12.7 × 4 허용 0.6 fpu = 441 kN · 풍화암 주면마찰 150 kPa ÷ 2.0 흔한 값 */ export const SUPPORT_DEFAULTS: Record = { nail: { type: "nail", rows: 3, first_m: 1.0, v_spacing_m: 1.5, h_spacing_m: 1.5, length_m: 6.0, angle_deg: 15, tensile_kN: 121.6, bond_kPa: 40, bond_length_m: 0, hole_dia_mm: 100, prestress_kN: 0, }, anchor: { type: "anchor", rows: 2, first_m: 1.5, v_spacing_m: 2.5, h_spacing_m: 2.5, length_m: 12.0, angle_deg: 30, tensile_kN: 441, bond_kPa: 75, bond_length_m: 6.0, hole_dia_mm: 115, prestress_kN: 200, }, }; /** 비탈 면 위 표고 z 인 점의 x — 노면 중심(x = 0)에서 그 쪽 바깥으로 처음 만나는 곳 */ export function faceX(surface: Point[], side: string, z: number): number | null { const s = side === "left" ? 1 : -1; const pts = surface .filter((p) => s * p[0] >= 0) .slice() .sort((a, b) => s * a[0] - s * b[0]); for (let i = 1; i < pts.length; i += 1) { const [a, b] = [pts[i - 1], pts[i]]; if ((a[1] - z) * (b[1] - z) <= 0 && a[1] !== b[1]) { return a[0] + ((z - a[1]) / (b[1] - a[1])) * (b[0] - a[0]); } } return null; } function roadZ(surface: Point[]): number | null { const pts = surface.slice().sort((a, b) => a[0] - b[0]); for (let i = 1; i < pts.length; i += 1) { const [a, b] = [pts[i - 1], pts[i]]; if (a[0] <= 0 && b[0] >= 0) return b[0] === a[0] ? a[1] : a[1] + (-a[0] / (b[0] - a[0])) * (b[1] - a[1]); } return null; } /** 배치 → supports — 행 높이 = 첫 행 + i × 수직 간격(비탈 높이 안만) · 머리 = 그 높이의 비탈 면 */ export function layoutSupports( surface: Point[], side: string, role: "cut" | "fill", heightM: number | null, layout: SupportLayout, ): SupportIn[] { const road = roadZ(surface); if (road === null) return []; const H = heightM ?? Infinity; const base = role === "cut" ? road : road - (heightM ?? 0); const out: SupportIn[] = []; for (let i = 0; i < Math.max(0, Math.floor(layout.rows)); i += 1) { const h = layout.first_m + i * layout.v_spacing_m; if (h <= 0 || h >= H) break; const z = base + h; const x = faceX(surface, side, z); if (x === null) break; out.push({ type: layout.type, side, x: round(x), z: round(z), angle_deg: layout.angle_deg, length_m: layout.length_m, spacing_m: layout.h_spacing_m, tensile_kN: layout.tensile_kN, bond_kPa: layout.bond_kPa, bond_length_m: layout.type === "anchor" ? layout.bond_length_m : 0, hole_dia_mm: layout.hole_dia_mm, prestress_kN: layout.type === "anchor" ? layout.prestress_kN : 0, }); } return out; } const round = (v: number): number => Math.round(v * 1000) / 1000; export interface SupportDraw { index: number; type: SupportType; head: Point; tail: Point; /** 부착 구간 — 네일 = 전 길이 · 앵커 = 끝에서 정착장 */ bond: [Point, Point]; /** 임계 활동면과 만나는 점(없으면 null) */ cross: Point | null; /** 결과 mode · 결과 없음 = "pending" */ mode: SupportMode | "pending"; force: number | null; } /** 두 선분 교점(a→b 위 비율 t 와 함께) */ function hit(a: Point, b: Point, c: Point, d: Point): [number, Point] | null { const r = [b[0] - a[0], b[1] - a[1]]; const q = [d[0] - c[0], d[1] - c[1]]; const den = r[0] * q[1] - r[1] * q[0]; if (Math.abs(den) < 1e-12) return null; const t = ((c[0] - a[0]) * q[1] - (c[1] - a[1]) * q[0]) / den; const u = ((c[0] - a[0]) * r[1] - (c[1] - a[1]) * r[0]) / den; if (t < 0 || t > 1 || u < 0 || u > 1) return null; return [t, [a[0] + t * r[0], a[1] + t * r[1]]]; } /** 선분이 꺾은선과 처음(머리 쪽) 만나는 점 */ export function crossPoint(head: Point, tail: Point, line: Point[]): Point | null { let best: [number, Point] | null = null; for (let i = 1; i < line.length; i += 1) { const h = hit(head, tail, line[i - 1], line[i]); if (h && (!best || h[0] < best[0])) best = h; } return best ? best[1] : null; } /** 그림 좌표 — direction = 그 비탈 활동 방향(+1 = +x 쪽으로 미끄러짐) */ export function supportDraws( supports: SupportIn[], direction: number, critical: Point[] | null, forces: SupportForce[] | null | undefined, ): SupportDraw[] { const into = direction >= 0 ? -1 : 1; return supports.map((s, index) => { const a = (s.angle_deg * Math.PI) / 180; const ux = into * Math.cos(a); const uz = -Math.sin(a); const head: Point = [s.x, s.z]; const at = (d: number): Point => [round(s.x + ux * d), round(s.z + uz * d)]; const tail = at(s.length_m); const bondFrom = s.type === "anchor" ? Math.max(0, s.length_m - s.bond_length_m) : 0; const f = forces?.find((v) => v.index === index) ?? null; return { index, type: s.type, head, tail, bond: [at(bondFrom), tail], cross: critical ? crossPoint(head, tail, critical) : null, mode: f ? f.mode : "pending", force: f ? f.mobilized_kN_per_m : null, }; }); }