Files
Aislo/E04_Stab3D/logic/E04_Stab3D_Raster.ts
T

216 lines
7.1 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.
/* =============================================================================
* E04_Stab3D_Raster.ts
* E04 3D 해석 화면 계산(PLAN 533-3) — 래스터 꼴 · 색 단계 · 범례 · 셀 찾기 · 3D/2D 비.
* 래스터 꼴 = 서버 `E04_Stab3D_Run.py` 머리 주석(grid · z · f · v · m · best).
* 순수 함수만(DOM · three · fetch 금지).
* ========================================================================== */
export interface Grid {
x0: number;
y0: number;
dx: number;
nx: number;
ny: number;
}
/** 전체 최소 활동면 — 구(ratio 1) · 타원체(회전축 방향 반축 = ratio·r · 활동 방향에 수직) */
export interface SlipBest {
x: number;
y: number;
z: number;
r: number;
ratio: number;
azimuth_deg: number; // 활동 방향(북 0° · 시계)
f: number;
volume: number;
method: number;
ix: number;
iy: number;
/** 못 뚫는 층에 잘린 셀 — [셀 번호, 층 윗면] · 활동면을 그 높이에서 자름 */
floor?: [number, number][];
/** 활동체 덩이 셀 번호(엔진 · 한 구가 자른 여러 덩이 중 이 덩이) — 없으면(옛 저장본) 잘린 셀 전부 */
cells?: number[];
}
export interface Summary3d {
start_m: number;
end_m: number;
f_min: number;
volume: number;
method: string;
chainage_m: number;
cells: number;
trials: number;
condition: string;
reference_fs: number;
verdict: "ok" | "ng";
/** 같은 임계면의 방법별 안전율 */
by_method: { hovland: number; bishop: number; janbu_corrected: number };
}
export interface Result3d {
key: string;
stale: boolean;
computed_at: string;
elapsed_s: number;
settings: Record<string, unknown>;
grid: Grid;
z: (number | null)[];
f: (number | null)[];
v: (number | null)[];
m: (number | null)[];
best: SlipBest;
summary: Summary3d;
}
export interface ColorStep {
/** 이 값 미만이면 이 색(마지막은 Infinity) */
below: number;
color: string;
}
/** 안전율 색 단계 — 1.3(우기) · 1.5(건기) = KDS 11 70 05 기준 경계 · 붉을수록 위험 */
export const FS_STEPS: readonly ColorStep[] = [
{ below: 1.0, color: "#a50026" },
{ below: 1.2, color: "#d73027" },
{ below: 1.3, color: "#f46d43" },
{ below: 1.5, color: "#fdae61" },
{ below: 2.0, color: "#fee08b" },
{ below: 3.0, color: "#a6d96a" },
{ below: Infinity, color: "#1a9850" },
];
/** 임계 체적 색 단계(m³ · 10 배씩) — 짙을수록 큼 */
export const VOLUME_STEPS: readonly ColorStep[] = [
{ below: 10, color: "#fde725" },
{ below: 100, color: "#5ec962" },
{ below: 1000, color: "#21918c" },
{ below: 10000, color: "#3b528b" },
{ below: Infinity, color: "#440154" },
];
export const NO_DATA_COLOR = "#9aa3ad";
/** 값 → 단계 번호(null · NaN = -1) */
export function stepIndex(steps: readonly ColorStep[], value: number | null): number {
if (value === null || !Number.isFinite(value)) return -1;
return steps.findIndex((step) => value < step.below);
}
export function stepColor(steps: readonly ColorStep[], value: number | null): string {
const index = stepIndex(steps, value);
return index < 0 ? NO_DATA_COLOR : steps[index].color;
}
/** "#rrggbb" → [r, g, b] 0~1 */
export function hexToRgb(hex: string): [number, number, number] {
const n = Number.parseInt(hex.slice(1), 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}
const fmt = (value: number, digits: number): string =>
value >= 1000 ? value.toLocaleString("en-US") : value.toFixed(digits);
/** 범례 줄 — "< 1.0" · "1.0 – 1.2" · … · "≥ 3.0" */
export function legendRows(
steps: readonly ColorStep[],
digits: number,
): { color: string; label: string }[] {
return steps.map((step, index) => {
const low = index === 0 ? null : steps[index - 1].below;
const label =
low === null
? `< ${fmt(step.below, digits)}`
: step.below === Infinity
? `≥ ${fmt(low, digits)}`
: `${fmt(low, digits)} – ${fmt(step.below, digits)}`;
return { color: step.color, label };
});
}
/** 셀 (ix, iy) 중심 모델 좌표 */
export function cellCenter(grid: Grid, ix: number, iy: number): { x: number; y: number } {
return { x: grid.x0 + ix * grid.dx, y: grid.y0 + iy * grid.dx };
}
/** 모델 좌표 → 셀(격자 밖 = null) */
export function cellAt(grid: Grid, x: number, y: number): { ix: number; iy: number } | null {
const ix = Math.round((x - grid.x0) / grid.dx);
const iy = Math.round((y - grid.y0) / grid.dx);
return ix < 0 || iy < 0 || ix >= grid.nx || iy >= grid.ny ? null : { ix, iy };
}
/** 래스터 최소 셀(값 없으면 null) */
export function rasterMin(
grid: Grid,
values: readonly (number | null)[],
): { ix: number; iy: number; value: number } | null {
let best = -1;
values.forEach((value, index) => {
if (value !== null && (best < 0 || value < (values[best] as number))) best = index;
});
return best < 0
? null
: { ix: best % grid.nx, iy: Math.floor(best / grid.nx), value: values[best] as number };
}
/** 값이 있는 셀 수 */
export function countCells(values: readonly (number | null)[]): number {
return values.reduce<number>((n, value) => (value === null ? n : n + 1), 0);
}
/** 래스터 → RGBA(줄 0 = 북쪽 끝 · 캔버스 그림용) · 값 없는 셀 = 투명 */
export function rasterRgba(
grid: Grid,
values: readonly (number | null)[],
steps: readonly ColorStep[],
): Uint8ClampedArray<ArrayBuffer> {
const out = new Uint8ClampedArray(grid.nx * grid.ny * 4);
const rgb = steps.map((step) => hexToRgb(step.color).map((c) => Math.round(c * 255)));
for (let iy = 0; iy < grid.ny; iy += 1) {
const row = grid.ny - 1 - iy;
for (let ix = 0; ix < grid.nx; ix += 1) {
const index = stepIndex(steps, values[iy * grid.nx + ix]);
if (index < 0) continue;
const at = (row * grid.nx + ix) * 4;
out[at] = rgb[index][0];
out[at + 1] = rgb[index][1];
out[at + 2] = rgb[index][2];
out[at + 3] = 255;
}
}
return out;
}
/** 2D 측점 한 줄의 3D 조건 안전율 — 지배 비탈 fs_by_condition · 없으면 조건별 최소 min · 없으면 null */
export function fs2dOf(
row: {
fs_by_condition?: Record<string, number | null>;
min?: Record<string, number | null>;
},
condition: string,
): number | null {
const v = row.fs_by_condition?.[condition] ?? row.min?.[condition];
return typeof v === "number" && Number.isFinite(v) ? v : null;
}
/** 구간 안 2D 측점 최소 안전율(없으면 null) */
export function min2d(
stations: readonly { chainage_m: number; fs_min: number | null }[],
start: number,
end: number,
): number | null {
let best: number | null = null;
for (const s of stations) {
if (s.chainage_m < start - 1e-6 || s.chainage_m > end + 1e-6) continue;
if (s.fs_min === null || !Number.isFinite(s.fs_min)) continue;
if (best === null || s.fs_min < best) best = s.fs_min;
}
return best;
}
/** 3D/2D 비 — 둘 중 하나라도 없으면 null */
export function ratio3d2d(f3d: number | null, f2d: number | null): number | null {
return f3d === null || f2d === null || !(f2d > 0) ? null : f3d / f2d;
}