Skip to main content

geometry_dag/
validation.rs

1//! 输入验证:全部 fail-closed,与 TS `inputValidation.ts` 的拒绝条件逐条对应。
2
3use crate::error::{DagError, DagResult};
4use crate::types::{
5    IndexedGeometry, MESHLET_MAX_TRIANGLES_LIMIT, MESHLET_MAX_VERTICES_LIMIT, SOURCE_TRIANGLES_BUDGET,
6    SOURCE_VERTICES_BUDGET,
7};
8
9/// 已验证的构建输入。
10#[derive(Debug, Clone)]
11pub struct ValidatedInput {
12    /// 通过验证的几何。
13    pub geometry: IndexedGeometry,
14    /// 簇顶点上限(1..=64)。
15    pub max_vertices: u32,
16    /// 簇三角形上限(1..=126)。
17    pub max_triangles: u32,
18}
19
20/// 验证网格与簇参数;任何非法输入返回带定位信息的错误。
21///
22/// 校验项与 TS `validateMeshletInput` 一致:positions 为紧凑 XYZ 三元组、indices 为完整
23/// 三角形、全部位置分量有限、全部索引落在顶点范围内、顶点/三角形数不超源预算、
24/// 簇上限在 `1..=默认上限` 区间内。
25///
26/// # Errors
27/// 见 [`DagError::InvalidInput`] / [`DagError::BudgetExceeded`]。
28pub fn validate_input(
29    geometry: &IndexedGeometry,
30    max_vertices: Option<u32>,
31    max_triangles: Option<u32>,
32) -> DagResult<ValidatedInput> {
33    if !geometry.positions.len().is_multiple_of(3) {
34        return Err(DagError::invalid_input(
35            "positions must contain tightly packed XYZ triples.",
36        ));
37    }
38    if !geometry.indices.len().is_multiple_of(3) {
39        return Err(DagError::invalid_input(
40            "indices must contain complete triangles.",
41        ));
42    }
43    let vertex_count = geometry.vertex_count();
44    let triangle_count = geometry.triangle_count();
45    budget(vertex_count as u64, SOURCE_VERTICES_BUDGET, "source vertices")?;
46    budget(triangle_count as u64, SOURCE_TRIANGLES_BUDGET, "source triangles")?;
47    if geometry.positions.iter().any(|v| !v.is_finite()) {
48        return Err(DagError::invalid_input("Position components must be finite."));
49    }
50    for (offset, &index) in geometry.indices.iter().enumerate() {
51        if index as usize >= vertex_count {
52            return Err(DagError::invalid_input(format!(
53                "Index {offset} ({index}) is outside the source vertex range ({vertex_count})."
54            )));
55        }
56    }
57    let max_vertices = meshlet_limit(max_vertices, MESHLET_MAX_VERTICES_LIMIT, "maxVertices")?;
58    let max_triangles = meshlet_limit(max_triangles, MESHLET_MAX_TRIANGLES_LIMIT, "maxTriangles")?;
59    Ok(ValidatedInput { geometry: geometry.clone(), max_vertices, max_triangles })
60}
61
62/// TS `budget`:超出上限即报错。
63pub(crate) fn budget(value: u64, maximum: u64, label: &str) -> DagResult<()> {
64    if value > maximum {
65        return Err(DagError::budget_exceeded(label, maximum));
66    }
67    Ok(())
68}
69
70/// TS `meshletLimit`:1..=maximum 的整数上限。
71fn meshlet_limit(value: Option<u32>, maximum: u32, label: &str) -> DagResult<u32> {
72    let resolved = value.unwrap_or(maximum);
73    if resolved < 1 || resolved > maximum {
74        return Err(DagError::invalid_input(format!(
75            "{label} must be an integer in 1..{maximum}."
76        )));
77    }
78    Ok(resolved)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    fn geometry(vertex_count: usize, triangle_count: usize) -> IndexedGeometry {
86        IndexedGeometry {
87            positions: vec![0.0; vertex_count * 3],
88            indices: vec![0; triangle_count * 3],
89        }
90    }
91
92    #[test]
93    fn accepts_valid_geometry() {
94        let mut g = geometry(4, 2);
95        g.indices.copy_from_slice(&[0, 1, 2, 1, 2, 3]);
96        assert!(validate_input(&g, None, None).is_ok());
97    }
98
99    #[test]
100    fn rejects_misaligned_arrays() {
101        // positions 缺一个分量(2.5 个顶点)。
102        let mut g = geometry(2, 1);
103        g.positions.truncate(5);
104        let err = validate_input(&g, None, None).unwrap_err();
105        assert!(err.to_string().contains("tightly packed"), "{err}");
106        // indices 缺一个分量(2.5 个三角形)。
107        let mut g = geometry(3, 2);
108        g.indices.truncate(5);
109        let err = validate_input(&g, None, None).unwrap_err();
110        assert!(err.to_string().contains("complete triangles"), "{err}");
111    }
112
113    #[test]
114    fn rejects_out_of_range_index() {
115        let mut g = geometry(2, 1);
116        g.indices.copy_from_slice(&[0, 1, 2]);
117        let err = validate_input(&g, None, None).unwrap_err();
118        assert!(err.to_string().contains("outside the source vertex range"));
119    }
120
121    #[test]
122    fn rejects_non_finite_positions() {
123        let mut g = geometry(1, 0);
124        g.positions[1] = f32::NAN;
125        assert!(validate_input(&g, None, None).is_err());
126        g.positions[1] = f32::INFINITY;
127        assert!(validate_input(&g, None, None).is_err());
128    }
129
130    #[test]
131    fn rejects_bad_meshlet_limits() {
132        let g = geometry(3, 1);
133        assert!(validate_input(&g, Some(0), None).is_err());
134        assert!(validate_input(&g, Some(65), None).is_err());
135        assert!(validate_input(&g, None, Some(127)).is_err());
136        assert!(validate_input(&g, Some(64), Some(126)).is_ok());
137    }
138
139    #[test]
140    fn rejects_empty_meshlet_limit() {
141        let g = geometry(0, 0);
142        assert!(validate_input(&g, Some(0), Some(0)).is_err());
143    }
144}