Files
Aislo/E06_StabRock/E06_StabRock_UI_Joints.ts
eomsangdonandClaude Sonnet 5.5 917127a63c style(setting_row): 설정 줄 부품에 좁은 꼴 추가 · E06 설정 줄을 부품으로 · D03 3D 창의 B05 클래스 빌림 끊음 (PLAN 553-4 뒤처리)
- 설정 줄 좁은 꼴(compact · 줄 28 · 글 12 · 선 없음) + 부품 모음 견본 + 지킴 시험
- E06 e06-setting 걷음 · 가이드 장면 선택자 갱신
- D03 3D 창 = 자기 클래스(d03-plan__3d-modal · 3d-body) · 모달 부품 그대로 · 가이드 장면 선택자 갱신

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 23:04:33 +09:00

337 lines
9.7 KiB
TypeScript
Raw Permalink 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, createInputField, createSelectField, el } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import { settingRow as settingRowPart } from "@ui/ui_template_setting_row";
import { e06Table, note } from "./E06_StabRock_UI_Parts";
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-error` · aria-invalid) */
function markInvalid(input: HTMLElement, bad: boolean): void {
input.classList.toggle("is-error", bad);
if (bad) input.setAttribute("aria-invalid", "true");
else input.removeAttribute("aria-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 = createInputField({ type: "number", size: 24 }).input;
input.classList.add("e06-input");
input.step = opts.step ?? "any";
input.dataset.path = opts.path;
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;
markInvalid(input, !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 sets = view.doc.sets;
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],
];
const rows = sets.map((row, k) => {
const name = createInputField({ size: 24 }).input;
name.classList.add("e06-input", "e06-input--name");
name.dataset.path = `sets.${k}.name`;
name.value = row.name;
name.disabled = !view.editable;
name.addEventListener("input", () => {
row.name = name.value;
markInvalid(name, !name.value.trim());
view.change();
});
const cells: HTMLElement[] = [name];
for (const [key, limit, optional] of fields) {
cells.push(
numberInput(
view,
row[key] as number | null,
limit,
(v) => ((row as unknown as Record<string, number | null>)[key] = v),
{ optional, path: `sets.${k}.${key}` },
),
);
}
const remove = createButton({
label: "×",
variant: "danger-ghost",
size: "sm",
onClick: () => {
sets.splice(k, 1);
view.rebuild();
},
});
remove.title = t("E06_Remove");
remove.disabled = !view.editable;
cells.push(remove);
return cells;
});
const list = e06Table([...COLUMNS.map((key) => t(key)), ""], rows, "e06-table");
list.rows.forEach((tr, k) => (tr.dataset.id = sets[k].id));
wrap.append(list.root);
if (!sets.length) wrap.append(note(t("E06_NoSets")));
return wrap;
}
/** 좁은 패널 설정 줄 — 설정 줄 부품 좁은 꼴 */
function settingRow(label: LocaleKey, control: HTMLElement): HTMLElement {
return settingRowPart(t(label), "", control, { compact: true });
}
function select<T extends string>(
view: JointsView,
value: T,
options: [T, LocaleKey][],
set: (v: T) => void,
path: string,
): HTMLSelectElement {
const box = createSelectField({
options: options.map(([v, key]) => ({ value: v, text: t(key) })),
size: 24,
}).select;
box.classList.add("e06-input");
box.dataset.path = path;
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-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(
note(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",
}),
),
note(t("E06_SettingsHint")),
note(t("E06_MinBlockHint")),
);
return [setsCard, setCard];
}