docs: sync project browser docs

This commit is contained in:
2026-04-04 01:02:57 +08:00
parent 42e2e1b8f2
commit 2abe0bbbd4
30 changed files with 1725 additions and 116 deletions

View File

@@ -0,0 +1,40 @@
# ProjectManager::CreateFolder
```cpp
AssetItemPtr CreateFolder(const std::string& name);
```
## 当前行为
`CreateFolder(...)` 当前在“当前目录”下创建新文件夹,而不是在任意传入路径下创建。
它会:
1. trim 输入名称
2. 拒绝空名、`.``..` 和 Windows 非法文件名字符
3. 在当前目录下计算唯一目标名
4. 执行 `create_directory(...)`
5. 刷新目录树
6. 查回新建目录对应的 `AssetItem`
7. 自动把它设为当前选中项
## 唯一命名规则
若目标目录已存在,当前会追加数字后缀:
- `New Folder`
- `New Folder 1`
- `New Folder 2`
这与 `tests/editor/test_action_routing.cpp``ProjectCommandsCreateFolderUsesUniqueDefaultName` 一致。
## 返回值语义
- 创建并刷新成功时,返回新目录对应的 `AssetItemPtr`
- 参数非法、目录创建失败或刷新后无法重新解析该目录时,返回 `nullptr`
## 相关文档
- [ProjectManager](ProjectManager.md)
- [RefreshCurrentFolder](RefreshCurrentFolder.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)

View File

@@ -0,0 +1,50 @@
# ProjectManager::DeleteItem
```cpp
bool DeleteItem(const std::string& fullPath);
```
## 当前行为
`DeleteItem(...)` 只允许删除当前项目 `Assets` 根目录之下的条目。
当前会依次校验:
- `fullPath` 非空
- `m_rootFolder` 已存在
- 目标路径存在
- 目标路径位于当前项目根目录之下
- 目标路径不是根目录本身
通过后才会执行真正删除。
## 删除语义
当前删除动作包括两步:
1. `fs::remove_all(itemPath)`
2. 额外删除同名 `.meta` sidecar
然后:
- 若当前选中的正是这个路径,会先清空选择
- 最后调用 [RefreshCurrentFolder](RefreshCurrentFolder.md)
删除 sidecar 的目的是让资源标识与导入元数据和资源本体一起消失,避免删掉文件后仍残留孤立的 `.meta`
## 返回值语义
- 删除并刷新成功时返回 `true`
- 任一前置校验失败或文件系统异常时返回 `false`
## 注意事项
- 当前不会弹出冲突确认或预览
- 删除目录时依赖 `remove_all(...)` 递归删除内容
- 只要路径越出项目根目录,当前就会直接拒绝
## 相关文档
- [ProjectManager](ProjectManager.md)
- [MoveItem](MoveItem.md)
- [RenameItem](RenameItem.md)

View File

@@ -0,0 +1,41 @@
# ProjectManager::Initialize
```cpp
void Initialize(const std::string& projectPath);
```
## 当前行为
`Initialize(...)` 会把传入路径记为 `m_projectPath`,然后把 `<projectPath>/Assets` 作为当前项目浏览根目录。
按当前实现:
1. 计算 `<project>/Assets`
2. 若不存在,则创建:
- `Assets/`
- `Assets/Scenes/`
3. 调用内部 `ScanDirectory(...)` 递归构建根目录树
4. 把根节点名称强制设为 `"Assets"`
5. 重置 `m_path` 为只包含根目录的一层
6. 清空当前选择
## 失败退化
若扫描或建目录过程中抛异常,当前不会把初始化直接判成失败,而是退化为:
- 构造一个最小可用的 `Folder` 根节点
- 仍把名字设为 `"Assets"`
- 仍重置路径栈与当前选择
也就是说,`ProjectManager` 更偏向“尽量给 ProjectPanel 一个可工作的空根目录”,而不是把异常往上抛。
## 注意事项
- 当前只保证 `Assets/Scenes`,不会自动生成旧文档里提到的一整套示例子目录。
- 初始化后当前位置总是根目录,不会保留旧会话中的最后浏览路径。
## 相关文档
- [ProjectManager](ProjectManager.md)
- [RefreshCurrentFolder](RefreshCurrentFolder.md)
- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md)

View File

