docs(api): add gaussian splat pages and fix doc generators

This commit is contained in:
2026-04-10 17:12:55 +08:00
parent 66ae9ec919
commit f917040e9a
8 changed files with 246 additions and 104 deletions

View File

@@ -4,20 +4,20 @@
**类型**: `submodule` **类型**: `submodule`
**描述**: Gaussian Splat 资源子模块,覆盖运行时 splat payload、artifact I/O 与对应 loader。 **描述**: 3DGS 资源子模块,覆盖 `GaussianSplat` 运行时资源、`.xcgsplat` artifact 协议与标准 loader。
## 概 ## 概
`Resources::GaussianSplat` 当前负责把压缩后的 splat payload 以资源形式接入引擎。按当前实现,这条链路分成三层: `Resources/GaussianSplat` 当前把 3DGS 资源链拆成三层:
1. `GaussianSplat` - [GaussianSplat](GaussianSplat/GaussianSplat.md)
- 保存 metadata、section table 与原始 payload 运行时资源对象,保存 section 元数据与 payload
2. `GaussianSplatArtifactIO` - [GaussianSplatArtifactIO](GaussianSplatArtifactIO/GaussianSplatArtifactIO.md)
- 读写 `.xcgsplat` artifact并在加载时构造 `GaussianSplat` `.xcgsplat` 的写入与回读协议。
3. `GaussianSplatLoader` - [GaussianSplatLoader](GaussianSplatLoader/GaussianSplatLoader.md)
- 作为 `IResourceLoader` 接入 `ResourceManager` 只面向 `.xcgsplat` artifact 的标准资源加载器。
和 [Model](../Model/Model.md) / [Mesh](../Mesh/Mesh.md) 这类图结构资源不同,`GaussianSplat` 更像“多 section 二进制块 + metadata”的只读 payload 资源 这条链路当前只负责资源层与 artifact 层,不在这里直接创建 GPU residency也不在这里承担最终渲染 pass
## 头文件 ## 头文件
@@ -28,5 +28,6 @@
## 相关文档 ## 相关文档
- [Resources](../Resources.md) - [Resources](../Resources.md)
- [Core / Asset](../../Core/Asset/Asset.md) - [Volume](../Volume/Volume.md)
- [Model](../Model/Model.md) - [Rendering](../../Rendering/Rendering.md)
- [API 总索引](../../../main.md)

View File

