feat(532-4): E03 2D 모형 벽 — B06 벽 도형 TS 서버 Node 묶음(build:e03-walls) · 복사본 구조물 → 측점별 walls · 벽 선 측점 지표 = 그려지는 설계선
- E03_StabSection_Walls_Node.ts — B06 computeStoredLayouts(옹벽 · 석축 · 돌쌓기 · 관 기슭막이) 꼭짓점 · drawnProfile 지표 · 계산 없음
- E03_StabSection_Walls.py — walls_by_station(복사본 루트) → {chainage_key: {walls, design_line}} · design_with_walls · walls_of · 입구 TS 가 묶음보다 새로우면 다시 지음
- common_util_cross_structure_areas.ts drawnProfile 내보냄(동작 그대로) · package.json build:e03-walls 줄 · .gitignore 묶음 폴더
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,5 +72,7 @@ config/b07_extent_node/
|
||||
config/b07_note_node/
|
||||
# 계산 등록부 서버 Node 입구 번들 — `npm run build:design-calc` 산출물(472-3).
|
||||
config/design_calc_node/
|
||||
# E03 안정성 모형 벽 도형 번들 — `npm run build:e03-walls` 산출물(532-4).
|
||||
config/e03_walls_node/
|
||||
|
||||
~$*
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""E03 2D 모형의 벽(옹벽 · 석축 · 돌쌓기 · 관 기슭막이) — 복사본 → 측점별 walls
|
||||
(532-4 · 분석 345-D ② ⑤).
|
||||
|
||||
도형은 짓지 않음 — B06 화면 TS 를 서버 Node 로 돌림(`E03_StabSection_Walls_Node.ts` · 5장 ②).
|
||||
얹기(어느 측점에 어느 벽) = B06 `attach_saved_shapes` 그대로(복사본 = 원본 프로젝트 배치).
|
||||
벽 재료 = 모형 `WALL_MATERIAL`(고강도 — 활동면이 벽을 못 지남) · 꼴 = `build_section_model(walls)`.
|
||||
벽이 선 측점은 지표도 B06 면적과 같은 「그려지는 설계선」(벽 상단에서 끊김)으로 바꿔 넘김.
|
||||
|
||||
부르는 법(E03 분석 길):
|
||||
found = walls_by_station(root) # 복사본 루트 → {chainage_key: entry}
|
||||
entry = found.get(chainage_key(cross["chainage_m"]))
|
||||
model = section_model_from_b06(cross, design_with_walls(design, entry), walls=walls_of(entry))
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_node_bundle import ROOT, build_bundle, bundle_stale, run_bundle_json
|
||||
from common_util.common_util_station_key import chainage_key
|
||||
from E01_StabRegister import E01_StabRegister_Source as source
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import WALL_MATERIAL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NPM_SCRIPT = "build:e03-walls"
|
||||
ENTRY = ROOT / "E03_StabSection" / "E03_StabSection_Walls_Node.ts"
|
||||
BUNDLE = ROOT / "config" / "e03_walls_node" / "E03_StabSection_Walls_Node.js"
|
||||
|
||||
|
||||
def _sections(root: Path) -> list[dict[str, Any]]:
|
||||
"""복사본 측점 → B06 상세 꼴(chainage_m · 중심 · samples · design)
|
||||
+ 형상 얹기(revetment · culvert …)."""
|
||||
from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes
|
||||
|
||||
sections = []
|
||||
for station in source.stations(root):
|
||||
cross = source.cross_section(root, station["chainage_m"])
|
||||
sections.append(
|
||||
{
|
||||
**cross,
|
||||
"center_x": station.get("center_x"),
|
||||
"center_y": station.get("center_y"),
|
||||
}
|
||||
)
|
||||
route = source.db_snapshot(root).get("route") or {}
|
||||
attach_saved_shapes(root, sections, route.get("route_data_path"))
|
||||
return sections
|
||||
|
||||
|
||||
def _fresh_bundle() -> bool:
|
||||
"""입구 TS 는 `SOURCE_DIRS` 밖(E03) — 입구가 묶음보다 새로우면 여기서 다시 지음."""
|
||||
stale = bundle_stale(BUNDLE) or ENTRY.stat().st_mtime > BUNDLE.stat().st_mtime
|
||||
return not stale or build_bundle(NPM_SCRIPT)
|
||||
|
||||
|
||||
def walls_by_station(root: Path | str) -> dict[str, dict[str, Any]]:
|
||||
"""복사본 루트 → {chainage_key: {walls: [모형 벽 …], design_line: [[offset, z] …] | None}}.
|
||||
|
||||
벽 · 트림이 없는 측점은 안 실음(그 측점 모형 = 종전 그대로). Node 실패는 던짐 — 벽을 빼고
|
||||
해석하면 활동면이 벽을 지난 값이 조용히 남음.
|
||||
"""
|
||||
return walls_for_sections(_sections(Path(root)))
|
||||
|
||||
|
||||
def walls_for_sections(sections: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||
"""형상 얹은 B06 횡단 목록 → 측점 entry(`walls_by_station` 몸 · 시험이 바로 부름)."""
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_definition
|
||||
|
||||
if not any(s.get("revetment") or s.get("culvert") for s in sections):
|
||||
return {}
|
||||
if not _fresh_bundle():
|
||||
raise RuntimeError("E03 벽 도형 번들을 못 지음")
|
||||
payload = json.loads(
|
||||
json.dumps({"definition": load_definition(), "sections": sections}, default=str)
|
||||
)
|
||||
out = run_bundle_json(BUNDLE, NPM_SCRIPT, payload)
|
||||
if not isinstance(out, dict):
|
||||
raise RuntimeError("E03 벽 도형 Node 실패")
|
||||
found: dict[str, dict[str, Any]] = {}
|
||||
for row in out["stations"]:
|
||||
walls = [
|
||||
{
|
||||
"material_id": WALL_MATERIAL["id"],
|
||||
"kind": w["kind"],
|
||||
"name": w.get("name"),
|
||||
"structure_id": w.get("structure_id"),
|
||||
"form": w.get("form"),
|
||||
"polygon": w["polygon"],
|
||||
}
|
||||
for w in row["walls"]
|
||||
]
|
||||
if walls or row.get("design_line"):
|
||||
found[chainage_key(float(row["chainage_m"]))] = {
|
||||
"walls": walls,
|
||||
"design_line": row.get("design_line"),
|
||||
}
|
||||
return found
|
||||
|
||||
|
||||
def walls_of(entry: dict[str, Any] | None) -> list[dict[str, Any]] | None:
|
||||
"""측점 entry → `section_model_from_b06(walls=…)` 값(없으면 None = 벽 없음)."""
|
||||
return (entry or {}).get("walls") or None
|
||||
|
||||
|
||||
def design_with_walls(design: dict[str, Any], entry: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""벽이 선 측점 = 설계선을 그려지는 선(벽 상단에서 끊김)으로 바꾼 사본 · 아니면 그대로."""
|
||||
line = (entry or {}).get("design_line")
|
||||
if not line:
|
||||
return design
|
||||
return {**design, "design_line": [{"offset_m": o, "elevation_m": z} for o, z in line]}
|
||||
@@ -0,0 +1,102 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_Walls_Node.ts
|
||||
* E 2D 해석 모형의 벽(옹벽 · 석축 · 돌쌓기 · 관 기슭막이) 단면 꼭짓점을 **서버가** 내는 입구(532-4).
|
||||
*
|
||||
* 도형은 B06 화면이 그리는 TS 그대로(`computeStoredLayouts` — 정본만 읽는 한 벌 · CLAUDE.md 5장 ②) ·
|
||||
* 지표 = B06 면적과 같은 「실제로 그려지는 설계선」(`drawnProfile` · 벽 상단에서 끊김). 여기에 계산 없음.
|
||||
* 파이썬 껍데기 = `E03_StabSection_Walls.py` · 번들 = `npm run build:e03-walls`.
|
||||
*
|
||||
* 실행: node <번들> <입력.json> <출력.json>
|
||||
* 입력 { definition?: 구조물 정의 · sections: 형상 얹은 B06 횡단(chainage_m · samples · design · revetment …) }
|
||||
* 출력 { stations: [{ chainage_m, walls: [{ kind, name, structure_id, form, polygon: [[offset, z], …] }],
|
||||
* design_line: [[offset, z], …] | null(트림 없음 = 설계선 그대로) }] } — 입력 차례 그대로
|
||||
* 끝 코드: 0 성공 / 2 인자 오류
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { drawnProfile } from "@util/common_util_cross_structure_areas";
|
||||
import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog";
|
||||
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { WallLayout } from "../B06_Section/logic/B06_Section_Cross_Culvert_Types";
|
||||
import {
|
||||
computeStoredLayouts,
|
||||
trimOfLayouts,
|
||||
} from "../B06_Section/logic/B06_Section_Structure_Layouts";
|
||||
|
||||
/** 트림 경계 바로 바깥 점 — 벽 쪽 연직 단차를 살림(`computeStructureAreas` 와 같은 값). */
|
||||
const BOUNDARY_EPS_M = 1e-6;
|
||||
|
||||
type Pair = [number, number];
|
||||
|
||||
function stationWalls(section: CrossSection, sections: readonly CrossSection[]) {
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
const walls: Array<{
|
||||
kind: string;
|
||||
name: string | null;
|
||||
structure_id: string | null;
|
||||
form: string | null;
|
||||
polygon: Pair[];
|
||||
}> = [];
|
||||
const push = (kind: string, list: readonly WallLayout[] | undefined, name: string | null) => {
|
||||
for (const wall of list ?? []) {
|
||||
if (wall.points.length >= 3)
|
||||
walls.push({
|
||||
kind,
|
||||
name,
|
||||
structure_id: kind === "own" ? (layouts?.own?.structureId ?? null) : null,
|
||||
form: wall.form,
|
||||
polygon: wall.points.map((p) => [p.offset, p.elevation]),
|
||||
});
|
||||
}
|
||||
};
|
||||
push("own", layouts?.own?.walls, layouts?.own?.name ?? null);
|
||||
push("culvert", layouts?.culvert?.walls, "기슭막이");
|
||||
push("culvert", layouts?.culvert?.basinExtras, "기슭막이");
|
||||
const trim = layouts ? trimOfLayouts(layouts) : null;
|
||||
const line = layouts?.design.design_line;
|
||||
let design_line: Pair[] | null = null;
|
||||
if (trim && Array.isArray(line) && line.length >= 2) {
|
||||
const ground = section.samples
|
||||
.filter((s) => s.valid !== false && typeof s.elevation_m === "number")
|
||||
.map((s) => ({ offset: s.offset_m ?? 0, elevation: s.elevation_m as number }))
|
||||
.sort((a, b) => a.offset - b.offset);
|
||||
const profile = drawnProfile({
|
||||
designLine: line as Array<{ offset_m: number; elevation_m: number }>,
|
||||
ground,
|
||||
trim,
|
||||
});
|
||||
if (profile && ground.length >= 2) {
|
||||
const lo = ground[0].offset;
|
||||
const hi = ground[ground.length - 1].offset;
|
||||
const grid = new Set<number>();
|
||||
const add = (o: number) => {
|
||||
if (o >= lo && o <= hi) grid.add(Math.round(o * 1e6) / 1e6);
|
||||
};
|
||||
ground.forEach((g) => add(g.offset));
|
||||
line.forEach((p) => add(p.offset_m));
|
||||
for (const b of [trim.minOffset, trim.maxOffset])
|
||||
[b, b - BOUNDARY_EPS_M, b + BOUNDARY_EPS_M].forEach(add);
|
||||
trim.minSlope?.points.forEach((p) => add(p.offset));
|
||||
trim.maxSlope?.points.forEach((p) => add(p.offset));
|
||||
design_line = [...grid].sort((a, b) => a - b).map((o) => [o, profile.drawnZ(o)]);
|
||||
}
|
||||
}
|
||||
return { chainage_m: section.chainage_m, walls, design_line };
|
||||
}
|
||||
|
||||
const [inputPath, outputPath] = process.argv.slice(2);
|
||||
if (!inputPath || !outputPath) {
|
||||
console.error("사용법: node <번들> <입력.json> <출력.json>");
|
||||
process.exit(2);
|
||||
}
|
||||
const input = JSON.parse(readFileSync(inputPath, "utf8")) as {
|
||||
definition?: CatalogDefinition | null;
|
||||
sections: CrossSection[];
|
||||
};
|
||||
if (input.definition) useCatalogDefinition(input.definition);
|
||||
const sections = input.sections ?? [];
|
||||
writeFileSync(
|
||||
outputPath,
|
||||
JSON.stringify({ stations: sections.map((s) => stationWalls(s, sections)) }),
|
||||
"utf8",
|
||||
);
|
||||
@@ -72,8 +72,9 @@ function interpolator(
|
||||
};
|
||||
}
|
||||
|
||||
/** 폐회로의 위(실제로 그려지는 설계선)·아래(지반선) 보간기 — 면적과 벽 겹침이 같은 선을 봄. */
|
||||
function drawnProfile(
|
||||
/** 폐회로의 위(실제로 그려지는 설계선)·아래(지반선) 보간기 — 면적과 벽 겹침이 같은 선을 봄 ·
|
||||
* E03 안정성 모형 지표도 이 선(`E03_StabSection_Walls_Node.ts`). */
|
||||
export function drawnProfile(
|
||||
input: StructureAreaInput,
|
||||
): { drawnZ: (offset: number) => number; ground: (offset: number) => number } | null {
|
||||
const design = interpolator(
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"build:b07-extent": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Cross_Extent_Node.ts --outDir ../config/b07_extent_node",
|
||||
"build:b07-note": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Note_Options_Node.ts --outDir ../config/b07_note_node",
|
||||
"build:design-calc": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../A00_Common/design/logic/design_calc_node.ts --outDir ../config/design_calc_node",
|
||||
"build:e03-walls": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../E03_StabSection/E03_StabSection_Walls_Node.ts --outDir ../config/e03_walls_node",
|
||||
"install:b07-cad": "npm --prefix B07_DesignDetail/openwebcad install",
|
||||
"build:b07-cad": "npm run install:b07-cad && npm --prefix B07_DesignDetail/openwebcad run build",
|
||||
"preview": "node ./config/node_modules/vite/bin/vite.js preview --configLoader runner",
|
||||
|
||||
Reference in New Issue
Block a user