@@ -0,0 +1,49 @@
# ProjectManager::MoveItem
```cpp
bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath);
```
## 当前行为
`MoveItem(...)` 当前只支持“把一个项目内条目移动到另一个项目内目录”。
它会校验:
- 源路径和目标目录路径都非空
- `m_rootFolder` 已存在
- 源路径存在
- 目标路径存在且是目录
- 源和目标都位于项目根目录之下
- 源路径不是根目录本身
- 若源是目录,目标不能位于它自己的子树里
- 目标最终路径不能与源路径相同
- 目标最终路径不能已存在同名条目
## 成功路径
校验通过后,当前会:
1. 把目标路径拼成 `destFolder / source.filename()`
2. 对资源本体执行 `fs::rename(sourcePath, destPath)`
3. 若存在同名 `.meta` sidecar则同步移动 `.meta`
4. 调用 [RefreshCurrentFolder](RefreshCurrentFolder.md)
同步移动 sidecar 是当前资源移动行为里很重要的一部分,因为它让导入元数据和资源路径保持一致,不会把 `.meta` 留在旧目录。
## 返回值语义
- 移动成功并刷新完成时返回 `true`
- 任一校验失败或文件系统异常时返回 `false`
## 当前限制
- 当前不做自动重命名避让;目标已有同名条目会直接失败
- 当前不会主动保留被移动条目的选择状态;若路径变化导致旧 `m_selectedItemPath` 失效,刷新后选择可能被清掉
## 相关文档
- [ProjectManager](ProjectManager.md)
- [DeleteItem](DeleteItem.md)
- [RenameItem](RenameItem.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)

View File

