Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/* 빠진 횡단 띠 — E02 · E03 화면 위(E01_StabRegister_UI_Missing.ts) */
|
||||
.e-missing {
|
||||
margin: 0 0 8px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--color-warning);
|
||||
border-left-width: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.e-missing > summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.e-missing > p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* =============================================================================
|
||||
* E01_StabRegister_UI_Missing.ts
|
||||
* 빠진 횡단 띠(요구 351 · PLAN 532-5) — E02 · E03 화면 위 「횡단 없는 측점 N 개 — 해석에서 빠짐」.
|
||||
* 목록 = 서버 `missing_stations`(E01 복사본에 횡단 파일이 없는 측점 누가거리) · 누르면 펼침.
|
||||
* ========================================================================== */
|
||||
|
||||
import "./E01_StabRegister_UI_Missing.css";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
|
||||
/** 빠진 측점이 없으면 빈 칸(띠 없음) */
|
||||
export function buildMissingBanner(missing: number[] | undefined): HTMLElement {
|
||||
const list = missing ?? [];
|
||||
if (!list.length) return el("div");
|
||||
const box = el("details", {
|
||||
className: "e-missing",
|
||||
attrs: { "data-role": "missing-stations", "data-count": String(list.length) },
|
||||
});
|
||||
box.append(
|
||||
el("summary", { text: t("E_Missing_Head").replace("{n}", String(list.length)) }),
|
||||
el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }),
|
||||
);
|
||||
return box;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"""E02 지반 조건 API (요구 347-2 · 3 · 4 · PLAN 532-3).
|
||||
|
||||
- `GET /api/projects/{id}/stab/ground` — 작업본(없으면 초기값) + 기본값 + 고칠 수 있는지
|
||||
+ 횡단 없는 측점(`missing_stations` · 화면 띠)
|
||||
- `PUT …/stab/ground` — [저장] 검사 뒤 작업본 `E02_StabGround/ground.json`
|
||||
· 측점(B06 암 경계)은 서버 값
|
||||
- `POST …/stab/ground/reset` — [초기화] 초기값을 작업본에 덮어씀(재계산 아님)
|
||||
@@ -27,7 +28,7 @@ from common_util.common_util_project_copy import MANIFEST_NAME
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
from config.config_system_design import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
from E01_StabRegister.E01_StabRegister_Source import db_snapshot, source_dir
|
||||
from E01_StabRegister.E01_StabRegister_Source import db_snapshot, missing_stations, source_dir
|
||||
from E02_StabGround import E02_StabGround_Model as model
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["E02 Stab Ground"])
|
||||
@@ -96,6 +97,12 @@ async def seed_initial(project_id: str) -> None:
|
||||
ensure_initial(root)
|
||||
|
||||
|
||||
def _missing(project_root: Path) -> list[float]:
|
||||
"""횡단 없어 해석에서 빠진 측점 — 복사본이 없으면 빈 목록."""
|
||||
root = source_dir(project_root)
|
||||
return missing_stations(root) if (root / MANIFEST_NAME).is_file() else []
|
||||
|
||||
|
||||
def _read_working(project_root: Path) -> dict[str, Any] | None:
|
||||
path = model.ground_path(project_root)
|
||||
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
|
||||
@@ -119,6 +126,7 @@ async def get_ground(
|
||||
"saved": working is not None,
|
||||
"defaults": defaults,
|
||||
"editable": _editable(session, project),
|
||||
"missing_stations": _missing(root),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
|
||||
import { buildCards, type E02View } from "./E02_StabGround_UI_Tables";
|
||||
import {
|
||||
checkGround,
|
||||
@@ -33,6 +34,7 @@ interface GroundResponse {
|
||||
saved?: boolean;
|
||||
defaults?: GroundDefaults;
|
||||
editable?: boolean;
|
||||
missing_stations?: number[];
|
||||
}
|
||||
|
||||
const cacheKey = (projectId: string): string => `e02_ground_cache:${projectId}`;
|
||||
@@ -95,6 +97,7 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
let view: E02View | null = null;
|
||||
let refreshStations = (): void => {};
|
||||
let dirty = false;
|
||||
let missing: number[] = [];
|
||||
|
||||
const paintStatus = (errors: CheckError[]): void => {
|
||||
const lines: string[] = [];
|
||||
@@ -127,7 +130,7 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
if (!view) return;
|
||||
const cards = buildCards(view);
|
||||
refreshStations = cards.refreshStations;
|
||||
main.replaceChildren(cards.root);
|
||||
main.replaceChildren(buildMissingBanner(missing), cards.root);
|
||||
mark();
|
||||
};
|
||||
|
||||
@@ -221,6 +224,7 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
void (async () => {
|
||||
try {
|
||||
const body = await request(`/projects/${projectId}/stab/ground`);
|
||||
missing = body.missing_stations ?? [];
|
||||
view = {
|
||||
ground: body.ground,
|
||||
defaults: body.defaults as GroundDefaults,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- `POST /api/projects/{id}/stab/analyze2d` — 전 측점 해석(뒤에서 · 바뀐 측점만) · 같은 지문 = 안 셈
|
||||
- `GET …/stab/progress` — 진행률
|
||||
- `GET …/stab/result2d` — 요약 + 측점 목록(판정 · 대표 단면)
|
||||
- `GET …/stab/result2d` — 요약 + 측점 목록(판정 · 대표 단면 · 벽 수) + 횡단 없는 측점
|
||||
- `GET …/stab/section/{station_key}` — 그 측점 지층 폴리곤 · 지하수선 · 임계면 · 후보 · 절편 · 격자
|
||||
- `PUT …/stab/representative` — 대표 단면 켬/끔 → 작업본 `E03_StabSection/representative.json`
|
||||
- `GET …/stab/corridor3d` — 복사본 노선면(1 m 솎음 · 3D 색칠 보기용)
|
||||
@@ -10,8 +10,8 @@
|
||||
입력 = E01 복사본(측점 · 횡단 · DTM) + E02 지반 조건(작업본 · 없으면 초기값)
|
||||
계산 자리 = ③ 서버 단독.
|
||||
결과 = 파생 저장본 `_derived/stab2d_station/<측점 지문>` · `_derived/stab2d_summary/<전체 지문>` —
|
||||
측점 지문 = 엔진 코드 · 복사 지문 · 물성 · 조건 · 하중 · 그 측점 지층. 공유 회사(보기만)는
|
||||
`require_project_access` 가 POST · PUT 를 막음.
|
||||
측점 지문 = 엔진 코드 · 복사 지문 · 물성 · 조건 · 하중 · 지하수 갈래 · 그 측점 지층 · 벽.
|
||||
공유 회사(보기만)는 `require_project_access` 가 POST · PUT 를 막음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -49,6 +49,7 @@ from E01_StabRegister import E01_StabRegister_Source as src
|
||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||
from E02_StabGround import E02_StabGround_Router as e02
|
||||
from E03_StabSection import E03_StabSection_View as view
|
||||
from E03_StabSection import E03_StabSection_Walls as walls_mod
|
||||
from E03_StabSection import E03_StabSection_Worker as worker
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import (
|
||||
ground_from_cross,
|
||||
@@ -94,8 +95,9 @@ def engine_hash() -> str:
|
||||
|
||||
|
||||
def _ground(root: Path) -> dict[str, Any]:
|
||||
"""E02 지반 조건 — 작업본 · 없으면 초기값."""
|
||||
return e02._read_working(root) or e02.ensure_initial(root)
|
||||
"""E02 지반 조건 — 작업본 · 없으면 초기값(옛 정본 = 빠진 지하수 칸 기본값 · E02 화면과 같음)."""
|
||||
ground = e02._read_working(root) or e02.ensure_initial(root)
|
||||
return ground_model.upgrade(ground, ground_model.load_defaults())
|
||||
|
||||
|
||||
def _layers(resolved: dict[str, Any] | None) -> list | None:
|
||||
@@ -109,6 +111,12 @@ def _layers(resolved: dict[str, Any] | None) -> list | None:
|
||||
return [[resolved["upper"], None]]
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _walls(source: str, _copy_fp: str) -> dict[str, dict[str, Any]]:
|
||||
"""측점별 벽(532-4 · Node 한 번 약 2 s) — 복사본은 동결이라 복사 지문으로 기억."""
|
||||
return walls_mod.walls_by_station(source)
|
||||
|
||||
|
||||
def plan_run(root: Path, ground: dict[str, Any]) -> dict[str, Any]:
|
||||
"""측점마다 입력 · 지문 + 전체 지문(판정 기준 포함) — 복사본이 없으면 400."""
|
||||
try:
|
||||
@@ -126,10 +134,16 @@ def plan_run(root: Path, ground: dict[str, Any]) -> dict[str, Any]:
|
||||
"road_surcharge_kpa": float(ground.get("road_surcharge_kpa") or 0.0),
|
||||
}
|
||||
resolved = {chainage_key(r["chainage_m"]): r for r in ground_model.resolve_stations(ground)}
|
||||
try:
|
||||
found = _walls(str(source), copy_fp)
|
||||
except RuntimeError as error:
|
||||
raise HTTPException(status_code=500, detail=str(error)) from error
|
||||
items = []
|
||||
for station in stations:
|
||||
key = chainage_key(station["chainage_m"])
|
||||
layers = _layers(resolved.get(key))
|
||||
entry = found.get(key)
|
||||
parts = [engine_hash(), copy_fp, shared, key, layers] + ([entry] if entry else [])
|
||||
items.append(
|
||||
{
|
||||
"key": key,
|
||||
@@ -138,7 +152,8 @@ def plan_run(root: Path, ground: dict[str, Any]) -> dict[str, Any]:
|
||||
"ground_type": station.get("ground_type"),
|
||||
"center": [station.get("center_x"), station.get("center_y")],
|
||||
"layers": layers,
|
||||
"fp": derived_key(engine_hash(), copy_fp, shared, key, layers),
|
||||
"walls": entry,
|
||||
"fp": derived_key(*parts), # 벽 없는 측점 = 종전 지문 꼴
|
||||
}
|
||||
)
|
||||
factors = ground.get("safety_factors") or {}
|
||||
@@ -153,7 +168,12 @@ def _station_input(source: Path, item: dict[str, Any], sampler: Any) -> dict[str
|
||||
ground = widen_ground(cross, sampler) if sampler else ground_from_cross(cross)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
ground = ground_from_cross(cross)
|
||||
return {"cross": cross, "design": cross["design"], "ground": [list(p) for p in ground]}
|
||||
return {
|
||||
"cross": cross,
|
||||
"design": walls_mod.design_with_walls(cross["design"], item.get("walls")),
|
||||
"walls": walls_mod.walls_of(item.get("walls")),
|
||||
"ground": [list(p) for p in ground],
|
||||
}
|
||||
|
||||
|
||||
def _sampler(source: Path) -> Any:
|
||||
@@ -166,7 +186,12 @@ def _sampler(source: Path) -> Any:
|
||||
|
||||
def _meta(item: dict[str, Any], design: dict[str, Any]) -> dict[str, Any]:
|
||||
keys = ("key", "chainage_m", "label", "ground_type", "center", "fp")
|
||||
return {**{k: item[k] for k in keys}, "section_mode": design.get("section_mode")}
|
||||
walls = len(walls_mod.walls_of(item.get("walls")) or [])
|
||||
return {
|
||||
**{k: item[k] for k in keys},
|
||||
"section_mode": design.get("section_mode"),
|
||||
"walls": walls,
|
||||
}
|
||||
|
||||
|
||||
def _run_worker(job: dict[str, Any], emit: Any) -> None:
|
||||
@@ -323,8 +348,9 @@ async def result2d(
|
||||
summary, fresh = _latest_summary(root, plan["fp"])
|
||||
editable = e02._editable(session, project)
|
||||
running = bool((_JOBS.get(str(project_id)) or {}).get("running"))
|
||||
missing = src.missing_stations(plan["source"]) # 횡단 없어 해석에서 빠진 측점(화면 띠)
|
||||
if not summary:
|
||||
rows = [view.station_row(item, None, plan["factors"]) for item in plan["items"]]
|
||||
rows = [view.station_row(_meta(item, {}), None, plan["factors"]) for item in plan["items"]]
|
||||
view.mark_representative(rows, _overrides(root))
|
||||
return {
|
||||
"status": "none",
|
||||
@@ -334,6 +360,7 @@ async def result2d(
|
||||
"conditions": plan["shared"]["conditions"],
|
||||
"factors": plan["factors"],
|
||||
"stations": rows,
|
||||
"missing_stations": missing,
|
||||
}
|
||||
rows = summary["stations"]
|
||||
view.mark_representative(rows, _overrides(root))
|
||||
@@ -344,6 +371,7 @@ async def result2d(
|
||||
"running": running,
|
||||
**{k: v for k, v in summary.items() if k != "stations"},
|
||||
"stations": rows,
|
||||
"missing_stations": missing,
|
||||
}
|
||||
|
||||
|
||||
@@ -360,18 +388,22 @@ async def section(project_id: UUID, station_key: str) -> dict[str, Any]:
|
||||
raise HTTPException(status_code=404, detail="그 측점 해석 결과가 없습니다.")
|
||||
value = saved["value"]
|
||||
cross = src.cross_section(plan["source"], row["chainage_m"])
|
||||
entry = next((it["walls"] for it in plan["items"] if it["key"] == station_key), None)
|
||||
design = walls_mod.design_with_walls(cross["design"], entry)
|
||||
kw = {
|
||||
"materials": plan["shared"]["materials"],
|
||||
"layers": value["inputs"]["layers"],
|
||||
"ground": [tuple(p) for p in value["inputs"]["ground"]],
|
||||
"road_surcharge_kpa": plan["shared"]["road_surcharge_kpa"],
|
||||
"walls": walls_mod.walls_of(entry),
|
||||
}
|
||||
dry = section_model_from_b06(cross, cross["design"], **kw)
|
||||
wet = section_model_from_b06(cross, cross["design"], wet=True, **kw)
|
||||
water = {
|
||||
cond: (wet["groundwater"].get("line") if cond == "wet" else None)
|
||||
for cond in summary["conditions"]
|
||||
}
|
||||
dry = section_model_from_b06(cross, design, **kw)
|
||||
water = {}
|
||||
for cond in summary["conditions"]:
|
||||
# 수위선 = E02 고른 갈래(r_u · 없음 = 선 없음)
|
||||
model = section_model_from_b06(cross, design, wet=cond == "wet", **kw)
|
||||
gw = ground_model.station_groundwater(ground, cond, model)
|
||||
water[cond] = gw.get("line") if gw.get("mode") == "line" else None
|
||||
out = view.section_view(row, value["out"], dry, water)
|
||||
out["fresh"] = fresh
|
||||
return out
|
||||
|
||||
@@ -82,7 +82,12 @@ export function buildList(
|
||||
onToggle(row.key);
|
||||
});
|
||||
tr.append(
|
||||
el("td", { text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key }),
|
||||
el("td", {
|
||||
text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key,
|
||||
children: row.walls
|
||||
? [el("span", { className: "e03-wall-tag", text: "▦", attrs: { title: t("E03_Walls") } })]
|
||||
: [],
|
||||
}),
|
||||
el("td", {
|
||||
text: row.sides.length
|
||||
? roleText(row, { cut: t("E03_CutShort"), fill: t("E03_FillShort") })
|
||||
|
||||
@@ -20,12 +20,14 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
|
||||
import { buildList } from "./E03_StabSection_UI_List";
|
||||
import { buildPlan } from "./E03_StabSection_UI_Plan";
|
||||
import { buildResult } from "./E03_StabSection_UI_Result";
|
||||
import {
|
||||
buildLegends,
|
||||
createSectionCanvas,
|
||||
WALL_ID,
|
||||
type SectionToggles,
|
||||
} from "./E03_StabSection_UI_Section";
|
||||
import { openView3d } from "./E03_StabSection_UI_View3D";
|
||||
@@ -46,6 +48,8 @@ interface Result2d {
|
||||
computed_at?: string;
|
||||
summary?: Record<string, unknown>;
|
||||
stations: StationRow[];
|
||||
/** 횡단 없어 해석에서 빠진 측점 누가거리(E01 복사본) */
|
||||
missing_stations?: number[];
|
||||
}
|
||||
|
||||
interface Progress {
|
||||
@@ -156,6 +160,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
panel.append(controls, ngBox, status, listHost, actions);
|
||||
|
||||
// ---- 본문 ----
|
||||
const missingHost = el("div");
|
||||
const planHost = el("div");
|
||||
const toolbar = el("div", { className: "e03-card__head" });
|
||||
const sectionCard = el("div", { className: "e03-card" });
|
||||
@@ -168,7 +173,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
const resultHost = el("div", { className: "e03-result", attrs: { style: "min-height: 0" } });
|
||||
const body = el("div", { className: "e03-body" });
|
||||
body.append(sectionCard, resultHost);
|
||||
main.append(planHost, body);
|
||||
main.append(missingHost, planHost, body);
|
||||
|
||||
const repOf = (row: StationRow): { on: boolean; user: boolean | null } => {
|
||||
const server = row.representative ?? { auto: [], user: null, on: false };
|
||||
@@ -290,10 +295,9 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
toolbar.append(title, sideField.root, flags);
|
||||
const draw = { view, analysis, toggles, picked };
|
||||
canvas.render(draw);
|
||||
legendHost.append(
|
||||
buildLegends(draw),
|
||||
el("p", { className: "e03-muted", text: t("E03_NoWalls") }),
|
||||
);
|
||||
legendHost.append(buildLegends(draw));
|
||||
if (view.regions.some((r) => r.material_id === WALL_ID))
|
||||
legendHost.append(el("p", { className: "e03-muted", text: t("E03_Walls_Hint") }));
|
||||
resultHost.append(
|
||||
buildResult(view, analysis, picked, (i) => {
|
||||
picked = i;
|
||||
@@ -335,6 +339,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
result = await api<Result2d>(`${base}/result2d`);
|
||||
if (before === "none" && result.status === "ok") active = null; // 첫 해석 뒤 = 가장 낮은 측점으로
|
||||
sections.clear();
|
||||
missingHost.replaceChildren(buildMissingBanner(result.missing_stations));
|
||||
const conds = result.conditions;
|
||||
if (!conds.includes(condition)) condition = conds[0] ?? "dry";
|
||||
condField.setOptions(
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
type Point,
|
||||
type SectionView,
|
||||
} from "./logic/E03_StabSection_Logic";
|
||||
import { materialClass, svgEl } from "./E03_StabSection_UI_Section";
|
||||
import { WALL_ID, materialClass, svgEl } from "./E03_StabSection_UI_Section";
|
||||
|
||||
const fmt = (v: number | null | undefined, d = 2): string =>
|
||||
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
|
||||
@@ -32,7 +32,7 @@ export function verdictChip(ok: boolean | null): HTMLElement {
|
||||
});
|
||||
}
|
||||
|
||||
function kpi(analysis: Analysis): HTMLElement {
|
||||
function kpi(analysis: Analysis, walls: number): HTMLElement {
|
||||
const c = analysis.critical;
|
||||
const dl = el("dl", { className: "e03-kpi" });
|
||||
const row = (label: string, value: HTMLElement | string, big = false): void => {
|
||||
@@ -54,6 +54,8 @@ function kpi(analysis: Analysis): HTMLElement {
|
||||
: "-",
|
||||
);
|
||||
row(t("E03_Height"), `${fmt(analysis.height_m)} m`);
|
||||
if (walls)
|
||||
row(t("E03_Walls"), el("span", { text: String(walls), attrs: { "data-role": "walls" } }));
|
||||
if (c?.center) {
|
||||
row(t("E03_Center"), `${fmt(c.center[0])}, ${fmt(c.center[1])}`);
|
||||
row(t("E03_Radius"), `${fmt(c.radius)} m`);
|
||||
@@ -284,7 +286,8 @@ export function buildResult(
|
||||
}
|
||||
const head = (key: LocaleKey): HTMLElement =>
|
||||
el("h3", { className: "e03-card__title", text: t(key) });
|
||||
root.append(head("E03_Result"), kpi(analysis), methodTable(analysis));
|
||||
const walls = view.regions.filter((r) => r.material_id === WALL_ID).length;
|
||||
root.append(head("E03_Result"), kpi(analysis, walls), methodTable(analysis));
|
||||
root.append(head("E03_Slices"), sliceTable(analysis, picked, onPick));
|
||||
root.append(head("E03_Fbd"), freeBody(view, analysis, picked));
|
||||
root.append(head("E03_Graph"), stressGraph(analysis, picked));
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_UI_Section.ts
|
||||
* E03 가운데 횡단(PLAN 532-3) — 지층 색 · 지표 · 지하수선 · 노면 하중 · 등안전율 색 지도(원호 중심 격자
|
||||
* E03 가운데 횡단(PLAN 532-3) — 지층 색 · 벽(회색 빗금 · 532-5) · 지표 · 지하수선 · 노면 하중 · 등안전율 색 지도(원호 중심 격자
|
||||
* 최소 안전율) · 원호 중심 점 · 다른 후보(옅게) · 임계 활동면(굵게 · 중심 · 반경) · 절편(누르면 고름).
|
||||
* 도형 좌표 = 결과 좌표 그대로 · 묶음 transform(`viewMatrix`) 하나 — 계산은 logic.
|
||||
* 줌 · 팬 = 공용 `attachSvgZoomPan`(휠 = 줌 · 가운데 끌기 = 팬 · 두 번 누름 = 원배율).
|
||||
* ========================================================================== */
|
||||
|
||||
import "./E03_StabSection_UI_Walls.css";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
@@ -27,6 +28,8 @@ import {
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const PAD = 28;
|
||||
const MAX_SCALE = 30;
|
||||
/** 벽(옹벽 · 석축) 물성 id — 서버 `WALL_MATERIAL` */
|
||||
export const WALL_ID = "wall";
|
||||
|
||||
export function svgEl<K extends keyof SVGElementTagNameMap>(
|
||||
tag: K,
|
||||
@@ -87,7 +90,9 @@ export function buildLegends(draw: SectionDraw): HTMLElement {
|
||||
for (const m of draw.view.materials.filter((x) => used.has(x.id))) {
|
||||
const item = el("span", { className: "e03-legend__item" });
|
||||
item.append(
|
||||
el("span", { className: `e03-legend__swatch ${materialClass(draw.view, m.id)}` }),
|
||||
el("span", {
|
||||
className: `e03-legend__swatch ${m.id === WALL_ID ? "e03-wall-swatch" : materialClass(draw.view, m.id)}`,
|
||||
}),
|
||||
m.name,
|
||||
);
|
||||
box.append(item);
|
||||
@@ -121,10 +126,34 @@ export function createSectionCanvas(onPick: (index: number | null) => void): Sec
|
||||
const ns = { "vector-effect": "non-scaling-stroke" };
|
||||
const unit = 1 / fit.k; // 1 px 의 결과 좌표 길이
|
||||
|
||||
// 벽 빗금 — 결과 좌표 패턴(화면 6 px 간격)
|
||||
const hatch = svgEl("pattern", {
|
||||
id: "e03-wall-hatch",
|
||||
patternUnits: "userSpaceOnUse",
|
||||
width: 6 * unit,
|
||||
height: 6 * unit,
|
||||
patternTransform: "rotate(45)",
|
||||
});
|
||||
hatch.append(
|
||||
svgEl("rect", { width: 6 * unit, height: 6 * unit, class: "e03-wall-hatch__bg" }),
|
||||
svgEl("line", {
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 0,
|
||||
y2: 6 * unit,
|
||||
"stroke-width": 1.5 * unit,
|
||||
class: "e03-wall-hatch__line",
|
||||
}),
|
||||
);
|
||||
const defs = svgEl("defs");
|
||||
defs.append(hatch);
|
||||
world.append(defs);
|
||||
for (const region of view.regions) {
|
||||
const wall = region.material_id === WALL_ID;
|
||||
const ring = svgEl("polygon", {
|
||||
points: pointsText(region.polygon),
|
||||
class: `e03-region ${materialClass(view, region.material_id)}`,
|
||||
class: wall ? "e03-wall" : `e03-region ${materialClass(view, region.material_id)}`,
|
||||
...(wall ? { fill: "url(#e03-wall-hatch)", "data-role": "wall" } : {}),
|
||||
...ns,
|
||||
});
|
||||
world.append(ring);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/* E03 횡단 벽(옹벽 · 석축) 회색 빗금 · 목록 표시(532-5) */
|
||||
.e03-wall {
|
||||
stroke: var(--e03-surface-line);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.e03-wall-hatch__bg {
|
||||
fill: #9a9a9a;
|
||||
fill-opacity: 0.35;
|
||||
}
|
||||
|
||||
.e03-wall-hatch__line {
|
||||
stroke: #6b6b6b;
|
||||
}
|
||||
|
||||
.e03-wall-swatch {
|
||||
background: repeating-linear-gradient(45deg, #6b6b6b 0 2px, #c8c8c8 2px 5px);
|
||||
}
|
||||
|
||||
.e03-wall-tag {
|
||||
margin-left: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
@@ -76,6 +76,7 @@ def station_row(
|
||||
"ground_type": meta.get("ground_type"),
|
||||
"center": meta.get("center"),
|
||||
"fp": meta.get("fp"),
|
||||
"walls": meta.get("walls") or 0,
|
||||
"sides": list(sides.values()),
|
||||
"cases": cases,
|
||||
"min": minimum,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
`python -m E03_StabSection.E03_StabSection_Worker <job.json>` — 서버(`Router.run_all`)가 띄움.
|
||||
측점마다 프로세스로 나눠 돌리고 끝날 때마다 파생 저장본을 쓰고 한 줄(JSON)을 찍음 → 서버 진행률.
|
||||
서버 프로세스에서 바로 프로세스 풀을 열면 자식마다 `main.py` 를 다시 읽어 이 모듈을 입구로 둠.
|
||||
job = {root, keep, kwargs, workers, items: [{fp, meta, layers, cross, design, ground}]}.
|
||||
job = {root, keep, kwargs, workers, items: [{fp, meta, layers, cross, design, walls, ground}]}.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,6 +24,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def _analyze(item: dict[str, Any], kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||
kw = {**kwargs, "layers": item["layers"], "ground": [tuple(p) for p in item["ground"]]}
|
||||
if item.get("walls"):
|
||||
kw["walls"] = item["walls"]
|
||||
return analyze_station(item["cross"], item["design"], **kw)
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface StationRow {
|
||||
min: Partial<Record<Condition, number | null>>;
|
||||
verdict: Partial<Record<Condition, Verdict>>;
|
||||
error: string | null;
|
||||
/** 그 측점 벽(옹벽 · 석축) 수 — 옛 저장본은 없음 */
|
||||
walls?: number;
|
||||
representative?: { auto: string[]; user: boolean | null; on: boolean };
|
||||
}
|
||||
|
||||
|
||||
@@ -140,4 +140,10 @@ export const ui_locales_e = {
|
||||
E02_Err_empty: ["지층이 하나는 있어야 합니다", "At least one layer is required"],
|
||||
E02_Err_type: ["꼴이 틀립니다", "Invalid shape"],
|
||||
E02_Err_few_points: ["점이 둘 이상 있어야 합니다", "At least two points are required"],
|
||||
|
||||
/* --- E02 · E03 빠진 횡단 띠 --- */
|
||||
E_Missing_Head: [
|
||||
"횡단 없는 측점 {n} 개 — 해석에서 빠짐",
|
||||
"{n} stations without a cross-section — left out of the analysis",
|
||||
],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
@@ -73,9 +73,10 @@ export const ui_locales_e03 = {
|
||||
E03_Legend_Map: ["등안전율(Fs)", "Safety factor"],
|
||||
E03_Legend_Layers: ["지층", "Layers"],
|
||||
E03_Offset: ["거리", "Offset"],
|
||||
E03_NoWalls: [
|
||||
"옹벽 · 석축은 아직 해석에 반영 전입니다(벽 없는 단면으로 셈).",
|
||||
"Retaining walls and masonry are not yet reflected (analyzed without walls).",
|
||||
E03_Walls: ["벽 있음", "Walls"],
|
||||
E03_Walls_Hint: [
|
||||
"옹벽 · 석축(회색 빗금)은 강한 물체로 넣어 활동면이 지나지 않습니다.",
|
||||
"Retaining walls (gray hatch) are modeled as rigid bodies — slip surfaces do not cross them.",
|
||||
],
|
||||
|
||||
/* --- 우측 결과 --- */
|
||||
|
||||
Reference in New Issue
Block a user