@@ -8,54 +8,62 @@
**源文件**: `engine/src/Resources/GaussianSplat/GaussianSplat.cpp` **源文件**: `engine/src/Resources/GaussianSplat/GaussianSplat.cpp`
**描述**: Gaussian Splat 运行时资源对象,保存 metadata、section table 与原始 payload。 **描述**: 3DGS 运行时资源对象,保存 splat metadata、section table 和 artifact payload 字节数据
## 概 ## 概
`GaussianSplat` 当前把 splat 数据拆成三层状态 `GaussianSplat` 当前 3DGS 资源在运行时的最小封装。它并不直接创建 GPU buffer 或 texture而是把 `.xcgsplat` loader 读出的内容保存在 CPU 侧,供后续
- `GaussianSplatMetadata` - 资源系统缓存
- 内容版本、splat/chunk/camera 数量、bounds 与 section format - artifact round-trip
- `GaussianSplatSection` - 渲染缓存层上传 GPU 资源
- 每个 section 的类型、格式、payload 偏移、大小和元素布局
- `payload`
- 实际的二进制块数据
这让资源对象可以在不理解每种 section 语义的前提下,统一保存和查询 payload 使用
## 声明概览
| 声明 | 类型 | 说明 |
|------|------|------|
| `GaussianSplatSectionType` | `enum class` | section 语义类型,区分 `Positions``Color``SH``Chunks` 等 payload 分区。 |
| `GaussianSplatSectionFormat` | `enum class` | section 数据布局格式,描述 float/packed/color/sh/chunk 等编码方式。 |
| `GaussianSplatSection` | `struct` | 单个 section 的偏移、大小、元素数和 stride 描述。 |
| `GaussianSplatMetadata` | `struct` | 资源级 metadata记录 splat 数量、bounds 与各 section 的格式。 |
## 当前状态模型 ## 当前状态模型
| 字段 | 说明 | | 字段 | 说明 |
|------|------| |------|------|
| `m_metadata` | 版本、bounds、计数 section format | | `m_metadata` | 当前资源的 splat 数量、bounds、chunk/camera 计数 section format |
| `m_sections` | section 描述表 | | `m_sections` | section table描述每个 payload 分段的偏移和布局 |
| `m_payload` | 原始二进制 payload | | `m_payload` | artifact 读回的原始 payload 字节数组 |
## 当前实现行为 ## 当前实现行为
- `CreateOwned(...)` - `CreateOwned(...)`
- 先验 section table再接管 metadata / sections / payload -验 section 类型唯一且范围不越界
- `FindSection(...)` - 成功时接管 section 与 payload 所有权,并把资源标记为 valid
-`GaussianSplatSectionType` 查找 section 记录
- `GetSectionData(...)`
- 根据 section 的 `dataOffset` 返回 payload 里的起始地址
- `Clear()` - `Clear()`
- 清空 metadata、section table 和 payload把资源标记为 invalid - 清空 metadata、section table 和 payload重置 valid 状态
- `FindSection(...)`
- 按 section type 线性查找当前资源里的分段定义
- `GetSectionData(...)`
- 若目标 section 不存在或 `dataSize == 0`,返回 `nullptr`
- 否则返回 payload 中对应偏移的只读指针
- `Release()` - `Release()`
- 当前直接执行 `delete this` - 当前直接执行 `delete this`
## 测试与调用链 ## 测试与调用链
- `tests/Resources/GaussianSplat/test_gaussian_splat.cpp`
- 覆盖 `CreateOwned(...)` 的 metadata / payload 保存与非法 section layout 拒绝
- `engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp` - `engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp`
- 当前把 artifact 读回结果装配成 `GaussianSplat` - 当前通过 `CreateOwned(...)` 构造 loader 读回的运行时资源
- `GaussianSplatLoader`
- 负责把 `.xcgsplat` artifact 交给资源系统并恢复成 `GaussianSplat`
## 当前实现边界 ## 当前实现边界
- 当前对象只提供按 section 粗粒度查询,不做更高层语义解码 - 当前资源只保存 CPU 侧 metadata 和 payload不直接持有 GPU 资源
- `ValidateSections(...)` 会拒绝 `Unknown` section、越界 payload 区间和重复 section type - section 校验当前只保证类型唯一和边界合法,不做更高层的 3DGS 语义验证
- payload 生命周期完全由资源对象拥有,不借用外部内存 - 后续 GPU residency cache 需要自行决定哪些 section 被上传成 buffer、哪些被上传成 texture
## 相关文档 ## 相关文档

View File

