feat(472-4 ② 횡단 짝 모으기): [저장] · [확정] 횡단 설계선 · 면적 · 소단 구간을 Node 입구 한 갈래로(C1 · C2 · C4 · X3 풀림)

- Router_Design enforce 셋 · 넓힘 뒤 설계 — 파이썬은 입력 조립만 · 측점 묶음을 Node 한 번(B06_Section_Engine_Design_Node · cross.design) · Node 못 부르면 파이썬 짝
- 소단 구간 셈을 화면 파일에서 common_util_berm_spans.ts 로 · 서버도 같은 TS(cross.berm_spans) · Persist 가 다시 내보냄
- 값: 고정 자료 기준값 0 차(면적 · 계획고 · 누가토량 −17,570.7) · 설계선 점 13 곳 1e-4 m(반올림 동점 · 브레인 승인) — 기준값 시험에 판정 더함
- 시험: compute_cross_design 갈아 끼우던 시험 다섯을 묶음 자리로(까닭: [저장] 다시 셈이 묶음으로 부름) · 소단 글 시험 자리 옮김
- 등록부 · 위키 계산 목록 · B06 지침 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 09:21:42 +09:00
co-authored by Claude Opus 5.5
parent 454313f88e
commit 3a64f9a812
20 changed files with 406 additions and 177 deletions
+12 -1
View File
@@ -11,7 +11,8 @@
* excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m ·
* has_foundation }] } ·
* cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) ·
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } } }
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } ·
* berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } } }
* 출력 { results: { [id]: 값 }, available: [id…] }
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
* server-calc 묶음과 같은 차례).
@@ -19,6 +20,7 @@
* ========================================================================== */
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";
@@ -63,6 +65,10 @@ interface CalcInputs {
has_foundation?: boolean;
}>;
};
berm?: {
structures: Parameters<typeof bermSpansFromStructures>[0];
chainages: number[];
};
cross_design?: {
standard: CrossDesignOptions["standard"];
stations?: Array<{
@@ -179,6 +185,11 @@ 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;
+10 -12
View File
@@ -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"],
},
{
@@ -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]
+19 -7
View File
@@ -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]
+47 -43
View File
@@ -15,6 +15,11 @@ 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
# 포장 · 세월교 구간(나눈 파일) — 이 모듈 이름으로 다시 내보냄(부르는 곳 · 시험 갈아 끼움 그대로).
@@ -130,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]],
@@ -146,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):
@@ -157,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
),
@@ -179,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(
@@ -203,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
),
@@ -239,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(
@@ -262,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):
@@ -273,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
),
@@ -295,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]:
+9 -98
View File
@@ -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` 가 그대로 읽는다. */
+1
View File
@@ -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` 도)
+107
View File
@@ -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;
}
+2
View File
@@ -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 | 손으로 맞춘 목록 둘 · 거울 없음 |
+4 -1
View File
@@ -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
+4 -1
View File
@@ -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
+20
View File
@@ -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
+7 -3
View File
@@ -40,6 +40,8 @@ NODE_IDS = {
"haul.plan",
"excavation.trench",
"corridor.mesh",
"cross.design",
"cross.berm_spans",
}
pytestmark = pytest.mark.skipif(
@@ -200,7 +202,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")
@@ -209,8 +211,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
@@ -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
+6 -1
View File
@@ -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: {})