Files
s-canvas/CURVE_SMOOTHING_PLAN.md
HYUNJUNGLEE e9cc6bfcf4 Phase 0 of expert feedback (#1~#11): infrastructure + design + 1차 fixes
Implementations (즉시 동작):
- #1 crash logging: harness/crash_logger.py (sys.excepthook + threading +
  faulthandler, 회전 파일 logs/scanvas.log). main 진입점 통합.
- #2 smooth curves (1차): gate_3d_builder ogee profile를 arc-length parametric
  CubicSpline로 4× densify (8pt→32pt, 36→132 cells, 60 FPS 안전).
- #3 TIN colormap: matplotlib "terrain"의 파란색 범위 제거 → 짙은갈색→황토→
  모래→능선 LinearSegmentedColormap. 9 사이트 교체. 회귀 테스트 추가.
- #5 uv: pyproject.toml + UV_GUIDE.md. base/[py313]/[dev]/[build] extras + hatchling.
- #6,#7,#8 dev cycle infra: .pre-commit-config.yaml (ruff+secrets+위생),
  .gitea/workflows/ci.yml (Py3.11+3.13 matrix), tests/test_regressions.py
  (18 회귀 테스트, iter=1~7 fix 박제), CONTRIBUTING.md (Red→Green 알고리즘).

Design docs (다음 세션 마이그레이션 청사진):
- #4 UI/UX 전면 수정: UI_REDESIGN_PLAN.md (12 popup→1 inspector, vtkTkRenderWidget
  embedding 게이트, 4 phase × 7 sessions).
- #10 Core/Plugin: ARCHITECTURE_PLAN.md (Core 14 / Plugin 7 구조물 + 2 렌더 + 1 QA,
  STRUCTURE_REGISTRY 확장, manifest 기반 디스커버리).
- #11 perf hotspots: PERFORMANCE_BASELINE.md (19 핫스팟, P1: 타일 직렬DL 5~30s,
  캡처 직렬 4.5~15s, numpy 벡터화 가능 Python loops, 텍스처 4회 반복read).

Behavior preservation: ruff 0 errors, pytest 17 passed/1 skipped(bpy),
import 33/33 OK on Py3.13.13.

Item #2 P2/P3 곡선, #4 UI 마이그레이션, #10 Phase 1 추출, #11 P1 최적화는 차기 세션.

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

