feat(B03/B04): LAS 없이 도엽등고선으로 3D 서피스를 만들어 설계한다

- B03: 'LAS 없이 설계' 토글 — LAS 필수카드 비활성화, las_free 플래그로
  업로드·완료 검증 면제, 계획노선 CSV를 WF1 분석 입력으로 사용
- B04: 신규 Engine_SheetSurface — 도엽_등고선.geojson을 노선 bbox+300m
  직사각형으로 절취, 배수유역 엔진의 등고선 정점구름·Delaunay TIN 보간을
  재사용해 dtm_sheet.npz(1m 격자, LAS DTM과 동일 형식) + 프리뷰 glb 생성.
  build_surface_sampler('sheet','dtm')로 종·횡단·배수 하류 계산 무수정 동작
- WF1: las_free면 run_sheet_surface_analysis로 분기, sheet/dtm 자동 확정.
  VWorld·도엽 확보 블록을 download_geodata()로 추출해 두 경로가 공유
- LAS가 있어도 도엽 서피스를 함께 생성·등록(참고용), B04에
  '도엽등고 3D 서피스' 별도 컨테이너로 표시

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 14:27:41 +09:00
co-authored by Claude Opus 5
parent 6d8fd46d87
commit 3572694f73
14 changed files with 1047 additions and 200 deletions
+42 -13
View File
@@ -20,6 +20,7 @@ from config.config_db import get_db_pool
from config.config_system import (
AUTO_DESIGN_CHAIN_ENABLED,
SEND_ANALYSIS_COMPLETION_EMAIL,
SURFACE_CONTOUR_INTERVAL_M,
SURFACE_MODEL_PRECOMPUTE,
SURFACE_MODEL_SOURCE_FILTERS,
)
@@ -79,9 +80,15 @@ async def trigger_wf1_analysis_and_email(
input_file = await get_input_file(connection, project_id, input_file_id)
project_root = Path(resolve_stored_project_path(stored_path))
las_path = project_root / Path(str(input_file["raw_file_path"]))
if not las_path.is_file():
raise FileNotFoundError("원본 LAS/LAZ 파일을 찾을 수 없습니다.")
source_path = project_root / Path(str(input_file["raw_file_path"]))
# LAS 없는 설계(2026-08-30): 입력이 계획노선 CSV면 도엽등고선 서피스 분석으로 간다.
las_free = str(input_file.get("file_type") or "").lower() not in {"las", "laz"}
if not source_path.is_file():
raise FileNotFoundError(
"계획 노선 파일을 찾을 수 없습니다."
if las_free
else "원본 LAS/LAZ 파일을 찾을 수 없습니다."
)
from B04_PreProcess.B04_PreProcess_Engine import run_surface_analysis
from B04_PreProcess.B04_PreProcess_Repository import save_surface_analysis_to_db
@@ -92,15 +99,27 @@ async def trigger_wf1_analysis_and_email(
def _on_progress(percent: int, stage: str, message: str) -> None:
write_surface_progress(project_root, percent, stage, message)
analysis_result = await asyncio.to_thread(
run_surface_analysis,
project_root,
las_path,
source_filters=source_filters,
methods=methods,
force=False,
on_progress=_on_progress,
)
if las_free:
from B04_PreProcess.B04_PreProcess_Engine_SheetSurface import (
run_sheet_surface_analysis,
)
analysis_result = await asyncio.to_thread(
run_sheet_surface_analysis,
project_root,
source_path,
on_progress=_on_progress,
)
else:
analysis_result = await asyncio.to_thread(
run_surface_analysis,
project_root,
source_path,
source_filters=source_filters,
methods=methods,
force=False,
on_progress=_on_progress,
)
auto_confirmation_error: str | None = None
auto_confirmed = False
@@ -124,7 +143,17 @@ async def trigger_wf1_analysis_and_email(
find_surface_model_for_selection,
)
selection = surface_confirmation_defaults()
# LAS 없는 설계는 도엽 서피스 모델(sheet/dtm)로 확정한다.
selection = (
{
"source_filter": "sheet",
"method": "dtm",
"smooth": False,
"contour_interval_m": SURFACE_CONTOUR_INTERVAL_M,
}
if las_free
else surface_confirmation_defaults()
)
try:
model_id = await find_surface_model_for_selection(
connection, project_id, selection