///|
/// 校验错误类型
pub(all) struct ValidationError {
/// 校验规则路径(例如 "scenes[0].nodes[2]")
path : String
/// 错误详细说明
message : String
} derive(Debug, ToJson, Eq)
///|
/// 校验报告
pub(all) struct ValidationReport {
is_valid : Bool
errors : Array[ValidationError]
warnings : Array[ValidationError]
} derive(Debug, ToJson, Eq)
///|
/// 校验 glTF 文档完整性与规范合规性
///
/// # Example
/// ```mbt check
/// test "validation invalid node index" {
/// let doc = @gltf.GltfDocument::{
/// version: "2.0",
/// generator: None,
/// copyright: None,
/// scenes: [{ name: Some("Scene"), nodes: [99] }], // 99 是无效的 node 索引
/// nodes: [
/// {
/// name: Some("Root"),
/// children: [],
/// mesh: None,
/// skin: None,
/// camera: None,
/// translation: None,
/// rotation: None,
/// scale: None,
/// },
/// ],
/// meshes: [],
/// materials: [],
/// textures: [],
/// images: [],
/// samplers: [],
/// accessors: [],
/// buffer_views: [],
/// buffers: [],
/// animations: [],
/// skins: [],
/// default_scene: Some(0),
/// }
/// let report = @gltf.validate_document(doc)
/// inspect(report.is_valid, content="false")
/// inspect(report.errors.length(), content="1")
/// }
/// ```
pub fn validate_document(doc : GltfDocument) -> ValidationReport {
let errors : Array[ValidationError] = []
let warnings : Array[ValidationError] = []
// 1. 验证基础元数据
if doc.version != "2.0" {
errors.push({
path: "asset.version",
message: "Unsupported glTF version: \{doc.version}. Only 2.0 is supported.",
})
}
// 2. 验证场景引用
for s_idx, scene in doc.scenes {
for n_idx, node_idx in scene.nodes {
if node_idx < 0 || node_idx >= doc.nodes.length() {
errors.push({
path: "scenes[\{s_idx}].nodes[\{n_idx}]",
message: "Node index \{node_idx} is out of bounds (total nodes: \{doc.nodes.length()})",
})
}
}
}
// 3. 验证节点层次结构
for n_idx, node in doc.nodes {
for c_idx, child_idx in node.children {
if child_idx < 0 || child_idx >= doc.nodes.length() {
errors.push({
path: "nodes[\{n_idx}].children[\{c_idx}]",
message: "Child node index \{child_idx} is out of bounds",
})
}
if child_idx == n_idx {
errors.push({
path: "nodes[\{n_idx}].children[\{c_idx}]",
message: "Self-referencing node cycle detected",
})
}
}
match node.mesh {
Some(mesh_idx) =>
if mesh_idx < 0 || mesh_idx >= doc.meshes.length() {
errors.push({
path: "nodes[\{n_idx}].mesh",
message: "Mesh index \{mesh_idx} is out of bounds (total meshes: \{doc.meshes.length()})",
})
}
None => ()
}
}
// 4. 验证网格与材质引用
for m_idx, mesh in doc.meshes {
for p_idx, prim in mesh.primitives {
match prim.indices {
Some(acc_idx) =>
if acc_idx < 0 || acc_idx >= doc.accessors.length() {
errors.push({
path: "meshes[\{m_idx}].primitives[\{p_idx}].indices",
message: "Accessor index \{acc_idx} is out of bounds",
})
}
None => ()
}
match prim.material {
Some(mat_idx) =>
if mat_idx < 0 || mat_idx >= doc.materials.length() {
errors.push({
path: "meshes[\{m_idx}].primitives[\{p_idx}].material",
message: "Material index \{mat_idx} is out of bounds",
})
}
None => ()
}
for attr_name, acc_idx in prim.attributes {
if acc_idx < 0 || acc_idx >= doc.accessors.length() {
errors.push({
path: "meshes[\{m_idx}].primitives[\{p_idx}].attributes['\{attr_name}']",
message: "Accessor index \{acc_idx} is out of bounds",
})
}
}
}
}
// 5. 验证材质引用的纹理
for mat_idx, mat in doc.materials {
match mat.pbr_metallic_roughness.base_color_texture {
Some(tex_info) =>
if tex_info.index < 0 || tex_info.index >= doc.textures.length() {
errors.push({
path: "materials[\{mat_idx}].pbrMetallicRoughness.baseColorTexture.index",
message: "Texture index \{tex_info.index} is out of bounds",
})
}
None => ()
}
match mat.normal_texture {
Some(tex_info) =>
if tex_info.index < 0 || tex_info.index >= doc.textures.length() {
errors.push({
path: "materials[\{mat_idx}].normalTexture.index",
message: "Texture index \{tex_info.index} is out of bounds",
})
}
None => ()
}
}
// 6. 验证纹理、图像、采样器
for tex_idx, tex in doc.textures {
match tex.sampler {
Some(sam_idx) =>
if sam_idx < 0 || sam_idx >= doc.samplers.length() {
errors.push({
path: "textures[\{tex_idx}].sampler",
message: "Sampler index \{sam_idx} is out of bounds",
})
}
None => ()
}
match tex.source {
Some(img_idx) =>
if img_idx < 0 || img_idx >= doc.images.length() {
errors.push({
path: "textures[\{tex_idx}].source",
message: "Image index \{img_idx} is out of bounds",
})
}
None => ()
}
}
// 7. 验证访问器与缓冲视口
for acc_idx, acc in doc.accessors {
match acc.buffer_view {
Some(view_idx) =>
if view_idx < 0 || view_idx >= doc.buffer_views.length() {
errors.push({
path: "accessors[\{acc_idx}].bufferView",
message: "BufferView index \{view_idx} is out of bounds",
})
}
None => ()
}
}
for view_idx, view in doc.buffer_views {
if view.buffer < 0 || view.buffer >= doc.buffers.length() {
errors.push({
path: "bufferViews[\{view_idx}].buffer",
message: "Buffer index \{view.buffer} is out of bounds",
})
}
}
{ is_valid: errors.length() == 0, errors, warnings }
}