201 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Curve Smoothing 전략 (Phase 0 — 분석 + 1차 패치)
작성: pyvista-renderer subagent
배경: 사용자 피드백 #2 — "곡선은 float 형태이므로, 매끄럽지 않게 보일 수 밖에 없으니, 매끄럽고 우수한 품질을 위한 고민 필요"
문제: ogee 프로파일·radial gate 스킨·실린더 등 모든 곡선이 polyline/저해상도 디스크리션으로 빌드되어 줌인 시 직선 분절(faceting)로 보임.
---
## 1. 거친 곡선 카탈로그
| 부위 | 파일:라인 | 현재 점/단편 개수 | 시각 품질 (1-5) | 우선순위 |
|---|---|---|---|---|
| **Spillway ogee 프로파일** | `gate_3d_builder.py:78` (`_build_spillway_body`) | DXF 추출 5~50점, 기본값 8점 | **2** (정점에서 꺾임 명확) | **P1** ← 1차 패치 |
| **Radial gate skin (Tainter)** | `gate_3d_builder.py:428` (`_make_radial_skin`, `n_circ=16`) | 17 ribs × 2 = 34점 | 3 (옆면 폴리곤 가시) | P2 |
| **Pier nose (삼각 물가르기)** | `gate_3d_builder.py:307` (`_make_pier_nose`) | 평면 삼각형 1개 | 4 (의도된 sharp edge) | P4 (변경 불필요) |
| **Gate arm (tube)** | `gate_3d_builder.py:480` (`tube(n_sides=8)`) | 8각형 단면 | 3 | P3 |
| **Intake-tower jack/crane 실린더** | `intake_tower_3d_builder.py:191,302` (`Cylinder(resolution=16)`) | 16각형 | 3 | P3 |
| **Valve/handle/flange 실린더** | `valve_chamber_3d_builder.py:200,221,251,265,302,365,378` (`resolution=8~20`) | 8~20각형 | 2~3 (handle 16, flange 16, weep 12, anchor 8 — anchor가 특히 거침) | P2 |
| **Anchor bar (옹벽)** | `retaining_wall_3d_builder.py:212` (`Cylinder(resolution=8)`) | 8각형 (수십~수백 개) | 2 (다수 객체라 누적 거침) | P2 |
| **Weep hole** | `retaining_wall_3d_builder.py:302` (`resolution=12`) | 12각형 | 3 | P3 |
| **Backfill 경사면 (옹벽)** | `retaining_wall_3d_builder.py:_build_backfill` | 8 vertex 박스 | 4 (경사 자체 OK, normal 이슈 없음) | P4 |
| **Polygon reconstructor 외곽선** | `polygon_reconstructor.py` 전반 | 원본 LINE 그대로 | 3 (사용처에 따라 다름) | P3 |
**핵심 인사이트**:
- "곡선이 거침"은 **두 종류**로 나뉜다:
1. **Sweep 단면 곡선** (ogee 프로파일) — 점 개수가 적어 extrude 시 단면 자체가 각짐 → **Spline densify**가 정공법.
2. **회전체 다각형화** (Cylinder, tube) — `resolution=N` 인자로 단순 증가 가능 → **저비용 win**.
- `mesh.smooth_taubin()`은 사후 적용으로 normal을 부드럽게 만들지만, **각진 면 자체를 점 추가로 늘려주지는 않음** (정점 수 동일). 이미 직선인 polyline에는 효과 제한.
- `mesh.subdivide(nsub=2)`는 cell이 4배 증가 — 단순한 box face들에 적용하면 GPU 낭비. **곡면 의심 영역에만 선택 적용**해야.
---
## 2. 권장 smoothing 전략 매트릭스
| 부위 | 전략 | 새 점 개수 | 예상 cell 증가 | 성능 영향 | 1차 패치 포함? |
|---|---|---|---|---|---|
| **Ogee profile** | `scipy.interpolate.CubicSpline` (parametric, x→z 단조 아니므로 arc-length 매개화) | 8~50 → ×4 (32~200) | sweep cell ×4 ≈ +수백 | 무시 가능 | **✓ 1차 적용** |
| Radial gate skin | `n_circ` 16 → 32 또는 CubicSpline ang | 34 → 66 | +30 cell | 무시 | P2 |
| Cylinder (jack, valve body) | `resolution` 16 → 32 | +16 cells/cyl | 객체당 무시 | P3 |
| Anchor bar | `resolution` 8 → 16 (개수 많음 주의) | +8 cells × N개 | 격자 200개면 +1.6K cells | 60 FPS 영향 가능 — **격자 cap 200 유지 필수** | P3 |
| 임의 곡면 메시 (사후) | `mesh.smooth_taubin(n_iter=10, pass_band=0.1)` | 정점 수 유지 | +0 | 무시 (CPU 1회) | P3 |
| Box face (subdivide 불필요) | — | — | — | — | 적용 안 함 |
| 폴리곤 외곽 (chamber, pier) | Chaikin / B-spline 보간 | ×2~×4 | side cells ×2~4 | 가능 (큰 다각형은 100K cell 위험) | P2 — **선택 적용** |
---
## 3. 1차 패치 — Ogee profile spline (gate_3d_builder.py)
### 변경 위치
`_build_spillway_body()` 내부, `closed_pts_2d` 생성 전에 ogee profile (x, z)를 **CubicSpline로 4× 보간**.
### 핵심 함수 추가
```python
def _densify_profile(profile_2d, n_factor=4, n_min=4):
"""(x, z) 프로파일 점을 arc-length parametric CubicSpline로 보간.
이유: ogee는 단조 함수가 아닐 수 있고(상류 옹벽 수직부에서 x=동일이 여러 z),
parametric (s, x), (s, z) 곡선이 안전하다.
n_factor: 출력 점 개수 = max(n_factor * len(profile), n_min)
"""
import numpy as np
from scipy.interpolate import CubicSpline
if profile_2d is None or len(profile_2d) < 4:
return list(profile_2d) if profile_2d else []
pts = np.asarray(profile_2d, dtype=float)
# arc-length 누적 거리(매개변수 s)
diffs = np.diff(pts, axis=0)
seg_len = np.sqrt((diffs ** 2).sum(axis=1))
s = np.concatenate([[0.0], np.cumsum(seg_len)])
if s[-1] <= 1e-9:
return list(profile_2d)
# 정규화 [0, 1]
s_norm = s / s[-1]
cs_x = CubicSpline(s_norm, pts[:, 0], bc_type='natural')
cs_z = CubicSpline(s_norm, pts[:, 1], bc_type='natural')
n_out = max(n_factor * len(profile_2d), n_min)
s_new = np.linspace(0.0, 1.0, n_out)
return list(zip(cs_x(s_new).tolist(), cs_z(s_new).tolist()))
```
### `_build_spillway_body()` 본문 수정
변경 전 `profile = p.ogee_profile` 직후에:
```python
profile = self._densify_profile(p.ogee_profile, n_factor=4)
```
### Diff 형식
```diff
@@ class GateBuilder ... _build_spillway_body
def _build_spillway_body(self):
"""Ogee 프로파일을 span 방향으로 extrude하여 본체 생성."""
p = self.params
- profile = p.ogee_profile
+ # 매끄러운 곡면을 위한 CubicSpline 보간 (사용자 피드백 #2 대응)
+ profile = self._densify_profile(p.ogee_profile, n_factor=4)
if len(profile) < 3:
return
@@
+ @staticmethod
+ def _densify_profile(profile_2d, n_factor: int = 4, n_min: int = 4):
+ """(x, z) 프로파일 점을 arc-length parametric CubicSpline로 보간.
+ ogee는 단조 함수가 아닐 수 있어 parametric (s, x), (s, z) 곡선 사용.
+ """
+ if profile_2d is None or len(profile_2d) < 4:
+ return list(profile_2d) if profile_2d else []
+ try:
+ from scipy.interpolate import CubicSpline
+ except Exception:
+ return list(profile_2d)
+ pts = np.asarray(profile_2d, dtype=float)
+ diffs = np.diff(pts, axis=0)
+ seg_len = np.sqrt((diffs ** 2).sum(axis=1))
+ s = np.concatenate([[0.0], np.cumsum(seg_len)])
+ if s[-1] <= 1e-9:
+ return list(profile_2d)
+ s_norm = s / s[-1]
+ cs_x = CubicSpline(s_norm, pts[:, 0], bc_type='natural')
+ cs_z = CubicSpline(s_norm, pts[:, 1], bc_type='natural')
+ n_out = max(n_factor * len(profile_2d), n_min)
+ s_new = np.linspace(0.0, 1.0, n_out)
+ return list(zip(cs_x(s_new).tolist(), cs_z(s_new).tolist()))
```
### Cell 수 영향 추정
- 기본 ogee: 8점 → 보간 후 32점 → closed_pts_2d = 34점 (앞·뒤 바닥 각 1개 추가)
- prism cells = 측면 strip 34×2 + 양 끝 fan (32+32) = 68 + 64 = 132 cells
- 변경 전: 8점 → 10점 → 20 + 16 = 36 cells
- **+96 cells 증가**, 단일 메시 100K 임계 대비 미미함.
DXF에서 50점 추출된 경우: 200점 보간 → cells ≈ 800 — 여전히 안전.
---
## 4. 적용 후 검증 방법
1. **Compile + Import 테스트**
```sh
python -m py_compile gate_3d_builder.py
python -c "from gate_3d_builder import GateBuilder; print('ok')"
```
2. **메쉬 빌드 smoke test** (옵션)
```sh
python gate_3d_builder.py # __main__ block에 이미 있음
```
`meshes[0]` (spillway body)의 `n_points`, `n_cells`가 변경 전 대비 약 4배 증가 확인.
3. **시각 비교** (사용자 측에서)
- `pl.add_mesh(spillway_body, show_edges=True)` 로 wireframe 비교.
- Ogee 정점 부근(crest)에서 직선 분절이 곡선으로 부드럽게 변경되었는지 확인.
- capture_image PNG로 before/after 저장 → AI 입력에서도 차이 확인.
4. **60 FPS 게이트** (performance-guardian 확인)
- 단일 spillway body cells < 1K 유지: P1 OK.
- 전체 scene 실시간 회전 시 FPS 측정 (PyVista plotter `pl.add_text(f"FPS: {pl.iren.get_event_observer_count()}")`).
---
## 5. P2/P3 후보 (다음 라운드)
### P2 — Radial gate skin (`_make_radial_skin`)
- `n_circ = 16 → 32` (1줄 변경)
- Cell 수 +32 — 무시.
- 또는 CubicSpline로 ang_sill ↔ ang_top 사이 36점 균등.
### P2 — Polygon reconstructor 결과 (chamber, pier polygon)
- `polygon_reconstructor.py`에서 반환된 polygon points를 빌더가 사용하기 전에 **Chaikin 알고리즘** 1~2회 적용해 corner를 라운딩.
- 단, sanity check (`_validate_pier_polys`)와 호환 유지 (bbox 기반이므로 OK).
- **주의**: 직각 corner를 의도한 pier에는 적용 부적절. layer 이름이나 geometry hint로 곡면 의심 폴리곤만 선별.
### P3 — Cylinder resolution 일괄 상향
- `gate_3d_builder.py:tube(n_sides=8)` → 16
- `intake_tower_3d_builder.py:Cylinder(resolution=16)` → 24
- `valve_chamber_3d_builder.py:Cylinder(resolution=8~20)` → 16~32
- `retaining_wall_3d_builder.py:Cylinder(resolution=8)` → 12 (격자 200개라 cell 폭증 우려; 8 → 12면 +800 cells, 안전)
### P3 — `pl.add_mesh(..., smooth_shading=True)`
- VTK side에서 normal averaging만 켜면 무료로 면이 매끄러워 보임. 정점 수 변화 없음.
- 단, 박스(직각 corner)에 적용 시 corner가 둥글어 보여 이상해짐 → **곡면 mesh에만 선별 적용**.
### P3 — `mesh.smooth_taubin(n_iter=10, pass_band=0.1)` post-build
- ogee/radial skin 등에 추가 적용 가능. CPU에서 1회 — 인터랙션 무관.
### P4 — NURBS-level (장기)
- 수문 trunnion radial sweep을 진짜 parametric surface로 정의 → VTK `vtkParametricSpline`.
- 효과 크지만 빌더 구조 변경 큼. Phase 1+에서 검토.
---
## Appendix: 왜 ogee를 1차 적용 대상으로?
- 사용자 줌인 시 **가장 먼저 보는 부분이 spillway 본체 단면** (ogee 곡선이 spillway의 시그니처 형상).
- DXF 파서가 추출하는 점 수 자체가 적은 케이스(8~20점)가 흔함 → **직선 분절이 가장 명확히 보임**.
- 위험 낮음: extrude 함수(`_extrude_2d_profile`, `_triangulate_prism`) 그대로, 입력 점 수만 늘어남.
- 효과 즉각: 다음 빌드 결과부터 곡면이 부드러워짐.