- 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>
301 lines
10 KiB
TypeScript
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", "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;
|
|
}
|