1use crate::container_10d::crc32c::crc32c;
16use crate::container_10d::header::Container10dHeader;
17use crate::container_10d::integrity::{compute_whole_file_crc32c, seal_whole_file_crc32c};
18use crate::container_10d::mesh_section::{
19 decode_mesh_section, encode_mesh_section, encoded_len, MeshSectionError,
20};
21use crate::container_10d::node_section::{
22 parse_node_header, read_node, write_node_section_aos, NodeMiniHeader, NodeSectionError,
23};
24use crate::container_10d::provenance_section::{
25 encode_provenance_section, encoded_len as provenance_encoded_len, ProvenanceSectionError,
26 ProvenanceSidecar,
27};
28use crate::container_10d::section::{
29 encode_container, parse_section_table, AlignmentTier, SectionInput, SectionTableError,
30 SectionType,
31};
32use crate::render::assets::{
33 import_asset, mesh_to_nquins_with_dev, mesh_to_nquins_with_meta, AssetError, Mesh,
34};
35use crate::tensor::Tensor10D;
36use crate::NQuin;
37use std::collections::HashMap;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct Compile10dExtras {
42 pub topology: bool,
44 pub spatial_index: bool,
46}
47
48impl Compile10dExtras {
49 pub const VISION: Self = Self {
51 topology: true,
52 spatial_index: true,
53 };
54}
55
56#[derive(Debug, PartialEq, Eq)]
61pub enum Compile10dError {
62 Import(AssetError),
64 Mesh(MeshSectionError),
66 Section(SectionTableError),
68 Provenance(ProvenanceSectionError),
70 Nodes(NodeSectionError),
72 ExtraSection { kind: &'static str },
74 NoMeshSection,
76 NoNodesSection,
78 BadHeader,
80 SectionOutOfBounds,
82}
83
84impl std::fmt::Display for Compile10dError {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 Self::Import(e) => write!(f, ".10d compile: source import: {e}"),
88 Self::Mesh(e) => write!(f, ".10d compile: mesh section: {e}"),
89 Self::Section(e) => write!(f, ".10d compile: section table: {e:?}"),
90 Self::Provenance(e) => write!(f, ".10d compile: provenance section: {e}"),
91 Self::Nodes(e) => write!(f, ".10d compile: Tensor10DNodes section: {e}"),
92 Self::ExtraSection { kind } => write!(f, ".10d compile: extra section {kind} failed"),
93 Self::NoMeshSection => write!(f, ".10d: no QuantizedMesh section in container"),
94 Self::NoNodesSection => write!(f, ".10d: no Tensor10DNodes section in container"),
95 Self::BadHeader => write!(f, ".10d: container header failed to parse"),
96 Self::SectionOutOfBounds => write!(f, ".10d: section byte range outside container"),
97 }
98 }
99}
100
101impl std::error::Error for Compile10dError {}
102
103pub fn compile_mesh_to_10d(mesh: &Mesh) -> Result<Vec<u8>, Compile10dError> {
110 compile_mesh_to_10d_with_provenance(mesh, None)
111}
112
113pub fn compile_mesh_to_10d_with_provenance(
120 mesh: &Mesh,
121 provenance: Option<&ProvenanceSidecar>,
122) -> Result<Vec<u8>, Compile10dError> {
123 compile_mesh_to_10d_with_nodes_and_provenance(mesh, &[], provenance)
124}
125
126pub fn compile_mesh_to_10d_with_nodes(
131 mesh: &Mesh,
132 nodes: &[Tensor10D],
133) -> Result<Vec<u8>, Compile10dError> {
134 compile_mesh_to_10d_with_nodes_and_provenance(mesh, nodes, None)
135}
136
137pub fn compile_mesh_to_10d_with_nodes_and_provenance(
139 mesh: &Mesh,
140 nodes: &[Tensor10D],
141 provenance: Option<&ProvenanceSidecar>,
142) -> Result<Vec<u8>, Compile10dError> {
143 compile_mesh_to_10d_with_extras(mesh, nodes, provenance, Compile10dExtras::default())
144}
145
146pub fn compile_mesh_to_10d_vision(
151 mesh: &Mesh,
152 nodes: &[Tensor10D],
153) -> Result<Vec<u8>, Compile10dError> {
154 compile_mesh_to_10d_with_extras(mesh, nodes, None, Compile10dExtras::VISION)
155}
156
157pub fn compile_mesh_to_10d_vision_with_provenance(
159 mesh: &Mesh,
160 nodes: &[Tensor10D],
161 provenance: &ProvenanceSidecar,
162) -> Result<Vec<u8>, Compile10dError> {
163 compile_mesh_to_10d_with_extras(mesh, nodes, Some(provenance), Compile10dExtras::VISION)
164}
165
166pub fn compile_mesh_to_10d_with_extras(
168 mesh: &Mesh,
169 nodes: &[Tensor10D],
170 provenance: Option<&ProvenanceSidecar>,
171 extras: Compile10dExtras,
172) -> Result<Vec<u8>, Compile10dError> {
173 let mut payload = vec![0u8; encoded_len(mesh.vertex_count(), mesh.triangle_count())];
175 let written = encode_mesh_section(mesh, &mut payload).map_err(Compile10dError::Mesh)?;
176 payload.truncate(written);
177
178 let node_payload: Option<Vec<u8>> = if nodes.is_empty() {
180 None
181 } else {
182 let mut buf = vec![0u8; NodeMiniHeader::payload_bytes(nodes.len())];
183 let n = write_node_section_aos(nodes, &mut buf).map_err(Compile10dError::Nodes)?;
184 buf.truncate(n);
185 Some(buf)
186 };
187
188 let prov_payload: Option<Vec<u8>> = match provenance {
190 Some(p) => {
191 let mut buf = vec![0u8; provenance_encoded_len(p)];
192 let n = encode_provenance_section(p, &mut buf).map_err(Compile10dError::Provenance)?;
193 buf.truncate(n);
194 Some(buf)
195 }
196 None => None,
197 };
198
199 let topo_payload = if extras.topology {
201 encode_topology_for_mesh(mesh)?
202 } else {
203 None
204 };
205 let spatial_payload = if extras.spatial_index {
206 encode_spatial_index_for_mesh(mesh)?
207 } else {
208 None
209 };
210
211 let header = Container10dHeader::proposed();
213 let mut inputs = vec![SectionInput {
214 section_type: SectionType::QuantizedMesh,
215 alignment_tier: AlignmentTier::Page,
216 stride: 0,
217 element_count: 0,
218 payload: &payload,
219 }];
220 if let Some(np) = &node_payload {
221 inputs.push(SectionInput {
224 section_type: SectionType::Tensor10DNodes,
225 alignment_tier: AlignmentTier::CacheLine,
226 stride: 0,
227 element_count: 0,
228 payload: np,
229 });
230 }
231 if let Some(pp) = &prov_payload {
232 inputs.push(SectionInput {
233 section_type: SectionType::ProvenanceSidecar,
234 alignment_tier: AlignmentTier::Word,
235 stride: 0,
236 element_count: 0,
237 payload: pp,
238 });
239 }
240 if let Some(tp) = &topo_payload {
241 inputs.push(SectionInput {
242 section_type: SectionType::Topology,
243 alignment_tier: AlignmentTier::Word,
244 stride: 0,
245 element_count: 0,
246 payload: tp,
247 });
248 }
249 if let Some(sp) = &spatial_payload {
250 inputs.push(SectionInput {
251 section_type: SectionType::SpatialIndex,
252 alignment_tier: AlignmentTier::Page,
253 stride: 0,
254 element_count: 0,
255 payload: sp,
256 });
257 }
258 let needed = match encode_container(&header, &inputs, &mut []) {
260 Err(SectionTableError::OutputBufferTooSmall { needed, .. }) => needed,
261 Ok(n) => n,
262 Err(e) => return Err(Compile10dError::Section(e)),
263 };
264 let mut out = vec![0u8; needed];
265 let total = encode_container(&header, &inputs, &mut out).map_err(Compile10dError::Section)?;
266 out.truncate(total);
267
268 seal_whole_file_crc32c(&mut out);
270 Ok(out)
271}
272
273#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-scientific"))]
274fn encode_topology_for_mesh(mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
275 use crate::container_10d::topology_section::{
276 encode_topology_section, encoded_len as topo_len,
277 };
278 use crate::specialized_libs::computational_geometry::{
279 build_triangle_half_edges, required_edge_slots, EdgeSlot, HalfEdge,
280 };
281
282 if mesh.triangle_count() == 0 || mesh.vertex_count() == 0 {
283 return Ok(None);
284 }
285 let vc = mesh.vertex_count() as u32;
286 let fc = mesh.triangle_count() as u32;
287 let mut edges = vec![HalfEdge::default(); mesh.triangles.len().saturating_mul(3)];
288 let mut slots = vec![EdgeSlot::default(); required_edge_slots(mesh.triangles.len())];
289 build_triangle_half_edges(vc, &mesh.triangles, &mut edges, &mut slots).map_err(|_| {
290 Compile10dError::ExtraSection {
291 kind: "topology_half_edges",
292 }
293 })?;
294 let need = topo_len(vc, fc, edges.len() as u32);
295 let mut buf = vec![0u8; need];
296 let n = encode_topology_section(vc, fc, &edges, &mut buf).map_err(|_| {
297 Compile10dError::ExtraSection {
298 kind: "topology_encode",
299 }
300 })?;
301 buf.truncate(n);
302 Ok(Some(buf))
303}
304
305#[cfg(not(any(not(target_arch = "wasm32"), feature = "wasm-scientific")))]
306fn encode_topology_for_mesh(_mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
307 Ok(None)
308}
309
310#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-scientific"))]
311fn encode_spatial_index_for_mesh(mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
312 use crate::container_10d::spatial_index_section::{
313 encode_spatial_index_section, encoded_len as spatial_len,
314 };
315 use crate::specialized_libs::computational_geometry::{
316 build_bvh_recursive, build_kd_tree_3d, Aabb, BvhNode, KdNode, Point3,
317 };
318
319 if mesh.triangle_count() == 0 || mesh.vertex_count() == 0 {
320 return Ok(None);
321 }
322
323 let mut aabbs = Vec::with_capacity(mesh.triangles.len());
324 for tri in &mesh.triangles {
325 let p0 = mesh.positions[tri[0] as usize];
326 let p1 = mesh.positions[tri[1] as usize];
327 let p2 = mesh.positions[tri[2] as usize];
328 let min = Point3::new(
329 p0[0].min(p1[0]).min(p2[0]) as f64,
330 p0[1].min(p1[1]).min(p2[1]) as f64,
331 p0[2].min(p1[2]).min(p2[2]) as f64,
332 );
333 let max = Point3::new(
334 p0[0].max(p1[0]).max(p2[0]) as f64,
335 p0[1].max(p1[1]).max(p2[1]) as f64,
336 p0[2].max(p1[2]).max(p2[2]) as f64,
337 );
338 aabbs.push(Aabb::new(min, max));
339 }
340 let n = aabbs.len();
341 let mut bvh_nodes = vec![BvhNode::default(); 2 * n];
342 let mut bvh_indices = vec![0u32; n];
343 let mut bvh_codes = vec![0u64; n];
344 let mut bvh_sort = vec![0u32; n];
345 let (bvh_count, bvh_root) = build_bvh_recursive(
346 &aabbs,
347 &mut bvh_nodes,
348 &mut bvh_indices,
349 &mut bvh_codes,
350 &mut bvh_sort,
351 )
352 .map_err(|_| Compile10dError::ExtraSection { kind: "bvh_build" })?;
353
354 let points: Vec<[f64; 3]> = mesh
355 .positions
356 .iter()
357 .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
358 .collect();
359 let np = points.len();
360 let mut kd_nodes = vec![KdNode::default(); np];
361 let mut kd_indices = vec![0u32; np];
362 let mut kd_codes = vec![0u64; np];
363 let mut kd_sort = vec![0u32; np];
364 let (kd_count, kd_root) = build_kd_tree_3d(
365 &points,
366 &mut kd_nodes,
367 &mut kd_indices,
368 &mut kd_codes,
369 &mut kd_sort,
370 )
371 .map_err(|_| Compile10dError::ExtraSection { kind: "kd_build" })?;
372
373 let need = spatial_len(bvh_count as u32, kd_count as u32, n as u32, np as u32);
374 let mut buf = vec![0u8; need];
375 encode_spatial_index_section(
376 &bvh_nodes[..bvh_count],
377 &bvh_indices,
378 bvh_root as u32,
379 &kd_nodes[..kd_count],
380 &kd_indices,
381 kd_root as u32,
382 &mut buf,
383 )
384 .map_err(|_| Compile10dError::ExtraSection {
385 kind: "spatial_encode",
386 })?;
387 Ok(Some(buf))
388}
389
390#[cfg(not(any(not(target_arch = "wasm32"), feature = "wasm-scientific")))]
391fn encode_spatial_index_for_mesh(_mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
392 Ok(None)
393}
394
395#[inline]
399pub fn compiled_digest(container_10d: &[u8]) -> u32 {
400 compute_whole_file_crc32c(container_10d)
401}
402
403pub fn decode_10d_mesh(container_10d: &[u8]) -> Result<Mesh, Compile10dError> {
407 let header =
408 Container10dHeader::parse(container_10d).map_err(|_| Compile10dError::BadHeader)?;
409 let descs = parse_section_table(container_10d, &header).map_err(Compile10dError::Section)?;
410 for d in descs {
411 if d.typ() == Some(SectionType::QuantizedMesh) {
412 let start = d.byte_offset as usize;
413 let end = start
414 .checked_add(d.byte_length as usize)
415 .ok_or(Compile10dError::SectionOutOfBounds)?;
416 let payload = container_10d
417 .get(start..end)
418 .ok_or(Compile10dError::SectionOutOfBounds)?;
419 return decode_mesh_section(payload).map_err(Compile10dError::Mesh);
420 }
421 }
422 Err(Compile10dError::NoMeshSection)
423}
424
425pub fn decode_10d_nodes(
430 container_10d: &[u8],
431 out: &mut [Tensor10D],
432) -> Result<usize, Compile10dError> {
433 let header =
434 Container10dHeader::parse(container_10d).map_err(|_| Compile10dError::BadHeader)?;
435 let descs = parse_section_table(container_10d, &header).map_err(Compile10dError::Section)?;
436 for d in descs {
437 if d.typ() == Some(SectionType::Tensor10DNodes) {
438 let start = d.byte_offset as usize;
439 let end = start
440 .checked_add(d.byte_length as usize)
441 .ok_or(Compile10dError::SectionOutOfBounds)?;
442 let payload = container_10d
443 .get(start..end)
444 .ok_or(Compile10dError::SectionOutOfBounds)?;
445 let (nh, _) = parse_node_header(payload).map_err(Compile10dError::Nodes)?;
446 let count = (nh.node_count as usize).min(out.len());
447 for i in 0..count {
448 out[i] = read_node(payload, i).map_err(Compile10dError::Nodes)?;
449 }
450 return Ok(count);
451 }
452 }
453 Err(Compile10dError::NoNodesSection)
454}
455
456#[derive(Debug, Clone)]
464pub struct CompiledAsset {
465 pub mesh: Mesh,
467 pub container_10d: Vec<u8>,
469 pub compiled_digest: u32,
471 pub source_digest: u32,
473 pub quins: Vec<NQuin>,
475 pub lexicon: HashMap<u64, String>,
477}
478
479pub fn compile_asset(
491 source_bytes: &[u8],
492 hint: Option<&str>,
493 asset_uri: &str,
494 source_format: &str,
495) -> Result<CompiledAsset, Compile10dError> {
496 compile_organ_asset(
497 source_bytes,
498 hint,
499 asset_uri,
500 source_format,
501 None,
502 None,
503 None,
504 )
505}
506
507#[allow(clippy::too_many_arguments)]
513pub fn compile_organ_asset(
514 source_bytes: &[u8],
515 hint: Option<&str>,
516 asset_uri: &str,
517 source_format: &str,
518 body_system: Option<&str>,
519 anatomy_model: Option<&str>,
520 provenance: Option<&ProvenanceSidecar>,
521) -> Result<CompiledAsset, Compile10dError> {
522 let mesh = import_asset(source_bytes, hint).map_err(Compile10dError::Import)?;
523 let container_10d = compile_mesh_to_10d_with_provenance(&mesh, provenance)?;
527 let compiled = compiled_digest(&container_10d);
528 let source = crc32c(source_bytes);
529 let (quins, lexicon) = mesh_to_nquins_with_meta(
530 &mesh,
531 asset_uri,
532 source_format,
533 source,
534 compiled,
535 body_system,
536 anatomy_model,
537 );
538 Ok(CompiledAsset {
539 mesh,
540 container_10d,
541 compiled_digest: compiled,
542 source_digest: source,
543 quins,
544 lexicon,
545 })
546}
547
548pub fn compile_developmental_asset(
553 source_bytes: &[u8],
554 hint: Option<&str>,
555 asset_uri: &str,
556 source_format: &str,
557 gestational_age_days: u16,
558 carnegie_stage: u8,
559) -> Result<CompiledAsset, Compile10dError> {
560 let mesh = import_asset(source_bytes, hint).map_err(Compile10dError::Import)?;
561 let container_10d = compile_mesh_to_10d(&mesh)?;
562 let compiled = compiled_digest(&container_10d);
563 let source = crc32c(source_bytes);
564 let (quins, lexicon) = mesh_to_nquins_with_dev(
565 &mesh,
566 asset_uri,
567 source_format,
568 source,
569 compiled,
570 gestational_age_days,
571 carnegie_stage,
572 );
573 Ok(CompiledAsset {
574 mesh,
575 container_10d,
576 compiled_digest: compiled,
577 source_digest: source,
578 quins,
579 lexicon,
580 })
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::container_10d::integrity::verify_whole_file_crc32c;
587
588 fn cube() -> Mesh {
590 Mesh {
591 positions: vec![
592 [0.0, 0.0, 0.0],
593 [1.0, 0.0, 0.0],
594 [1.0, 1.0, 0.0],
595 [0.0, 1.0, 0.0],
596 [0.0, 0.0, 1.0],
597 [1.0, 0.0, 1.0],
598 [1.0, 1.0, 1.0],
599 [0.0, 1.0, 1.0],
600 ],
601 triangles: vec![
602 [0, 3, 2],
603 [0, 2, 1],
604 [4, 5, 6],
605 [4, 6, 7],
606 [0, 1, 5],
607 [0, 5, 4],
608 [3, 7, 6],
609 [3, 6, 2],
610 [0, 4, 7],
611 [0, 7, 3],
612 [1, 2, 6],
613 [1, 6, 5],
614 ],
615 min: [0.0, 0.0, 0.0],
616 max: [1.0, 1.0, 1.0],
617 }
618 }
619
620 #[test]
621 fn container_is_well_formed_and_sealed() {
622 let mut bytes = compile_mesh_to_10d(&cube()).unwrap();
623 assert!(
624 bytes.len() > 64,
625 "container must be larger than the 64-byte header"
626 );
627 verify_whole_file_crc32c(&mut bytes).expect(".10d whole-file CRC must verify");
629 }
630
631 #[test]
632 fn bundles_provenance_physically_and_still_decodes_the_mesh() {
633 use crate::container_10d::provenance_section::{
634 decode_provenance_section, validate_provenance,
635 };
636
637 let source = b"<the original source GLB bytes for this organ>".to_vec();
638 let sidecar = ProvenanceSidecar::new(source.clone(), "model/gltf-binary", "CC-BY-4.0");
639 let mut bytes = compile_mesh_to_10d_with_provenance(&cube(), Some(&sidecar)).unwrap();
640
641 verify_whole_file_crc32c(&mut bytes).expect(".10d whole-file CRC must verify");
643 let mesh = decode_10d_mesh(&bytes).unwrap();
645 assert_eq!(mesh.triangle_count(), cube().triangle_count());
646
647 let header = Container10dHeader::parse(&bytes).unwrap();
649 let descs = parse_section_table(&bytes, &header).unwrap();
650 let prov = descs
651 .iter()
652 .find(|d| d.typ() == Some(SectionType::ProvenanceSidecar))
653 .expect("provenance section bundled in the .10d");
654 let payload = &bytes[prov.byte_offset as usize..][..prov.byte_length as usize];
655 let view = decode_provenance_section(payload).unwrap();
656 validate_provenance(&view).expect("bundled provenance validates before use");
657 assert_eq!(view.licence(), "CC-BY-4.0");
658 assert_eq!(view.source_bytes(), source.as_slice());
659 }
660
661 #[test]
662 fn round_trips_the_mesh_within_quantization_tolerance() {
663 let m = cube();
664 let bytes = compile_mesh_to_10d(&m).unwrap();
665 let back = decode_10d_mesh(&bytes).unwrap();
666 assert_eq!(back.vertex_count(), m.vertex_count());
667 assert_eq!(back.triangle_count(), m.triangle_count());
668 assert_eq!(back.triangles, m.triangles, "indices are exact");
669 let tol = 1.0 / 65535.0 * 1.001;
671 for (a, b) in m.positions.iter().zip(back.positions.iter()) {
672 for k in 0..3 {
673 assert!((a[k] - b[k]).abs() <= tol, "vertex {a:?} vs {b:?} axis {k}");
674 }
675 }
676 }
677
678 #[test]
679 fn compilation_is_deterministic() {
680 let m = cube();
681 let a = compile_mesh_to_10d(&m).unwrap();
682 let b = compile_mesh_to_10d(&m).unwrap();
683 assert_eq!(a, b, "identical mesh → byte-identical .10d");
684 assert_eq!(compiled_digest(&a), compiled_digest(&b));
685 }
686
687 #[test]
688 fn digest_changes_when_geometry_changes() {
689 let a = compile_mesh_to_10d(&cube()).unwrap();
690 let mut m2 = cube();
691 m2.positions[6] = [1.0, 1.0, 0.5]; let b = compile_mesh_to_10d(&m2).unwrap();
693 assert_ne!(compiled_digest(&a), compiled_digest(&b));
694 }
695
696 #[test]
697 fn decode_rejects_garbage() {
698 assert_eq!(decode_10d_mesh(&[0u8; 8]), Err(Compile10dError::BadHeader));
699 }
700
701 #[test]
702 fn mesh_with_nodes_round_trips_sigma() {
703 use crate::tensor::Tensor10D;
704 let mesh = cube();
705 let nodes = [
706 Tensor10D::ground_truth(0.0, 0.0, 0.5, 0.5, 0.0, 1.0, 0.9, 0.0, 0.42),
707 Tensor10D::parallel_context(1.0, 0.0, 0.0, 0.1, 0.2, 0.0, 2.0, 0.5, 0.0, 0.7),
708 ];
709 let mut bytes = compile_mesh_to_10d_with_nodes(&mesh, &nodes).unwrap();
710 verify_whole_file_crc32c(&mut bytes).expect("seal");
711 let back = decode_10d_mesh(&bytes).unwrap();
712 assert_eq!(back.triangle_count(), mesh.triangle_count());
713 let mut out = [Tensor10D::default(); 4];
714 let n = decode_10d_nodes(&bytes, &mut out).unwrap();
715 assert_eq!(n, 2);
716 assert!((out[0].sigma - 0.42).abs() < 1e-5);
717 assert!((out[0].x - 0.5).abs() < 1e-5);
718 assert!((out[1].q - 1.0).abs() < 1e-5);
719 assert!((out[1].sigma - 0.7).abs() < 1e-5);
720 let plain = compile_mesh_to_10d(&mesh).unwrap();
722 assert!(matches!(
723 decode_10d_nodes(&plain, &mut out),
724 Err(Compile10dError::NoNodesSection)
725 ));
726 }
727
728 #[test]
729 fn vision_seal_includes_topology_and_spatial_when_cg() {
730 use crate::tensor::Tensor10D;
731 let mesh = cube();
732 let nodes = [Tensor10D::ground_truth(
733 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 0.33,
734 )];
735 let mut bytes = compile_mesh_to_10d_vision(&mesh, &nodes).unwrap();
736 verify_whole_file_crc32c(&mut bytes).expect("seal");
737 let header = Container10dHeader::parse(&bytes).unwrap();
738 let descs = parse_section_table(&bytes, &header).unwrap();
739 let types: Vec<_> = descs.iter().filter_map(|d| d.typ()).collect();
740 assert!(types.contains(&SectionType::QuantizedMesh));
741 assert!(types.contains(&SectionType::Tensor10DNodes));
742 #[cfg(any(not(target_arch = "wasm32"), feature = "wasm-scientific"))]
743 {
744 assert!(
745 types.contains(&SectionType::Topology),
746 "expected Topology section, got {types:?}"
747 );
748 assert!(
749 types.contains(&SectionType::SpatialIndex),
750 "expected SpatialIndex section, got {types:?}"
751 );
752 }
753 let back = decode_10d_mesh(&bytes).unwrap();
754 assert_eq!(back.triangle_count(), mesh.triangle_count());
755 }
756
757 const TRI_OBJ: &[u8] = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
759
760 #[test]
761 fn compile_asset_binds_manifest_to_its_container() {
762 let a = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:tri", "obj").unwrap();
763
764 let back = decode_10d_mesh(&a.container_10d).unwrap();
766 assert_eq!(back.triangle_count(), 1);
767 assert_eq!(back.vertex_count(), 3);
768
769 assert_eq!(a.compiled_digest, compiled_digest(&a.container_10d));
771 assert_eq!(a.source_digest, crc32c(TRI_OBJ));
773
774 let objs: Vec<u64> = a.quins.iter().map(|q| q.object).collect();
776 assert!(
777 objs.contains(&(a.compiled_digest as u64)),
778 "manifest cites compiledDigest"
779 );
780 assert!(
781 objs.contains(&(a.source_digest as u64)),
782 "manifest cites sourceDigest"
783 );
784 }
785
786 #[test]
787 fn compile_asset_is_deterministic() {
788 let a = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:tri", "obj").unwrap();
789 let b = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:tri", "obj").unwrap();
790 assert_eq!(a.container_10d, b.container_10d, "byte-identical container");
791 assert_eq!(a.compiled_digest, b.compiled_digest);
792 assert_eq!(a.source_digest, b.source_digest);
793 }
794
795 #[test]
796 fn compile_asset_surfaces_import_errors() {
797 let err = compile_asset(b"\x00\x01\x02\x03", None, "urn:asset:junk", "obj");
799 assert!(matches!(err, Err(Compile10dError::Import(_))));
800 }
801
802 #[test]
803 fn compile_organ_asset_binds_body_system_and_model() {
804 let plain = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:organ", "obj").unwrap();
805 let organ = compile_organ_asset(
806 TRI_OBJ,
807 Some("obj"),
808 "urn:asset:organ",
809 "obj",
810 Some("respiratory"),
811 Some("male"),
812 None,
813 )
814 .unwrap();
815 assert_eq!(organ.container_10d, plain.container_10d);
817 assert_eq!(organ.compiled_digest, plain.compiled_digest);
818 assert_eq!(
819 organ.quins.len(),
820 plain.quins.len() + 2,
821 "two anatomy facts added"
822 );
823 let vals: Vec<&str> = organ.lexicon.values().map(String::as_str).collect();
825 assert!(vals.contains(&"respiratory"), "bodySystem fact present");
826 assert!(vals.contains(&"male"), "anatomyModel fact present");
827 let none = compile_organ_asset(
829 TRI_OBJ,
830 Some("obj"),
831 "urn:asset:organ",
832 "obj",
833 None,
834 None,
835 None,
836 )
837 .unwrap();
838 assert_eq!(none.quins.len(), plain.quins.len());
839 }
840
841 #[test]
842 fn compile_developmental_asset_binds_the_t_axis_coordinate() {
843 let plain = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:fetal", "obj").unwrap();
844 let dev =
846 compile_developmental_asset(TRI_OBJ, Some("obj"), "urn:asset:fetal", "obj", 44, 18)
847 .unwrap();
848 assert_eq!(dev.container_10d, plain.container_10d);
850 assert_eq!(
851 dev.quins.len(),
852 plain.quins.len() + 2,
853 "gestationalAgeDays + carnegieStage"
854 );
855 assert!(
858 dev.quins.iter().any(|q| q.object == 44),
859 "gestationalAgeDays=44"
860 );
861 assert!(dev.quins.iter().any(|q| q.object == 18), "carnegieStage=18");
862 }
863}