@@ -6,35 +6,203 @@
**源文件**: `editor/src/Managers/ProjectManager.h`
**描述**: `IProjectManager` 的默认实现,负责扫描项目 `Assets` 目录、维护面包屑路径,并支持基础文件夹与文件操作。
**描述**: `IProjectManager` 的默认实现,负责项目 `Assets` 目录扫描成 `AssetItem` 树,并提供路径导航、选择同步与基础文件操作。
## 概述
`ProjectManager` 当前本质上是一个面向 `Assets` 目录的轻量文件系统浏览器。
`ProjectManager` 当前不是 `AssetDatabase`,也不是资源导入服务;但它也已经不只是“轻量文件浏览器
它负责:
按当前实现,它承担三类稳定职责:
- 初始化项目目录结构
- 扫描目录并构建 `AssetItem`
- 维护当前路径栈
- 创建文件夹、删除项、移动项
- `<Project>/Assets` 扫描为 `AssetItem` 树,供 [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) 消费
- 维护浏览路径、当前选择与刷新后的状态恢复
- 处理创建、删除、移动、重命名等基础文件系统操作
## 当前实现说明
因此它更准确的定位是:
- `Initialize(projectPath)` 会确保 `<project>/Assets` 以及若干默认子目录存在。
- 若第一次初始化发现目录不存在,会自动创建 `Textures/Models/Scripts/Materials/Scenes` 等目录,并生成少量示例文件。
- `GetCurrentItems()` 返回当前路径末尾节点的 `children`
- 目录扫描时,文件夹会排在文件前面,并按名字排序。
- 资源类型当前按扩展名启发式推断,例如 `.png -> Texture``.fbx -> Model``.cs/.cpp/.h -> Script`
- editor 侧项目文件系统投影层
- Project Browser 的默认执行层
## 当前实现边界
而不是 engine 侧资产数据库本身。
- 目前是实时扫描树,不是文件系统监听模式。
- 异常处理大多是吞掉异常并保持当前状态,错误反馈很弱。
- 类型判断完全靠扩展名映射,不依赖资源导入数据库。
## 内部状态模型
当前长期持有的核心状态只有四类:
| 状态 | 作用 |
|------|------|
| `m_rootFolder` | 当前 `Assets` 根节点。 |
| `m_path` | 当前浏览路径的目录节点栈,也是面包屑来源。 |
| `m_selectedItemPath` | 当前选中项的完整路径字符串。 |
| `m_projectPath` | 当前项目根目录。 |
这套设计的直接收益是:
- 刷新树时,不需要保留旧 `AssetItem` 指针身份
- 只要路径还能重新解析,当前目录和当前选中项就能在重建后恢复
## 扫描与类型推断
### 根目录初始化
[Initialize](Initialize.md) 当前固定把 `<project>/Assets` 当作根目录:
- 若目录不存在,会先创建 `Assets/``Assets/Scenes/`
- 根节点名称固定重写为 `Assets`
- 初始化后 `m_path` 只包含根节点
它不会额外创建旧文档里提到的演示子目录,也不会在这里预热资产数据库。
### 递归扫描
`ScanDirectory(...)` 当前会:
- 遍历目录项
- 对子目录递归构建子 `AssetItem`
- 跳过显示层面的 `.meta` 文件
- 把目录排在文件前面,再按名称排序
这意味着 `ProjectPanel` 看到的是“过滤掉 `.meta` 侧车文件后的浏览树”,而不是磁盘上的逐项镜像。
### 类型推断
文件项当前主要按扩展名启发式推断:
| 扩展名 | 当前类型 |
|------|------|
| 图片扩展名 | `Texture` |
| `.fbx/.obj/.gltf/.glb` | `Model` |
| `.cs/.cpp/.h` | `Script` |
| `.mat` | `Material` |
| `.xc/.unity/.scene` | `Scene` |
| `.prefab` | `Prefab` |
| 其他 | `File` |
同时还会记录:
- `extensionLower`
- `isImageAsset`
- `canUseImagePreview`
`ProjectPanel` 的图标与缩略图策略使用。
## 路径导航与选择恢复
### 路径导航
当前路径相关公开 API 基本都围绕 `m_path` 工作:
- `NavigateToFolder(...)`
- 重新解析从根到目标目录的完整路径
- 成功后清空当前选择
- `NavigateBack()`
- 只在 `m_path.size() > 1` 时后退
- 后退后清空当前选择
- `NavigateToIndex(index)`
- 直接截断面包屑到指定层级
- 同样会清空当前选择
`GetCurrentPath()` 始终返回从 `"Assets"` 开始的逻辑路径,而不是磁盘绝对路径。
### 选择恢复
`ProjectManager` 当前把选择保存成 `m_selectedItemPath`,而不是保留旧 `AssetItemPtr`
因此:
- `RefreshCurrentFolder()` 重建目录树后,只要同一路径仍存在,选择可以恢复
- 若原路径不存在,则 `SyncSelection()` 自动清除选择
这也是 `ProjectSelectionSurvivesRefreshWhenItemOrderChanges` 能成立的根本原因。
## 基础文件操作
当前真正值得文档化的,不是 getter / setter而是这几组带规则的写操作。
### 创建目录
[CreateFolder](CreateFolder.md) 当前会:
- trim 名称
- 拒绝空名、`.``..` 和 Windows 非法文件名字符
- 在当前目录下生成唯一目录名,如 `New Folder 1`
- 刷新树并自动选中新目录
### 删除资源
[DeleteItem](DeleteItem.md) 当前会:
- 拒绝空路径、根目录本身和项目根之外的路径
- 删除目标本体
- 额外删除同名 `.meta` sidecar
- 必要时清空选择
- 刷新目录树
这里删除 `.meta` 并不是“顺手清理垃圾文件”,而是为了避免资源本体被移除后仍残留旧 GUID / 导入元数据。
### 移动资源
[MoveItem](MoveItem.md) 当前会:
- 要求源和目标都位于当前 `Assets` 根目录之下
- 要求目标是已存在目录
- 禁止移动根目录本身
- 禁止把目录移动到自己的子目录
- 禁止覆盖已有同名目标
- 成功后同步移动 `.meta` sidecar
同步移动 sidecar 的意义同样是保持资源身份与导入配置跟随资源本体一起迁移,而不是把 `.meta` 留在旧路径。
### 重命名资源
[RenameItem](RenameItem.md) 当前会:
- trim 并校验新名字
- 对文件项在“不写扩展名”时自动保留原扩展名
- 支持 Windows 下 case-only rename
- 成功后同步重命名 `.meta` sidecar
- 若被重命名项正好是当前选中项,会同步更新 `m_selectedItemPath`
因此当前重命名语义已经不只是 UI 上改显示名而是显式维护资源路径、sidecar 和选择状态的一致性。
## 公开方法
| 方法 | 说明 |
|------|------|
| [Initialize](Initialize.md) | 初始化项目根、确保 `Assets/Scenes` 存在并建立根目录树。 |
| [RefreshCurrentFolder](RefreshCurrentFolder.md) | 重建目录树并尽量保留当前路径与当前选择。 |
| [CreateFolder](CreateFolder.md) | 在当前目录创建唯一命名的新文件夹。 |
| [DeleteItem](DeleteItem.md) | 删除项目内资源或目录,并移除同名 `.meta` sidecar。 |
| [MoveItem](MoveItem.md) | 在项目目录内移动资源或目录,并同步移动 `.meta` sidecar。 |
| [RenameItem](RenameItem.md) | 重命名资源或目录,支持保留扩展名与 case-only rename。 |
其余 getter / setter / 导航方法当前语义较直接,类型页中已概述,不再逐个拆页。
## 测试与行为锚点
和当前 `ProjectManager` 直接相关的测试锚点主要在 `tests/editor/test_action_routing.cpp`
- `ProjectCommandsCreateFolderMoveAssetAndOpenFolderHelper`
- `ProjectCommandsCreateFolderUsesUniqueDefaultName`
- `ProjectCommandsRenameAssetUpdatesSelectionAndPreservesFileExtension`
- `ProjectCommandsRejectInvalidMoveTargets`
- `ProjectSelectionSurvivesRefreshWhenItemOrderChanges`
- `ProjectCommandsRejectMovingFolderIntoItsDescendant`
这些测试虽然多数通过 `ProjectCommands` 外层 helper 进入,但已经把 `ProjectManager` 的真实文件系统行为钉得比较牢。
## 当前限制
- 当前仍是主动扫描树,不是文件系统监听器模型。
- 类型识别仍然是扩展名启发式,不直接查询资产数据库。
- 目录扫描和大部分文件系统错误当前都以“吞异常并保持可用”为主,诊断能力较弱。
- `.meta` 文件只在变更操作时被显式跟随处理,不会单独显示在浏览树里。
## 相关文档
- [Managers](../Managers.md)
- [IProjectManager](../../Core/IProjectManager/IProjectManager.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md)
- [AssetItem](../../Core/AssetItem/AssetItem.md)
- [AssetDatabase](../../../Core/Asset/AssetDatabase/AssetDatabase.md)

