feat(B06): 세월교 노면을 월류 높이만큼 내려 앉힌다
횡단도에 그려지던 노면은 사실 월류부 가장 아래를 자른 자리다 — 구체 위 노면은 월류 높이만큼 낮게 앉아야 한다(2026-08-30 사용자 확정). 계획고를 통째로 내려 잡아 측벽·바닥판·십자선·절성토 면적이 한 번에 따라오게 했다. 전폭 평행 하강이라 횡단경사·측구·사면 규칙은 그대로다. 백엔드 - `_ford_set`이 `ford_height_m`을 `overflow_depth_m`으로 스펙에 싣는다. - `compute_cross_design(surface_drop_m=)` — 계획고를 그만큼 내려 계산하고, 내린 양을 `surface_drop_m`으로 echo한다(프론트가 점선을 되그리는 데 쓴다). - `ford_surface_drops()`·`ford_drop_at()` — 관 지점 정본에서 측점별 하강량을 읽는다 (포장 구간과 같은 방식). 설계 계산 경로 전부(기본값·재계산·프리뷰·종횡단 확정)에 물렸다. - `enforce_ford_surface_drops()` — 저장분이 옛 계획고면 상세 조회에서 다시 계산한다. 월류 높이를 지우면 같은 경로로 원래 계획고로 되돌아온다. 프론트 - 십자선을 하강량만큼 내려 찍는다(`cardDesignElevation`). 종단 계획고를 그대로 쓰던 자리다. - `appendFordSurfaceDropPlan()` — "월류가 없었다면" 노면을 점선으로 남긴다. 물넘이포장의 「기존 계획고 점선」과 같은 표기라 한 규칙으로 읽힌다. - 측벽·바닥판은 내려간 설계선(`design_line`)에 그대로 붙으므로 따로 손대지 않았다. 자체검증(공용 브라우저 5174 + 워크트리 백엔드 8001, 측점 7+9.7, 월류 높이 0.39m): - `tmp/tests/test_ford_surface_drop.py` 2건 통과 — 계획고·좌우 노견이 정확히 0.39m 내려가고, 절토는 늘고 성토는 줄며, 하강이 없으면 `surface_drop_m` 키 자체가 없다. - 화면 실측: y축 눈금 1m = 24.375px에서 십자선이 점선보다 9.50px 아래 (= 0.390m, 오차 0.001m). 수정 전에는 두 선이 같은 자리였다. - 세월교 측벽 자동 높이 1.70m → 1.50m. 이론값 1.31m이나 하한(관경 1.0 + 최소 토피 0.5 = 1.5m)에 걸린 값이다 — 기존 규칙 그대로다. - 성토 면적 18.46 → 13.85 ㎡. 이웃 일반 측점에는 점선이 그려지지 않는다. - 검증에 쓴 `pipe_points.json` 임시 옵션은 백업본으로 되돌렸다(정본 무변경). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,10 @@ from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과
|
||||
# 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다.
|
||||
_FORD_DROP_TOLERANCE_M = 0.02
|
||||
|
||||
PREVIEW_DESIGN_FIELDS = (
|
||||
"ground_type",
|
||||
"roadbed_width_m",
|
||||
@@ -69,6 +73,36 @@ def pavement_ranges(project_root: Path) -> list[tuple[float, float]]:
|
||||
return ranges
|
||||
|
||||
|
||||
def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]:
|
||||
"""세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)].
|
||||
|
||||
구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼
|
||||
내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로
|
||||
관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다).
|
||||
"""
|
||||
if project_root is None:
|
||||
return []
|
||||
drops: list[tuple[float, float]] = []
|
||||
try:
|
||||
for chainage, spec in load_culvert_sets(project_root).items():
|
||||
if spec.get("type") != "ford":
|
||||
continue
|
||||
depth = float(spec.get("overflow_depth_m") or 0.0)
|
||||
if depth > 0:
|
||||
drops.append((chainage, depth))
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다
|
||||
logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)")
|
||||
return drops
|
||||
|
||||
|
||||
def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float:
|
||||
"""그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다."""
|
||||
for at, depth in drops:
|
||||
if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M:
|
||||
return depth
|
||||
return 0.0
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]], stored: Any = None) -> bool:
|
||||
"""이 측점을 포장으로 볼 것인가 — 구간 안이면 강제, 밖이면 사용자 저장값(기본 비포장)."""
|
||||
for start, end in ranges:
|
||||
@@ -118,6 +152,7 @@ def enforce_pavement_ranges(
|
||||
ranges = pavement_ranges(project_root)
|
||||
if not ranges:
|
||||
return 0
|
||||
ford_drops = ford_surface_drops(project_root)
|
||||
changed = 0
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
@@ -141,6 +176,57 @@ def enforce_pavement_ranges(
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *_USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def enforce_ford_surface_drops(
|
||||
longitudinal: dict[str, Any],
|
||||
cross_sections: list[dict[str, Any]],
|
||||
project_root: Path,
|
||||
standard: dict[str, Any] | None = None,
|
||||
) -> int:
|
||||
"""세월교가 앉은 측점의 계획고를 월류 높이만큼 내려 다시 계산한다.
|
||||
|
||||
저장분은 월류 높이를 넣기 전에 계산된 것이라 노면이 그대로다 — 포장 구간과 같은
|
||||
방식으로 **저장분과 지금 값이 다를 때만** 다시 계산한다(2026-08-30 사용자 확정).
|
||||
월류 높이를 지우면 같은 경로로 원래 계획고로 되돌아온다. 사용자 조작값은 승계한다.
|
||||
"""
|
||||
drops = ford_surface_drops(project_root)
|
||||
changed = 0
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict):
|
||||
continue
|
||||
chainage = float(section.get("chainage_m", 0.0))
|
||||
wanted = ford_drop_at(chainage, drops)
|
||||
stored = float(design.get("surface_drop_m") or 0.0)
|
||||
if abs(wanted - stored) < 1e-6:
|
||||
continue
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
section.get("samples", []),
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
surface_drop_m=wanted,
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
@@ -194,6 +280,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_drops = ford_surface_drops(project_root)
|
||||
for section in cross_sections:
|
||||
if section.get("design"):
|
||||
continue
|
||||
@@ -209,6 +296,7 @@ def attach_default_designs(
|
||||
paved=paved_at(chainage, paved_ranges),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
)
|
||||
design.update(status="provisional", pavement_suggested=suggested)
|
||||
section["design"] = design
|
||||
@@ -227,6 +315,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_drops = ford_surface_drops(project_root)
|
||||
stored_by_chainage = {
|
||||
round(float(record["chainage_m"]), 3): (record.get("design") or {})
|
||||
for record in stored_designs
|
||||
@@ -258,6 +347,7 @@ def recompute_designs_for_alignment(
|
||||
),
|
||||
two_stage_slope=bool(stored.get("two_stage_slope", True)),
|
||||
ditch_enabled=stored.get("ditch_enabled"),
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
@@ -295,6 +385,7 @@ def compute_default_designs(
|
||||
default_modes = default_section_modes(longitudinal)
|
||||
pavement = pavement_suggestions(longitudinal)
|
||||
paved_ranges = pavement_ranges(root)
|
||||
ford_drops = ford_surface_drops(root)
|
||||
results: list[tuple[float, dict[str, Any]]] = []
|
||||
for chainage_m in chainages:
|
||||
cross_path = cross_dir / cross_filename(chainage_m)
|
||||
@@ -315,6 +406,7 @@ def compute_default_designs(
|
||||
paved=paved_at(chainage_m, paved_ranges),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
surface_drop_m=ford_drop_at(chainage_m, ford_drops),
|
||||
)
|
||||
design["status"] = "provisional"
|
||||
design["pavement_suggested"] = suggested
|
||||
|
||||
Reference in New Issue
Block a user