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:
@@ -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`). 지표면~암반 경계선이 토사, 그 아래가 암이다.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user