diff --git a/docs/superpowers/specs/2026-03-26-asset-browser-design.md b/docs/superpowers/specs/2026-03-26-asset-browser-design.md new file mode 100644 index 0000000..6a60dac --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-asset-browser-design.md @@ -0,0 +1,176 @@ +# Asset Browser Design + +## Overview + +에디터 도킹 패널에 파일 시스템 탐색기를 추가한다. 지정된 에셋 루트 디렉토리에서 파일/폴더를 플랫 리스트로 표시하고, 폴더 이동/뒤로가기를 지원. 읽기 전용. + +## Scope + +- 에셋 루트 디렉토리 기준 파일/폴더 목록 표시 +- 폴더 클릭 → 해당 폴더로 이동 +- 뒤로가기 (부모 디렉토리, root까지만) +- 파일 선택 시 이름/크기/확장자 정보 표시 +- 로딩/프리뷰 없음 (읽기 전용) + +## Data Model + +```rust +pub struct AssetBrowser { + root: PathBuf, + current: PathBuf, + entries: Vec, + selected_file: Option, // 선택된 파일 이름 +} + +struct DirEntry { + name: String, + is_dir: bool, + size: u64, +} +``` + +## API + +```rust +impl AssetBrowser { + /// 에셋 루트 디렉토리로 초기화. entries를 즉시 스캔. + pub fn new(root: PathBuf) -> Self; + + /// 현재 디렉토리의 파일/폴더를 다시 스캔. + pub fn refresh(&mut self); + + /// 하위 폴더로 이동. root 바깥으로 나가지 못하게 가드. + /// 실제 디렉토리가 아니면 무시. + pub fn navigate_to(&mut self, dir_name: &str); + + /// 부모 디렉토리로 이동. root에서는 무시. + pub fn go_up(&mut self); + + /// 현재 경로 (root 기준 상대 경로 문자열). + pub fn relative_path(&self) -> String; + + /// 현재 entries 접근. + pub fn entries(&self) -> &[DirEntry]; +} + +/// 에셋 브라우저 패널 UI 렌더링. +pub fn asset_browser_panel( + ui: &mut UiContext, + browser: &mut AssetBrowser, + rect: &Rect, +); +``` + +## refresh 로직 + +```rust +fn refresh(&mut self) { + self.entries.clear(); + self.selected_file = None; + if let Ok(read_dir) = std::fs::read_dir(&self.current) { + for entry in read_dir.flatten() { + let meta = entry.metadata().ok(); + let is_dir = meta.as_ref().map_or(false, |m| m.is_dir()); + let size = meta.as_ref().map_or(0, |m| m.len()); + let name = entry.file_name().to_string_lossy().to_string(); + self.entries.push(DirEntry { name, is_dir, size }); + } + } + // 정렬: 폴더 먼저, 그 다음 파일. 각각 이름순. + self.entries.sort_by(|a, b| { + b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)) + }); +} +``` + +## navigate_to 로직 + +```rust +fn navigate_to(&mut self, dir_name: &str) { + let target = self.current.join(dir_name); + // root 바깥 가드: target이 root의 하위여야 함 + if target.starts_with(&self.root) && target.is_dir() { + self.current = target; + self.refresh(); + } +} +``` + +## go_up 로직 + +```rust +fn go_up(&mut self) { + if self.current != self.root { + if let Some(parent) = self.current.parent() { + if parent.starts_with(&self.root) || parent == self.root { + self.current = parent.to_path_buf(); + self.refresh(); + } + } + } +} +``` + +## UI 렌더링 + +``` +asset_browser_panel(ui, browser, rect): + ui.layout = LayoutState::new(rect.x + 4, rect.y + 4) + + // 현재 경로 + ui.text(&format!("Path: {}", browser.relative_path())) + + // 뒤로가기 (root가 아닐 때) + if current != root: + if ui.button("[..]"): + browser.go_up() + + // 항목 목록 + for entry in browser.entries(): + if entry.is_dir: + label = format!("[D] {}", entry.name) + else: + label = format!(" {}", entry.name) + + // 클릭 가능한 텍스트 행 + 하이라이트 if selected + if clicked: + if entry.is_dir: + browser.navigate_to(&entry.name) + else: + browser.selected_file = Some(entry.name.clone()) + + // 선택된 파일 정보 + if let Some(file) = &browser.selected_file: + ui.text("-- File Info --") + ui.text(&format!("Name: {}", file)) + // entries에서 찾아서 크기 표시 + if found: + ui.text(&format!("Size: {}", format_size(size))) + ui.text(&format!("Type: {}", extension)) +``` + +크기 포맷: `format_size(bytes)` → "123 B", "4.5 KB", "1.2 MB" + +## File Structure + +- `crates/voltex_editor/src/asset_browser.rs` — AssetBrowser, DirEntry, asset_browser_panel, format_size +- `crates/voltex_editor/src/lib.rs` — 모듈 추가 +- `examples/editor_demo/src/main.rs` — Console 패널(panel 3)을 AssetBrowser로 교체 + +## Testing + +### AssetBrowser 로직 (파일 시스템 사용, GPU 불필요) +- `test_new_scans_entries`: 임시 디렉토리에 파일/폴더 생성 → new() → entries 확인 +- `test_refresh`: 파일 추가 후 refresh → entries 업데이트 확인 +- `test_navigate_to`: 하위 폴더로 이동 → current 변경, entries 변경 확인 +- `test_go_up`: 하위에서 go_up → 부모로 이동 확인 +- `test_go_up_at_root`: root에서 go_up → current 변경 안 됨 +- `test_root_guard`: root 바깥 경로 시도 → 무시 +- `test_entries_sorted`: 폴더가 파일보다 먼저, 이름순 + +### format_size +- `test_format_size`: 0, 500, 1024, 1500000 등 + +### UI (draw_list 검증) +- `test_panel_draws_commands`: browser + UiContext → draw_list에 커맨드 생성 확인