@@ -8,48 +8,41 @@
**源文件**: `engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp` **源文件**: `engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp`
**描述**: `.xcgsplat` artifact 的读写入口,负责把 Gaussian Splat payload 序列化到磁盘并恢复运行时资源 **描述**: `.xcgsplat` artifact 的读写入口,负责把 `GaussianSplat` 资源写成磁盘协议并恢复运行时对象
## 概 ## 概
`GaussianSplatArtifactIO` 当前公开两个函数: `GaussianSplatArtifactIO` 当前公开两个函数:
- `WriteGaussianSplatArtifactFile(...)` - `WriteGaussianSplatArtifactFile(...)`
-`GaussianSplat` 的 metadata、section table 和 payload 写成 `.xcgsplat` -`GaussianSplat` 写成 `.xcgsplat`
- `LoadGaussianSplatArtifact(...)` - `LoadGaussianSplatArtifact(...)`
- 读取 `.xcgsplat` 并恢复 `GaussianSplat` - 读取 `.xcgsplat` 并恢复 `GaussianSplat`
这两个函数共同构成当前 3DGS 资源的 artifact 协议边界。
## 当前实现行为 ## 当前实现行为
### 写入路径 ### 写入路径
- 会先解析 artifact 路径并在需要时创建父目录。 - 会先解析 artifact 路径,必要时创建父目录。
- 文件头 magic 当前校验为 `XCGSP01` 协议 - 文件头当前写入 `GaussianSplatArtifactFileHeader`magic 固定校验为 `XCGSP01`
- 写入顺序是: - metadata 会序列化 splat 数量、chunk/camera 计数、bounds 和各 section format。
- `GaussianSplatArtifactFileHeader` - section table 会顺序写入 section type、format、offset、size、elementCount 与 stride。
- `GaussianSplatArtifactHeader` - 若 payload 非空,最后会把整段 payload 原样写入文件尾。
- section table
- payload 字节块
### 读取路径 ### 读取路径
- 会解析真实路径,必要时回退到 `ResourceManager::Get().GetResourceRoot()` -解析真实路径,必要时回退到 `ResourceManager::Get().GetResourceRoot()`
- header 校验失败会直接返回错误的 `LoadResult` - 会校验 file header 和 `kGaussianSplatArtifactSchemaVersion`
- 读取 section table 后会还原成 `GaussianSplatSection` 数组,再读 payload - 读取 section table 后恢复 `GaussianSplatSection` 数组。
- 成功时通过内部 `CreateOwnedGaussianSplatResource(...)` 构造资源对象 - payload 读回后会通过内部 `CreateOwnedGaussianSplatResource(...)` 构造运行时资源。
## 测试与调用链
- `tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp`
- 覆盖写 artifact、再由 loader 读回的往返路径
- `engine/src/Resources/GaussianSplat/GaussianSplatLoader.cpp`
- 当前把实际 `.xcgsplat` 读取委托给 `LoadGaussianSplatArtifact(...)`
## 当前实现边界 ## 当前实现边界
- 当前只处理 `.xcgsplat` artifact不直接解析外部原始 Gaussian Splat 源格式 - 当前只处理 `.xcgsplat` artifact不直接解析 `.ply` 等 source asset
- artifact 的 section 语义依赖 header 里的 metadata/format不在这里做高层解释 - schema version 不匹配时直接失败,不做向后兼容恢复
- 读取失败时直接返回错误,不做容错修复 - artifact 协议当前只保证 section 化 payload 的稳定保存,不承担 GPU 资源预热
## 相关文档 ## 相关文档

View File

@@ -10,9 +10,9 @@
**描述**: `GaussianSplat` 资源的标准 loader负责识别 `.xcgsplat` artifact 并通过 `GaussianSplatArtifactIO` 读入运行时资源。 **描述**: `GaussianSplat` 资源的标准 loader负责识别 `.xcgsplat` artifact 并通过 `GaussianSplatArtifactIO` 读入运行时资源。
## 概 ## 概
`GaussianSplatLoader` 继承自 `IResourceLoader`,是 `ResourceManager` 当前注册的 Gaussian Splat 资源入口。实现策略和 `ModelLoader` 类似 `GaussianSplatLoader` 继承自 `IResourceLoader`,是当前 3DGS 资源进入 `ResourceManager` 的正式入口。它的实现刻意保持很窄
- 只接受 `.xcgsplat` - 只接受 `.xcgsplat`
- 实际反序列化委托给 `LoadGaussianSplatArtifact(...)` - 实际反序列化委托给 `LoadGaussianSplatArtifact(...)`
@@ -38,12 +38,11 @@
- `GetDefaultSettings()` 当前返回 `nullptr` - `GetDefaultSettings()` 当前返回 `nullptr`
- 文件末尾通过 `REGISTER_RESOURCE_LOADER(GaussianSplatLoader)` 注册到资源系统 - 文件末尾通过 `REGISTER_RESOURCE_LOADER(GaussianSplatLoader)` 注册到资源系统
## 测试与调用链 ## 当前实现边界
- `tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp` - 当前只面向 `.xcgsplat` artifact不承担 `.ply` source import。
- 覆盖扩展名判断、非法路径、artifact 往返加载与 `ResourceManager` 注册 - loader 只构造 CPU 侧运行时资源,不直接创建设备侧 GPU 资源。
- `engine/src/Core/Asset/ResourceManager.cpp` - 更底层的 schema 校验与 payload 反序列化继续留在 [GaussianSplatArtifactIO](../GaussianSplatArtifactIO/GaussianSplatArtifactIO.md)。
- 当前持有全局 `GaussianSplatLoader` 并注册到资源系统
## 相关文档 ## 相关文档

View File

