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

10 KiB
Raw Permalink Blame History

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 매개화) 850 → ×4 (32200) 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× 보간.

핵심 함수 추가

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 직후에:

profile = self._densify_profile(p.ogee_profile, n_factor=4)

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 테스트

    python -m py_compile gate_3d_builder.py
    python -c "from gate_3d_builder import GateBuilder; print('ok')"
    
  2. 메쉬 빌드 smoke test (옵션)

    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) 그대로, 입력 점 수만 늘어남.
  • 효과 즉각: 다음 빌드 결과부터 곡면이 부드러워짐.