fix(profile): B05 [저장] 구조물 측점 목록 = 서버 측점 규칙 · 소단 · 옹벽 · 흙막이 · 기슭막이 측점 지움 막음 (238-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -15,7 +15,12 @@ import aiomysql
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import chainage_key
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
from B05_Profile.B05_Profile_Engine_Sections import generate_irregular_sections
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
_load_pipe_points,
|
||||
_load_route_polyline,
|
||||
generate_irregular_sections,
|
||||
resolve_extra_stations,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions
|
||||
from B05_Profile.B05_Profile_Repository import get_surface_crs_epsg
|
||||
from B05_Profile.B05_Profile_Schema import RouteConfirmRequest
|
||||
@@ -350,17 +355,25 @@ async def _append_irregular_cross_sections(
|
||||
save_sampling_snapshot(project_root, request)
|
||||
stored_options = await get_latest_section_options(connection, project_id)
|
||||
crs_epsg = await get_surface_crs_epsg(connection, project_id, request.surface_model_id)
|
||||
irregular_stations = await asyncio.to_thread(
|
||||
generate_irregular_sections,
|
||||
project_root,
|
||||
str(route["route_data_path"]),
|
||||
request.filter_key,
|
||||
request.method,
|
||||
request.smooth,
|
||||
extra_stations=request.extra_stations(),
|
||||
options=_section_options_from_stored(stored_options),
|
||||
crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None,
|
||||
)
|
||||
route_data_path = str(route["route_data_path"])
|
||||
|
||||
def _generate() -> list[dict[str, Any]]:
|
||||
# 측점 목록 = 서버 정본(관 · 구조물)에서 — 화면 목록(관 · A군)을 받아 적으면 서버가 심은
|
||||
# 소단 · 옹벽 · 흙막이 · 독립 기슭막이 시작 · 종료 측점이 통째로 빠짐(238-1).
|
||||
polyline = _load_route_polyline(project_root, route_data_path)
|
||||
extras = resolve_extra_stations(project_root, _load_pipe_points(project_root, polyline))
|
||||
return generate_irregular_sections(
|
||||
project_root,
|
||||
route_data_path,
|
||||
request.filter_key,
|
||||
request.method,
|
||||
request.smooth,
|
||||
extra_stations=extras,
|
||||
options=_section_options_from_stored(stored_options),
|
||||
crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None,
|
||||
)
|
||||
|
||||
irregular_stations = await asyncio.to_thread(_generate)
|
||||
if not irregular_stations:
|
||||
return
|
||||
longitudinal = await get_longitudinal_section(connection, project_id, route["id"])
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""238-1 B05 [저장]이 구조물 측점을 지우지 않음.
|
||||
|
||||
[저장](경로 확정 길 · `_append_irregular_cross_sections`)은 종단 비정규 측점을 통째로 갈아 끼운다.
|
||||
그 목록을 화면이 보낸 「관 + A군」으로 쓰면 서버가 심어 둔 소단 · 옹벽 · 흙막이 · 독립 기슭막이
|
||||
시작 · 종료 측점이 빠진다 → 서버 측점 규칙(`resolve_extra_stations`)으로 만든 목록을 씀.
|
||||
|
||||
- 사본: 임시 폴더 프로젝트(직선 노선 · 관 하나 + 기슭막이 · 소단 · 옹벽 · 흙막이)
|
||||
- 앞: B06 [측점 만들기]와 같은 길(서버 목록으로 심음) · 뒤: 화면 목록(관만)을 실은 [저장]
|
||||
- 판정: 앞뒤 구조물 측점 같음 · 소단 측점(깊이 patch 가 붙는 자리) 남음 · 관 옮김은 반영
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
import B05_Profile.B05_Profile_Engine_Sections as sections
|
||||
import B05_Profile.B05_Profile_Router_Confirm as confirm
|
||||
from B05_Profile.B05_Profile_Schema import RouteConfirmRequest
|
||||
from common_util.common_util_drainage_pipes import route_signature
|
||||
from common_util.common_util_route_geometry import RouteVertex
|
||||
|
||||
LINE = [[0.0, 0.0, 100.0], [350.0, 0.0, 100.0]]
|
||||
ROUTE = "B05_Profile/route/route.geojson"
|
||||
LONG = "B06_Section/longitudinal/longitudinal.json"
|
||||
PIPE = {"chainage_m": 150.0, "source": "user", "options": {"pipe_diameter_mm": 800}}
|
||||
REVET = {
|
||||
"chainage_m": 301.0,
|
||||
"source": "user",
|
||||
"facility": "revetment",
|
||||
"start_m": 296.0,
|
||||
"end_m": 316.0,
|
||||
}
|
||||
STRUCTURES = [
|
||||
{"type_id": "soil_guard", "chainage_m": 45.0, "start_m": 41.0, "end_m": 51.0},
|
||||
{"type_id": "berm", "chainage_m": 201.0, "start_m": 196.0, "end_m": 206.0},
|
||||
{"type_id": "retaining_wall", "chainage_m": 251.0, "start_m": 246.0, "end_m": 256.0},
|
||||
]
|
||||
|
||||
|
||||
class _Flat:
|
||||
def sample_xy(self, xy):
|
||||
count = len(np.asarray(xy, dtype=np.float64))
|
||||
return np.full(count, 100.0), np.ones(count, dtype=bool)
|
||||
|
||||
|
||||
def _write(root: Path, rel: str, data) -> None:
|
||||
path = root / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
def _write_pipes(root: Path, points) -> None:
|
||||
vertices = [RouteVertex(x=p[0], y=p[1], z=p[2], chainage_m=0.0) for p in LINE]
|
||||
_write(
|
||||
root,
|
||||
"B04_PreProcess/drainage/edits/pipe_points.json",
|
||||
{"route_signature": route_signature(vertices), "points": points},
|
||||
)
|
||||
|
||||
|
||||
def _project(root: Path) -> None:
|
||||
_write(root, ROUTE, {"geometry": {"type": "LineString", "coordinates": LINE}})
|
||||
_write_pipes(root, [PIPE, REVET])
|
||||
for item in STRUCTURES:
|
||||
item["placement"] = "interval"
|
||||
_write(root, "B05_Profile/route/structures.json", {"revision": 1, "structures": STRUCTURES})
|
||||
regular = [{"chainage_m": float(c), "kind": "regular"} for c in range(0, 351, 20)]
|
||||
_write(root, LONG, {"stations": regular})
|
||||
|
||||
|
||||
def _irregular(root: Path) -> list[tuple[str, str]]:
|
||||
data = json.loads((root / LONG).read_text(encoding="utf-8"))
|
||||
return [
|
||||
(f"{float(s['chainage_m']):.2f}", s.get("structure"))
|
||||
for s in data["stations"]
|
||||
if s.get("kind") == "irregular"
|
||||
]
|
||||
|
||||
|
||||
def _patch(monkeypatch, root: Path) -> None:
|
||||
async def _value(value, *_a, **_k):
|
||||
return value
|
||||
|
||||
monkeypatch.setattr(sections, "build_surface_sampler", lambda *_a, **_k: _Flat())
|
||||
monkeypatch.setattr(confirm, "get_project_storage_relative_path", lambda *_a: _value("p"))
|
||||
monkeypatch.setattr(confirm, "resolve_stored_project_path", lambda _p: str(root))
|
||||
monkeypatch.setattr(confirm, "get_latest_section_options", lambda *_a: _value(None))
|
||||
monkeypatch.setattr(confirm, "get_surface_crs_epsg", lambda *_a: _value(None))
|
||||
monkeypatch.setattr(
|
||||
confirm,
|
||||
"get_longitudinal_section",
|
||||
lambda *_a: _value({"longitudinal_file_path": LONG}),
|
||||
)
|
||||
|
||||
|
||||
def _plant_like_b06(root: Path) -> None:
|
||||
"""B06 [측점 만들기]와 같은 길 — 서버 목록으로 뜨고 종단에 병합."""
|
||||
pipes = sections._load_pipe_points(root, LINE)
|
||||
stations = sections.generate_irregular_sections(
|
||||
root,
|
||||
ROUTE,
|
||||
"csf",
|
||||
"tin",
|
||||
False,
|
||||
extra_stations=sections.resolve_extra_stations(root, pipes),
|
||||
)
|
||||
confirm._merge_irregular_into_longitudinal(root, LONG, stations)
|
||||
|
||||
|
||||
def _save(root: Path, screen: list[tuple[float, str]]) -> None:
|
||||
"""B05 [저장] — 화면은 관 · A군 목록만 실어 보냄."""
|
||||
request = RouteConfirmRequest(
|
||||
filter_key="csf",
|
||||
method="tin",
|
||||
surface_model_id=1,
|
||||
irregular_stations=[{"chainage_m": c, "structure": s} for c, s in screen],
|
||||
)
|
||||
assert request.can_regenerate()
|
||||
asyncio.run(
|
||||
confirm._append_irregular_cross_sections(
|
||||
None, "p", {"id": 1, "route_data_path": ROUTE}, request
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_save_keeps_server_planted_stations(tmp_path, monkeypatch):
|
||||
_project(tmp_path)
|
||||
_patch(monkeypatch, tmp_path)
|
||||
_plant_like_b06(tmp_path)
|
||||
before = _irregular(tmp_path)
|
||||
labels = {label for _key, label in before}
|
||||
assert {"소단", "옹벽", "흙막이", "기슭막이"} <= labels
|
||||
assert [k for k, label in before if label == "기슭막이"] == ["296.00", "301.00", "316.00"]
|
||||
|
||||
_save(tmp_path, [(150.0, "파형강관 D800")])
|
||||
|
||||
assert _irregular(tmp_path) == before
|
||||
# 소단 깊이(194 · 측점 patch)가 붙는 자리 = 소단 측점 — 그대로 섬
|
||||
assert [k for k, label in _irregular(tmp_path) if label == "소단"] == [
|
||||
"196.00",
|
||||
"201.00",
|
||||
"206.00",
|
||||
]
|
||||
|
||||
|
||||
def test_save_follows_moved_pipe(tmp_path, monkeypatch):
|
||||
_project(tmp_path)
|
||||
_patch(monkeypatch, tmp_path)
|
||||
_plant_like_b06(tmp_path)
|
||||
before = _irregular(tmp_path)
|
||||
|
||||
# 관 150 → 165 (관 초안이 [저장] 차례상 먼저 정본에 들어감 · design_save.ts)
|
||||
_write_pipes(tmp_path, [{**PIPE, "chainage_m": 165.0}, REVET])
|
||||
_save(tmp_path, [(165.0, "파형강관 D800")])
|
||||
|
||||
after = _irregular(tmp_path)
|
||||
expected = sorted(
|
||||
[item for item in before if item[0] != "150.00"] + [("165.00", "파형강관 D800")],
|
||||
key=lambda item: float(item[0]),
|
||||
)
|
||||
assert after == expected
|
||||
Reference in New Issue
Block a user