Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -9,7 +9,10 @@
|
||||
* context?: { earthwork_conversion · natural_spoil_min_ground_slope ·
|
||||
* haul_equipment_limits · 채집석 · 구조물 잔토(B08) } ·
|
||||
* excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m ·
|
||||
* has_foundation }] } } }
|
||||
* has_foundation }] } ·
|
||||
* cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) ·
|
||||
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } ·
|
||||
* berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } } }
|
||||
* 출력 { results: { [id]: 값 }, available: [id…] }
|
||||
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
|
||||
* server-calc 묶음과 같은 차례).
|
||||
@@ -17,6 +20,10 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
|
||||
import type { BermSpec } from "@util/common_util_cross_berm";
|
||||
import { computeCrossDesign } from "@util/common_util_cross_design";
|
||||
import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design";
|
||||
import { pipeTrenchWidthM, wallTrenchAreaM2, wallTrenchWidthM } from "@util/common_util_excavation";
|
||||
import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
|
||||
import type { MassHaulResult } from "@util/common_util_mass_haul";
|
||||
@@ -58,6 +65,67 @@ interface CalcInputs {
|
||||
has_foundation?: boolean;
|
||||
}>;
|
||||
};
|
||||
berm?: {
|
||||
structures: Parameters<typeof bermSpansFromStructures>[0];
|
||||
chainages: number[];
|
||||
};
|
||||
cross_design?: {
|
||||
standard: CrossDesignOptions["standard"];
|
||||
stations?: Array<{
|
||||
samples: CrossGroundSample[];
|
||||
design_elevation_m: number | null;
|
||||
options: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
/** 파이썬 `compute_cross_design` 키 → TS 짝 키(거울 시험 test_b06_cross_design_mirror 표와 같음). */
|
||||
const CROSS_OPTION_KEYS: Record<string, keyof CrossDesignOptions> = {
|
||||
ground_type: "groundType",
|
||||
section_mode: "sectionMode",
|
||||
ditch_side: "ditchSide",
|
||||
ditch_type: "ditchType",
|
||||
paved: "paved",
|
||||
rock_boundary_offset_m: "rockBoundaryOffsetM",
|
||||
two_stage_slope: "twoStageSlope",
|
||||
cut_slope_ratio: "cutSlopeRatio",
|
||||
ditch_enabled: "ditchEnabled",
|
||||
ditch_choice: "ditchChoice",
|
||||
surface_drop_m: "surfaceDropM",
|
||||
plan_radius_m: "planRadiusM",
|
||||
curve_outer_side: "curveOuterSide",
|
||||
curve_widening_m: "curveWideningM",
|
||||
shoulder_extend_left_m: "shoulderExtendLeftM",
|
||||
shoulder_extend_right_m: "shoulderExtendRightM",
|
||||
tilted: "tilted",
|
||||
tilt_pct: "tiltPct",
|
||||
};
|
||||
|
||||
/** 파이썬 BermSpec(NamedTuple → dict) → TS BermSpec. */
|
||||
function bermOf(value: unknown): BermSpec | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const berm = value as Record<string, number | null>;
|
||||
return {
|
||||
widthM: berm.width_m as number,
|
||||
intervalM: berm.interval_m as number,
|
||||
slopeDeg: berm.slope_deg as number,
|
||||
depthM: berm.depth_m as number,
|
||||
depthLeftM: berm.depth_left_m ?? null,
|
||||
depthRightM: berm.depth_right_m ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function crossOptions(
|
||||
raw: Record<string, unknown>,
|
||||
standard: CrossDesignOptions["standard"],
|
||||
): CrossDesignOptions {
|
||||
const options: Record<string, unknown> = { standard };
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (key === "berm") options.berm = bermOf(value);
|
||||
else if (key in CROSS_OPTION_KEYS) options[CROSS_OPTION_KEYS[key]] = value;
|
||||
else throw new Error(`모르는 횡단 설계 키: ${key}`);
|
||||
}
|
||||
return options as unknown as CrossDesignOptions;
|
||||
}
|
||||
|
||||
/** 한 번 돌림 — 같은 id 는 한 번만 셈(memo) · 앞 id 는 `need` 로. */
|
||||
@@ -117,6 +185,23 @@ function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
),
|
||||
})),
|
||||
}),
|
||||
// 소단 구간(C4) — 구조물 정본 → 구간 · 측점마다 제원(겹치면 먼저 놓은 것).
|
||||
"cross.berm_spans": () => {
|
||||
const spans = bermSpansFromStructures(inputs.berm?.structures ?? []);
|
||||
return { spans, at: (inputs.berm?.chainages ?? []).map((c) => bermSpecAt(spans, c)) };
|
||||
},
|
||||
// 측점마다 TS 짝(`computeCrossDesign`) — 못 세우면 그 자리 { error }(파이썬은 ValueError 로 되던짐).
|
||||
"cross.design": () => {
|
||||
const batch = inputs.cross_design;
|
||||
return (batch?.stations ?? []).map((station) => {
|
||||
try {
|
||||
const options = crossOptions(station.options, batch!.standard);
|
||||
return computeCrossDesign(station.samples, station.design_elevation_m, options);
|
||||
} catch (error) {
|
||||
return { error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
});
|
||||
},
|
||||
// 편집 전 정본 — 계획선 샘플은 저장분 그대로(corridor 묶음과 같은 값) · 측점 둘 미만이면 null.
|
||||
"corridor.mesh": () => {
|
||||
const detail = inputs.detail;
|
||||
|
||||
@@ -365,11 +365,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_cross_design.ts#computeCrossDesign`,
|
||||
runner: `py:${B06}B06_Section_Engine_Design.py#compute_cross_design`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.design",
|
||||
pair: `${B06}B06_Section_Engine_Design.py#compute_cross_design`,
|
||||
mirror: ["test_b06_cross_design_mirror", "test_178_2_berm_fill_mirror"],
|
||||
note: "파이썬 짝은 B07 · 추천 · Route_Save · B05 확정도 부름 — 짝 남음",
|
||||
note: "[저장] · [확정] 다시 셈 = Node(472-4 ②) · 파이썬 짝 남음(B07 · 추천 · Route_Save · B05 확정 · 넓힘 찾기) · 알려진 차 = 설계선 점 반올림 동점 1e-4 m",
|
||||
},
|
||||
{
|
||||
id: "cross.areas",
|
||||
@@ -380,11 +379,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_cross_design_areas.ts#splitCutAreas`,
|
||||
runner: `py:${B06}B06_Section_Engine_Areas.py#_split_cut_areas`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.design",
|
||||
pair: `${B06}B06_Section_Engine_Areas.py#_split_cut_areas`,
|
||||
mirror: ["test_b06_cross_design_mirror"],
|
||||
note: "X3 — 표준 측점 = 파이썬 · 구조물 측점 = Node(structure.layouts)",
|
||||
note: "설계선과 한 셈(cross.design) · X3 한 갈래 — 표준 측점 · 구조물 측점(structure.layouts) 모두 Node",
|
||||
},
|
||||
{
|
||||
id: "cross.enforce",
|
||||
@@ -398,6 +396,7 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
runner: `py:${B06}B06_Section_Router_Design.py#enforce_design_lines`,
|
||||
until: "472-4",
|
||||
pair: `${B06}B06_Section_Router_Design.py#enforce_design_lines`,
|
||||
note: "구간 판정 = 파이썬 짝(Router_Design_Ranges) · 다시 세운 설계 = Node cross.design",
|
||||
mirror: ["test_349_0n_cross_enforce_mirror"],
|
||||
},
|
||||
{
|
||||
@@ -408,12 +407,11 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
outputs: ["소단 구간"],
|
||||
site: "browser_now",
|
||||
stage: null,
|
||||
ts: `${B06}B06_Section_UI_Page_Persist.ts#bermSpansFromStructures`,
|
||||
runner: `py:${B06}B06_Section_Engine_Structures_Wall.py#load_berm_spans`,
|
||||
until: "472-4",
|
||||
ts: `${U}common_util_berm_spans.ts#bermSpansFromStructures`,
|
||||
runner: "node:cross.berm_spans",
|
||||
pair: `${B06}B06_Section_Engine_Structures_Wall.py#load_berm_spans`,
|
||||
mirror: ["test_272_berm_source"],
|
||||
note: "TS 가 화면 파일 안 — common_util 로(472-4 ②)",
|
||||
note: "[저장] 다시 셈 = Node(472-4 ②) · 파이썬 짝은 Node 못 부를 때만",
|
||||
},
|
||||
{
|
||||
id: "cross.berm_slope",
|
||||
@@ -424,9 +422,9 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_cross_berm.ts#cutProfilePoints`,
|
||||
runner: `py:${U}common_util_cross_berm.py#cut_profile_points`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.design",
|
||||
pair: `${U}common_util_cross_berm.py#cut_profile_points`,
|
||||
note: "설계선 셈 안(cross.design) · 파이썬 짝 남음(cross.design 과 같은 까닭)",
|
||||
mirror: ["test_178_2_berm_fill_mirror"],
|
||||
},
|
||||
{
|
||||
@@ -441,8 +439,8 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
runner: `py:${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
|
||||
until: "472-4",
|
||||
pair: `${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
|
||||
mirror: [],
|
||||
note: "거울 시험 없음 — 옮기기 전 먼저",
|
||||
mirror: ["test_472_4_underdrain_mirror"],
|
||||
note: "얹기 = GET 상세(파이썬) · 알려진 차 = 옛 underdrain 걷음 TS 만 · 기울임은 cross.design 안",
|
||||
},
|
||||
{
|
||||
id: "cross.fit",
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""횡단 설계선 · 면적 · 소단 구간을 서버 Node 입구로(PLAN 472-4 ② · 등록부 `cross.design` ·
|
||||
`cross.berm_spans`) — 브라우저와 같은 TS(`common_util_cross_design` · `common_util_berm_spans`).
|
||||
|
||||
파이썬은 입력 조립만 — 측점마다 `compute_cross_design` 키를 모아 Node 한 번에 보냄.
|
||||
Node 를 못 부르면 파이썬 짝으로(거울 시험 `test_b06_cross_design_mirror` · `test_272_berm_source` ·
|
||||
알려진 차 = 설계선 점 반올림 동점 1e-4 m — 파이썬 round 짝수 쪽 · TS 위쪽).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import _resolve_group, compute_cross_design
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 한 측점 입력 = (지반 표본, 계획고, `compute_cross_design` 키 — standard 뺌)
|
||||
DesignJob = tuple[list[dict[str, Any]], float | None, dict[str, Any]]
|
||||
|
||||
|
||||
def full_standard(standard: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""config 기본값 위에 저장분을 덮은 표준 횡단 한 벌 — TS 는 config 를 모름(입력으로 받음)."""
|
||||
out: dict[str, Any] = {}
|
||||
for key in ("soil", "rock", "paved"):
|
||||
g = _resolve_group(key, standard)
|
||||
out[key] = {
|
||||
"road_width_m": g["road_width_m"],
|
||||
"shoulder_left_m": g["shoulder_left_m"],
|
||||
"shoulder_right_m": g["shoulder_right_m"],
|
||||
"ditch": {
|
||||
"top_width_m": g["ditch_top_width_m"],
|
||||
"bottom_width_m": g["ditch_bottom_width_m"],
|
||||
"depth_m": g["ditch_depth_m"],
|
||||
},
|
||||
"ditch_l_type": {"width_m": g["l_ditch_width_m"], "depth_m": g["l_ditch_depth_m"]},
|
||||
"cross_slope_pct": {"min": g["cross_slope_pct"]},
|
||||
"fill_slope_ratio": g["fill_slope_ratio"],
|
||||
"cut_slope_ratio": g["cut_slope_ratio"],
|
||||
"pavement_thickness_m": g["pavement_thickness_m"],
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def _python(job: DesignJob, standard: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
samples, elevation, options = job
|
||||
try:
|
||||
return compute_cross_design(samples, elevation, standard=standard, **options)
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
def compute_cross_designs(
|
||||
jobs: list[DesignJob], standard: dict[str, Any] | None
|
||||
) -> list[dict[str, Any] | None]:
|
||||
"""측점 묶음 횡단 설계 — 차례 그대로 결과 · None = 못 세움(파이썬 ValueError 자리)."""
|
||||
if not jobs:
|
||||
return []
|
||||
stations = [
|
||||
{
|
||||
"samples": samples,
|
||||
"design_elevation_m": elevation,
|
||||
"options": {
|
||||
key: (value._asdict() if key == "berm" and value is not None else value)
|
||||
for key, value in options.items()
|
||||
},
|
||||
}
|
||||
for samples, elevation, options in jobs
|
||||
]
|
||||
results = run_calc(
|
||||
["cross.design"],
|
||||
{"cross_design": {"standard": full_standard(standard), "stations": stations}},
|
||||
)
|
||||
if results is None:
|
||||
logger.warning("Node 횡단 설계 실패 — 파이썬 짝으로 셈(%d 측점)", len(jobs))
|
||||
return [_python(job, standard) for job in jobs]
|
||||
return [None if "error" in row else row for row in results["cross.design"]]
|
||||
|
||||
|
||||
def berm_specs_at(
|
||||
project_root: Path, chainages: list[float]
|
||||
) -> list[dict[str, float] | None] | None:
|
||||
"""측점마다 소단 제원(구조물 정본 구간) · None = 정본을 못 읽음(부르는 쪽이 저장분 그대로)."""
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans
|
||||
|
||||
try:
|
||||
structures = [
|
||||
item.model_dump(include={"type_id", "start_m", "end_m", "chainage_m", "options"})
|
||||
for item in load_structures(str(project_root))[1]
|
||||
]
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽으면 저장분으로 물러섬
|
||||
logger.exception("B06 소단 구간을 읽지 못했습니다 (저장분 소단을 씀)")
|
||||
return None
|
||||
results = run_calc(
|
||||
["cross.berm_spans"], {"berm": {"structures": structures, "chainages": chainages}}
|
||||
)
|
||||
if results is not None:
|
||||
return results["cross.berm_spans"]["at"]
|
||||
logger.warning("Node 소단 구간 실패 — 파이썬 짝으로 셈")
|
||||
spans = load_berm_spans(project_root)
|
||||
return None if spans is None else [berm_span_at(spans, c) for c in chainages]
|
||||
@@ -118,6 +118,7 @@ def extend_unclosed_sections(
|
||||
sampler: Any,
|
||||
) -> int:
|
||||
"""저장 설계가 미교차인 측점을 넓혀 샘플·설계를 자리에서 갈고 횡단 파일에 남긴다. 넓힌 수."""
|
||||
from B06_Section.B06_Section_Engine_Design_Node import compute_cross_designs
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
USER_TOUCHED_KEYS,
|
||||
ford_drop_at,
|
||||
@@ -132,23 +133,21 @@ def extend_unclosed_sections(
|
||||
return 0
|
||||
drops = ford_surface_drops(project_root)
|
||||
cross_dir = project_root / "B06_Section" / "cross_sections"
|
||||
changed = 0
|
||||
done: list[tuple[dict[str, Any], dict[str, Any], float, list[dict[str, Any]], dict, tuple]] = []
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict) or not design.get("slope_unclosed"):
|
||||
continue
|
||||
chainage = float(section.get("chainage_m", 0.0))
|
||||
|
||||
def compute(samples: list[dict[str, Any]], design=design, chainage=chainage) -> dict:
|
||||
return compute_cross_design(
|
||||
samples,
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
try:
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -162,6 +161,12 @@ def extend_unclosed_sections(
|
||||
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
||||
**stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4)
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
|
||||
# 넓힘 찾기 = 파이썬 짝(되풀이 셈) · 넓힌 뒤 정본 설계 = Node 한 번(472-4 ② · 아래)
|
||||
def compute(samples: list[dict[str, Any]], z=elevation, opts=options) -> dict:
|
||||
return compute_cross_design(samples, z, standard=standard, **opts)
|
||||
|
||||
try:
|
||||
result = extend_unclosed(
|
||||
@@ -174,7 +179,14 @@ def extend_unclosed_sections(
|
||||
samples, info = result
|
||||
if not info["left_m"] and not info["right_m"]:
|
||||
continue # 첫 걸음부터 지표면 밖 — 샘플이 그대로라 갈 것이 없음
|
||||
recomputed = compute(samples)
|
||||
done.append((section, design, chainage, samples, info, (samples, elevation, options)))
|
||||
results = compute_cross_designs([job for *_rest, job in done], standard)
|
||||
changed = 0
|
||||
for (section, design, chainage, samples, info, _job), recomputed in zip(
|
||||
done, results, strict=True
|
||||
):
|
||||
if recomputed is None:
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
"""B06 라우터의 횡단 설계 파일 읽기와 프리뷰 계산.
|
||||
|
||||
짝(349-0n): `pavement_ranges` · `ford_surface_drops` · `paved_at` · `ford_drop_at` ↔
|
||||
`B06_Section/logic/B06_Section_Cross_Enforce.ts` · 거울 시험 `test_349_0n_cross_enforce_mirror.py`.
|
||||
포장 · 세월교 구간 읽기 · 판정(짝 349-0n) = `B06_Section_Router_Design_Ranges.py` · 다시 내보냄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import find_cross_file
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets
|
||||
from B06_Section.B06_Section_Engine_Design import (
|
||||
ROUTE_CARRY_KEYS,
|
||||
compute_cross_design,
|
||||
@@ -20,7 +15,22 @@ from B06_Section.B06_Section_Engine_Design import (
|
||||
stored_shoulder_extend,
|
||||
stored_tilt,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Design_Node import (
|
||||
DesignJob,
|
||||
berm_specs_at,
|
||||
compute_cross_designs,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Tilt import tilt_args, tilt_ranges
|
||||
|
||||
# 포장 · 세월교 구간(나눈 파일) — 이 모듈 이름으로 다시 내보냄(부르는 곳 · 시험 갈아 끼움 그대로).
|
||||
from B06_Section.B06_Section_Router_Design_Ranges import ( # noqa: F401
|
||||
ford_drop_at,
|
||||
ford_pavement_ranges,
|
||||
ford_surface_drops,
|
||||
paved_at,
|
||||
pavement_ranges,
|
||||
pavement_suggestions,
|
||||
)
|
||||
from common_util.common_util_cross_berm import (
|
||||
BERM_DEFAULT_DEPTH_M,
|
||||
BERM_DEFAULT_INTERVAL_M,
|
||||
@@ -29,15 +39,8 @@ from common_util.common_util_cross_berm import (
|
||||
BermSpec,
|
||||
)
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과
|
||||
# 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다.
|
||||
_FORD_DROP_TOLERANCE_M = 0.02
|
||||
|
||||
PREVIEW_DESIGN_FIELDS = (
|
||||
"ground_type",
|
||||
"roadbed_width_m",
|
||||
@@ -52,98 +55,6 @@ PREVIEW_DESIGN_FIELDS = (
|
||||
)
|
||||
|
||||
|
||||
def pavement_suggestions(longitudinal: dict[str, Any]) -> dict[float, bool]:
|
||||
mapping: dict[float, bool] = {}
|
||||
stations = longitudinal.get("stations")
|
||||
for station in stations if isinstance(stations, list) else []:
|
||||
suggested = station.get("pavement_suggested")
|
||||
if isinstance(suggested, bool):
|
||||
mapping[round(float(station.get("chainage_m", 0.0)), 3)] = suggested
|
||||
return mapping
|
||||
|
||||
|
||||
def pavement_ranges(project_root: Path) -> list[tuple[float, float]]:
|
||||
"""포장으로 볼 누가거리 구간 — 구조물 정본 G군 + 물넘이포장(포장 필수).
|
||||
|
||||
포장 지정은 **사용자가 구간으로 준다**(2026-08-28 사용자 확정). 종단경사 자동 판정은
|
||||
더 이상 포장을 켜지 않는다 — 경고 표기(`pavement_suggested`)만 남는다.
|
||||
물넘이포장은 콘크리트 등으로 노면을 만드는 시설이라 범위 안이 항상 포장이다.
|
||||
읽기 실패는 비치명 — 빈 목록이면 사용자 지정이 없는 것과 같다.
|
||||
"""
|
||||
ranges: list[tuple[float, float]] = []
|
||||
try:
|
||||
types = structure_type_map()
|
||||
for structure in load_structures(str(project_root))[1]:
|
||||
definition = types.get(structure.type_id)
|
||||
if definition is None or definition.group != "G":
|
||||
continue
|
||||
anchor = structure.chainage_m
|
||||
start = structure.start_m if structure.start_m is not None else anchor
|
||||
end = structure.end_m if structure.end_m is not None else anchor
|
||||
if start is None or end is None:
|
||||
continue
|
||||
ranges.append((min(float(start), float(end)), max(float(start), float(end))))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)")
|
||||
return ranges + ford_pavement_ranges(project_root)
|
||||
|
||||
|
||||
def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""물넘이포장 범위(기준 ± 월류 폭/2) — 포장 필수 · 측구 없음(286-2). 판정은 `paved_at`."""
|
||||
if project_root is None:
|
||||
return []
|
||||
sets = load_culvert_sets(project_root).items()
|
||||
halves = [
|
||||
(at, float(s.get("span_m") or 0.0) / 2.0)
|
||||
for at, s in sets
|
||||
if s.get("type") == "ford_pavement"
|
||||
]
|
||||
return [(at - half, at + half) for at, half in halves]
|
||||
|
||||
|
||||
def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)].
|
||||
|
||||
구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼
|
||||
내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로
|
||||
관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다).
|
||||
"""
|
||||
if project_root is None:
|
||||
return []
|
||||
drops: list[tuple[float, float]] = []
|
||||
try:
|
||||
for chainage, spec in load_culvert_sets(project_root).items():
|
||||
if spec.get("type") != "ford":
|
||||
continue
|
||||
depth = float(spec.get("overflow_depth_m") or 0.0)
|
||||
if depth > 0:
|
||||
drops.append((chainage, depth))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)")
|
||||
return drops
|
||||
|
||||
|
||||
def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float:
|
||||
"""그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다."""
|
||||
for at, depth in drops:
|
||||
if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M:
|
||||
return depth
|
||||
return 0.0
|
||||
|
||||
|
||||
# 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool:
|
||||
"""이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시).
|
||||
|
||||
브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1).
|
||||
"""
|
||||
tol = _PAVED_EDGE_TOLERANCE_M
|
||||
return any(start - tol <= chainage_m <= end + tol for start, end in ranges)
|
||||
|
||||
|
||||
# 사용자 조작값 — 다시 계산해도 그대로 승계한다. **이 목록이 서버의 유일한 한 벌이다.**
|
||||
#
|
||||
# 브라우저 쪽 짝은 `B06_Section_Cross_Refresh.ts` 의 `PRESERVED_KEYS` 다(+ `status`·
|
||||
@@ -224,6 +135,26 @@ def stored_standard_cross_section(longitudinal_row: dict[str, Any] | None) -> di
|
||||
return standard if isinstance(standard, dict) else None
|
||||
|
||||
|
||||
def apply_designs(
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]], standard: dict[str, Any] | None
|
||||
) -> int:
|
||||
"""모은 측점을 Node 한 번(TS 짝 · 472-4 ②)으로 다시 세워 자리에서 갈아 끼움 · 바꾼 수.
|
||||
|
||||
`design` = 승계 원천(사용자 조작값 · 상태) · 못 세운 측점은 저장분 그대로(옛 ValueError 자리).
|
||||
"""
|
||||
results = compute_cross_designs([job for _section, _design, job in jobs], standard)
|
||||
changed = 0
|
||||
for (section, design, _job), recomputed in zip(jobs, results, strict=True):
|
||||
if recomputed is None:
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def enforce_pavement_ranges(
|
||||
longitudinal: dict[str, Any],
|
||||
cross_sections: list[dict[str, Any]],
|
||||
@@ -240,7 +171,7 @@ def enforce_pavement_ranges(
|
||||
ranges = pavement_ranges(project_root)
|
||||
tilts = tilt_ranges(project_root)
|
||||
ford_drops = ford_surface_drops(project_root)
|
||||
changed = 0
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict):
|
||||
@@ -251,15 +182,12 @@ def enforce_pavement_ranges(
|
||||
if bool(design.get("paved")) == wanted and stored_tilt(design) == tilt:
|
||||
continue
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
section.get("samples", []),
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=wanted,
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -273,14 +201,11 @@ def enforce_pavement_ranges(
|
||||
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
||||
**tilt,
|
||||
)
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
||||
return apply_designs(jobs, standard)
|
||||
|
||||
|
||||
def enforce_design_lines(
|
||||
@@ -297,29 +222,23 @@ def enforce_design_lines(
|
||||
확장) + 지금 종단 계획고 · 세월교 월류 하강 · 곡선부 확폭. 사용자 조작값은 승계.
|
||||
소단 제원은 구조물 정본(`structures.json`)의 구간이 원천 — 쪽마다 옮긴 깊이는 측점 patch(194-1).
|
||||
"""
|
||||
from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans
|
||||
|
||||
ford_drops = ford_surface_drops(project_root)
|
||||
tilts = tilt_ranges(project_root)
|
||||
spans = load_berm_spans(project_root)
|
||||
changed = 0
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict):
|
||||
continue
|
||||
chainage = float(section.get("chainage_m", 0.0))
|
||||
if spans is not None: # 구간을 지웠으면 소단 없음 · 못 읽으면 저장분 그대로
|
||||
design = {**design, "berm": berm_span_at(spans, chainage)}
|
||||
targets = [section for section in cross_sections if isinstance(section.get("design"), dict)]
|
||||
chainages = [float(section.get("chainage_m", 0.0)) for section in targets]
|
||||
berms = berm_specs_at(project_root, chainages) # Node 소단 구간(C4) · None = 못 읽음
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
||||
for index, (section, chainage) in enumerate(zip(targets, chainages, strict=True)):
|
||||
design = section["design"]
|
||||
if berms is not None: # 구간을 지웠으면 소단 없음 · 못 읽으면 저장분 그대로
|
||||
design = {**design, "berm": berms[index]}
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
section.get("samples", []),
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -333,14 +252,11 @@ def enforce_design_lines(
|
||||
**stored_shoulder_extend(design),
|
||||
**tilt_args(chainage, tilts),
|
||||
)
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
||||
return apply_designs(jobs, standard)
|
||||
|
||||
|
||||
def enforce_ford_surface_drops(
|
||||
@@ -356,7 +272,7 @@ def enforce_ford_surface_drops(
|
||||
월류 높이를 지우면 같은 경로로 원래 계획고로 되돌아온다. 사용자 조작값은 승계한다.
|
||||
"""
|
||||
drops = ford_surface_drops(project_root)
|
||||
changed = 0
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict):
|
||||
@@ -367,15 +283,12 @@ def enforce_ford_surface_drops(
|
||||
if abs(wanted - stored) < 1e-6:
|
||||
continue
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
section.get("samples", []),
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -389,14 +302,11 @@ def enforce_ford_surface_drops(
|
||||
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
||||
**stored_tilt(design),
|
||||
)
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
||||
return apply_designs(jobs, standard)
|
||||
|
||||
|
||||
def default_section_modes(longitudinal: dict[str, Any]) -> dict[float, str]:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""B06 포장 · 세월교 구간 읽기와 판정 — `B06_Section_Router_Design.py` 에서 나눔(472-4 · 동작 같음).
|
||||
|
||||
짝(349-0n): `pavement_ranges` · `ford_surface_drops` · `paved_at` · `ford_drop_at` ↔
|
||||
`B06_Section/logic/B06_Section_Cross_Enforce.ts` · 거울 시험 `test_349_0n_cross_enforce_mirror.py`.
|
||||
부르는 쪽은 `B06_Section_Router_Design` 이름으로 씀(다시 내보냄 · 시험이 그 이름을 갈아 끼움).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과
|
||||
# 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다.
|
||||
_FORD_DROP_TOLERANCE_M = 0.02
|
||||
|
||||
|
||||
def pavement_suggestions(longitudinal: dict[str, Any]) -> dict[float, bool]:
|
||||
mapping: dict[float, bool] = {}
|
||||
stations = longitudinal.get("stations")
|
||||
for station in stations if isinstance(stations, list) else []:
|
||||
suggested = station.get("pavement_suggested")
|
||||
if isinstance(suggested, bool):
|
||||
mapping[round(float(station.get("chainage_m", 0.0)), 3)] = suggested
|
||||
return mapping
|
||||
|
||||
|
||||
def pavement_ranges(project_root: Path) -> list[tuple[float, float]]:
|
||||
"""포장으로 볼 누가거리 구간 — 구조물 정본 G군 + 물넘이포장(포장 필수).
|
||||
|
||||
포장 지정은 **사용자가 구간으로 준다**(2026-08-28 사용자 확정). 종단경사 자동 판정은
|
||||
더 이상 포장을 켜지 않는다 — 경고 표기(`pavement_suggested`)만 남는다.
|
||||
물넘이포장은 콘크리트 등으로 노면을 만드는 시설이라 범위 안이 항상 포장이다.
|
||||
읽기 실패는 비치명 — 빈 목록이면 사용자 지정이 없는 것과 같다.
|
||||
"""
|
||||
ranges: list[tuple[float, float]] = []
|
||||
try:
|
||||
types = structure_type_map()
|
||||
for structure in load_structures(str(project_root))[1]:
|
||||
definition = types.get(structure.type_id)
|
||||
if definition is None or definition.group != "G":
|
||||
continue
|
||||
anchor = structure.chainage_m
|
||||
start = structure.start_m if structure.start_m is not None else anchor
|
||||
end = structure.end_m if structure.end_m is not None else anchor
|
||||
if start is None or end is None:
|
||||
continue
|
||||
ranges.append((min(float(start), float(end)), max(float(start), float(end))))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)")
|
||||
return ranges + ford_pavement_ranges(project_root)
|
||||
|
||||
|
||||
def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""물넘이포장 범위(기준 ± 월류 폭/2) — 포장 필수 · 측구 없음(286-2). 판정은 `paved_at`."""
|
||||
if project_root is None:
|
||||
return []
|
||||
sets = load_culvert_sets(project_root).items()
|
||||
halves = [
|
||||
(at, float(s.get("span_m") or 0.0) / 2.0)
|
||||
for at, s in sets
|
||||
if s.get("type") == "ford_pavement"
|
||||
]
|
||||
return [(at - half, at + half) for at, half in halves]
|
||||
|
||||
|
||||
def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)].
|
||||
|
||||
구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼
|
||||
내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로
|
||||
관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다).
|
||||
"""
|
||||
if project_root is None:
|
||||
return []
|
||||
drops: list[tuple[float, float]] = []
|
||||
try:
|
||||
for chainage, spec in load_culvert_sets(project_root).items():
|
||||
if spec.get("type") != "ford":
|
||||
continue
|
||||
depth = float(spec.get("overflow_depth_m") or 0.0)
|
||||
if depth > 0:
|
||||
drops.append((chainage, depth))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)")
|
||||
return drops
|
||||
|
||||
|
||||
def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float:
|
||||
"""그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다."""
|
||||
for at, depth in drops:
|
||||
if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M:
|
||||
return depth
|
||||
return 0.0
|
||||
|
||||
|
||||
# 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool:
|
||||
"""이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시).
|
||||
|
||||
브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1).
|
||||
"""
|
||||
tol = _PAVED_EDGE_TOLERANCE_M
|
||||
return any(start - tol <= chainage_m <= end + tol for start, end in ranges)
|
||||
@@ -25,12 +25,6 @@ import {
|
||||
writeState,
|
||||
} from "../A00_Common/b_page_state";
|
||||
import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options";
|
||||
import {
|
||||
BERM_DEFAULT_DEPTH_M,
|
||||
BERM_DEFAULT_INTERVAL_M,
|
||||
BERM_DEFAULT_SLOPE_DEG,
|
||||
BERM_DEFAULT_WIDTH_M,
|
||||
} from "@util/common_util_cross_berm";
|
||||
import {
|
||||
crossDesignChoices,
|
||||
readBermDepths,
|
||||
@@ -43,7 +37,15 @@ import type { StandardCrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_Section_UI_Section_View";
|
||||
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import { catalogRow } from "../common_util/common_util_structure_catalog";
|
||||
// 소단 구간(C4) — common_util 로 옮김 · 부르는 곳 그대로(다시 내보냄).
|
||||
import type { BermSpan } from "@util/common_util_berm_spans";
|
||||
export {
|
||||
bermSpansFromStructures,
|
||||
bermSpecAt,
|
||||
isBermType,
|
||||
type BermSessionSpec,
|
||||
type BermSpan,
|
||||
} from "@util/common_util_berm_spans";
|
||||
import { readKeyed, writeKeyed, type KeyedState } from "./B06_Section_Station_Keys";
|
||||
|
||||
/**
|
||||
@@ -107,27 +109,6 @@ function persistKeyed(name: KeyedState, storageKey: string, values: Map<string,
|
||||
else writeByKey(storageKey, JSON.stringify(Object.fromEntries(values)));
|
||||
}
|
||||
|
||||
/** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
|
||||
export interface BermSessionSpec {
|
||||
width_m: number;
|
||||
interval_m: number;
|
||||
slope_deg: number;
|
||||
/** 노면 끝 아래 깊이(m · ≥ 0 · 178-2). */
|
||||
depth_m: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 놓은 소단 **한 구간** — 종단 범위 + 제원.
|
||||
*
|
||||
* 사용자는 측점 하나가 아니라 **구간**에 놓는다(2026-09-07 확정: 「길이 + 기준측점 전·후」).
|
||||
* 그래서 세션에는 구간 목록으로 두고, 측점별 제원은 읽는 자리에서 편다 — 구간을 측점으로
|
||||
* 펴서 저장하면 나중에 「어디부터 어디까지 놓았나」를 되짚을 수 없다.
|
||||
*/
|
||||
export interface BermSpan extends BermSessionSpec {
|
||||
start_m: number;
|
||||
end_m: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션에 쌓인 소단 구간 목록. 없거나 손상되면 빈 목록.
|
||||
*
|
||||
@@ -143,76 +124,6 @@ export function writeBermSpans(projectId: string, routeId: number, spans: BermSp
|
||||
writeState("berm", spans, projectId, routeId);
|
||||
}
|
||||
|
||||
/** 소단 = 표 형상 berm(323-4 · 파이썬 `load_berm_spans` 짝) — 종류 키로 안 가름. */
|
||||
export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape === "berm";
|
||||
|
||||
/**
|
||||
* 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다
|
||||
* (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리).
|
||||
*
|
||||
* 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을
|
||||
* 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은
|
||||
* 그 사본만 본다.
|
||||
*/
|
||||
export function bermSpansFromStructures(
|
||||
structures: ReadonlyArray<{
|
||||
type_id: string;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
chainage_m?: number | null;
|
||||
options?: Record<string, string | number>;
|
||||
}>,
|
||||
): BermSpan[] {
|
||||
const spans: BermSpan[] = [];
|
||||
for (const item of structures) {
|
||||
if (!isBermType(item.type_id)) continue;
|
||||
const anchor = item.chainage_m ?? item.start_m ?? null;
|
||||
const start = item.start_m ?? anchor;
|
||||
const end = item.end_m ?? anchor;
|
||||
if (start === null || end === null) continue;
|
||||
const options = item.options ?? {};
|
||||
const number = (key: string, fallback: number): number => {
|
||||
const parsed = Number(options[key]);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
const width = number("width_m", BERM_DEFAULT_WIDTH_M);
|
||||
const interval = number("interval_m", BERM_DEFAULT_INTERVAL_M);
|
||||
if (width <= 0) continue; // 폭이 0이면 계단이 없다(간격은 성토 한 단에 안 씀 · 178-2)
|
||||
// 깊이 = 노면 끝 아래 · 음수는 0(도로보다 높을 수 없음) · 빈 값은 기본 1.0
|
||||
const rawDepth = Number(options.depth_m);
|
||||
const depth =
|
||||
options.depth_m === undefined || options.depth_m === "" || !Number.isFinite(rawDepth)
|
||||
? BERM_DEFAULT_DEPTH_M
|
||||
: Math.max(rawDepth, 0);
|
||||
spans.push({
|
||||
start_m: Math.min(start, end),
|
||||
end_m: Math.max(start, end),
|
||||
width_m: width,
|
||||
interval_m: interval,
|
||||
slope_deg: number("slope_deg", BERM_DEFAULT_SLOPE_DEG),
|
||||
depth_m: depth,
|
||||
});
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/** 그 측점을 덮는 소단 제원 — 없으면 null. 겹치면 먼저 놓은 것이 이긴다. */
|
||||
export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpec | null {
|
||||
const found = spans.find(
|
||||
(span) =>
|
||||
chainageM >= Math.min(span.start_m, span.end_m) - 1e-6 &&
|
||||
chainageM <= Math.max(span.start_m, span.end_m) + 1e-6,
|
||||
);
|
||||
return found
|
||||
? {
|
||||
width_m: found.width_m,
|
||||
interval_m: found.interval_m,
|
||||
slope_deg: found.slope_deg,
|
||||
depth_m: found.depth_m ?? BERM_DEFAULT_DEPTH_M,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 암 경계선 오프셋 저장소 — 값(Map)과 조정창 제어기를 함께 낸다. */
|
||||
export interface RockBoundaryStore {
|
||||
/** 측점키(누가거리 2자리) → 오프셋(m). `buildCrossPatches` 가 그대로 읽는다. */
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
① 짝(ts ↔ py · 거울 시험):
|
||||
|
||||
- `Engine_Design.py` ↔ `common_util_cross_design.ts` · `Engine_Areas.py` ↔ `common_util_cross_design_areas.ts` · `test_b06_cross_design_mirror.py`
|
||||
- [저장] · [확정] 다시 셈(`Router_Design` enforce · 넓힘 뒤 설계 · 소단 구간)은 Node(`Engine_Design_Node.py` → design-calc `cross.design` · `cross.berm_spans`) · 파이썬 짝은 B07 · 추천 · Route_Save · Node 못 부를 때
|
||||
- `Engine_Culvert.py` ↔ `common_util_culvert_sets.ts` · `logic/UI_Cross_Culvert_Const.ts` · `test_b06_culvert_sets_mirror.py` · `test_b06_pipe_wall_height_mirror.py`
|
||||
- `Engine_Structures_Wall.py` ↔ `common_util_structure_walls.ts` · `test_b06_structure_walls_mirror.py`
|
||||
- 지형 표본 = `common_util_surface_sampler` 하나(`Engine_SampleExtend` 도)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* =============================================================================
|
||||
* common_util_berm_spans.ts
|
||||
* 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4 · 화면 파일
|
||||
* `B06_Section_UI_Page_Persist.ts` 에서 옮김 · 그쪽이 다시 내보냄).
|
||||
* 브라우저(시설 줄 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans`)가 같은 셈.
|
||||
* 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `load_berm_spans` · `berm_span_at`
|
||||
* (Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py`).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
BERM_DEFAULT_DEPTH_M,
|
||||
BERM_DEFAULT_INTERVAL_M,
|
||||
BERM_DEFAULT_SLOPE_DEG,
|
||||
BERM_DEFAULT_WIDTH_M,
|
||||
} from "./common_util_cross_berm";
|
||||
import { catalogRow } from "./common_util_structure_catalog";
|
||||
|
||||
/** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
|
||||
export interface BermSessionSpec {
|
||||
width_m: number;
|
||||
interval_m: number;
|
||||
slope_deg: number;
|
||||
/** 노면 끝 아래 깊이(m · ≥ 0 · 178-2). */
|
||||
depth_m: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 놓은 소단 **한 구간** — 종단 범위 + 제원.
|
||||
*
|
||||
* 사용자는 측점 하나가 아니라 **구간**에 놓는다(2026-09-07 확정: 「길이 + 기준측점 전·후」).
|
||||
* 그래서 세션에는 구간 목록으로 두고, 측점별 제원은 읽는 자리에서 편다 — 구간을 측점으로
|
||||
* 펴서 저장하면 나중에 「어디부터 어디까지 놓았나」를 되짚을 수 없다.
|
||||
*/
|
||||
export interface BermSpan extends BermSessionSpec {
|
||||
start_m: number;
|
||||
end_m: number;
|
||||
}
|
||||
|
||||
/** 소단 = 표 형상 berm(323-4 · 파이썬 `load_berm_spans` 짝) — 종류 키로 안 가름. */
|
||||
export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape === "berm";
|
||||
|
||||
/**
|
||||
* 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다
|
||||
* (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리).
|
||||
*
|
||||
* 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을
|
||||
* 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은
|
||||
* 그 사본만 본다.
|
||||
*/
|
||||
export function bermSpansFromStructures(
|
||||
structures: ReadonlyArray<{
|
||||
type_id: string;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
chainage_m?: number | null;
|
||||
options?: Record<string, string | number>;
|
||||
}>,
|
||||
): BermSpan[] {
|
||||
const spans: BermSpan[] = [];
|
||||
for (const item of structures) {
|
||||
if (!isBermType(item.type_id)) continue;
|
||||
const anchor = item.chainage_m ?? item.start_m ?? null;
|
||||
const start = item.start_m ?? anchor;
|
||||
const end = item.end_m ?? anchor;
|
||||
if (start === null || end === null) continue;
|
||||
const options = item.options ?? {};
|
||||
const number = (key: string, fallback: number): number => {
|
||||
const parsed = Number(options[key]);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
const width = number("width_m", BERM_DEFAULT_WIDTH_M);
|
||||
const interval = number("interval_m", BERM_DEFAULT_INTERVAL_M);
|
||||
if (width <= 0) continue; // 폭이 0이면 계단이 없다(간격은 성토 한 단에 안 씀 · 178-2)
|
||||
// 깊이 = 노면 끝 아래 · 음수는 0(도로보다 높을 수 없음) · 빈 값은 기본 1.0
|
||||
const rawDepth = Number(options.depth_m);
|
||||
const depth =
|
||||
options.depth_m === undefined || options.depth_m === "" || !Number.isFinite(rawDepth)
|
||||
? BERM_DEFAULT_DEPTH_M
|
||||
: Math.max(rawDepth, 0);
|
||||
spans.push({
|
||||
start_m: Math.min(start, end),
|
||||
end_m: Math.max(start, end),
|
||||
width_m: width,
|
||||
interval_m: interval,
|
||||
slope_deg: number("slope_deg", BERM_DEFAULT_SLOPE_DEG),
|
||||
depth_m: depth,
|
||||
});
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/** 그 측점을 덮는 소단 제원 — 없으면 null. 겹치면 먼저 놓은 것이 이긴다. */
|
||||
export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpec | null {
|
||||
const found = spans.find(
|
||||
(span) =>
|
||||
chainageM >= Math.min(span.start_m, span.end_m) - 1e-6 &&
|
||||
chainageM <= Math.max(span.start_m, span.end_m) + 1e-6,
|
||||
);
|
||||
return found
|
||||
? {
|
||||
width_m: found.width_m,
|
||||
interval_m: found.interval_m,
|
||||
slope_deg: found.slope_deg,
|
||||
depth_m: found.depth_m ?? BERM_DEFAULT_DEPTH_M,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -24,6 +24,8 @@ NODE_IDS = frozenset(
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
"cross.design",
|
||||
"cross.berm_spans",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
|
||||
# 계산 목록 — TS · 파이썬 짝 · Node 묶음 · 실행 자리
|
||||
|
||||
- Node 묶음 — sc = server-calc · co = corridor · ex = b07-extent · 다시 지음 `npm run -s build:server-calc|corridor|b07-extent`.
|
||||
- Node 묶음 — sc = server-calc · co = corridor · ex = b07-extent · dc = design-calc(등록부 입구 `A00_Common/design/logic/design_calc_node.ts` · 파이썬 `common_util_calc_runner.run_calc`) · 다시 지음 `npm run -s build:server-calc|corridor|b07-extent|design-calc`.
|
||||
- 실행 자리 — 짝 = 브라우저 즉시 TS + [저장] 정본 파이썬 · ② = 같은 TS 를 브라우저 · Node 둘 다 · 브라우저 = 입력 도구 · 표시만 · 서버 = 무거운 GIS · 추천.
|
||||
- 줄 더함 · 바꿈 = 새 계산 · 짝 · 묶음이 바뀐 커밋에서.
|
||||
|
||||
@@ -34,11 +34,11 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
| P6 | 옮긴 시설 꼭짓점 세움 | `logic/B05_Profile_Grade_Seat.ts:32` · `common_util_structure_station.ts:356` | `B05_Profile_Router_Confirm.py:220 · 306` | — | 짝 · `test_427_1_follow_seat` · `test_459_route_seat_mirror` |
|
||||
| P7 | 직선화 · 자유지정 · 쉬프트 · 시종점 | `logic/B05_Profile_UI_Profile_Straighten.ts` · `logic/design_profile_free.ts` · `logic/B05_Profile_UI_Profile_Pivot.ts` · `logic/B05_Profile_EndOffsets.ts` | — | — | 브라우저(편집값만 저장 → P2) |
|
||||
| P8 | 종단 경고(기울기 · 법정 L · 최소 R · 피복) | `B05_Profile_UI_Panel_Fields.ts` · `logic/B05_Profile_UI_Profile_Guard.ts` · `B05_Profile_UI_Profile_MinCover.ts` · 경고 글 `logic/B05_Profile_UI_Profile_Alignment.ts` buildAlignment | `B05_Profile_Engine_Grade.py` · `B05_Profile_PageSettings.py` · 경고 글 `B05_Profile_Engine_Grade_Alignment.py` build_alignment | — | 짝 · [저장] · [확정] = `B06_Section_Server_Calc_Prebuild.py` refresh_alignment 가 경고만 다시 씀(기하 그대로) · `test_442_2_grade_limit_mirror` · `test_472_save_warnings` |
|
||||
| C1 | 횡단 설계선 | `common_util_cross_design.ts:267` · `logic/B06_Section_Cross_Stage.ts:93` · `B06_Section_Cross_Refresh.ts:330` | `B06_Section_Engine_Design.py:195` | ex | 짝 · `test_b06_cross_design_mirror` |
|
||||
| C2 | 단면적(절 · 성 · 측구 · 층따기) | `common_util_cross_design_areas.ts` | `B06_Section_Engine_Areas.py` | sc · ex | 짝 · 정본 = 표준 측점 파이썬 · 구조물 측점 Node |
|
||||
| C3 | 확정 보정(포장 · 세월교 · 기울임) | `logic/B06_Section_Cross_Enforce.ts:40` | `B06_Section_Router_Design.py` · `B06_Section_Engine_Tilt.py` | — | 짝 · `test_349_0n_cross_enforce_mirror` |
|
||||
| C4 | 소단 구간(구조물 → 구간) | `B06_Section_UI_Page_Persist.ts:157`(시설 줄이 부름) | `B06_Section_Engine_Structures_Wall.py:124` | — | 짝 · `test_272_berm_source` |
|
||||
| C5 | 소단 사면 점 | `common_util_cross_berm.ts` | `common_util_cross_berm.py` | sc · co · ex | 짝 · `test_178_2_berm_fill_mirror` |
|
||||
| C1 | 횡단 설계선 | `common_util_cross_design.ts:267` · `logic/B06_Section_Cross_Stage.ts:93` · `B06_Section_Cross_Refresh.ts:330` | `B06_Section_Engine_Design.py:195`(B07 · 추천 · Route_Save · B05 확정 · 넓힘 찾기) | dc · ex | ② · [저장] · [확정] 다시 셈 = Node(`B06_Section_Engine_Design_Node.py` 측점 묶음 · 파이썬은 입력 조립) · `test_b06_cross_design_mirror` · `test_472_1_golden` 설계선 점 1e-4 |
|
||||
| C2 | 단면적(절 · 성 · 측구 · 층따기) | `common_util_cross_design_areas.ts` | `B06_Section_Engine_Areas.py` | dc · sc · ex | ② · C1 과 한 셈 · 정본 = 표준 · 구조물 측점 모두 Node |
|
||||
| C3 | 확정 보정(포장 · 세월교 · 기울임) | `logic/B06_Section_Cross_Enforce.ts:40` | `B06_Section_Router_Design_Ranges.py` · `B06_Section_Engine_Tilt.py` | dc(다시 세운 설계) | 구간 판정 짝 · `test_349_0n_cross_enforce_mirror` |
|
||||
| C4 | 소단 구간(구조물 → 구간) | `common_util_berm_spans.ts`(시설 줄 · 다시 셈이 부름) | `B06_Section_Engine_Structures_Wall.py:124`(Node 못 부를 때만) | dc | ② · `test_272_berm_source` · `test_472_3_calc_runner` |
|
||||
| C5 | 소단 사면 점 | `common_util_cross_berm.ts` | `common_util_cross_berm.py` | dc · sc · co · ex | C1 과 한 셈 · `test_178_2_berm_fill_mirror` |
|
||||
| C6 | 맹암거 얹기 · 기울임 | `common_util_structure_underdrain.ts` | `B06_Section_Engine_Tilt.py` | — | 짝 · 거울 시험 없음 |
|
||||
| C7 | 반폭 맞춤 · 성토 5 m 경고 | `logic/B06_Section_UI_Cross_Fit.ts` · `logic/B06_Section_UI_Cross_FillSlope_Warn.ts` | — | ex(Fit) | 브라우저 |
|
||||
| F1 | 시설 따라감(노선 바뀜) | `common_util_route_follow.ts:291` · `logic/B05_Profile_Pipeline_Follow.ts` · `logic/B05_Profile_Structure_Follow.ts` | `common_util_route_follow.py:120` | — | 짝 · `test_410_follow_xy` · `test_344_2_route_pairs` |
|
||||
@@ -64,7 +64,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
|---|---|---|---|---|
|
||||
| X1 | 단면 모양 기본값 | `"left_cut"`(`B06_Section_Cross_Refresh.ts:438`) | `default_section_modes`(`B06_Section_Router_Design.py:402`) | 사용자 협의 · 손대지 않음 |
|
||||
| X2 | 지반 표본 출처 · 입력 선 | 격자 `.bin` · `/route/plan` planned 선 | npz `sample_xy` · 저장 경로 폴리라인 | 노선 초안 없는 차례 = 정본 표본 유지로 막음 |
|
||||
| X3 | 단면적 두 갈래 | 전부 TS | 표준 측점 파이썬 · 구조물 측점 Node | 1e-4 ㎡ 차 |
|
||||
| X3 | 단면적 두 갈래 | 전부 TS | 모두 Node(472-4 ②) | 풀림 · 남은 차 = 설계선 점 반올림 동점 1e-4 m(파이썬 round 짝수 쪽 · TS 위쪽) |
|
||||
| X4 | 평면 발자국 | TS PlanFootprint | B07 파이썬 PlanSlope | 거울 시험 |
|
||||
| X5 | 경고 기준표 | `B05_Profile_UI_Panel_Fields.ts` 안 표 | `FOREST_ROAD_PROFILE_CRITERIA` | 거울 시험 · 자료 두 벌 |
|
||||
| X6 | 노선 바뀜 넘기는 칸 | `B06_Section_Station_Keys.ts:46` 근처 | `B05_Profile_Route_Save.py:275-279` CARRY_INPUT_KEYS · ROUTE_CARRY_KEYS | 손으로 맞춘 목록 둘 · 거울 없음 |
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* 472-4 C6 거울 헬퍼 — 맹암거 얹기 TS 짝(vite ssr) · test_472_4_underdrain_mirror.py 가 JSON 으로 받음. */
|
||||
export {
|
||||
attachUnderdrainSpecs,
|
||||
underdrainSpecsFrom,
|
||||
} from "../../common_util/common_util_structure_underdrain";
|
||||
@@ -42,8 +42,11 @@ def test_글은_재계산에서도_남는다(monkeypatch):
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [{"paved": kw["paved"]} for _s, _z, kw in jobs],
|
||||
)
|
||||
sections = [{"chainage_m": 40.0, "design": {"paved": False, "structure_note": "메모"}}]
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 1
|
||||
|
||||
@@ -202,7 +202,8 @@ def test_좌측_패널_칸_구간_깊이_나름():
|
||||
"""옛 구조물 옵션 depth_m → 세션 구간 → 측점 제원(기본값) · 서버 payload(같은 꼴).
|
||||
|
||||
좌측 칸은 194-1 에서 숨김 — 옮기기는 횡단도 ▲▼(`test_194_1_berm_depth`)."""
|
||||
source = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
# 소단 구간 셈은 472-4 C4 에서 `common_util_berm_spans.ts` 로 옮김
|
||||
source = (ROOT / "common_util" / "common_util_berm_spans.ts").read_text(encoding="utf-8")
|
||||
assert "options.depth_m" in source and "Math.max(rawDepth, 0)" in source
|
||||
refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
|
||||
assert "depthM: spec.depth_m" in refresh
|
||||
|
||||
@@ -59,8 +59,11 @@ def test_CAD_칸은_재계산에서도_남는다(monkeypatch):
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [{"paved": kw["paved"]} for _s, _z, kw in jobs],
|
||||
)
|
||||
sections = [{"chainage_m": 40.0, "design": {"paved": False, "structure_note_cad": CAD}}]
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 1
|
||||
|
||||
@@ -26,6 +26,7 @@ if str(ROOT) not in sys.path:
|
||||
import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402
|
||||
import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
@@ -157,7 +158,8 @@ def server(r) -> list[dict]:
|
||||
"""서버 [확정] 차례 그대로 — 저장분(보정 전) → 포장 구간 → 세월교 하강."""
|
||||
patch = pytest.MonkeyPatch()
|
||||
sets = culvert_sets_from(parse_pipe_points(PIPES))
|
||||
for module in (design_mod, tilt_mod):
|
||||
# 구간 읽기는 472-4 에서 `_Router_Design_Ranges` 로 나눔 — 정본 읽기를 그 모듈에서 갈아 끼움.
|
||||
for module in (ranges_mod, tilt_mod):
|
||||
patch.setattr(module, "load_culvert_sets", lambda _root: sets)
|
||||
patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
|
||||
@@ -27,6 +27,7 @@ from test_349_0n_cross_enforce_mirror import OFFSETS, _stations, _z # noqa: E40
|
||||
import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402
|
||||
import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402
|
||||
import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
@@ -109,7 +110,8 @@ def server(r) -> list[dict]:
|
||||
"""서버 [확정] — 저장분(입력 없는 측점은 config 암 경계) → 포장 구간 → 세월교 하강."""
|
||||
patch = pytest.MonkeyPatch()
|
||||
sets = culvert_sets_from(parse_pipe_points(PIPES))
|
||||
for module in (design_mod, tilt_mod):
|
||||
# 구간 읽기는 472-4 에서 `_Router_Design_Ranges` 로 나눔 — 정본 읽기를 그 모듈에서 갈아 끼움.
|
||||
for module in (ranges_mod, tilt_mod):
|
||||
patch.setattr(module, "load_culvert_sets", lambda _root: sets)
|
||||
patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES))
|
||||
|
||||
@@ -19,6 +19,7 @@ X3 면적 두 갈래(구조물 = 관 351 하나 · 두 길 다 Node TS 로 셈)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -131,6 +132,25 @@ def test_두_길_사이_알려진_차만(got, pair) -> None:
|
||||
assert not gone, f"안 나타난 알려진 차 {gone} — KNOWN 에서 지움"
|
||||
|
||||
|
||||
def test_저장_설계선_점_정본과_1e4_안(tmp_path) -> None:
|
||||
"""[저장] 다시 셈 설계선(472-4 ② = Node TS) — 정본과 점 수 · offset 같음 · 고도 1e-4 m 안.
|
||||
|
||||
알려진 차 = 반올림 동점(파이썬 round 짝수 쪽 · TS 위쪽) 13 점(브레인 10-19 승인).
|
||||
"""
|
||||
detail = copy.deepcopy(paths.read("detail.json"))
|
||||
before = {s["chainage_m"]: s["design"]["design_line"] for s in detail["cross_sections"]}
|
||||
sections, _ = paths._save_tail(detail, paths.stage_project(tmp_path / "project"))
|
||||
moved = 0
|
||||
for section in sections:
|
||||
old, new = before[section["chainage_m"]], section["design"]["design_line"]
|
||||
assert [p["offset_m"] for p in new] == [p["offset_m"] for p in old], section["chainage_m"]
|
||||
for a, b in zip(old, new, strict=True):
|
||||
gap = abs(a["elevation_m"] - b["elevation_m"])
|
||||
assert gap <= 1e-4 + 1e-9, (section["chainage_m"], a, b)
|
||||
moved += gap > 0
|
||||
assert moved == 13, f"반올림 동점 점 수 {moved} — 알려진 차(13)와 다름"
|
||||
|
||||
|
||||
def test_화면_참값() -> None:
|
||||
stored = GOLDEN["stored"]
|
||||
assert stored["final_cumulative_m3"] == -17570.7
|
||||
|
||||
@@ -40,6 +40,8 @@ NODE_IDS = {
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
"cross.design",
|
||||
"cross.berm_spans",
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
@@ -196,7 +198,7 @@ def test_id_마다_실행기_있음(loaded) -> None:
|
||||
if row["site"] == "browser_only":
|
||||
ok = kind == "browser" and _has_name(ref)
|
||||
elif kind == "node":
|
||||
ok = ref == row["id"] and ref in NODE_IDS
|
||||
ok = ref in NODE_IDS # 한 셈에서 나오는 값(면적 · 소단 사면)은 그 셈의 id
|
||||
else:
|
||||
ok = kind == "py" and _has_name(ref)
|
||||
ok = ok and ("until" not in row or kind == "py")
|
||||
@@ -205,8 +207,10 @@ def test_id_마다_실행기_있음(loaded) -> None:
|
||||
if not ok:
|
||||
bad.append(row["id"])
|
||||
assert bad == []
|
||||
node_rows = {row["id"] for row in loaded["registry"] if row["runner"].startswith("node:")}
|
||||
assert node_rows == NODE_IDS
|
||||
node_runs = {
|
||||
row["runner"][5:] for row in loaded["registry"] if row["runner"].startswith("node:")
|
||||
}
|
||||
assert node_runs == NODE_IDS
|
||||
|
||||
|
||||
def test_묶음_STAGE_DEPS_등록부_입력과_같음(loaded) -> None:
|
||||
|
||||
@@ -68,3 +68,36 @@ def test_실행기_값_server_calc_와_같음() -> None:
|
||||
def test_Node_실행기_아닌_id_는_ValueError() -> None:
|
||||
with pytest.raises(ValueError, match="stations.list"):
|
||||
runner.run_calc(["masshaul.cumulative", "stations.list"], {})
|
||||
|
||||
|
||||
# ── 472-4 ② — 횡단 설계선 · 소단 구간 Node 길 ──────────────────────────────────
|
||||
|
||||
|
||||
def test_소단_구간_Node_와_파이썬_짝_같음(tmp_path, monkeypatch) -> None:
|
||||
from test_272_berm_source import STRUCTURE_CASES, _project
|
||||
|
||||
from B06_Section import B06_Section_Engine_Design_Node as node_mod
|
||||
from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans
|
||||
|
||||
calls = []
|
||||
real = node_mod.run_calc
|
||||
monkeypatch.setattr(
|
||||
node_mod, "run_calc", lambda ids, inputs: calls.append(ids) or real(ids, inputs)
|
||||
)
|
||||
chainages = [0.0, 5.0, 50.0, 65.0, 80.0, 100.0, 150.0, 250.0]
|
||||
for index, structures in enumerate(STRUCTURE_CASES):
|
||||
root = _project(tmp_path / f"c{index}", structures)
|
||||
got = node_mod.berm_specs_at(root, chainages)
|
||||
spans = load_berm_spans(root)
|
||||
want = [berm_span_at(spans, c) for c in chainages]
|
||||
assert got == [pytest.approx(w) if w else w for w in want], index
|
||||
assert len(calls) == len(STRUCTURE_CASES) # 파이썬 물러섬 아님
|
||||
|
||||
|
||||
def test_횡단_설계_못_세운_측점은_None() -> None:
|
||||
from B06_Section.B06_Section_Engine_Design_Node import compute_cross_designs
|
||||
|
||||
ground = [{"offset_m": o, "elevation_m": 100.0 + 0.3 * o} for o in range(-20, 21)]
|
||||
options = {"ground_type": "soil", "section_mode": "left_cut"}
|
||||
got = compute_cross_designs([(ground, 100.0, options), (ground, None, options)], None)
|
||||
assert got[0]["cut_area_m2"] > 0 and got[1] is None
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""PLAN 472-4 ② C6 (요구 273) — 맹암거 얹기 거울: TS `common_util_structure_underdrain` ↔ 파이썬
|
||||
`B06_Section_Engine_Tilt.underdrains_from` · `attach_underdrain_specs`.
|
||||
|
||||
못박는 것
|
||||
- 구조물 목록 → 제원(높이 · 경사 · 빈 값 · 0 · 음수 · 글 · 맹암거 아닌 종류) 두 쪽 같음.
|
||||
- 기준 측점(±0.02 m) 얹기 · 얹은 수 같음.
|
||||
- 알려진 차 하나 — 옛 `underdrain` 이 남은 측점: TS 는 걷음 · 파이썬은 둠
|
||||
(서버는 파일에서 새로 읽어 옛 값 없음 · Node 로 옮길 때 한 벌).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_4_underdrain_mirror.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _drain(at: float, **options) -> dict:
|
||||
return {
|
||||
"type_id": "cross_underdrain",
|
||||
"placement": "point",
|
||||
"chainage_m": at,
|
||||
"options": options,
|
||||
}
|
||||
|
||||
|
||||
STRUCTURES = [
|
||||
_drain(40.0, culvert_height_m=1.2, culvert_slope_pct=3),
|
||||
_drain(80.0, culvert_height_m="", culvert_slope_pct="2.5"),
|
||||
_drain(120.0, culvert_height_m=0, culvert_slope_pct=-1),
|
||||
_drain(160.0),
|
||||
{"type_id": "berm", "placement": "interval", "start_m": 0.0, "end_m": 50.0, "options": {}},
|
||||
]
|
||||
SECTIONS = [
|
||||
{"chainage_m": 40.01},
|
||||
{"chainage_m": 79.99},
|
||||
{"chainage_m": 120.0},
|
||||
{"chainage_m": 160.03}, # 0.02 밖
|
||||
{"chainage_m": 200.0, "underdrain": {"chainage_m": 200.0}}, # 옛 값 — 알려진 차
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts(tmp_path_factory) -> dict:
|
||||
work = tmp_path_factory.mktemp("u4724")
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_472_4_underdrain_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
)
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_472_4_underdrain_entry.js", bundle)
|
||||
job = json.dumps({"structures": STRUCTURES, "sections": SECTIONS})
|
||||
script = (
|
||||
f"const m = await import({json.dumps(bundle.as_uri())});"
|
||||
f"const job = {job};"
|
||||
"const specs = m.underdrainSpecsFrom(job.structures);"
|
||||
"const count = m.attachUnderdrainSpecs(job.sections, specs);"
|
||||
"console.log(JSON.stringify({specs, count, sections: job.sections}));"
|
||||
)
|
||||
out = subprocess.run( # noqa: S603
|
||||
["node", "--input-type=module", "-e", script],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def _python() -> tuple[list[dict], int, list[dict]]:
|
||||
specs = underdrains_from([StructureInstance(**item) for item in STRUCTURES])
|
||||
sections = copy.deepcopy(SECTIONS)
|
||||
return specs, attach_underdrain_specs(sections, specs), sections
|
||||
|
||||
|
||||
def test_제원_같음(ts) -> None:
|
||||
specs, _count, _sections = _python()
|
||||
assert ts["specs"] == specs
|
||||
|
||||
|
||||
def test_얹기_같음_알려진_차_하나(ts) -> None:
|
||||
_specs, count, sections = _python()
|
||||
assert ts["count"] == count == 3
|
||||
assert ts["sections"][:4] == sections[:4]
|
||||
# 알려진 차 — 옛 underdrain: TS 걷음 · 파이썬 둠
|
||||
assert "underdrain" not in ts["sections"][4] and "underdrain" in sections[4]
|
||||
@@ -31,6 +31,8 @@ TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
# 값이 소수점 아래에서 갈리는지 보려면 자릿수를 그대로 비교해야 한다. 두 쪽 다 반올림해
|
||||
# 내보내므로 완전 일치를 기대하되, 부동소수 마지막 자리만 어긋나는 것은 허용한다.
|
||||
# 알려진 차(472-4 ②) — 반올림 동점(…5)에서 파이썬 round 는 짝수 쪽 · TS 는 위쪽이라 설계선 점이
|
||||
# 1e-4 m 갈릴 수 있음(이 시험 입력엔 없음 · [저장] 정본은 Node TS 쪽 · test_472_1_golden).
|
||||
_TOLERANCE = 1e-9
|
||||
|
||||
|
||||
|
||||
@@ -146,8 +146,11 @@ def test_enforce_turns_paved_on_and_off(monkeypatch):
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [{"paved": kw["paved"]} for _s, _z, kw in jobs],
|
||||
)
|
||||
sections = [
|
||||
{"chainage_m": 20.0, "design": {"paved": False}}, # 범위 안 · 끔 → 켬
|
||||
|
||||
@@ -80,7 +80,12 @@ def _install_fakes(monkeypatch) -> dict:
|
||||
}
|
||||
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 200.0)])
|
||||
monkeypatch.setattr(design_mod, "compute_cross_design", fake_compute)
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [fake_compute(s, z, standard=standard, **kw) for s, z, kw in jobs],
|
||||
)
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
return seen
|
||||
|
||||
@@ -120,7 +120,12 @@ def _fake_engine(monkeypatch):
|
||||
return {"paved": True, "cut_area_m2": 1.0, "fill_area_m2": 2.0}
|
||||
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 200.0)])
|
||||
monkeypatch.setattr(design_mod, "compute_cross_design", fake_compute)
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [fake_compute(s, z, standard=standard, **kw) for s, z, kw in jobs],
|
||||
)
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user