Files
Aislo/E03_StabSection/E03_StabSection_UI_Support.ts
T

301 lines
10 KiB
TypeScript

/* =============================================================================
* E03_StabSection_UI_Support.ts
* E03 「보강 검토」 탭 네일 · 앵커 가정(PLAN 536-3 뒷부분) — 종류 · 배치(행 수 · 첫 행 높이 · 수직 · 수평 간격) ·
* 제원 → supports 목록(`layoutSupports` · 입력 꼴 그대로 POST …/stab/whatif/{측점}) · 최소 길이 찾기 ·
* 전후 안전율 표 · 네일마다 동원력 · mode 표 · 후 단면 그림(머리 · 정착 구간 · 활동면과 만나는 점 · mode 색).
* 기본값 = 국내 흔한 값 후보(사용자 확정 전) · 계산은 서버.
* ========================================================================== */
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import { slopeTable } from "./E03_StabSection_UI_Reinforce";
import { createSectionCanvas } from "./E03_StabSection_UI_Section";
import type { Condition, SectionView, SideCase, StationRow } from "./logic/E03_StabSection_Logic";
import {
layoutSupports,
supportDraws,
SUPPORT_DEFAULTS,
type SupportForce,
type SupportIn,
type SupportLayout,
type SupportType,
} from "./logic/E03_StabSection_Support";
export interface SupportRecord {
type: "support";
station_key: string;
assume: { side: string; role: "cut" | "fill"; supports: SupportIn[] };
search: {
found: number | null;
tried: [number, boolean][];
longest: number;
condition: Condition | null;
} | null;
before: Partial<Record<Condition, SideCase>>;
after: Partial<Record<Condition, SideCase>>;
support_forces: Partial<Record<Condition, SupportForce[]>> | null;
supports_pending: boolean;
elapsed_s: number;
view: SectionView;
}
export interface SupportAsk {
side: string;
ratio: null;
bench_w: 0;
bench_h: 0;
search: boolean;
condition: Condition | null;
supports: SupportIn[];
}
export interface SupportOptions {
row: StationRow;
/** 지금 측점 단면(원본) — 비탈 면 · 활동 방향 */
view: SectionView;
condition: Condition;
side: string | null;
editable: boolean;
busy: boolean;
record: SupportRecord | undefined;
run: (ask: SupportAsk) => void;
}
/** 마지막 배치 — 다시 그려도(계산 중 · 측점 바꿈) 입력 그대로 */
let layout: SupportLayout = { ...SUPPORT_DEFAULTS.nail };
type NumKey = Exclude<keyof SupportLayout, "type">;
const FIELDS: NumKey[] = [
"rows",
"first_m",
"v_spacing_m",
"h_spacing_m",
"length_m",
"angle_deg",
"tensile_kN",
"bond_kPa",
"bond_length_m",
"hole_dia_mm",
"prestress_kN",
];
const ANCHOR_ONLY: NumKey[] = ["bond_length_m", "prestress_kN"];
const MODES = ["tensile", "pullout", "stripping", "prestress", "not_crossing"] as const;
const fmt = (v: number | null | undefined, d = 2): string =>
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
function assumeText(rec: SupportRecord): string {
const s = rec.assume.supports;
const first = s[0];
if (!first) return "";
let text =
`${t(`E03_Sp_${first.type}` as LocaleKey)} ${s.length}${t("E03_Sp_Count")} · ` +
`L ${fmt(first.length_m, 1)} m · ${first.angle_deg}° · @${first.spacing_m} m`;
if (rec.search)
text += rec.search.found
? ` · ${t("E03_Sp_Found")} (${rec.search.tried.length}${t("E03_Iter")})`
: ` · ${t("E03_Sp_NotFound").replace("{n}", String(rec.search.longest))}`;
return `${text} · ${rec.elapsed_s} s`;
}
function forceTable(rec: SupportRecord, condition: Condition): HTMLElement {
const forces = rec.support_forces?.[condition] ?? null;
const table = el("table", { className: "e03-table", attrs: { "data-role": "support-table" } });
const head = ["E03_Sp_No", "E03_Sp_Type", "E03_Sp_Head", "E03_Sp_Length", "E03_Sp_Force"];
table.append(
el("thead", {
children: [
el("tr", {
children: [
...head.map((k, i) =>
el("th", { className: i > 1 ? "is-num" : "", text: t(k as LocaleKey) }),
),
el("th", { text: t("E03_Sp_Mode") }),
],
}),
],
}),
);
const body = el("tbody");
rec.assume.supports.forEach((s, i) => {
const f = forces?.find((v) => v.index === i);
const mode = f?.mode ?? "pending";
body.append(
el("tr", {
attrs: { "data-i": String(i), "data-mode": mode },
children: [
el("td", { text: String(i + 1) }),
el("td", { text: t(`E03_Sp_${s.type}` as LocaleKey) }),
el("td", { className: "is-num", text: `${fmt(s.x)} / ${fmt(s.z)}` }),
el("td", { className: "is-num", text: fmt(s.length_m, 1) }),
el("td", { className: "is-num", text: fmt(f?.mobilized_kN_per_m, 1) }),
el("td", {
children: [
el("span", {
className: `e03-support e03-support--${mode}`,
children: [el("span", { className: "e03-support-swatch" })],
}),
el("span", { text: t(`E03_Sp_Mode_${mode}` as LocaleKey) }),
],
}),
],
}),
);
});
table.append(body);
return table;
}
function legend(): HTMLElement {
const box = el("p", { className: "e03-muted", attrs: { "data-role": "support-legend" } });
for (const mode of MODES)
box.append(
el("span", {
className: `e03-support e03-support--${mode}`,
children: [el("span", { className: "e03-support-swatch" })],
}),
el("span", { text: `${t(`E03_Sp_Mode_${mode}` as LocaleKey)} ` }),
);
return box;
}
export function buildSupport(o: SupportOptions): HTMLElement {
const host = el("div", { className: "e03-card", attrs: { "data-role": "support" } });
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.record?.assume.side ?? o.side ?? undefined,
compact: true,
});
const typeField = createSelectField({
label: t("E03_Sp_Type"),
options: (["nail", "anchor"] as SupportType[]).map((v) => ({
value: v,
text: t(`E03_Sp_${v}` as LocaleKey),
})),
value: layout.type,
compact: true,
});
typeField.select.dataset.role = "support-type";
const inputs = new Map<NumKey, HTMLInputElement>();
const fields = el("div", { className: "e03-panel__row" });
fields.append(sideField.root, typeField.root);
for (const key of FIELDS) {
const f = createInputField({
label: t(`E03_Sp_F_${key}` as LocaleKey),
type: "number",
min: 0,
compact: true,
value: String(layout[key]),
});
f.input.step = key === "rows" ? "1" : "0.1";
f.input.dataset.field = key;
f.input.disabled = layout.type === "nail" && ANCHOR_ONLY.includes(key);
f.input.addEventListener("input", () => {
const v = Number(f.input.value);
if (f.input.value.trim() !== "" && Number.isFinite(v)) layout = { ...layout, [key]: v };
});
inputs.set(key, f.input);
fields.append(f.root);
}
typeField.select.addEventListener("change", () => {
layout = { ...SUPPORT_DEFAULTS[typeField.select.value as SupportType] };
for (const [key, input] of inputs) {
input.value = String(layout[key]);
input.disabled = layout.type === "nail" && ANCHOR_ONLY.includes(key);
}
});
const ask = (search: boolean): SupportAsk | null => {
const side = sideField.select.value;
const info = sides.find((s) => s.side === side);
if (!info) return null;
const supports = layoutSupports(o.view.surface, side, info.role, info.height_m, layout);
return {
side,
ratio: null,
bench_w: 0,
bench_h: 0,
search,
condition: search ? o.condition : null,
supports,
};
};
const send = (search: boolean): void => {
const a = ask(search);
if (a && a.supports.length) o.run(a);
};
const run = createButton({
label: o.busy ? t("E03_Rf_Busy") : t("E03_Sp_Run"),
variant: "filled",
onClick: () => send(false),
});
const find = createButton({
label: t("E03_Sp_Search"),
variant: "ghost",
onClick: () => send(true),
});
run.disabled = find.disabled = o.busy || !o.editable;
run.dataset.role = "support-run";
find.dataset.role = "support-search";
const buttons = el("div", { className: "e03-panel__row" });
buttons.append(find, run);
host.append(
el("h3", { className: "e03-card__title", text: t("E03_Sp_Title") }),
el("p", { className: "e03-muted", text: t("E03_Sp_Hint") }),
el("p", {
className: "e03-muted",
text: t("E03_Sp_Source"),
attrs: { "data-role": "support-source" },
}),
fields,
buttons,
);
const rec = o.record;
if (!rec) return host;
host.append(
el("p", {
className: "e03-muted",
text: assumeText(rec),
attrs: { "data-role": "support-assume" },
}),
);
if (rec.supports_pending)
host.append(
el("p", {
className: "e03-muted",
text: t("E03_Sp_Pending"),
attrs: { "data-role": "support-pending" },
}),
);
host.append(slopeTable(rec), forceTable(rec, o.condition), legend());
const canvas = createSectionCanvas(() => undefined);
canvas.root.style.height = "260px";
canvas.root.style.flex = "none";
canvas.root.dataset.role = "support-section";
host.append(canvas.root);
const analyses = rec.view.analyses.filter((a) => a.condition === o.condition);
const analysis = analyses.find((a) => a.side === rec.assume.side) ?? analyses[0] ?? null;
const direction =
rec.view.analyses.find((a) => a.side === rec.assume.side)?.direction ??
(rec.assume.side === "left" ? -1 : 1);
const draw = {
view: rec.view,
analysis,
toggles: { map: false, centers: false, candidates: false },
picked: null,
supports: supportDraws(
rec.assume.supports,
direction,
analysis?.critical?.points ?? null,
rec.support_forces?.[o.condition],
),
};
requestAnimationFrame(() => canvas.render(draw));
return host;
}