Files
Aislo/E03_StabSection/logic/E03_StabSection_Support.ts
T
eomsangdonandClaude Opus 5.5 8aeae00470 feat(536-3): E 보강 검토 뒷부분 — 네일 · 앵커 가정 입력 · supports 꼴 POST whatif · 최소 길이 찾기 · 횡단 그림 · 결과 표 · E05 네일 · 앵커 줄 (엔진 고리 전 = 그림만)
- logic/E03_StabSection_Support: 배치(행 수 · 첫 행 높이 · 수직 · 수평 간격 · 제원) → supports(브레인 정함 꼴 + side) · 머리 = 비탈 면 · 방향 = 활동 반대(비탈 속) · 그림 좌표(머리 · 끝 · 정착 구간 · 임계면 교점 · mode)
- UI_Support: 「네일 · 앵커 가정」 카드 · 기본값 후보(지식DB 안정성평가 02 §4 · 사용자 확정 전) · [보강 해석] · [최소 길이 찾기] · 동원력 · 지배 표 · 후 단면 mode 색
- Router_WhatIf: supports 받음 · 엔진에 supports 인자 있으면 넘김(cases[i].result.support_forces 읽음) · 없으면 supports_pending · 최소 길이 = 성긴 걸음 + 이분(0.5 m · 25 m 까지) · 지금 기울기 그대로면 원본 설계선 · 저장본 종류 support
- E05 s8: 1 절 가정 줄에 네일 · 앵커 · 3 절 네일마다 동원력 · 지배(있을 때만)
- 시험 test_536_3 14(배치 해석해 · 그림 좌표 · 엔진 전 그림만 · 가짜 엔진 최소 길이 5.0 m · E05 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:18:18 +09:00

221 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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";
/** 서버 · 엔진 입력 꼴(브레인 정함 · 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) · 앵커 = 강연선 φ12.7 × 4 허용 0.6 fpu = 441 kN 흔한 값 */
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: 80,
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: 150,
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,
};
});
}