Skip to main content

qualia_client_core/
vision_10d_browse.rs

1//! F1 — list / inspect sealed vision `.10d` assets under storage.
2//!
3//! Scans `{storage}/vision_geometry/**/recon.10d` (and sibling `.10d`) for
4//! Library / desktop browse. Does not invent digests; reports file facts only.
5
6use qualia_core_db::container_10d::{
7    header::Container10dHeader, integrity::verify_whole_file_crc32c,
8    mesh_section::decode_mesh_section, node_section::parse_node_header, parse_section_table,
9    section::SectionType,
10};
11use qualia_core_db::render::compile_10d::{compiled_digest, decode_10d_mesh};
12use serde::Serialize;
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Serialize)]
16pub struct Vision10dEntry {
17    pub path: String,
18    pub filename: String,
19    pub media_digest_hex: Option<String>,
20    pub size_bytes: u64,
21    pub section_count: u32,
22    pub has_mesh: bool,
23    pub has_tensor_nodes: bool,
24    pub has_provenance: bool,
25    pub crc_valid: bool,
26    pub compiled_digest_hex: Option<String>,
27    pub mesh_vertices: Option<u32>,
28    pub mesh_triangles: Option<u32>,
29    pub node_count: Option<u32>,
30}
31
32/// List vision recon containers under `{storage_root}/vision_geometry`.
33pub fn list_vision_10d_containers(storage_root: &Path) -> Result<Vec<Vision10dEntry>, String> {
34    let root = storage_root.join("vision_geometry");
35    if !root.exists() {
36        return Ok(Vec::new());
37    }
38    let mut out = Vec::new();
39    scan_vision_dir(&root, storage_root, &mut out)?;
40    out.sort_by(|a, b| a.path.cmp(&b.path));
41    Ok(out)
42}
43
44fn scan_vision_dir(dir: &Path, base: &Path, out: &mut Vec<Vision10dEntry>) -> Result<(), String> {
45    let rd = std::fs::read_dir(dir).map_err(|e| format!("read_dir {}: {e}", dir.display()))?;
46    for ent in rd.flatten() {
47        let path = ent.path();
48        if path.is_dir() {
49            scan_vision_dir(&path, base, out)?;
50            continue;
51        }
52        if path.extension().and_then(|e| e.to_str()) != Some("10d") {
53            continue;
54        }
55        out.push(inspect_vision_10d_path(&path, base)?);
56    }
57    Ok(())
58}
59
60/// Inspect one `.10d` path relative to storage (or absolute under storage).
61pub fn inspect_vision_10d(
62    storage_root: &Path,
63    relative_or_abs: &str,
64) -> Result<Vision10dEntry, String> {
65    let p = PathBuf::from(relative_or_abs);
66    let full = if p.is_absolute() {
67        p
68    } else {
69        storage_root.join(relative_or_abs)
70    };
71    inspect_vision_10d_path(&full, storage_root)
72}
73
74fn inspect_vision_10d_path(path: &Path, base: &Path) -> Result<Vision10dEntry, String> {
75    let filename = path
76        .file_name()
77        .and_then(|n| n.to_str())
78        .unwrap_or("?")
79        .to_string();
80    let size_bytes = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
81    let relative = path
82        .strip_prefix(base)
83        .ok()
84        .and_then(|p| p.to_str())
85        .unwrap_or(&filename)
86        .to_string();
87
88    // Parent folder name is often the media digest hex from Gs continuum.
89    let media_digest_hex = path
90        .parent()
91        .and_then(|p| p.file_name())
92        .and_then(|n| n.to_str())
93        .filter(|s| s.len() >= 8 && s.chars().all(|c| c.is_ascii_hexdigit()))
94        .map(|s| s.to_string());
95
96    let bytes = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?;
97    let mut bytes_mut = bytes.clone();
98    let crc_valid = verify_whole_file_crc32c(&mut bytes_mut).is_ok();
99
100    let (section_count, has_mesh, has_tensor_nodes, has_provenance, node_count, mesh_v, mesh_t) =
101        match Container10dHeader::parse(&bytes) {
102            Ok(header) => {
103                let descs = parse_section_table(&bytes, &header).ok();
104                let mut hm = false;
105                let mut ht = false;
106                let mut hp = false;
107                let mut nc = None;
108                let mut mv = None;
109                let mut mt = None;
110                if let Some(ref descs) = descs {
111                    for d in descs.iter() {
112                        match d.typ() {
113                            Some(SectionType::QuantizedMesh) => {
114                                hm = true;
115                                let start = d.byte_offset as usize;
116                                let end = start.saturating_add(d.byte_length as usize);
117                                if let Some(payload) = bytes.get(start..end) {
118                                    if let Ok(m) = decode_mesh_section(payload) {
119                                        mv = Some(m.vertex_count() as u32);
120                                        mt = Some(m.triangle_count() as u32);
121                                    }
122                                }
123                            }
124                            Some(SectionType::Tensor10DNodes) => {
125                                ht = true;
126                                let start = d.byte_offset as usize;
127                                let end = start.saturating_add(d.byte_length as usize);
128                                if let Some(payload) = bytes.get(start..end) {
129                                    if let Ok((nh, _)) = parse_node_header(payload) {
130                                        nc = Some(nh.node_count);
131                                    }
132                                }
133                            }
134                            Some(SectionType::ProvenanceSidecar) => hp = true,
135                            _ => {}
136                        }
137                    }
138                }
139                (header.section_count, hm, ht, hp, nc, mv, mt)
140            }
141            Err(_) => (0, false, false, false, None, None, None),
142        };
143
144    // Prefer decode_10d_mesh counts when header parse path missed them.
145    let (mesh_vertices, mesh_triangles) = if mesh_v.is_some() {
146        (mesh_v, mesh_t)
147    } else if let Ok(m) = decode_10d_mesh(&bytes) {
148        (
149            Some(m.vertex_count() as u32),
150            Some(m.triangle_count() as u32),
151        )
152    } else {
153        (None, None)
154    };
155
156    let compiled_digest_hex = if crc_valid || has_mesh {
157        Some(format!("{:08x}", compiled_digest(&bytes)))
158    } else {
159        None
160    };
161
162    Ok(Vision10dEntry {
163        path: relative,
164        filename,
165        media_digest_hex,
166        size_bytes,
167        section_count,
168        has_mesh,
169        has_tensor_nodes,
170        has_provenance,
171        crc_valid,
172        compiled_digest_hex,
173        mesh_vertices,
174        mesh_triangles,
175        node_count,
176    })
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use qualia_core_db::render::assets::Mesh;
183    use qualia_core_db::render::compile_10d::compile_mesh_to_10d_with_nodes;
184    use qualia_core_db::tensor::Tensor10D;
185    use std::fs;
186
187    #[test]
188    fn lists_recon_under_vision_geometry() {
189        let dir = std::env::temp_dir().join(format!("vision_10d_browse_{}", std::process::id()));
190        let _ = fs::remove_dir_all(&dir);
191        let digest = "aabbccddeeff0011";
192        let recon = dir.join("vision_geometry").join(digest);
193        fs::create_dir_all(&recon).unwrap();
194        let mesh = Mesh {
195            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
196            triangles: vec![[0, 1, 2]],
197            min: [0.0, 0.0, 0.0],
198            max: [1.0, 1.0, 0.0],
199        };
200        let nodes = [Tensor10D::ground_truth(
201            0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 0.3,
202        )];
203        let bytes = compile_mesh_to_10d_with_nodes(&mesh, &nodes).unwrap();
204        fs::write(recon.join("recon.10d"), &bytes).unwrap();
205
206        let list = list_vision_10d_containers(&dir).unwrap();
207        assert_eq!(list.len(), 1);
208        assert!(list[0].has_mesh);
209        assert!(list[0].has_tensor_nodes);
210        assert_eq!(list[0].node_count, Some(1));
211        assert_eq!(list[0].media_digest_hex.as_deref(), Some(digest));
212        assert!(list[0].crc_valid);
213        let _ = fs::remove_dir_all(&dir);
214    }
215}