Files
Aislo/E06_StabRock/E06_StabRock_UI_Stereonet.ts
T
eomsangdonandClaude Opus 5.5 f2e8bd6d5f feat(536-1): E06 암반 평사투영 — Markland 판정 · Hoek-Bray 평면 · 쐐기 안전율 · 스테레오넷 · 절리 입력 · 화면
- 엔진(자체 · NumPy · SciPy): 평면 · 쐐기(교선) · 전도 Markland(측방 한계 20° 기본) · 평면 Hoek-Bray(인장균열 없음 · 임계 · 깊이 · 수압 · kh · 수동 볼트) · 쐐기 SWedge 꼴(미끄럼 방향 8 경우 · 두 면 · 한 면 · 수압 중력식 · kh · 볼트)
- 스테레오넷 좌표(등각 · 등면적 하반구 · 극점 · 대원 · 교선 · 마찰원 · 일광 엔벨로프 · 위험 영역 = 판정 부등식)
- 길: 절리 작업본 E06_StabRock/joints.json(초기값 · 저장 · 초기화) · 해석 = 암 측점 깎기 쪽만 · 파생 저장본 stab_rock · 결과 · 측점 스테레오넷
- 화면: 좌 절리군 표 · 설정 / 가운데 위험 띠 · 스테레오넷 / 우 측점 결과 · 안전율 표 · E 단계 막대에 E06(끝에 덧붙임)
- 시험 test_536_1(Hoek Sau Mau Ping 0.8184 · 1.0655 · Sharma 윗면 무게 · Hoek-Bray 쐐기 계수 · Kumsar · Priest 두 예제 · Markland 경계 · 투영 왕복 · 위험 영역 = 판정) · test_528 E 단계 수 4 → 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:26:37 +09:00

194 lines
6.6 KiB
TypeScript