@@ -1,13 +1,13 @@
# API 文档重构状态 # API 文档重构状态
**生成时间**: `2026-04-10 17:07:09` **生成时间**: `2026-04-10 17:09:30`
**来源**: `docs/api/_tools/audit_api_docs.py` **来源**: `docs/api/_tools/audit_api_docs.py`
## 摘要 ## 摘要
- Markdown 页面数(全部): `3934` - Markdown 页面数(全部): `3969`
- Markdown 页面数canonical: `3906` - Markdown 页面数canonical: `3941`
- Public headers 数: `384` - Public headers 数: `384`
- `XCEditor` public headers 数: `64`canonical 已覆盖 `64` - `XCEditor` public headers 数: `64`canonical 已覆盖 `64`
- `XCEngine` public headers 数: `320`canonical 已覆盖 `320` - `XCEngine` public headers 数: `320`canonical 已覆盖 `320`

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import argparse
import os import os
import re import re
from dataclasses import dataclass from dataclasses import dataclass
@@ -10,7 +11,11 @@ from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
DOC_ROOT = SCRIPT_DIR.parent if SCRIPT_DIR.name == "_tools" else SCRIPT_DIR DOC_ROOT = SCRIPT_DIR.parent if SCRIPT_DIR.name == "_tools" else SCRIPT_DIR
REPO_ROOT = DOC_ROOT.parents[1] REPO_ROOT = DOC_ROOT.parents[1]
INCLUDE_ROOT = REPO_ROOT / "engine" / "include" / "XCEngine" DEFAULT_ROOT_NAME = "XCEngine"
DEFAULT_INCLUDE_ROOT = REPO_ROOT / "engine" / "include" / DEFAULT_ROOT_NAME
INCLUDE_ROOT = DEFAULT_INCLUDE_ROOT
CANONICAL_ROOT_NAME = DEFAULT_ROOT_NAME
CANONICAL_DOC_ROOT = DOC_ROOT / CANONICAL_ROOT_NAME
MAIN_INDEX = DOC_ROOT / "main.md" MAIN_INDEX = DOC_ROOT / "main.md"
@@ -21,7 +26,7 @@ def dir_index_name(doc_dir: Path) -> str:
def dir_index_path(doc_dir: Path) -> Path: def dir_index_path(doc_dir: Path) -> Path:
return doc_dir / dir_index_name(doc_dir) return doc_dir / dir_index_name(doc_dir)
MODULES = ( DEFAULT_MODULES = (
"Audio", "Audio",
"Components", "Components",
"Core", "Core",
@@ -34,6 +39,7 @@ MODULES = (
"Scene", "Scene",
"Threading", "Threading",
) )
MODULES = DEFAULT_MODULES
TYPE_START_RE = re.compile( TYPE_START_RE = re.compile(
r"^(class|struct|enum class|enum)\s+([A-Za-z_]\w*)(?:\s*:\s*public\s+([^{;]+))?" r"^(class|struct|enum class|enum)\s+([A-Za-z_]\w*)(?:\s*:\s*public\s+([^{;]+))?"
) )
@@ -626,7 +632,7 @@ def infer_page_type(stem: str, primary: Declaration | None) -> str:
def scope_label(relative_parts: tuple[str, ...]) -> str: def scope_label(relative_parts: tuple[str, ...]) -> str:
if not relative_parts: if not relative_parts:
return "当前模块" return "当前模块"
return "`XCEngine/" + "/".join(relative_parts) + "` 子目录" return f"`{CANONICAL_ROOT_NAME}/" + "/".join(relative_parts) + "` 子目录"
def infer_description(stem: str, relative_parts: tuple[str, ...]) -> str: def infer_description(stem: str, relative_parts: tuple[str, ...]) -> str:
@@ -637,11 +643,20 @@ def relative_link(from_path: Path, to_path: Path) -> str:
return os.path.relpath(to_path, from_path.parent).replace("\\", "/") return os.path.relpath(to_path, from_path.parent).replace("\\", "/")
def canonical_header_reference(header_path: Path) -> str:
parts = list(header_path.parts)
if "include" in parts:
include_index = parts.index("include")
if include_index + 1 < len(parts):
return Path(*parts[include_index + 1 :]).as_posix()
return header_path.relative_to(INCLUDE_ROOT.parent).as_posix()
def build_page(header_path: Path) -> tuple[str, dict[str, str]]: def build_page(header_path: Path) -> tuple[str, dict[str, str]]:
relative_header = header_path.relative_to(INCLUDE_ROOT.parent).as_posix() relative_header = canonical_header_reference(header_path)
relative_doc_dir = header_path.parent.relative_to(INCLUDE_ROOT) relative_doc_dir = header_path.parent.relative_to(INCLUDE_ROOT)
stem = header_path.stem stem = header_path.stem
doc_path = DOC_ROOT / "XCEngine" / relative_doc_dir / stem / f"{stem}.md" doc_path = CANONICAL_DOC_ROOT / relative_doc_dir / stem / f"{stem}.md"
lines = header_path.read_text(encoding="utf-8", errors="ignore").splitlines() lines = header_path.read_text(encoding="utf-8", errors="ignore").splitlines()
namespace_map = build_namespace_map(lines) namespace_map = build_namespace_map(lines)
@@ -792,11 +807,45 @@ def refresh_dir_index(include_dir: Path, doc_dir: Path) -> None:
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description="Generate canonical API pages from public headers.")
parser.add_argument(
"--root-name",
default=DEFAULT_ROOT_NAME,
help="Canonical doc root name under docs/api, e.g. XCEngine or XCEditor.",
)
parser.add_argument(
"--include-root",
default=str(DEFAULT_INCLUDE_ROOT),
help="Source include root mirrored by the canonical doc tree.",
)
parser.add_argument(
"--modules",
nargs="*",
default=None,
help="Optional explicit top-level module list. Defaults to legacy XCEngine modules or auto-detected directories for other roots.",
)
args = parser.parse_args()
global INCLUDE_ROOT, CANONICAL_ROOT_NAME, CANONICAL_DOC_ROOT, MODULES
INCLUDE_ROOT = Path(args.include_root).resolve()
CANONICAL_ROOT_NAME = args.root_name
CANONICAL_DOC_ROOT = DOC_ROOT / CANONICAL_ROOT_NAME
if args.modules:
MODULES = tuple(args.modules)
elif INCLUDE_ROOT == DEFAULT_INCLUDE_ROOT and CANONICAL_ROOT_NAME == DEFAULT_ROOT_NAME:
MODULES = DEFAULT_MODULES
else:
MODULES = tuple(
path.name
for path in sorted(INCLUDE_ROOT.iterdir())
if path.is_dir()
)
generated = 0 generated = 0
method_generated = 0 method_generated = 0
for module in MODULES: for module in MODULES:
include_module_dir = INCLUDE_ROOT / module include_module_dir = INCLUDE_ROOT / module
doc_module_dir = DOC_ROOT / "XCEngine" / module doc_module_dir = CANONICAL_DOC_ROOT / module
headers = sorted(include_module_dir.rglob("*.h")) headers = sorted(include_module_dir.rglob("*.h"))
for header in headers: for header in headers:
stem = header.stem stem = header.stem

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import argparse
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -9,11 +10,12 @@ from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
API_ROOT = SCRIPT_DIR.parent if SCRIPT_DIR.name == "_tools" else SCRIPT_DIR API_ROOT = SCRIPT_DIR.parent if SCRIPT_DIR.name == "_tools" else SCRIPT_DIR
REPO_ROOT = API_ROOT.parents[1] REPO_ROOT = API_ROOT.parents[1]
INCLUDE_ROOT = REPO_ROOT / "engine" / "include" / "XCEngine" DEFAULT_ROOT_NAME = "XCEngine"
DOC_ROOT = API_ROOT / "XCEngine" DEFAULT_INCLUDE_ROOT = REPO_ROOT / "engine" / "include" / DEFAULT_ROOT_NAME
DESCRIPTION_MAP = { DESCRIPTION_MAPS = {
"XCEngine": {
"XCEngine": "与 `engine/include/XCEngine` 平行的 API 文档根目录。", "XCEngine": "与 `engine/include/XCEngine` 平行的 API 文档根目录。",
"XCEngine/Audio": "音频系统、混音器、效果器与后端接口。", "XCEngine/Audio": "音频系统、混音器、效果器与后端接口。",
"XCEngine/Components": "组件系统与游戏对象 API。", "XCEngine/Components": "组件系统与游戏对象 API。",
@@ -38,6 +40,26 @@ DESCRIPTION_MAP = {
"XCEngine/RHI/OpenGL": "OpenGL 后端 public headers。", "XCEngine/RHI/OpenGL": "OpenGL 后端 public headers。",
"XCEngine/Scene": "场景与场景管理器 API。", "XCEngine/Scene": "场景与场景管理器 API。",
"XCEngine/Threading": "线程封装、同步原语和任务系统。", "XCEngine/Threading": "线程封装、同步原语和任务系统。",
},
"XCEditor": {
"XCEditor": "与 `new_editor/include/XCEditor` 平行的 API 文档根目录。",
"XCEditor/Collections": "Editor 集合视图与滚动/重命名/标签条等组合控件协议。",
"XCEditor/Fields": "Editor inspector 字段控件、交互状态机与 property-grid 协议。",
"XCEditor/Foundation": "Editor 命令、快捷键与主题等基础协作契约。",
"XCEditor/Shell": "Editor 工作区、panel、dock、viewport 与 menu 壳层装配协议。",
"XCEditor/Widgets": "Editor 专用绘制/布局辅助与通用 widget primitive helper。",
},
}
NAMESPACE_MAPS = {
"XCEditor": {
"XCEditor": "XCEngine::UI::Editor",
"XCEditor/Collections": "XCEngine::UI::Editor::Widgets",
"XCEditor/Fields": "XCEngine::UI::Editor::Widgets",
"XCEditor/Foundation": "XCEngine::UI::Editor",
"XCEditor/Shell": "XCEngine::UI::Editor",
"XCEditor/Widgets": "XCEngine::UI::Editor::Widgets",
},
} }
@@ -46,48 +68,64 @@ class DirInfo:
include_dir: Path include_dir: Path
doc_dir: Path doc_dir: Path
relative: str relative: str
root_name: str
namespace: str namespace: str
type_label: str type_label: str
description: str description: str
def to_namespace(relative: str) -> str: def to_namespace(relative: str, root_name: str) -> str:
if relative == "XCEngine": mapped = NAMESPACE_MAPS.get(root_name, {}).get(relative)
return "XCEngine" if mapped:
return mapped
if relative == root_name:
return root_name
return "::".join(relative.split("/")) return "::".join(relative.split("/"))
def get_type_label(relative: str) -> str: def get_type_label(relative: str, root_name: str) -> str:
if relative == "XCEngine": if relative == root_name:
return "module-root" return "module-root"
if relative.count("/") == 1: if relative.count("/") == 1:
return "module" return "module"
return "submodule" return "submodule"
def get_description(relative: str) -> str: def get_description(relative: str, root_name: str) -> str:
return DESCRIPTION_MAP.get(relative, "与当前 public headers 平行的文档目录节点。") return DESCRIPTION_MAPS.get(root_name, {}).get(
relative,
"与当前 public headers 平行的文档目录节点。",
)
def iter_include_dirs() -> list[DirInfo]: def iter_include_dirs(
dirs = [INCLUDE_ROOT] include_root: Path,
dirs.extend(path for path in sorted(INCLUDE_ROOT.rglob("*")) if path.is_dir()) doc_root: Path,
root_name: str,
) -> list[DirInfo]:
dirs = [include_root]
dirs.extend(path for path in sorted(include_root.rglob("*")) if path.is_dir())
items: list[DirInfo] = [] items: list[DirInfo] = []
for include_dir in dirs: for include_dir in dirs:
relative = "XCEngine" relative = root_name
if include_dir != INCLUDE_ROOT: if include_dir != include_root:
relative = "XCEngine/" + include_dir.relative_to(INCLUDE_ROOT).as_posix() relative = root_name + "/" + include_dir.relative_to(include_root).as_posix()
doc_dir = DOC_ROOT if include_dir == INCLUDE_ROOT else DOC_ROOT / include_dir.relative_to(INCLUDE_ROOT) doc_dir = (
doc_root
if include_dir == include_root
else doc_root / include_dir.relative_to(include_root)
)
items.append( items.append(
DirInfo( DirInfo(
include_dir=include_dir, include_dir=include_dir,
doc_dir=doc_dir, doc_dir=doc_dir,
relative=relative, relative=relative,
namespace=to_namespace(relative), root_name=root_name,
type_label=get_type_label(relative), namespace=to_namespace(relative, root_name),
description=get_description(relative), type_label=get_type_label(relative, root_name),
description=get_description(relative, root_name),
) )
) )
return items return items
@@ -116,8 +154,8 @@ def build_dir_index(info: DirInfo) -> str:
index_path = dir_index_path(info.doc_dir) index_path = dir_index_path(info.doc_dir)
title = info.relative.split("/")[-1] title = info.relative.split("/")[-1]
if info.relative == "XCEngine": if info.relative == info.root_name:
title = "XCEngine API 平行目录" title = f"{info.root_name} API 平行目录"
lines: list[str] = [] lines: list[str] = []
lines.append(f"# {title}") lines.append(f"# {title}")
@@ -156,7 +194,7 @@ def build_dir_index(info: DirInfo) -> str:
lines.append("## 相关文档") lines.append("## 相关文档")
lines.append("") lines.append("")
if info.relative == "XCEngine": if info.relative == info.root_name:
lines.append("- [API 总索引](../main.md)") lines.append("- [API 总索引](../main.md)")
lines.append("- [API 文档重构状态](../_meta/rebuild-status.md)") lines.append("- [API 文档重构状态](../_meta/rebuild-status.md)")
else: else:
@@ -167,12 +205,28 @@ def build_dir_index(info: DirInfo) -> str:
def main() -> int: def main() -> int:
DOC_ROOT.mkdir(parents=True, exist_ok=True) parser = argparse.ArgumentParser(description="Scaffold parallel API directory index pages.")
infos = iter_include_dirs() parser.add_argument(
"--root-name",
default=DEFAULT_ROOT_NAME,
help="Canonical doc root name under docs/api, e.g. XCEngine or XCEditor.",
)
parser.add_argument(
"--include-root",
default=str(DEFAULT_INCLUDE_ROOT),
help="Source include root mirrored by the canonical doc tree.",
)
args = parser.parse_args()
include_root = Path(args.include_root).resolve()
doc_root = API_ROOT / args.root_name
doc_root.mkdir(parents=True, exist_ok=True)
infos = iter_include_dirs(include_root, doc_root, args.root_name)
for info in infos: for info in infos:
info.doc_dir.mkdir(parents=True, exist_ok=True) info.doc_dir.mkdir(parents=True, exist_ok=True)
dir_index_path(info.doc_dir).write_text(build_dir_index(info), encoding="utf-8") dir_index_path(info.doc_dir).write_text(build_dir_index(info), encoding="utf-8")
print(f"Generated {len(infos)} directory index files under {DOC_ROOT}") print(f"Generated {len(infos)} directory index files under {doc_root}")
return 0 return 0

View File

@@ -0,0 +1,38 @@
# API 文档目录结构阶段进度XCEditor 与 Model 收口
## 本阶段范围
- 新建 `docs/api/XCEditor/**` canonical 目录树,并生成 `new_editor/include/XCEditor/**` 对应的 header 总览页与方法页。
- 新建 `docs/api/XCEngine/Resources/Model/Model.md` 模块索引,并生成 `Model` / `ModelArtifactIO` / `ModelLoader` / `AssimpModelImporter` 对应页面。
- 处理并发新增的 `docs/api/XCEngine/Resources/GaussianSplat/**`,补齐模块索引与 `GaussianSplat` / `GaussianSplatArtifactIO` / `GaussianSplatLoader` 页面。
- 修正目录脚手架与 canonical 生成器的两个问题:
- 根索引页错误把 `XCEditor` 当成普通子目录,生成了失效的 `../api.md` 链接。
- 窄范围生成 `Resources/Model` 时,头文件引用缺少 `XCEngine/` 前缀。
## 当前结果
执行时间:`2026-04-10`
审计命令:
```powershell
python -B docs/api/_tools/audit_api_docs.py
```
审计结果:
- `Public headers: 381`
- `Valid header refs (canonical): 381`
- `Invalid header refs: 0`
- `Editor source headers: 144`
- `Valid source refs (Editor canonical): 144`
- `Invalid source refs: 0`
- `Broken .md links: 0`
- `Missing directory index pages: 0`
## 并行协作说明
- `docs/api/XCEditor/**` 本轮已经落成,不再作为待认领空树任务。
- `docs/api/XCEngine/Resources/Model/**` 本轮已经补齐目录索引与 header 页面,不再重复认领。
- `docs/api/XCEngine/Resources/GaussianSplat/**` 也已补齐,如头文件继续扩展,请直接在现有树上增量同步。
- 后续如果 `new_editor/include/XCEditor/**``engine/include/XCEngine/Resources/Model/**` 再发生结构变动,应直接基于当前树增量同步,不要回退到“先补目录骨架”的阶段。