Files
Aislo/E03_StabSection/E03_StabSection_UI_Reinforce.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

316 lines
11 KiB
TypeScript

/* =============================================================================
* E03_StabSection_UI_Reinforce.ts
* E03 「보강 검토」 탭(PLAN 536-3) — 고른 측점 비탈 가정(1:n 완화 · 소단 폭 · 높이 · 최소 완화 찾기)
* → 서버 재해석(POST …/stab/whatif/{측점}) · 전후 안전율 · 판정 표 · 후 단면 그림 ·
* 벽 있는 측점 = 벽 외적 안정 표(POST …/stab/wallcheck/{측점} · 활동 · 전도 · 지지력).
* 가정은 형상만 바꾼 따로 저장본(원본 결과 그대로) · 계산은 서버.
* ========================================================================== */
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import { createSectionCanvas } from "./E03_StabSection_UI_Section";
import { verdictChip } from "./E03_StabSection_UI_Result";
import type { Condition, SectionView, SideCase, StationRow } from "./logic/E03_StabSection_Logic";
export interface WhatIfRecord {
type: "slope";
station_key: string;
assume: {
side: string;
role: "cut" | "fill";
ratio: number;
ratio_before: number;
bench_w: number;
bench_h: number;
};
search: {
found: number | null;
gentlest: number;
tried: [number, boolean][];
condition: Condition | null;
} | null;
before: Partial<Record<Condition, SideCase>>;
after: Partial<Record<Condition, SideCase>>;
elapsed_s: number;
view: SectionView;
}
type Check = "sliding" | "overturning" | "bearing";
interface WallCase {
case: "normal" | "seismic";
fs: Record<Check, number>;
required: Record<Check, number>;
ok: Record<Check, boolean>;
coefficients: { ka: number; kae: number | null };
base: { e: number; q_max: number; q_ult: number };
geometry: { H: number; B: number };
}
export interface WallRecord {
type: "wall";
params: { method: string; water_ratio: number; kh: number };
walls: { name: string | null; side: string; role: string; cases: WallCase[] }[];
}
export interface SlopeAsk {
side: string;
ratio: number | null;
bench_w: number;
bench_h: number;
search: boolean;
/** 최소 완화 찾기 목표 조건(그 조건 기준안전율) */
condition: Condition | null;
}
export interface WallAsk {
method: string;
water_ratio: number;
}
export interface ReinforceOptions {
row: StationRow;
condition: Condition;
side: string | null;
editable: boolean;
busy: boolean;
slope: WhatIfRecord | undefined;
wall: WallRecord | undefined;
runSlope: (ask: SlopeAsk) => void;
runWall: (ask: WallAsk) => void;
}
const fmt = (v: number | null | undefined, d = 2): string =>
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
const CONDS: Condition[] = ["dry", "wet", "seismic"];
const CHECKS: Check[] = ["sliding", "overturning", "bearing"];
function num(input: HTMLInputElement): number | null {
const v = Number(input.value);
return input.value.trim() === "" || !Number.isFinite(v) ? null : v;
}
/** 전후 안전율 · 판정 표 — 비탈 가정 · 네일 · 앵커 가정 같이 씀 */
export function slopeTable(rec: Pick<WhatIfRecord, "before" | "after">): HTMLElement {
const table = el("table", { className: "e03-table", attrs: { "data-role": "whatif-table" } });
const head = ["E03_Condition", "E03_Rf_Before", "E03_Rf_After", "E03_Required"] as LocaleKey[];
table.append(
el("thead", {
children: [
el("tr", {
children: [
...head.map((k, i) => el("th", { className: i ? "is-num" : "", text: t(k) })),
el("th", { text: t("E03_Rf_VerdictBefore") }),
el("th", { text: t("E03_Rf_VerdictAfter") }),
],
}),
],
}),
);
const body = el("tbody");
for (const cond of CONDS) {
const a = rec.after[cond];
if (!a) continue;
const b = rec.before[cond];
const tr = el("tr", { attrs: { "data-cond": cond } });
tr.append(
el("td", { text: t(`E03_Cond_${cond}` as LocaleKey) }),
el("td", { className: "is-num", text: fmt(b?.fs, 3) }),
el("td", { className: "is-num", text: fmt(a.fs, 3), attrs: { "data-role": "fs-after" } }),
el("td", { className: "is-num", text: fmt(a.required) }),
el("td", { children: [verdictChip(b?.ok ?? null)] }),
el("td", { children: [verdictChip(a.ok)] }),
);
body.append(tr);
}
table.append(body);
return table;
}
function assumeText(rec: WhatIfRecord): string {
const a = rec.assume;
let text = `1:${a.ratio_before} → 1:${a.ratio}`;
if (a.bench_w && a.bench_h) text += ` · ${t("E03_Rf_Bench")} ${a.bench_w} m / ${a.bench_h} m`;
if (rec.search)
text += rec.search.found
? ` · ${t("E03_Rf_Found")} (${rec.search.tried.length}${t("E03_Iter")})`
: ` · ${t("E03_Rf_NotFound").replace("{n}", String(rec.search.gentlest))}`;
return `${text} · ${rec.elapsed_s} s`;
}
function wallTable(rec: WallRecord): HTMLElement {
const box = el("div", { attrs: { "data-role": "wall-table" } });
for (const wall of rec.walls) {
for (const c of wall.cases) {
box.append(
el("p", {
className: "e03-muted",
text:
`${wall.name ?? t("E03_Rf_Wall")} · ${t(`E03_Rf_Case_${c.case}` as LocaleKey)} · ` +
`H ${fmt(c.geometry.H)} m · B ${fmt(c.geometry.B)} m · Ka ${fmt(c.coefficients.ka, 3)}` +
(c.coefficients.kae ? ` · KAE ${fmt(c.coefficients.kae, 3)}` : "") +
` · e ${fmt(c.base.e)} m · q_max ${fmt(c.base.q_max, 1)} kPa`,
}),
);
const table = el("table", { className: "e03-table" });
table.append(
el("thead", {
children: [
el("tr", {
children: [
el("th", { text: t("E03_Rf_Check") }),
el("th", { className: "is-num", text: t("E03_Rf_Fs") }),
el("th", { className: "is-num", text: t("E03_Required") }),
el("th", { text: t("E03_Verdict") }),
],
}),
],
}),
);
const body = el("tbody");
for (const k of CHECKS)
body.append(
el("tr", {
attrs: { "data-check": k, "data-case": c.case },
children: [
el("td", { text: t(`E03_Rf_${k}` as LocaleKey) }),
el("td", { className: "is-num", text: fmt(c.fs[k]) }),
el("td", { className: "is-num", text: fmt(c.required[k], 1) }),
el("td", { children: [verdictChip(c.ok[k])] }),
],
}),
);
table.append(body);
box.append(table);
}
}
return box;
}
export function buildReinforce(o: ReinforceOptions): HTMLElement {
const host = el("div", { className: "e03-card", attrs: { "data-role": "reinforce" } });
const sides = o.row.sides;
const sideField = createSelectField({
label: t("E03_Side"),
options: sides.map((s) => ({
value: s.side,
text: `${t(s.side === "left" ? "E03_Left" : "E03_Right")} · ${t(s.role === "cut" ? "E03_Cut" : "E03_Fill")}`,
})),
value: o.slope?.assume.side ?? o.side ?? undefined,
compact: true,
});
const ratio = createInputField({
label: t("E03_Rf_Ratio"),
type: "number",
min: 0.1,
max: 4,
compact: true,
value: o.slope ? String(o.slope.assume.ratio) : "",
placeholder: t("E03_Rf_RatioNow"),
});
const benchW = createInputField({
label: t("E03_Rf_BenchW"),
type: "number",
min: 0,
compact: true,
value: o.slope?.assume.bench_w ? String(o.slope.assume.bench_w) : "",
});
const benchH = createInputField({
label: t("E03_Rf_BenchH"),
type: "number",
min: 0,
compact: true,
value: o.slope?.assume.bench_h ? String(o.slope.assume.bench_h) : "",
});
for (const f of [ratio, benchW, benchH]) f.input.step = "0.05";
const ask = (search: boolean): SlopeAsk => ({
side: sideField.select.value,
ratio: search ? null : num(ratio.input),
bench_w: num(benchW.input) ?? 0,
bench_h: num(benchH.input) ?? 0,
search,
condition: search ? o.condition : null,
});
const run = createButton({
label: o.busy ? t("E03_Rf_Busy") : t("E03_Rf_Run"),
variant: "filled",
onClick: () => o.runSlope(ask(false)),
});
const find = createButton({
label: t("E03_Rf_Search"),
variant: "ghost",
onClick: () => o.runSlope(ask(true)),
});
run.disabled = find.disabled = o.busy || !o.editable;
run.dataset.role = "whatif-run";
find.dataset.role = "whatif-search";
const fields = el("div", { className: "e03-panel__row" });
fields.append(sideField.root, ratio.root, benchW.root, benchH.root);
const buttons = el("div", { className: "e03-panel__row" });
buttons.append(find, run);
host.append(
el("h3", { className: "e03-card__title", text: t("E03_Rf_Title") }),
el("p", { className: "e03-muted", text: t("E03_Rf_Hint") }),
el("p", {
className: "e03-muted",
text: t("E03_Rf_Goal").replace("{c}", t(`E03_Cond_${o.condition}` as LocaleKey)),
}),
fields,
buttons,
);
if (o.slope) {
host.append(
el("p", {
className: "e03-muted",
text: assumeText(o.slope),
attrs: { "data-role": "assume" },
}),
slopeTable(o.slope),
el("h4", { className: "e03-card__title", text: t("E03_Rf_AfterSection") }),
);
const canvas = createSectionCanvas(() => undefined);
canvas.root.style.height = "260px";
canvas.root.style.flex = "none";
canvas.root.dataset.role = "whatif-section";
host.append(canvas.root);
const analyses = o.slope.view.analyses.filter((a) => a.condition === o.condition);
const analysis = analyses.find((a) => a.side === o.slope!.assume.side) ?? analyses[0] ?? null;
const draw = {
view: o.slope.view,
analysis,
toggles: { map: false, centers: false, candidates: false },
picked: null,
};
requestAnimationFrame(() => canvas.render(draw));
}
if (o.row.walls) {
const method = createSelectField({
label: t("E03_Rf_Method"),
options: [
{ value: "coulomb", text: "Coulomb" },
{ value: "rankine", text: "Rankine" },
],
value: o.wall?.params.method,
compact: true,
});
const water = createInputField({
label: t("E03_Rf_Water"),
type: "number",
min: 0,
max: 1,
compact: true,
value: String(o.wall?.params.water_ratio ?? 0),
});
water.input.step = "0.1";
const check = createButton({
label: t("E03_Rf_WallRun"),
variant: "ghost",
onClick: () => o.runWall({ method: method.select.value, water_ratio: num(water.input) ?? 0 }),
});
check.disabled = o.busy || !o.editable;
check.dataset.role = "wall-run";
const row = el("div", { className: "e03-panel__row" });
row.append(method.root, water.root, check);
host.append(el("h3", { className: "e03-card__title", text: t("E03_Rf_WallTitle") }), row);
if (o.wall) host.append(wallTable(o.wall));
}
return host;
}