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>
631 lines
29 KiB
Markdown
631 lines
29 KiB
Markdown
# S-CANVAS Core/Plugin 분리 설계 (Phase 0 — 분석)
|
||
|
||
> **상태**: 분석/설계 단계 (Phase 0). 코드 변경 없음.
|
||
> **작성**: library-architect subagent (read-only survey).
|
||
> **기준 일자**: 2026-05-08
|
||
> **이전 단계**: 사용자 피드백 #10 — "코드 구조를 Core(핵심), plug-in(추가)으로 구분 필요".
|
||
|
||
---
|
||
|
||
## 0. 설계 원칙
|
||
|
||
이 계획은 다음 4가지를 동시에 만족해야 한다.
|
||
|
||
1. **현재 동작 깨지 않음** — `python scanvas_maker.py` 가 모든 단계에서 그대로 동작.
|
||
2. **점진적 마이그레이션** — 한 번에 모든 파일을 옮기지 않음. 2~3 세션에 걸쳐.
|
||
3. **`STRUCTURE_REGISTRY` 확장(replace 아님)** — 이미 잘 짜인 싱글톤 레지스트리를
|
||
plugin entry-point 의 첫 번째 사용처로 만든다.
|
||
4. **library-architect.md 의 Phase 2 비전과 정렬** — `manifest.json` 기반 동적
|
||
디스커버리, `.scanvas-lib` ZIP 배포가 최종 목표.
|
||
|
||
> **비목표**: PyPI 엔트리포인트, 온라인 마켓, Phase 3 의 "꿈" 단계는 본 문서 범위가
|
||
> 아니다. 로컬 폴더/ZIP 디스커버리만 다룬다.
|
||
|
||
---
|
||
|
||
## 1. 현재 구조 진단
|
||
|
||
### 1.1 모듈 그래프 (의존 방향)
|
||
|
||
```
|
||
┌──────────────────────────────┐
|
||
│ scanvas_maker.py (~7000 LOC)│ ← GUI 진입점, 거의 모든 것 import
|
||
│ CustomTkinter SCanvasApp │
|
||
└─────────────┬────────────────┘
|
||
│
|
||
┌─────────────┬───────┼───────┬─────────────┬─────────────┐
|
||
│ │ │ │ │ │
|
||
▼ ▼ ▼ ▼ ▼ ▼
|
||
resource_paths splash dem_extender geo_referencing tile_downloader dxf_geometry
|
||
▲
|
||
│ (공유)
|
||
┌────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
structure_templates.py ── REGISTRY (싱글톤)
|
||
│ ├── SpillwayGateTemplate ──→ gate_parser, gate_3d_builder
|
||
│ ├── IntakeTowerTemplate ──→ intake_tower_parser, intake_tower_3d_builder
|
||
│ ├── ValveChamberTemplate ──→ valve_chamber_parser, valve_chamber_3d_builder
|
||
│ ├── DetailedRetainingWallTemplate ──→ retaining_wall_parser, retaining_wall_3d_builder
|
||
│ ├── BuildingTemplate / BridgeTemplate / TunnelPortalTemplate (placeholder)
|
||
│ └── GenericStructureTemplate
|
||
│
|
||
structure_placement / structure_vlm_feedback / filename_classifier
|
||
view_detector / view_reconstructor / polygon_reconstructor / detail_parser / optional_detector
|
||
|
||
렌더링 백엔드 (선택 가능):
|
||
├── gemini_renderer.py (Gemini Vertex/API)
|
||
└── blender_renderer.py (Blender 헤드리스 + bpy 빌더)
|
||
└── gate_3d_builder_bpy.py + apply_blender_patch.py + fix_bpy_import.py
|
||
+ validate_gate_params.py + params_to_json.py
|
||
|
||
품질·이력:
|
||
harness/
|
||
├── seed_manager.py
|
||
├── quality_validator.py
|
||
├── prompt_registry.py
|
||
└── logger.py (SQLite ORM JobRecord)
|
||
|
||
런타임 자산 (읽기):
|
||
Design/, prompt_templates/, structure_types/
|
||
|
||
사용자 데이터 (쓰기, %LOCALAPPDATA%\S-CANVAS\):
|
||
cache/dem/, scanvas_jobs.db, scanvas_*.log
|
||
```
|
||
|
||
### 1.2 Core 후보 vs Plugin 후보
|
||
|
||
판정 기준: "이걸 빼면 GUI 가 떠도 1·1.5·2 단계 파이프라인이 되는가?"
|
||
|
||
| 모듈 | 분류 | 사유 |
|
||
|---|---|---|
|
||
| `scanvas_maker.py` | **Core** | GUI 엔트리·이벤트 루프·상태 보유 |
|
||
| `splash.py` | **Core** | 부팅 1회 호출, 의존성도 cv2/PIL 만 |
|
||
| `resource_paths.py` | **Core** | 모든 IO 경로의 진실. 대체 불가 |
|
||
| `dxf_geometry.py` | **Core** | 모든 파서/빌더의 공통 전처리 |
|
||
| `geo_referencing.py` | **Core** | 4점 매칭은 파이프라인 필수 |
|
||
| `dem_extender.py` | **Core** | 1.5 단계의 핵심. AWS 의존이지만 옵션 분기 있음 |
|
||
| `tile_downloader.py` | **Core** | 2 단계 (위성 결합) 필수 |
|
||
| `polygon_reconstructor.py` | **Core (util)** | 여러 파서가 공유 |
|
||
| `optional_detector.py` | **Core (util)** | 부속 컴포넌트 검출, 파서 공통 |
|
||
| `filename_classifier.py` | **Core (util)** | 키워드 → template_id 매핑, GUI 가 직접 호출 |
|
||
| `detail_parser.py` | **Core (util)** | 모든 구조물 파서가 공유하는 치수 추출 |
|
||
| `view_detector.py` / `view_reconstructor.py` | **Core (util)** | template 의 `try_view_based_*` 가 호출 |
|
||
| `structure_placement.py` | **Core (util)** | 파이프라인의 "위치인식 → 굴착 → 배치" 4단계 중 핵심 |
|
||
| `harness/*` | **Core** (선택적) | 이미 try/except 로 import 보호되어 있음. 항상 로드는 하지만 없어도 동작 |
|
||
| `structure_templates.py` | **Core (host)** + Plugin | `StructureTemplate` ABC 와 `TemplateRegistry` 는 Core, 7개 구체 클래스는 Plugin |
|
||
| `gate_*`, `intake_tower_*`, `valve_chamber_*`, `retaining_wall_*` (parser+builder 쌍) | **Plugin (structure)** | 각각 1종의 구조물. 빠져도 다른 구조물은 작동 |
|
||
| `gate_3d_builder_bpy.py` | **Plugin (structure-renderer variant)** | Blender 백엔드용 같은 구조물의 변종 |
|
||
| `validate_gate_params.py` | **Plugin (helper)** | gate-bpy 와 같이 묶여야 의미가 있음 |
|
||
| `apply_blender_patch.py`, `fix_bpy_import.py` | **Plugin (tooling)** | 일회성 패치 도구. 마이그레이션 대상 아님 |
|
||
| `params_to_json.py` | **Core (util)** | 모든 파서 결과를 JSON 직렬화. plugin 도 사용 |
|
||
| `gemini_renderer.py` | **Plugin (render)** | AI 렌더 백엔드의 한 종류. 다른 백엔드로 교체 가능 |
|
||
| `blender_renderer.py` | **Plugin (render)** | 같은 위치, 다른 엔진 |
|
||
| `structure_vlm_feedback.py` | **Plugin (qa)** | Gemini Vision 기반 QA. 빠져도 빌드 자체는 됨 |
|
||
| `structure_types/*.yaml`, `prompt_templates/*.yaml` | **데이터** | 자체로는 코드 아님. 각 plugin 이 자기 yaml 동봉 가능 |
|
||
|
||
### 1.3 발견된 사실
|
||
|
||
- `STRUCTURE_REGISTRY` 는 **이미** 잘 설계된 plugin host 다 (Singleton + ABC + dict).
|
||
단지 `_register_defaults()` 가 import-bound 하드코딩이라는 점만 동적으로
|
||
바꾸면 된다.
|
||
- `harness/` 는 **이미 plugin-스타일** (`try: from harness... except ImportError`).
|
||
이 패턴을 다른 옵션 모듈(`structure_vlm_feedback`, 렌더러들)이 모방한다.
|
||
- `scanvas_maker.py` 의 import 블록(line 31-80) 자체가 **암묵적 디스커버리**다:
|
||
각 모듈을 try-import 해서 가용성 플래그(`HARNESS_AVAILABLE`,
|
||
`STRUCTURE_TEMPLATES_AVAILABLE`, `DEM_EXTENDER_AVAILABLE`,
|
||
`STRUCTURE_VLM_AVAILABLE` 등)를 세운다. → 명시적 plugin manager 로 통합 가능.
|
||
- 구조물 plugin 은 `parser + builder` 2-파일 쌍이 표준. 일관성 양호.
|
||
- Blender 트랙은 `*_bpy.py` 접미사로 구분되어 있지만, 같은 디렉토리에 섞여 있다.
|
||
|
||
---
|
||
|
||
## 2. 제안 레이아웃
|
||
|
||
### 2.1 디렉토리 트리 (목표)
|
||
|
||
```
|
||
D:\2026\PROGRAM\1_S-CANVAS\
|
||
├── scanvas_maker.py # 메인 진입점 (그대로)
|
||
├── splash.py # (그대로)
|
||
├── resource_paths.py # (그대로)
|
||
│
|
||
├── core/ # ★ 신규 패키지
|
||
│ ├── __init__.py
|
||
│ ├── plugin_manager.py # ★ 플러그인 로더 (신규)
|
||
│ ├── manifest.py # ★ Manifest dataclass + 검증 (신규)
|
||
│ ├── interfaces.py # ★ Plugin abstract bases (신규)
|
||
│ │
|
||
│ ├── geo/ # 좌표·DXF 공통
|
||
│ │ ├── __init__.py
|
||
│ │ ├── dxf_geometry.py ← 이동
|
||
│ │ ├── geo_referencing.py ← 이동
|
||
│ │ ├── tile_downloader.py ← 이동
|
||
│ │ ├── dem_extender.py ← 이동
|
||
│ │ └── polygon_reconstructor.py ← 이동
|
||
│ │
|
||
│ ├── parsing/ # 공통 파서·검출
|
||
│ │ ├── __init__.py
|
||
│ │ ├── detail_parser.py ← 이동
|
||
│ │ ├── view_detector.py ← 이동
|
||
│ │ ├── view_reconstructor.py ← 이동
|
||
│ │ ├── optional_detector.py ← 이동
|
||
│ │ └── filename_classifier.py ← 이동
|
||
│ │
|
||
│ ├── structures/ # 구조물 호스트 (ABC + Registry)
|
||
│ │ ├── __init__.py
|
||
│ │ ├── base.py ← structure_templates.py 의 추상부
|
||
│ │ ├── registry.py ← TemplateRegistry + REGISTRY
|
||
│ │ ├── placement.py ← structure_placement.py
|
||
│ │ └── params_io.py ← params_to_json.py
|
||
│ │
|
||
│ └── harness/ ← 그대로 (이미 패키지)
|
||
│ ├── __init__.py
|
||
│ ├── seed_manager.py
|
||
│ ├── quality_validator.py
|
||
│ ├── prompt_registry.py
|
||
│ └── logger.py
|
||
│
|
||
├── plugins/ # ★ 신규 패키지 (디스커버리 루트)
|
||
│ ├── __init__.py
|
||
│ │
|
||
│ ├── structures/ # ── 구조물 라이브러리 ──
|
||
│ │ ├── spillway_gate/
|
||
│ │ │ ├── manifest.json # ★ 신규
|
||
│ │ │ ├── plugin.py # SpillwayGateTemplate (구 structure_templates.py 의 클래스)
|
||
│ │ │ ├── parser.py ← gate_parser.py
|
||
│ │ │ ├── builder_pyvista.py ← gate_3d_builder.py
|
||
│ │ │ ├── builder_blender.py ← gate_3d_builder_bpy.py
|
||
│ │ │ ├── validate.py ← validate_gate_params.py
|
||
│ │ │ ├── parameters.json # ★ 신규 (UI 폼 자동 생성용)
|
||
│ │ │ └── samples/ # ★ 신규 (1~3개 sample params)
|
||
│ │ │
|
||
│ │ ├── intake_tower/
|
||
│ │ │ ├── manifest.json
|
||
│ │ │ ├── plugin.py
|
||
│ │ │ ├── parser.py ← intake_tower_parser.py
|
||
│ │ │ └── builder_pyvista.py ← intake_tower_3d_builder.py
|
||
│ │ │
|
||
│ │ ├── valve_chamber/ … (동일 구조)
|
||
│ │ ├── retaining_wall/ … (동일 구조)
|
||
│ │ │
|
||
│ │ ├── building/ # 현재 placeholder. 같은 레이아웃에 진입
|
||
│ │ ├── bridge/
|
||
│ │ └── tunnel_portal/
|
||
│ │
|
||
│ ├── render/ # ── 렌더 백엔드 ──
|
||
│ │ ├── gemini/
|
||
│ │ │ ├── manifest.json
|
||
│ │ │ └── plugin.py ← gemini_renderer.py
|
||
│ │ └── blender/
|
||
│ │ ├── manifest.json
|
||
│ │ ├── plugin.py ← blender_renderer.py
|
||
│ │ └── tooling/ ← apply_blender_patch.py, fix_bpy_import.py
|
||
│ │
|
||
│ └── qa/ # ── 품질/피드백 ──
|
||
│ └── vlm_feedback/
|
||
│ ├── manifest.json
|
||
│ └── plugin.py ← structure_vlm_feedback.py
|
||
│
|
||
├── structure_types/ # (그대로) — yaml은 plugin 외부 공유 데이터
|
||
│ └── structure_v1.yaml
|
||
│
|
||
├── prompt_templates/ # (그대로)
|
||
│ └── prompt_v1.yaml
|
||
│
|
||
├── Design/ # (그대로)
|
||
│
|
||
├── user_plugins/ # ★ 신규 — 사용자 설치 라이브러리 (외부 ZIP 풀어 놓는 곳)
|
||
│ └── (ex: SpillwayGate_v2.scanvas-lib 풀린 폴더)
|
||
│
|
||
├── _unused/ # (그대로) — 격리된 옛 코드
|
||
└── venv313/ # (그대로)
|
||
```
|
||
|
||
### 2.2 핵심 결정
|
||
|
||
- **Core 는 단일 파이썬 패키지**(`core/`) 로 통일. 모든 공유 유틸·ABC·레지스트리는
|
||
여기 들어간다.
|
||
- **Plugins 는 디렉토리 한 개 + manifest.json 한 개** = 한 plugin. 파일 수는
|
||
그 안에서 자유롭게.
|
||
- **두 디스커버리 루트**:
|
||
1. `plugins/` (소스 트리, 번들 plugin) — 항상 로드
|
||
2. `user_plugins/` (사용자 데이터 폴더) — 옵트인. ZIP 압축 해제 위치
|
||
- **렌더링 백엔드도 plugin** — Gemini/Blender 가 동급. 미래에 ComfyUI·Stability 등을
|
||
추가할 수 있게.
|
||
|
||
---
|
||
|
||
## 3. 플러그인 인터페이스
|
||
|
||
### 3.1 Abstract Bases (`core/interfaces.py`)
|
||
|
||
```python
|
||
# core/interfaces.py
|
||
from abc import ABC, abstractmethod
|
||
from pathlib import Path
|
||
import pyvista as pv
|
||
|
||
# (1) 구조물 plugin — 기존 StructureTemplate 의 외부 노출 이름
|
||
class StructurePlugin(ABC):
|
||
plugin_id: str # "spillway_gate" — manifest.id 와 일치
|
||
template_id: str # registry 키 (legacy 호환을 위해 별도 유지)
|
||
name_ko: str
|
||
description: str
|
||
icon_hint: str = ""
|
||
required_files: tuple[int, int, int] = (1, 2, 1)
|
||
supports_view_based: bool = True
|
||
|
||
@abstractmethod
|
||
def get_parameter_schema(self) -> "list[ParamField]": ...
|
||
@abstractmethod
|
||
def parse(self, dxf_paths: list[str]) -> "StructureParams": ...
|
||
@abstractmethod
|
||
def build_meshes(self, params) -> list[tuple[pv.PolyData, str, float]]: ...
|
||
|
||
def validate_params(self, params) -> tuple[bool, str]:
|
||
return True, "" # 기본 통과
|
||
|
||
# (2) 렌더 백엔드 plugin
|
||
class RenderPlugin(ABC):
|
||
plugin_id: str # "gemini", "blender"
|
||
name_ko: str
|
||
requires_credentials: bool
|
||
|
||
@abstractmethod
|
||
def is_available(self) -> tuple[bool, str]:
|
||
"""라이브러리/외부 의존이 갖춰졌는지 검사."""
|
||
@abstractmethod
|
||
def render(self, app, *, control_map: Path, prompt: str, **kwargs) -> Path:
|
||
"""제어맵 + 프롬프트 → PNG 경로."""
|
||
|
||
# (3) QA plugin
|
||
class QAPlugin(ABC):
|
||
plugin_id: str
|
||
|
||
@abstractmethod
|
||
def evaluate(self, *, build_meshes, original_dxf, current_params) -> dict:
|
||
"""JSON-able 결과 (missing/incorrect/excess 등)."""
|
||
|
||
# (4) 데이터 클래스 (기존 그대로 노출)
|
||
# StructureParams, ParamField — core/structures/base.py 에서 재export
|
||
```
|
||
|
||
> **핵심**: `StructurePlugin` 은 **현재의 `StructureTemplate` 와 100% 호환** 되도록
|
||
> 같은 메서드 시그니처를 유지. 즉 기존 클래스에 부모 하나만 바꾸는 것으로 충분.
|
||
|
||
### 3.2 Manifest 스키마 (`core/manifest.py`)
|
||
|
||
library-architect.md 의 비전을 그대로 차용하되, **현재 단계에 필요한 필드만**:
|
||
|
||
```json
|
||
{
|
||
"schema_version": "1.0",
|
||
"id": "spillway_gate",
|
||
"name": "여수로 래디얼 수문",
|
||
"name_en": "Spillway Radial Gate",
|
||
"kind": "structure",
|
||
"category": "hydraulic_structure",
|
||
"subcategory": "gate",
|
||
"version": "1.2.0",
|
||
"author": "Saman Corp.",
|
||
"license": "Proprietary",
|
||
"description": "ogee 여수로 + 래디얼 수문 + 공도교 + 개폐장치",
|
||
"min_scanvas_version": "0.5.0",
|
||
|
||
"entry": {
|
||
"type": "python_module",
|
||
"module": "plugin",
|
||
"class": "SpillwayGatePlugin",
|
||
"registry_id": "spillway_gate"
|
||
},
|
||
|
||
"parameters_schema": "parameters.json",
|
||
"samples": ["samples/sample_default.json"],
|
||
|
||
"metadata": {
|
||
"complexity": "high",
|
||
"polygon_estimate": 35000,
|
||
"supports_pyvista": true,
|
||
"supports_blender": true,
|
||
"requires_gpu": false
|
||
}
|
||
}
|
||
```
|
||
|
||
`kind` 값:
|
||
- `structure` → `StructurePlugin` 로딩, `STRUCTURE_REGISTRY` 에 등록
|
||
- `render` → `RenderPlugin` 로딩, `RENDER_REGISTRY` 에 등록
|
||
- `qa` → `QAPlugin` 로딩
|
||
|
||
`Manifest` 데이터클래스는 `core/manifest.py` 에서 정의하고, JSON 스키마 검증을
|
||
거친 뒤 plugin manager 에 넘긴다.
|
||
|
||
---
|
||
|
||
## 4. 디스커버리 메커니즘
|
||
|
||
### 4.1 단계별 디스커버리 정책
|
||
|
||
`core/plugin_manager.py` 의 `discover()` 알고리즘:
|
||
|
||
```python
|
||
# 의사코드
|
||
def discover(roots: list[Path]) -> dict[str, LoadedPlugin]:
|
||
found = {}
|
||
for root in roots:
|
||
for plugin_dir in root.iterdir():
|
||
if not plugin_dir.is_dir():
|
||
continue
|
||
mf = plugin_dir / "manifest.json"
|
||
if not mf.exists():
|
||
continue
|
||
try:
|
||
manifest = Manifest.load_validated(mf)
|
||
except ValidationError as e:
|
||
log.warning(f"[plugin] {plugin_dir.name}: invalid manifest: {e}")
|
||
continue
|
||
if not _version_compatible(manifest.min_scanvas_version):
|
||
log.warning(f"[plugin] {plugin_dir.name}: needs >= {manifest.min_scanvas_version}")
|
||
continue
|
||
try:
|
||
cls = _import_entry(plugin_dir, manifest.entry)
|
||
instance = cls()
|
||
except Exception as e:
|
||
log.warning(f"[plugin] {plugin_dir.name}: failed to load: {e}")
|
||
continue
|
||
found[manifest.id] = LoadedPlugin(manifest, instance, plugin_dir)
|
||
return found
|
||
```
|
||
|
||
**디스커버리 루트 우선순위**:
|
||
|
||
1. `<asset_root>/plugins/` — 번들 plugin (소스 트리 또는 PyInstaller `_MEIPASS`)
|
||
2. `<user_data_dir>/user_plugins/` — 사용자 설치 plugin
|
||
3. `(미래)` ZIP 경로 직접 — `*.scanvas-lib` 파일 압축 해제 후 (2) 로 이동
|
||
|
||
같은 `manifest.id` 가 둘 이상 발견되면 **버전 비교 후 최신 우선**, 동률이면
|
||
**user_plugins 우선** (사용자 override).
|
||
|
||
### 4.2 진입점 로딩
|
||
|
||
`plugin_dir / manifest.entry.module + ".py"` 에서 `manifest.entry.class` 를 import.
|
||
파이썬 import path 충돌 회피를 위해 `importlib.util.spec_from_file_location` 으로
|
||
**고유 모듈 이름**(`scanvas_plugin_<id>_<sha8>`) 을 부여한다.
|
||
|
||
### 4.3 등록
|
||
|
||
- `kind == "structure"` → `core/structures/registry.py` 의 `REGISTRY._templates[mf.id] = instance`
|
||
(기존 `_register_defaults()` 가 하던 일)
|
||
- `kind == "render"` → `core/render/registry.py` (신규) 의 `RENDER_REGISTRY[mf.id] = instance`
|
||
- `kind == "qa"` → `QA_REGISTRY[mf.id] = instance`
|
||
|
||
`scanvas_maker.py` 의 부팅 코드 1줄만 추가:
|
||
|
||
```python
|
||
from core.plugin_manager import bootstrap_plugins
|
||
bootstrap_plugins() # discover → register, 모든 REGISTRY 가 이 시점 이후 사용 가능
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 마이그레이션 단계
|
||
|
||
### Phase 1 — Core 추출 (1 세션, 2~3시간)
|
||
|
||
**범위**: 디렉토리 이동 + import 경로 갱신만.
|
||
|
||
**작업**:
|
||
1. `core/` 패키지 생성 + 빈 `__init__.py`
|
||
2. 다음 파일들을 `core/<subdir>/` 로 git mv (또는 단순 이동):
|
||
- `dxf_geometry.py`, `geo_referencing.py`, `tile_downloader.py`,
|
||
`dem_extender.py`, `polygon_reconstructor.py` → `core/geo/`
|
||
- `detail_parser.py`, `view_detector.py`, `view_reconstructor.py`,
|
||
`optional_detector.py`, `filename_classifier.py` → `core/parsing/`
|
||
- `structure_placement.py`, `params_to_json.py` → `core/structures/`
|
||
- `harness/` → `core/harness/`
|
||
3. 각 파일의 상대 import 갱신 (e.g., `from view_detector import ...` →
|
||
`from core.parsing.view_detector import ...`)
|
||
4. `scanvas_maker.py` 의 import 블록 갱신
|
||
5. **`structure_templates.py` 는 아직 그대로 둔다.** 거대 파일이라 분할은
|
||
Phase 2 에서.
|
||
|
||
**Risk**: 순환 import. `dxf_geometry` ↔ `view_detector` 같은 사이클이 잠재.
|
||
**Mitigation**: 이동 전 `python -c "import scanvas_maker"` smoke test. 한 모듈씩
|
||
이동하면서 매번 검증.
|
||
|
||
**Rollback**: Phase 1 은 단순 이동이라 git revert 로 즉시 복구.
|
||
|
||
**Backwards compat**: 다음 5개 파일을 **shim 으로 유지**:
|
||
```python
|
||
# /dxf_geometry.py (1줄짜리 shim)
|
||
from core.geo.dxf_geometry import * # noqa: F401, F403
|
||
from core.geo.dxf_geometry import __all__ # noqa: F401
|
||
```
|
||
→ 외부 사용자(있다면)나 일회성 스크립트가 깨지지 않음. Phase 3 에 제거.
|
||
|
||
---
|
||
|
||
### Phase 2 — Structure plugin 추출 (1~2 세션)
|
||
|
||
**범위**: `structure_templates.py` 의 7개 구체 클래스를 `plugins/structures/<id>/`
|
||
로 분할 + manifest.json 작성.
|
||
|
||
**작업** (1 plugin 씩, 가장 단순한 것부터):
|
||
1. `plugins/structures/spillway_gate/` 생성
|
||
2. `gate_parser.py`, `gate_3d_builder.py`, `gate_3d_builder_bpy.py`,
|
||
`validate_gate_params.py` 를 그 안으로 이동
|
||
3. `structure_templates.py` 의 `SpillwayGateTemplate` 클래스를 추출하여
|
||
`plugins/structures/spillway_gate/plugin.py` 로 이동, 부모를 `StructurePlugin`
|
||
으로 변경
|
||
4. `manifest.json`, `parameters.json`, `samples/sample_default.json` 작성
|
||
5. `core/structures/registry.py` 의 `_register_defaults()` 에서 `SpillwayGateTemplate`
|
||
import 를 제거 — plugin manager 가 대신 등록
|
||
6. **레거시 import 호환**: `gate_parser.py` 등 기존 경로 import 가 있는 파일
|
||
(현재 `gemini_renderer.py`, `apply_blender_patch.py`, `params_to_json.py` 등)을
|
||
확인하여 새 경로로 갱신. 외부 사용자용 shim 은 아래 backward compat 섹션 참조.
|
||
7. 동일 절차로 IntakeTower → ValveChamber → RetainingWall → Building/Bridge/Tunnel
|
||
→ Generic 순서.
|
||
|
||
**Risk**: 6번 항목. 특히 `from gate_parser import GateParams` 같은 import 가
|
||
plugin 외부 코드에서 발견될 가능성.
|
||
**Mitigation**: 각 plugin 추출 후 grep 으로 잔존 import 확인. shim 추가.
|
||
|
||
**Rollback**: plugin 단위로 git tag 를 찍어 두면 한 plugin 만 되돌리기 가능.
|
||
|
||
---
|
||
|
||
### Phase 3 — Render/QA plugin 추출 + 사용자 plugin 디스커버리 (1 세션)
|
||
|
||
**범위**:
|
||
1. `gemini_renderer.py` → `plugins/render/gemini/plugin.py` (RenderPlugin 인터페이스 적용)
|
||
2. `blender_renderer.py` → `plugins/render/blender/plugin.py`
|
||
3. `structure_vlm_feedback.py` → `plugins/qa/vlm_feedback/plugin.py`
|
||
4. `core/plugin_manager.py` 의 `bootstrap_plugins()` 가 `user_data_dir() / "user_plugins"`
|
||
를 두 번째 디스커버리 루트로 사용하도록 활성화
|
||
5. `*.scanvas-lib` (ZIP) → `user_plugins/<id>/` 풀어주는 헬퍼 `import_library_zip(path)`
|
||
6. GUI 사이드바에 "라이브러리 가져오기" 버튼 (드래그 앤 드롭 → import_library_zip)
|
||
|
||
**Risk**: PyInstaller 번들에서 `_MEIPASS` 안의 plugin 디렉토리를 importlib 가
|
||
정확히 찾는지. **Mitigation**: Phase 1 의 smoke test 를 PyInstaller 빌드 후에도
|
||
재실행. spec 파일에 `--add-data "plugins/;plugins/"` 추가.
|
||
|
||
**Rollback**: plugin manager bootstrap 호출 1줄을 주석 처리하면 즉시 옛 동작
|
||
복귀.
|
||
|
||
---
|
||
|
||
## 6. Backward Compatibility
|
||
|
||
### 6.1 기존 import 가 살아남는 방법
|
||
|
||
| 옛 경로 | 새 경로 | shim 전략 |
|
||
|---|---|---|
|
||
| `from gate_parser import GateParams, parse_gate_dxf` | `plugins.structures.spillway_gate.parser` | **별도 shim 모듈 안 둠**. 호출자 모두 내부 코드라 직접 갱신 |
|
||
| `from gate_3d_builder import GateBuilder` | `plugins.structures.spillway_gate.builder_pyvista` | 위와 동일 |
|
||
| `from structure_templates import REGISTRY as STRUCTURE_REGISTRY` | `from core.structures.registry import REGISTRY as STRUCTURE_REGISTRY` | `structure_templates.py` 를 **shim 한 줄**로 축소 |
|
||
| `from harness.logger import ...` | `from core.harness.logger import ...` | `/harness/__init__.py` 안에 `from core.harness import *` |
|
||
| `from dxf_geometry import ...` | `from core.geo.dxf_geometry import ...` | 루트의 같은 이름 .py 를 1줄 shim 으로 유지 |
|
||
| `from gemini_renderer import run_gemini_render` | `plugins.render.gemini.plugin` | 루트 shim |
|
||
|
||
### 6.2 shim 수명
|
||
|
||
- Phase 1·2 동안: shim 적극 사용 → 단계마다 외부 진입점이 안 깨짐.
|
||
- Phase 3 이후 1 release: shim 유지하되 `DeprecationWarning` 한 줄 발사.
|
||
- Phase 3 + 1 release 후: shim 제거.
|
||
|
||
### 6.3 데이터 호환
|
||
|
||
- `scanvas_jobs.db` (SQLite) — 스키마 변경 없음. ORM 모듈 위치만 이동.
|
||
- `prompt_templates/*.yaml`, `structure_types/*.yaml` — 파일 위치/포맷 변경 없음.
|
||
- 사용자 입력 파라미터 (StructureParams) — dataclass 형태 그대로.
|
||
|
||
---
|
||
|
||
## 7. 영향 받는 파일 (예상)
|
||
|
||
### 7.1 이동 (총 ~25 파일)
|
||
|
||
```
|
||
[Phase 1, Core 추출, 14 파일]
|
||
dxf_geometry.py, geo_referencing.py, tile_downloader.py, dem_extender.py,
|
||
polygon_reconstructor.py
|
||
detail_parser.py, view_detector.py, view_reconstructor.py,
|
||
optional_detector.py, filename_classifier.py
|
||
structure_placement.py, params_to_json.py
|
||
harness/seed_manager.py, harness/quality_validator.py,
|
||
harness/prompt_registry.py, harness/logger.py
|
||
|
||
[Phase 2, Structure plugin, 8 파일 + 7 manifest]
|
||
gate_parser.py, gate_3d_builder.py, gate_3d_builder_bpy.py, validate_gate_params.py
|
||
intake_tower_parser.py, intake_tower_3d_builder.py
|
||
valve_chamber_parser.py, valve_chamber_3d_builder.py
|
||
retaining_wall_parser.py, retaining_wall_3d_builder.py
|
||
+ structure_templates.py 에서 7 클래스 추출 분리
|
||
|
||
[Phase 3, Render/QA plugin, 3 파일]
|
||
gemini_renderer.py
|
||
blender_renderer.py (+ apply_blender_patch.py, fix_bpy_import.py 동반)
|
||
structure_vlm_feedback.py
|
||
```
|
||
|
||
### 7.2 신규 (총 ~30 파일)
|
||
|
||
```
|
||
core/__init__.py, core/plugin_manager.py, core/manifest.py, core/interfaces.py
|
||
core/geo/__init__.py, core/parsing/__init__.py, core/structures/__init__.py
|
||
core/structures/base.py, core/structures/registry.py
|
||
core/render/__init__.py, core/render/registry.py
|
||
core/qa/__init__.py, core/qa/registry.py
|
||
|
||
plugins/__init__.py
|
||
plugins/structures/<id>/manifest.json × 7
|
||
plugins/structures/<id>/parameters.json × 7
|
||
plugins/structures/<id>/samples/sample_default.json × 7
|
||
plugins/render/gemini/manifest.json
|
||
plugins/render/blender/manifest.json
|
||
plugins/qa/vlm_feedback/manifest.json
|
||
```
|
||
|
||
### 7.3 수정 (그대로 두지만 import 갱신)
|
||
|
||
```
|
||
scanvas_maker.py — import 블록 + bootstrap_plugins() 호출 1줄 추가
|
||
structure_templates.py — 7 클래스 추출 후 shim 으로 축소 (선택)
|
||
splash.py — 변경 없음
|
||
resource_paths.py — user_plugins() 헬퍼 추가
|
||
```
|
||
|
||
### 7.4 삭제 후보 (이미 `_unused/` 가 있으므로 동일 정책)
|
||
|
||
```
|
||
apply_blender_patch.py, fix_bpy_import.py
|
||
→ plugins/render/blender/tooling/ 로 이동 (아카이브 성격)
|
||
```
|
||
|
||
### 7.5 변경 없음
|
||
|
||
```
|
||
Design/, prompt_templates/, structure_types/, cache/, venv313/, _unused/
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 검증 체크리스트 (각 Phase 완료 시)
|
||
|
||
- [ ] `python scanvas_maker.py` 가 정상 부팅 (스플래시 → GUI)
|
||
- [ ] DXF 로드 → Step 1 TIN 생성 → Step 1.5 DEM 확장 → Step 2 위성 결합 → Step 3 캡처
|
||
- [ ] 7종 구조물 모두 `STRUCTURE_REGISTRY.list_choices()` 에 나타남
|
||
- [ ] 1종 구조물 빌드 (예: spillway_gate) 후 PyVista 미리보기 정상
|
||
- [ ] Step 4 AI 렌더 (Gemini 또는 Stability) 1회 성공
|
||
- [ ] `harness` SQLite 작업 이력 1건 기록 확인
|
||
- [ ] PyInstaller `pyinstaller scanvas.spec` 빌드 성공 (Phase 3 종료 시)
|
||
|
||
---
|
||
|
||
## 9. 미결 / 추가 논의 필요
|
||
|
||
1. **새 카테고리 트리** (`hydraulic_structure / transportation / building / landscape /
|
||
terrain`) 와 **기존 `structure_v1.yaml` 의 type 키** 의 연결 — yaml 의 `terrain`,
|
||
`road`, `embankment` 같은 type 은 카테고리가 아니라 *render mode* 다. 따라서
|
||
카테고리는 **manifest 에서만** 사용하고, yaml type 은 별개로 둔다.
|
||
2. **PyPI 엔트리포인트 활용 여부** — 본 계획은 manifest 스캔만 채택. 진짜 외부
|
||
배포가 필요해지면 별도 phase 4 로 검토.
|
||
3. **테스트 전략** — 현재 코드에 unit test 가 거의 없다. Phase 1 이전에 최소
|
||
smoke test (`python scanvas_maker.py --self-test`) 를 추가해 마이그레이션 안전망
|
||
확보 권장.
|
||
|
||
---
|
||
|
||
## 10. 요약 (의사결정자용 한 페이지)
|
||
|
||
- **Core**: GUI · 파이프라인 · DXF I/O · TIN · DEM · 위성타일 · harness · structure ABC/Registry.
|
||
→ `core/` 단일 패키지로 격리.
|
||
- **Plugin**: 7종 구조물 빌더 · 2종 렌더(Gemini/Blender) · 1종 QA(VLM).
|
||
→ `plugins/<kind>/<id>/manifest.json + plugin.py` 표준.
|
||
- **호스트**: 기존 `STRUCTURE_REGISTRY` (싱글톤) 가 그대로 plugin host. 단지
|
||
`_register_defaults()` 를 `plugin_manager.discover()` 로 교체.
|
||
- **마이그레이션**: 3 phase, 각 1~2 세션, 각 phase 완료 시 동작 검증 가능.
|
||
- **호환성**: 모든 기존 import 는 1~2줄 shim 으로 살아남음 → 사용자 임팩트 0.
|
||
- **최종 비전**: 사용자가 `*.scanvas-lib` ZIP 을 GUI 에 드롭 → 즉시 새 구조물 사용.
|
||
Phase 3 끝나면 도달.
|