Files
Aislo/E06_StabRock/E06_StabRock_UI_Result.ts
T
eomsangdonandClaude Opus 5.5 f2e8bd6d5f feat(536-1): E06 암반 평사투영 — Markland 판정 · Hoek-Bray 평면 · 쐐기 안전율 · 스테레오넷 · 절리 입력 · 화면
- 엔진(자체 · NumPy · SciPy): 평면 · 쐐기(교선) · 전도 Markland(측방 한계 20° 기본) · 평면 Hoek-Bray(인장균열 없음 · 임계 · 깊이 · 수압 · kh · 수동 볼트) · 쐐기 SWedge 꼴(미끄럼 방향 8 경우 · 두 면 · 한 면 · 수압 중력식 · kh · 볼트)
- 스테레오넷 좌표(등각 · 등면적 하반구 · 극점 · 대원 · 교선 · 마찰원 · 일광 엔벨로프 · 위험 영역 = 판정 부등식)
- 길: 절리 작업본 E06_StabRock/joints.json(초기값 · 저장 · 초기화) · 해석 = 암 측점 깎기 쪽만 · 파생 저장본 stab_rock · 결과 · 측점 스테레오넷
- 화면: 좌 절리군 표 · 설정 / 가운데 위험 띠 · 스테레오넷 / 우 측점 결과 · 안전율 표 · E 단계 막대에 E06(끝에 덧붙임)
- 시험 test_536_1(Hoek Sau Mau Ping 0.8184 · 1.0655 · Sharma 윗면 무게 · Hoek-Bray 쐐기 계수 · Kumsar · Priest 두 예제 · Markland 경계 · 투영 왕복 · 위험 영역 = 판정) · test_528 E 단계 수 4 → 5

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

