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
+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 {