Files
Aislo/E03_StabSection/E03_StabSection_UI_Plan.ts
T
eomsangdonandClaude Opus 5.5 ecd027f028 feat(532-3): E03 2D 측점 해석 길 · 화면 — 파생 저장본(측점 지문) · 진행률 · 임계면 · 등안전율 · 절편 자유물체도 · 평면 띠 · 3D 색칠
- 길: POST analyze2d(뒤에서 · 일꾼 프로세스 병렬 · 바뀐 측점만) · GET progress · result2d · section · corridor3d · PUT representative
- 입력 = E01 복사본(측점 · 횡단 · DTM 넓힘) + E02 지반 조건 · 엔진 = sub4 analyze_station · 벽 = 다음 일감(532-4)
- 화면: 측점 목록(조건 · 정렬 · N.G 만 · 대표 단면) · 횡단(지층 · 지하수선 · 임계면 · 후보 · 등안전율 지도 · 중심 격자 · 절편) · 결과 · 절편표 · 자유물체도 · 절편력 그래프 · 평면 띠 · 3D 보기
- 계산 = logic/E03_StabSection_Logic.ts · 시험 test_532_3_e03_section.py

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

90 lines
3.4 KiB
TypeScript

/* =============================================================================
* E03_StabSection_UI_Plan.ts
* E03 위 평면 위험 띠(PLAN 532-3) — 측점 중심을 이은 노선을 그 조건 판정 색으로(이웃 측점 사이
* 가운데서 끊음 · 북쪽 위 · 같은 축척) · 고른 측점 표시 · 누르면 가장 가까운 측점.
* 범례 = 판정마다 아이콘 + 글 + 측점 수. 계산은 logic `bandSegments` · `planFit`.
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import {
bandSegments,
planFit,
verdictOf,
type Condition,
type Point,
type StationRow,
} from "./logic/E03_StabSection_Logic";
import { svgEl } from "./E03_StabSection_UI_Section";
const H = 96;
const PAD = 10;
export function buildPlan(
rows: StationRow[],
condition: Condition,
active: string | null,
onSelect: (key: string) => void,
): HTMLElement {
const box = el("div", { className: "e03-plan e03-card" });
const head = el("div", { className: "e03-card__head" });
head.append(el("h3", { className: "e03-card__title", text: t("E03_Plan") }));
const legend = el("div", { className: "e03-legend", attrs: { "data-role": "plan-legend" } });
const counts = { ok: 0, ng: 0, none: 0 };
rows.forEach((r) => (counts[verdictOf(r, condition)] += 1));
for (const v of ["ng", "ok", "none"] as const) {
if (!counts[v]) continue;
const item = el("span", { className: `e03-verdict e03-verdict--${v}` });
item.append(`${t(`E03_Verdict_${v}` as LocaleKey)} ${counts[v]}`);
legend.append(item);
}
head.append(legend);
box.append(head);
const host = el("div");
box.append(host);
const draw = (): void => {
const W = Math.max(host.clientWidth, 300);
const segs = bandSegments(rows, condition);
const all: Point[] = segs.flatMap((s) => s.points);
const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, "data-role": "plan" });
if (all.length) {
const fit = planFit(all, W, H, PAD);
const scr = (p: Point): Point => [fit.k * p[0] + fit.tx, -fit.k * p[1] + fit.ty];
for (const seg of segs) {
svg.append(
svgEl("polyline", {
points: seg.points.map((p) => scr(p).join(",")).join(" "),
class: `e03-plan__seg e03-plan__seg--${seg.verdict}`,
"data-verdict": seg.verdict,
}),
);
}
const centers = rows.filter((r) => r.center && r.center[0] !== null && r.center[1] !== null);
const picked = centers.find((r) => r.key === active);
if (picked) {
const [x, y] = scr([picked.center![0]!, picked.center![1]!]);
svg.append(
svgEl("circle", { cx: x, cy: y, r: 6, class: "e03-plan__pick", "data-key": picked.key }),
);
}
svg.addEventListener("click", (ev) => {
const box2 = svg.getBoundingClientRect();
const px = ((ev.clientX - box2.left) / box2.width) * W;
const py = ((ev.clientY - box2.top) / box2.height) * H;
let best: StationRow | null = null;
let dist = Infinity;
for (const r of centers) {
const [x, y] = scr([r.center![0]!, r.center![1]!]);
const d = Math.hypot(x - px, y - py);
if (d < dist) [best, dist] = [r, d];
}
if (best && dist < 30) onSelect(best.key);
});
}
host.replaceChildren(svg);
};
new ResizeObserver(draw).observe(host);
return box;
}