View File

@@ -0,0 +1,38 @@
# ProjectManager::RefreshCurrentFolder
```cpp
void RefreshCurrentFolder();
```
## 当前行为
这个方法当前不是“只刷新当前目录 children 列表”,而是调用内部 `RebuildTreePreservingPath()` 重建整棵 `Assets` 树,然后尽量恢复当前浏览状态。
内部主要流程是:
1. 记录旧的目录路径栈 `m_path`
2. 重新从 `<project>/Assets` 扫描整棵树
3. 重新解析旧路径栈,尽量恢复当前面包屑位置
4. 通过 `SyncSelection()` 重新校验当前 `m_selectedItemPath`
## 保留语义
- 若原浏览路径中的每一级目录仍然存在,则当前目录会被保留
- 若中间某一级目录已不存在,则恢复过程会在该层停止
- 若当前选中项路径仍能在当前目录项中重新解析,选择会保留
- 若当前选中项已不存在,则选择会被清空
这也是当前刷新后即使目录项顺序变化,选择仍可恢复的原因。
## 失败语义
当前实现把异常吞掉,因此:
- 刷新失败时不会抛异常
- 也不会提供错误返回值
- 上层看到的通常是“保持旧状态”
## 相关文档
- [ProjectManager](ProjectManager.md)
- [Initialize](Initialize.md)

View File

@@ -0,0 +1,66 @@
# ProjectManager::RenameItem
```cpp
bool RenameItem(const std::string& sourceFullPath, const std::string& newName);
```
## 当前行为
`RenameItem(...)` 当前支持目录与文件重命名,但文件和目录的规则并不完全一样。
前置校验包括:
- `sourceFullPath` 非空
- `m_rootFolder` 已存在
- 新名字在 trim 后仍非空
- 新名字不是 `.` / `..`
- 新名字不包含 Windows 非法字符
- 源路径存在且位于项目根目录之下
- 源路径不是根目录本身
## 文件与目录的差异
### 目录
- 目录直接使用 trim 后的新名字
### 文件
当前会先走内部 `BuildRenamedEntryName(...)`
- 若新名字本身已经带扩展名,就直接使用该文件名
- 若新名字没带扩展名,则自动保留原文件扩展名
这也是 `ProjectCommandsRenameAssetUpdatesSelectionAndPreservesFileExtension` 这个测试成立的原因。
## case-only rename
Windows 下大小写不敏感,直接把 `foo.txt` 改成 `Foo.txt` 往往不能按普通重命名处理。
当前实现专门走了 `RenamePathCaseAware(...)`
- 普通路径变化时直接 `fs::rename(...)`
- 仅大小写变化时,先改到一个临时路径,再改到目标路径
因此 case-only rename 当前是被显式支持的。
## 成功路径
真正重命名成功后,当前还会:
1. 同步重命名 `.meta` sidecar
2. 若当前选中的正是原路径,则把 `m_selectedItemPath` 更新到新路径
3. 调用 [RefreshCurrentFolder](RefreshCurrentFolder.md)
这意味着当前实现维护的是“资源文件 + sidecar + 当前选择”三者一致性,而不只是单纯改一个文件名。
## 返回值语义
- 重命名成功并刷新完成时返回 `true`
- 任一校验失败、目标名非法或文件系统异常时返回 `false`
## 相关文档
- [ProjectManager](ProjectManager.md)
- [MoveItem](MoveItem.md)
- [DeleteItem](DeleteItem.md)