Files
Aislo/E04_Stab3D/E04_Stab3D_UI_Settings.ts
T
eomsangdonandClaude Opus 5.5 cbc4cf1a59 feat(E04): 3D 해석 화면 — 구간 · 탐색 설정 · 평면 안전율 · 체적 지도 · 3D 보기(잘린 활동면) · 결과 · 진행 · 끊기 + logic 시험 (533-3 화면 부분)
- 3D 보기 = 해석 격자(합성 지표) 직접 그림 — E 복사본에 지표면 모델 행 · GLB 없음(loadSurface 못 씀)
- 2D 최소 = E03 GET …/stab/section 의 stations[].fs_min(없으면 「2D 결과 없음」)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:24:38 +09:00

142 lines
4.3 KiB
TypeScript

/* =============================================================================
* E04_Stab3D_UI_Settings.ts
* E04 좌측 탐색 설정 칸(PLAN 533-3) — 구간 시작 · 끝 · 기본(모양 · 탐색 폭 · 방법) · 고급(접힘 ·
* 기둥 · 구 중심 격자 · 반지름 증분 · 깊이 · 부피 제약) · [기본값]. 기본값 = 서버 SearchSettings.
* ========================================================================== */
import { createPanelCard } from "@ui/ui_template_collapsible";
import {
createButton,
createInputField,
createSelectField,
el,
type InputFieldHandle,
} from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
export interface Settings {
start_m: number;
end_m: number;
method: string;
shape: string;
ellipsoid_ratio: number;
box_half_width_m: number;
cell_m: number;
spacing_m: number;
h_min_m: number;
h_max_m: number;
h_step_m: number;
dr_m: number;
max_depth_m: number;
v_min_m3: number;
v_max_m3: number;
}
type NumberKey = Exclude<keyof Settings, "method" | "shape">;
const BASIC: [NumberKey, LocaleKey][] = [
["ellipsoid_ratio", "E04_Ratio"],
["box_half_width_m", "E04_BoxWidth"],
];
const ADVANCED: [NumberKey, LocaleKey][] = [
["cell_m", "E04_Cell"],
["spacing_m", "E04_Spacing"],
["h_min_m", "E04_HMin"],
["h_max_m", "E04_HMax"],
["h_step_m", "E04_HStep"],
["dr_m", "E04_Dr"],
["max_depth_m", "E04_MaxDepth"],
["v_min_m3", "E04_VMin"],
["v_max_m3", "E04_VMax"],
];
export function buildSettingsCards() {
const fields = new Map<NumberKey, InputFieldHandle>();
const listeners: (() => void)[] = [];
const changed = (): void => listeners.forEach((fn) => fn());
const number = (key: NumberKey, label: LocaleKey): HTMLElement => {
const field = createInputField({ label: t(label), type: "number", compact: true });
field.input.step = "any";
field.input.dataset.key = key;
field.input.addEventListener("change", changed);
fields.set(key, field);
return field.root;
};
const rangeRow = el("div", { className: "e04-range" });
rangeRow.append(number("start_m", "E04_Start"), number("end_m", "E04_End"));
const shape = createSelectField({
label: t("E04_Shape"),
compact: true,
options: (["both", "sphere", "ellipsoid"] as const).map((v) => ({
value: v,
text: t(`E04_Shape_${v}` as LocaleKey),
})),
onChange: changed,
});
const method = createSelectField({
label: t("E04_Method"),
compact: true,
options: (["bishop", "hovland"] as const).map((v) => ({
value: v,
text: t(`E04_Method_${v}` as LocaleKey),
})),
onChange: changed,
});
const basicCard = createPanelCard(t("E04_Card_Search"), false);
basicCard.append(shape.root, ...BASIC.map(([k, l]) => number(k, l)), method.root);
let defaults: Partial<Settings> = {};
const advancedCard = createPanelCard(t("E04_Card_Advanced"), true);
const grid = el("div", { className: "e04-advanced" });
grid.append(...ADVANCED.map(([k, l]) => number(k, l)));
advancedCard.append(
grid,
createButton({
label: t("E04_Defaults"),
variant: "ghost",
size: "sm",
onClick: () => {
const range = { start_m: read().start_m, end_m: read().end_m };
write({ ...read(), ...defaults, ...range, method: "bishop" });
changed();
},
}),
);
function read(): Settings {
const out = { shape: shape.select.value, method: method.select.value } as Settings;
for (const [key, field] of fields) out[key] = Number(field.input.value);
return out;
}
function write(values: Partial<Settings>): void {
for (const [key, field] of fields) {
const value = values[key];
if (typeof value === "number" && Number.isFinite(value)) field.input.value = String(value);
}
if (values.shape) shape.select.value = values.shape;
if (values.method) method.select.value = values.method;
}
return {
rangeRow,
basicCard,
advancedCard,
read,
write,
setRange(start: number, end: number) {
write({ start_m: start, end_m: end });
changed();
},
setDefaults(next: Partial<Settings>, maxSection: number) {
defaults = next;
fields.get("end_m")!.input.title = `≤ ${maxSection} m`;
},
onChange(fn: () => void) {
listeners.push(fn);
},
};
}