/* =============================================================================
* E06_StabRock_UI_Stereonet.ts
* E06 가운데 스테레오넷(SVG · 하반구) — 서버가 내려 준 투영 좌표(반지름 1 · x = 동 · y = 북)를
* 그리기만 함(계산 = E06_StabRock_Stereonet.py). 보기 = 전체 · 평면 · 쐐기 · 전도.
* 평면 · 전도 = 극점 + 극점 마찰원(φ) · 쐐기 = 교선 + 교선 마찰원 · 위험 영역 = 모드 색 ·
* 일광 엔벨로프(평면) · 사면 대원 = 굵은 선. 위험 판정된 절리 · 교선 = 테두리 강조.
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
const SVG_NS = "http://www.w3.org/2000/svg";
const SIZE = 440;
const R = 190;
const C = SIZE / 2;
export type NetMode = "all" | "planar" | "wedge" | "toppling";
type XY = [number, number];
export interface NetData {
projection: string;
slope: { pole: XY; great_circle: XY[] };
planes: { id: string; pole: XY; great_circle: XY[] }[];
intersections: { ids: [string, string]; point: XY }[];
friction_pole_r: number;
friction_line_r: number;
daylight_envelope: XY[];
zones: Record<"planar" | "wedge" | "toppling", XY[] | null>;
}
export interface NetMarks {
planar: string[];
toppling: string[];
wedge: string[]; // "J1|J2"
}
function svg<K extends keyof SVGElementTagNameMap>(
tag: K,
attrs: Record<string, string | number> = {},
text?: string,
): SVGElementTagNameMap[K] {
const node = document.createElementNS(SVG_NS, tag);
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
if (text !== undefined) node.textContent = text;
return node;
}
const sx = (p: XY): number => C + R * p[0];
const sy = (p: XY): number => C - R * p[1];
const path = (pts: XY[], close = false): string =>
pts.map((p, i) => `${i ? "L" : "M"}${sx(p).toFixed(1)},${sy(p).toFixed(1)}`).join("") +
(close ? "Z" : "");
/** 절리 색 번호(0~5) — 표 · 그림 같은 차례 */
export const jointTone = (index: number): number => index % 6;
export function drawStereonet(net: NetData, mode: NetMode, marks: NetMarks): SVGSVGElement {
const root = svg("svg", {
viewBox: `0 0 ${SIZE} ${SIZE}`,
class: "e06-net",
"data-role": "stereonet",
"data-projection": net.projection,
"data-mode": mode,
});
const clip = svg("clipPath", { id: "e06-net-clip" });
clip.append(svg("circle", { cx: C, cy: C, r: R }));
const defs = svg("defs");
defs.append(clip);
root.append(defs, svg("circle", { cx: C, cy: C, r: R, class: "e06-net__rim" }));
const body = svg("g", { "clip-path": "url(#e06-net-clip)" });
root.append(body);
const show = (m: Exclude<NetMode, "all">): boolean => mode === "all" || mode === m;
for (const m of ["planar", "wedge", "toppling"] as const) {
const zone = net.zones[m];
if (zone && show(m))
body.append(
svg("path", {
d: path(zone, true),
class: `e06-net__zone e06-net__zone--${m}`,
"data-zone": m,
}),
);
}
// 마찰원
if (show("planar") || show("toppling"))
body.append(
svg("circle", {
cx: C,
cy: C,
r: R * net.friction_pole_r,
class: "e06-net__friction",
"data-role": "friction-pole",
}),
);
if (show("wedge"))
body.append(
svg("circle", {
cx: C,
cy: C,
r: R * net.friction_line_r,
class: "e06-net__friction e06-net__friction--line",
"data-role": "friction-line",
}),
);
if (show("planar"))
body.append(
svg("path", {
d: path(net.daylight_envelope),
class: "e06-net__envelope",
"data-role": "daylight",
}),
);
// 대원
net.planes.forEach((p, i) =>
body.append(
svg("path", {
d: path(p.great_circle),
class: `e06-net__gc e06-tone-${jointTone(i)}`,
"data-id": p.id,
}),
),
);
body.append(
svg("path", { d: path(net.slope.great_circle), class: "e06-net__slope", "data-role": "slope" }),
);
// 극점(평면 · 전도 보기) · 교선(쐐기 보기)
if (mode !== "wedge")
net.planes.forEach((p, i) => {
const risky = marks.planar.includes(p.id) || marks.toppling.includes(p.id);
const g = svg("g", {
class: `e06-net__pole e06-tone-${jointTone(i)}${risky ? " is-risk" : ""}`,
"data-pole": p.id,
});
g.append(svg("circle", { cx: sx(p.pole), cy: sy(p.pole), r: 5 }));
g.append(svg("text", { x: sx(p.pole) + 7, y: sy(p.pole) - 6 }, p.id));
root.append(g);
});
if (mode === "all" || mode === "wedge")
for (const line of net.intersections) {
const key = line.ids.join("|");
const risky = marks.wedge.includes(key);
const [x, y] = [sx(line.point), sy(line.point)];
const g = svg("g", { class: `e06-net__line${risky ? " is-risk" : ""}`, "data-line": key });
g.append(svg("path", { d: `M${x},${y - 6}L${x + 5.5},${y + 4}L${x - 5.5},${y + 4}Z` }));
g.append(svg("text", { x: x + 7, y: y + 12 }, line.ids.join("·")));
root.append(g);
}
// 사면 극점 · 방위
root.append(
svg("rect", {
x: sx(net.slope.pole) - 4,
y: sy(net.slope.pole) - 4,
width: 8,
height: 8,
class: "e06-net__slope-pole",
}),
);
for (const [label, x, y] of [
["N", C, C - R - 8],
["E", C + R + 10, C + 4],
["S", C, C + R + 18],
["W", C - R - 10, C + 4],
] as const)
root.append(svg("text", { x, y, class: "e06-net__dir", "text-anchor": "middle" }, label));
root.append(svg("line", { x1: C - 6, y1: C, x2: C + 6, y2: C, class: "e06-net__tick" }));
root.append(svg("line", { x1: C, y1: C - 6, x2: C, y2: C + 6, class: "e06-net__tick" }));
return root;
}
export function netLegend(mode: NetMode): HTMLElement {
const box = el("div", { className: "e06-legend" });
const items: [string, LocaleKey][] = [
["slope", "E06_Lg_Slope"],
["friction", "E06_Lg_Friction"],
];
if (mode === "all" || mode === "planar")
items.push(["planar", "E06_Lg_Planar"], ["daylight", "E06_Lg_Daylight"]);
if (mode === "all" || mode === "wedge")
items.push(["wedge", "E06_Lg_Wedge"], ["line", "E06_Lg_Line"]);
if (mode === "all" || mode === "toppling") items.push(["toppling", "E06_Lg_Toppling"]);
if (mode !== "wedge") items.push(["pole", "E06_Lg_Pole"]);
for (const [cls, key] of items) {
const row = el("span", { className: "e06-legend__row" });
row.append(
el("i", { className: `e06-legend__mark e06-legend__mark--${cls}` }),
el("span", { text: t(key) }),
);
box.append(row);
}
return box;
}