1use 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
32pub 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
60pub 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 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 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}