220 lines
7.7 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.
/* =============================================================================
* E06_StabRock_UI_Result.ts
* E06 결과 — 위험 띠(누가 축 · 측점 판정 색 · 누르면 그 측점) · 우측 측점 목록(쪽 · 모드 수 ·
* 최소 안전율 · 판정) · 고른 측점 사면 · 모드별 안전율 표. 값은 서버 저장본 그대로(계산 없음).
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
export type Verdict = "NG" | "check" | "OK" | "none";
export interface RockCase {
mode: "planar" | "wedge";
ids: string[];
condition: "dry" | "wet" | "seismic";
F: number | null;
formed: boolean;
persistence_short: boolean;
required: number | null;
ok: boolean | null;
sliding?: string | null;
}
export interface StationEntry {
key: string;
chainage_m: number;
label: string | null;
ground_type: string;
slope: { side: string; dip_direction: number; dip: number; height_m: number; upper_dip: number };
gamma_r: number;
joints: {
id: string;
name: string;
dip_direction: number;
dip: number;
phi: number;
c: number;
}[];
kinematic: {
planar: string[];
toppling: string[];
wedge: { ids: [string, string]; trend: number; plunge: number }[];
};
cases: RockCase[];
counts: { planar: number; wedge: number; toppling: number };
min_F: number | null;
verdict: Verdict;
}
export interface RockResult {
fingerprint: string;
summary: {
slopes: number;
verdicts: Partial<Record<Verdict, number>>;
modes: Record<string, number>;
};
stations: StationEntry[];
}
const ORDER: Verdict[] = ["NG", "check", "OK", "none"];
export const entryId = (e: StationEntry): string => `${e.key}|${e.slope.side}`;
const fmt = (v: number | null | undefined, d = 2): string =>
v === null || v === undefined ? "–" : v.toFixed(d);
function badge(verdict: Verdict): HTMLElement {
return el("span", {
className: `e06-verdict e06-verdict--${verdict.toLowerCase()}`,
text: t(`E06_V_${verdict}` as LocaleKey),
});
}
/** 위험 띠 — 측점마다 가장 나쁜 판정 · 이웃 측점 가운데서 끊음 */
export function buildBand(
stations: StationEntry[],
active: string | null,
onSelect: (id: string) => void,
): HTMLElement {
const box = el("div", { className: "e06-band", attrs: { "data-role": "band" } });
const worst = new Map<number, StationEntry>();
for (const e of stations) {
const prev = worst.get(e.chainage_m);
if (!prev || ORDER.indexOf(e.verdict) < ORDER.indexOf(prev.verdict)) worst.set(e.chainage_m, e);
}
const list = [...worst.values()].sort((a, b) => a.chainage_m - b.chainage_m);
const legend = el("div", { className: "e06-band__legend" });
legend.append(el("strong", { text: t("E06_Band") }));
for (const v of ORDER) {
const n = list.filter((e) => e.verdict === v).length;
if (n) legend.append(badge(v), el("span", { className: "e06-band__count", text: String(n) }));
}
const strip = el("div", { className: "e06-band__strip" });
if (list.length) {
const lo = list[0].chainage_m;
const hi = Math.max(list[list.length - 1].chainage_m, lo + 1);
list.forEach((e, i) => {
const a = i ? (list[i - 1].chainage_m + e.chainage_m) / 2 : lo;
const b = i < list.length - 1 ? (e.chainage_m + list[i + 1].chainage_m) / 2 : hi;
const seg = el("button", {
className: `e06-band__seg e06-band__seg--${e.verdict.toLowerCase()}${
active?.startsWith(`${e.key}|`) ? " is-active" : ""
}`,
attrs: {
type: "button",
"data-key": e.key,
title: `${e.label ?? e.key} · ${t(`E06_V_${e.verdict}` as LocaleKey)}`,
},
});
seg.style.left = `${((a - lo) / (hi - lo)) * 100}%`;
seg.style.width = `${Math.max(0.4, ((b - a) / (hi - lo)) * 100)}%`;
seg.addEventListener("click", () => onSelect(entryId(e)));
strip.append(seg);
});
const ticks = el("div", { className: "e06-band__ticks" });
ticks.append(
el("span", { text: `${lo.toFixed(0)} m` }),
el("span", { text: `${hi.toFixed(0)} m` }),
);
box.append(legend, strip, ticks);
} else box.append(legend, el("p", { className: "e06-hint", text: t("E06_NoRock") }));
return box;
}
export function buildList(
stations: StationEntry[],
active: string | null,
onSelect: (id: string) => void,
): HTMLElement {
const table = el("table", { className: "e06-list", attrs: { "data-role": "station-list" } });
const head = el("tr");
for (const key of [
"E06_L_Station",
"E06_L_Side",
"E06_L_Modes",
"E06_L_MinF",
"E06_L_Verdict",
] as LocaleKey[])
head.append(el("th", { text: t(key) }));
table.append(el("thead", { children: [head] }), el("tbody"));
for (const e of stations) {
const id = entryId(e);
const tr = el("tr", {
className: id === active ? "is-active" : "",
attrs: { "data-id": id, "data-verdict": e.verdict },
});
tr.append(
el("td", { text: e.label ?? e.key }),
el("td", { text: t(e.slope.side === "left" ? "E06_Left" : "E06_Right") }),
el("td", { text: `${e.counts.planar} · ${e.counts.wedge} · ${e.counts.toppling}` }),
el("td", { text: fmt(e.min_F) }),
el("td", { children: [badge(e.verdict)] }),
);
tr.addEventListener("click", () => onSelect(id));
table.tBodies[0].append(tr);
}
return table;
}
export function buildDetail(e: StationEntry): HTMLElement {
const box = el("div", { className: "e06-detail", attrs: { "data-role": "detail" } });
const s = e.slope;
box.append(
el("h4", { text: `${e.label ?? e.key} · ${t(s.side === "left" ? "E06_Left" : "E06_Right")}` }),
el("p", {
className: "e06-hint",
text: t("E06_SlopeLine")
.replace("{dd}", s.dip_direction.toFixed(0))
.replace("{dip}", s.dip.toFixed(1))
.replace("{h}", s.height_m.toFixed(2))
.replace("{up}", s.upper_dip.toFixed(1))
.replace("{g}", e.gamma_r.toFixed(1)),
}),
);
const kin = e.kinematic;
const kinText = [
`${t("E06_Mode_planar")} ${kin.planar.join(", ") || "–"}`,
`${t("E06_Mode_wedge")} ${kin.wedge.map((w) => `${w.ids.join("·")}(${w.trend.toFixed(0)}/${w.plunge.toFixed(0)})`).join(", ") || "–"}`,
`${t("E06_Mode_toppling")} ${kin.toppling.join(", ") || "–"}`,
];
box.append(el("p", { className: "e06-kin", text: kinText.join("\n") }));
if (!e.cases.length) {
box.append(
el("p", {
className: "e06-hint",
text: t(e.joints.length ? "E06_NoRisk" : "E06_NoJointsHere"),
}),
);
return box;
}
const table = el("table", { className: "e06-cases" });
const head = el("tr");
for (const key of [
"E06_C_Mode",
"E06_C_Joints",
"E06_C_Cond",
"E06_C_F",
"E06_C_Req",
"E06_L_Verdict",
] as LocaleKey[])
head.append(el("th", { text: t(key) }));
table.append(el("thead", { children: [head] }), el("tbody"));
for (const c of e.cases) {
const verdict: Verdict = c.F === null ? "check" : c.ok === false ? "NG" : "OK";
const note = !c.formed ? t("E06_NotFormed") : c.persistence_short ? t("E06_Persist") : "";
const tr = el("tr", { attrs: { "data-mode": c.mode, "data-cond": c.condition } });
tr.append(
el("td", { text: t(`E06_Mode_${c.mode}` as LocaleKey) }),
el("td", { text: c.ids.join("·") }),
el("td", { text: t(`E06_Cond_${c.condition}` as LocaleKey) }),
el("td", { text: `${fmt(c.F, 3)}${note ? ` ${note}` : ""}` }),
el("td", { text: fmt(c.required) }),
el("td", { children: [badge(verdict)] }),
);
table.tBodies[0].append(tr);
}
box.append(table);
if (kin.toppling.length)
box.append(el("p", { className: "e06-hint", text: t("E06_ToppleNote") }));
return box;
}