Skip to main content

qualia_client_core/
vision_10d_load.rs

1//! F2 + D3 + F3 + F4 — load sealed vision `.10d` into mesh + spectral/acoustic paint.
2//!
3//! Portal / desktop consume this instead of re-parsing mesh-only. CRC is verified
4//! fail-closed. σ → colour via `render::spectral`; σ → Hz via `render::acoustic`.
5//! F3: temporal scrub on node `t`. F4: optional citable provenance barrier.
6
7use crate::vision_10d_rights::{evaluate_vision_10d_barrier, Vision10dAccess, Vision10dBarrier};
8use qualia_core_db::container_10d::{
9    header::Container10dHeader, integrity::verify_whole_file_crc32c,
10    node_section::parse_node_header, parse_section_table, section::SectionType,
11};
12use qualia_core_db::render::acoustic::sigma_to_center_frequency_hz;
13use qualia_core_db::render::assets::Mesh;
14use qualia_core_db::render::compile_10d::{compiled_digest, decode_10d_mesh, decode_10d_nodes};
15use qualia_core_db::render::spectral::sigma_to_display_rgb;
16use qualia_core_db::tensor::Tensor10D;
17use serde::Serialize;
18use std::path::Path;
19
20/// Spectral + acoustic view of a vision node (D3).
21#[derive(Debug, Clone, Copy, Serialize)]
22pub struct VisionNodePaint {
23    pub x: f32,
24    pub y: f32,
25    pub z: f32,
26    pub t: f32,
27    pub sigma: f32,
28    pub rgb: [u8; 3],
29    pub frequency_hz: f32,
30    /// True when q==0 (ground truth). Vision detections use q>0.
31    pub ground_truth: bool,
32}
33
34/// Loaded vision `.10d` package for browser / portal paint.
35#[derive(Debug, Clone, Serialize)]
36pub struct Vision10dLoaded {
37    pub path: Option<String>,
38    pub size_bytes: u64,
39    pub compiled_digest_hex: String,
40    pub crc_valid: bool,
41    pub mesh_vertices: u32,
42    pub mesh_triangles: u32,
43    pub node_count: u32,
44    pub has_topology: bool,
45    pub has_spatial_index: bool,
46    pub has_provenance: bool,
47    pub paint: Vec<VisionNodePaint>,
48    /// Mean σ across nodes (for single-colour mesh tint).
49    pub mean_sigma: f32,
50    pub mean_rgb: [u8; 3],
51    pub mean_frequency_hz: f32,
52}
53
54/// Decode container bytes into a paint package (browse access: unattested OK).
55pub fn load_vision_10d_bytes(bytes: &[u8]) -> Result<(Mesh, Vision10dLoaded), String> {
56    load_vision_10d_bytes_with_access(bytes, Vision10dAccess::BrowseAllowUnattested)
57}
58
59/// Load with explicit F4 access policy.
60pub fn load_vision_10d_bytes_with_access(
61    bytes: &[u8],
62    access: Vision10dAccess,
63) -> Result<(Mesh, Vision10dLoaded), String> {
64    match evaluate_vision_10d_barrier(bytes, access) {
65        Vision10dBarrier::Permit => {}
66        Vision10dBarrier::Deny { reason } => {
67            return Err(format!("vision .10d barrier: {reason}"));
68        }
69    }
70
71    let mut bytes_mut = bytes.to_vec();
72    let crc_valid = verify_whole_file_crc32c(&mut bytes_mut).is_ok();
73    if !crc_valid {
74        return Err("vision .10d: whole-file CRC failed".into());
75    }
76
77    let header = Container10dHeader::parse(&bytes_mut).map_err(|e| format!("header: {e}"))?;
78    let descs =
79        parse_section_table(&bytes_mut, &header).map_err(|e| format!("section table: {e}"))?;
80
81    let mut has_topology = false;
82    let mut has_spatial_index = false;
83    let mut has_provenance = false;
84    for d in descs.iter() {
85        match d.typ() {
86            Some(SectionType::Topology) => has_topology = true,
87            Some(SectionType::SpatialIndex) => has_spatial_index = true,
88            Some(SectionType::ProvenanceSidecar) => has_provenance = true,
89            _ => {}
90        }
91    }
92
93    let mesh = decode_10d_mesh(&bytes_mut).map_err(|e| format!("mesh: {e}"))?;
94
95    // Count nodes from header when present.
96    let mut node_cap = 0usize;
97    for d in descs.iter() {
98        if d.typ() == Some(SectionType::Tensor10DNodes) {
99            let start = d.byte_offset as usize;
100            let end = start.saturating_add(d.byte_length as usize);
101            if let Some(payload) = bytes_mut.get(start..end) {
102                if let Ok((nh, _)) = parse_node_header(payload) {
103                    node_cap = nh.node_count as usize;
104                }
105            }
106        }
107    }
108
109    let mut nodes = vec![Tensor10D::default(); node_cap.max(1)];
110    let n = if node_cap == 0 {
111        0
112    } else {
113        decode_10d_nodes(&bytes_mut, &mut nodes).unwrap_or(0)
114    };
115    nodes.truncate(n);
116
117    let paint: Vec<VisionNodePaint> = nodes.iter().map(node_to_paint).collect();
118    let mean_sigma = if paint.is_empty() {
119        0.35
120    } else {
121        paint.iter().map(|p| p.sigma).sum::<f32>() / paint.len() as f32
122    };
123    let (mr, mg, mb) = sigma_to_display_rgb(mean_sigma);
124    let mean_hz = sigma_to_center_frequency_hz(mean_sigma);
125
126    let loaded = Vision10dLoaded {
127        path: None,
128        size_bytes: bytes.len() as u64,
129        compiled_digest_hex: format!("{:08x}", compiled_digest(&bytes_mut)),
130        crc_valid: true,
131        mesh_vertices: mesh.vertex_count() as u32,
132        mesh_triangles: mesh.triangle_count() as u32,
133        node_count: n as u32,
134        has_topology,
135        has_spatial_index,
136        has_provenance,
137        paint,
138        mean_sigma,
139        mean_rgb: [mr, mg, mb],
140        mean_frequency_hz: mean_hz,
141    };
142    Ok((mesh, loaded))
143}
144
145/// Load from storage-relative or absolute path (browse access).
146pub fn load_vision_10d_path(
147    storage_root: &Path,
148    relative_or_abs: &str,
149) -> Result<(Mesh, Vision10dLoaded), String> {
150    load_vision_10d_path_with_access(
151        storage_root,
152        relative_or_abs,
153        Vision10dAccess::BrowseAllowUnattested,
154    )
155}
156
157/// Load path with F4 access policy.
158pub fn load_vision_10d_path_with_access(
159    storage_root: &Path,
160    relative_or_abs: &str,
161    access: Vision10dAccess,
162) -> Result<(Mesh, Vision10dLoaded), String> {
163    let p = Path::new(relative_or_abs);
164    let full = if p.is_absolute() {
165        p.to_path_buf()
166    } else {
167        storage_root.join(relative_or_abs)
168    };
169    let bytes = std::fs::read(&full).map_err(|e| format!("read {}: {e}", full.display()))?;
170    let (mesh, mut loaded) = load_vision_10d_bytes_with_access(&bytes, access)?;
171    loaded.path = Some(
172        full.strip_prefix(storage_root)
173            .ok()
174            .and_then(|x| x.to_str())
175            .unwrap_or(relative_or_abs)
176            .to_string(),
177    );
178    Ok((mesh, loaded))
179}
180
181/// F3 — temporal scrub: keep paint nodes with `t` in `[t_slice ± t_window/2]`.
182///
183/// Returns indices into the original `paint` slice (caller filters).
184pub fn temporal_scrub_paint(
185    paint: &[VisionNodePaint],
186    t_slice: f32,
187    t_window: f32,
188    out_indices: &mut [u32],
189) -> usize {
190    let half = t_window * 0.5;
191    let lo = t_slice - half;
192    let hi = t_slice + half;
193    let mut n = 0usize;
194    for (i, p) in paint.iter().enumerate() {
195        if p.t >= lo && p.t <= hi {
196            if n < out_indices.len() {
197                out_indices[n] = i as u32;
198                n += 1;
199            }
200        }
201    }
202    n
203}
204
205/// F3 — filter paint into a new Vec (cold path).
206pub fn temporal_scrub_paint_vec(
207    paint: &[VisionNodePaint],
208    t_slice: f32,
209    t_window: f32,
210) -> Vec<VisionNodePaint> {
211    let half = t_window * 0.5;
212    let lo = t_slice - half;
213    let hi = t_slice + half;
214    paint
215        .iter()
216        .copied()
217        .filter(|p| p.t >= lo && p.t <= hi)
218        .collect()
219}
220
221/// D3: map one Tensor10D to spectral RGB + acoustic Hz.
222pub fn node_to_paint(t: &Tensor10D) -> VisionNodePaint {
223    let (r, g, b) = sigma_to_display_rgb(t.sigma);
224    VisionNodePaint {
225        x: t.x,
226        y: t.y,
227        z: t.z,
228        t: t.t,
229        sigma: t.sigma,
230        rgb: [r, g, b],
231        frequency_hz: sigma_to_center_frequency_hz(t.sigma),
232        ground_truth: t.is_ground_truth(),
233    }
234}
235
236/// Per-vertex colours for a mesh, painted by nearest vision node σ (or mean).
237pub fn mesh_vertex_colors_from_nodes(mesh: &Mesh, nodes: &[Tensor10D]) -> Vec<[f32; 4]> {
238    let paints: Vec<_> = nodes.iter().map(node_to_paint).collect();
239    if paints.is_empty() {
240        let (r, g, b) = sigma_to_display_rgb(0.35);
241        return vec![
242            [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0];
243            mesh.vertex_count()
244        ];
245    }
246    mesh.positions
247        .iter()
248        .map(|p| {
249            let mut best = 0usize;
250            let mut best_d = f32::INFINITY;
251            for (i, n) in paints.iter().enumerate() {
252                // Nodes may be in normalised image space; blend with mesh-space
253                // when magnitudes look like unit square, else use mean.
254                let dx = p[0] - n.x;
255                let dy = p[1] - n.y;
256                let dz = p[2] - n.z;
257                let d = dx * dx + dy * dy + dz * dz;
258                if d < best_d {
259                    best_d = d;
260                    best = i;
261                }
262            }
263            let c = paints[best].rgb;
264            [
265                c[0] as f32 / 255.0,
266                c[1] as f32 / 255.0,
267                c[2] as f32 / 255.0,
268                1.0,
269            ]
270        })
271        .collect()
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use qualia_core_db::container_10d::provenance_section::ProvenanceSidecar;
278    use qualia_core_db::render::compile_10d::{
279        compile_mesh_to_10d_vision, compile_mesh_to_10d_vision_with_provenance,
280    };
281
282    #[test]
283    fn load_vision_seal_with_paint() {
284        let mesh = Mesh {
285            positions: vec![
286                [0.0, 0.0, 0.0],
287                [1.0, 0.0, 0.0],
288                [0.0, 1.0, 0.0],
289                [0.0, 0.0, 1.0],
290            ],
291            triangles: vec![[0, 1, 2], [0, 2, 3]],
292            min: [0.0, 0.0, 0.0],
293            max: [1.0, 1.0, 1.0],
294        };
295        let nodes = [
296            Tensor10D::parallel_context(1.0, 0.0, 0.0, 0.2, 0.2, 0.0, 0.0, 1.0, 0.0, 0.2),
297            Tensor10D::parallel_context(1.0, 0.0, 0.0, 0.8, 0.8, 0.0, 1.0, 1.0, 0.0, 0.8),
298        ];
299        let bytes = compile_mesh_to_10d_vision(&mesh, &nodes).unwrap();
300        let (m, loaded) = load_vision_10d_bytes(&bytes).unwrap();
301        assert_eq!(m.triangle_count(), 2);
302        assert_eq!(loaded.node_count, 2);
303        assert_eq!(loaded.paint.len(), 2);
304        assert!(loaded.crc_valid);
305        assert!(loaded.mean_frequency_hz > 0.0);
306        let colors = mesh_vertex_colors_from_nodes(&m, &nodes);
307        assert_eq!(colors.len(), 4);
308        // Host product path includes C3 extras (portal wasm may skip them).
309        assert!(loaded.has_topology, "expected Topology on host vision seal");
310        assert!(
311            loaded.has_spatial_index,
312            "expected SpatialIndex on host vision seal"
313        );
314    }
315
316    #[test]
317    fn temporal_scrub_keeps_window() {
318        let paint = [
319            VisionNodePaint {
320                x: 0.0,
321                y: 0.0,
322                z: 0.0,
323                t: 0.0,
324                sigma: 0.1,
325                rgb: [0, 0, 0],
326                frequency_hz: 100.0,
327                ground_truth: false,
328            },
329            VisionNodePaint {
330                x: 0.0,
331                y: 0.0,
332                z: 0.0,
333                t: 5.0,
334                sigma: 0.2,
335                rgb: [0, 0, 0],
336                frequency_hz: 100.0,
337                ground_truth: false,
338            },
339            VisionNodePaint {
340                x: 0.0,
341                y: 0.0,
342                z: 0.0,
343                t: 10.0,
344                sigma: 0.3,
345                rgb: [0, 0, 0],
346                frequency_hz: 100.0,
347                ground_truth: false,
348            },
349        ];
350        let kept = temporal_scrub_paint_vec(&paint, 5.0, 2.0);
351        assert_eq!(kept.len(), 1);
352        assert!((kept[0].t - 5.0).abs() < 1e-5);
353        let mut idx = [0u32; 8];
354        let n = temporal_scrub_paint(&paint, 5.0, 12.0, &mut idx);
355        assert_eq!(n, 3);
356    }
357
358    #[test]
359    fn citable_load_requires_provenance() {
360        let mesh = Mesh {
361            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
362            triangles: vec![[0, 1, 2]],
363            min: [0.0, 0.0, 0.0],
364            max: [1.0, 1.0, 0.0],
365        };
366        let nodes = [Tensor10D::default()];
367        let bare = compile_mesh_to_10d_vision(&mesh, &nodes).unwrap();
368        assert!(load_vision_10d_bytes_with_access(
369            &bare,
370            Vision10dAccess::CitableRequireProvenance
371        )
372        .is_err());
373        let prov = ProvenanceSidecar::new(b"src", "image/rgb8", "CC0");
374        let sealed = compile_mesh_to_10d_vision_with_provenance(&mesh, &nodes, &prov).unwrap();
375        let (_m, loaded) =
376            load_vision_10d_bytes_with_access(&sealed, Vision10dAccess::CitableRequireProvenance)
377                .unwrap();
378        assert!(loaded.has_provenance);
379    }
380}