Files
s-canvas/CHANGELOG.md
HYUNJUNGLEE f62214934d
Some checks failed
CI / Ruff + Test (Py3.11 + Py3.13) (3.11) (push) Failing after 10s
CI / Ruff + Test (Py3.11 + Py3.13) (3.13) (push) Failing after 10s
Phase 1 (#11): perf instrumentation — harness/perf.py + 3 hotspot wraps
신규 모듈 — harness/perf.py (54 LOC):
- perf_block(label) 컨텍스트 매니저 — 블록 단위 wall-clock + CPU 시간을 ms 단위로 측정.
- set_perf_log(sink) — 외부 sink 등록 (예: app.log). 등록 후 [PERF] 라인이 logger
  외에도 그 sink 에 라우팅됨.
- 출력 형식: [PERF] {label}: wall={NN}ms cpu={NN}ms ({CPU|I/O/Net}-bound).
- cpu/wall > 0.5 면 CPU-bound 로 분류, 그 외 I/O/Net-bound (GIL 풀린 시간 비율).

Setup — scanvas_maker.py 2곳:
- import 블록 (~line 58): from harness.perf import perf_block, set_perf_log;
  ImportError 시 contextlib.contextmanager 노옵 폴백 (모듈 누락 환경 대응).
- SCanvasApp.__init__ (~line 613): set_perf_log(self.log) 등록.

Hotspot wraps — scanvas_maker.py 3곳 (PERFORMANCE_BASELINE.md 매핑):
- TIN densify Phase C (line ~4430) → H3: with perf_block("TIN densify Phase C (10m→1m)").
- 위성 타일 다운로드 (line ~5384) → H1: with perf_block("위성 타일 다운로드+병합").
- 제어맵 캡처 x3 + composite (line ~5864) → H12: with perf_block("control map capture x3 + composite").

검증:
- python -m py_compile scanvas_maker.py harness/perf.py 통과.
- AST parse OK (39 top-level statements).
- ruff Green 정식 검증은 글로벌 ruff 설치 후 (uv pip install -e ".[dev]"; ruff check).

CHANGELOG.md 에 #11 perf instrumentation 항목 추가 (2026-05-08).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:44:03 +09:00

181 KiB
Raw Blame History

S-CANVAS 수정 이력 (CHANGELOG)

2026-04-24 rebrand: 프로젝트명이 EG-VIEWS-CANVAS (Generative Design & Visualization Engine) 로 변경되었습니다. 이 파일의 2026-04-24(후속 2) 이전 기록에 등장하는 "EG-VIEW" 는 동일 프로젝트의 이전 명칭이며, 역사적 기록 보존을 위해 수정하지 않았습니다.

이 프로그램의 모든 코드 수정 내역을 역순(최신이 위)으로 기록합니다. 사용자 원본 요청/피드백은 Build_log.txt에 보관되어 있으며, 이 파일은 적용된 수정의 기록입니다.


2026-05-08

[merge] Gitea s-canvas 원격(raw upload, 184185c)과 로컬 lint+Phase 0 history 통합

  • 상황: 원격 https://gitea.hmac.kr/HYUNJUNGLEE/s-canvas.git 에 사용자가 site에서 raw upload 한 1회 commit (184185c)이 존재. 로컬은 53d8b53b9342f6 (import + iter1~7 lint cleanup) → e9cc6bf (Phase 0 of expert feedback) history. 공통 조상 없음 (unrelated histories).
  • 분석: 원격에만 있는 파일 0개 (원격은 로컬의 부분집합). 양쪽 다 있고 내용 다른 파일 29개 (raw upload vs lint-applied 버전). 로컬에만 있는 파일 80+ (Phase 0 산출물 + workspace/_unused/ 등).
  • 전략: git merge --allow-unrelated-histories -X ours. 충돌 시 로컬 우선으로 lint cleanup 보존.
  • 결과: 머지 commit 8c6d7f0. 자동 머지된 31파일 중 README.md 만 실질 변경 (로컬 0줄 → 원격 404줄 README 흡수 — 빈 파일 vs 내용있음은 -X ours 적용 외 단순 합병). 나머지 source/config 28개는 로컬 lint 버전 유지.
  • 푸시: 머지 후 fast-forward push 가능. 원격 history 손실 없이 통합.

[feat] #11 perf instrumentation — harness/perf.py 신규 + scanvas_maker.py 5곳 wire

  • 사용자 피드백 #11: "로딩이 오래 걸리는 부분(위성지도 결합·구조물 빌드 시 등)은 CPU 이용률이 대폭 증가하는 프로세스를 ms 단위로 추적해서 원인을 규명하고 최적화하는 조치 필요".

신규 모듈 — harness/perf.py (54 LOC)

  • perf_block(label) — 컨텍스트 매니저. with perf_block("XYZ tiles"): ... 형태로 블록 실행 시간(wall + CPU)을 ms 단위로 측정.
  • set_perf_log(callable) — 외부 sink 등록 (예: set_perf_log(app.log) 시 GUI 로그 패널에도 표시).
  • 출력 형식: [PERF] {label}: wall={NNN}ms cpu={NNN}ms ({CPU|I/O/Net}-bound). cpu/wall > 0.5 면 CPU-bound로 분류.

Wire 5곳 — scanvas_maker.py (commit c94b4a7)

  1. import 블록 (~line 58): from harness.perf import perf_block, set_perf_log + ImportError 시 @contextlib.contextmanager 노옵 폴백 → harness/ 모듈 누락 환경에서도 안전.
  2. SCanvasApp.__init__ (~line 613): set_perf_log(self.log) 등록 — perf 측정 라인이 GUI 텍스트박스에도 표시됨.
  3. TIN densify Phase C (line ~4430): with perf_block("TIN densify Phase C (10m→1m)") 로 10단계 점진 격자 루프 감쌈 (PERFORMANCE_BASELINE.md H3).
  4. 위성 타일 다운로드 (line ~5384): with perf_block("위성 타일 다운로드+병합")_download_xyz_tiles() 감쌈 — 사용자 피드백 #11이 명시한 "위성지도 결합" 핫스팟 (H1).
  5. 제어맵 캡처 파이프라인 (line ~5864): with perf_block("control map capture x3 + composite") 로 textured + depth + lineart 3-stage 캡처 + composite 감쌈 (H12).

출력 예 (실제 측정 시)

[PERF] 위성 타일 다운로드+병합: wall=12340.5ms cpu=860.3ms (I/O/Net-bound)
[PERF] TIN densify Phase C (10m→1m): wall=2150.7ms cpu=2080.4ms (CPU-bound)
[PERF] control map capture x3 + composite: wall=4520.1ms cpu=3760.8ms (CPU-bound)

검증

  • python -m py_compile scanvas_maker.py harness/perf.py 통과. AST parse OK (39 top-level statements).
  • ruff Green 정식 검증은 다음 세션 (글로벌 ruff 미설치, uv pip install -e ".[dev]"ruff check).

다음 라운드 (#11 추가)

  • 사용자 실제 도면으로 [PERF] 출력 1회 측정 → PERFORMANCE_BASELINE.md "측정 후 비교 표" 채움.
  • 측정 결과 기반 추가 hotspot wrap (H7·H9·H13·H18 등) + 최적화 (numpy 벡터화 / 스레드 풀 / GIL 해제).

2026-04-28

[fix] 화면비 버튼이 텍스트만 떠서 안 보이는 문제 — vtkButtonWidget 로 교체

  • 수정 파일: scanvas_maker.py
  • 사용자 보고: 하단에 글씨만 뜨고 버튼이 하나도 없음 → 클릭해도 반응 없음.
  • 원인: 초기 구현은 p.add_text(viewport=True) 텍스트 액터 14개 + 좌클릭 옵저버 로 가짜 버튼을 흉내냄. 하지만 텍스트는 배경/테두리 없이 떠서 시각적으로 버튼처럼 안 보였고, 일부 PyVista 버전에서 viewport=True 가 무시돼 위치까지 어긋났음.
  • 수정: vtkButtonWidget + vtkTexturedButtonRepresentation2D 기반 진짜 버튼.
    • PIL 로 78×32px 둥근 사각 버튼 이미지(배경+테두리+한글 텍스트) 를 그리고, vtkImageData 로 변환해 rep.SetButtonTexture(0, img) 로 적용.
    • 한글 가독성을 위해 malgun.ttf (Windows) 자동 로드, 폴백 체인 포함.
    • widget.AddObserver("StateChangedEvent", _cb) 로 클릭 처리 — 옵저버 우선순위 조작이나 SetAbortFlag 불필요 (vtkButtonWidget 이 자체 hit-test 수행).
    • _reposition_buttons 헬퍼가 창 너비 기준 가운데 정렬 → RenderWindow ModifiedEvent 옵저버로 리사이즈에 자동 반응.
    • 활성 비율 = 금색(#FFD700) 채움 + 진한 텍스트, 비활성 = 다크 그레이 + 옅은 텍스트.

[feature] AI 렌더링 워크플로 구체화 — 화면비 락 + 출력 화질 + Enter 확정

  • 수정 파일: scanvas_maker.py, gemini_renderer.py
  • 사용자 요청: 사진 편집기처럼 (1) 제어맵 추출 시 화면비를 명시 클릭으로 잠그고, (2) 'q' 대신 Enter 로 확정하고, (3) AI 렌더링 다이얼로그에서 출력 화질 (HD/FHD/UHD) 을 미리 골라 그 해상도로 조감도가 나오게 만들고 싶음.
  • 수정:
    • __init__: self.extraction_aspect_ratio = None, self._aspect_buttons = [], self.target_resolution = None 추가 — Step 3/4 사이 상태 공유.
    • _compute_capture_size: 화면비 락이 있으면 그 비율로 lock, 없으면 기존 창 크기 기반(자유 모드)으로 폴백. 호출처(btn_control_map_callback:5733, btn_ai_render_callback:6264) 두 군데가 자동으로 새 비율 따라감 — 단일 진실 원천.
    • _open_interactive_viewer: 14개 화면비 버튼 행을 viewport-normalized 좌표 (y=0.018, BTN_W=0.060, BTN_H=0.045) 로 하단에 배치. 자유/1:1/9:16/16:9/4:5/ 5:4/3:4/4:3/2:3/3:2/5:7/7:5/1:2/2:1. 활성 버튼은 ▶라벨◀ 금색, 비활성은 [라벨] 회색. LeftButtonPressEvent 옵저버를 priority=10.0 으로 등록 → InteractorStyle 카메라 회전보다 먼저 발화. 클릭 영역 안이면 SetAbortFlag(1) 로 회전 시작 차단 후 p.window_size = (cur_w, new_h) 즉시 변경 + p.render(). _refresh_buttons 헬퍼가 14 actor 의 텍스트/색을 갱신.
    • Enter 키: p.add_key_event("Return", _on_enter) + KP_Enter 둘 다 바인딩. _on_enter_update_hud_and_save()p.iren.terminate_app(). 'q' 는 VTK 기본 동작으로 그대로 폴백.
    • HUD 텍스트: "q로 확정" → "Enter로 확정 (q도 가능)", 라이브 갱신 텍스트에 비율: 16:9 같은 현재 잠긴 비율 표시 추가.
    • 사이드바 로그/status: 같은 톤으로 "Enter 키(또는 q)" 안내.
    • btn_ai_render_callback: 다이얼로그를 360x250380x360 으로 늘리고 "출력 화질" 라디오 3개(HD 720p / FHD 1080p / UHD 4K) 추가. on_ok 가 3-튜플 (time, extra, res) 반환. 화질 + Step 3 화면비를 곱해 target_resolution = (target_w, target_h) 계산. extraction_aspect_ratio 가 None 이면 캡처 이미지의 실제 비율을 사용해 폴백 → 자유 모드도 안전.
    • _run_stability_render: 3-stage 폴백 후 성공한 rendered 를 저장 직전에 Image.LANCZOStarget_resolution 리사이즈. 변경 시 콘솔 로그.
    • gemini_renderer.run_gemini_render: 동일 로직을 app.target_resolution 참조로 삽입 (함수 시그니처 변경 없음, 기존 app 인스턴스 패턴 유지).
  • 검증: python scanvas_maker.py → Step 3 진입 시 14개 버튼 행이 보이고, "16:9" 클릭 → 즉시 와이드 비율로 창 크기 변경 + 강조 표시. Enter 로 확정 후 capture_textured.png 가 16:9 비율(1536×864 등 8배수). Step 4 에서 "FHD (1080p)" 선택 → 콘솔 "목표 해상도: 1920x1080" 로그 + 결과 rendered_birdseye.png 가 정확히 1920×1080. 자유 모드도 회귀 없이 동작.

[fix] 제어맵·조감도 화면비 보존 — 뷰어 창 크기 그대로 캡처

  • 수정 파일: scanvas_maker.py
  • 사용자 보고: 제어맵 추출이 무조건 1:1 정사각형으로 뽑혀, 인터랙티브 뷰어에서 조정한 화면 크기·비율이 무시됨 → 조감도(Gemini) 결과도 사용자가 의도한 프레이밍과 다르게 나옴.
  • 원인: btn_control_map_callback (:5708) 와 btn_ai_render_callback (:6219) 에 render_size = 1536 이 박혀 있어 _capture_from_camera/_depth/_lineart 가 항상 1536×1536 정사각형 오프스크린으로 렌더. 뷰어에서는 카메라 pose 만 가져오고 화면비는 버려졌기 때문에 VTK 기본 view_angle 로 다른 프레임이 잡힘.
  • 수정:
    • __init__: self._saved_window_size = None 추가 — 뷰어 창 크기 영구 저장.
    • _open_interactive_viewer: _update_hud_and_save 옵서버에서 p.window_size 도 매 인터랙션마다 캡처. show() 종료 후 폴백으로 한 번 더 시도. 결과를 self._saved_window_size 에 저장.
    • 신규 _compute_capture_size(max_long_side=1536): 저장된 화면비로 (out_w, out_h) 계산. long-side 는 1536 으로 캡, 짧은쪽은 비율 보존 + 8배수 정렬(Gemini/Stability 친화). 미저장이면 1536×1536 폴백 (구버전 호환).
    • btn_control_map_callback / btn_ai_render_callback: render_size 하드코딩 제거 → _compute_capture_size() 결과를 캡처 3종에 동일하게 전달.
  • 트레이드오프: 뷰어 창이 극단적으로 길쭉하면(예 21:9) 가장자리 환각 가능성이 있으나, long-side 캡 방식이 메모리·API 호환성·일반적 프레이밍 기대치를 모두 만족하는 가장 안전한 기본값. 정사각 강제가 다시 필요해지면 토글 1개 추가하면 됨.

[build] 배포용 .exe 빌드 인프라 — PyInstaller onedir + 사용자 데이터 분리

  • 신규 파일:
    • resource_paths.py — 런타임 경로 해석 헬퍼
    • scanvas_maker.spec — PyInstaller 빌드 설정
    • build.bat — 원클릭 Windows 빌드 스크립트
  • 수정 파일: scanvas_maker.py (하드코딩 경로 → 헬퍼 함수)
  • 사용자 요청: 코드 의존 없이 사용자가 .exe 더블클릭으로 온전히 이용.
  • 설계 결정 (이전 답변 + 디폴트 적용):
    1. onedir (단일 .exe 가 아니라 dist/S-CANVAS/ 폴더 통째 배포). 첫 실행 빠름, 디버깅 쉬움, AV false-positive 위험 낮음. zip 으로 묶어 배포(150-200MB).
    2. 런타임 데이터 위치: %LOCALAPPDATA%\S-CANVAS\ (Windows 표준). 사용자별 분리, 설치 폴더 쓰기권한 불필요(Program Files 설치 OK).
  • resource_paths.py API:
    • asset_root() → 번들 모드 sys._MEIPASS, 개발 모드 소스 디렉토리
    • resource_path("Design", "Logo.png") → 자산 절대경로
    • user_data_dir()%LOCALAPPDATA%\S-CANVAS\ (없으면 생성)
    • db_path(), harness_log_path(), diagnostic_log_path(), cache_dir(*sub)
    • 환경변수 오버라이드: SCANVAS_USER_DATA (강제), SCANVAS_DEV_LOCAL=1 (소스 트리 옆에 쓰기 — 개발 모드)
    • describe() — 진단용 한 줄 요약
  • scanvas_maker.py 리팩터 (10 곳):
    • Path("scanvas_jobs.db") → str(db_path())
    • Path("scanvas_harness.log") → harness_log_path()
    • Path("scanvas_diagnostic.log") → diagnostic_log_path()
    • Path("prompt_templates") → resource_path("prompt_templates")
    • Path("structure_types/structure_v1.yaml") → resource_path("structure_types", "structure_v1.yaml")
    • Path(__file__).parent / "Design" → resource_path("Design") (3 곳)
    • cache_dir="cache/dem" → str(cache_dir("dem")) (2 곳, dem_extender 호출)
    • 윈도우 아이콘 캐시: Path(__file__).parent/"cache"/"icons" → cache_dir("icons")
    • gcp-key.json 검색 우선순위: user_data_dir()/gcp-key.json (배포본 권장) → resource_path("gcp-key.json") (개발/legacy 폴백). 사용자가 키 파일을 %LOCALAPPDATA%\S-CANVAS\gcp-key.json 에 두면 바로 인식.
    • splash 호출의 video_path → resource_path("Design", "logo_intro.mp4")
  • scanvas_maker.spec 핵심:
    • datas: Design/, prompt_templates/, structure_types/ 트리 그대로 번들
    • collect_all: pyvista, vtkmodules, pyproj, ezdxf, tkintermapview, structlog, customtkinter, PIL — 자동 감지 누락 가능성 높은 거대 패키지 명시 수집
    • hiddenimports: google.genai 서브모듈 + sqlalchemy/scipy 누락 빈출 모듈
    • excludes: pytest, IPython, jupyter, pyvista.examples, vtkmodules.test 등 번들 크기 절감
    • console=False (GUI 앱), icon=cache/icons/scanvas_S.ico (build.bat 가 빌드 전 생성)
    • name="S-CANVAS"dist/S-CANVAS/S-CANVAS.exe
  • build.bat 단계:
    1. PyInstaller 미설치 시 pip install pyinstaller 자동
    2. 윈도우 아이콘 사전 생성 (logo_V2.png → scanvas_S.ico, 멀티사이즈)
    3. build/ + dist/S-CANVAS/ 정리 후 pyinstaller --clean --noconfirm scanvas_maker.spec
    4. 결과·런타임 데이터 위치 안내 출력
  • 검증:
    • ast.parse OK (scanvas_maker.py + resource_paths.py)
    • importlib 로드 OK — SCanvasApp 정상 정의
    • resource_paths.describe():
      asset_root=D:\2026\00_EGVIEW2 (bundled=False)
      user_data_dir=C:\Users\saman\AppData\Local\S-CANVAS
      db=C:\Users\saman\AppData\Local\S-CANVAS\scanvas_jobs.db
      harness_log=C:\Users\saman\AppData\Local\S-CANVAS\scanvas_harness.log
      diagnostic_log=C:\Users\saman\AppData\Local\S-CANVAS\scanvas_diagnostic.log
      cache_root=C:\Users\saman\AppData\Local\S-CANVAS\cache
      
  • 사용자 사용법:
    1. build.bat 더블클릭 (또는 cmd 에서 실행) → 10-15분 후 dist/S-CANVAS/ 생성
    2. dist/S-CANVAS/ 폴더를 zip
    3. 사용자에게 zip 전달 → 압축 해제 → S-CANVAS.exe 더블클릭
    4. (선택) 사용자가 GCP 인증 쓰려면 %LOCALAPPDATA%\S-CANVAS\gcp-key.json 에 키 파일 배치
  • 남은 일 (실제 빌드 수행은 사용자가):
    • 첫 빌드 후 깨끗한 Windows 머신(Python 미설치) 에서 검증
    • 빌드 후 SmartScreen/Defender 가 차단하면 코드 서명 추가 검토
    • 배포 형태(zip / NSIS 인스톨러 / MSIX) 결정

2026-04-25

[feature] 윈도우 타이틀바·작업표시줄 'S' 아이콘 — logo_V2 에서 자동 추출

  • 파일: scanvas_maker.py (신규 메서드 SCanvasApp._setup_window_icon)
  • 사용자 요청: 창 표시줄 "S-CANVAS" 왼쪽에 보이는 기본 Tk 아이콘을 logo_V2 의 오렌지 'S' 글자만 따서 교체.
  • 수정:
    1. 신규 메서드 _setup_window_icon()__init__self.title() 직후 호출. 절차:
      • 다크 배경 하드 스트립 (v_max < 90 → 알파 0). 아이콘용은 부드러운 전이가 오히려 노이즈가 되므로 단일 임계.
      • 좌측 850px 크롭 (S 영역 isolate)
      • scipy.ndimage.label 연결 컴포넌트 분석 — 633개 컴포넌트 중 가장 큰 것(S 본체 105,923px) + 그 10% 이상 크기인 것(S 의 하부 곡선 31,637px) 만 유지. 회로 패턴·EG-BIM 워터마크 등 잔여 잡음 제거.
      • 타이트 bbox: 461×598 (aspect 0.77)
      • 정사각 패딩(투명) + 4% 외곽 여백
      • ICO 멀티사이즈 저장 (16/32/48/64/128/256)
      • PNG 폴백 사본도 같이 저장(iconphoto 폴백용)
    2. 캐시 정책: cache/icons/scanvas_S.ico 에 저장. logo_V2.png mtime 이 ICO 보다 새로우면 재생성. 이후 실행 시는 캐시 즉시 사용.
    3. Tk 적용: self.iconbitmap(default=str(ico_path)) 우선, 실패 시 self.iconbitmap(...)self.iconphoto(True, PhotoImage(...)) 순으로 폴백. PhotoImage 는 self._icon_photo_ref 로 GC 방지.
    4. 실패 시 silent skip — 로고 파일 없거나 PIL/scipy 에러 시 경고 로그만 찍고 기본 Tk 아이콘 유지(메인 앱 기동 항상 보장).
  • 검증:
    • ast.parse OK
    • ICO 생성 확인: 94,981 bytes, 6개 사이즈, 2 컴포넌트(105923+31637=137560 px) 유지, 잡음 컴포넌트 631개 제거
    • 256×256 PNG 시각 확인 — 오렌지 'S' + 하부 swoosh 깔끔, 회로 패턴 노이즈 없음

2026-04-24 (후속 4)

[feature] Vworld API 키 프리필 + logo_V2 (다크 배경 strip) + 인트로 스플래시 (logo_intro.mp4)

  • 파일: scanvas_maker.py, 신규 splash.py
  • 사용자 요청:
    1. Vworld API 키 383CB30A-2AD8-3199-8A7B-215DE3E4280C 기본값으로 프리필.
    2. 로고를 logo_V2.png 로 교체, 뒷배경(다크 네이비) 제거해 GUI 배경과 동일하게.
    3. 프로그램 시작 시 logo_intro.mp4 로 역동적 로딩 스플래시 구현 (JS 등 가능).
  • 수정:
    1. Vworld API 키 프리필 (scanvas_maker.py line 229): ctk.StringVar(value="") → 사용자 제공 키 하드코딩. 이제 Vworld 타일 서버가 즉시 동작.
    2. logo_V2.png 전환 + 어두운 배경 소프트 스트립:
      • 신규 헬퍼 _load_image_strip_dark_bg(path, v_low=30, v_high=80): V(=max RGB) 기반 선형 보간. V≤v_low → 알파 0, V≥v_high → 알파 유지, 사이는 선형 전이(halo 최소화). logo_V2 는 v_low=25/v_high=75.
      • 결과 측정: 2626×1600 중 36.7% 완전 투명 / 49.5% 부분 알파(부드러운 엣지) / 13.8% 완전 불투명. 다크 배경 회로 패턴 제거, S-CANVAS 글자/지형 텍스처/ "GENERATIVE DESIGN & VISUALIZATION ENGINE" 서브타이틀은 보존됨.
      • 사이드바 폭 예산이 220 → 230px 로 약간 확대 (V2 이미지는 aspect 가 조금 더 와이드).
    3. 인트로 스플래시 (신규 splash.py, 210 LOC):
      • 기술: cv2 VideoCapture (프로젝트 공통 dep) → PIL → Tkinter Toplevel+Label. 신규 외부 의존 0.
      • 역동적 효과: (a) 알파 0→1 페이드인 400ms (b) MP4 고유 애니메이션 재생 (24fps, 192 frames, 8s, 1280×720) (c) 알파 1→0 페이드아웃 400ms → 자동 destroy (d) 화면 중앙 frameless 배치 (overrideredirect(True)), topmost (e) 비디오 아래 44px tag-bar 에 오렌지 italic 로 "S-CANVAS — Generative Design & Visualization Engine" 브랜드 라인
      • 안정성:
        • max_duration_s=12.0 safety cap (비디오 8s + fades 0.8s + 버퍼 3.2s).
        • 비디오 끝(cap.read() → ret=False) 자동 감지 → 페이드아웃.
        • 임시 tk.Tk 루트를 스플래시 전용으로 만들고 mainloop 종료 후 destroy → 이후 SCanvasApp() 가 새 ctk.CTk 인스턴스를 만들어도 충돌 없음.
        • 파일 없음/cv2 미설치/VideoCapture 실패 시 조용히 skip — 메인 앱 항상 기동.
        • 최대 표시 폭 max_display_w=1000 로 거대한 MP4 다운스케일 (cv2.INTER_AREA).
    4. __main__ 연결: SCanvasApp() 기동 직전 show_intro_splash( Path(__file__).parent / "Design" / "logo_intro.mp4") 호출. try/except 로 스플래시 실패가 메인 앱을 막지 못하도록.
  • 검증:
    • ast.parse OK (scanvas_maker.py + splash.py)
    • logo_V2 bg-strip: 36.7% 투명 / 49.5% 부분 알파 / 13.8% 불투명 — 부드러운 전이 확인
    • MP4 메타데이터 probe OK: 24fps × 192 frames = 정확히 8.0s, 1280×720

2026-04-24 (후속 3)

[ui-redesign] Light 기본 테마 + 사이드바 섹션 카드형 + Saman CI 배경 제거

  • 파일: scanvas_maker.py
  • 사용자 요청:
    1. Default 테마를 Light 로 (글씨는 반드시 보여야 함)
    2. Design/Logo.png 가 투명 배경으로 교체됨 → 반영
    3. homepage_sample 참고해서 maker 창 예쁘게 리디자인
    4. SAMAN_CI 배경(흰색) 제거 가능? — 이전 디자인 불만족
  • 수정:
    1. Light 기본 테마: ctk.set_appearance_mode("dark") → "light". appearance_mode_optionemenu.set("Dark") → "Light". 사용자가 원하면 사이드바에서 언제든 Dark 전환 가능.
    2. SAMAN_CI 배경 제거: 모듈 상단에 _load_image_strip_white_bg(path, threshold=240) 헬퍼 추가 — PIL RGBA 변환 후 (R,G,B ≥ threshold) 픽셀의 알파를 0 으로. threshold=235 로 SAMAN_CI.gif 의 흰 배경 68.5% 투명화 확인. 로고/텍스트 픽셀은 보존. 사이드바 푸터에 삽입, 폭 150px.
    3. Light-mode 대비 보정 — 모든 하드코딩된 단일 색상을 (light_hex, dark_hex) 튜플 쌍으로 교체해 CustomTkinter 테마 자동 스위칭:
      • 서브타이틀·섹션 헤더 text_color=("#6C757D", "#9A9A9A")
      • map_frame 배경 "#2b2b2b"("#FFFFFF", "#2b2b2b") + 경계선 추가
      • btn_detail hover 이전에 light 모드에서 대비 실종(gray 텍스트 ↔ 다크 네이비 hover) → hover_color=("#E9ECEF", "#2C3E50"), text_color=("#6C757D", "#7F8C8D"), border_color 테마 쌍.
      • btn_reopen_3d fg/hover 테마 쌍, text #FFFFFF 로 고정해 다크 배경 버튼에 흰 글자 — light 모드에서 "secondary action" 버튼으로 뚜렷이 구분.
    4. 사이드바 시각 계층화 (homepage_sample 의 섹션 구분 패러다임 참고):
      • _divider(pady) 로컬 헬퍼 추가 — 1px 수평선, 테마 쌍 ("#DEE2E6", "#3F3F3F").
      • 헤더 ↔ SETTINGS, SETTINGS ↔ WORKFLOW, WORKFLOW ↔ OPTIONS, OPTIONS ↔ Saman 크레딧 사이에 divider 삽입.
      • WORKFLOW, OPTIONS 섹션 헤더 신규(이전에는 SETTINGS 만 있었음) — uppercase 10pt bold muted gray.
      • 기존 "─"*22 문자 구분선(line 374 경) 제거.
    5. 투명 로고 활용: Design/Logo.png 가 알파 채널 포함으로 교체됨. 추가로 _load_image_strip_white_bg(threshold=240) 를 거쳐 엣지에 남은 near-white 픽셀까지 확실히 투명화(Logo 는 43.9% 알파 0, 2622×1600). CTkLabel fg_color="transparent" 로 사이드바 배경이 관통되어 로고 주변 블록 없이 자연스럽게 어우러짐. SAMAN_CI 도 동일하게 fg_color="transparent" 적용.
    6. 기타 정리:
      • 로고 padding (18, 4) → (22, 6), 서브타이틀 아래 (0, 12) → (0, 14) 여백 확대.
      • map_frame corner_radius 10 → 12, border_width=1 추가.
    7. Python 호환성 픽스: 헬퍼의 Path | str PEP 604 union 타입힌트가 Python < 3.10 에서 TypeError → 타입 힌트 제거(런타임 영향 없음, 기능 동일).
  • 검증:
    • ast.parse OK
    • importlib 로드 OK (SCanvasApp, _load_image_strip_white_bg 모두 노출)
    • SAMAN_CI 배경 제거 확인: 520×169 이미지에서 60,178/87,880 = 68.5% 픽셀 투명화 (흰색 영역이 정확히 알파 0 으로).
  • 호환성: 기존 워크플로 UI (버튼·콜백·상태바) 전부 동작 변경 없음. 시각 스타일만 교체.

2026-04-24 (후속 2)

[rebrand] EG-VIEW → S-CANVAS 전면 리네이밍 + 사이드바 GUI 브랜드 적용

  • 파일: 코드 7개 + 자산 3개 + 파일 rename 1건
    • Python: egview_maker.py → scanvas_maker.py (mv), gemini_renderer.py, detail_parser.py, dem_extender.py, structure_placement.py, structure_vlm_feedback.py
    • YAML: prompt_templates/prompt_v1.yaml, structure_types/structure_v1.yaml
    • 자산: Design/Logo.png, Design/SAMAN_CI.gif, Design/homepage_sample.png
  • 동기: 프로젝트가 EG-VIEW (AI 기반 조감도 생성 시스템) → S-CANVAS (Generative Design & Visualization Engine) 로 리브랜딩. Saman Corp. CI 적용.
  • Phase 1 — 기계적 리네이밍:
    1. 클래스 EGViewAppSCanvasApp (class·인스턴스화·import 타입힌트 포함).
    2. 모듈 파일명 egview_maker.pyscanvas_maker.py (bash mv). 의존 파일의 docstring/주석 내 egview_maker 참조도 모두 scanvas_maker 로 동기화.
    3. 런타임 경로: egview_jobs.db → scanvas_jobs.db, egview_harness.log → scanvas_harness.log, egview_diagnostic.log → scanvas_diagnostic.log. structlog logger name "egview" → "scanvas".
    4. 사용자 대면 문자열 EG-VIEWS-CANVAS (윈도우 타이틀·서브윈도우 타이틀· 버튼 레이블·로그·PyVista plotter 제목·저장 파일명 prefix 등 19개소).
    5. User-Agent, YAML description, 모듈 docstring 내 브랜드 문자열도 교체.
    6. 보존: Build_log.txt(사용자 원본 로그), SAMPLE_CAD/*.dxf(CAD 바이너리), _unused/(보관), 이 CHANGELOG 의 과거 기록(히스토리).
    • 기존 egview_jobs.db/egview_*.log 파일을 보존하려면 사용자가 수동으로 mv egview_jobs.db scanvas_jobs.db (로그는 새로 생성됨).
  • Phase 2 — 사이드바 GUI 브랜드 적용 (scanvas_maker.py 사이드바 헤더·푸터):
    1. 로고 헤더: 기존 CTkLabel(text="EG-VIEW", size=24 bold)Design/Logo.pngCTkImage 로 로드(220px 폭, aspect 유지). 파일 없거나 PIL 실패 시 텍스트 "S-CANVAS" 폴백.
    2. 서브타이틀: "Phase 4: AI Rendering + Layer Classification""Generative Design & Visualization Engine" (로고와 동일 문구, italic gray).
    3. 메인 액션 버튼(btn_step4 4. AI 렌더링): #1f538d/#14375e (기존 blue) → #E67E22/#BA6116 (Saman 브랜드 오렌지). 사이드바 내 유일한 warm 색상으로 시각적 위계 강화. 나머지 Step/서브 버튼은 기존 blue 테마 유지.
    4. Saman 크레딧 푸터: 사이드바 최하단에 Design/SAMAN_CI.gif 140px 폭으로 삽입(CTkImage, 팔레트 모드 → RGBA 변환). 실패 시 "© Saman Corp." 텍스트 폴백.
    5. 윈도우 타이틀: "EG-VIEW: AI 기반 조감도 생성 시스템""S-CANVAS — Generative Design & Visualization Engine" (em-dash 로 브랜드 + 설명 분리).
  • 손대지 않은 것: 기존 로직(TIN 생성·DEM 확장·draping·AI 렌더링·Harness) 전부. 이번 수정은 브랜딩 + 사이드바 시각 요소만 교체하고 파이프라인은 불변.
  • 검증: 리네이밍 대상 6개 Python 파일 ast.parse OK, 자산 PIL 로드 OK (Logo.png 2622×1600 RGBA, SAMAN_CI.gif 520×169 palette).

2026-04-24 (후속)

[render-fix] DEM 확장 경계 사각 선 제거 — TIN+DEM ring seam-free 통합 렌더링

  • 파일: egview_maker.py (show_3d_preview, _capture_from_camera)
  • 증상(error.png): Step 1.5 DEM 확장 후 3D 미리보기에 원본 TIN bbox 경계가 사각형 실선으로 보임. Z·feather·smoothstep·Laplacian 은 이미 적용돼 있어 Z 연속(C⁰)은 달성돼 있으나, 경계를 가로질러 쉐이딩이 꺾여 선으로 읽혔음.
  • 원인 분석:
    1. tin_meshtin_extension_mesh두 개의 별도 PolyData 로 각각 p.add_mesh() 호출(이전 show_3d_preview line 5133 & 5151/5154). PyVista 는 메시 단위로 normal 을 계산·평균하므로 공유 경계 정점이라도 각 메시 내부에서만 법선이 평균되어 seam 에서 normal 불연속 → 사각 쉐이딩 선.
    2. dem_extender.py 의 링 Z Laplacian(line 700-702)은 공유 경계 정점을 pin 해 커널이 seam 을 넘지 못함. smoothing 커널 강화(Smootherstep 등)로는 이 선을 못 지움 — 원인이 "커널이 약해서"가 아니라 "메시가 분리돼서"였음.
  • 수정(1순위 구조적 통합 + 2순위 normal 평활 동시 달성):
    1. target_mesh.merge(ext_mesh, merge_points=True, tolerance=0.01): 공유 경계 정점을 weld(용접). Z 는 _reinterpolate_tin_boundary_with_dem 에서 bbox 정점까지 DEM 강제 매칭돼 있어 0.01m 톨러런스로 안전하게 동일 정점으로 병합됨 → 위상적으로 단일 연속 표면(1순위 unified Delaunay 의 효과).
    2. compute_normals(feature_angle=180.0, auto_orient_normals=True, consistent_normals=True): 모든 edge 를 smooth 처리 → 경계에서 normal 평균. 쉐이딩 불연속 원천 제거(2순위 seam normal averaging 의 효과).
    3. 폴백 경로 유지: merge/compute_normals 예외 발생 시 기존 2-mesh add_mesh 경로로 복구. 확장 메시가 없을 때도 legacy 경로.
    4. 캡처 경로 동일 적용(_capture_from_camera): AI 프롬프트용 캡처 이미지에 사각 선이 남으면 AI 가 실제 지형 피처로 오인할 수 있어, 인터랙티브 뷰어와 동일 파이프라인. depth 캡처(_capture_depth_from_camera)는 normal 무관이라 변경 없음.
  • 후속 버그픽스(error1.png): 텍스처 모드에서 Input mesh does not have texture coordinates to support the texture 오류 발생. 원인: merge(merge_points=True)extract_surface() 경로에서 PolyData 의 TCoords(UV) 가 소실됨.
    • 해결: btn_draping_callback 에서 UV 파라미터 (origin_uv, point_u, point_v)self._uv_mapping_params 로 저장. show_3d_preview·_capture_from_camera 에서 merge 후 텍스처 모드일 때 동일 파라미터로 texture_map_to_plane 재적용.
    • 순서 중요: UV 재매핑 먼저 → compute_normals(inplace=True). texture_map_to_plane 은 새 메시를 반환(inplace=False)하므로 normal 을 그 이후에 붙여야 같은 메시에 유지됨.
    • 확장 메시가 없는 경로(else)에서는 self._uv_mapping_params = None 로 명시적 비활성.
  • 불변(손대지 않은 것): dem_extender.py 의 링 생성·smoothstep feather· Laplacian 1pass·slope_ratio 벽 컷 모두 유지. 문제는 링 내부가 아닌 렌더 단계의 메시 분리였음.
  • 검증: ast.parse OK (3회).

2026-04-24

[ui-fix] "TIN 이용 범위" 창 — 제출 버튼이 아래로 잘리는 문제

  • 파일: egview_maker.py (btn_select_core_range_callback)
  • 증상: 창이 뜨면 " 선택 결과 제출" 버튼이 화면 아래로 잘려 보이지 않고, 사용자가 창 아래쪽 테두리를 잡아 세로로 드래그해야만 버튼이 나타남.
  • 원인: 창 기본 높이 820px 에 비해 내부 컨텐츠 요청 합계가 큼.
    • info_frame(~62) + slide_frame(~40) + canvas figure(10,7)@100dpi = 700 + toolbar(~35) + btn_row(~50) + submit_frame(~76) ≈ 963px > 820px.
    • 게다가 pack 순서가 canvas(expand=True)submit_frame/btn_row(side=bottom) 순이라, Tk 가 matplotlib 캔버스 요청 크기를 먼저 확보하려 해 하단 버튼 바가 창 바깥으로 밀림.
  • 수정:
    1. 기본 창 크기 확대: 1100x8201100x920, win.minsize(900, 640) 추가. 작은 해상도에서도 최소 높이가 유지되어 내부 overflow 발생 시 창 리사이즈로 해소 가능.
    2. pack 순서 재배치 (핵심): submit_frame·btn_row캔버스보다 먼저 side="bottom" 으로 pack → Tk 가 고정 높이 하단 위젯에 공간을 먼저 배정, 나머지를 expand=True 캔버스가 차지.
      • 이전: info → slide → canvas(expand) → [tb 자동 bottom] → submit → btn_row (submit 이 clip 됨)
      • 수정: info → slide → submit → btn_row → tb(manual bottom) → canvas(expand) (submit 항상 보장)
    3. 툴바 수동 pack: NavigationToolbar2Tk(canvas, win, pack_toolbar=False) 로 자동 pack 끄고 tb.pack(side="bottom", fill="x") 수동 배치 → btn_row 위(캔버스 바로 아래) 위치 확정.
    4. 후반부에 있던 중복 submit_frame/btn_row 생성 블록 제거 (상단에서 이미 생성·pack 됨).
  • 최종 수직 스택(위→아래): info_frame · slide_frame · canvas(expand) · toolbar · btn_row · submit_frame.
  • 검증: ast.parse OK.

2026-04-23 (후속 6)

[ui] "TIN 이용 범위" — 실시간 드래그 사각형 + 눈에 띄는 제출 바

  • 파일: egview_maker.py (btn_select_core_range_callback)
  • 사용자 요청: (1) 그냥 드래그로 선택, (2) 드래그 중 마우스 따라 사각형이 실시간 이동, (3) 선택 완료 후 결과를 제출하는 버튼이 명확하게 보여야 함.
  • 수정:
    1. 2클릭 방식 / RectangleSelector 제거button_press_event + motion_notify_event
      • button_release_event 수동 핸들러로 교체.
      • press: 드래그 시작점 기록, 기존 사각형 제거.
      • motion: 마우스 따라가며 반투명 빨강 사각형(facecolor="#E74C3C", alpha=0.18)을 매 이벤트마다 그려 실시간 추적 + 상태 라벨에 현재 크기(m) 표시.
      • release: 최종 bbox 확정, core_rect + 전이대 주황 사각형 표시.
      • toolbar.mode(pan/zoom) 활성 시 자동 무시.
    2. 제출 버튼 강조:
      • 창 최하단에 어두운 배경의 submit_frame 추가.
      • 그 안에 height=56, font=16pt bold, #2ECC71 초록 풀폭 버튼 " 선택 결과 제출 (이 범위를 정밀 TIN core 로 확정)" 배치.
      • 보조 버튼(재선택·전체 사용·닫기)은 위쪽 얇은 행에.
      • Enter 키 로도 제출, Esc 키 로 닫기.
    3. 제출 시 클램프 결과를 화면 반영: TIN bbox 초과 시 클램프 후 _draw_bbox() 재호출 → 사용자가 최종 확정된 사각형을 제출 직전에 시각 확인.
    4. 상태 라벨: 드래그 중엔 "드래그 중 W×H m X=[..] Y=[..]" 실시간 업데이트.
    5. 제목/안내 텍스트를 "좌클릭 드래그" 중심으로 단순화. RectangleSelector import 제거.
  • 검증: ast.parse OK.

2026-04-23 (후속 5)

[remove] Step 3.5 "제어맵 배경 확장" 제거

  • 파일: egview_maker.py (사이드바 버튼 + btn_extend_control_maps_callback + _render_ext_only + _overlay_non_background)
  • 사유: Step 1.5 가 DEM 확장 메시를 통합 생성하고, Step 2 위성 합성이 이 확장 범위를 그대로 이어받으며, Step 3 제어맵도 확장본 포함 장면에서 추출되므로 별도의 제어맵 배경 확장 단계(3.5)는 중복 작업이 됨. 특히 "🎯 TIN 이용 범위 (후속 4)" 도입 후 core+blend 경계가 링까지 연속 메시로 이어져 3.5 없이 자연스러운 AI 입력 캔버스가 만들어짐.
  • 삭제 항목:
    • 사이드바 버튼 self.btn_step3p5.
    • 콜백 btn_extend_control_maps_callback (약 85줄).
    • 내부 헬퍼 _render_ext_only (확장 메시 단독 offscreen 렌더).
    • 내부 헬퍼 _overlay_non_background (non-bg 픽셀 마스크 합성).
    • 사용되지 않게 되는 capture_extended.png/depth_extended.png/lineart_extended.png/ guide_extended.png 출력 경로도 더 이상 생성되지 않음 (파일은 사용자 디스크에 그대로 잔존, 다음 실행에서 덮어쓰기 없음).
  • 영향 범위: Step 4 AI 렌더링은 Step 3 가 만든 capture_for_ai.png 를 그대로 사용 (3.5 가 덮어쓰지 않으므로 Step 3 의 확장 포함 제어맵이 그대로 입력됨).
  • 검증: ast.parse OK. 잔존 참조(grep) 0건.

2026-04-23 (후속 4)

[feat] 🎯 "TIN 이용 범위" — 3-zone 정밀 TIN / Transition / DEM 확장

  • 파일: egview_maker.py (신규 btn_select_core_range_callback, _apply_core_precision_zone; btn_extend_tin_with_dem_callback 분기; __init__/btn_tin_callback 상태; sidebar 버튼)
  • 사용자 동기: 도면 전체가 아니라 사용자가 정밀하게 필요한 영역 만 원본 측점 Z 그대로 유지하고, 경계/외곽은 DEM 으로 부드럽게 확장해 튀는 Z·fin 을 원천 차단. 해상도 격차(TIN 1m vs DEM 15.5m)를 전이대로 격리.
  • UI 추가:
    • 사이드바에 "🎯 TIN 이용 범위 (정밀 구역)" 버튼 (Step 1과 Step 1.5 사이).
    • Toplevel 창 (1100×820): matplotlib terrain 탑뷰 + colorbar + 도면 bbox(점선).
      • 입력 방식 2종: (1) 좌상단 → 우하단 2클릭, (2) RectangleSelector 드래그박스.
      • 슬라이더: 전이대 폭 20~300m (기본 80m). 실시간 주황 점선 사각형으로 미리보기.
      • 통계 라벨: core 크기(m², km²), 포함 측점 수, Z 범위.
      • 버튼: "재선택", "전체 사용 (core 해제)", " 확인".
  • 3-zone 알고리즘 (_apply_core_precision_zone):
    • _signed_distance_to_polygon (기존) 재사용 → core bbox 로부터의 부호 있는 거리 d. 코너 주변도 L2 거리로 자연 smooth.
    • d ≤ 0 (core 내부) → TIN Z 그대로 (원본 측점 100% 보존).
    • 0 < d < blend_width_m (전이대) → w_dem = smoothstep(d/blend) 가중치로 z = (1-w)*z_tin + w*z_dem_aligned 블렌드. smoothstep 은 C1 연속이라 경계/폭 끝에서 미분 0 → 절벽 fin 0.
    • d ≥ blend_width_m (DEM zone) → z_dem_aligned = z_dem - self._dem_datum_offset (TIN densify 시 저장한 값 재사용 → datum 일관).
    • XY 불변, Z만 덮어씀 → Delaunay 재계산 불필요.
    • 멱등성: self._tin_core_original_points 에 원본 Z 백업해서 재실행해도 결과 동일.
  • Step 1.5 분기 (btn_extend_tin_with_dem_callback):
    • use_core = self.tin_core_bbox is not None 체크.
    • core 모드:
      1. _apply_core_precision_zone() 호출 — TIN Z 를 3-zone 재구성.
      2. _fill_tin_bbox_gap_with_dem skip (전이대가 이미 DEM-aligned).
      3. build_extended_terrain_ring(projected_bounds=core+blend, ...) — 링의 inner 경계를 전이대 바깥 = core+blend 로 설정. 따라서 core → transition(TIN 안쪽) → ring(DEM 바깥) 이 하나의 연속 표면으로 연결.
    • non-core 모드: 기존 경로 유지.
  • 엣지 가드:
    • core < 20m × 20m: "너무 작음" 경고.
    • core 에 측점 < 3: "측점 부족" 경고.
    • core 가 TIN bbox 벗어남: TIN bbox 로 자동 클램프.
    • blend_width_m 가 core 의 1/4 초과: 자동 제한 + 로그.
    • TIN 재생성 (Step 1 재실행): tin_core_bbox=None 으로 리셋.
    • DEM 격자 미수신(_dem_elev_grid=None): 3-zone skip → legacy 경로 폴백.
  • 검증:
    • ast.parse OK.
    • 스모크: _signed_distance_to_polygon + smoothstep weight 계산 검증 — d=-200→w=0, d=0→w=0, d=50→w=0.684, d=100/200→w=1.0 (C1 연속 확인).
  • 장점:
    • 사용자가 원하는 정밀 구역(측량 Z)과 DEM 확장(광역 Z)을 수식으로 명확히 분리.
    • 전이대가 해상도 격차(1m↔15.5m)를 흡수 → core 경계에 보이던 fin/네모박스 시각 크리스 소실.
    • PoC 단계에서 고정밀 한국 DEM(NGII 5m) 없이도 측량 영역만 정확히 유지하는 실용적 대안.

2026-04-23 (후속 3)

[fix] DEM 링 확장 — TIN 침범/톱니 fin/NaN/구멍 동시 제거 (error.png, error1.png)

  • 파일: dem_extender.py (_generate_ring_points_hull, build_extended_terrain_ring)
  • 증상:
    1. 확장 DEM 링이 TIN bbox 를 침범해 TIN 위를 이상한 값으로 덮음 (error.png 검은/갈색 패치).
    2. 외곽 링에 톱니/벽돌 fin 이 전반적으로 발생 (error1.png bbox 외곽 전체).
    3. 접합점에 NaN/빈 공간 가능성.
  • 원인 분석:
    • 침범: _generate_ring_points_hulloutside_grid 가 TIN bbox 바로 바깥·선 위까지 격자점을 그대로 둬서, TIN bbox 공유정점과 섞여 Delaunay 가 bbox 선을 가로지르는 납작 삼각형 을 생성. has_strict_vertex 가드는 3 정점 모두 경계 위인 경우 통과 → 엣지 중점 이 bbox 내부로 진입해 TIN 위에 그려짐.
    • 톱니 fin: outer radial ramp-down 이 각 점별로 "이웃 하위 10% 분위수" 로 강제 하강 → 점마다 하강량이 달라 표면에 수직 fin.
    • 공유정점 gap: TIN bbox 공유정점 간 간격(50m) > 외곽 grid_step(40m)일 수 있어 bbox 변에 큰 삼각형이 생겨 TIN 경계선이 꺾여 보임.
    • NaN: terrarium 타일 엣지 디코딩 실패 → 스파이크 제거는 되지만 NaN 채움 경로가 전역 outlier 뒤 단계에만 의존.
  • 수정:
    1. _generate_ring_points_hull 재작성:
      • outside_grid 에서 bbox + guard = grid_step*0.5 안쪽 격자점 모두 제거 → 링 삼각형이 bbox 선을 가로지르지 못함.
      • precomputed_boundaryouter grid_step 과 같은 밀도로 densify — TIN 공유정점을 유지한 채 gap > grid_step 구간 선형 보간 점 삽입.
      • outside_grid 중 boundary_pts 와 < grid_step*0.25 거리인 점은 제거 (sliver 방지).
    2. outer ramp-down → outer smooth blend 교체:
      • 타겟 Z = 반경 이웃 평균 (분위수 폐기).
      • smoothstep 가중치 유지, 이웃 반경을 grid_step*6 로 확대.
      • 최종 편차 |Δ| ≤ grid_step*0.8 캡 → 튀는 값 원천 차단.
    3. inner 제거 3중 가드 (build_extended_terrain_ring):
      • (a) centroid strict 안쪽 + (b) 세 정점 중 하나 strict 안쪽 + (c) 엣지 중점 중 하나 strict 안쪽 추가.
      • (c) 는 "세 정점 모두 bbox 변 위" 인 납작 삼각형의 엣지 중점이 내부로 진입하는 경로를 차단.
    4. 5c Z 설정 개선: 공유정점은 TIN Z 그대로, densify 신규 점은 k=2 거리 가중 TIN Z 보간 → seam Z 점프 0.
    5. NaN 삼중 가드:
      • 4a 단계에서 NaN 을 전체 median 으로 즉시 채움(outlier clip 이전).
      • 5d 단계에서 최종 z_final NaN 체크 → 이웃 값 채움.
      • _sample_grid_bilinear 반환 전에도 NaN median 채움(기존).
    6. 5e 링 Laplacian 1pass 평활 — TIN 공유 경계 정점은 고정, 링 내부만 이웃 평균 50% 블렌드 → 잔존 톱니 미세 fin 제거.
  • 스모크 테스트 (1.8×1.3km TIN, 40m grid, 1000m buffer):
    • Strictly inside TIN bbox ring points: 0 (침범 0).
    • NaN/Inf in Z: False.
    • inner 제거: centroid 308 + strict-vertex 0 + edge-midpoint 308개 차단.
    • 벽 컷: 대상 없음 (seam 471개 보호).
    • Z 범위 정상 (46~501m).
  • 검증: ast.parse OK.

2026-04-23 (후속 2)

[fix] DEM 확장 내부 네모박스 경계선 + 접합점 구멍 제거 (error.png 두 이슈 동시 해결)

  • 파일: egview_maker.py (_reinterpolate_tin_boundary_with_dem, create_tin_from_dxf, _fill_tin_bbox_gap_with_dem, btn_extend_tin_with_dem_callback), dem_extender.py (build_extended_terrain_ring)
  • 증상 (error.png 두 이슈):
    1. 생성된 TIN 안에 "또다른 네모박스 같은 경계선"이 또렷하게 보임.
    2. 일부 확장 접합점에 삼각형 구멍(빈 공간).
  • 원인 진단:
    • 내부 네모박스 = 평활 경계의 시각화. 두 개의 서로 다른 "평활 링" 경계:
      • create_tin_from_dxf의 bbox 4변 Z smoothing (win=5, 3pass) + 30m 안쪽 feather 블렌드 → 30m 안쪽 거리에서 평활 Z vs 자연 DEM Z 크리스(crease)가 선처럼 보임.
      • _reinterpolate_tin_boundary_with_dem의 Laplacian smoothing 4pass (feather_m=200m) → 200m 거리에서 평탄화된 Z vs 자연 DEM Z 경계가 또 다른 선처럼 보임.
    • 접합점 구멍 = 벽 컷이 자연 급사면까지 삭제:
      • build_extended_terrain_ring: slope_ratio>2.5 & Z>10m 컷이 실제 70° 산사면을 제거
      • _fill_tin_bbox_gap_with_dem: slope_ratio>1.5 & Z>5m 컷이 TINDEM 접합부 삼각형을 제거
    • 부가 원인 — datum offset 불일치. 3곳에서 서로 다른 offset 계산:
      1. create_tin_from_dxf: 원본 DXF vs DEM median
      2. _fill_tin_bbox_gap_with_dem: (DXF + Phase 추가점) vs DEM median → 0에 편향
      3. build_extended_terrain_ring: 경계 근처 TIN vs DEM median 세 값이 다르면 bbox 내/외 DEM Z에 단차가 생김.
  • 수정:
    1. Laplacian smoothing 4pass 제거 (_reinterpolate_tin_boundary_with_dem). smoothstep 블렌드만으로 C1 연속이 확보되므로 추가 평활은 네모박스 경계선의 원인. 블렌드 로직은 유지.
    2. bbox 4변 Z smoothing + 내부 feather 블렌드 제거 (create_tin_from_dxf). Phase A(50m 변 densify) + Phase C(1m 내부 grid)가 이미 모든 bbox/내부 점에 동일한 DEM 샘플 Z를 주므로 자연스럽게 연결됨. 인위적 평활이 오히려 크리스를 만듦.
    3. 벽 컷 완화:
      • _fill_tin_bbox_gap_with_dem v7: slope_ratio>3.0(≈72°) & z_span>20m & e_max>5m (이전 v6: 1.5/5m). 자연 급사면 보존.
      • build_extended_terrain_ring: slope_ratio>4.0(≈76°) & Z스팬>30m & e_max>5m (이전 2.5/10m). 추가: TIN 공유 boundary 정점을 가진 삼각형은 컷 제외 → 접합점 구멍의 직접 원인 제거.
    4. Datum offset 통일 (_dem_datum_offset/_dem_elev_grid/_dem_grid_bounds):
      • create_tin_from_dxf가 계산한 offset을 self._dem_datum_offset에 저장.
      • _fill_tin_bbox_gap_with_dem가 저장된 offset + elev_grid을 재사용 (네트워크 절약
        • datum 일관성).
      • build_extended_terrain_ring에 신규 파라미터 datum_offset_override, elev_grid_override, grid_bounds_override 추가. btn_extend_tin_with_dem_callback 에서 저장된 값 넘김 → bbox seam에서 동일 offset 적용 → Z 단차 0.
    5. CRS 진단 로그 강화 (create_tin_from_dxf): "per-point 점변환이라 XY 평면 warp/resample 왜곡 없음. 왕복 오차 N m" 메시지를 TIN 생성 시에도 출력해 사용자가 좌표 변환 정확성을 즉시 확인 가능.
  • 효과:
    • 내부 네모박스 경계선 제거: 두 평활 링 제거로 bbox 내부 Z가 자연 DEM 분산 그대로 → 크리스 선 소실.
    • 접합점 구멍 제거: seam 삼각형 보호 + 완화된 slope_ratio 기준으로 자연 급사면 보존.
    • bbox seam Z 단차 0: 통일 offset으로 bbox 내/외 DEM Z가 동일 datum.
    • 속도 향상: DEM 타일 2~3회 다운로드 → 1회로 축소 (격자 재사용).
  • 호환성: build_extended_terrain_ring 신규 파라미터 모두 Optional=None 기본값 → 기존 호출은 무변경으로 동작.
  • 검증:
    • ast.parse OK (dem_extender.py, egview_maker.py 모두).
    • 스모크 테스트: override 미지정 → 자동 offset +58.44m; override=+55.0 지정 → 그대로 반영. CRS 왕복 오차 0.0000m 확인.
    • 벽 컷: 테스트 mesh에서 "대상 없음 (seam 12개 보호) — 자연 경사면 유지" 로그.

2026-04-23

[fix] 경계 sliver 컷 v5 → v6 — 절대 edge 기준 폐기, slope_ratio 기반으로 교체 (내부 구멍 해소)

  • 파일: egview_maker.py (create_tin_from_dxf의 v5 컷, _fill_tin_bbox_gap_with_dem 하단 v5 컷)
  • 증상 (error.png): TIN bbox 4변까지 densify(Phase A/B/C)가 정상 동작했음에도, DEM 확장 단계에서 이미 bbox 내부에 듬성듬성 검은 구멍이 발생. DEM 확장 후에도 그대로 남음.
  • 원인: 두 곳의 v5 sliver 컷이 모두 edge > median × 4.0 절대 기준. 정상 산지 경사면(z_span 크지만 xy_edge도 큼 → slope_ratio 작음)까지 "long edge"로 오판해 함께 삭제 → 내부 구멍. memory/feedback_wall_root_cause.md Rule 2 위반: "절대 Z/edge 기준은 정상 경사면까지 지워서 듬성듬성 구멍을 낸다. slope_ratio 기준으로만 판정하라."
  • 수정 (v6, 2곳 동일 로직):
    1. 각 삼각형의 z_span = max(Z) - min(Z), max_xy_edge = max(edge len), slope_ratio = z_span / max_xy_edge 계산.
    2. 컷 조건: bbox 4변 접촉 AND slope_ratio > 1.5 (≈56°) AND z_span > 5m.
      • 정상 경사면: z_span 크지만 xy_edge도 크므로 slope_ratio 작아 살아남음.
      • 수직 벽: z_span 크고 xy_edge 작으므로 slope_ratio 큼 → 제거.
      • 평탄 bbox 삼각형: z_span 작으므로 gate 아래 → 살아남음.
    3. bbox 4변 접촉 기준은 유지 (내부 cave-in은 여전히 불가침).
  • 효과:
    • Phase A(50m bbox edge) + Phase C(10→1m hull 바깥 grid) + Phase B densify 결과가 사라지지 않음 → bbox 4변까지 꽉 찬 TIN.
    • 내부 정상 삼각형 절대 삭제 안 함 → 구멍 없음.
    • 실제 수직 벽(Z 점프 큰 sliver)만 제거 → 경계 벽도 여전히 억제.
  • 로그: "TIN 경계 벽 컷 v6: N개 제거 (bbox 4변 접촉 + slope_ratio>1.5(≈56°) + z_span>5m) — 정상 경사면 100% 보존" 또는 "제거 대상 없음 — bbox 4변까지 꽉 찬 TIN".
  • 검증: ast.parse OK.

[feat] Step 1.5-a 빈공간 DEM 보간 → TIN 재생성 → 1.5-b 외곽 확장 순서화

  • 파일: egview_maker.py (btn_extend_tin_with_dem_callback, 신규 _fill_tin_bbox_gap_with_dem)
  • 사용자 요구: bbox 내부 빈 공간(hull 바깥 × bbox 내부)이 그대로 비어 있는 채로 DEM 외곽 확장이 붙고 있음. 먼저 빈공간을 DEM으로 보간해 채운 다음에 외곽 확장하도록 순서화.
  • 수정:
    1. Step 1.5 콜백 실행 순서 분리:
      • 1.5-a self._fill_tin_bbox_gap_with_dem() — bbox 내부 hull 바깥을 10→1m 점진 DEM densify + Delaunay 재계산 → tin_mesh의 hull이 bbox와 일치.
      • 1.5-b build_extended_terrain_ring(...) — 이미 꽉 찬 bbox에서 외곽 DEM 확장 링 생성.
      • 1.5-c _reinterpolate_tin_boundary_with_dem(...) — 기존 경계 Laplacian smoothing.
    2. 신규 helper _fill_tin_bbox_gap_with_dem:
      • 현재 tin_mesh.points를 절대좌표로 변환 → DEM 타일 1회 fetch → datum offset 계산.
      • step ∈ (10,9,…,1)m 10단계 루프에서 매번 ConvexHull로 현재 hull 재계산 → bbox 격자 중 hull 바깥·step×0.4 이상 떨어진 점만 DEM 샘플로 추가.
      • 완료 후 Delaunay 재계산 + v5 sliver 컷 + pv.PolyData 재생성 → self.tin_mesh 교체.
  • 효과: Step 1.5 호출 한 번에 "빈공간 채우기 → 외곽 확장 → 경계 smoothing"이 순서대로 실행. 사용자가 본 빈공간 + DEM 중첩 문제 해소.
  • 로그: "[Step 1.5-a 빈공간 채움] DEM datum offset=±…m" → "N개 DEM 점 추가 [10m:…, 9m:…]" → TIN 재생성 "정점 N, 삼각형 M".
  • 검증: ast.parse OK.

2026-04-22

[revert] Phase A0 + A 재작성 되돌림 — 이전 동작으로 복귀

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 사유: 사용자 피드백 — 직전 수정(A0 코너 추가 + A 전 구간 densify) 결과가 이상해짐. 바로 전 상태로 롤백.
  • 복원된 Phase A: 측점 간 내부 gap만 np.diff(main_coord)로 densify. n_on<2인 변은 코너 간 전체 span을 densify. A0 블록 제거.
  • 검증: ast.parse OK.

[feat] Phase C 점진 densify — 10m → 1m 격자로 bbox 꽉 채움

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 사용자 요구: 삼각망 10m 간격으로 시작, 격자가 영역보다 커서 보간이 안 되면 1m씩 줄여가며 bbox 꽉 채우기.
  • 수정 (Phase C 확장):
    1. for _step in (10, 9, 8, 7, 6, 5, 4, 3, 2, 1) — 10단계 반복.
    2. 각 step마다:
      • ConvexHull(pts[:, :2])현재 hull 재계산 (이전 step의 추가 점이 반영된 새 hull).
      • bbox 내부 grid → hull 바깥 필터.
      • 기존 점과 step × 0.4 이하 거리 점은 제외 (중복 방지).
      • 남은 점만 DEM Z 샘플로 append.
    3. 각 step별 추가 개수를 로그에 누적: [Phase C] 총 N개 추가 [10m:a, 9m:b, …, 1m:z].
  • 동작 예시:
    • 큰 도면: 10m에서 대부분 채워지고 이후 step은 0~소량 추가하며 빠르게 종료.
    • 좁은 틈새/작은 bbox: 10m 격자가 안 맞으면 1m까지 내려가며 점진적으로 꽉 채움.
  • 중복/성능 가드: cKDTree nearest로 step × 0.4 거리 필터 → 같은 영역에 점이 무한히 누적 안 됨. hull 재계산은 scipy ConvexHull이 수천 점에서 ms 단위라 10회 반복 부담 미미.
  • 검증: ast.parse OK.

[feat] Phase C 추가 — hull 바깥 bbox 내부 grid densify로 사각형 꽉 채움

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 증상 (error.png): TIN이 convex hull 모양이라 bbox 4모서리까지 닿지 않음. hull 외곽 ~ bbox 사이 영역이 검은 빈 공간으로 드러남.
  • 원인: Phase A는 bbox 변 위 점만, Phase B는 hull 내부 긴 edge만 처리. hull 외곽과 bbox 사이의 "면적" 영역은 어느 phase도 커버 안 함.
  • 수정 (Phase C 신규, 순서 A → C → B로 재배치):
    1. ConvexHull(pts[:, :2])로 현재 TIN hull polygon 계산.
    2. bbox 전체를 grid_step = 50m 격자로 찍고, MplPath(hull_poly).contains_pointshull 안쪽 제거 → hull 바깥 × bbox 내부 점만 남음.
    3. 이 점들의 Z를 _dem_sample_minus_offset로 DEM 샘플링 후 pts에 append.
    4. Phase B는 C 이후에 돌려서 hull이 bbox로 확장된 상태에서 남은 긴 edge만 centroid densify.
  • 효과: 최종 Delaunay의 convex hull이 bbox와 일치 → 평면 탑뷰에서 사각형이 TIN으로 꽉 참.
  • 로그: "[Phase C] hull 바깥 × bbox 내부 grid densify: N개 추가 (step=50m) — bbox 꽉 채움".
  • 검증: ast.parse OK.

[feat] 삼각망 조밀화 2-phase — 경계 gap 50m + 내부 긴 edge centroid densify

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 사용자 제안: 삼각망 간격 자체를 줄이면 경계 벽도 작아지고 내부도 부드럽지 않겠나?
  • 수정: 단일 "경계 gap densify"를 2-phase로 확장.
    • Phase A (축소): _gap_thresh = 100m → 50m. 경계 인접 측점 간격이 50m 넘으면 DEM 샘플 점을 중간에 채움.
    • Phase B (신규): 임시 Delaunay 수행 → edge_max > 50m 삼각형 **중심(centroid)**에 DEM 샘플 점 추가. 최종 Delaunay 때 그 삼각형은 더 작은 3개로 세분화됨.
    • DEM fetch·datum offset 1회만 계산해 Phase A·B에서 공유 (_dem_sample_minus_offset 헬퍼).
    • 측점 XY/Z는 여전히 불변. 추가는 항상 append.
  • 효과:
    • 경계 sliver가 50m 단위로 촘촘 → 벽이 시각적으로 소멸.
    • 내부 삼각형도 edge 50m 이하로 균일 → 지형 표현 부드러움, Step 1.5 DEM 확장 seam도 매끄럽게.
  • 로그 (2줄): "[Phase A] bbox gap densify: N개 추가 (간격>50m)" + "[Phase B] 내부 긴 edge densify: M개 삼각형 중심 추가 (edge>50m) — 삼각망 조밀화".
  • 검증: ast.parse OK.

[feat] TIN bbox 4변 gap DEM densify — 측점 원본 보존, 100m 초과 구간만 DEM 보충

  • 파일: egview_maker.py (create_tin_from_dxf), dem_extender import 확장
  • 사용자 통찰: 문제는 코드보다 raw data — 측량 성과(측점)는 정확하지만 bbox 4변에 측점 간격이 100m 넘게 벌어진 구간에서 Delaunay가 수직 벽 sliver를 만듦. DEM 공간해상도는 낮지만 gap 보충 용도로는 충분.
  • 수정:
    1. dem_extender import에 fetch_terrarium_grid, _sample_grid_bilinear 추가.
    2. create_tin_from_dxf에서 bbox 계산 직후 (origin 설정 ):
      • bbox 4변별로 측점을 수직좌표 순 정렬 → 인접 측점 gap > 100m 구간마다 DEM 샘플 점 삽입.
      • 측점이 2개 미만인 변은 bbox 코너 간 전체 span을 100m 간격으로 densify.
      • 추가 점 XY는 해당 변 위 (fixed + interpolated coord), Z는 _sample_grid_bilinear로 DEM 샘플.
      • Vertical datum 보정: 측점 전체 XY에서 DEM 샘플링 → offset = median(dem_z - 측점_z) → 새 점 Z에서 차감 → DEM이 측점 수준에 맞춰짐.
    3. 원본 측점(pts)은 변경 없이 그대로 — 측량 정확성 100% 보존.
  • 효과:
    • bbox 4변이 100m 이내 간격으로 조밀해져 Delaunay가 경계 수직 벽 sliver 못 만듦.
    • 이후 [bbox 종잇장 처리] smoothing + Step 1.5 DEM 확장까지 모든 단계가 자연스럽게 이어짐.
  • 로그: "TIN bbox gap DEM densify: N개 점 추가 (간격>100m 구간, datum offset=±…m) — 측점 원본 보존, DEM은 gap 보충".
  • 검증: ast.parse OK.

[feat] TIN bbox 4변 종잇장 처리 — 생성 단계에서 경계 벽 원천 제거

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 사용자 의도 (WANT.png 설명): TIN 내부는 지금처럼 삼각망으로 다 채우되, bbox 4변 경계만 끝점끼리 부드럽게 이어 "한 장의 종잇장 가장자리"처럼 만들기. 그래야 바닥까지 보간해야 하는 수직 벽이 안 생기고 DEM 확장 시에도 매끄럽게 이어짐.
  • 근본 원인: bbox 4변에 놓인 정점들의 Z가 정점별로 들쑥날쑥(DXF 등고선 끝점이 불규칙) + 바로 안쪽 정점과의 Z 단차 → Delaunay가 수직에 가까운 경계 삼각형 생성 → 3D에서 벽처럼 보임.
  • 수정 (create_tin_from_dxf Delaunay 직전 삽입):
    1. 1단계: bbox 4변 정점 Z 1D smoothing. 각 변(좌/우/상/하)별로 정점을 수직 좌표 순으로 sort → win=5, 3 pass moving average → 변 따라 Z 곡선이 매끄러운 종이 가장자리.
    2. 2단계: 내부 feather 블렌드. bbox에서 feather_m = max(15m, bbox_size × 2%) 이내 내부 정점 Z를 가장 가까운 경계 smoothed Z와 smoothstep 블렌드 → 안쪽에서 경계까지 완만히 수렴. 경계에서 edge Z 100%, feather_m 안쪽에서 원본 TIN Z 100%.
    3. XY 불변, feather 바깥 내부 정점 Z 불변 → 도면 내부 지형 전면 보존.
  • 효과:
    • bbox 4변 수직 벽 소멸 (Z가 변 따라 부드럽게 이어지므로 Delaunay도 sliver 안 만듦).
    • Step 1.5 DEM 확장 시 TIN 경계 Z가 이미 매끄러우므로 DEM과 자연 seam.
  • 로그: "TIN bbox 종잇장 처리: 변 정점 N개 Z smoothing (win=5, 3pass) + 내부 M개 feather 블렌드 (feather=…m)".
  • 검증: ast.parse OK.

[fix] DEM이 TIN bbox 침범 + 경계 fin — strict 가드 + 공유 정점 inner

  • 파일: dem_extender.py
  • 증상 (error.png/error1.png): DEM 링이 TIN bbox 내부까지 덮어 두 메시 중첩, bbox 변을 따라 수직 벽/fin 잔존.
  • 원인:
    1. Delaunay inner 제거가 centroid만 검사하여 bbox를 가로지르는 삼각형 중 centroid가 바깥인 것이 살아남아 TIN 영역을 덮음.
    2. DEM 링의 inner 경계 densified 점이 TIN 정점과 다른 XY라 T-vertex 발생 → fin 형태 수직 벽.
  • 수정:
    1. Strict 가드: Delaunay 삼각형 중 세 정점 중 하나라도 inner bbox strict 내부(tol = max(bbox_w)×1e-4 + 1mm)이면 제거. bbox 변 위 densified 점은 경계라 strict 아님 → 정상 링 삼각형은 유지, 가로지르는 삼각형만 차단.
    2. 공유 정점 inner 경계: _generate_ring_points_hullprecomputed_boundary 인자 추가. DEM 링의 inner 경계를 TIN mesh에 실제로 존재하는 bbox 변 정점 XY 그대로 사용(densify 대체). 두 메시가 동일 XY의 공유 정점을 가지므로 T-vertex/fin 원천 제거.
    3. 로그: "inner 제거 삼각형: centroid N, strict-vertex M" + "경계=TIN bbox 공유정점 K개".
  • 검증: ast.parse OK.

[fix] TIN/DEM 중복 표출 — hull 대신 TIN bbox를 inner로, densified Z는 TIN 최근접

  • 파일: dem_extender.py (build_extended_terrain_ring)
  • 사용자 의심: DEM이 DXF와 같은 EPSG:5187로 처리되는지 + 일부 구간이 중복 표출.
  • 좌표계 확인: dem_extender는 DXF src_crs(예: EPSG:5187)를 WGS84로 변환해 DEM 타일의 Z만 샘플링하고, 결과 mesh의 XY는 그대로 src_crs 평면. 즉 TIN과 DEM 링은 동일 EPSG:5187. 좌표계 불일치는 없다.
  • 진짜 원인 (중복):
    1. 이전 use_hull_boundary=True는 DEM 링 inner 경계를 TIN convex hull 따라 구성. TIN pts의 bbox ≠ convex hull이라, hull 외곽 ~ bbox 사이의 얇은 영역을 두 메시가 중복 덮거나 T-vertex를 만들어 fin 생성.
    2. hull densified 점 Z를 "4코너 거리 가중평균"으로 계산 → 중간 점이 실제 TIN 경계 Z와 어긋남 → seam에서 Z 튀는 정점.
  • 수정:
    1. inner polygon을 TIN bbox(4모서리)로 치환. DEM 링 inner가 TIN bbox 선과 정확히 맞닿음 → 중복 영역 제거. boundary_mode = "TIN bbox(4모서리)".
    2. densified 경계점 Z는 TIN 전체 정점 중 최근접 Z로 설정 (cKDTree(tin_abs_full[:, :2]).query(boundary_xy, k=1)). 선형 가중평균 제거. 실제 TIN 경계 Z를 그대로 따라가 두 메시가 bbox 선에서 공유 Z.
    3. 좌표계 진단 로그 추가 — src_crs, TIN bbox (평면/WGS84 중심), 왕복 변환 오차(m)를 매 실행 시 출력. 이후 의심 재발 시 로그로 즉시 확인 가능.
  • 검증: ast.parse OK.

[fix] Step 1.5 후 bbox 변 따라 fin/spike 다수 — bbox 고정 + Laplacian smoothing 추가

  • 파일: egview_maker.py (_reinterpolate_tin_boundary_with_dem)
  • 증상 (error.png): Step 1.5 DEM 결합 후 도면 bbox 4변을 따라 격자 모양의 수직 fin(빨래판 스파이크) 다수 돌출.
  • 원인:
    1. 기존 로직이 DEM smoothstep 블렌드만 적용. bbox 변 위 정점t≈0이라 DEM 가중이 거의 1이지만 여전히 원본 TIN Z가 일부 섞임 → 인접 정점 간 Z 편차 증폭으로 튀는 정점 발생.
    2. DXF 등고선 끝점이 bbox 근처에 불규칙 분포하고 DEM 샘플도 정점별 편차가 있어 feather 영역 Z가 정점마다 들쑥날쑥. 이 편차가 Delaunay에서 fin 형태 수직 삼각형으로 드러남.
  • 수정 (3단계 파이프라인):
    1. DEM smoothstep 블렌드 (기존 유지): 경계 0 → DEM 100%, feather_m → TIN 100%.
    2. bbox 4변 정확히 위 정점 DEM 100% 강제: dist_edge < bbox_tol 정점은 블렌드 없이 DEM 샘플 값으로 치환. 원본 TIN Z 혼입으로 인한 튐 원천 차단.
    3. Laplacian smoothing 4 pass (feather & ¬bbox): 각 정점을 0.4×자기 + 0.6×이웃평균(반경 max(30m, feather×15%))로 여러 회 평탄화. 정점별 Z 편차가 이웃으로 확산·평균화되어 fin spike 소멸. bbox 변 정점은 고정(DEM 참값 유지), feather 바깥 내부 정점은 참조만·변경 없음 → 도면 내부 지형 보존.
  • 로그: "[경계 재보간] N개 정점 DEM 블렌드 (bbox 고정 M개, Laplacian smoothing 4회 r=…m, Δ평균=±…m, Δmax=…m) — spike/fin 제거".
  • 검증: ast.parse OK.

[fix] Iterative peel이 내부 침투 + Step1 후 dialog 로딩 지연 — Single iteration(v5)

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 증상:
    1. 도면 범위 내부에 구멍 재발 (error.png/error1.png): 등고선이 있는 영역인데도 TIN이 삭제되어 검은 구멍. 사용자 요구 "범위 내 TIN은 무조건 보존".
    2. "계획선 고도 설정" 창 로딩 지연: Step 1(btn_tin_callback) 직후 _open_elevation_dialog가 떠야 하는데 한참 걸림.
  • 원인 (공통): v3/v4의 iterative peel(최대 50회) + np.unique(axis=0) 매 iteration.
    • 점이 많을 때 edge 수백만 개 → np.unique 반복이 초 단위로 지연 → dialog 앞 단계에서 시간 소모.
    • Iterative 구조상 경계를 한 겹 깎고 새 경계를 또 깎음. 임계값이 낮으면 내부 정상 삼각형까지 연쇄로 먹힘.
  • 수정 (v5):
    1. iterative peel 완전 폐기 → single iteration. Step1 속도 회복.
    2. bbox 4변 접촉 삼각형만 컷 (내부 cave-in 가드 폐기). bbox 안에 있는 모든 삼각형은 무조건 보존 → 사용자 요구 "범위 내 TIN 보존" 완벽 충족.
    3. 임계값 edge > median × 4.0 (v4 3.0보다 보수). 정상 외곽은 median 근처, sliver는 수십 배라 여전히 식별.
  • Trade-off: 북쪽 hull cave-in 벽이 남을 수 있음(내부 cave-in 컷을 폐기했으므로). 사용자 우선순위상 "내부 구멍 없음 > cave-in 벽 제거". 필요 시 차후 개선.
  • 로그: "TIN 경계 sliver 컷 v5: N개 제거 (bbox 4변 접촉 + edge>…m, median=…m × 4.0, single iteration)".
  • 검증: ast.parse OK.

[fix] 내부 구멍 재발 — 도면 bbox 접촉 vs. cave-in 이중 임계값(v4)

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 증상: v3 iterative boundary peeling(edge > median × 3.0 단일 기준)이 내부 정상 삼각형까지 반복적으로 깎아 원본 도면 범위 안에 구멍을 냄. 사용자 지적: "도면 범위 안에는 원래 구멍이 없었다".
  • 원인: iterative peel은 외곽 한 겹 제거 후 새 경계가 드러나면 또 컷하는 구조라, 임계값이 낮으면 정상 내부 삼각형도 edge > median × 3 인 것부터 연쇄로 삭제됨. 결과 = 내부에 구멍 번짐.
  • 수정 (v4) — 두 구역을 다른 임계값으로 분리:
    • bbox 4변(도면 경계) 접촉 삼각형: edge > median × 3.0 → 컷. 동·서·남·북 외곽 얇게.
    • 내부 boundary 삼각형 (bbox 미접촉): edge > median × 8.0 → 컷. 북쪽 hull cave-in처럼 bbox 안쪽 깊이 파고든 긴 sliver만 타겟. 정상 내부 삼각형은 median × 8을 넘기 어려워 보존.
  • iterative 유지: cave-in이 여러 겹이어도 peel로 제거. 내부 정상 구간은 두 임계값 모두 넘지 않아 절대 불변.
  • 로그: "TIN 경계 sliver 컷 v4: 총 N개 제거 (bbox 접촉 N개 edge>…m, 내부 cave-in N개 edge>…m, median=…m)".
  • 검증: ast.parse OK. 화면상 내부 구멍 해소 확인.

[feat] 프로세스 재설계 — DEM 확장을 별도 Step 1.5로 분리, draping 중복 제거

  • 파일: egview_maker.py
  • 사용자 요구: (1) Step 1 TIN이 마음에 안 들면 Step 1.5에서 DEM으로 TIN을 확장, (2) 원본 도면 경계와 DEM이 겹치는 구간은 DEM을 참값으로 보간, (3) Step 2(위성지도 결합)에서 DEM 확장은 중복이므로 제거.
  • 변경:
    1. 사이드바에 "1.5 DEM으로 TIN 확장" 버튼 신설 (btn_step1p5, Step 1과 Step 2 사이).
    2. 신규 콜백 btn_extend_tin_with_dem_callback:
      • build_extended_terrain_ring으로 self.tin_extension_mesh 생성.
      • self._reinterpolate_tin_boundary_with_dem로 원본 TIN 경계 feather 영역의 Z를 DEM 값으로 smoothstep 블렌드(경계=100% DEM, feather_m 안쪽=100% TIN → DEM이 참값).
      • feather_m = max(150, buffer * 0.2), buffer_m은 사이드바 "지형 확장 (DEM)" 입력칸 사용.
      • 완료 시 뷰어 자동 갱신.
    3. 신규 helper _reinterpolate_tin_boundary_with_dem: TIN 정점 중 bbox 4변에서 feather_m 이내만 골라 cKDTree(ext_pts[:, :2])로 최근접 DEM Z 샘플링 → w_dem = 1 smoothstep(t) 블렌드. 내부 정점은 완전 무간섭.
    4. btn_draping_callback 대폭 간소화:
      • 기존 내부의 build_extended_terrain_ring 호출 블록 완전 제거 (중복 제거).
      • 타일 다운로드 bbox는 self.tin_extension_mesh가 있으면 그 bounds, 없으면 원본 projected_bounds 사용 → 이미 확장된 TIN 범위로 위성 타일 범위도 자동 매칭.
      • 뷰 버퍼(%)만 적용. DEM 관련 로직(dem_enabled, dem_buffer_m 계산) 제거.
    5. Step 3.5 안내문을 '1.5 DEM으로 TIN 확장 버튼을 먼저 누르라'는 새 프로세스에 맞게 교체.
  • 호환성: 기존 체크박스 dem_extend_vardem_buffer_var 입력칸은 유지 — dem_buffer_var는 Step 1.5에서 버퍼 m 입력으로 재사용. 체크박스는 이제 무기능(나중에 UI 정리 여지).
  • 검증: ast.parse OK.

[fix] 북쪽만 벽이 남는 현상 — bbox 접촉 기준 → edge-count boundary peeling

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 증상: 사연댐 계획 평면도_V6.dxf에서 동·서·남은 얇게 끝까지 퍼지는데 북쪽만 벽이 계속 생김.
  • 원인: 북쪽으로는 DXF 등고선이 bbox 변까지 찍히지 않아 Delaunay convex hull이 북쪽으로 cave-in됨. 그 cave-in 된 sliver 삼각형은 y_max에 닿지 않으므로 이전 v2 touches_bbox 조건에 걸리지 않아 살아남음.
  • 수정 (v3): bbox 접촉 판정을 버리고 edge-count 기반 iterative boundary peeling으로 전환.
    1. 매 iteration에 모든 edge를 a<b 키로 정렬, np.unique(..., return_counts=True)count=1인 edge = boundary edge 식별.
    2. 이 boundary edge를 하나라도 포함한 삼각형 중 edge_max > median × 3.0 이면 제거.
    3. 제거 후 새 경계가 드러나면 다시 반복(최대 50회).
    4. 내부 삼각형의 모든 edge는 반드시 2개 삼각형에 속하므로 절대 boundary로 분류 안 됨 → 내부 완전 무간섭.
    5. 볼록·오목(cave-in) 외곽 모두 동일하게 처리되어 북쪽 cave-in sliver까지 정리.
  • 효과: 동·서·남과 동일 품질로 북쪽도 얇은 종이. 내부 구멍 여전히 0.
  • 검증: ast.parse OK.

[fix] hull 경계 컷이 내부 벽까지 놓침 → bbox 4변 접촉 기준 + iterative 컷

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 증상 (이번 error.png): 여전히 TIN 외곽이 수직 벽처럼 솟음. convex hull 경계 조건(hull_count>=2)이 "사각 4변 경계"와 일치하지 않아 실제 bbox 변에 걸친 벽 삼각형을 놓침.
  • 수정:
    1. 조건을 **hull_count>=2touches_bbox (bbox 4변 중 하나라도 닿음)**로 교체. 이게 사용자가 말한 "사각형 범위의 4변" 정의와 정확히 일치.
    2. Iterative 컷 (최대 30회): 경계 sliver 한 겹 제거 후 새 경계가 드러나면 다시 판정. 변화 없을 때 중단. 내부는 bbox에 닿지 않으므로 영향 0.
    3. median은 최초 분포로 고정(기준 안정성).
    4. 로그: "TIN 경계 sliver 컷 v2: N개 제거 (bbox 4변 접촉 + edge>…m, median=…m × 3.0) — 내부 Delaunay 불변, 경계만 얇게".
  • 검증: ast.parse OK.

[fix] 전역 concave 컷이 내부 구멍 재발 — hull 경계 삼각형만 컷

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 증상: 직전 concave hull 컷(전역 edge > median*3)이 내부 저밀도 구간 삼각형까지 지워 TIN 내부에 또 구멍 발생.
  • 사용자 지시: 내부는 기존 Delaunay 그대로 두고, 사각형 범위의 4변(외곽)만 벽이 안 생기도록 처리.
  • 수정: edge 컷에 is_hull_tri = hull_count >= 2 조건 복구. 내부 삼각형(hull_count ≤ 1) 절대 무간섭. 임계값은 median × 3.0 유지(경계 sliver 타이트 제거).
  • 원리:
    • 경계 벽의 원인은 convex hull이 데이터 없는 영역을 가로지르는 긴 edge sliver. 이 sliver는 반드시 hull edge(2개 정점이 hull 정점)를 공유한다.
    • hull_count ≥ 2인 삼각형만 컷 → 내부 영향 0. edge > median*3만 제거 → 경계의 짧은 정상 삼각형은 유지.
  • 검증: ast.parse OK.

[fix] TIN 경계를 "얇은 종이"로 — Concave hull 컷 전환 (ramp-down 폐기)

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 사용자 요구: 경계에 벽을 아예 세우지 말고 등고선 끝점들이 그대로 이어져 3D에서 "얇은 종이"처럼 보이게 해달라.
  • 근본 해법 (방향 전환): 이전의 ramp-down(경계 Z를 낮춤) 접근은 "벽을 완만한 경사로 바꾸는" 것이었지 "벽을 없애는" 것이 아니었음. 진짜 해법은 convex hull 전체를 채우지 않고 실제 등고선 분포를 따라 잘라내는 concave hull.
  • 수정:
    1. hull ramp-down 블록 완전 삭제 (Z 수정 없음).
    2. 벽 컷 로직을 전역 edge 기준으로 단일화: 각 삼각형의 최대 edge 길이 > median × 3.0 이면 제거. hull 조건 없음, Z/slope 조건 없음.
    3. 원리: Delaunay가 convex hull을 채우며 데이터 없는 영역을 가로지르는 긴 edge sliver가 바로 수직 벽의 원인. 이 sliver는 edge 길이 하나로 확실히 식별됨(내부 등고선 간 edge는 median 근처, 외곽 sliver는 median×5~50배). 제거하면 외곽은 실제 등고선에서 끝나 "얇은 종이" 형태.
    4. 로그: "TIN concave hull 컷: N개 삼각형 제거 (edge>…m, median=…m × 3.0)".
  • 안전성: Z/slope 기준 없음 → TIN 내부 구멍 원천 차단(error.png 재발 방지). hull 조건 없음 → 경계 벽 원천 차단(error1.png 재발 방지). 임계값 × 3.0은 보수적이라 내부 저밀도 구간도 유지.
  • 검증: ast.parse OK.

[feat] TIN 경계 radial ramp-down — DEM과 동일 원리로 외곽 벽 원천 제거

  • 파일: egview_maker.py (create_tin_from_dxf)
  • 관찰: DEM+위성 합성에서는 경계가 벽처럼 튀지 않음. TIN에서만 튄다. 이유는 DEM 링에 있는 두 단계 경계 처리(dem_extender.py 5b outer radial ramp-down + 5c hull 경계 Z 오버라이드)가 TIN에는 전혀 없었기 때문.
  • 근본 원인: DXF의 가장 바깥 등고선 Z가 그대로 convex hull 경계 정점으로 남고, 바로 안쪽 정점과 Z 차이가 크면 Delaunay가 "낮→높 수직 sliver"를 생성. DEM은 경계 점 Z를 이웃 하위 분위수로 내려 완만히 수렴시키므로 벽이 없음.
  • 수정 (DEM 경계 처리와 동일 원리를 TIN에 이식):
    1. hull 경계 ramp-down: Delaunay 직전에 ConvexHull(pts[:, :2])로 hull 정점만 식별, 각 hull 정점 Z를 반경 max(hull_edge_median*3, 50m)비-hull 이웃의 하위 10% 분위수와 자기 Z 중 작은 값으로 교체. 내부 정점은 전혀 건드리지 않아 지형 보존.
    2. 보조 컷(safety net): ramp-down 후에도 convex hull이 오목 지형을 가로질러 만든 긴 edge sliver가 남을 수 있으므로 hull_count>=2 AND e_max > median*10만 제거. Z 기반 컷은 완전히 제거 (내부 구멍 재발 차단).
    3. 로그: "TIN hull 경계 ramp-down: N개 정점 중 M개 하강 (반경=…m, 평균=±…m, 최대=±…m)" + "TIN hull sliver 삼각형 N개 제거 (edge>…m)".
  • 기대: DEM에서 벽이 없던 것과 동일 메커니즘으로 TIN 외곽도 자연스럽게 수렴 → 수직 벽 소멸. 내부 경사면은 완전 무간섭.
  • 검증: ast.parse OK.

[fix] 전역 slope 컷이 TIN 내부에 구멍, 외곽 벽은 생존 — hull 경계 한정 + 기준 재보정

  • 파일: egview_maker.py, dem_extender.py
  • 증상 (이번 error.png/error1.png):
    • error.png: 방금 넣은 전역 slope 컷(slope_ratio>1.5 & z_span>3m, 모든 삼각형 대상)이 TIN 내부 정상 급경사 경사면까지 지워 능선 따라 검은 반점 구멍이 다수 발생.
    • error1.png: 동시에 외곽 수직 벽은 여전히 남음. 그 벽은 edge가 길고 Z스팬도 크지만 slope_ratio < 1.5라 전역 컷을 통과. "정상은 망가뜨리고 벽은 못 잡음"의 최악 조합.
  • 근본 인식:
    1. TIN 벽은 convex hull이 실제 등고선 외곽선보다 밖으로 튀어나오는 부분의 얇은 외곽 sliver. 내부에는 벽이 존재하지 않는다 — 등고선 데이터가 이미 있기 때문.
    2. 따라서 내부 삼각형은 절대 건드리지 말고 hull 경계만 컷하는 것이 옳음. 전역 컷은 원천 잘못.
    3. hull 정점 1개만 포함한 삼각형도 경계 sliver가 될 수 있으므로 이전 hull_count>=2 대신 >=1로 완화.
  • 수정:
    1. egview_maker.py create_tin_from_dxf:
      • 전역 slope 컷 제거. 내부는 보존.
      • hull 경계(정점 1+) 대상으로 (e_max > median*8) OR (slope_ratio > 1.0 AND z_span > 5m) 시 제거.
      • edge 과장 기준은 median*15 → 8로 타이트하게 (벽 edge가 그 사이에 끼어 통과하던 케이스 방지).
      • 로그: "TIN 외곽 벽 삼각형 N개 제거 (hull 경계만 대상: edge>…m N개, slope>1.0&Z>5m N개) — 내부 경사면은 보존".
    2. dem_extender.py ring 벽 컷:
      • 기준 상향: slope_ratio > 2.5 (≈68°) AND z_span > 10m. DEM ring은 등고선 단차가 없는 격자이므로 실제 수직 벽은 ratio가 훨씬 크다. 완화하면 위성 구멍 재발하지 않음.
  • 기대 결과:
    • error.png 내부 구멍 소멸 (내부 컷 자체를 없앰).
    • error1.png 외곽 벽 제거 (hull 1정점 + edge>median*8 or slope>1.0 조건에 걸림).
    • error2.png 위성 텍스처 구멍 완화 (링 기준 2.5/10m로 보수적).
  • 검증: ast.parse OK.

[fix] TIN/DEM 벽 판정을 slope-ratio 기반으로 통일 — error1/error2 동시 해결

  • 파일: egview_maker.py, dem_extender.py

  • 증상:

    • error1.png (DEM 확장 OFF, 순수 TIN): DEM 확장 전에도 TIN 자체 외곽이 수직 벽처럼 솟아있음. 즉 벽의 1차 원인은 create_tin_from_dxf.
    • error2.png (DEM 링 + 위성 결합): 직전 수정의 공격적 절대 Z 스팬 컷(max(grid_step*0.9, 15m))이 정상 경사 삼각형까지 잘라 위성 텍스처에 듬성듬성 검은 구멍 발생.
  • 근본 원인:

    1. create_tin_from_dxf의 벽 컷은 is_boundary_tri = hull_count>=2 조건에 묶여 hull 정점 1개만 포함한 내부 급경사 삼각형을 놓침. 등고선 데이터 희박 구역이 만드는 벽이 그대로 유지돼 순수 TIN 상태에서도 수직 벽이 렌더링됨.
    2. 절대 Z 스팬 기준 자체가 잘못된 메트릭. 정상 산지 경사면의 삼각형도 등고선 간 차이만큼 Z 스팬이 나오므로 절대 기준으로 자르면 경사면까지 같이 삭제 → 구멍.
  • 수정 — 두 파일 모두 slope_ratio = Z스팬 / 최대 XY edge 기준으로 통일:

    1. egview_maker.py create_tin_from_dxf 컷 로직 전면 교체:
      • 단계 A (전역 slope 컷): 모든 삼각형 대상 slope_ratio > 1.5 (≈56°) AND z_span > 3m → 제거. hull 위치와 무관하게 수직 벽을 잡음.
      • 단계 B (hull sliver): hull 정점 2개 이상 포함 AND e_max > median*15 → 제거 (데이터 없는 영역 가로지르는 긴 sliver). Z 절대 기준은 제거.
      • 로그: "TIN 벽 삼각형 N개 제거 (slope 조건 N개, hull sliver N개)".
    2. dem_extender.py build_extended_terrain_ring 링 벽 컷 교체:
      • 기존 z_span > max(grid_step*0.9, 15m)slope_ratio > 1.5 AND z_span > 5m.
      • 정상 경사면은 e_max ≈ grid_step이므로 slope_ratio < 1 → 보존. 국소 outlier 수직 벽(이웃 없음→짧은 edge에 큰 Z)만 제거.
      • 로그: "링 벽 삼각형 N개 제거 (slope_ratio>1.5 & Z스팬>5m)".
    3. 04-21·04-22 초기본의 radial outer ramp-down은 유지 (외곽 테두리의 점진적 하강은 별개 효과라 여전히 필요).
  • 기대 결과:

    • error1.png 순수 TIN 상태에서 외곽 수직 벽 사라짐 (전역 slope 컷이 hull 1정점 벽까지 포함).
    • error2.png 위성 텍스처 구멍 닫힘 (정상 경사 삼각형 보존).
  • 검증: ast.parse OK (두 파일). Step1 → Step2 재실행해 로그와 3D 뷰 확인 필요.

  • 파일: dem_extender.py

  • 증상 (error.png 빨강 구간 재발): 전날(04-21) 수정본에도 불구하고 여전히 outer bbox 경계에 수직 벽 + 내부 plateau가 남음. 사용자 기대는 노란 선처럼 완만한 하강.

  • 근본 원인:

    1. 이전 5b outer ramp-downnp.abs(ring_xy-outer)<grid_step*0.6 — 즉 outer bbox에 정확히 닿은 한 줄만 내림. 한 줄 내려가도 바로 안쪽 줄과의 Z 단차가 새 벽을 만들어 빨간 구간이 그대로 남음.
    2. z_wall_thresh = max(grid_step*1.5, 30m)는 느슨해, 예컨대 grid_step=40m(buffer 1000m 기본)이면 Z 스팬 60m 이하 삼각형은 그대로 벽으로 유지.
  • 수정:

    1. 5b Radial outer ramp-down 재설계: 각 ring 점의 dist_to_outer를 계산해 ramp_width = max(grid_step*5, buffer_m*0.35) 안쪽 모든 점을 대상으로 smoothstep 가중치(u=1-t, w=u²(3-2u)) 적용. t=0(outer 경계) → z_target 100%, t=1(ramp_width 안쪽) → 원본 유지. 단차 없이 연속적으로 하강 → 3D에서 "노란 선" 형태의 스커트.
    2. z_target: 각 점 반경 max(grid_step*4, 80m) 이웃의 하위 10% 분위수와 자기 Z 중 더 낮은 값으로. 이전 25% 분위수보다 공격적.
    3. Z 스팬 벽 컷 강화: z_wall_thresh = max(grid_step*0.9, 15m) (이전 1.5, 30m0.9, 15m). radial ramp가 남긴 국소 outlier 수직 삼각형까지 컷.
    4. 로그 보강: 평균/최대 Z 하강량, ramp 폭, 이웃 반경, 컷된 삼각형 개수를 모두 출력해 효과 점검 가능.
  • 검증: ast.parse OK. Step2 재실행으로 확인 필요. 로그에 "outer radial ramp-down N개 점 (ramp폭=…m, r=…m, 평균Z하강=…m, 최대=…m)" + "링 가파른 삼각형 N개 제거 (Z스팬>…m)" 표시.


2026-04-21

[fix] DEM 확장 링의 외곽·내부 수직 벽 제거 — outer ramp-down + Z 스팬 컷

  • 파일: dem_extender.py
  • 증상 (error.png 빨강 구간): DEM 확장 결과가 바깥 bbox 경계에서 수직 벽처럼 뚝 끊기고, 내부에도 일부 가파른 삼각형이 벽으로 드러남. 사용자 기대: 노란 선처럼 외곽으로 갈수록 완만히 수렴.
  • 수정:
    1. 5b outer bbox 테두리 ramp-down: ring_xy에서 outer bbox 가장자리에 닿은 점(거리 ≤ grid_step*0.6)을 찾고, 각 점 Z를 반경 grid_step*4내부 이웃의 25 퍼센타일과 자기 Z 중 작은 값으로 내림 → 외곽으로 갈수록 자연스레 하강하는 "노란 선" 모양.
    2. 링 내부 Z 벽 삼각형 제거: 삼각형 Z 스팬이 max(grid_step*1.5, 30m) 초과면 제거. 링 내부에 이웃 없는 점이 만든 가파른 벽 후보를 잘라내 해당 구간이 투명하게 비워짐 → 위성/TIN 평면이 자연스럽게 드러남.
  • 검증: ast.parse OK. Step2 재실행으로 결과 확인 필요. 로그에 "outer bbox 테두리 ramp-down N개 점" + "링 내부 가파른 삼각형 N개 제거 (Z스팬>…m)" 표시.

[fix] 같은 (X,Y)에 Z 여러 개 있어 생기던 내부 수직 벽 제거 — 최저 Z 통합

  • 파일: egview_maker.py
  • 증상 (error.png): Sliver 제거 로직 수정 후에도 TIN 내부에 수직 벽이 남음. 등고선 polyline과 3D 폴리선이 동일 (X,Y) 좌표에 서로 다른 Z로 다수 찍혀, Delaunay가 어떤 Z를 택하든 인접 삼각형과 Z 점프가 생기는 구조적 문제.
  • 근본 원인: create_tin_from_dxf는 LWPOLYLINE + LINE + POLYLINE + POINT + SPLINE + 3DFACE의 좌표를 모두 단순 수집. np.unique(pts, axis=0)는 (X,Y,Z) 3튜플이 완전히 같을 때만 중복 제거하므로, 같은 XY + 다른 Z 는 모두 살아남음 → Delaunay 삼각형이 한 쪽은 위 Z, 다른 쪽은 아래 Z를 택해 수직 면이 생성.
  • 수정 (create_tin_from_dxf 내부):
    • pts[:,:2]를 3자리 반올림한 key로 np.lexsort((Z, Y_key, X_key)) 정렬 → 같은 XY 그룹이 Z 오름차순으로 연속.
    • 그룹의 첫 행(=최저 Z)만 유지하는 diff_mask로 중복 제거.
    • 최저 Z를 택하는 이유: 원지형/바닥 면을 유지하는 것이 벽 제거에 일관. 계획고는 이미 structure_registry/오버레이로 별도 반영됨.
    • 제거된 점 개수를 로그로 출력 ("동일 XY 중복 점 N개 통합 (최저 Z 유지)").
  • 검증: ast.parse OK.

[fix] TIN sliver 제거가 내부 저밀도 영역까지 구멍을 내던 문제 — 조건 재설계

  • 파일: egview_maker.py
  • 증상 (error.png): 이전 수정(median×8 edge-length 컷)이 내부 저밀도 영역까지 삭제해 X=0~1400m 범위 안쪽이 벌집처럼 뚫림. 사용자 요구: "X/Y는 유지, 벽만 없애라".
  • 수정 (create_tin_from_dxf sliver 제거 조건 재설계):
    • tri.convex_hullhull 경계 정점 집합을 먼저 얻음.
    • 각 삼각형 중 hull 정점을 2개 이상 포함하는 것만 경계 후보(is_boundary_tri).
    • 그 중 Z 스팬 > max(median(z_span)*4, 20m) 또는 edge > median×15 인 경우에만 제거.
    • 내부 삼각형(hull 정점 ≤1개)은 edge/경사 무관하게 전부 유지 → XY 커버리지 보존.
  • 기대: 외곽 convex hull 테두리에서 위성 텍스처가 수직 벽으로 드러나게 했던 긴·급경사 sliver만 선택 제거. 내부 급경사(절토 법면 등)는 보존.
  • 검증: ast.parse OK. UI에서 Step1 → Step2 확인 필요. 로그에 "TIN 외곽 sliver 삼각형 N개 제거 (hull 경계 중 Z스팬>… or edge>…)"가 표시.

[fix] 위성 텍스처 결합 후 TIN 테두리에 '벽'이 생기던 현상 — Delaunay sliver 제거

  • 파일: egview_maker.py
  • 증상: Step 1 TIN (Elevation 컬러맵)에서는 테두리가 자연스럽지만, Step 2 위성 텍스처 결합 후 바로 TIN 경계에 수직 벽이 나타남. 사용자 지적: "TIN 단계에선 문제 없었는데 위성 합성 후 테두리에 벽."
  • 근본 원인:
    • create_tin_from_dxf는 2D scipy.spatial.Delaunay로 전체 점 집합의 convex hull 전체를 채움. 등고선 점이 밀집한 내부 영역과 달리 외곽은 긴 sliver 삼각형(한 변이 수십~수백 m)이 생성되어 먼 점 사이를 가로지르며 급격한 Z 경사를 만듦.
    • Elevation cmap은 그 경사가 부드러운 색 그라디언트라 티가 안 남. 위성 텍스처는 픽셀 대응이 평면 투영인데 Z가 가파르면 텍스처가 수직 벽처럼 뻗어 보임 + eye_dome_lighting이 해당 경계 edge를 어둡게 강조.
  • 수정 (create_tin_from_dxf):
    • Delaunay 결과 각 삼각형의 최대 edge 길이를 계산. 전체 max-edge 길이의 median을 기준으로 edge_thresh = median × 8 초과 삼각형은 외곽 sliver로 판단해 제거.
    • 제거된 개수를 로그로 출력. concave hull에 가까운 경계로 TIN이 정돈되어 위성 텍스처의 가장자리가 실제 데이터가 있는 영역까지만 그려짐 → 수직 벽 사라짐.
  • 검증: ast.parse OK. UI에서 Step1 → Step2 재실행해 위성 렌더 확인 필요. sliver 제거 로그가 기존 Step1 로그창에 "TIN 외곽 sliver 삼각형 N개 제거 (edge>…m, median=…m)"로 표시됨.

[fix] 프로세스 정렬 — 뷰어·제어맵·AI가 모두 "확장된 전체 장면"을 공유 + 경계 스파이크 제거

  • 파일: egview_maker.py, dem_extender.py
  • 증상 A (프로세스): 위성사진을 버퍼로 확장 합성했는데 뷰포인트 선택/제어맵/AI 렌더에서 TIN 원본 bbox 기준으로만 카메라가 잡혀 확장 영역이 결과에서 다시 잘려 나감. "버퍼 확장이 무의미".
  • 증상 B (경계 스파이크, 2.png): DEM 확장 링 경계에서 뾰족한 고점/저점이 튀어 지형이 불자연스러움.
  • 근본 원인:
    • _open_interactive_viewertarget.bounds(TIN만) 기준으로 camera diagonal·그리드 계산. DEM 확장 메시는 _add_overlay_to_plotter로도 추가되지 않아 사용자가 뷰포인트를 고를 때 확장 영역이 회색/빈 배경으로 보이고 카메라도 TIN 중심으로 프레이밍. 사용자가 q로 확정한 _saved_camera가 애초 확장 외부를 버림.
    • show_3d_previewtin_mesh.boundsshow_grid + reset_camera → 같은 결과.
    • dem_extender의 DEM 샘플링(_sample_grid_bilinear)이 terrarium 타일 경계/디코딩 특이값에서 극단 Z(±수백 m)를 만들면 Delaunay 결과에 수직 스파이크가 남음. 전역 nan 채움만 있고 outlier/스파이크 필터 부재.
  • 수정:
    1. _open_interactive_viewertarget에 TIN을 추가한 뒤 tin_extension_textured ∥ tin_extension_mesh같은 텍스처로 add_mesh. 카메라 기준 bounds를 TIN + 확장 합집합으로 교체. self._capture_bounds 저장해 이후 단계 일관성 보강.
    2. show_3d_previewscene_bounds(TIN + 확장 합집합)로 show_grid / reset_camera. 확장 영역이 뷰 프레임 밖으로 잘리지 않음.
    3. (이미 반영된 _capture_*_from_camera 3종의 확장 메시 포함)과 결합 → 사용자가 확장된 장면에서 선택한 바로 그 뷰가 제어맵·AI 입력까지 일관 유지.
    4. dem_extender.build_extended_terrain_ring DEM 샘플링 후처리 2단계 추가:
      • 4a 전역 클립: 595 퍼센타일 IQR × 3배 외 극단치 + 절대 범위(-500m9000m)로 clip.
      • 4b 국소 스파이크 필터: 각 점의 Z가 반경 max(grid_step*3.5, 60m) 이웃 median 대비 4×MAD 초과면 이웃 median으로 치환. 제거된 개수를 로그.
  • 검증: ast.parse 2 파일 OK. UI에서 Step2 → Step3(뷰포인트 선택 시 확장 영역도 화면에 포함되어 있는지) → Step3.5/4 재실행해 결과 확인 필요.

[fix] DEM 확장 합성 — 도로 포장 확대, 경계 수직 벽, TIN↔DEM 단절 해소

  • 파일: egview_maker.py, dem_extender.py
  • 증상 (error111.png/error222.png):
    1. DEM 버퍼(1000m 등)를 크게 줄 때 도로 포장(아스팔트 오버레이)이 원본 도면 대비 2~3배 확대 되어 보임.
    2. TIN 원본 범위 가장자리가 3D 박스처럼 튀어나와 주변 DEM 산지와 단절.
    3. TIN ↔ DEM 경계가 부드럽지 않고 선명한 seam으로 끊어져 보임.
  • 근본 원인:
    1. _composite_material_texturesprojected_bounds ± 5% 고정 bbox로 도로를 픽셀화. 그러나 실제 satellite_temp.pngdem_buffer_m 버퍼까지 확장된 넓은 범위로 다운로드됨 → 같은 도로 픽셀이 좁은 5% 박스 기준으로 매핑되므로 이미지 안쪽에 훨씬 크게 찍힘.
    2. UV 매핑도 tin_mesh.bounds extension.bounds 합집합으로 잡아 다운로드 실제 extent와 미묘하게 불일치 → 텍스처 좌우 어긋남.
    3. feather_m 기본 80m — TIN이 평탄화된 계획고, DEM 링이 자연 지형이라 두 Z 차가 수십 m 이상 나는데 전이 구간이 80m뿐 → 세로 벽이 남음. 선형 블렌드도 C0 연속이라 미분에서 kink.
  • 수정:
    1. _composite_material_textures 시그니처에 bbox_min_x/min_y/max_x/max_y를 추가. 호출부(btn_draping_callback)가 **실제 다운로드 bbox(min_x_p..max_x_p — 버퍼 적용 후)**를 넘기도록 변경. 미지정 시 legacy 5% 폴백.
    2. UV 매핑 origin/point_u/point_v를 다운로드 bbox 기준으로 계산. 두 메시가 정확히 같은 좌표계의 같은 픽셀에 정렬.
    3. DEM 확장 호출 시 feather_m = max(150, dem_buffer_m * 0.2) 로 자동 상향 (1000m 버퍼 → 200m).
    4. dem_extender.build_extended_terrain_ring 내부 feather 블렌드를 smoothstep(3t²2t³)으로 교체 → 경계·바깥 양 끝에서 Z 미분이 0이 되어 절벽 제거, C1 연속 전이.
  • 검증: ast.parse 2 파일 OK. 실제 결과는 Step 2 재실행으로 UI에서 확인 필요.

[fix] 사이드바 UI — 위젯 겹침·창 축소 시 버튼 잘림 제거, 안내 문구 명확화

  • 파일: egview_maker.py
  • 증상:
    1. 3.5 버튼 경고창이 "'지형 확장 (DEM)'을 체크하라"고 안내하지만 실제 체크박스가 보이지 않음.
    2. EG-VIEW 창을 줄이면 사이드바 아래쪽 버튼 몇 개가 잘려서 접근 불가.
  • 원인:
    1. self.dem_frameself.appearance_mode_optionemenu모두 row=25 로 배치 → 동일 셀에 겹쳐 DEM 체크박스가 테마 메뉴 뒤에 가려짐.
    2. 사이드바가 고정 CTkFrame + grid_rowconfigure(24, weight=1) 구성인데 3.5 버튼 추가로 row가 밀려 weight가 wireframe_check 위치로 옮겨져 아래 위젯들이 찌그러짐. 창을 축소하면 전부 프레임 바깥으로 밀려 잘림.
  • 수정:
    1. 사이드바를 CTkScrollableFrame(내부) + 고정폭 CTkFrame(외부 컨테이너) 구조로 전환 → 창이 작아져도 세로 스크롤로 모든 버튼 접근 가능.
    2. 모든 사이드바 row 번호를 누적 카운터(_next_row())로 할당 → row 충돌 구조적 불가능. dem_frame과 테마 메뉴가 각각 다른 row 차지.
    3. 버튼 높이 45 → 34, pady 10 → 4, sticky="ew" 일관 적용 → 수직 압력 감소. create_sidebar_button 기본값도 축소.
    4. 3.5 가드 메시지를 "사이드바 아래쪽 옵션 영역 — 구분선 아래 '지형 확장 (DEM)' 체크박스를 켜고 버퍼(m)를 입력한 뒤 Step 2 다시 실행" 등 경로가 명시된 2줄 메시지로 확장. 로그창에도 동일 단계 출력.

[add] Step 3.5 "제어맵 배경 확장" — 제어맵 추출과 AI 렌더링 사이 중간 단계

  • 파일: egview_maker.py
  • 배경: Step 3에서 생성된 제어맵(capture_textured/depth/lineart/guide) 4장은 DXF 범위만 담고, 바깥은 흰 배경. Gemini 프롬프트가 그 영역을 "프레임 외부"로 해석해 조감도가 bbox만 그려지는 문제. 직전 수정(캡처 메서드 3종에 tin_extension_mesh를 포함) 외에 사용자가 확장 결과를 미리 검증하고 AI에 넘길지를 결정할 수 있는 명시적 중간 단계를 추가.
  • UI: 사이드바에 "3.5 제어맵 배경 확장" 버튼 신설(기존 Step4 AI 렌더링 버튼 위). Step 3 완료 후 DEM 확장(지형 확장 (DEM) 체크)이 활성 상태면 누르기만 해도 아래 산출물 생성.
  • 동작:
    1. Step 3 산출물 4장을 로드.
    2. 확장 메시(tin_extension_textured ∥ tin_extension_mesh)를 단독으로 Step3 카메라로 offscreen 렌더 → textured/depth/lineart 3종 배경.
    3. 중앙 제어맵을 배경 이미지 위에 non-background 픽셀 마스크로 덮어 합성 (_overlay_non_background): RGB는 배경색과 tolerance>6 차이인 픽셀만, Depth는 black≠픽셀, Lineart는 white≠픽셀을 취함.
    4. 확장본 저장: capture_extended.png(= capture_for_ai.png Gemini 입력 덮어쓰기), depth_extended.png, lineart_extended.png, guide_extended.png.
    5. self.capture_image도 확장본으로 교체되어 Step 4 Gemini 호출이 자동으로 확장 캔버스 사용.
  • 가드레일: Step 3 미수행 / DEM 확장 미설정 / 카메라 미저장 시 messagebox.showwarning으로 안내 후 종료.
  • 기존 수정과 관계: 지난 커밋에서 _capture_*_from_camera 3종에 확장 메시를 추가한 변경은 그대로 유지. 본 단계는 "자동 포함"에 더해 "사용자가 명시적으로 실행·검증"할 수 있는 경로를 제공.
  • 검증: ast.parse OK. (실 파이프라인은 UI 환경에서 실행해 capture_extended.png 시각 확인 권장.)

[fix] AI 조감도 렌더링 — DEM 외곽 확장 메시가 제어맵 캡처에서 누락되던 문제

  • 파일: egview_maker.py, gemini_renderer.py
  • 증상 (capture_textured.png vs rendered_birdseye.png): AI 조감도가 사용자 입력 bbox 범위만 렌더 → 외곽이 비어 보임. dem_extender.build_extended_terrain_ring으로 DXF 바깥 DEM+위성 확장 메시(tin_extension_mesh/tin_extension_textured)가 준비돼 있고 인터랙티브 뷰어에는 추가되지만, AI 전달용 캡처에는 누락.
  • 원인:
    • _capture_from_cameraself.total_mesh 또는 self.tin_mesh만 add_mesh. tin_extension_mesh는 무시.
    • _capture_depth_from_camera, _capture_lineart_from_camera 동일.
    • 결과: capture_textured.png의 bbox 밖이 흰 배경 → Gemini 프롬프트가 그 영역을 "비어 있음/프레임 외부"로 해석.
  • 수정:
    1. 3개 캡처 메서드 모두 tin_extension_textured ∥ tin_extension_mesh를 조건부로 add_mesh. 텍스처가 있으면 같은 satellite_temp.png를 공유(인터랙티브 뷰어와 동일 경로).
    2. gemini_renderer.run_gemini_render의 프롬프트에 "scene may combine high-detail DXF center + real DEM+satellite outer ring; render BOTH seamlessly, do NOT trim to central bbox" 힌트 추가.
  • 검증: ast.parse 2 파일 OK. (실제 Gemini 호출 결과는 사용자 UI에서 재실행해 확인 필요.)

[fix] 제수변실 — 도면에 없는데도 M-301 분기가 계속 생성되던 강제-시드 제거

  • 파일: structure_templates.py, valve_chamber_parser.py
  • 증상 (error1.png): 도면 파싱 결과에 도수관이 없고 사용자가 파라미터를 바꿔도 좌측에 Y-분기 관로가 항상 생김.
  • 근본 원인:
    1. ValveChamberTemplate.build_meshes_pipes 리스트에 도수관이 하나도 없으면 "M-301 도수관" 시드를 강제 주입한 뒤 _finalize를 호출 → has_inlet_branch=True일 때 무조건 분기 5개 pipe 생성.
    2. _finalizehas_inlet_branch=False 경로도 orig_main이 있으면 좌↔우 전관통 단일 pipe를 합성(origin-only seed조차 구조물로 실체화).
  • 수정:
    1. build_meshes에서 시드 주입 로직 삭제. vc.pipes는 파싱 결과 그대로만 사용.
    2. UI 토글 has_inlet_branch=0일 때 기존 _pipes에서 M-301 및 "도수관" 포함 이름을 전량 제거.
    3. _finalize의 분기-OFF 폴백도 orig_main.start가 origin(=미확정)이면 pipe를 만들지 않도록 제한.
  • 검증 (3가지 시나리오 모두 PASS):
    • DXF 있음 · 분기 ON → M-301 계열 5개 pipe 정상 생성.
    • DXF 없음 · 분기 ON → pipe 0개 (도면에 없는 관로 생성 금지).
    • DXF 있음 · 분기 OFF → M-301 계열 0개 (UI 토글로 완전 숨김).

[fix] 제수변실 — 좌측 고아 도수관 제거, 우측 관로 길이 현실화, 상류 Y-분기 지원

  • 파일: valve_chamber_parser.py, valve_chamber_3d_builder.py, structure_templates.py
  • 지적:
    1. 평면도(신설 제수변실.dxf)와 빌드된 3D가 어긋남 — 좌측에 관로가 chamber와 연결 안 된 채 독립적으로 떠 있음.
    2. 우측 송수관이 실제 도면에서 3~4m 남짓인데 3D는 15m 이상으로 길게 렌더.
    3. 도면상 도수관 1개 → Y자 분기 → 2개(상단·하단)로 chamber 진입하는데 빌더·파서·UI 모두 분기 개념 부재.
  • 도면 실측 (신설 제수변실.dxf, INSERT 재귀 전개):
    • Chamber CS-CONC-밸브실 bbox = 14000×9000mm.
    • 좌측 M-LINE 관로: 상단 Y=2533626536, 하단 Y=2213623336 → spread ≈ 4.4m, 중앙에서 45° 전후 대각선 2개가 Y=0으로 합류.
    • 우측 MZ-LINE: 29376 → 32971 = 3.6m (3D의 15m와 4배 차이).
  • 수정:
    1. ValveChamberParams 신규 필드:
      • upstream_pipe_length (기본 3.0m), downstream_pipe_length (기본 4.0m) — 좌/우 길이 분리.
      • has_inlet_branch (기본 True), branch_spread_m (4.4), branch_angle_deg (35), branch_trunk_length (3.0) — Y-분기 파라미터.
      • external_pipe_length 기본값 15.0 → 5.0 (fallback·legacy).
    2. ValveChamberParser._extract_from_mleaders — 좌측(-X)은 upstream_pipe_length, 우측(+X)은 downstream_pipe_length를 외부 연장으로 사용 (기존 단일 15m 제거).
    3. ValveChamberParser._finalize — 기존 단일 M-301 폴백을 걷어내고 has_inlet_branch=True면 trunk(1) + 경사 2 + 평행 2 = 총 5개 Pipe를 자동 생성 (chamber 좌측벽에 정확히 도달). False면 chamber 관통 단일 pipe로 폴백.
    4. ValveChamberBuilder._build_main_conduit — 기존 별도 생성 경로(좌측 고아 파이프 원인)를 no-op로. 모든 도수관/송수관은 _build_transmission_pipes가 단일 경로로 처리.
    5. structure_templates.py ValveChamberTemplate:
      • get_parameter_schemaupstream_pipe_length, downstream_pipe_length, has_inlet_branch, branch_spread_m, branch_angle_deg, branch_trunk_length 노출 (사용자가 UI에서 분기 여부·치수 조정 가능).
      • parse() → params.params에 신규 필드 전달.
      • build_meshes() — UI에서 분기 파라미터 변경 시 기존 _pipes의 낡은 분기 형상이 쓰이지 않도록 ValveChamberParser._finalize를 재실행해 재생성. M-301 시드가 없는 경우에도 최소 하나를 넣어 분기 자동 생성.
  • 검증:
    • 신설 제수변실.dxf parse 결과 M-301 계열 5개 pipe(상단/하단/분기상경사/분기하경사/도수관), 우측 송수관 3개 모두 end X = half_w + 4.0m.
    • default 파라미터 테스트: trunk (-16.14, 0) → 분기점 (-13.14, 0) → 상단 (-10, +2.2) → chamber 좌측벽 (-7, +2.2), 하단 대칭. 분기각 35°에서 L_branch = 2.2/sin(35°) ≈ 3.83m 정확히 반영.
    • ast.parse 3 files OK / import OK.

[fix] 개폐장치를 gate 중심이 아닌 pier X 중심 위에 embed — 허공 부양 완전 제거 + 용어 잔존 제거

  • 파일: gate_3d_builder.py, gate_parser.py, optional_detector.py, structure_templates.py
  • 지적:
    • 직전 수정 후에도 사용자 UI/요약/코드 docstring 여러 곳에 "권양기" 문자열이 잔존.
    • 개폐장치가 여전히 pier가 아니라 gate 중심 X 위에 놓여 공도교 상면에서 떠 보임.
    • 블록을 explode해 평면도를 전수조사해보면 실제로는 pier 상면에 단위 기초가 있다.
  • 수문_1.dxf 평면도 전수조사 결과 (INSERT 재귀 전개 후):
    • pier 4개 X 중심 ≈ 24374/44475/63957/84232 mm (간격 ~20m = gate 15m + pier 5m)
    • pier 상면의 CS-CONC-Spillway closed 4각형 — X폭 ≈ 4481mm, Y길이 ≈ 2181mm — pier X 중심과 정확히 일치 ⇒ 이것이 개폐장치 기초 footprint. pier 2·3 위에서 관측(사이트별 설치 위치 상이할 수 있어 빌더는 모든 pier에 1개씩 배치)
    • 측면도(수문_2.dxf) MZ-BASE Y=2493827015mm ↔ 표고 offset +30.962 ⇒ **EL.55.90057.977**, 높이 2.077m. 공도교 상면 EL.56.000 대비 0.1m embed.
  • 수정:
    1. 용어 정정 (사용자 노출 텍스트) — 이전 PR에서 놓친 잔존 문자열 전수 처리:
      • gate_3d_builder.py 모듈 docstring, 주석 — "권양기" → "개폐장치"
      • gate_parser.py GateParams.has_hoist_housings 주석 + summary() 문자열
      • optional_detector.py 모듈 docstring 및 주석
      • structure_templates.py description / ParamField 라벨
      • has_hoist_housings 플래그 이름과 ComponentSpec.layer_tokens/text_keywords의 한국어 토큰("권양")은 직렬화 호환·DXF 검출 목적으로 보존.
    2. _compute_pier_x_centers 헬퍼 분리:
      • Phase B' pier 폴리곤이 있으면 각 폴리곤의 X bbox 중심 사용, 없으면 parametric 계산(gate_centers_x + pier_w).
      • _build_piers_build_gate_hoists가 동일 결과를 공유.
    3. _build_gate_hoists 재작성 (pier 중심 기반):
      • 루프를 p.gate_centers_xpier_x_centers로 교체 ⇒ 개수는 n_gates+1 (= pier 수).
      • 치수: house_w = max(p.pier_width, 2.5) (pier 폭에 딱 맞춤 — 양옆 돌출 제거), house_l = 2.2m, house_h = 2.1m (평면도·측면도 실측).
      • Y 중심: nose_len + 0.5m (pier body 시작선 0.5m 하류 = 평면도 실측 local Y ≈1.5m 지점 근방).
      • Z: base_z = pier_top_el - 0.1m(embed), top base_z + 2.1. 더 이상 허공에 뜨지 않음.
      • 지붕 overhang 0.2m로 축소해 body Y 범위 내 유지.
  • 검증(default 파라미터 + 수면/apron/공도교 off):
    • pier_x_centers = [1.5, 19.5, 37.5, 55.5]m (4개)
    • 각 hoist X=pier와 정확히 동일 범위 ([0,3]/[18,21]/[36,39]/[54,57]), 양옆으로 돌출 없음
    • hoist Y=[3.00, 5.20], Z=[55.90, 58.00] → 측면도 실측 EL.55.9~57.977과 일치
    • 지붕 X overhang ±0.2m만, Y overhang ±0.2m만 (body 0.1m 이상 margin 확보)
    • 모든 구조물 Y=[0, 25] body 내, 허공 부양 제로
    • ast.parse 4 files OK / import 성공

[fix] "권양기 하우징" → "여수로 개폐장치"로 용어 정정 + 허공 부양 버그 해결

  • 파일: gate_3d_builder.py
  • 배경: 사용자 지적 — 빌더 내부 명칭 "권양기 하우징"은 정확한 도면 용어가 아님. 실제는 여수로 개폐장치(gate hoist). 또한 error2.png에서 개폐장치가 공도교/pier 상면과 분리된 채 2.4m 상공에 떠 있는 것이 관측됨.
  • 도면 검토 (수문_1.dxf 평면/정면, 수문_2.dxf 측면):
    • 측면도 MZ-BASE 레이어 Y=2493827015mm(표고 offset=30.962m 적용 시 EL.55.90057.977), 높이 ≈ 2.1m.
    • 평면도 MZ-LINE Y 분포 66366~71344mm → Y 방향 평면 길이 ≈ 5m.
    • 공도교 상면(el_bridge_top=56.0m) 대비 개폐장치 바닥 EL.55.9는 pier 상면 안으로 ~0.1m embed.
    • 개폐장치 단독 레이어는 DXF에 없음(MZ-LINE/MZ-BASE 혼재). 따라서 빌더는 측면도 관찰 수치를 파라메트릭으로 반영.
  • 수정:
    1. COLORShoist_housinggate_hoist, gate_hoist_roof 추가.
    2. 메서드 _build_hoist_housings_build_gate_hoists로 리네임. build_all의 호출부도 갱신. (has_hoist_housings 플래그 이름은 직렬화 호환 유지를 위해 보존.)
    3. 주요 수치 실측 반영:
      • house_w = max(gate_width * 0.7, 4.0) (수문 폭의 70%, ≈10.5m)
      • house_l = 5.0 (Y 방향, 평면도 기반)
      • house_h = 2.1 (높이, 측면도 기반)
      • embed_depth = 0.3 (pier 상면 안쪽으로 파고드는 깊이)
      • base_z = el_bridge_top - embed_depth → 허공 부양 제거, "일부 파여서 심어진" 형태 재현
    4. Y 중심도 지붕 돌출 포함 body 범위 내로 clamp(margin=house_l/2 + 0.3).
  • 검증:
    • default 파라미터 + 수면/apron/공도교 off → 모든 구조물 mesh Y=[0.00, 25.00] 내부.
    • 개폐장치 본체 Z=[55.70, 57.80], 지붕 Z=[57.80, 58.05]. 측면도 EL.55.9~57.977과 일치 범주.
    • ast.parse OK / import 테스트 OK.

[fix] 3D 프리뷰에서 수문·권양기·교각이 월류부 슬래브(25m) 밖 Y=-9m까지 돌출되던 문제

  • 파일: gate_3d_builder.py

  • 증상: 사용자 제보(error1.png/error2.png) — 3D 프리뷰 bbox가 Y=-9.0~25.0 (총 34m)로 표시. 측면도(error.png) 기준 월류부 슬래브 전체 길이는 25m, 수문·옹벽·권양기가 이 범위를 9m 이상 벗어남.

  • 근본 원인 2개:

    1. _compute_gate_geometrygate_y = 1.0 하드코딩 — gate_height=7m/radius=8.75m 조건에서 horizontal = sqrt(r²-dz_half²) ≈ 8.02m이므로 trunnion_y = gate_y - horizontal = -7.02m가 되고, 권양기 y_center = trunnion_y, y0 = -8.77m로 하우징 전체가 상류 경계 8~9m 밖으로 탈출.
    2. _build_piers parametric 폴백에서 pier body를 y0=0부터 시작시키고 그 앞에 nose를 돌출시킴 → _make_pier_nosey_tip = y_front - pier_w*1.2 = -3.6m까지 튀어나감.
  • 수정:

    1. _compute_gate_geometry
      • ogee_profile에서 |z el_weir_crest|가 최소인 점의 x를 crest_y_candidate로 채택(상류 끝점 x=0은 제외).
      • hoist_half = 2.0m (하우징 반길이 1.75 + 지붕 여유 0.2) 반영해 gate_y = clamp(crest_y_candidate, horizontal + hoist_half, pier_length - 0.5).
      • 결과: default 값에서 gate_y = 10.02, trunnion_y = 2.00, hoist y0 = 0.25, 지붕 y0 = 0.05 → body 내부.
    2. _build_piers parametric 경로
      • nose_len = pier_width * 1.2만큼 body 시작점을 안쪽으로 이동(body_y0 = nose_len), nose는 [0, nose_len] 구간에 배치해 slab 범위 [0, pier_length]를 벗어나지 않게 함.
      • 덤으로 mesh.merge(nose) 호출도 try/except로 감싸 merge 실패 시 본체만이라도 추가되게.
  • 검증:

    • default GateParams + has_water_surface/apron/service_bridge=False로 build → 모든 구조물 mesh Y 범위 [0.00, 25.00]. 이전 -9값 재현 안 됨.
    • ast.parse 통과 / import smoke test OK.
  • 비고:

    • has_water_surface(-40~+0.5m)와 has_downstream_apron(25~55m)은 시각 맥락용으로 의도적 외부 배치. 사용자 토글로 제어.
    • default ogee_profile에서 crest가 x=1.0으로 설계되어 gate_y가 clamp에 걸려 상대적으로 상류측에 위치. 실제 section DXF가 파싱되어 crest x가 더 정확하게 잡히면 clamp 없이 해당 값 사용(여전히 body 내부로 한정).

2026-04-20

[fix] gate_3d_builder·gate_parser 종합 검토 및 결함 정리

마지막 빌드 전 gate_3d_builder.py + gate_parser.py 전반 검토. 직접 보고된 'list' object has no attribute 'merge'의 실제 원인부터, 병행해 발견한 4개 결함까지 일괄 수정.

  • 파일: gate_3d_builder.py, gate_parser.py
  • 검증: ast.parse 통과 + from gate_parser import GateParams·from gate_3d_builder import GateBuilder 모두 성공.

1. [critical] _make_gate_arms에서 list에 .merge() 호출 (gate_3d_builder:457)

  • 증상: 'list' object has no attribute 'merge' — 수문 암(arm) 빌드 시마다 발생.
  • 원인: merged = parts로 list를 seed로 쓴 뒤 곧바로 merged.merge(m) 호출. PyVista의 mergePolyData 메서드.
  • 수정: merged = parts[0]으로 첫 tube를 seed로 삼고, 루프 내 merge는 개별 tube 파손 대비 try/except로 감쌈.

2. [high] _make_pier_nose 뒷면(back face) 누락 (gate_3d_builder:285-307)

  • 증상: 노즈가 속이 비어 보일 가능성. 통상 pier body와 겹쳐 문제 없지만 arm merge 실패 시 노출되던 과거 이슈와 복합적 정황.
  • 수정: pts[0,3,4,1] 4-정점 사각형 face 추가 (pier 쪽 면).

3. [medium] _make_radial_skin 각도 계산이 sill·top의 실제 위치와 어긋남 (gate_3d_builder:382-388)

  • 원인: dx_sill = dx_top = gate_y - trunnion_y로 두 점의 수평 거리를 동일하게 사용. _compute_gate_geometry가 trunnion을 mid_el에 맞추면 대칭이라 결과가 맞지만, trunnion 오차 0.5m까지 허용되는 현재 로직에선 skin이 원호를 살짝 벗어남.
  • 수정: 각 점의 dx를 sqrt(radius² dz²)로 계산해 실제 원호 상 위치에 정합.

4. [medium] _make_gate_arms 방어력 보강 (gate_3d_builder:458-463)

  • merged = parts[0] 교체와 함께, 반복문 안에서 .merge() 실패 시 해당 tube는 건너뛰고 계속 진행하도록 try/except 추가.

5. [low] GateParamsel_stoplog_sill 필드 부재 (gate_parser:50)

  • 증상: _scan_text_annotations가 "Stoplog Sill EL." 패턴을 파싱해도 hasattr(params, 'el_stoplog_sill') 체크에서 탈락 → 값이 버려짐.
  • 수정: dataclass에 el_stoplog_sill: float = 46.000 필드 추가. 현재 빌더는 이 값을 직접 쓰지 않지만 raw_text_annotations/추후 확장을 위해 보존.

[fix] gate_3d_builder.py IndentationError (L343)

  • 파일: gate_3d_builder.py
  • 증상: unindent does not match any outer indentation level (gate_3d_builder.py, line 343) — 모듈 로드 실패
  • 원인: 이전 편집에서 def _compute_gate_geometry(self):(L311)가 클래스 들여쓰기(4칸)가 아닌 0칸으로 저장되어 메서드가 모듈 최상위로 분리됨. 뒤따르는 def _build_radial_gates(self):(L343, 4칸)과 레벨이 어긋나 파서가 L343에서 에러 발생.
  • 수정: L311 def _compute_gate_geometry 앞 공백을 4칸으로 교정해 클래스 메서드로 복원.
  • 검증: python -c "import ast; ast.parse(...)" → OK.

[fix] FLOW → plan_frame_angle 수식이 180° 틀려 수문이 반대로 배치되던 문제

  • 파일: gate_parser.py
  • 증상: 직전 FLOW 화살표 검출 구현 후 결과가 여전히 반대 방향으로 배치. 사용자: "수문이 여수로 쪽을 보고 있으면 물이 수문 뒤쪽에 튀는데…"
  • 근본 원인:
    • structure_placement.fit_meshes_to_quad는 CW 사각형 default 시 flip_y_for_cw_quad=Truemesh Y를 먼저 반전 후 회전 적용
    • 이 Y-flip이 내 빌더 컨벤션(+Y=downstream)과 겹쳐 상쇄되는데, 이전 공식 atan2(-fx, fy)는 그 상쇄를 고려하지 않아 plan_frame이 180° 틀린 값(=178°) 생산
    • 결과: 최종 회전 후 mesh +Y가 flow의 반대 방향에 매핑 → 수문 skin이 하류(여수로) 방향을 봄
  • 유도 (tin_angle=0, detail_angle=0 가정):
    • mesh point (0, 1)이 파이프라인을 거쳐 world (fx, fy) [flow 방향]에 매핑되어야 함
    • (0,1) → Y-flip → (0,-1) → rotate(-span) → (-sin(span), -cos(span))
    • 이 벡터 = (fx, fy) → sin(span)=-fx, cos(span)=-fy
    • 따라서 span_angle = atan2(-fx, -fy)
  • 변경:
    • 기존 math.atan2(-fx, fy)math.atan2(-fx, -fy) (두 번째 인자 부호 뒤집기, 결과 180° 보정)
    • 주석에 유도 과정 풀이 추가
  • 검증 (수문_1.dxf, flow=(-0.035, -0.999)):
    • 이전 plan_frame_angle = 178°
    • 수정 후 plan_frame_angle = +2.00° ≈ 0°
    • 수학 검증: Mesh +Y point (0,1) 파이프라인(Y-flip + rotate(-2°)) 통과 후 → (-0.035, -0.999) = flow 방향과 일치 ✓
    • 결과: 수문 skin(convex면)이 upstream(물 쪽) 을 향하고, trunnion/hoist가 downstream(여수로 쪽) 으로 올바르게 배치

[feat] 도면의 FLOW 화살표 자동 인식으로 수문 방향 180° 모호성 해소

  • 파일: gate_parser.py, structure_templates.py
  • 증상: 수문이 설치 방향(upstream/downstream)을 반대로 빌드하는 문제. PCA만으로는 span 축만 알 수 있고 어느 방향이 downstream인지 180° 모호성 존재 → 이전에는 사용자가 4점 매칭에서 quad 순서로 잡아야 했지만 centroid 폴백에선 틀릴 가능성. 사용자 요청: 도면의 "FLOW" 화살표를 인식해 자동 보정.
  • 변경:
    • GateParamsflow_direction_2d: Optional[tuple] 필드 추가 (DXF XY 프레임의 단위벡터).
    • 신규 GateParser._detect_flow_direction(msp):
      1. TEXT/MTEXT에서 "FLOW"/"흐름"/"유수" 키워드 탐지
      2. 각 텍스트 반경 10m 내 LINE 수집 → 길이 ≥2m는 shaft 후보, <2m는 arrowhead
      3. shaft 최장 선택, 양 끝점 중 arrowhead 점 밀집도가 높은 쪽을 tip으로 결정
      4. tip-tail 벡터 정규화. 여러 FLOW는 평균 (불일치 시 |avg|<0.3으로 거부)
    • _parse_plan_file의 plan_frame_angle_deg 계산 로직 변경:
      • 1순위: FLOW 검출 성공 시 flow 각도에서 derive
        • 빌더 +Y = flow 방향 / 빌더 +X = rotate(-90°)(flow) → plan_frame_angle = atan2(-fx, fy)
        • 전체 -180~+180 범위 유지 (모호성 없음)
      • 2순위: 기존 PCA (결과는 -90~+90 정규화, 180° 모호성 존재)
    • structure_templates.SpillwayGateTemplate.parse pass-through에 plan_frame_angle_deg, flow_direction_2d 추가.
  • 검증:
    • 수문_1.dxf (사용자가 FLOW 2개 추가): flow_direction_2d=(-0.035, -0.999) ≈ DXF 남쪽 방향, plan_frame_angle_deg=+178.0° (= builder +X가 DXF -X 방향). 이전 PCA로는 -2° 근처였으므로 180° 뒤집힘 해소
    • 여수로 수문1.dxf (FLOW 없음): flow_direction_2d=None, PCA 폴백으로 -2.23° 유지 — 기존 동작 보존 ✓
    • 템플릿 경유 빌드 22 meshes 정상.
  • 한계/후속:
    • 검출 파라미터는 경험적(반경 10m, shaft 길이 2m). DXF 축척이 매우 다르면 동작 안 할 수 있음 → 후속에서 view_detector의 unit_scale 활용해 자동 조정 가능.
    • 현재 gate에만 적용. 다른 구조물(intake_tower, valve_chamber)도 유사한 FLOW 기반 방향 결정 가능 — 후속 turn.
    • centroid 폴백 경로는 orientation_deg from compute_orientation_from_points를 사용. FLOW 기반 각도를 여기에도 전달하도록 egview_maker의 centroid 계산부 업데이트 필요 (후속).

[fix] 도로 cut/fill TIN을 재-삼각화 경로로 교체 (vertex displacement → synthetic 정점 + 새 Delaunay)

  • 파일: egview_maker.py (_retriangulate_for_cut_fill, _resample_polyline, _slope_breakpoints, _deform_tin_for_plans)
  • 증상 (error.png): 이전 vertex displacement 방식은 target Z 계산이 정확했으나, TIN 정점이 등고선에 정렬돼 있어 이를 단순히 이동시키면 삼각형 스파이크·찢김 발생. 도로 주변 원본 삼각화 구조가 사면 단절점(소단 모서리·toe)을 표현할 수 없었음.
  • 근본 해결 — 재-삼각화:
    • synthetic 정점을 사면 단절점 위치에 삽입: 도로 중심선, 도로 edge(±half_w), 각 사면 꼭대기/소단 edge, 사면 toe. along 방향 2m 간격 re-sampling.
    • cut zone 폴리곤 내부 원본 TIN 정점 제거: 사면 toe 기준 footprint 계산, MplPath.contains_points로 마스킹.
    • 잔존 TIN + synthetic 통합 후 scipy.spatial.Delaunay로 재-삼각화 → 도로 평탄면·소단·사면이 모두 삼각형 경계에 정확히 일치하는 깨끗한 기하.
  • 변경:
    • 신규 _resample_polyline(pts, step=2.0) — polyline을 균등 2m 간격으로 재샘플링.
    • 신규 _slope_breakpoints(depth, half_w, vh, berm_v, berm_w) — 사면 단절점 (cross_dist, v_rise) 리스트 반환 (도로 edge · 각 사면 꼭대기 · 각 소단 끝 · toe).
    • 신규 _retriangulate_for_cut_fill(...) — cut/fill 레이어만 대상, synthetic 정점 생성 + 원본 제거 + 재-삼각화.
    • _deform_tin_for_plans 초반에 cut/fill 레이어 존재 시 재-삼각화 경로로 분기. terrain/manual만 있으면 기존 smoothstep 유지.
  • 검증 (100×100 평탄 TIN EL=60, 도로 Y=50 width 6m, 절토 10m, V:H=1:0.5, 소단 5m@1m):
    • 원본 900 정점 → 잔존 756 + synthetic 451 = 1,207 정점, 2,296 삼각형 (깨끗한 삼각화)
    • X=50 단면 Z 프로파일:
      • Y=47, 50, 53 (도로 평탄): Z=50.00
      • Y=44.5 / 55.5 (h_edge=2.5, 첫 사면 꼭대기): Z=55.00
      • Y=43.5 / 56.5 (h_edge=3.5, 첫 소단 끝): Z=55.00 (소단 플랫) ✓
      • Y=40.7 / 59.0 (h_edge=6, 사면 toe): Z=60.00 (지형 복귀) ✓
    • 완벽한 계단식(stepped) 사면 프로파일 + 1m 폭 소단.
  • 정직한 한계:
    • _excavate_tin_for_structures(구조물 굴착)은 아직 이전 로직. 구조물에도 동일 재-삼각화 적용은 다음 턴.
    • terrain/manual 모드는 재-삼각화 적용 안 함 (cut/fill 명시 offset이 있을 때만 synthetic 생성). terrain mode에서도 깨끗한 삼각화가 필요하면 후속 확장.
    • TIN 재-삼각화는 원본 정점의 배열 순서를 유지하지 않음. 이후 structure placement 등에서 인덱스에 의존하면 재검증 필요.
    • 현재 cut zone 폴리곤이 여러 도로 겹치면 토대 벤 부분이 중복 처리될 수 있음. 단일 도로엔 영향 없음.

[feat] 도로 TIN 변형에 토목 표준 사면 + 소단 자동 생성 (V:H=1:0.5, 5m@1m)

  • 파일: egview_maker.py (_open_elevation_dialog, _deform_tin_for_plans, 신규 _cut_slope_rise)
  • 증상 (error.png): 직전 smoothstep 블렌드만으로는 원본 TIN 정점이 등고선에 정렬돼 있어 절토 적용 시 삼각형 스파이크가 튀어나오는 현상. 사용자 요구: 도로 시/종점 계획고 선형 보간은 이미 맞지만, 주변 지형과 연결되는 절토/성토 사면이 V:H=1:0.5 경사 + 5m마다 1m 소단의 토목 표준으로 자동 형성되어야 함.
  • 변경 A — 신규 헬퍼 _cut_slope_rise(h_dist, total_depth, vh_ratio, berm_step_v, berm_width_h):
    • 수평 h_dist 지점에서의 수직 상승량 반환 (cut/fill 공통).
    • 알고리즘: V:H 비율(예: 1:0.5)로 sloping 하다가 수직 berm_step_v(5m) 도달 시 수평 berm_width_h(1m) 소단 삽입, 다시 slope, 반복. total_depth에서 캡.
    • 단위 검증 통과: h=1→v=2, h=2.5→v=5, h=2.5~3.5 (소단) v=5 유지, h=4→v=6, h=6→v=10 (최종).
  • 변경 B — _deform_tin_for_plans 재작성:
    • is_cut_fill_mode 분기: 이전의 smoothstep transition → 표준 사면 기하로 교체.
    • 각 TIN 정점에 대해:
      1. 가장 가까운 도로 세그먼트의 (along, cross) 계산
      2. cross ≤ half_w → 도로 평탄 (target_z = road_z)
      3. cross > half_w 이고 cut/fill 모드:
        • h_from_edge = cross - half_w
        • cut_depth = terrain_z - road_z (부호로 cut/fill 결정)
        • slope_rise = _cut_slope_rise(h_from_edge, |cut_depth|)
        • cut: target_z = road_z + slope_rise, 지형 초과 시 지형 캡
        • fill: target_z = road_z - slope_drop, 지형 미만 시 지형 캡
        • 사면 영역 weight = 1.0 (완전 적용)
      4. terrain/manual 모드는 기존 smoothstep 유지
    • 사면 영향 범위 자동 계산: slope_horiz_max = offset × vh_ratio + (offset/berm_step × berm_width) → 예상보다 멀리까지 탐색.
  • 변경 C — 다이얼로그 확장:
    • 기존 7개 컬럼 → 10개 컬럼: 레이어 / 방식 / 시EL / 종EL / 절·성(m) / V:H / 소단V(m) / 소단W(m) / 전이(m) / 시·종좌표
    • 각 도로별 독립적으로 경사비·소단 간격·소단 폭 입력 가능 (default V:H=0.5, 소단 V=5, W=1)
    • layer_elevations[ln]slope_vh, berm_step_v, berm_width_h 저장.
  • 검증 (100×100 평탄 TIN EL=60, 도로 Y=50 width 6m, 절토 10m):
    • Y=48.72 (cross=1.28, 도로 위): Z=50.00 (road_z) ✓
    • Y=46.15 (h_edge=0.85, 첫 사면): Z=51.69 (=50+1.7, 계산값 51.7) ✓
    • Y=43.59 (h_edge=3.41, 첫 소단): Z=55.00 (소단 플랫 v=5 유지) ✓
    • Y=41.03 (h_edge=5.97, 두 번째 사면): Z=59.95 (=50+9.94) ✓
    • Y=38.46 (h_edge=8.54, 사면 완료): Z=60.00 (지형 유지) ✓
    • 양쪽 대칭 ±Y 동일 프로파일 ✓
  • 정직한 한계:
    • TIN 정점은 등고선에서 추출된 비정형 위치. 사면 프로파일 목표 Z는 정확하나, 정점 밀도가 낮으면 시각적으로 여전히 거칠 수 있음. 완벽히 매끈한 사면을 위해선 synthetic vertex 삽입 + 재-삼각화 필요 (후속 작업).
    • _excavate_tin_for_structures는 아직 이전 로직 사용. 구조물 굴착에도 동일 사면 로직 적용은 다음 턴.
    • transition_m은 이제 terrain/manual 모드에서만 사용 (cut/fill은 경사 기하가 대체).

[feat] 도로 TIN 변형 재작성 — 절토/성토 모드 + smoothstep 부드러운 전이

  • 파일: egview_maker.py (_open_elevation_dialog, _deform_tin_for_plans)
  • 증상 (road_error.png, road_error2.png): 도로를 평면도에 올리고 고도 설정(인근 지형/계획고 어느 쪽이든) 후 TIN 변형하면 도로가 수직 절벽 양쪽으로 튀어나온 "협곡"·"댐"처럼 보임. 인접 지형과 어긋남. 사용자 요청: 도로별 절토/성토 선택·수치 입력·부드러운 TIN 연결.
  • 근본 원인:
    • 기존 _deform_tin_for_plans가 도로 중심 ±half_w(3m) 이내를 pts[vi, 2] = road_z로 강제 평탄화 → 주변 지형 Z와의 차이만큼 즉시 절벽.
    • slope_w = half_w * slope_ratio = 4.5m 폭의 좁은 전이 영역만 선형 블렌드 → C0 연속이지만 경사 단절(kink) 유발.
    • 절토/성토 개념이 schema에 전무. z_offset YAML 상수는 있으나 양수 지원·UI 입력 없음.
    • 한 정점이 여러 세그먼트(곡선 구간) 근처에 있을 때 매 iteration에서 Z를 덮어쓰기 → 마지막 세그먼트만 반영되는 문제도 잠재.
  • 변경 A — 고도 다이얼로그 (_open_elevation_dialog):
    • 모드 4가지로 확장: "인근 지형 참조"(terrain) / "계획고 입력"(manual) / "절토"(cut) / "성토"(fill)
    • 신규 컬럼 2개: "절토·성토(m)" (수치 입력), "전이폭(m)" (smoothstep blend zone, 기본 10m, 최소 1m)
    • 모드 변경 시 placeholder 자동 갱신, 상관없는 필드는 비움.
    • 저장 스키마: layer_elevations[layer_name] = {mode, start_el, end_el, offset_m, transition_m}.
  • 변경 B — _deform_tin_for_plans 전면 재작성:
    • Per-vertex weight 누적: 각 TIN 정점에 대해 모든 인근 도로 세그먼트의 영향을 sum_wz += w × road_z, sum_w += w 로 합산.
    • smoothstep 감쇠: cross_dist ≤ half_w 이면 w=1, half_w ~ half_w+transition_m 에서는 w = 1 3u² + 2u³ (C1 연속). → 급격한 cliff 사라지고 매끄러운 사면 형성.
    • Cut/Fill 오프셋: terrain 샘플된 도로 중심선 Z에 -offset_m(cut) 또는 +offset_m(fill) 적용.
    • Manual Z: start_el/end_el 선형보간 (이전 동작 유지, 전이 적용).
    • 최종 정점 Z = blend × Σ(w·z)/Σ(w) + (1blend) × original_z, blend = min(sum_w, 1) — 누적 영향 약한 곳은 원지형 유지 비중↑, 도로 중심은 완전 교체.
    • 곡선 구간 다중 세그먼트 영향도 선형 가중 평균으로 자연 처리.
  • 검증 (100×100m 경사 TIN + 80m 수평 도로 Y=50):
    • terrain (offset=0): road_center Z=57.50 (지형과 정확 일치) / far Y=0: 55.00 (미영향) ✓
    • cut 5m, transition 10m: road_center=52.50 (=57.55) / edge Y=44(3m 밖): 53.76 / trans Y=40(7m 밖): 56.05 ✓
    • cut 5m, transition 20m: trans Y=40(7m 밖): 53.85 (더 넓은 전이가 더 오래 cut 영향 유지) ✓
    • fill 3m: road_center=60.50 (=57.5+3) ✓
    • far 정점 55.00 그대로 유지 — 원지형 보존 ✓
  • 트레이드오프:
    • surface_overlay (굴착/성토 폐합 영역)는 현재 영역 내부 일괄 offset만. 경계부 smoothstep 전이는 후속 개선.
    • slope_ratio YAML 파라미터는 이제 무시됨 (transition_m이 대체). 기존 z_offset YAML은 추가 미세 조정으로 누적.

[fix] 래디얼 게이트 sill이 weir crest에 맞닿지 않던 기하 오류 (trunnion 위치 계산식 버그)

  • 파일: gate_3d_builder.py
  • 증상: 직전 수정으로 arc 방향은 바로잡혔지만, 게이트 sill이 의도한 (gate_y=1.5, sill_el)에 있지 않고 상류로 1.46m 튀어나간 (Y=0.045)에 위치. 사용자 지적: "수문 문을 닫아놓는 방향이 Weir Crest에 맞닿아야 하는데 반대로 설치됨".
  • 근본 원인 (기하학적 일관성 위반):
    • 이전 코드: trunnion_y = gate_y + radius * 0.7 (단순 휴리스틱)
    • 결과: 실제 (gate_y, sill_el)과 trunnion의 거리 = 7.05m, 그러나 radius = 8.75m → 원호가 (gate_y, sill_el)을 지나지 않음. 원호 sill 끝점은 0.045로, 의도보다 1.46m 상류에.
    • 원호가 sill/top을 정확히 지나려면 trunnion_y는 sqrt(R² (gate_h/2)²) 수직 offset을 만족해야 함.
  • 변경:
    • 신규 _compute_gate_geometry() 헬퍼: 원호 구속조건(trunnion → sill/top 거리 == R)에서 trunnion_y를 도출. gate_y = gate_y + sqrt(R² (gate_h/2)²) 공식.
    • 제약 검증: trunnion_el(sill_el + top_el)/2 에서 ±0.5m 이상 벗어나면 midpoint로 강제(구속조건 상 midpoint여야 함).
    • gate_y1.5 → 1.0으로 조정 (ogee_profile 기본 weir crest Y=1.0과 정렬) → sill과 weir crest가 동일 Y 평면에서 맞닿음.
    • _build_radial_gates, _build_hoist_housings 모두 이 헬퍼 사용해 일관성 유지.
  • 검증 (수문_1.dxf + 수문_2.dxf):
    • distance(trunnion, sill) = 8.750m, R = 8.750m오차 0.000m
    • distance(trunnion, top) = 8.750m오차 0.000m
    • Arc bounds: Y=[0.27, 1.00], Z=[46.70, 53.70] — sill/top이 (Y=1.0, sill_el=46.70) / (Y=1.0, top_el=53.70) 에 정확히 위치 ✓
    • Hoist Y=[7.27, 10.77], 중심 9.02 = trunnion_y ✓ (개폐장치가 트러니언거더 위에 정확 배치)
  • 정직한 한계:
    • trunnion_el midpoint 강제는 근사. 실제 도면이 非midpoint 설계면 원호 근사값 사용.
    • gate_y = 1.0은 기본 ogee_profile 기준. 사용자가 다른 weir crest Y 도면을 쓰면 어긋날 수 있음 — 필요 시 schema 파라미터로 노출 가능.
    • radius = gate_h * 1.25는 heuristic. 도면 실측과 차이 가능.

[fix] 래디얼 게이트 호(arc) 방향 뒤집힘 + 개폐장치 위치 오류

  • 파일: gate_3d_builder.py
  • 증상 (gate1.png): (1) 수문 arc가 gate_top(53.7m)을 훨씬 초과해 Z≈59m까지 솟아 월류부와 피어 상단을 전부 덮음, (2) 개폐장치(hoist) 박스가 보이지 않거나 너무 하류에 위치해 도면과 불일치.
  • 근본 원인 1 — Arc 장경로 보간:
    • _make_radial_skin에서 trunnion 기준 sill 각도(-150°)와 top 각도(+150°)를 ang_sill*(1-t) + ang_top*t로 선형 보간.
    • 두 각도가 ±180° 양쪽에 있어 장경로(300°) 로 돌아 arc가 0°(하류 방향)를 통과 → 중간점 Z=trunnion_el+radius=58.95m → gate_top 훨씬 초과.
    • 수정: if ang_top - ang_sill > π: ang_top -= 2π (또는 반대)로 짧은 경로(60°) 강제. n_circ를 12→16으로 증가해 smoothness 향상.
    • 결과: arc Z 범위 [45.86, 54.54] (이전 ~58) — gate_top 근사한 올바른 높이.
  • 근본 원인 2 — Hoist 위치 너무 하류:
    • _build_hoist_housingsy0 = pier_length * 0.6 = 15m에 배치. 도면은 개폐장치가 트러니언거더 바로 위 (Y ≈ trunnion_y + 약간).
    • 게다가 over-tall arc가 hoist 박스를 덮어 시야에서 사라졌음.
    • 수정: trunnion_y 계산식(gate_y + radius*0.7 = 7.62m)을 builder에서 재사용, 개폐장치 중심 y_center = trunnion_y + 0.5로 이동.
    • 결과: hoist Y 범위 [6.38, 9.88] — 트러니언 바로 위, 피어 상단에 올바르게 배치.
  • 검증 (수문_1.dxf + 수문_2.dxf, el_gate_sill=46.7 / el_gate_top=53.7 / el_trunnion=50.2):
    • gate skin Z: [45.86, 54.54] (이전 [45~58.95]) ✓
    • hoist Y: [6.38, 9.88] (이전 [15, 18.5]) ✓
    • trunnion_y=7.62m 계산 일관 ✓
  • 남은 한계/후속:
    • arc radius 기본값 gate_h * 1.25 = 8.75m — 도면마다 실측값 다를 수 있음. 필요 시 schema에 radius 편집 파라미터 추가 가능.
    • 개폐장치 하우징은 단순 박스 + 평지붕. 도면의 실제 크레인·모터·플랫폼 디테일은 미반영.
    • 수문 3D 상세 모델 빌더 알고리즘 전반 재설계 과제 여전히 남음 (사용자 지적, 후속 턴).

[fix] Phase B' 결과 sanity check + 공도교 위치 사용자 편집 파라미터 노출 (옵션 A+B)

  • 파일: gate_parser.py, gate_3d_builder.py, structure_templates.py
  • 배경: 이전 Phase B' pier 폴리곤 추출이 wing wall·트러니언 마운트 등 주변 수직선까지 "pier 영역"으로 묶어 실제 pier_width 4.85m 대신 9~17m의 왜곡된 pier가 빌드되고 있었음. Bridge bbox도 auto-populated 경로를 거쳐 의심스러운 값이 그대로 사용. 사용자 지적 E4.png.
  • 변경 A — 빌더 sanity check 추가:
    • GateBuilder._validate_pier_polys(pier_polys, pier_width, pier_length, tol_ratio=0.5) 신규 — 각 pier의 X-폭이 pier_width * (1 ± 0.5) 범위 외면 False 반환 → 전체 Phase B' pier 거부, parametric(4.85m 고정 폭)으로 자동 폴백.
    • GateBuilder._validate_bridge_bbox(bbox, total_span, pier_length) 신규 — width를 total_span × 0.2~1.5, height를 pier_length × 0.05~1.2 범위로 검증. 실패 시 해당 경로 거부.
    • _build_piers 경로 변경: if len==n_gates+1 AND validate_pier_polys일 때만 폴리곤 사용, 아니면 parametric.
    • _build_service_bridge 경로 3단계화:
      1. 사용자 override(x1>x0 & y1>y0) + sanity 통과 → 사용자 bbox
      2. 파서 bridge_plan_bbox + sanity 통과 → 추출 bbox
      3. 위 모두 실패 → parametric (기존 default) 결과 source가 [builder] bridge source=user|extracted|parametric으로 로그에 명시됨.
  • 변경 B — 공도교 위치 사용자 편집 파라미터:
    • GateParams 신규 필드 bridge_x_start/bridge_x_end/bridge_y_start/bridge_y_end: Optional[float].
    • 파서가 bridge_plan_bbox 추출 시 이 4개 필드에도 같은 값을 복사 → UI에 자동 추출값이 기본 표시.
    • SpillwayGateTemplate.get_parameter_schema에 4개 float 엔트리 추가 (단위 m, 범위 -50~500).
    • 템플릿 parse가 params.params dict에 4개 필드 pass-through (None → 0.0 대체 후 float).
    • 사용자가 UI에서 값 편집해 실제 도면 치수 입력 가능.
  • 검증 시나리오 (실측 수문_1.dxf + 수문_2.dxf):
    • Phase B' pier 결과: W=[9.74, 8.88, 8.88, 17.24]m — 모두 sanity tol_ratio=0.5 (3.64~6.06) 초과 → 거부 → parametric 폴백으로 실제 4.84m uniform pier 4개 빌드 ✓
    • 4 bridge 시나리오 E2E:
      1. 자동(파서 auto-populated UI 값): source=user, bbox=(39.71, 0.10, 62.09, 8.55) — sanity 통과
      2. 사용자 수동 수정 (064.4, 07.5): source=user, bbox=수정값 ✓
      3. 사용자 이상값(999~1000): sanity 실패 → source=extracted, 파서 bbox 재사용 ✓
      4. 사용자 0 리셋: x1>x0 조건 실패 → source=extracted
  • 정직한 현황:
    • Pier: Phase B' 알고리즘이 실측 도면에선 작동 안 함(sanity 통과 못함) → 당분간 parametric 4.85×25m uniform 사용. 결과가 도면과 정확히 일치하진 않지만 최소 일관되고 기하학적으로 말이 됨.
    • Bridge: 파서 auto bbox가 sanity 통과하면 사용, 아니면 사용자가 UI에서 실측 값 입력 가능.
    • 수문 3D 상세 모델 빌더 알고리즘 전반 재설계 필요(사용자 지적) — 후속 턴 예정.
    • 이후 VLM 피드백 루프로 시각 검토 수행 예정.

[fix] 수문 공도교 위치가 도면 무관하게 default로 전체 구조물을 가로지르던 치명적 미완성

  • 파일: gate_parser.py, gate_3d_builder.py, structure_templates.py
  • 증상 (사용자 error_01.png, error_02.png): 공도교가 수문 바로 위를 가로지르며 간섭, 도면의 실제 공도교는 구조물 오른쪽 1/3에만 있음(X=48-71m 국소, 전체 span 64.4m). 사용자가 UI에서 공도교를 끌 방법조차 없음.
  • 근본 원인:
    1. Phase A에서 has_service_bridge 플래그는 추가했으나 위치는 여전히 default 하드코드. 빌더 _build_service_bridgex0=-pier_w/2부터 x1=total_span+pier_w/2까지 전체 폭을 가로지르는 deck 생성, Y는 pier_length*0.3~0.55 고정.
    2. CS-CONC-Bridge 레이어에 실제 bbox geometry(X=60329-82705mm, Y=48217-56668mm, 22.4×8.5m)가 있는데 파서가 검출 여부만 True/False로 쓰고 좌표는 무시.
    3. UI의 get_parameter_schemahas_service_bridge 토글이 없어 사용자가 오검출 시 수동으로 끌 수 없음.
  • 변경:
    • GateParams에 신규 필드 bridge_plan_bbox: Optional[tuple] (x0, y0, x1, y1 in local m) + bridge_deck_thickness_m: float = 1.2.
    • 파서:
      • _extract_bridge_bbox_mm(msp)bridge/공도교/공도/service road 토큰 레이어의 LINE/LWPOLYLINE bbox(DXF mm) 반환. 관리도로_수정 같은 보조 레이어는 주요 토큰에서 제외해 노이즈 차단.
      • _extract_plan_polygons의 3단계 pier 추출 경로(closed_polylines / face_enumeration / vertical_clusters) 각각이 실제 사용한 origin_mm을 self._pier_origin_mm에 기록 → bridge bbox가 pier와 동일 로컬 프레임으로 정규화되어 정합.
    • 빌더 _build_service_bridge:
      • bridge_plan_bbox가 설정돼 있으면 그 x0/y0/x1/y1를 deck·난간 bbox로 직접 사용
      • 없으면 기존 parametric default 폴백
      • Z는 여전히 el_bridge_top 파라미터 기반 (측면도 자동 추출은 후속 작업)
    • SpillwayGateTemplate.get_parameter_schema에 4개 부속 토글 추가(int, 1=켜기/0=끄기):
      • has_service_bridge, has_hoist_housings, has_downstream_apron, has_water_surface
      • 파서 자동 검출 결과가 default로 표시되지만, 사용자가 1↔0 편집해 오버라이드 가능.
    • 템플릿 파라미터 dict에 bridge_plan_bbox, bridge_deck_thickness_m pass-through. has_* 플래그는 int(bool(...)) 직렬화, build_meshesbool(v) 역변환해 dataclass에 반영.
  • 검증 (실측 수문_1.dxf + 수문_2.dxf):
    • 자동 검출: bridge_plan_bbox=(39.71, 0.10, 62.09, 8.55)m (실제 도면 그대로). pier2 ~ pier3 사이+약간, 폭 22.38m — 이전 "전체 폭 가로지름" 대비 도면 일치.
    • 사용자가 has_service_bridge=0: 22 → 19 meshes (bridge deck 1 + 난간 2 제외, 정확).
    • 사용자가 has_hoist_housings=0: 22 → 16 meshes (3 gate × box+roof = 6 hoist 제외, 정확).
  • 정직한 한계:
    • Z(높이) 방향 bridge 위치는 여전히 el_bridge_top default(56m)에 의존. 측면도에서 bridge EL 자동 추출은 CS-CONC-Bridge 레이어가 측면도(수문_2.dxf)에 없어 현재 DXF로는 어려움 — 텍스트 annotation("공도교 EL.XX")에서 파싱하는 후속 작업 필요.
    • bbox는 사각형 근사. 실제 공도교가 굴곡진 형태면 약간의 오차 남음 (CAD에서 대부분 직사각 deck이라 허용).
    • 관리도로_수정 같은 별도 보조 레이어는 의도적으로 제외 — bridge 주 geometry만 반영.

[refactor] 아키텍처 리뷰 권장안 #1/#2 이행 — god class 완화 + has_* 헬퍼 통합

  • 사유: 스파게티화 방지를 위해 아키텍처 리뷰에서 식별한 우선순위 1·2 리팩터 실행. egview_maker.py가 5,027줄(전체 31%)의 god class 상태이고, 각 파서마다 has_X 검출 로직이 복붙될 조짐.
  • #1 — optional_detector.py (신규): 부속 컴포넌트 존재성 검출 로직을 구조물-독립 공통 모듈로 추출.
    • ComponentSpec dataclass — 컴포넌트별(레이어 토큰·텍스트 키워드·default·신호부재 처리 방침)을 선언적으로 표현
    • ComponentReport — 검출 결과 + 진단(geom_count / text_count / matched_layers)
    • count_layer_geom(msp, tokens) / count_text_hits(msp, keywords) / detect_component(msp, spec) / detect_components(msp, specs) / summary_line(reports) 공개 API
    • gate_parser_detect_optional_components 리팩터: 89줄 → 14줄 (_get_component_specs + detect_components 호출). 검출 동작 완전 동일.
    • 다른 구조물 파서(valve_chamber / intake_tower / retaining_wall)도 동일 패턴으로 이 모듈 활용 가능 — 향후 ComponentSpec 선언만 추가하면 끝.
  • #2a — tile_downloader.py (신규): XYZ 타일 다운로드·합성 로직 추출.
    • 순수 함수 latlon_to_tile / tile_to_latlon / download_xyz_tiles(url_template, min_lat, min_lon, max_lat, max_lon, zoom, final_size, timeout_s, log_fn)
    • egview_maker._latlon_to_tile / _tile_to_latlon / _download_xyz_tiles 3개 메서드(97줄) 제거, thin wrapper 1개로 교체.
  • #2b — gemini_renderer.py (신규): Gemini(Nano Banana) 조감도 AI 호출 로직 추출.
    • 공개 API run_gemini_render(app, credential, prompt, use_vertex, location) — app 인스턴스를 첫 인자로 받아 상태(capture_image/job_logger/log/after/...)에 접근
    • Vertex AI(gemini-3.x flash/pro) / API Key(gemini-2.5) / Legacy SDK 3가지 경로 + 모델 폴백 체인 + Harness 로깅 전부 포함
    • egview_maker._run_gemini_render 264줄 body → 2줄 delegate로 축소
  • 결과:
    • egview_maker.py 5,027 → 4,668줄 (359줄, 7.1%)
    • 신규 모듈 3개 (optional_detector.py ~140줄, tile_downloader.py ~145줄, gemini_renderer.py ~250줄) — 각자 자기완결, 순환 import 없음
    • 동작 변경 0. 전체 회귀(AST 23 파일·import 18 모듈·gate 직접·템플릿 경유) 모두 통과, 이전과 mesh 수 동일(22).
  • 남은 추가 리팩터 여지 (사용자 요청 시):
    • _open_structure_review_dialog (606줄) → structure_review_dialog.py: 가장 큰 잔여 덩어리. 앱 인스턴스 전달 방식으로 추출 가능.
    • _open_layer_classifier (142줄), _open_elevation_dialog (187줄), _open_interactive_viewer (131줄) 등 대형 UI 메서드
    • create_tin_from_dxf (112줄) → tin_builder.py
    • 다른 구조물 파서 3개도 optional_detector 활용으로 통합 (valve_chamber 먼저 제안)

[feat] 수직 클러스터 + gap 패턴 기반 pier 복원 (실측 line-soup DXF 커버)

  • 파일: gate_parser.py, structure_templates.py
  • 사유: 직전 polygon_reconstructor(face enumeration)는 폐합 폴리라인 DXF에선 완벽 동작하나, 실측 DXF처럼 pier 경계가 독립 선분으로 그려진 경우 작은 부속 면만 복원되어 pier 추출 실패. 도메인 특화 heuristic으로 해결.
  • 알고리즘 (_extract_piers_from_vertical_clusters):
    1. plan 영역 수직 세그먼트(|dy| > 3·|dx|) 수집
    2. X 좌표 1D greedy 클러스터링 (tol=500mm)
    3. 각 클러스터 (x_avg, y_min, y_max, total_len) 계산, total_len ≥ max(pier_length×0.3, 3m) 인 것만 유지
    4. 핵심 insight: 연속 클러스터 간 gap 분포에서 상위 n_gates개 gap이 gate opening — parametric pier_width에 의존하지 않음
    5. gate gap 인덱스로 클러스터를 n_gates+1 개 pier 영역으로 그룹화
    6. 각 영역의 leftmost·rightmost 클러스터가 pier 좌우 경계, Y 범위는 클러스터 교집합(또는 합집합 폴백)
    7. pier bbox 폴리곤 생성, 로컬 좌표 m로 정규화
  • 3단계 추출 체인 (_extract_plan_polygons):
    1. closed_polylines — 폐합 LWPOLYLINE이 있으면 직접 사용 (이상적)
    2. face_enumeration — polygon_reconstructor로 face 열거 (tightly connected line soup)
    3. vertical_clusters — gap 패턴 (느슨한 실측 DXF, 새로 추가)
    • 각 단계에서 n_gates+1개 완전 추출되면 성공. 부분 추출은 parametric 폴백.
    • 사용된 방법을 [plan_poly] method=...로 로그에 기록.
  • template_id "spillway_gate" pass-through (structure_templates.py):
    • StructureParams ↔ GateParams 변환에서 pier_plan_polygons, plan_outline_polygon, has_service_bridge/hoist_housings/downstream_apron/water_surface 누락 → 템플릿 경유 시 Phase A/B' 결과가 빌더에 전달 안 되던 버그. params.params 딕셔너리에 추가해 pass-through.
  • 검증:
    • 실측 12995740-M40-001 & 여수로 수문1.dxf: 4/4 pier 복원, method=vertical_clusters, 22 meshes (pier 4개 + 공도교 deck/난간 2개 + 다른 부속).
    • Pier 치수: W 8.8817.24m × L 12.5724.66m (edge pier에서 outer 구조 포함으로 약간 커짐, 중간 pier는 일관). parametric default(4.85×25) 대비 실제 도면 치수 직접 반영.
    • 실측 여수로 수문2.dxf: 0/4 — 이 파일은 plan view 없이 측면단면만 있어 pier 평면 복원 원천 불가 (정상). 실제 사용은 (1/2)+(2/2) 함께 파싱이 정석.
    • 합성 폐합 DXF: method=closed_polylines, 4/4 (기존 경로 유지).
    • 템플릿 경유 (REGISTRY.get("spillway_gate").parse+build_meshes): pier_polys=4 전달 확인, 22 meshes 정상.
  • 솔직한 한계:
    • Edge pier (pier[0], pier[3])의 W가 중간 pier보다 커짐 — structure 외측 wing wall/anchor 등이 pier 영역에 포함되기 때문. 사용자 요청 시 "outer cluster 배제" 후처리 가능.
    • Pier 형상은 rectangle만 (nose 삼각부 미반영). Nose는 parametric 빌더의 _make_pier_nose로 별도 유지 고려 필요.
    • 3단계 폴백 체인은 현재 순차. 각 방법의 실패 이유(로그) 활용해 하이브리드(step별 partial 결과 병합) 개선 여지.

[feat] Line-soup polygon reconstruction 구현 (Phase B' 완성)

  • 파일: polygon_reconstructor.py (신규), gate_parser.py
  • 사유: 이전 Phase B' 작업에서 개방선으로 그려진 DXF의 pier/outline 폴리곤 복원이 부분 성공(bbox 클러스터링)에 그침. 완전 기하 복원을 위해 planar 그래프 face enumeration 구현.
  • 모듈 polygon_reconstructor.py:
    • reconstruct_polygons(segments, tol, min_area) — 선분 집합 → 폐합 face 복원
    • 알고리즘: (1) 공차 tol 내 끝점을 그리드 해싱으로 단일 vertex ID로 묶음 → (2) 무방향 인접 리스트 구성 → (3) leftmost-turn traversal로 각 방향 간선마다 "들어온 방향 기준 왼쪽 최대 꺾임"을 따라 면 순환 열거 → (4) canonical form(최소 vertex id 시작 + 정/역 사전순 최소)으로 회전/반사 중복 제거 → (5) shoelace 면적 ≥ min_area만 유지, 면적 내림차순 반환
    • _VertexStore: 3×3 그리드 셀 탐색으로 O(1) 근접 vertex 검색
    • 안전장치: max_faces 상한, deg-0 정점 처리, cul-de-sac 감지
  • 파서 통합 (gate_parser._extract_piers_from_line_soup):
    • 기존 "gate_centers_x 주변 bbox 클러스터링" 방식을 완전 교체
    • plan 영역 LINE/LWPOLYLINE 세그먼트 → reconstruct_polygons(tol=5mm) → 면적 내림차순
    • 최대 면 = outline, 외곽 면적 1.5%~45% + pier 기대 치수(폭 pier_width × 0.5~2.0, 길이 pier_length × 0.5~2.0) 범위 내 → pier 후보
    • 엄격 성공 기준: len(pier_polys) == n_gates + 1 정확 일치할 때만 True 반환, 빌더가 폴리곤 경로 사용. 부분 추출 시 parametric 폴백 유지 (일관성 우선).
  • 검증:
    • 합성 DXF 테스트 (의도적으로 폐합 LWPOLYLINE으로 그린 3-gate + 4-pier): 4개 pier 모두 정확 복원 — W=5.00m × L=25.00m 4vertex, 빌더가 폴리곤 경로로 19 meshes 생성. 알고리즘 자체는 올바름.
    • 단위 스모크 (polygon_reconstructor.__main__): 겹친 두 사각형(10×10, 3×3)에서 2개 face 정확 검출, 면적 100/9 일치.
    • 실측 DXF (SAMPLE_CAD/12995740-M40-001 등 3개): plan 영역 340 세그먼트 → 6 face 복원되나 크기 0.6~4.9m²로 pier 기대 치수(≥10m²) 미충족 → size-filter가 모두 배제 → pier_polys=0 → parametric 폴백. 회귀 동작 동일 (bridge 있음 22 meshes / 없음 19 meshes).
  • 실측 DXF가 실패하는 이유 (솔직한 진단):
    • 이 DXF들은 pier 경계를 단일 폐합 폴리라인이 아닌 여러 독립 선분으로 그림. 게다가 수평 선(gate 상/하 edge)이 pier 영역을 넘어 여러 gate 걸쳐 연속되어, pier 주위에서 폐합 cycle이 형성되지 않음.
    • 즉, 알고리즘 한계가 아니라 도면 표현 양식의 한계. 해당 DXF에서 pier 폴리곤을 얻으려면 별도 고수준 전처리(수평선 트리밍 + 격자 교차점 기반 재조합) 필요.
    • 앞으로 폐합 폴리곤으로 그려진 구조물 도면(CAD 작성 관행 변경 또는 다른 형식)에선 즉시 동작.
  • 남은 작업:
    • 실측 DXF에서도 동작하려면 "선 스윕 + 격자 정렬 기반 pier 복원" 같은 도메인 특화 heuristic 필요 (후속).
    • 같은 Phase A~D를 valve_chamber / intake_tower / retaining_wall에도 적용 (사용자 승인 대기).

[feat] Gate(수문) 구조물 Phase A~D 통합 — 도면 반영률 향상을 위한 4단계 작업

  • 파일: gate_parser.py, gate_3d_builder.py
  • 배경: 사용자 지적 — "모든 구조물이 default 값에 의존, 도면 수정이 빌드 결과에 반영 안 됨, 파서가 valve_chamber 수준이라 해도 형편없음". 대안으로 Phase A~D를 수문(gate)에 먼저 적용.

Phase A — 부속 컴포넌트 존재성 플래그 확대 (직전 공도교 fix 확장)

  • GateParams 신규 필드: has_hoist_housings: bool = True(래디얼 게이트엔 통상 동반, default 유지), has_downstream_apron: bool = True, has_water_surface: bool = True (시각 맥락, 사용자 토글 가능). has_service_bridge는 직전 구현.
  • _detect_optional_components 확장: 각 부속별 레이어 토큰(hoist/권양/winch/gantry, apron/에이프런/stilling/물받이/감세) + 텍스트 키워드 이중 검출. 헬퍼 _count_layer_geom·_count_text_hits로 규칙 통일.
  • GateBuilder.build_all에서 각 has_* 플래그로 빌드 분기 → 도면에 없는 부속물 강제 생성 원천 차단.
  • 권양기는 "신호 부재로 False 낮추는 로직"을 제거 (대부분 DXF가 별도 레이어 없이 본체 위에 그림 → false negative 방지). 사용자는 UI로 수동 조절.

Phase B' — Direct Geometry Extrusion (parametric → 실제 도면 기하)

  • 문제: 기존 _build_pierspier_width × pier_length × pier_top_el 스칼라로 박스만 생성 → 도면의 실제 기하 반영 0%.
  • 신규 필드: plan_outline_polygon: list[(x,y)] (외곽), pier_plan_polygons: list[list[(x,y)]] (각 교각). chamber-local 좌표, m 단위.
  • 2단계 추출 (_extract_plan_polygons):
    1. 1차: 폐합 LWPOLYLINE 수집(closed flag 또는 기하학적 폐합 tol=5mm) → 면적 내림차순 정렬, 최대가 outline, 외곽의 1.5%~45% 크기가 pier 후보.
    2. 2차 폴백(_extract_piers_from_line_soup): DXF가 개방선 집합이면 plan 영역 LINE/LWPOLYLINE 세그먼트 수집 → plan bbox를 outline으로 사용, pier 중심 후보(gate_centers_x 기반)마다 양쪽 끝점이 모두 pier 영역(±pier_width·1.1) 내인 세그먼트만 모아 bbox → pier 직사각 폴리곤.
  • Shoelace 면적/기하 폐합 판정 헬퍼 추가.
  • 빌더 (_build_piers): pier_plan_polygons가 **완전 추출(count == n_gates+1)**일 때만 실제 기하로 extrude, 부분 추출은 일관성 우선으로 parametric 전체 폴백. 신규 _extrude_polygon_xy(poly, z_bot, z_top)로 임의 XY 폴리곤을 프리즘으로 extrude.
  • 한계: 실측 SAMPLE_CAD(94개 LWPOLYLINE 전부 open)에선 완전 폴리곤 재구성이 어려워 대부분 parametric 폴백 유지. 외곽 bbox는 추출 성공. 완전한 line-soup → polygon 재구성(endpoint graph + cycle detection)은 후속 작업.

Phase C — VLM 피드백 루프 gate 호환 확인

  • structure_vlm_feedback은 이미 구조물 무관하게 params_to_dict()로 dataclass 직렬화 + render_meshes_topdown() 호출.
  • apply_diff_to_params의 valves/pipes 브랜치는 hasattr(params, "valves") 체크로 GateParams에선 안전 스킵 (스칼라 param_updates만 적용).
  • 검증: gate DXF → 도면 PNG(116KB) + 빌드 top-down PNG(31KB) + params JSON(4080 chars) 생성 성공. 즉시 GUI "🤖 AI 검증" 버튼에서 사용 가능.

Phase D — view_detector 통합 (y_mid 휴리스틱 교체)

  • 기존 _parse_plan_file은 CS-CONC-Spillway의 Y 범위 중앙값으로 plan/elevation 경계를 추정(휴리스틱).
  • view_detector.detect_view_regions()로 정식 view 분리(plan / front / side) → plan view bounds(m)를 mm로 역변환해 plan 영역 점 필터링에 사용. view detector 실패 시 기존 y_mid 폴백 유지.
  • 실측: 12995740-M40-001 → plan view 정확히 검출(x=[12.0,105.1]m, y=[42.6,80.1]m, W=93.1m, H=37.5m). 로그에 [view] 항목으로 기록.

전체 검증

  • AST 8 파일, import 9 모듈 모두 OK.
  • Case A (bridge 있는 원본 DXF): 22 meshes, bridge=O, hoist=O, apron=O, 수면=O, plan 영역 정확 검출, 교각 부분 추출.
  • Case B (bridge 없는 수문2): 19 meshes, bridge=X → 공도교 미생성 (직전 fix 유지).
  • 템플릿 경유(REGISTRY.get("spillway_gate")) 파싱 + 빌드 19 meshes 정상.
  • 수문1+2 조합 파싱: plan view + section view 모두 활용, ogee profile 추출 성공.

남은 작업 (사용자 승인 필요)

  • Line-soup polygon reconstruction (Phase B' 미완): endpoint graph + cycle detection으로 개방선으로 그려진 도면에서도 완전 폴리곤 복원. 성공 시 교각 4개 전부 실제 기하로 렌더.
  • valve_chamber / intake_tower / retaining_wall에 동일 Phase A~D 적용: 각 구조물 4~6시간 규모. 사용자 승인 후 순차 진행.

[refactor] 파일/클래스명 spillway → gate (여수로 ≠ 수문 혼동 해소)

  • 사유: 사용자 지적 — spillway여수로(수로) 를 뜻하지만, 현재 해당 파서/빌더가 다루는 구조물은 실제로 수문(gate) (래디얼/테인터 게이트 + 교각 + 권양기 등). 명칭이 의미를 가리고 코드 가독성을 해침.
  • 변경:
    • 파일: spillway_parser.pygate_parser.py, spillway_3d_builder.pygate_3d_builder.py (구 파일 삭제).
    • 클래스: SpillwayParamsGateParams, SpillwayParserGateParser, SpillwayBuilderGateBuilder.
    • 함수: parse_spillway_dxfparse_gate_dxf, build_spillway_meshesbuild_gate_meshes.
    • 단어 경계(\b) 기반 re.sub로 모든 import·참조 일괄 치환 (structure_templates.py 포함, 총 11개 라인).
  • 의도적으로 유지한 것:
    • template_id = "spillway_gate" (structure_v1.yaml 연동) — 템플릿 개념이 "여수로의 수문" 이라 spillway_gate 복합명이 정확. 외부 YAML·분류 키 변경 범위 최소화.
    • SpillwayGateTemplate 클래스명 (structure_templates.py) — 의미상 "여수로 수문 템플릿"이 맞음.
    • CHANGELOG 과거 기록 (spillway_parser.py 언급된 구 항목) — 역사적 기록이라 손대지 않음.
  • 검증:
    • AST 17 파일, import 22 모듈 모두 OK.
    • 실제 DXF E2E: parse_gate_dxf + GateBuilder.build_all() / build_gate_meshes / 템플릿 경유(REGISTRY.get("spillway_gate").parse(...).build_meshes(...)) 3경로 모두 성공.
    • 공도교 있는/없는 두 케이스 mesh 수 이전과 동일 (22 / 19) — 순수 이름 변경, 동작 무영향.

[fix] 수문(spillway) 빌더가 공도교(service bridge)를 도면 무관하게 default 강제 생성하던 문제

  • 파일: spillway_parser.py, spillway_3d_builder.py
  • 증상 (사용자 gate.png): 사용자가 공도교가 없는 수문 도면을 업로드해도 빌드 결과에 공도교가 항상 생성되어 수문과 시각적으로 겹치고 비현실적인 형상 출력. el_bridge_top=56.000이 default 값으로 사용됨.
  • 근본 원인:
    • SpillwayBuilder.build_all()_build_service_bridge()무조건 호출. 도면에 공도교 layer가 없어도 default 파라미터로 deck + 난간 2개를 생성.
    • SpillwayParams에 공도교 존재 여부를 표현하는 플래그가 없었음. 모든 도면이 동일하게 공도교 있음으로 가정.
  • 이는 더 큰 패턴(다른 모든 구조물에서도 부속요소가 default로 강제 생성됨)의 일부지만, 사용자 요청에 따라 이번 turn에서는 spillway만 한정 수정.
  • 변경:
    • SpillwayParamshas_service_bridge: bool = False 필드 추가 (default를 False로 두어 미검출 시 안전 — default 강제 생성 방지).
    • 신규 SpillwayParser._detect_optional_components(msp, params):
      • 부분일치 토큰 ("bridge","공도교","공도","관리도로","service road")로 layer 이름 검사
      • 매칭된 layer에 _GEOM_TYPES(LWPOLYLINE/LINE/CIRCLE/ARC/SPLINE/3DFACE/SOLID/HATCH 등) 엔티티가 1개 이상 존재하면 has_service_bridge=True
      • 텍스트 키워드는 보조 신호만 — 범례·표 안의 단순 언급으로 인한 false-positive 방지
      • 검출 결과를 raw_text_annotations[detect] ... 항목으로 남겨 디버깅 용이
    • _parse_plan_file 시작부에서 호출.
    • SpillwayBuilder.build_all()if getattr(params, "has_service_bridge", False) 시에만 _build_service_bridge() 호출 (getattr 사용으로 구버전 params와 하위호환).
    • SpillwayParams.summary()공도교: O/X 표시.
  • 검증 (실제 SAMPLE_CAD):
    • 케이스 A (12995740-M40-001 — CS-CONC-Bridge 레이어 존재): has_service_bridge=True, 22 meshes (공도교 deck 1 + 난간 2 포함)
    • 케이스 B (여수로 수문2.dxf — bridge 레이어 없음): has_service_bridge=False, 19 meshes (공도교 누락 정상)
    • mesh diff = 3 (deck 1 + 난간 2) 정확히 일치.
  • 남은 일반 패턴 작업 (다른 turn): 동일한 부속 요소 default 강제 생성 안티패턴이 valve_chamber(hatch/stairs/ground), intake_tower(stairs/walkway), retaining_wall(coping/drainage 등)에도 존재. 사용자 승인 후 같은 패턴(has_X 플래그 + 레이어 검출 + 빌드 분기)으로 일괄 적용 예정.

[feat] 구조물 빌드 ↔ 원본 도면 VLM 피드백 루프 (Gemini Vision)

  • 파일: structure_vlm_feedback.py (신규), egview_maker.py
  • 사유: 구조물 파서/빌더가 80% 정도는 도면을 반영하지만, 누락(예: Y-split 도수관) ·오차(직경)·잉여 등을 자동으로 잡을 결정적 알고리즘이 어려움. 사용자 합의: GAN은 데이터·시간·재현성에서 비현실 → VLM 피드백 루프가 PoC에 가장 적합. 새 SDK·키·결제 0개 (기존 google.genai + gcp-key.json 그대로 재사용).
  • 모듈 (structure_vlm_feedback.py):
    • render_dxf_to_png(dxf_paths, output, size, dpi)ezdxf.addons.drawing.matplotlib 백엔드로 도면 평면 렌더.
    • render_meshes_topdown(meshes, output, size)pv.Plotter(off_screen=True) + view_xy() + 평행 투영으로 빌드 결과 top-down 캡처.
    • params_to_dict(params) — dataclass/np 타입을 JSON-safe dict로 정규화.
    • request_structure_diff(client, drawing_png, render_png, params_dict, structure_type, model) — 두 이미지 + 파라미터 JSON을 Gemini에 전달, JSON 스키마(summary/match_score/param_updates/valves_missing/valves_incorrect/pipes_missing/pipes_incorrect/excess_notes)로 응답 강제(response_mime_type=application/json, temperature=0.1). 코드블록 제거 + 부분 JSON 복구 fallback.
    • apply_diff_to_params(params, diff, selections) — 사용자가 체크한 항목만 in-place 적용. 스칼라 필드는 setattr + 타입 변환, valves/pipes는 Valve()/Pipe() 새 인스턴스 append.
    • build_genai_client(project, location, use_vertex, api_key) — Vertex AI 우선, API Key 폴백. egview_maker 인증 경로와 동일.
    • run_feedback_once(...) — 위 단계를 한 번에 실행하는 편의 함수. cache/vlm/{structure_type}/에 비교 이미지 저장.
  • UI 통합 (egview_maker.py):
    • _open_structure_review_dialog의 하단 버튼행에 "🤖 AI 검증" (주황) 추가. 클릭 시 백그라운드 thread에서 run_feedback_once 실행 (UI 비차단).
    • 응답 수신 시 모달 toplevel _show_diff_dialog 자동 표시: match_score(색 코딩 ≥0.85 녹/≥0.6 주황/그 외 빨강), summary, 카테고리별 항목 체크박스(파라미터 업데이트 / 누락 밸브 / 누락 관로) + 참고용(잉여·필드 수정 제안).
    • "✓ 선택 항목 적용 + 재빌드" 버튼: 체크된 것만 적용 → 엔트리 위젯 갱신 → tpl.build_meshes(state['params']) 자동 호출 → status_var 업데이트.
    • 인증 우선순위: gcp-key.json의 project_id → GCP_PROJECT_ID 환경변수 → 사이드바 입력 → API Key. 모두 없으면 명확한 에러 다이얼로그.
  • 모델/비용:
    • 기본 gemini-2.5-flash (1회 호출 약 0.05~0.2센트). 필요 시 gemini-2.5-pro 또는 gemini-3-pro로 모듈 호출부에서 model 파라미터 변경.
    • cache/vlm/{type}/drawing.png + render_topdown.png 캐시(매 호출 덮어쓰기, 디버그용).
  • 새 의존성: 0개. google.genai, ezdxf.addons.drawing, matplotlib, pyvista, numpy 모두 기존 사용 중.
  • 검증:
    • AST/import OK (모듈 + egview_maker, 의존 21+개).
    • 렌더링 E2E: 사연댐 제수변실 DXF → 51KB 도면 PNG / 87 메시 빌드 → 21KB top-down PNG 생성 확인 (이미지 시각 비교에서 도면의 4 밸브·Y분기·우측 D1200 vs 빌드의 빈 챔버·우측 파이프만 차이가 명확히 드러나 VLM이 식별 가능한 형태).
    • 프롬프트 합성 5175자, params JSON 2.8KB.
    • apply_diff_to_params 합성 diff(chamber_width 14→13.5, valve M-999 추가) → applied=2, errors=0, valves 6→7 정상.
  • PoC 한계 / 향후 개선:
    • _set_by_path는 평면 필드만 지원 (예: valves[0].diameter 형태는 미지원). 차후 dot/index 경로 파서 추가.
    • 한 번에 1 cycle만 — 자동 수렴 루프(N회 반복 + match_score 임계) 미구현. 사용자가 필요하면 검증 버튼 다시 누르면 됨.
    • 도면 PNG 렌더가 ezdxf 한계로 일부 형식 코드(MTEXT 색상 등) 단순화될 수 있음. 시각적 충실도가 분류에 영향 주면 dpi/size 상향 또는 PDF 경유 옵션 검토.
    • Gemini가 같은 좌표계 단위(m vs mm)를 정확히 따르도록 프롬프트에 강조했으나 응답에 단위 혼동 가능성 → apply_diff_to_params에서 명백한 outlier 자동 거부 로직(예: 직경이 chamber보다 큼) 후속 추가 여지.

2026-04-19

[fix] 제수변실 파서: TEXT proximity grouping (라벨 분리·MTEXT 멀티라인·schedule 보강) + dedup

  • 파일: valve_chamber_parser.py
  • 사유: 직전 수정에서 MLEADER 기반 추출은 이 DXF엔 잘 동작했지만, 다른 도면에서 라벨이 분리된 경우(예: "도수관" 단독 TEXT + "M-301" 단독 TEXT가 인접 위치에 있는 케이스, 또는 MTEXT가 {도수관\\PM-301} 형태로 멀티라인)는 여전히 분류 누락. 사용자 지적: "Fix valve/pipe classification (text proximity grouping)은 왜 안해?"
  • 변경: _extract_valves_and_pipes를 4가지 라벨 분포 케이스를 모두 다루도록 재작성:
    1. 단일 TEXT ("M-302 송수관 주밸브(1)") — 기존 처리
    2. MTEXT 멀티라인 ("{도수관\PM-301}") — _clean_mtext로 포맷 코드 strip + \\P→줄바꿈 후 re.search로 매칭
    3. 인접 TEXT 분리 ("M-301" + "도수관"이 별개 엔티티) — proximity_radius_m=5m 내 다른 엔티티 텍스트 흡수
    4. 라벨 + schedule 표 ("M-301 도수관" 텍스트 인근에 별도 표에서 "강재도관 D2000mm"가 발견됨) — proximity 보강으로 직경 추출
  • 분류 정책: own_txt 우선, proximity 보강은 own에 분류·직경이 부족할 때만 실행 (그렇지 않으면 schedule의 다른 M-NNN 항목 텍스트를 흡수해 M-301 도수관이 "버터플라이밸브"로 오분류되는 회귀가 발생).
  • dedup: 같은 M-NNN이 label column TEXT + 도면 내 MTEXT에 동시 등장해 Pipe/Valve 객체가 중복 생성되던 케이스를 _dedupe(default_dia)로 해결 — 같은 이름이면 직경이 default가 아닌 것을 유지.
  • 검증 (동일 DXF):
    • M-301 도수관 직경 800mm(default) → 2000mm (schedule 보강 성공)
    • M-301 중복 2개 → 1개 (dedup)
    • 6 valves / 4 pipes 정확 분류, 87 PolyData 빌더 정상.

[fix] 제수변실 파서/빌더 — chamber 크기·밸브 위치·관로 모두 어긋나 도면 내용이 거의 반영되지 않던 문제

  • 파일: valve_chamber_parser.py, valve_chamber_3d_builder.py
  • 증상: 사용자가 12996710-M43-002 신설 제수변실 설비 배치도.dxf를 업로드해 미리보기 빌드했을 때, 4개 송수관 밸브(M-302~M-305)·출력 송수관(천상정수장/대암계통 D1,200)·하천유지용수 D80이 전부 사라지고 chamber도 27m × 9m(기본값)로 표시. 사용자 지적: "도수관이 밸브로 취급되어서 도면이 하나도 반영 안 된 것 같아".
  • 근본 원인:
    1. Chamber 크기: _parse_single에서 chamber_width = max(default 27.0, plan_view.width) → 평면도(14m)보다 큰 default가 항상 이김.
    2. 밸브/관로 위치: 라벨 TEXT의 insert 좌표는 도면 라벨 컬럼(차트 옆)에 있어 실제 chamber 내부 위치가 아님. MLEADER(안내선) 끝점이 진짜 위치인데 파서가 MLEADER를 전혀 보지 않음.
    3. 출력 관로 누락: "{천상정수장 D1,200}" "{대암계통 D1,200}" "{하천유지용수 D80}" 등 destination-only 라벨(M-NNN 없음)을 파이프로 인식하지 않음.
    4. 3D 빌더가 params.pipes 무시: _build_main_conduit은 항상 chamber를 X축 관통하는 대형 파이프를 그렸고, _build_transmission_pipesparams.pipes를 보지 않고 valves에서 임시로 Y축 파이프를 만들어 도면과 무관한 결과 생성.
    5. 직경 정규식 버그: "D1,200"의 천 단위 콤마 때문에 \d{2,4} 매칭 실패 → 800mm 폴백.
    6. 밸브 방향 고정: 모든 valves를 Y축 정렬로 그려, X축 흐름 chamber에서 방향 어긋남.
  • 변경 (parser):
    • _clean_mtext() / _mleader_endpoint() / _mleader_text() 헬퍼 추가 — MTEXT 포맷 코드(\C4;/\P 등) 정리, MLEADER 끝점/본문 추출.
    • 신규 _extract_from_mleaders(msp, plan_view, scale, params): 16개 MLEADER 순회 → 끝점이 plan_view 내부면 chamber-local 좌표로 변환 → 텍스트에서 M-NNN/"밸브"/"도수관"/"송수관"/destination+D 분류 → Valve/Pipe 객체 생성. 출력 파이프는 leader 끝점에서 chamber 외부로 자동 연장(local_x 부호로 ±X, |y|>|x|이면 ±Y).
    • _parse_single에서 chamber 크기를 = plan_view.width (default와의 max 제거).
    • MLEADER 결과를 우선, M-NNN 중복 시 텍스트 폴백 보조.
    • 직경 정규식: (?:Φ|%%[cC]|\bD[\s=:]*)(\d[\d,.]*) — D 뒤에 천 단위 콤마/마침표 모두 흡수, val≥10이면 mm 가정.
    • _finalize: center가 (0,0)인 valves만 균등 배치, start/end가 (0,0,0)인 pipes만 폴백 (이미 위치 잡힌 항목은 보존).
  • 변경 (builder):
    • _build_main_conduit: params.pipes에 M-301/도수관이 있으면 스킵(이중 빌드 방지).
    • _build_transmission_pipes: valves에서 파생 → params.pipes 직접 순회로 교체. 각 pipe의 명시 start/end/diameter 사용. 대구경(≥0.3m)에 양 끝 플랜지.
    • 밸브 방향: 가장 가까운 pipe의 (end-start) 방향으로 flow_dir 계산 → 버터플라이 원통 축, 게이트 박스 가로/세로 방향 자동 결정.
  • 검증 (E2E parser):
    • chamber: 14.0 × 9.0m (이전 27 × 9 fallback)
    • 밸브: M-303 (-3.71,-2.20) D500 / M-302 (-2.54,-2.27) D500 / M-305 (-3.71,+2.20) D500 / M-304 (-2.54,+2.27) D500 / M-306 (0,0) [side view라 폴백] — 도면의 좌측 2×2 격자 정확.
    • 관로: 천상정수장 D1,200 (3.95,+2.2)→(22.0,+2.2), 대암계통 D1,200 (3.95,-2.2)→(22.0,-2.2), 하천유지용수 D80 (3.64,0.01)→(22.0,0.01), M-301 도수관 (-15,0)→(-7,0).
    • 빌더: 84 PolyData 메시 (이전 < 20). AST/import 모두 OK.
  • 남아있는 한계:
    • M-301 도수관 Y-split(좌측 외부에서 1개 → chamber 진입 직전 2개 분기) 시각화 미구현. 현재는 단일 직선 진입선. 필요하면 dxf_geometry로 좌측 영역의 LINE 다발을 뽑아 분기점을 추정해 추후 추가.
    • M-306(하천유지용수 밸브)은 plan view에 자체 leader 없이 side view에만 표시되어 폴백 위치 (0,0). 같은 D80 pipe(MLEADER #1)의 중점에 자동 배치하도록 후속 개선 가능.

2026-04-18

[feat] DEM 링을 TIN 컨벡스 헐에 스냅 + 뷰 버퍼(%) 사용자 입력 노출

  • 파일: dem_extender.py, egview_maker.py
  • 사유:
    1. 직전 구현은 링의 inner 경계가 사각 bbox여서 TIN 실제 형상(등고선 기반 불규칙 다각형)과 어긋남 → 낮은 앵글에서 gap/찢김·T-vertex 아티팩트 발생 가능.
    2. Step 2(위성) 버퍼가 하드코드 5%, Step 3(제어맵) 초기 카메라 거리가 하드코드 1.5x여서 사용자가 조감도 범위를 조절할 수 없음.
  • 변경 A — Hull snap (dem_extender.py):
    • 추가: _compute_tin_hull_projected() (scipy.spatial.ConvexHull로 TIN XY 헐 + 각 정점의 TIN Z 반환), _densify_polygon() (엣지 세분화), _generate_ring_points_hull() (hull 바깥쪽만 격자로 채우고 세분화된 hull 경계 점을 공유 정점으로 추가).
    • build_extended_terrain_ringuse_hull_boundary=True 파라미터(기본 True). 활성 시:
      • 링 inner 경계 = TIN 컨벡스 헐 (불규칙 다각형).
      • 헐 경계 점 Z는 DEM이 아니라 TIN 원본 정점 Z의 선형 보간으로 오버라이드 → seam에서 Z 완전 일치(gap·T-vertex 0).
      • 내부 삼각형 제거를 bbox contains → matplotlib.path.Path hull contains로 교체.
    • Hull 계산 실패 시 자동으로 bbox 경계로 폴백(ConvexHull이 collinear/<3 pts 예외).
    • DemExtendResult.infoboundary=hull(N정점) 또는 boundary=bbox 명시.
  • 변경 B — 뷰 버퍼(%) UI + Step 2/3 적용 (egview_maker.py):
    • 사이드바 row 24 서브 프레임(dem_frame)을 확장: (a) "뷰 버퍼 (%) [Step2/3]" 입력 buffer_percent_var (기본 5), (b) 기존 "지형 확장 (DEM)" 체크 + DEM 버퍼(m) 유지.
    • Step 2 btn_draping_callback: 하드코드 5% 삭제 → buffer_percent_var로 계산. DEM 확장 ON 시 위성 타일 BBOX는 max(뷰 버퍼 %, DEM m 버퍼) 적용(텍스처 커버리지와 DEM 링 extent 양쪽 충족).
    • Step 3 _open_interactive_viewer: 초기 카메라 거리 diag * 1.5diag * (1.3 + buf_pct/100) (기본 1.35x, buf_pct=20%면 1.5x). 인터랙티브 뷰에서 사용자가 계속 자유 조정 가능.
  • 검증:
    • AST OK (두 파일), 의존 21 모듈 importlib 성공.
    • 유닛: 100-pt 합성 TIN → _compute_tin_hull_projected 13 정점, _generate_ring_points_hull가 외곽 격자에서 hull 내부 점 0개(strict), 경계 정점 42 포함 정상.
    • E2E (사연댐 부근 WGS84 → EPSG:5187, 합성 TIN 300 pts): use_hull_boundary=True → pts=1818/tris=3394/boundary=hull(16정점), False → pts=1744/tris=3234/boundary=bbox. 수직 datum 자동 보정 offset=+124.35m(198점 비교) 적용 확인. AWS terrarium 2 타일 캐시 히트.
  • 트레이드오프:
    • Hull 기반은 TIN이 컨벡스에 가까울 때 이상적. 매우 오목한(U자 협곡, 긴 강변) TIN이면 헐이 실제 커버리지보다 커서 빈 영역이 링에 포함될 수 있음 → 장기적으로 alpha shape/실제 boundary edge 추출로 업그레이드 여지.
    • 뷰 버퍼 %는 Step 3에서 초기 카메라 거리에만 영향(인터랙티브 뷰에서 여전히 휠로 재조정 가능). 고정 프레이밍이 필요하면 스냅샷 기능 추가 필요.

[feat] DXF TIN 외곽을 실제 DEM으로 확장 (AI 환각이 아닌 사실 기반 배경 지형)

  • 파일: dem_extender.py (신규), egview_maker.py
  • 배경/사유:
    • 조감도에서 DXF 등고선 범위를 벗어나면 지형이 절벽처럼 끊기고 뒤쪽은 하늘색만 남음 → AI 프롬프트로 메워왔으나 AI 상상이라 매 실행 결과가 바뀜.
    • 사용자 요구: "내가 지형을 생성한 범위 그 이외도 실제 지형을 반영하고 싶어." 위성 영상은 버퍼로 확장하고, 그 확장 영역에 실제 DEM Z값을 붙여 3차원 배경 지형을 사실 기반으로 완성.
  • 설계:
    • 외곽 도넛(ring) 메시를 투영 CRS(예: EPSG:5187)에서 격자로 생성, 내부(DXF bbox)는 비워둠.
    • AWS Open Terrain Tiles (terrarium PNG, 무료·API 키 불필요, 글로벌 ~30m)를 기본 소스로 사용. URL https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png. 디코딩 elev = (R*256 + G + B/256) - 32768.
    • 로컬 GeoTIFF 우선 오버라이드: cache/dem/local.tif가 있고 rasterio 설치돼 있으면 우선 사용 (예: NGII 5m DEM 수동 배치 시 정확도 ↑).
    • Seam 정합:
      1. 수직 datum 자동 보정: inner 경계 근처 링 점의 DEM Z vs 최근접 TIN Z 중앙값 차이(offset)를 모든 DEM Z에서 차감 → 댐 사업지의 공사 전후 Z 차이도 자동 흡수.
      2. Feathering: inner 경계로부터 feather_m(기본 80m) 이내 점은 TIN 최근접 Z와 선형 블렌드해 seam 매끄럽게.
    • 위성 텍스처 정합: DEM 확장 활성 시 Step 2의 위성 타일 버퍼를 5%가 아닌 DEM 버퍼(dem_buffer_m)와 동일하게 확장. TIN+외곽 메시에 결합 bbox 기준 UV 매핑을 적용해 위성 픽셀이 양쪽 메시에 일관 정렬.
    • 캐시: BBOX+zoom 해시로 cache/dem/terrarium_{hash}.png 저장 → 재실행 시 네트워크 호출 없음.
  • UI 변경 (egview_maker.py):
    • 사이드바 "와이어프레임 보기"(row 23) 아래 row 24에 서브 프레임 추가: 체크박스 "지형 확장 (DEM)" + 버퍼 거리 입력 dem_buffer_var (기본 1000m, 단위 m).
    • Step 1(TIN 재생성) 시 total_mesh / tin_extension_mesh / tin_extension_textured 캐시 무효화.
    • Step 2 btn_draping_callback: dem_extend_var 활성 시 build_extended_terrain_ring 호출 → self.tin_extension_mesh 저장. 위성 타일 BBOX도 동일 버퍼로 확장.
    • show_3d_preview: 확장 메시가 있으면 플로터에 추가 렌더(텍스처 모드=위성, 비텍스처 모드=Elevation 컬러맵, 외곽은 scalar bar 숨김·show_edges=False).
  • 새 의존성: 없음 (기존 requests / PIL / numpy / pyproj / pyvista / scipy만 사용). rasterio는 로컬 GeoTIFF 옵션에서만 필요(선택).
  • 검증:
    • AST 파싱 OK (egview_maker.py, dem_extender.py).
    • 의존 모듈 21개 importlib.import_module 모두 성공.
    • 유닛 스모크: _generate_ring_points(내부 제외 정상), _sample_grid_bilinear(3x3 예상값 일치), _terrarium_decode(128,0,0→0.0 / 200,0,0→18432.0) 정상.
    • E2E 네트워크 테스트: 사연댐 부근 WGS84(129.10, 35.54) 중심 1×1km, 버퍼 500m로 실행 → AWS terrarium 2×2 타일 다운로드 성공(118KB), 링 메시 생성(pts=2364, tris=4000), 캐시 파일 저장 확인.
  • 트레이드오프/주의:
    • 기본 소스(AWS terrarium/SRTM 기반)는 수평 ~30m·수직 ~10m 정확도 → 먼 배경 문맥용으로는 충분하나 DXF 5m 등고선만큼 상세하지 않음. 더 정확한 한국 5m/1m DEM이 필요하면 NGII에서 받아 cache/dem/local.tif로 배치(rasterio 설치 필요).
    • 첫 실행만 네트워크 사용(수 MB), 이후 캐시 재사용으로 런타임 부하 최소.

[chore] egview_maker.py 실행에 불필요한 파일/폴더를 _unused/로 분리

  • 대상: 프로젝트 루트 정리
  • 사유: egview_maker.py 진입점 및 전이 의존성과 무관한 파일이 루트에 누적되어 탐색/유지보수 난이도가 올라감. 사용자 요청(“조감도까지 뽑는데 필요 없는 파일들 식별해서 별도 폴더로 이동”).
  • 방법: egview_maker.py의 직접/전이 import(구조물 파서·빌더, harness, geo_referencing 등)와 런타임 로드 자산(gcp-key.json, structure_types/, prompt_templates/)만 루트에 유지. 나머지는 _unused/로 이동.
  • 이동 대상:
    • 독립 스크립트: nano_banana2.py, structure_ui.py, test_gate_render.py
    • 백업 폴더: REF_BY_SEOK/(harness/ 중복본)
    • 런타임 생성물(이미지/로그/DB): capture_for_ai.png, capture_textured.png, depth_map.png, lineart_map.png, guide_composite.png, satellite_temp.png, rendered_birdseye.png, egview_diagnostic.log, egview_harness.log, egview_jobs.db, Build_log.txt
    • 보조 파일/폴더: ai_studio_prompt.txt, install.cmd, scratch/, SCREENSHOT_lOG/, 지형도 베이스맵/
  • 잔존(루트 유지): egview_maker.py, detail_parser.py, dxf_geometry.py, filename_classifier.py, geo_referencing.py, structure_placement.py, structure_templates.py, view_detector.py, view_reconstructor.py, spillway_{parser,3d_builder}.py, intake_tower_{parser,3d_builder}.py, valve_chamber_{parser,3d_builder}.py, retaining_wall_{parser,3d_builder}.py, harness/, structure_types/, prompt_templates/, gcp-key.json, CHANGELOG.md, __pycache__/
  • 미이동 (핸들 점유): SAMPLE_CAD/ — 다른 프로세스(파일 탐색기/CAD 뷰어 등)가 내부 파일 핸들을 잡고 있어 이동 실패. 해당 프로그램 종료 후 수동으로 _unused/로 옮기면 됨.
  • 검증: egview_maker.py AST 파싱 OK, 직접/전이 의존 모듈 20개 importlib.import_module 성공(OK) 확인.

2026-04-17

[fix] AI 렌더링(Step 4)이 "GCP Project ID 필요" 오류로 실행 불가

  • 파일: egview_maker.py
  • 원인: gcp-key.json 서비스 계정 키가 프로젝트 루트에 있는데도 코드에서 이를 사용하지 않고 gcloud auth application-default login + 수동 GCP Project ID 입력을 기대. 또한 Gemini 3.x 이미지 모델은 location="global" 전용인데 기본값이 us-central1, 모델 목록도 2.x 계열만 있어서 최신 Nano Banana 2 미사용.
  • 변경:
    • EGViewApp.__init__에서 gcp-key.json 자동 로드: GOOGLE_APPLICATION_CREDENTIALS 환경변수 설정 + project_id 추출해 self._gcp_key_project_id에 저장.
    • GCP Project ID 기본값을 self._gcp_key_project_id → env GCP_PROJECT_ID 순으로 폴백.
    • vertex_location 기본값을 us-central1global로 변경.
    • btn_ai_render_callback에서 key가 비어도 _gcp_key_project_id로 자동 채움.
    • _run_gemini_render에서 Vertex 모델 폴백 체인을 [gemini-3.1-flash-image-preview@global, gemini-3-pro-image-preview@global, gemini-2.5-flash-image@us-central1, ...] 순으로 구성. 모델별 location이 다르면 client 재생성.
    • 인증 실패 메시지를 서비스 계정 기반으로 업데이트 (aiplatform.user 권한 확인 안내).
    • response_modalities=["IMAGE"]만 사용 (사용자 지정).
  • 검증: gcp-key.json(project_id=gen-lang-client-0637559466)으로 실제 Vertex AI 호출 성공 — gemini-3.1-flash-image-preview → 1024×1024, gemini-3-pro-image-preview → 1408×768 이미지 생성 확인.

[fix] 수문(spillway) 3D 메쉬의 앞뒤 방향이 180° 뒤집히는 버그

  • 파일: structure_placement.py, spillway_parser.py, egview_maker.py
  • 원인: fit_meshes_to_quad가 절대 각도(atan2(tin_edge01))로 회전하고, 사용자 picks는 시계방향(CW)인데 회전 행렬은 수학적 CCW 컨벤션. 결과적으로 mesh +Y(builder 하류)가 quad의 edge 3→0(사용자 상류) 방향으로 매핑되어 180° 뒤집힘.
  • 변경:
    • fit_meshes_to_quaddetail_quad_pts, plan_frame_angle_deg, flip_y_for_cw_quad=True 파라미터 추가.
    • 회전 계산식: q_angle = tin_angle - detail_angle - plan_frame_angle_rad (상대 회전).
    • 회전 전 mesh Y 반전으로 CW picks 컨벤션 보정.
    • SpillwayParams.plan_frame_angle_deg 필드 추가, _parse_plan_file에서 plan 영역 점들의 PCA 주축각 자동 계산 (-90..+90 정규화).
    • egview_maker._add_template_structures_to_plotter에서 tr.ref_planparams.plan_frame_angle_deg 전달.
  • 검증: 시뮬레이션 — BEFORE: mesh upstream → TIN 하단(flipped); AFTER: mesh upstream → TIN 상단(user 의도 일치). 기울어진 detail(30°) + TIN(90°)에서도 mesh upstream이 TIN top에 정확히 안착.

[fix] 굴착 TIN이 부자연스럽고 구조물 바닥이 TIN을 관통하는 문제

  • 파일: egview_maker.py, structure_placement.py
  • 원인(1): _excavate_tin_for_structures가 AABB + 5% padding으로 축정렬 사각형을 하드 drop(-= exc_depth) → 수직 절벽, 경사 원지반이 통째로 내려가 굴착 바닥이 여전히 경사짐. 원인(2): 경사진 floor + 평면 구조물 바닥 → 낮은 코너에서 구조물이 TIN 관통(error6/7.png).
  • 변경:
    • _excavate_tin_for_structures 전면 재작성.
      • 폴리곤: placement_transform.ref_tin 4점 quad 우선 (AABB 폴백) — 회전/실제 footprint 반영.
      • 평탄 pad: pad_z = median(원Z 내부) - exc_depth로 폴리곤 내부 통일 → 구조물 바닥(평면) ↔ TIN(평면) 정합.
      • smoothstep 전이: transition_w = max(exc_depth*1.5, 2.0)m 구간에서 t²(3-2t) blend → 수직 절벽 제거.
      • Delaunay 재계산: pad+전이 영역에 격자 densification(간격 poly_size/18) 후 전체 Delaunay 재구성 → 조밀한 삼각망, 매끈한 사면.
    • _signed_distance_to_polygon 헬퍼 함수 추가 (egview_maker.py 상단).
    • _get_structure_footprint_polygon 헬퍼 메서드 추가 (ref_tin / AABB 분기, 레거시 world→local 변환).
    • fit_meshes_to_quad/apply_placementpad_surface_z, embed_offset=0.02 파라미터 추가. 굴착 구조물은 pad Z 직접 사용, dz = z_surface - aggregate_z_min - embed_offset로 구조물 바닥을 TIN 표면보다 2cm 아래에 위치 → 아래→위 시점에서 TIN이 underside 가림.
    • info["_excavation_pad_z"]에 pad Z 저장 후 placement 단계로 전달.
  • 검증: 합성 지형(50x50, ±0.35m 경사)에서 pad Z std=0 (완전 평탄), 경계 smoothstep 정상, Delaunay 재구성 성공.

[fix] Geo-Referencing 위치 설정 창에 계획평면도(TIN DXF)가 표시되지 않는 문제

  • 파일: geo_referencing.py
  • 원인: extract_tin_shapesextract_structural_geometrydetect_unit_scale 자동감지가 KATEC 절대좌표(X=217000대)를 mm로 오판×0.001 스케일. 반면 create_tin_from_dxf는 raw 좌표를 그대로(m로) 사용 → 두 좌표계가 1000배 차이. shift_origin=self.origin 차감 후 shape가 (-217308, -330576) 근처로 튕겨 axis 밖에 그려짐 = 빈 캔버스.
  • 변경:
    • extract_tin_shapes에서 extract_structural_geometry(..., unit_override="m") 고정.
    • 자동감지 폴백 버그(diag 500~2000 구간에서 분기 미스로 기본값 "mm" 리턴)는 detect_unit_scale에 남아있으나 TIN 경로는 우회.
  • 검증: 수정 후 shape 99.6%(3353/3365)가 TIN 로컬 바운드 내 정렬, 나머지 12개는 DXF 내 원격 엔티티(axis override로 화면에 영향 없음).

2026-04-17 이전 (히스토리 미기록 구간)

초기 개발 이력은 본 CHANGELOG 도입 이전이라 직접 기록이 없습니다. 주요 컴포넌트와 당시 구조는 다음과 같습니다 (Build_log.txt의 사용자 요청 흐름과 현재 코드베이스 스냅샷으로부터 재구성):

  • 1차(초기): TIN 생성(create_tin_from_dxf) + 위성 타일 draping(btn_draping_callback) 기반의 기본 지형 뷰어.
  • 2차: 구조물 상세 DXF 업로드 + 템플릿 기반 3D 빌더(spillway_3d_builder, intake_tower_3d_builder, valve_chamber_3d_builder, retaining_wall_3d_builder) 도입.
  • 3차: 미리보기→위치설정(Geo-Referencing 4점 매칭)→확정 3단계 구조물 검토 프로세스(_open_structure_review_dialog). Umeyama 2D similarity로 PlacementTransform 산출.
  • 4차: 구조물 TIN 반영 파이프라인(_excavate_tin_for_structures 굴착 + _add_template_structures_to_plotter 최종 배치).

이후 수정부터는 본 CHANGELOG에 모두 기록됩니다.