feat(b05,b06): 물넘이포장 콘크리트 윗면 = 설계 노면 · 포장층 노견까지 · 월류 높이 점선은 노면 위 (287-2 · 287-4)

- 설계 계산 tilted · tilt_pct(파이썬 · TS 짝) — 물넘이 범위 노면 = 경사각도로 지대 낮은 쪽 한 경사 · 측구 없음 · 중심점 = 계획고(콘크리트 윗면) · 노면 끝에서 사면이 이어짐(짧으면 성토)
- 서버 tilt_ranges · tilt_args · stored_tilt — 기본 설계 · [저장] 재계산 · 포장 범위 맞춤 · 저장분 재계산이 같은 값(옛 ford_paved 갈음)
- 횡단 그림 — 포장층 = 노면공 포장 형상(appendPavementOverlay)을 노견까지 · 물넘이 표식 · 월류 높이선 = 노면 + 높이 같은 각도 · 옛 파임 · 노면 걷기 그림 걷음
- 3D 노면 파기 걷음(설계 노면이 곧 윗면) · 코리도 빌드 판 94
- 시험 test_287 · 286_1 · 횡단 설계 거울(기울임 갈래) · 물넘이 명세 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
2026-10-12 20:57:54 +09:00
co-authored by Claude Opus 5.5
parent 0c7f71d103
commit a5a9a7cec0
23 changed files with 273 additions and 288 deletions
+2 -1
View File
@@ -107,6 +107,7 @@ async def sync_uphill_overrides_into_designs(
compute_cross_design,
curve_widening_args,
stored_shoulder_extend,
stored_tilt,
)
from B06_Section.B06_Section_Repository import get_cross_section_designs
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
@@ -175,7 +176,7 @@ async def sync_uphill_overrides_into_designs(
surface_drop_m=ford_drop_at(float(chainage), ford_drops),
**curve_widening_args(cross_record),
**stored_shoulder_extend(design), # 노견 확장(139-6)
ford_paved=bool(design.get("ford_paved")), # 물넘이 자리 측구 없음(286-2)
**stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4)
)
next_design["status"] = design.get("status", "provisional")
next_design["pavement_suggested"] = design.get("pavement_suggested", pavement_suggested)
@@ -181,7 +181,8 @@ function fnv1a(text: string): string {
// (세월교 유입·유출, BOX 좌·우, 본체는 `body`). 91 저장본엔 배수관 세트 키만 있다.
// 93 = 측점 사이 행 자리를 **노선 길이 직선 보간**으로(115-5 P0-2) — 균일 Catmull-Rom 이
// 행을 최대 7.2 m 미끄러뜨리고 되접었다. 모든 행 자리가 바뀌어 저장본을 만료한다.
const BUILD_VERSION = 93;
// 94 = 물넘이포장 노면을 파지 않음(287-4) — 설계 노면이 콘크리트 윗면 · 저장본을 만료한다.
const BUILD_VERSION = 94;
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
+2 -17
View File
@@ -9,7 +9,6 @@
* (2026-08-23 사용자 지시)의 핵심.
* ========================================================================== */
import { fordDeckElevationAt } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
import { structureSilhouettes } from "./B05_Profile_UI_Corridor_Station_Structure";
@@ -227,22 +226,8 @@ export function classifyStation(section: CrossSection): StationPieces | null {
if (!design || !line || line.length < 2) return null;
const roadL = design.road_edges.left.offset_m;
const roadR = design.road_edges.right.offset_m;
// 물넘이포장 — 노면(노견 포함)이 파인다. 횡단도와 **같은 산식**으로 표고를 내려
// 3D도 같은 자리에 같은 깊이로 팬다(2026-08-28 사용자 지시). 노면 밖 비탈은 그대로라
// 노견 끝에 파임 벽(수직 단차)이 선다 — 횡단도 그림과 같은 표현이다.
const ford = section.ford_pavement;
const carved =
ford && ford.depth_m
? line.map((point) =>
point.offset_m <= roadL + 1e-9 && point.offset_m >= roadR - 1e-9
? {
...point,
elevation_m: fordDeckElevationAt(design, ford, point.offset_m, point.elevation_m),
}
: point,
)
: line;
const sorted = [...carved].sort((a, b) => a.offset_m - b.offset_m);
// 물넘이포장 — 설계 노면이 곧 콘크리트 윗면(287-4 · 서버가 지대 낮은 쪽 한 경사로 냄) · 파지 않음
const sorted = [...line].sort((a, b) => a.offset_m - b.offset_m);
const cwL = design.carriageway_edges?.left.offset_m ?? roadL;
const cwR = design.carriageway_edges?.right.offset_m ?? roadR;
+3
View File
@@ -495,6 +495,9 @@ export interface CrossDesign {
right: { offset_m: number; elevation_m: number };
};
design_elevation_m: number;
/** 물넘이포장 · 맹암거 자리(286-2 · 287-4) — 측구 없음 · 노면 = 지대 낮은 쪽 한 경사(`tilt_pct`). */
tilted?: boolean;
tilt_pct?: number;
cut_area_m2: number;
/**
* 절토 내역(합 = `cut_area_m2`). 지표면~암반 경계선이 토사, 그 아래가 암이다.
+3 -2
View File
@@ -359,8 +359,9 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
choice?.ditch_type ??
(typeof design.ditch_type === "string" ? design.ditch_type : "standard"),
paved: pavedBySpecs(section),
// 물넘이 자리 = 측구 없음(286-2 · 서버 `ford_pavement_ranges` 짝 = 얹힌 제원 범위)
fordPaved: Boolean(section.ford_pavement),
// 물넘이 자리 = 측구 없음 · 지대 낮은 쪽 한 경사(286-2 · 287-4 · 서버 `tilt_ranges` 짝 = 얹힌 제원)
tilted: Boolean(section.ford_pavement),
tiltPct: section.ford_pavement?.slope_pct ?? null,
standard,
berm: bermAt(section.chainage_m, design),
rockBoundaryOffsetM,
+4 -5
View File
@@ -513,11 +513,10 @@ def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]:
def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]:
"""물넘이포장 1개소 — 노면을 월류 폭만큼, 노선 중심에서 월류 높이만큼 판 자리.
"""물넘이포장 1개소 — 월류 폭 범위의 노면 = 콘크리트 윗면(287-4).
구조물을 얹는 것이 아니라 **계획고를 파낸 노면**이라 다른 세트와 형상이 다르다.
바닥은 유입(상류) 쪽이 높고 유출 쪽이 낮게 기운다 — 경사를 사용자가 비워 두면
화면이 노면 일반 기울기로 채운다(2026-08-28 사용자 확정). 파임 폭은 노견까지 전폭.
설계 노면을 경사각도(`slope_pct`)로 지대 낮은 쪽 한 경사로 기울임(`tilt_ranges` →
`compute_cross_design(tilted=…)`) · 월류 높이(`depth_m`) = 노면 위 점선(그림 전용).
"""
defaults = _registry_defaults("ford_pavement")
values = dict(options or {})
@@ -536,7 +535,7 @@ def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]:
{
"type": "ford_pavement",
"span_m": span,
# 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다).
# 월류 높이 — 노면 위 점선. 없으면 안 그린다(수치를 지어내지 않는다).
"depth_m": depth if depth and depth > 0 else None,
"slope_pct": slope,
"defaulted": _defaulted(
+19 -6
View File
@@ -155,6 +155,13 @@ def stored_shoulder_extend(stored: dict[str, Any] | None) -> dict[str, float]:
return out
def stored_tilt(stored: dict[str, Any] | None) -> dict[str, Any]:
"""저장분의 구조물 자리 기울기(287-4 · 288-1) — `compute_cross_design(**...)` 로 넘긴다."""
value = (stored or {}).get("tilt_pct")
pct = float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
return {"tilted": bool((stored or {}).get("tilted")), "tilt_pct": pct}
def curve_widening_args(section: dict[str, Any] | None) -> dict[str, Any]:
"""측점 기록에서 곡선부 확폭 입력을 뽑는다 — `compute_cross_design(**...)` 로 넘긴다.
@@ -194,7 +201,8 @@ def compute_cross_design(
spoil_fill: dict[str, Any] | None = None,
shoulder_extend_left_m: float = 0.0,
shoulder_extend_right_m: float = 0.0,
ford_paved: bool = False,
tilted: bool = False,
tilt_pct: float | None = None,
) -> dict[str, Any]:
"""측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다.
@@ -211,8 +219,9 @@ def compute_cross_design(
입력이며, None·45m 이상이면 확폭이 없다.
curve_outer_side: 곡선 **바깥쪽**("left"/"right"). 확폭은 그쪽으로만 붙는다
(2026-09-06 사용자 확정). 값이 없으면 확폭을 넣지 않는다.
ford_paved: 물넘이포장 범위 안 측점(286-2) — 측구 없음 · 결과에 `ford_paved` 로 되실음.
TS 짝 `fordPaved`.
tilted · tilt_pct: 물넘이포장 범위 · 맹암거 측점(286-2 · 287-4 · 288-1) — 측구 없음 ·
노면 = `tilt_pct`(비면 표준 횡단경사)로 지대 낮은 쪽 한 경사. 결과에 `tilted` · `tilt_pct`
로 되실음(`stored_tilt`). TS 짝 `tilted` · `tiltPct`.
surface_drop_m: 노면을 통째로 내리는 양(m) — 세월교 월류 높이. 구체 위 노면은 월류
높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로
횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자).
@@ -246,6 +255,8 @@ def compute_cross_design(
paved_group = _resolve_group("paved", standard)
# 포장 중첩: 횡단경사와 포장층 두께만 포장 그룹을 따른다.
cross_slope_pct = paved_group["cross_slope_pct"] if paved else group["cross_slope_pct"]
if tilted and tilt_pct is not None and float(tilt_pct) > 0:
cross_slope_pct = float(tilt_pct) # 구조물 자리 경사(287-4 · 288-1)
resolved_ditch_side = _resolve_ditch_side(section_mode, ditch_side)
valid = sorted(
@@ -302,7 +313,7 @@ def compute_cross_design(
berm=berm,
shoulder_extend_left_m=shoulder_extend_left_m,
shoulder_extend_right_m=shoulder_extend_right_m,
no_ditch=bool(ford_paved),
tilted=bool(tilted),
)
# 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야
@@ -533,8 +544,10 @@ def compute_cross_design(
# 뒤에 쓸 자리가 있다. **되살릴 때는 저장분에서 읽지 말고 계산해서 쓸 것.**
"cut_slope_segments": geometry.cut_slope_segments(),
}
if ford_paved:
result["ford_paved"] = True
if tilted:
result["tilted"] = True
if tilt_pct is not None:
result["tilt_pct"] = round(float(tilt_pct), 4)
if drop > 0:
# 내려 앉힌 양 — 프론트가 "월류가 없었다면" 노면을 점선으로 되그리는 데 쓴다.
result["surface_drop_m"] = round(drop, 4)
@@ -62,7 +62,7 @@ class _SectionGeometry:
berm: BermSpec | None = None,
shoulder_extend_left_m: float = 0.0,
shoulder_extend_right_m: float = 0.0,
no_ditch: bool = False,
tilted: bool = False,
) -> None:
half_road = group["road_width_m"] / 2.0
# 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽).
@@ -96,6 +96,11 @@ class _SectionGeometry:
# 횡단경사: 측구 방향으로 내려가는 단일 사면 (좌=+offset 규약).
slope = cross_slope_pct / 100.0
self.slope_per_offset = -slope if ditch_side == "left" else slope
# 물넘이포장 · 맹암거 자리(287-4 · 288-1) — 노면 = 한 경사로 지대 낮은 쪽(노면 끝
# 원지반이 낮은 쪽)으로 기움 · 측구 없음(아래). ⚠ TS 짝 `SectionGeometry`.
if tilted and ground_at is not None:
low_left = ground_at(self.left_extent) < ground_at(-self.right_extent)
self.slope_per_offset = -slope if low_left else slope
# 단면 유형 자동 판정(D-2): 각 측 절/성토 역할을 노면 끝 지반이 설계면보다
# 높은지(절토)/낮은지(성토)로 결정한다. 좌절/우절/양절/양성이 모두 지형에서
@@ -145,8 +150,9 @@ class _SectionGeometry:
self.has_ditch = auto_ditch if (choice is None or both_fill) else choice
#: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다.
self.ditch_choice = None if both_fill else choice
# 물넘이포장 자리(286-2) — 파인 노면이라 측구가 안 섬 · 선택은 그대로(시설을 걷으면 되살아남).
if no_ditch:
# 물넘이포장 · 맹암거 자리(286-2 · 288-1) — 측구가 안 섬 · 선택은 그대로(시설을 걷으면
# 되살아남).
if tilted:
self.has_ditch = False
# 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
@@ -25,6 +25,7 @@ from B06_Section.B06_Section_Engine_Design import (
compute_cross_design,
curve_widening_args,
stored_shoulder_extend,
stored_tilt,
)
from common_util.common_util_json import atomic_write_json
@@ -158,7 +159,7 @@ def extend_unclosed_sections(
berm=stored_berm(design),
**curve_widening_args(section),
**stored_shoulder_extend(design), # 노견 확장(139-6)
ford_paved=bool(design.get("ford_paved")), # 물넘이 자리 측구 없음(286-2)
**stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4)
)
try:
+6 -2
View File
@@ -23,7 +23,11 @@ from __future__ import annotations
from pathlib import Path
from typing import Any
from B06_Section.B06_Section_Engine_Design import compute_cross_design, stored_shoulder_extend
from B06_Section.B06_Section_Engine_Design import (
compute_cross_design,
stored_shoulder_extend,
stored_tilt,
)
from common_util.common_util_structure_face_role import structure_face_role
#: 폭을 좁혀 가는 이분법 반복 수. TS 짝(`solveSpoilWidthM`)과 같은 값이다.
@@ -217,7 +221,7 @@ def enforce_spoil_fills(
},
**curve_widening_args(section),
**stored_shoulder_extend(design), # 노견 확장(139-6)
ford_paved=bool(design.get("ford_paved")), # 물넘이 자리 측구 없음(286-2)
**stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4)
)
except (ValueError, KeyError):
return None
+3 -2
View File
@@ -46,12 +46,13 @@ from B06_Section.B06_Section_Router_Design import (
from B06_Section.B06_Section_Router_Design import (
USER_TOUCHED_KEYS,
ford_drop_at,
ford_pavement_ranges,
ford_surface_drops,
paved_at,
pavement_ranges,
stored_berm,
stored_cut_slope,
tilt_args,
tilt_ranges,
)
from B06_Section.B06_Section_Router_Design import (
attach_default_designs as _attach_default_designs,
@@ -601,7 +602,7 @@ async def compute_cross_section_design(
berm=stored_berm(stored_design or {}),
**curve_widening_args(cross_record),
**stored_shoulder_extend(stored_design or {}), # 노견 확장(139-6)
ford_paved=paved_at(request.chainage_m, ford_pavement_ranges(project_root)),
**tilt_args(request.chainage_m, tilt_ranges(project_root)),
)
# B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
design["status"] = "provisional"
+35 -13
View File
@@ -13,6 +13,7 @@ from B06_Section.B06_Section_Engine_Design import (
compute_cross_design,
curve_widening_args,
stored_shoulder_extend,
stored_tilt,
)
from common_util.common_util_cross_berm import (
BERM_DEFAULT_DEPTH_M,
@@ -93,6 +94,27 @@ def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]
return [(at - half, at + half) for at, half in halves]
def tilt_ranges(project_root: Path | None) -> list[tuple[float, float, float | None]]:
"""측구 없이 노면을 지대 낮은 쪽 한 경사로 기울이는 자리(287-4) — 물넘이포장 범위 ·
경사 = 경사각도(%)."""
if project_root is None:
return []
out: list[tuple[float, float, float | None]] = []
for at, spec in load_culvert_sets(project_root).items():
if spec.get("type") == "ford_pavement":
half = float(spec.get("span_m") or 0.0) / 2.0
out.append((at - half, at + half, spec.get("slope_pct")))
return out
def tilt_args(chainage_m: float, ranges: list[tuple[float, float, float | None]]) -> dict[str, Any]:
"""그 측점의 구조물 자리 기울기 — `compute_cross_design(**...)` 로 넘김 ·
저장분 짝 `stored_tilt`."""
hit = next((r for r in ranges if paved_at(chainage_m, [(r[0], r[1])])), None)
pct = hit[2] if hit else None
return {"tilted": hit is not None, "tilt_pct": float(pct) if pct is not None else None}
def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]:
"""세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)].
@@ -215,7 +237,7 @@ def enforce_pavement_ranges(
사용자 조작값(반폭·구조물 조정)은 그대로 승계한다.
"""
ranges = pavement_ranges(project_root)
ford_ranges = ford_pavement_ranges(project_root)
tilts = tilt_ranges(project_root)
ford_drops = ford_surface_drops(project_root)
changed = 0
for section in cross_sections:
@@ -224,8 +246,8 @@ def enforce_pavement_ranges(
continue
chainage = float(section.get("chainage_m", 0.0))
wanted = paved_at(chainage, ranges)
ford_paved = paved_at(chainage, ford_ranges) # 물넘이 자리 = 측구 없음(286-2)
if bool(design.get("paved")) == wanted and bool(design.get("ford_paved")) == ford_paved:
tilt = tilt_args(chainage, tilts) # 물넘이 자리 = 측구 없음 · 기울기(286-2 · 287-4)
if bool(design.get("paved")) == wanted and stored_tilt(design) == tilt:
continue
try:
recomputed = compute_cross_design(
@@ -248,7 +270,7 @@ def enforce_pavement_ranges(
berm=stored_berm(design),
**curve_widening_args(section),
**stored_shoulder_extend(design), # 노견 확장(139-6)
ford_paved=ford_paved,
**tilt,
)
except (ValueError, KeyError):
continue
@@ -277,7 +299,7 @@ def enforce_design_lines(
from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans
ford_drops = ford_surface_drops(project_root)
ford_ranges = ford_pavement_ranges(project_root)
tilts = tilt_ranges(project_root)
spans = load_berm_spans(project_root)
changed = 0
for section in cross_sections:
@@ -308,7 +330,7 @@ def enforce_design_lines(
berm=stored_berm(design),
**curve_widening_args(section),
**stored_shoulder_extend(design),
ford_paved=paved_at(chainage, ford_ranges),
**tilt_args(chainage, tilts),
)
except (ValueError, KeyError):
continue
@@ -364,7 +386,7 @@ def enforce_ford_surface_drops(
berm=stored_berm(design),
**curve_widening_args(section),
**stored_shoulder_extend(design), # 노견 확장(139-6)
ford_paved=bool(design.get("ford_paved")),
**stored_tilt(design),
)
except (ValueError, KeyError):
continue
@@ -437,7 +459,7 @@ def attach_default_designs(
modes = default_section_modes(longitudinal)
pavement = pavement_suggestions(longitudinal)
paved_ranges = pavement_ranges(project_root) if project_root else []
ford_ranges = ford_pavement_ranges(project_root)
tilts = tilt_ranges(project_root)
ford_drops = ford_surface_drops(project_root)
for section in cross_sections:
if section.get("design"):
@@ -456,7 +478,7 @@ def attach_default_designs(
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
surface_drop_m=ford_drop_at(chainage, ford_drops),
**curve_widening_args(section),
ford_paved=paved_at(chainage, ford_ranges),
**tilt_args(chainage, tilts),
)
design.update(status="provisional", pavement_suggested=suggested)
section["design"] = design
@@ -517,7 +539,7 @@ def recompute_designs_for_alignment(
modes = default_section_modes(longitudinal)
pavement = pavement_suggestions(longitudinal)
paved_ranges = pavement_ranges(project_root) if project_root else []
ford_ranges = ford_pavement_ranges(project_root)
tilts = tilt_ranges(project_root)
ford_drops = ford_surface_drops(project_root)
stored_by_chainage = {
round(float(record["chainage_m"]), 3): (record.get("design") or {})
@@ -576,7 +598,7 @@ def recompute_designs_for_alignment(
berm=session_berms.get(key) or stored_berm(stored),
**curve_widening_args(section),
**stored_shoulder_extend(stored), # 노견 확장(139-6)
ford_paved=paved_at(chainage, ford_ranges),
**tilt_args(chainage, tilts),
)
except (ValueError, KeyError):
continue
@@ -621,7 +643,7 @@ def compute_default_designs(
default_modes = default_section_modes(longitudinal)
pavement = pavement_suggestions(longitudinal)
paved_ranges = pavement_ranges(root)
ford_ranges = ford_pavement_ranges(root)
tilts = tilt_ranges(root)
ford_drops = ford_surface_drops(root)
results: list[tuple[float, dict[str, Any]]] = []
for chainage_m in chainages:
@@ -661,7 +683,7 @@ def compute_default_designs(
surface_drop_m=ford_drop_at(chainage_m, ford_drops),
berm=stored_berm(user),
**curve_widening_args(cross),
ford_paved=paved_at(chainage_m, ford_ranges),
**tilt_args(chainage_m, tilts),
)
for key in ("berm", "cut_slope_ratio_user"):
if user.get(key) is not None:
+41 -161
View File
@@ -1,36 +1,28 @@
/* =============================================================================
* B06_Section_UI_Cross_Ford_Pavement.ts
* 물넘이포장 — 계획고를 파낸 노면을 횡단도에 그린다(2026-08-28 사용자 확정).
* 물넘이포장 — 횡단도 그림(287-2 · 287-4 · 그림_183).
*
* 다른 시설은 구조물을 얹지만 물넘이는 **노면 자체가 내려앉는다**. 그래서 그림도 다르다.
* · 파임 범위 = 노견까지 **전폭**.
* · 깊이 = **노선 중심**에서 잰 월류 높이(`depth_m`).
* · 바닥은 **유입(상류)이 높고 유출이 낮게** 기운다. 경사를 비우면 그 측점의 노면
* 횡단경사(`cross_slope_pct`)를 쓴다 — 사용자가 폼에서 바꿀 수 있다.
* · 기존 계획고는 **점선**, 물넘이 바닥은 **실선**.
* · 포장 필수 — 일반 포장층과 구분되게 진한 회색 + 빗금으로 채운다.
*
* 유입측 = 표고가 높은 쪽 — 설계선 양 끝(비탈이 원지반을 만나는 자리) 표고로 가름 · 노면은 경사각도
* (`ford_slope_deg` → `slope_pct`)만큼 표고 낮은 쪽(유출)으로 기움(286-2). 양 끝이 같으면 `section_mode`
* (상단측 절토 = 상류).
* · 콘크리트 윗면 = 설계 노면 — 서버(`compute_cross_design(tilted=…)`) · 짝 TS 가 노면을 경사각도
* (`ford_slope_deg` → `slope_pct`)로 지대 낮은 쪽 한 경사로 기울여 낸 면 · 설계 중심점이 그 위.
* 노면 밖은 설계선 그대로 — 노면 끝에서 성토 · 절토 사면이 이어짐(늘어나지 못한 자리 = 성토).
* · 포장층 = 노면공 포장 형상(`appendPavementOverlay`)을 노견까지.
* · 월류 높이선(회색 점선) = 노면 위로 월류 높이(`depth_m`)만큼 · 노면과 같은 각도.
* · 도수로 · 돌붙임 띠 = 유출(지대 낮은 쪽) 성토사면.
* ========================================================================== */
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import { appendPavementOverlay } from "./B06_Section_UI_Cross_Design_Surface";
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
const SVG_NS = "http://www.w3.org/2000/svg";
/** 빗금 패턴 id는 문서 안에서 유일해야 한다 — 카드마다 하나씩 번호를 준다. */
let hatchSeq = 0;
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
export interface FordPavementSpec {
/** 관 지점 누가거리 — 폭 절반 걸친 옆 카드도 같은 값. */
chainage_m?: number;
span_m: number;
/** 노선 중심에서 잰 파임 깊이(m). 없으면 그리지 않는다 — 수치를 지어내지 않는다. */
/** 월류 높이(m) — 노면 위 점선. 없으면 점선을 안 그림(수치를 지어내지 않는다). */
depth_m: number | null;
/** 유입 → 유출 바닥 경사(%). 비우면 노면 횡단경사를 쓴다. */
/** 노면 경사(%) — 경사각도 환산 · 서버 설계 노면이 이 값으로 기욺. */
slope_pct: number | null;
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
defaulted?: string[];
@@ -38,46 +30,19 @@ export interface FordPavementSpec {
chute?: { structure: string; thickness_m: number; length_m?: number };
}
interface Edge {
offset_m: number;
elevation_m: number;
}
/** 유입(상류)이 좌측(+offset)인가 — 설계선 양 끝 표고가 높은 쪽(286-2) · 같으면 단면유형. */
/** 유입(상류)이 좌측(+offset)인가 — 기울인 노면이면 높은 노면 끝 · 아니면 설계선 양 끝 표고가 높은
* 쪽(286-2) · 같으면 단면유형. */
export function inflowLeft(design: CrossDesign): boolean {
const edges = design.road_edges;
if (design.tilted && edges && Math.abs(edges.left.elevation_m - edges.right.elevation_m) > 1e-6)
return edges.left.elevation_m > edges.right.elevation_m;
const line = [...(design.design_line ?? [])].sort((a, b) => a.offset_m - b.offset_m);
const gap = line.length >= 2 ? line[line.length - 1].elevation_m - line[0].elevation_m : 0;
return Math.abs(gap) > 1e-3 ? gap > 0 : design.section_mode !== "right_cut";
}
/** 유입측(상류) 부호 — 그 방향으로 갈수록 바닥이 높아진다. */
function inflowSign(design: CrossDesign, offsetM: number): number {
const towardLeft = offsetM > 0;
return towardLeft === inflowLeft(design) ? 1 : -1;
}
/**
* 파인 노면의 표고(m). 횡단도와 3D 코리도가 **같은 식**을 써야 두 그림이 어긋나지 않는다.
* 중심에서 깊이만큼 내리고, 유입측으로 갈수록 경사만큼 올린다.
*/
export function fordDeckElevationAt(
design: CrossDesign,
spec: FordPavementSpec,
offsetM: number,
fallbackElevationM = 0,
): number {
const depth = spec.depth_m ?? 0;
const slope = (spec.slope_pct ?? design.cross_slope_pct) / 100;
const center = design.design_elevation_m ?? fallbackElevationM;
return center - depth + slope * Math.abs(offsetM) * inflowSign(design, offsetM);
}
function bottomAt(design: CrossDesign, spec: FordPavementSpec, edge: Edge): number {
return fordDeckElevationAt(design, spec, edge.offset_m, edge.elevation_m);
}
function line(points: string[], className: string): SVGPolylineElement {
const polyline = document.createElementNS(SVG_NS, "polyline");
const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
polyline.setAttribute("points", points.join(" "));
polyline.setAttribute("class", className);
return polyline;
@@ -127,8 +92,9 @@ export function appendFordSurfaceDropPlan(
}
/**
* 물넘이 파임을 그린다. 그렸으면 true — 호출부는 일반 포장층 박스를 건너뛴다
* (같은 자리에 두 겹으로 깔리면 색 구분이 사라진다).
* 물넘이포장을 그린다(287-2 · 287-4) — 포장층(노면공 형상 · 노견까지) · 월류 높이선 · 도수로 띠.
* 그렸으면 true — 호출부는 일반 포장층을 건너뜀(두 겹으로 안 깔림). 포장층에 물넘이 표식
* (`b06-chart__ford-pavement` · 287-5 고름이 노면공 포장 고름과 가름).
*/
export function appendFordPavementOverlay(
svg: SVGElement,
@@ -137,66 +103,34 @@ export function appendFordPavementOverlay(
x: (offset: number) => number,
y: (elevation: number) => number,
): boolean {
if (!spec || !spec.depth_m || !design.road_edges) return false;
const { left, right } = design.road_edges;
const bottomLeft = bottomAt(design, spec, left);
const bottomRight = bottomAt(design, spec, right);
// ① 기존 계획고(점선) — 파기 전 노면이 어디였는지 남긴다.
svg.append(
line(
[
`${x(left.offset_m)},${y(left.elevation_m)}`,
`${x(right.offset_m)},${y(right.elevation_m)}`,
],
"b06-chart__ford-deck-plan",
),
);
// ② 포장층 — 바닥선에서 두께만큼 아래로. 일반 포장과 색·빗금으로 구분한다.
const thickness = design.pavement_thickness_m ?? 0.2;
const hatchId = `b06-ford-hatch-${(hatchSeq += 1)}`;
const defs = document.createElementNS(SVG_NS, "defs");
const pattern = document.createElementNS(SVG_NS, "pattern");
pattern.setAttribute("id", hatchId);
pattern.setAttribute("patternUnits", "userSpaceOnUse");
pattern.setAttribute("width", "6");
pattern.setAttribute("height", "6");
const stroke = document.createElementNS(SVG_NS, "path");
stroke.setAttribute("d", "M0,6 L6,0");
stroke.setAttribute("class", "b06-chart__ford-pavement-hatch");
pattern.append(stroke);
defs.append(pattern);
svg.append(defs);
const polygon = document.createElementNS(SVG_NS, "polygon");
polygon.setAttribute(
"points",
[
`${x(left.offset_m)},${y(bottomLeft)}`,
`${x(right.offset_m)},${y(bottomRight)}`,
`${x(right.offset_m)},${y(bottomRight - thickness)}`,
`${x(left.offset_m)},${y(bottomLeft - thickness)}`,
].join(" "),
);
polygon.setAttribute("class", "b06-chart__ford-pavement");
polygon.setAttribute("fill", `url(#${hatchId})`);
svg.append(polygon);
// ③ 물넘이 바닥(실선) — 횡단 기준선이라 가장 위에 올린다.
svg.append(
line(
[`${x(left.offset_m)},${y(bottomLeft)}`, `${x(right.offset_m)},${y(bottomRight)}`],
"b06-chart__ford-deck",
),
);
const edges = design.road_edges;
if (!spec || !edges) return false;
const from = svg.children.length;
// 노면공 항목은 안 넘김 — 물넘이 자체 포장(노면공 포장 고름과 안 섞임)
appendPavementOverlay(svg, { ...design, paved: true, carriageway_edges: edges }, x, y, null);
[...svg.children]
.slice(from)
.filter((el) => el.getAttribute("class") === "b06-chart__pavement")
.forEach((el) => el.setAttribute("class", "b06-chart__pavement b06-chart__ford-pavement"));
// 월류 높이선 — 콘크리트 윗면(설계 노면) 위로 월류 높이만큼 · 같은 각도(그림_183)
const depth = spec.depth_m ?? 0;
if (depth > 0)
svg.append(
line(
[
`${x(edges.left.offset_m)},${y(edges.left.elevation_m + depth)}`,
`${x(edges.right.offset_m)},${y(edges.right.elevation_m + depth)}`,
],
"b06-chart__ford-deck-plan",
),
);
if (spec.chute)
appendChuteBand(svg, [{ points: fordChuteLine(design, spec.chute) }], spec.chute, x, y);
return true;
}
/**
* 유출측(`inflowSign` 반대) 노견 바깥 성토사면 선(136-2) — 설계선을 노견에서 바깥으로 따라
* 유출측(`inflowLeft` 반대) 노견 바깥 성토사면 선(136-2) — 설계선을 노견에서 바깥으로 따라
* 내려가는 동안만(오르면 절토라 멈춤). 성토가 없으면 빈 선(띠 안 그림).
*/
export function outletFillLine(design: CrossDesign): OffsetPoint[] {
@@ -242,60 +176,6 @@ export function fordChuteLine(design: CrossDesign, chute?: { length_m?: number }
return out;
}
/**
* 물넘이 자리 노면 · 측구 설계선을 걷는다(66-2 · 66-3) — 노면은 파인 바닥(실선)과 옛 계획고(점선)가
* 대신하고, 측구는 서지 않음. `from` = 설계선 그리기 앞의 자식 수(그 뒤에 붙은 선만 본다).
* 측구 자리는 바닥 끝에서 절토 비탈 발까지 실선으로 잇는다. 면적 · 서버 설계선은 그대로(그림만).
*/
export function hideFordPavedRoad(
layer: SVGElement,
from: number,
design: CrossDesign,
spec: FordPavementSpec,
x: (offset: number) => number,
y: (elevation: number) => number,
): void {
const edges = design.road_edges;
const designLine = design.design_line;
if (!edges || !designLine?.length) return;
const ditch = design.ditch;
const ditchWidth =
design.ditch_enabled === false || ditch.type === "none"
? 0
: ditch.type === "standard"
? ditch.top_width_m
: ditch.width_m;
// +offset = 좌측 · 측구는 노견 바깥 그 쪽에 붙는다
const leftEdge = Math.max(edges.left.offset_m, edges.right.offset_m);
const rightEdge = Math.min(edges.left.offset_m, edges.right.offset_m);
const hi = leftEdge + (design.ditch_side === "left" ? ditchWidth : 0);
const lo = rightEdge - (design.ditch_side === "right" ? ditchWidth : 0);
const [xa, xb] = [x(lo), x(hi)];
const inside = (px: number): boolean =>
px >= Math.min(xa, xb) - 0.5 && px <= Math.max(xa, xb) + 0.5;
for (const node of [...layer.children].slice(from)) {
if (node.tagName !== "line" || !node.classList.contains("b06-chart__design-cross")) continue;
if (inside(Number(node.getAttribute("x1"))) && inside(Number(node.getAttribute("x2"))))
node.remove();
}
if (!(ditchWidth > 0) || !design.ditch_side) return;
const edge = design.ditch_side === "left" ? leftEdge : rightEdge;
const foot = design.ditch_side === "left" ? hi : lo;
const sorted = [...designLine].sort((a, b) => a.offset_m - b.offset_m);
const at = sorted.find((point) => Math.abs(point.offset_m - foot) < 1e-3);
if (!at) return;
const edgeElevation = design.ditch_side === "left" ? edges.left : edges.right;
layer.append(
line(
[
`${x(edge)},${y(fordDeckElevationAt(design, spec, edge, edgeElevation.elevation_m))}`,
`${x(foot)},${y(at.elevation_m)}`,
],
"b06-chart__ford-deck",
),
);
}
/**
* 좌측 패널의 물넘이 칸을 캐시 제원에 얹는다(66-2 · 66-3) — 월류 높이 = 파임 깊이 · 경사각(°) → %.
* 서버 `_ford_pavement_set` 과 같은 환산(저장 뒤 서버가 정본으로 다시 냄). 바꿨으면 true.
-11
View File
@@ -17,7 +17,6 @@ import {
} from "./B06_Section_UI_Cross_Areas";
import {
appendFordPavementOverlay,
hideFordPavedRoad,
appendFordSurfaceDropPlan,
} from "./B06_Section_UI_Cross_Ford_Pavement";
import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire";
@@ -513,16 +512,6 @@ export function createCrossSectionCard(
[...plotLayer.children]
.slice(designFrom)
.forEach((el) => el.setAttribute(DESIGN_LINE_ATTR, ""));
// 물넘이 자리 = 노면 · 측구 설계선 걷음(66-2 · 66-3 — 옛 계획고 점선이 드러남)
if (fordPaved && section.ford_pavement)
hideFordPavedRoad(
plotLayer,
designFrom,
section.design,
section.ford_pavement,
x,
toDisplayY,
);
// 사토장(유용토운반작업장) — 교본 6장 3절이 횡단도 표시를 요구한다. 값은 설계 결과에서
// 그대로 오고 여기서 다시 세지 않는다. 선 종류는 터파기 파선과 갈라 둔다.
appendSpoilFillOverlay(plotLayer, section.design, x, toDisplayY);
@@ -254,7 +254,7 @@
stroke-width: 2.2;
}
/* 물넘이포장(2026-08-28): 파인 노면 — 기존 계획고 점선 + 바닥 실선 + 진한 회색 빗금 포장 */
/* 물넘이포장(287-2 · 287-4): 노면공 포장층(노견까지) + 월류 높이 점선 */
.b06-chart__ford-deck-plan {
fill: none;
stroke: var(--color-text-secondary);
@@ -262,24 +262,11 @@
stroke-dasharray: 5 4;
}
.b06-chart__ford-deck {
fill: none;
stroke: var(--color-text-body);
stroke-width: 1.8;
stroke-linejoin: round;
}
.b06-chart__ford-pavement {
stroke: var(--color-text-body);
stroke-width: 1;
}
.b06-chart__ford-pavement-hatch {
stroke: color-mix(in srgb, var(--color-text-body) 70%, transparent);
stroke-width: 1.2;
fill: none;
}
/* 배수관 세트(2026-08-19): 배관·기슭막이·돌붙임 — 분리 도형 3종 + 집수정 라벨 */
/* 관은 실무 도면처럼 점선 윤곽 2줄 느낌 — 채움은 식별용으로만 아주 옅게. */
.b06-chart__culvert-pipe {
+13 -7
View File
@@ -98,8 +98,10 @@ export interface CrossDesignOptions {
ditchChoice?: boolean | null;
/** 세월교 월류 높이만큼 노면을 통째로 내린다(m). */
surfaceDropM?: number;
/** 물넘이포장 범위 안 측점 — 측구 없음(286-2). 짝: 파이썬 `compute_cross_design(ford_paved=…)`. */
fordPaved?: boolean;
/** 물넘이포장 범위 · 맹암거 측점 — 측구 없음 · 노면 = `tiltPct`(비면 표준 횡단경사)로 지대 낮은 쪽 한 경사
* (286-2 · 287-4 · 288-1). 짝: 파이썬 `compute_cross_design(tilted=…, tilt_pct=…)`. */
tilted?: boolean;
tiltPct?: number | null;
/** 이 측점의 평면 곡선반경(m) — 곡선부 확폭을 정하는 입력. 직선이면 null. */
planRadiusM?: number | null;
/** 곡선 **바깥쪽**("left"/"right") — 확폭이 붙는 쪽(2026-09-06 사용자 확정). */
@@ -189,8 +191,9 @@ export interface CrossDesignResult {
/** 이 측점에 놓인 소단 제원 — 옹벽 의무 판정이 사면을 도막으로 끊는 데 쓴다. */
berm?: { width_m: number; interval_m: number; slope_deg: number; depth_m?: number };
surface_drop_m?: number;
/** 물넘이포장 범위 안 측점(286-2) — 측구 없음. */
ford_paved?: boolean;
/** 물넘이포장 · 맹암거 자리(286-2 · 287-4) — 측구 없음 · 지대 낮은 쪽 한 경사. */
tilted?: boolean;
tilt_pct?: number;
pavement_thickness_m?: number;
rock_boundary_offset_m?: number;
}
@@ -316,7 +319,9 @@ export function computeCrossDesign(
}
const pavedGroup = resolveGroup("paved", options.standard);
// 포장 중첩: 횡단경사와 포장층 두께만 포장 그룹을 따른다.
const crossSlopePct = paved ? pavedGroup.cross_slope_pct : group.cross_slope_pct;
const tiltPct = options.tilted && (options.tiltPct ?? 0) > 0 ? options.tiltPct! : null;
// 구조물 자리 경사(287-4 · 288-1 · 파이썬 짝)
const crossSlopePct = tiltPct ?? (paved ? pavedGroup.cross_slope_pct : group.cross_slope_pct);
const resolvedDitchSide = resolveDitchSide(sectionMode, options.ditchSide);
const valid: Array<[number, number]> = samples
@@ -371,7 +376,7 @@ export function computeCrossDesign(
berm: options.berm ?? null,
shoulderExtendLeftM: options.shoulderExtendLeftM ?? 0,
shoulderExtendRightM: options.shoulderExtendRightM ?? 0,
noDitch: Boolean(options.fordPaved),
tilted: Boolean(options.tilted),
});
// 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만).
@@ -586,7 +591,8 @@ export function computeCrossDesign(
design_line: designLine,
cut_slope_segments: geometry.cutSlopeSegments(),
};
if (options.fordPaved) result.ford_paved = true;
if (options.tilted) result.tilted = true;
if (options.tilted && options.tiltPct != null) result.tilt_pct = round4(options.tiltPct);
if (drop > 0) result.surface_drop_m = round4(drop);
if (options.berm) {
// 소단 제원을 설계에 되싣는다 — 짝 파이썬과 같은 까닭이고, **옹벽 의무 판정
@@ -145,8 +145,8 @@ export class SectionGeometry {
/** 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6). */
shoulderExtendLeftM?: number;
shoulderExtendRightM?: number;
/** 물넘이포장 자리 — 측구 없음(286-2 · 파이썬 짝 `no_ditch`). */
noDitch?: boolean;
/** 물넘이포장 · 맹암거 자리 — 측구 없음 · 노면 = 지대 낮은 쪽 한 경사(286-2 · 287-4 · 파이썬 짝 `tilted`). */
tilted?: boolean;
}) {
const { group } = params;
const halfRoad = group.road_width_m / 2;
@@ -171,6 +171,12 @@ export class SectionGeometry {
// 횡단경사: 측구 방향으로 내려가는 단일 사면 (좌=+offset 규약).
const slope = params.crossSlopePct / 100;
this.slopePerOffset = params.ditchSide === "left" ? -slope : slope;
// 물넘이포장 · 맹암거 자리(287-4 · 288-1) — 노면 = 한 경사로 지대 낮은 쪽(노면 끝 원지반이 낮은 쪽)
// 으로 기움 · 측구 없음(아래 · 파이썬 짝)
if (params.tilted && params.groundAt) {
const lowLeft = params.groundAt(this.leftExtent) < params.groundAt(-this.rightExtent);
this.slopePerOffset = lowLeft ? -slope : slope;
}
// 단면유형 자동 판정(D-2) — 노면 끝 지반이 설계면보다 높으면 절토, 낮으면 성토.
const groundAt = params.groundAt;
@@ -215,8 +221,8 @@ export class SectionGeometry {
this.hasDitch = choice === null || bothFill ? autoDitch : choice;
/** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */
this.ditchChoice = bothFill ? null : choice;
// 물넘이포장 자리(286-2) — 파인 노면이라 측구가 안 섬 · 선택은 그대로(파이썬 짝)
if (params.noDitch) this.hasDitch = false;
// 물넘이포장 · 맹암거 자리(286-2 · 288-1) — 측구가 안 섬 · 선택은 그대로(파이썬 짝)
if (params.tilted) this.hasDitch = false;
// 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
// 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`.
+1 -1
View File
@@ -505,7 +505,7 @@ function fordPavementSet(
{
type: "ford_pavement",
span_m: span,
// 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다).
// 월류 높이 — 노면 위 점선(287-4). 없으면 안 그린다(수치를 지어내지 않는다).
depth_m: depth && depth > 0 ? depth : null,
slope_pct: used !== null ? round(Math.tan((used * Math.PI) / 180) * 100, 4) : legacy,
defaulted: defaulted(values, [
@@ -25,23 +25,8 @@ const station = loadTs(path.join(ROOT, "common_util", "common_util_structure_sta
const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"), {
"./B06_Section_UI_Cross_Wall_Hatch": { appendChuteBand() {} },
"./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} },
});
// 286-2 노면 기울기 — 설계선 양 끝 표고가 높은 쪽 = 유입 · 표고 낮은 쪽(유출)으로 기움
const SPEC = { span_m: 6, depth_m: 0.3, slope_pct: 5 };
const designOf = (left, right, mode) => ({
design_elevation_m: 100,
cross_slope_pct: 2,
section_mode: mode,
design_line: [
{ offset_m: -10, elevation_m: right },
{ offset_m: 10, elevation_m: left },
],
});
const deck = (design) => ({
left: ford.fordDeckElevationAt(design, SPEC, 3),
right: ford.fordDeckElevationAt(design, SPEC, -3),
});
// 286-3 도수로 · 돌붙임 길이 — 유출(좌 · 우가 높음) 성토사면 1:1.5(5 m 내려감 · 사면 길이 √(7.5²+5²) ≈ 9.01)
const sets = loadTs(path.join(ROOT, "common_util", "common_util_culvert_sets.ts"));
const slopeDesign = {
@@ -73,8 +58,5 @@ const out = {
lineTooLong: ford.fordChuteLine(slopeDesign, chuteOf({ ford_chute_1_length_m: 50 })),
fordMarks: station.pipeStationMarks({ chainage_m: 230, facility: "ford_pavement" }),
planted: station.plantedChainages(input.pipes, input.structures, input.types),
deckLeftHigh: deck(designOf(105, 95, "right_cut")),
deckRightHigh: deck(designOf(95, 105, "left_cut")),
deckFlat: deck(designOf(100, 100, "right_cut")),
};
process.stdout.write(JSON.stringify(out));
@@ -36,7 +36,65 @@ const change = (form, c, i) => {
return form.state().chute[0].area;
};
// 287-2 · 287-4 그림 — DOM 흉내(속성 · 자식만)
class Node {
constructor(tag) {
this.tag = tag;
this.attrs = {};
this.children = [];
this.dataset = {};
this.style = {};
}
setAttribute(k, v) {
this.attrs[k] = String(v);
}
getAttribute(k) {
return this.attrs[k] ?? null;
}
append(...kids) {
this.children.push(...kids);
}
}
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
// 서버가 지대 낮은 쪽(우)으로 5 % 기울인 노면 · 노견 끝 ±3 · 차도 끝 ±2.5
const tilted = {
section_mode: "left_cut",
tilted: true,
cross_slope_pct: 5,
design_elevation_m: 100,
pavement_thickness_m: 0.2,
paved: true,
road_edges: {
left: { offset_m: 3, elevation_m: 100.15 },
right: { offset_m: -3, elevation_m: 99.85 },
},
carriageway_edges: {
left: { offset_m: 2.5, elevation_m: 100.125 },
right: { offset_m: -2.5, elevation_m: 99.875 },
},
design_line: [
{ offset_m: 6, elevation_m: 103 },
{ offset_m: 3, elevation_m: 100.15 },
{ offset_m: -3, elevation_m: 99.85 },
{ offset_m: -7, elevation_m: 97.2 },
],
};
const svg = new Node("g");
const drew = pave.appendFordPavementOverlay(
svg,
{ span_m: 8, depth_m: 0.4, slope_pct: 5 },
tilted,
(o) => o,
(e) => e,
);
const ofClass = (c) => svg.children.filter((k) => (k.attrs.class ?? "").split(" ").includes(c));
const out = {
drew,
pavePoints: ofClass("b06-chart__ford-pavement").map((k) => k.attrs.points),
planPoints: ofClass("b06-chart__ford-deck-plan").map((k) => k.attrs.points),
inflowLeft: pave.inflowLeft(tilted),
// 287-1 길이 넣음 = 월류 폭 × 길이 · 비움 = 기본값 · 폭 바꿈 = 길이 있을 때만 다시 셈 · 폭 비면 기본 폭
areaByLength: change(formOf({ width: "6" }, { length_m: "4", area: "30" }), "chute", "length_m"),
areaCleared: change(formOf({ width: "6" }, { length_m: "", area: "24" }), "chute", "length_m"),
@@ -103,14 +103,12 @@ def _ground(slope: float) -> list[dict]:
def test_물넘이_자리_측구_없음():
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
plain = compute_cross_design(_ground(0.35), 100.4, **kwargs)
ford = compute_cross_design(_ground(0.35), 100.4, ford_paved=True, **kwargs)
assert plain["ditch_enabled"] is True and "ford_paved" not in plain
assert ford["ditch_enabled"] is False and ford["ford_paved"] is True
ford = compute_cross_design(_ground(0.35), 100.4, tilted=True, **kwargs)
assert plain["ditch_enabled"] is True and "tilted" not in plain
assert ford["ditch_enabled"] is False and ford["tilted"] is True
assert ford["ditch_area_m2"] == 0
# 사용자가 측구를 켜도 물넘이 자리는 안 섬
forced = compute_cross_design(
_ground(0.35), 100.4, ford_paved=True, ditch_choice=True, **kwargs
)
forced = compute_cross_design(_ground(0.35), 100.4, tilted=True, ditch_choice=True, **kwargs)
assert forced["ditch_enabled"] is False
@@ -125,21 +123,22 @@ def test_물넘이_범위_서버_판정(tmp_path):
assert paved_at(230.0, ranges) and paved_at(226.0, ranges) and not paved_at(240.0, ranges)
def test_노면은_표고_낮은_쪽으로(ts):
# 좌 높음(입력 단면유형이 반대여도 지형) · 우 높음 · 같으면 단면유형(right_cut = 우 상류)
assert ts["deckLeftHigh"]["left"] > ts["deckLeftHigh"]["right"]
assert ts["deckRightHigh"]["right"] > ts["deckRightHigh"]["left"]
assert ts["deckFlat"]["right"] > ts["deckFlat"]["left"]
# 경사 5 % · 노선 중심 100 − 깊이 0.3 · 3 m 에서 ±0.15
assert ts["deckLeftHigh"]["left"] == pytest.approx(99.85)
assert ts["deckLeftHigh"]["right"] == pytest.approx(99.55)
def test_노면은_표고_낮은_쪽으로():
# 287-4 — 설계 노면 = 경사각도(%)로 지대 낮은 쪽 한 경사 · 중심 = 계획고(콘크리트 윗면)
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
for slope, high in ((0.35, "left"), (-0.35, "right")):
got = compute_cross_design(_ground(slope), 100.4, tilted=True, tilt_pct=5.0, **kwargs)
left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right"))
assert (left > right) == (high == "left")
assert got["design_elevation_m"] == 100.4 and got["cross_slope_pct"] == 5.0
assert got["tilt_pct"] == 5.0
def test_저장_뒤_서버가_물넘이_측구를_맞춤():
router = _read("B06_Section", "B06_Section_Router_Design.py")
assert 'bool(design.get("ford_paved")) == ford_paved' in router
assert "stored_tilt(design) == tilt" in router
refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts")
assert "fordPaved: Boolean(section.ford_pavement)," in refresh
assert "tilted: Boolean(section.ford_pavement)," in refresh
CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30}
@@ -3,6 +3,9 @@
못박는 것
- 287-1 도수로/돌붙임 길이 ↔ 면적 — 길이 넣음 = 월류 폭 × 길이 · 비움 = 칸 기본값 · 월류 폭을 바꾸면
길이가 있을 때만 다시 셈 · 폭이 비면 물넘이 기본 폭.
- 287-2 · 287-4 콘크리트 윗면 = 설계 노면(서버 · TS 짝이 경사각도로 지대 낮은 쪽 한 경사 · 중심점이 그 위)
· 포장층 = 노면공 포장 형상 노견까지(노면공 항목과 안 섞임) · 월류 높이 점선 = 노면 위 같은 각도 ·
3D 노면을 파지 않음 · 설계 측구 없음.
진짜 TS 는 `helper_287_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
"""
@@ -55,3 +58,26 @@ def test_도수로_길이_면적(ts):
def test_물넘이_폼이_길이_면적을_잇는다():
form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts")
assert "syncChuteArea(paveForm, c, item), paveFlowBox.onChange(c, item)" in form
def _pts(text: str) -> list[float]:
return [float(v) for pair in text.split() for v in pair.split(",")]
def test_포장층_노견까지_점선은_노면_위(ts):
assert ts["drew"] is True
# 포장층 하나 = 노견 끝(±3)까지 · 윗면 = 설계 노면 · 두께 0.2
assert len(ts["pavePoints"]) == 1
assert _pts(ts["pavePoints"][0]) == pytest.approx([3, 100.15, -3, 99.85, -3, 99.65, 3, 99.95])
# 월류 높이선 = 노면 + 0.4 · 같은 각도
assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, -3, 100.25])
assert ts["inflowLeft"] is True # 노면 높은 쪽 = 유입 · 도수로 띠 = 낮은 쪽
def test_3D_노면을_파지_않고_옛_파임_그림을_걷음():
station = _read("B05_Profile", "B05_Profile_UI_Corridor_Station.ts")
assert "fordDeckElevationAt" not in station
pave = _read("B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts")
assert "hideFordPavedRoad" not in pave and "fordDeckElevationAt" not in pave
view = _read("B06_Section", "B06_Section_UI_Cross_View.ts")
assert "hideFordPavedRoad" not in view
@@ -228,7 +228,21 @@ CASES = [
"section_mode": "left_cut",
"ditch_side": "left",
"paved": True,
"ford_paved": True,
"tilted": True,
},
},
{
# 287-4 — 물넘이포장 자리 노면 = 경사각도로 지대 낮은 쪽 한 경사(지반이 좌로 오름 = 우가 낮음)
"name": "물넘이포장 자리 노면 기울임",
"samples": _ground(0.35),
"design_elevation_m": 100.4,
"options": {
"ground_type": "soil",
"section_mode": "left_cut",
"ditch_side": "left",
"paved": True,
"tilted": True,
"tilt_pct": 5.2408,
},
},
]
@@ -254,7 +268,8 @@ const camel = {
curve_widening_m: "curveWideningM",
shoulder_extend_left_m: "shoulderExtendLeftM",
shoulder_extend_right_m: "shoulderExtendRightM",
ford_paved: "fordPaved",
tilted: "tilted",
tilt_pct: "tiltPct",
};
// 사토장 제원만 칸 이름이 다르다 — 파이썬은 snake, TS 는 camel 이다.
const spoilCamel = { side: "side", width_m: "widthM", slope_ratio_n: "slopeRatioN" };