Files
Aislo/E03_StabSection/E03_StabSection_UI_Reinforce.ts
T
eomsangdonandClaude Opus 5.5 498c738cee fix(536-3): 네일 · 앵커 최소 길이 = 1 m 성긴 걸음 + 이분(길이 대 Fs 단조 아님) · 입력 칸 격자 · 네일 표시 작게 · 보강 단면 그림 즉시 그림(붙으면 ResizeObserver 가 다시)
- 실측: 0+720 우 깎기 우기 L 3 1.049 · 7.5 1.211 · 12 1.156 — 옛 4.5 m 걸음이 O.K 창을 건너뜀
- requestAnimationFrame 이 안 도는 창(ORCA)에서 단면 그림이 비던 것

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:45:40 +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,
};
canvas.render(draw); // 붙기 전 = 기본 폭 · 붙으면 ResizeObserver 가 다시 그림
}
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;
}