222 lines
7.4 KiB
TypeScript
222 lines
7.4 KiB
TypeScript
/* =============================================================================
|
||
* 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<SupportType, SupportLayout> = {
|
||
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,
|
||
};
|
||
});
|
||
}
|