Files
Aislo/E06_StabRock/E06_StabRock_UI_Joints.ts
T

332 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* E06_StabRock_UI_Joints.ts
* E06 좌측 — 절리군 표(구간 · 경사방향 · 경사 · φj · cj · 지속성 · [추가] [지움]) + 해석 설정
* (측방 한계 · 암 단위중량 · 우기 수위 · 인장균열 · 볼트). 칸을 바꾸면 doc 을 고치고 onChange —
* 캐시 · 저장은 페이지 몫. 범위 = 서버 LIMITS(벗어나면 칸 빨강 · 마우스 글).
* ========================================================================== */
import { createPanelCard } from "@ui/ui_template_collapsible";
import { createButton, el } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
export interface JointSet {
id: string;
name: string;
from_m: number;
to_m: number;
dip_direction: number;
dip: number;
phi: number;
c: number;
persistence_m: number | null;
}
export type Projection = "equal_angle" | "equal_area";
export interface RockSettings {
projection: Projection;
lateral_planar_deg: number;
lateral_toppling_deg: number;
gamma_r: number | null;
water_fill_wet: number;
min_block_m3: number;
crack: { mode: "none" | "critical" | "depth"; z: number | null };
bolt: { force: number; plunge: number };
}
export interface JointsDoc {
version: number;
sets: JointSet[];
settings: RockSettings;
}
export interface RockDefaults {
settings: RockSettings;
new_set: { phi: number; c: number };
limits: Record<string, [number, number]>;
}
export interface JointsView {
doc: JointsDoc;
defaults: RockDefaults;
editable: boolean;
endM: number; // 마지막 암 측점(새 줄 구간 끝)
change: () => void; // 값만 바뀜
rebuild: () => void; // 줄 수가 바뀜
}
/** 숫자 칸 — 범위 밖 · 빈 칸(필수)이면 is-invalid · 바꾸면 set 부르고 change */
function numberInput(
view: JointsView,
value: number | null,
limit: string,
set: (v: number | null) => void,
opts: { optional?: boolean; step?: string; path: string },
): HTMLInputElement {
const [lo, hi] = view.defaults.limits[limit] ?? [-Infinity, Infinity];
const input = el("input", {
className: "e06-input",
attrs: { type: "number", step: opts.step ?? "any", "data-path": opts.path },
}) as HTMLInputElement;
input.value = value === null ? "" : String(value);
input.disabled = !view.editable;
const check = (): boolean => {
const raw = input.value.trim();
const num = Number(raw);
const ok = raw === "" ? Boolean(opts.optional) : Number.isFinite(num) && num >= lo && num <= hi;
input.classList.toggle("is-invalid", !ok);
input.title = ok ? "" : `${t("E06_Err_Range")} (${lo} ~ ${hi})`;
return ok;
};
input.addEventListener("input", () => {
const raw = input.value.trim();
set(raw === "" ? null : Number(raw));
check();
view.change();
});
check();
return input;
}
function nextId(sets: JointSet[]): string {
let k = sets.length + 1;
while (sets.some((s) => s.id === `J${k}`)) k += 1;
return `J${k}`;
}
const COLUMNS: LocaleKey[] = [
"E06_Col_Name",
"E06_Col_From",
"E06_Col_To",
"E06_Col_DipDir",
"E06_Col_Dip",
"E06_Col_Phi",
"E06_Col_C",
"E06_Col_Persist",
];
function jointTable(view: JointsView): HTMLElement {
const wrap = el("div", { className: "e06-table-wrap" });
const table = el("table", { className: "e06-table" });
const head = el("tr");
for (const key of COLUMNS) head.append(el("th", { text: t(key) }));
head.append(el("th"));
table.append(el("thead"), el("tbody"));
table.tHead!.append(head);
view.doc.sets.forEach((row, k) => {
const tr = el("tr", { attrs: { "data-id": row.id } });
const name = el("input", {
className: "e06-input e06-input--name",
attrs: { type: "text", "data-path": `sets.${k}.name` },
}) as HTMLInputElement;
name.value = row.name;
name.disabled = !view.editable;
name.addEventListener("input", () => {
row.name = name.value;
name.classList.toggle("is-invalid", !name.value.trim());
view.change();
});
tr.append(el("td", { children: [name] }));
const fields: [keyof JointSet, string, boolean][] = [
["from_m", "chainage_m", false],
["to_m", "chainage_m", false],
["dip_direction", "dip_direction", false],
["dip", "dip", false],
["phi", "phi", false],
["c", "c", false],
["persistence_m", "persistence_m", true],
];
for (const [key, limit, optional] of fields) {
const input = numberInput(
view,
row[key] as number | null,
limit,
(v) => ((row as unknown as Record<string, number | null>)[key] = v),
{ optional, path: `sets.${k}.${key}` },
);
tr.append(el("td", { children: [input] }));
}
const remove = createButton({
label: "×",
variant: "danger-ghost",
size: "sm",
onClick: () => {
view.doc.sets.splice(k, 1);
view.rebuild();
},
});
remove.title = t("E06_Remove");
remove.disabled = !view.editable;
tr.append(el("td", { children: [remove] }));
table.tBodies[0].append(tr);
});
wrap.append(table);
if (!view.doc.sets.length) wrap.append(el("p", { className: "e06-hint", text: t("E06_NoSets") }));
return wrap;
}
function settingRow(label: LocaleKey, control: HTMLElement): HTMLElement {
const row = el("label", { className: "e06-setting" });
row.append(el("span", { text: t(label) }), control);
return row;
}
function select<T extends string>(
view: JointsView,
value: T,
options: [T, LocaleKey][],
set: (v: T) => void,
path: string,
): HTMLSelectElement {
const box = el("select", {
className: "e06-input",
attrs: { "data-path": path },
}) as HTMLSelectElement;
for (const [v, key] of options) box.append(el("option", { text: t(key), attrs: { value: v } }));
box.value = value;
box.disabled = !view.editable;
box.addEventListener("change", () => {
set(box.value as T);
view.rebuild();
});
return box;
}
/** 최소 블록 크기 칸 + [기본값] — 기본값 = 근거 후보(사용자 확정 전) */
function minBlockControl(view: JointsView, s: RockSettings): HTMLElement {
const reset = createButton({
label: t("E06_Defaults"),
variant: "ghost",
size: "sm",
onClick: () => {
s.min_block_m3 = view.defaults.settings.min_block_m3;
view.change();
view.rebuild();
},
});
reset.disabled = !view.editable;
const box = el("span", { className: "e06-setting__ctl" });
box.append(
numberInput(view, s.min_block_m3, "min_block_m3", (v) => (s.min_block_m3 = v ?? NaN), {
step: "0.01",
path: "settings.min_block_m3",
}),
reset,
);
return box;
}
export function buildJointCards(view: JointsView): HTMLElement[] {
const s = view.doc.settings;
const setsCard = createPanelCard(t("E06_Card_Joints"), false);
const add = createButton({
label: t("E06_Add"),
variant: "ghost",
size: "sm",
onClick: () => {
const id = nextId(view.doc.sets);
view.doc.sets.push({
id,
name: id,
from_m: 0,
to_m: Math.ceil(view.endM),
dip_direction: 0,
dip: 45,
phi: view.defaults.new_set.phi,
c: view.defaults.new_set.c,
persistence_m: null,
});
view.rebuild();
},
});
add.disabled = !view.editable;
setsCard.append(
el("p", { className: "e06-hint", text: t("E06_JointsHint") }),
jointTable(view),
el("div", { className: "e06-row-actions", children: [add] }),
);
const setCard = createPanelCard(t("E06_Card_Settings"), true);
const crack = s.crack;
setCard.append(
settingRow(
"E06_Set_LateralPlanar",
numberInput(
view,
s.lateral_planar_deg,
"lateral_deg",
(v) => (s.lateral_planar_deg = v ?? NaN),
{
path: "settings.lateral_planar_deg",
},
),
),
settingRow(
"E06_Set_LateralTopple",
numberInput(
view,
s.lateral_toppling_deg,
"lateral_deg",
(v) => (s.lateral_toppling_deg = v ?? NaN),
{ path: "settings.lateral_toppling_deg" },
),
),
settingRow(
"E06_Set_Gamma",
numberInput(view, s.gamma_r, "gamma_r", (v) => (s.gamma_r = v), {
optional: true,
path: "settings.gamma_r",
}),
),
settingRow(
"E06_Set_WaterWet",
numberInput(view, s.water_fill_wet, "water_fill_wet", (v) => (s.water_fill_wet = v ?? NaN), {
step: "0.05",
path: "settings.water_fill_wet",
}),
),
settingRow("E06_Set_MinBlock", minBlockControl(view, s)),
settingRow(
"E06_Set_Crack",
select(
view,
crack.mode,
[
["none", "E06_Crack_None"],
["critical", "E06_Crack_Critical"],
["depth", "E06_Crack_Depth"],
],
(v) => {
crack.mode = v;
crack.z = v === "depth" ? (crack.z ?? 1) : null;
},
"settings.crack.mode",
),
),
);
if (crack.mode === "depth")
setCard.append(
settingRow(
"E06_Set_CrackZ",
numberInput(view, crack.z, "crack_z", (v) => (crack.z = v), { path: "settings.crack.z" }),
),
);
setCard.append(
settingRow(
"E06_Set_BoltForce",
numberInput(view, s.bolt.force, "bolt_force", (v) => (s.bolt.force = v ?? NaN), {
path: "settings.bolt.force",
}),
),
settingRow(
"E06_Set_BoltPlunge",
numberInput(view, s.bolt.plunge, "bolt_plunge", (v) => (s.bolt.plunge = v ?? NaN), {
path: "settings.bolt.plunge",
}),
),
el("p", { className: "e06-hint", text: t("E06_SettingsHint") }),
el("p", { className: "e06-hint", text: t("E06_MinBlockHint") }),
);
return [setsCard, setCard];
}