Skip to main content

oxihuman_export/
core_pack.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! OHPK v1 — the OxiHuman core-pack container format.
5//!
6//! A single self-describing binary asset pack that bundles a base mesh plus a
7//! set of sparse morph targets, so that the CLI can *write* one file and the
8//! WASM runtime can *read* it with zero-copy, no-filesystem, `wasm32`-safe
9//! parsing.  The design target is a base mesh plus ~40 morph targets in
10//! `<= 2 MB` once DEFLATE-compressed.
11//!
12//! # Container layout
13//!
14//! ```text
15//! ┌────────────────────────────── header (uncompressed) ──────────────────┐
16//! │ magic  "OHPK"           : 4 bytes                                      │
17//! │ version                 : u8   ( = 1 )                                 │
18//! │ flags                   : u8   (bit0 = body is DEFLATE-compressed)     │
19//! │ uncompressed body length: u32 LE                                       │
20//! └───────────────────────────────────────────────────────────────────────┘
21//! ┌────────────────────── body (DEFLATE-compressed) ─────────────────────┐
22//! │ manifest_json_len       : u32 LE                                      │
23//! │ manifest JSON           : manifest_json_len bytes                     │
24//! │ base-mesh section       : see [`BaseMesh`] docs                       │
25//! │ target_count            : u32 LE                                      │
26//! │ per-target sections     : target_count × target section              │
27//! └──────────────────────────────────────────────────────────────────────┘
28//! ```
29//!
30//! Quantisation ideas (sparse varint index deltas + max-abs i16 delta
31//! quantisation) are ported from the WASM `compressed_target::LitePack`
32//! encoder; nothing is imported across crates.
33//!
34//! # Model units
35//!
36//! The MakeHuman `base.obj` is authored in **decimetres** — one model unit is
37//! one decimetre, i.e. `100 mm`.  See [`MODEL_UNIT_MM`].  All quantisation
38//! error statistics are reported both in raw model units and in millimetres so
39//! that downstream tools can reason about them physically.
40
41use anyhow::{anyhow, ensure, Context, Result};
42use serde::{Deserialize, Serialize};
43
44// ───────────────────────────── format constants ────────────────────────────
45
46/// Magic bytes at the very start of every OHPK file.
47pub const OHPK_MAGIC: [u8; 4] = *b"OHPK";
48
49/// Current OHPK container version.
50pub const OHPK_VERSION: u8 = 1;
51
52/// Flag bit: the body is compressed with `oxiarc-deflate` (raw DEFLATE).
53pub const OHPK_FLAG_DEFLATE: u8 = 0x01;
54
55/// DEFLATE compression level used when writing the body (0..=9).
56pub const OHPK_DEFLATE_LEVEL: u8 = 9;
57
58/// Millimetres per model unit.
59///
60/// MakeHuman's `base.obj` is authored in decimetres, so `1 unit = 1 dm =
61/// 100 mm`.  Multiply any model-unit quantity by this constant to obtain
62/// millimetres.
63pub const MODEL_UNIT_MM: f32 = 100.0;
64
65/// Number of representable steps across the signed 16-bit range
66/// (`i16::MAX - i16::MIN`).
67const I16_STEPS: f32 = 65_535.0;
68
69/// Offset that maps the unsigned quantisation index `[0, 65535]` onto the
70/// signed `i16` range `[-32768, 32767]`.
71const I16_OFFSET: f32 = 32_768.0;
72
73/// Symmetric half-range used for max-abs delta quantisation (`i16::MAX`).
74const I16_HALF: f32 = 32_767.0;
75
76/// Convert a length expressed in model units to millimetres.
77#[inline]
78pub fn model_units_to_mm(units: f32) -> f32 {
79    units * MODEL_UNIT_MM
80}
81
82// ───────────────────────────────── manifest ────────────────────────────────
83
84/// One provenance file entry (name + hex SHA-256 of the upstream source).
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct CorePackFile {
87    /// Upstream file name (repo-relative).
88    pub name: String,
89    /// Lower-case hex SHA-256 of the upstream file bytes.
90    pub sha256: String,
91}
92
93/// Provenance block describing where the packed assets came from.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct CorePackProvenance {
96    /// Upstream repository (e.g. a MakeHuman community data repo URL).
97    pub upstream_repo: String,
98    /// Exact upstream commit the assets were taken from.
99    pub upstream_commit: String,
100    /// Per-file integrity records.
101    #[serde(default)]
102    pub files: Vec<CorePackFile>,
103}
104
105/// The JSON manifest embedded at the head of every OHPK body.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct CorePackManifest {
108    /// Human-readable pack name.
109    pub name: String,
110    /// Pack version string (independent of the container version).
111    pub version: String,
112    /// SPDX licence id — CC0 for the shipped MakeHuman-derived core pack.
113    #[serde(default = "default_license")]
114    pub license: String,
115    /// Upstream provenance / integrity information.
116    pub provenance: CorePackProvenance,
117    /// Optional minimum modelled age in years (safety floor).
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub age_floor_years: Option<f32>,
120    /// Free-form category tags for the pack as a whole.
121    #[serde(default)]
122    pub categories: Vec<String>,
123    /// Names of every morph target contained in the pack, in order.
124    #[serde(default)]
125    pub target_names: Vec<String>,
126}
127
128fn default_license() -> String {
129    "CC0-1.0".to_string()
130}
131
132impl CorePackManifest {
133    /// Construct a minimal manifest with the CC0 licence and empty provenance.
134    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
135        Self {
136            name: name.into(),
137            version: version.into(),
138            license: default_license(),
139            provenance: CorePackProvenance {
140                upstream_repo: String::new(),
141                upstream_commit: String::new(),
142                files: Vec::new(),
143            },
144            age_floor_years: None,
145            categories: Vec::new(),
146            target_names: Vec::new(),
147        }
148    }
149}
150
151// ─────────────────────────── quantisation reports ──────────────────────────
152
153/// Quantisation error for a single morph target.
154#[derive(Debug, Clone, PartialEq)]
155pub struct TargetErrorReport {
156    /// Target name.
157    pub name: String,
158    /// Maximum absolute per-component error, in model units.
159    pub max_error_units: f32,
160    /// The same error expressed in millimetres.
161    pub max_error_mm: f32,
162}
163
164/// Aggregate quantisation-honesty report for a whole pack.
165#[derive(Debug, Clone, PartialEq)]
166pub struct QuantizationReport {
167    /// Maximum absolute base-position error (any axis), in model units.
168    pub base_pos_max_error_units: f32,
169    /// The base-position error expressed in millimetres.
170    pub base_pos_max_error_mm: f32,
171    /// Maximum absolute UV error, if the pack carries UVs (dimensionless).
172    pub base_uv_max_error: Option<f32>,
173    /// Per-target quantisation errors.
174    pub targets: Vec<TargetErrorReport>,
175}
176
177// ─────────────────────────── low-level byte writer ─────────────────────────
178
179#[inline]
180fn push_u16(buf: &mut Vec<u8>, v: u16) {
181    buf.extend_from_slice(&v.to_le_bytes());
182}
183
184#[inline]
185fn push_u32(buf: &mut Vec<u8>, v: u32) {
186    buf.extend_from_slice(&v.to_le_bytes());
187}
188
189#[inline]
190fn push_i16(buf: &mut Vec<u8>, v: i16) {
191    buf.extend_from_slice(&v.to_le_bytes());
192}
193
194#[inline]
195fn push_f32(buf: &mut Vec<u8>, v: f32) {
196    buf.extend_from_slice(&v.to_le_bytes());
197}
198
199/// Append an unsigned LEB128 varint.
200fn push_uvarint(buf: &mut Vec<u8>, mut v: u64) {
201    loop {
202        let byte = (v & 0x7f) as u8;
203        v >>= 7;
204        if v == 0 {
205            buf.push(byte);
206            return;
207        }
208        buf.push(byte | 0x80);
209    }
210}
211
212// ─────────────────────────── low-level byte reader ─────────────────────────
213
214/// A bounds-checked, panic-free forward cursor over an in-memory byte slice.
215struct Reader<'a> {
216    data: &'a [u8],
217    pos: usize,
218}
219
220impl<'a> Reader<'a> {
221    fn new(data: &'a [u8]) -> Self {
222        Self { data, pos: 0 }
223    }
224
225    #[inline]
226    fn remaining(&self) -> usize {
227        self.data.len().saturating_sub(self.pos)
228    }
229
230    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
231        let end = self
232            .pos
233            .checked_add(n)
234            .ok_or_else(|| anyhow!("OHPK reader length overflow"))?;
235        ensure!(
236            end <= self.data.len(),
237            "OHPK truncated: need {n} bytes at offset {} but only {} remain",
238            self.pos,
239            self.remaining()
240        );
241        let slice = &self.data[self.pos..end];
242        self.pos = end;
243        Ok(slice)
244    }
245
246    fn u8(&mut self) -> Result<u8> {
247        Ok(self.take(1)?[0])
248    }
249
250    fn u16(&mut self) -> Result<u16> {
251        let b: [u8; 2] = self
252            .take(2)?
253            .try_into()
254            .map_err(|_| anyhow!("OHPK u16 slice conversion"))?;
255        Ok(u16::from_le_bytes(b))
256    }
257
258    fn u32(&mut self) -> Result<u32> {
259        let b: [u8; 4] = self
260            .take(4)?
261            .try_into()
262            .map_err(|_| anyhow!("OHPK u32 slice conversion"))?;
263        Ok(u32::from_le_bytes(b))
264    }
265
266    fn i16(&mut self) -> Result<i16> {
267        let b: [u8; 2] = self
268            .take(2)?
269            .try_into()
270            .map_err(|_| anyhow!("OHPK i16 slice conversion"))?;
271        Ok(i16::from_le_bytes(b))
272    }
273
274    fn f32(&mut self) -> Result<f32> {
275        let b: [u8; 4] = self
276            .take(4)?
277            .try_into()
278            .map_err(|_| anyhow!("OHPK f32 slice conversion"))?;
279        Ok(f32::from_le_bytes(b))
280    }
281
282    /// Read a `u32`-length-prefixed byte run.
283    fn len_prefixed_u32(&mut self) -> Result<&'a [u8]> {
284        let n = self.u32()? as usize;
285        self.take(n)
286    }
287
288    /// Read a `u16`-length-prefixed UTF-8 string.
289    fn string_u16(&mut self) -> Result<String> {
290        let n = self.u16()? as usize;
291        let bytes = self.take(n)?;
292        String::from_utf8(bytes.to_vec()).context("OHPK invalid UTF-8 string")
293    }
294
295    /// Decode an unsigned LEB128 varint.
296    fn uvarint(&mut self) -> Result<u64> {
297        let mut result: u64 = 0;
298        let mut shift: u32 = 0;
299        loop {
300            let byte = self.u8()?;
301            ensure!(shift < 64, "OHPK varint overflow");
302            result |= u64::from(byte & 0x7f) << shift;
303            if byte & 0x80 == 0 {
304                return Ok(result);
305            }
306            shift += 7;
307        }
308    }
309}
310
311// ─────────────────────────── quantisation helpers ──────────────────────────
312
313/// Per-axis affine quantiser: `value ≈ q * scale + bias` where `q: i16`.
314#[derive(Debug, Clone, Copy)]
315struct AxisQuant {
316    scale: f32,
317    bias: f32,
318}
319
320impl AxisQuant {
321    /// Fit a quantiser mapping `[lo, hi]` onto the full signed 16-bit range.
322    fn fit(lo: f32, hi: f32) -> Self {
323        let span = hi - lo;
324        if span > 0.0 && span.is_finite() {
325            let scale = span / I16_STEPS;
326            // q = -32768 → lo, q = 32767 → hi.
327            let bias = lo + I16_OFFSET * scale;
328            Self { scale, bias }
329        } else {
330            // Degenerate (flat / non-finite) axis: everything maps to `lo`.
331            Self {
332                scale: 0.0,
333                bias: lo,
334            }
335        }
336    }
337
338    #[inline]
339    fn quantise(&self, v: f32) -> i16 {
340        if self.scale > 0.0 {
341            let t = ((v - self.bias) / self.scale).round();
342            t.clamp(-I16_OFFSET, I16_HALF) as i16
343        } else {
344            0
345        }
346    }
347
348    #[inline]
349    fn dequantise(&self, q: i16) -> f32 {
350        f32::from(q) * self.scale + self.bias
351    }
352}
353
354/// Fit one [`AxisQuant`] per component of an interleaved point buffer.
355///
356/// `stride` is the number of `f32` components per element; only the first
357/// `dims` of them are considered.
358fn fit_axes(data: &[f32], stride: usize, dims: usize) -> Vec<AxisQuant> {
359    let mut lo = vec![f32::INFINITY; dims];
360    let mut hi = vec![f32::NEG_INFINITY; dims];
361    for chunk in data.chunks_exact(stride) {
362        for (axis, slot) in chunk.iter().take(dims).enumerate() {
363            let v = *slot;
364            if v < lo[axis] {
365                lo[axis] = v;
366            }
367            if v > hi[axis] {
368                hi[axis] = v;
369            }
370        }
371    }
372    (0..dims)
373        .map(|axis| {
374            let (l, h) = if lo[axis].is_finite() && hi[axis].is_finite() {
375                (lo[axis], hi[axis])
376            } else {
377                (0.0, 0.0)
378            };
379            AxisQuant::fit(l, h)
380        })
381        .collect()
382}
383
384// ───────────────────────────── builder targets ─────────────────────────────
385
386struct BuilderTarget {
387    name: String,
388    category: String,
389    sparse: Vec<(u32, [f32; 3])>,
390}
391
392// ───────────────────────────────── builder ─────────────────────────────────
393
394/// Incrementally assembles an OHPK v1 core pack in memory.
395///
396/// Nothing touches the filesystem; the whole pack is produced as a `Vec<u8>`
397/// from [`build`](CorePackBuilder::build), making the builder usable on
398/// `wasm32` as well.
399pub struct CorePackBuilder {
400    manifest: Option<CorePackManifest>,
401    base_positions: Vec<f32>,
402    base_indices: Vec<u32>,
403    base_uvs: Option<Vec<f32>>,
404    helper_meta: Option<Vec<u8>>,
405    targets: Vec<BuilderTarget>,
406}
407
408impl Default for CorePackBuilder {
409    fn default() -> Self {
410        Self::new()
411    }
412}
413
414impl CorePackBuilder {
415    /// Create an empty builder.
416    pub fn new() -> Self {
417        Self {
418            manifest: None,
419            base_positions: Vec::new(),
420            base_indices: Vec::new(),
421            base_uvs: None,
422            helper_meta: None,
423            targets: Vec::new(),
424        }
425    }
426
427    /// Set the base mesh.
428    ///
429    /// * `positions` — flat `x, y, z` triples (length must be a multiple of 3).
430    /// * `indices`   — triangle indices (stored raw as `u32`).
431    /// * `uvs`       — optional flat `u, v` pairs; when present its length must
432    ///   be exactly `2 * n_verts`.
433    ///
434    /// Length validation happens in [`build`](CorePackBuilder::build).
435    pub fn set_base_mesh(
436        &mut self,
437        positions: &[f32],
438        indices: &[u32],
439        uvs: Option<&[f32]>,
440    ) -> &mut Self {
441        self.base_positions = positions.to_vec();
442        self.base_indices = indices.to_vec();
443        self.base_uvs = uvs.map(<[f32]>::to_vec);
444        self
445    }
446
447    /// Attach opaque vertex-group / helper metadata for future use.
448    pub fn set_helper_metadata(&mut self, bytes: &[u8]) -> &mut Self {
449        self.helper_meta = Some(bytes.to_vec());
450        self
451    }
452
453    /// Add a sparse morph target.
454    ///
455    /// `sparse` is a list of `(vertex_index, [dx, dy, dz])` in model units.
456    /// Entries may be supplied in any order — they are sorted by index at
457    /// encode time.
458    pub fn add_target(
459        &mut self,
460        name: impl Into<String>,
461        category: impl Into<String>,
462        sparse: &[(u32, [f32; 3])],
463    ) -> &mut Self {
464        self.targets.push(BuilderTarget {
465            name: name.into(),
466            category: category.into(),
467            sparse: sparse.to_vec(),
468        });
469        self
470    }
471
472    /// Replace the pack manifest.
473    pub fn set_manifest(&mut self, manifest: CorePackManifest) -> &mut Self {
474        self.manifest = Some(manifest);
475        self
476    }
477
478    /// Serialise the pack to OHPK v1 bytes.
479    ///
480    /// Fails when no base mesh has been set, when buffer lengths are
481    /// inconsistent, or when compression fails.
482    pub fn build(&self) -> Result<Vec<u8>> {
483        ensure!(
484            !self.base_positions.is_empty(),
485            "cannot build an empty core pack: no base mesh set"
486        );
487        ensure!(
488            self.base_positions.len().is_multiple_of(3),
489            "base positions length {} is not a multiple of 3",
490            self.base_positions.len()
491        );
492        let n_verts = self.base_positions.len() / 3;
493        ensure!(n_verts > 0, "cannot build a core pack with zero vertices");
494
495        if let Some(uvs) = &self.base_uvs {
496            ensure!(
497                uvs.len() == n_verts * 2,
498                "UV buffer length {} does not match 2 * n_verts ({})",
499                uvs.len(),
500                n_verts * 2
501            );
502        }
503
504        let manifest = self.resolved_manifest();
505        let manifest_json =
506            serde_json::to_vec(&manifest).context("serialising OHPK manifest to JSON")?;
507
508        // ---- body ---------------------------------------------------------
509        let mut body = Vec::new();
510        push_u32(&mut body, u32::try_from(manifest_json.len()).context("manifest too large")?);
511        body.extend_from_slice(&manifest_json);
512
513        self.encode_base_mesh(&mut body, n_verts)?;
514
515        push_u32(
516            &mut body,
517            u32::try_from(self.targets.len()).context("too many targets")?,
518        );
519        for target in &self.targets {
520            encode_target(&mut body, target);
521        }
522
523        // ---- header + compression ----------------------------------------
524        let uncompressed_len = u32::try_from(body.len()).context("OHPK body exceeds 4 GiB")?;
525        let compressed = oxiarc_deflate::deflate(&body, OHPK_DEFLATE_LEVEL)
526            .map_err(|e| anyhow!("OHPK deflate failed: {e}"))?;
527
528        let mut out = Vec::with_capacity(10 + compressed.len());
529        out.extend_from_slice(&OHPK_MAGIC);
530        out.push(OHPK_VERSION);
531        out.push(OHPK_FLAG_DEFLATE);
532        push_u32(&mut out, uncompressed_len);
533        out.extend_from_slice(&compressed);
534        Ok(out)
535    }
536
537    /// Clone the manifest, filling `target_names` from the added targets when
538    /// the caller left it empty.
539    fn resolved_manifest(&self) -> CorePackManifest {
540        let mut manifest = self
541            .manifest
542            .clone()
543            .unwrap_or_else(|| CorePackManifest::new("oxihuman-core", "0"));
544        if manifest.target_names.is_empty() {
545            manifest.target_names = self.targets.iter().map(|t| t.name.clone()).collect();
546        }
547        manifest
548    }
549
550    fn encode_base_mesh(&self, body: &mut Vec<u8>, n_verts: usize) -> Result<()> {
551        let pos_axes = fit_axes(&self.base_positions, 3, 3);
552        let n_indices = self.base_indices.len();
553
554        push_u32(body, u32::try_from(n_verts).context("n_verts too large")?);
555        push_u32(body, u32::try_from(n_indices).context("n_indices too large")?);
556
557        for a in &pos_axes {
558            push_f32(body, a.scale);
559        }
560        for a in &pos_axes {
561            push_f32(body, a.bias);
562        }
563
564        // Quantise positions and record the honest max per-component error.
565        let mut pos_max_err = 0.0f32;
566        let mut quantised: Vec<i16> = Vec::with_capacity(n_verts * 3);
567        for chunk in self.base_positions.chunks_exact(3) {
568            for (axis, value) in chunk.iter().enumerate() {
569                let q = pos_axes[axis].quantise(*value);
570                let err = (value - pos_axes[axis].dequantise(q)).abs();
571                if err > pos_max_err {
572                    pos_max_err = err;
573                }
574                quantised.push(q);
575            }
576        }
577        push_f32(body, pos_max_err);
578        for q in &quantised {
579            push_i16(body, *q);
580        }
581
582        for idx in &self.base_indices {
583            push_u32(body, *idx);
584        }
585
586        // Optional UV section.
587        match &self.base_uvs {
588            Some(uvs) => {
589                body.push(1u8);
590                let uv_axes = fit_axes(uvs, 2, 2);
591                for a in &uv_axes {
592                    push_f32(body, a.scale);
593                }
594                for a in &uv_axes {
595                    push_f32(body, a.bias);
596                }
597                let mut uv_max_err = 0.0f32;
598                let mut uv_q: Vec<i16> = Vec::with_capacity(n_verts * 2);
599                for pair in uvs.chunks_exact(2) {
600                    for (axis, value) in pair.iter().enumerate() {
601                        let q = uv_axes[axis].quantise(*value);
602                        let err = (value - uv_axes[axis].dequantise(q)).abs();
603                        if err > uv_max_err {
604                            uv_max_err = err;
605                        }
606                        uv_q.push(q);
607                    }
608                }
609                push_f32(body, uv_max_err);
610                for q in &uv_q {
611                    push_i16(body, *q);
612                }
613            }
614            None => body.push(0u8),
615        }
616
617        // Optional helper-metadata section (reserved for future use).
618        match &self.helper_meta {
619            Some(meta) => {
620                body.push(1u8);
621                push_u32(body, u32::try_from(meta.len()).context("helper meta too large")?);
622                body.extend_from_slice(meta);
623            }
624            None => body.push(0u8),
625        }
626
627        Ok(())
628    }
629}
630
631/// Encode a single sparse target section into `body`.
632fn encode_target(body: &mut Vec<u8>, target: &BuilderTarget) {
633    // Sort affected vertices ascending so index deltas stay non-negative.
634    let mut sparse = target.sparse.clone();
635    sparse.sort_by_key(|(idx, _)| *idx);
636
637    // Max-abs component drives the symmetric i16 delta quantiser.
638    let max_abs = sparse
639        .iter()
640        .flat_map(|(_, d)| d.iter())
641        .fold(0.0f32, |acc, &c| acc.max(c.abs()));
642    let scale = if max_abs > 0.0 && max_abs.is_finite() {
643        max_abs / I16_HALF
644    } else {
645        0.0
646    };
647
648    // Quantise deltas and record the honest per-component max error.
649    let mut quantised: Vec<[i16; 3]> = Vec::with_capacity(sparse.len());
650    let mut max_err = 0.0f32;
651    for (_, d) in &sparse {
652        let mut q = [0i16; 3];
653        for axis in 0..3 {
654            let qi = if scale > 0.0 {
655                (d[axis] / scale).round().clamp(-I16_HALF, I16_HALF) as i16
656            } else {
657                0
658            };
659            let deq = f32::from(qi) * scale;
660            let err = (d[axis] - deq).abs();
661            if err > max_err {
662                max_err = err;
663            }
664            q[axis] = qi;
665        }
666        quantised.push(q);
667    }
668
669    let name = target.name.as_bytes();
670    let category = target.category.as_bytes();
671    push_u16(body, name.len() as u16);
672    body.extend_from_slice(name);
673    push_u16(body, category.len() as u16);
674    body.extend_from_slice(category);
675
676    push_f32(body, scale);
677    push_f32(body, max_err);
678    push_u32(body, sparse.len() as u32);
679
680    // Delta-encoded vertex indices as unsigned varints.
681    let mut prev = 0u32;
682    for (idx, _) in &sparse {
683        let delta = idx.wrapping_sub(prev);
684        push_uvarint(body, u64::from(delta));
685        prev = *idx;
686    }
687
688    // Quantised deltas, i16 × 3 each.
689    for q in &quantised {
690        push_i16(body, q[0]);
691        push_i16(body, q[1]);
692        push_i16(body, q[2]);
693    }
694}
695
696// ─────────────────────────────── parsed types ──────────────────────────────
697
698/// A parsed base mesh with its quantisers retained for de-quantisation.
699struct BaseMesh {
700    n_verts: usize,
701    pos_axes: [AxisQuant; 3],
702    pos_max_err: f32,
703    positions_q: Vec<i16>,
704    indices: Vec<u32>,
705    uvs: Option<UvSection>,
706    helper_meta: Option<Vec<u8>>,
707}
708
709struct UvSection {
710    axes: [AxisQuant; 2],
711    max_err: f32,
712    data_q: Vec<i16>,
713}
714
715/// A parsed morph target inside a [`CorePack`].
716pub struct CorePackTarget {
717    name: String,
718    category: String,
719    scale: f32,
720    max_abs_error_units: f32,
721    indices: Vec<u32>,
722    deltas_q: Vec<[i16; 3]>,
723}
724
725impl CorePackTarget {
726    /// Target name.
727    #[inline]
728    pub fn name(&self) -> &str {
729        &self.name
730    }
731
732    /// Target category.
733    #[inline]
734    pub fn category(&self) -> &str {
735        &self.category
736    }
737
738    /// Number of affected vertices.
739    #[inline]
740    pub fn len(&self) -> usize {
741        self.indices.len()
742    }
743
744    /// Whether the target affects no vertices.
745    #[inline]
746    pub fn is_empty(&self) -> bool {
747        self.indices.is_empty()
748    }
749
750    /// Honest maximum absolute per-component quantisation error (model units).
751    #[inline]
752    pub fn max_abs_error_units(&self) -> f32 {
753        self.max_abs_error_units
754    }
755
756    /// The affected vertex indices (sorted ascending).
757    #[inline]
758    pub fn indices(&self) -> &[u32] {
759        &self.indices
760    }
761
762    /// De-quantised sparse deltas as `(vertex_index, [dx, dy, dz])`.
763    pub fn sparse(&self) -> Vec<(u32, [f32; 3])> {
764        self.indices
765            .iter()
766            .zip(&self.deltas_q)
767            .map(|(&idx, q)| {
768                (
769                    idx,
770                    [
771                        f32::from(q[0]) * self.scale,
772                        f32::from(q[1]) * self.scale,
773                        f32::from(q[2]) * self.scale,
774                    ],
775                )
776            })
777            .collect()
778    }
779}
780
781/// A fully parsed OHPK v1 core pack held in memory.
782pub struct CorePack {
783    manifest: CorePackManifest,
784    base: BaseMesh,
785    targets: Vec<CorePackTarget>,
786}
787
788impl CorePack {
789    /// Parse an OHPK v1 pack from bytes.
790    ///
791    /// Every malformed input — bad magic, unsupported version, truncated
792    /// stream, an oversized declared body length, or a corrupt DEFLATE
793    /// payload — yields an `Err` and never panics.
794    pub fn parse(bytes: &[u8]) -> Result<Self> {
795        ensure!(bytes.len() >= 10, "OHPK too short for a header");
796        ensure!(bytes[0..4] == OHPK_MAGIC, "bad OHPK magic bytes");
797        let version = bytes[4];
798        ensure!(
799            version == OHPK_VERSION,
800            "unsupported OHPK version {version} (expected {OHPK_VERSION})"
801        );
802        let flags = bytes[5];
803        let declared_len = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]) as usize;
804        let payload = &bytes[10..];
805
806        let body = if flags & OHPK_FLAG_DEFLATE != 0 {
807            let inflated =
808                oxiarc_deflate::inflate(payload).map_err(|e| anyhow!("OHPK inflate failed: {e}"))?;
809            ensure!(
810                inflated.len() == declared_len,
811                "OHPK body length mismatch: header says {declared_len}, inflated {}",
812                inflated.len()
813            );
814            inflated
815        } else {
816            ensure!(
817                payload.len() == declared_len,
818                "OHPK body length mismatch: header says {declared_len}, payload {}",
819                payload.len()
820            );
821            payload.to_vec()
822        };
823
824        Self::parse_body(&body)
825    }
826
827    fn parse_body(body: &[u8]) -> Result<Self> {
828        let mut r = Reader::new(body);
829
830        let manifest_bytes = r.len_prefixed_u32().context("reading manifest JSON")?;
831        let manifest: CorePackManifest =
832            serde_json::from_slice(manifest_bytes).context("parsing OHPK manifest JSON")?;
833
834        let base = parse_base_mesh(&mut r)?;
835
836        let target_count = r.u32().context("reading target count")? as usize;
837        let mut targets = Vec::with_capacity(target_count.min(1024));
838        for i in 0..target_count {
839            let t = parse_target(&mut r).with_context(|| format!("parsing target #{i}"))?;
840            targets.push(t);
841        }
842
843        Ok(Self {
844            manifest,
845            base,
846            targets,
847        })
848    }
849
850    /// The embedded manifest.
851    #[inline]
852    pub fn manifest(&self) -> &CorePackManifest {
853        &self.manifest
854    }
855
856    /// Number of vertices in the base mesh.
857    #[inline]
858    pub fn vertex_count(&self) -> usize {
859        self.base.n_verts
860    }
861
862    /// De-quantised base positions as flat `x, y, z` triples.
863    pub fn base_positions(&self) -> Vec<f32> {
864        let mut out = Vec::with_capacity(self.base.positions_q.len());
865        for chunk in self.base.positions_q.chunks_exact(3) {
866            for (axis, &q) in chunk.iter().enumerate() {
867                out.push(self.base.pos_axes[axis].dequantise(q));
868            }
869        }
870        out
871    }
872
873    /// The raw base-mesh triangle indices.
874    #[inline]
875    pub fn base_indices(&self) -> &[u32] {
876        &self.base.indices
877    }
878
879    /// De-quantised base UVs as flat `u, v` pairs, if present.
880    pub fn base_uvs(&self) -> Option<Vec<f32>> {
881        self.base.uvs.as_ref().map(|uv| {
882            let mut out = Vec::with_capacity(uv.data_q.len());
883            for pair in uv.data_q.chunks_exact(2) {
884                for (axis, &q) in pair.iter().enumerate() {
885                    out.push(uv.axes[axis].dequantise(q));
886                }
887            }
888            out
889        })
890    }
891
892    /// Opaque helper / vertex-group metadata, if the pack carries any.
893    #[inline]
894    pub fn helper_metadata(&self) -> Option<&[u8]> {
895        self.base.helper_meta.as_deref()
896    }
897
898    /// The parsed morph targets.
899    #[inline]
900    pub fn targets(&self) -> &[CorePackTarget] {
901        &self.targets
902    }
903
904    /// Build the honest quantisation-error report for the whole pack.
905    pub fn quantization_report(&self) -> QuantizationReport {
906        let targets = self
907            .targets
908            .iter()
909            .map(|t| TargetErrorReport {
910                name: t.name.clone(),
911                max_error_units: t.max_abs_error_units,
912                max_error_mm: model_units_to_mm(t.max_abs_error_units),
913            })
914            .collect();
915        QuantizationReport {
916            base_pos_max_error_units: self.base.pos_max_err,
917            base_pos_max_error_mm: model_units_to_mm(self.base.pos_max_err),
918            base_uv_max_error: self.base.uvs.as_ref().map(|uv| uv.max_err),
919            targets,
920        }
921    }
922}
923
924fn parse_base_mesh(r: &mut Reader<'_>) -> Result<BaseMesh> {
925    let n_verts = r.u32().context("reading n_verts")? as usize;
926    let n_indices = r.u32().context("reading n_indices")? as usize;
927
928    let pos_axes = [
929        AxisQuant {
930            scale: r.f32()?,
931            bias: 0.0,
932        },
933        AxisQuant {
934            scale: r.f32()?,
935            bias: 0.0,
936        },
937        AxisQuant {
938            scale: r.f32()?,
939            bias: 0.0,
940        },
941    ];
942    // Biases are stored after the three scales.
943    let biases = [r.f32()?, r.f32()?, r.f32()?];
944    let pos_axes = [
945        AxisQuant {
946            scale: pos_axes[0].scale,
947            bias: biases[0],
948        },
949        AxisQuant {
950            scale: pos_axes[1].scale,
951            bias: biases[1],
952        },
953        AxisQuant {
954            scale: pos_axes[2].scale,
955            bias: biases[2],
956        },
957    ];
958
959    let pos_max_err = r.f32().context("reading base position error")?;
960
961    let mut positions_q = Vec::with_capacity(n_verts.min(1 << 20) * 3);
962    for _ in 0..n_verts * 3 {
963        positions_q.push(r.i16()?);
964    }
965
966    let mut indices = Vec::with_capacity(n_indices.min(1 << 21));
967    for _ in 0..n_indices {
968        indices.push(r.u32()?);
969    }
970
971    // Optional UV section.
972    let has_uvs = r.u8().context("reading has_uvs flag")?;
973    let uvs = if has_uvs != 0 {
974        let axes = [
975            AxisQuant {
976                scale: r.f32()?,
977                bias: 0.0,
978            },
979            AxisQuant {
980                scale: r.f32()?,
981                bias: 0.0,
982            },
983        ];
984        let uv_bias = [r.f32()?, r.f32()?];
985        let axes = [
986            AxisQuant {
987                scale: axes[0].scale,
988                bias: uv_bias[0],
989            },
990            AxisQuant {
991                scale: axes[1].scale,
992                bias: uv_bias[1],
993            },
994        ];
995        let max_err = r.f32()?;
996        let mut data_q = Vec::with_capacity(n_verts.min(1 << 20) * 2);
997        for _ in 0..n_verts * 2 {
998            data_q.push(r.i16()?);
999        }
1000        Some(UvSection {
1001            axes,
1002            max_err,
1003            data_q,
1004        })
1005    } else {
1006        None
1007    };
1008
1009    // Optional helper-metadata section.
1010    let has_helper = r.u8().context("reading has_helper flag")?;
1011    let helper_meta = if has_helper != 0 {
1012        Some(r.len_prefixed_u32().context("reading helper meta")?.to_vec())
1013    } else {
1014        None
1015    };
1016
1017    Ok(BaseMesh {
1018        n_verts,
1019        pos_axes,
1020        pos_max_err,
1021        positions_q,
1022        indices,
1023        uvs,
1024        helper_meta,
1025    })
1026}
1027
1028fn parse_target(r: &mut Reader<'_>) -> Result<CorePackTarget> {
1029    let name = r.string_u16().context("reading target name")?;
1030    let category = r.string_u16().context("reading target category")?;
1031    let scale = r.f32().context("reading target scale")?;
1032    let max_abs_error_units = r.f32().context("reading target error")?;
1033    let n = r.u32().context("reading target affected count")? as usize;
1034
1035    let mut indices = Vec::with_capacity(n.min(1 << 20));
1036    let mut prev = 0u32;
1037    for _ in 0..n {
1038        let delta = u32::try_from(r.uvarint()?).context("target index delta overflow")?;
1039        let idx = prev.wrapping_add(delta);
1040        indices.push(idx);
1041        prev = idx;
1042    }
1043
1044    let mut deltas_q = Vec::with_capacity(n.min(1 << 20));
1045    for _ in 0..n {
1046        deltas_q.push([r.i16()?, r.i16()?, r.i16()?]);
1047    }
1048
1049    Ok(CorePackTarget {
1050        name,
1051        category,
1052        scale,
1053        max_abs_error_units,
1054        indices,
1055        deltas_q,
1056    })
1057}
1058
1059// ──────────────────────────────────── tests ────────────────────────────────
1060
1061#[cfg(test)]
1062mod tests {
1063    use super::*;
1064
1065    fn sample_manifest() -> CorePackManifest {
1066        CorePackManifest {
1067            name: "oxihuman-core".to_string(),
1068            version: "1.2.3".to_string(),
1069            license: "CC0-1.0".to_string(),
1070            provenance: CorePackProvenance {
1071                upstream_repo: "https://example.invalid/makehuman-data".to_string(),
1072                upstream_commit: "0123456789abcdef0123456789abcdef01234567".to_string(),
1073                files: vec![
1074                    CorePackFile {
1075                        name: "base.obj".to_string(),
1076                        sha256: "a".repeat(64),
1077                    },
1078                    CorePackFile {
1079                        name: "targets/smile.target".to_string(),
1080                        sha256: "b".repeat(64),
1081                    },
1082                ],
1083            },
1084            age_floor_years: Some(18.0),
1085            categories: vec!["body".to_string(), "face".to_string()],
1086            target_names: vec!["smile".to_string(), "blink".to_string()],
1087        }
1088    }
1089
1090    /// A tiny quad mesh (4 verts, 2 triangles) spanning a known cube-ish box.
1091    fn sample_base() -> (Vec<f32>, Vec<u32>, Vec<f32>) {
1092        let positions = vec![
1093            -1.0, -2.0, 0.5, // v0
1094            1.0, -2.0, 0.5, // v1
1095            1.0, 2.0, -0.5, // v2
1096            -1.0, 2.0, -0.5, // v3
1097        ];
1098        let indices = vec![0u32, 1, 2, 0, 2, 3];
1099        let uvs = vec![0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
1100        (positions, indices, uvs)
1101    }
1102
1103    fn build_sample() -> Vec<u8> {
1104        let (positions, indices, uvs) = sample_base();
1105        let mut b = CorePackBuilder::new();
1106        b.set_manifest(sample_manifest());
1107        b.set_base_mesh(&positions, &indices, Some(&uvs));
1108        // Two sparse targets.
1109        b.add_target(
1110            "smile",
1111            "face",
1112            &[(1u32, [0.01, -0.02, 0.03]), (3u32, [-0.005, 0.004, 0.0])],
1113        );
1114        b.add_target("blink", "face", &[(2u32, [0.0, 0.0, 0.001])]);
1115        b.build().expect("build sample pack")
1116    }
1117
1118    #[test]
1119    fn manifest_exact_round_trip() {
1120        let bytes = build_sample();
1121        let pack = CorePack::parse(&bytes).expect("parse pack");
1122        assert_eq!(pack.manifest(), &sample_manifest());
1123    }
1124
1125    #[test]
1126    fn manifest_defaults_license_cc0() {
1127        let m = CorePackManifest::new("x", "1");
1128        assert_eq!(m.license, "CC0-1.0");
1129    }
1130
1131    #[test]
1132    fn base_positions_round_trip_within_bound() {
1133        let (positions, _indices, _uvs) = sample_base();
1134        let bytes = build_sample();
1135        let pack = CorePack::parse(&bytes).expect("parse pack");
1136
1137        let decoded = pack.base_positions();
1138        assert_eq!(decoded.len(), positions.len());
1139
1140        // Recompute the actual max per-component error and compare it to the
1141        // honestly reported one.
1142        let mut actual_max = 0.0f32;
1143        for (o, d) in positions.iter().zip(&decoded) {
1144            actual_max = actual_max.max((o - d).abs());
1145        }
1146        let report = pack.quantization_report();
1147        // Reported error must be honest: never smaller than reality …
1148        assert!(
1149            report.base_pos_max_error_units + 1e-9 >= actual_max,
1150            "reported {} < actual {}",
1151            report.base_pos_max_error_units,
1152            actual_max
1153        );
1154        // … and it is in fact exact (same de-quantisation formula).
1155        assert!(
1156            (report.base_pos_max_error_units - actual_max).abs() < 1e-6,
1157            "reported {} vs actual {}",
1158            report.base_pos_max_error_units,
1159            actual_max
1160        );
1161        // mm conversion follows the decimetre convention.
1162        assert!(
1163            (report.base_pos_max_error_mm - actual_max * MODEL_UNIT_MM).abs() < 1e-4,
1164            "mm conversion mismatch"
1165        );
1166
1167        // The error must respect the per-axis quantisation step (span/65535/2).
1168        // The x-span is 2.0, y-span 4.0, z-span 1.0 → worst half-step = 4/65535/2.
1169        let worst_half_step = 4.0f32 / I16_STEPS / 2.0;
1170        assert!(
1171            actual_max <= worst_half_step + 1e-6,
1172            "actual {} exceeds half-step {}",
1173            actual_max,
1174            worst_half_step
1175        );
1176    }
1177
1178    #[test]
1179    fn base_uvs_round_trip() {
1180        let (_p, _i, uvs) = sample_base();
1181        let bytes = build_sample();
1182        let pack = CorePack::parse(&bytes).expect("parse pack");
1183        let decoded = pack.base_uvs().expect("uvs present");
1184        assert_eq!(decoded.len(), uvs.len());
1185        for (o, d) in uvs.iter().zip(&decoded) {
1186            assert!((o - d).abs() < 1e-3, "uv {o} vs {d}");
1187        }
1188    }
1189
1190    #[test]
1191    fn base_indices_round_trip() {
1192        let (_p, indices, _u) = sample_base();
1193        let bytes = build_sample();
1194        let pack = CorePack::parse(&bytes).expect("parse pack");
1195        assert_eq!(pack.base_indices(), indices.as_slice());
1196    }
1197
1198    #[test]
1199    fn sparse_target_round_trip() {
1200        let bytes = build_sample();
1201        let pack = CorePack::parse(&bytes).expect("parse pack");
1202        assert_eq!(pack.targets().len(), 2);
1203
1204        let smile = &pack.targets()[0];
1205        assert_eq!(smile.name(), "smile");
1206        assert_eq!(smile.category(), "face");
1207        assert_eq!(smile.len(), 2);
1208
1209        let sparse = smile.sparse();
1210        // Sorted by index ascending: 1 then 3.
1211        assert_eq!(sparse[0].0, 1);
1212        assert_eq!(sparse[1].0, 3);
1213
1214        // Values recovered within the reported target error bound.
1215        let report = pack.quantization_report();
1216        let smile_err = report
1217            .targets
1218            .iter()
1219            .find(|t| t.name == "smile")
1220            .map(|t| t.max_error_units)
1221            .expect("smile in report");
1222
1223        let expected = [
1224            (1u32, [0.01f32, -0.02, 0.03]),
1225            (3u32, [-0.005, 0.004, 0.0]),
1226        ];
1227        for ((idx, got), (eidx, want)) in sparse.iter().zip(expected.iter()) {
1228            assert_eq!(idx, eidx);
1229            for k in 0..3 {
1230                assert!(
1231                    (got[k] - want[k]).abs() <= smile_err + 1e-6,
1232                    "target comp {} got {} want {} err bound {}",
1233                    k,
1234                    got[k],
1235                    want[k],
1236                    smile_err
1237                );
1238            }
1239        }
1240        // The report exposes mm as well.
1241        assert!(smile_err >= 0.0);
1242        let smile_mm = report
1243            .targets
1244            .iter()
1245            .find(|t| t.name == "smile")
1246            .map(|t| t.max_error_mm)
1247            .expect("smile mm");
1248        assert!((smile_mm - smile_err * MODEL_UNIT_MM).abs() < 1e-4);
1249    }
1250
1251    #[test]
1252    fn target_error_is_honest() {
1253        let bytes = build_sample();
1254        let pack = CorePack::parse(&bytes).expect("parse pack");
1255        let expected: std::collections::HashMap<u32, [f32; 3]> = [
1256            (1u32, [0.01f32, -0.02, 0.03]),
1257            (3u32, [-0.005, 0.004, 0.0]),
1258        ]
1259        .into_iter()
1260        .collect();
1261
1262        let smile = &pack.targets()[0];
1263        let mut actual_max = 0.0f32;
1264        for (idx, got) in smile.sparse() {
1265            let want = expected[&idx];
1266            for k in 0..3 {
1267                actual_max = actual_max.max((got[k] - want[k]).abs());
1268            }
1269        }
1270        // Reported error must be >= actual and exact to float epsilon.
1271        assert!(smile.max_abs_error_units() + 1e-9 >= actual_max);
1272        assert!((smile.max_abs_error_units() - actual_max).abs() < 1e-6);
1273    }
1274
1275    #[test]
1276    fn deflate_round_trip_through_oxiarc() {
1277        // A larger, mostly-flat target to exercise real DEFLATE on the body.
1278        let positions: Vec<f32> = (0..300).map(|i| (i as f32) * 0.01 - 1.5).collect();
1279        let indices: Vec<u32> = (0..99).collect();
1280        let mut sparse: Vec<(u32, [f32; 3])> = Vec::new();
1281        for i in (0..100).step_by(7) {
1282            sparse.push((i as u32, [0.002, -0.001, 0.0005]));
1283        }
1284        let mut b = CorePackBuilder::new();
1285        b.set_manifest(CorePackManifest::new("deflate-test", "1"));
1286        b.set_base_mesh(&positions, &indices, None);
1287        b.add_target("wave", "body", &sparse);
1288        let bytes = b.build().expect("build");
1289
1290        // Header must advertise DEFLATE.
1291        assert_eq!(&bytes[0..4], &OHPK_MAGIC);
1292        assert_eq!(bytes[4], OHPK_VERSION);
1293        assert_ne!(bytes[5] & OHPK_FLAG_DEFLATE, 0);
1294
1295        let pack = CorePack::parse(&bytes).expect("parse");
1296        assert_eq!(pack.vertex_count(), 100);
1297        assert_eq!(pack.base_indices().len(), 99);
1298        assert!(pack.base_uvs().is_none());
1299        assert_eq!(pack.targets().len(), 1);
1300        assert_eq!(pack.targets()[0].len(), sparse.len());
1301    }
1302
1303    #[test]
1304    fn empty_pack_build_errors() {
1305        let b = CorePackBuilder::new();
1306        assert!(b.build().is_err());
1307    }
1308
1309    #[test]
1310    fn odd_positions_length_errors() {
1311        let mut b = CorePackBuilder::new();
1312        b.set_base_mesh(&[0.0, 1.0], &[], None); // length 2, not a multiple of 3
1313        assert!(b.build().is_err());
1314    }
1315
1316    #[test]
1317    fn mismatched_uv_length_errors() {
1318        let mut b = CorePackBuilder::new();
1319        b.set_base_mesh(&[0.0, 0.0, 0.0], &[0], Some(&[0.0])); // needs 2 uvs, got 1
1320        assert!(b.build().is_err());
1321    }
1322
1323    #[test]
1324    fn bad_magic_rejected() {
1325        let mut bytes = build_sample();
1326        bytes[0] = b'X';
1327        assert!(CorePack::parse(&bytes).is_err());
1328    }
1329
1330    #[test]
1331    fn bad_version_rejected() {
1332        let mut bytes = build_sample();
1333        bytes[4] = 99;
1334        assert!(CorePack::parse(&bytes).is_err());
1335    }
1336
1337    #[test]
1338    fn truncated_header_rejected() {
1339        let bytes = build_sample();
1340        assert!(CorePack::parse(&bytes[0..6]).is_err());
1341    }
1342
1343    #[test]
1344    fn truncated_body_rejected() {
1345        let bytes = build_sample();
1346        // Cut the compressed body in half — inflate must fail, not panic.
1347        let cut = 10 + (bytes.len() - 10) / 2;
1348        assert!(CorePack::parse(&bytes[0..cut]).is_err());
1349    }
1350
1351    #[test]
1352    fn oversized_declared_length_rejected() {
1353        let mut bytes = build_sample();
1354        // Force the declared uncompressed length to a huge value.
1355        bytes[6] = 0xff;
1356        bytes[7] = 0xff;
1357        bytes[8] = 0xff;
1358        bytes[9] = 0x7f;
1359        let res = CorePack::parse(&bytes);
1360        assert!(res.is_err());
1361    }
1362
1363    #[test]
1364    fn empty_input_rejected() {
1365        assert!(CorePack::parse(&[]).is_err());
1366    }
1367
1368    #[test]
1369    fn helper_metadata_round_trip() {
1370        let (positions, indices, _uvs) = sample_base();
1371        let mut b = CorePackBuilder::new();
1372        b.set_manifest(CorePackManifest::new("helper", "1"));
1373        b.set_base_mesh(&positions, &indices, None);
1374        b.set_helper_metadata(&[1, 2, 3, 4, 5]);
1375        let bytes = b.build().expect("build");
1376        let pack = CorePack::parse(&bytes).expect("parse");
1377        assert_eq!(pack.helper_metadata(), Some(&[1u8, 2, 3, 4, 5][..]));
1378    }
1379
1380    #[test]
1381    fn target_names_auto_filled_when_empty() {
1382        let (positions, indices, _uvs) = sample_base();
1383        let mut manifest = CorePackManifest::new("auto", "1");
1384        manifest.target_names.clear();
1385        let mut b = CorePackBuilder::new();
1386        b.set_manifest(manifest);
1387        b.set_base_mesh(&positions, &indices, None);
1388        b.add_target("a", "cat", &[(0u32, [0.1, 0.0, 0.0])]);
1389        b.add_target("b", "cat", &[(1u32, [0.0, 0.1, 0.0])]);
1390        let bytes = b.build().expect("build");
1391        let pack = CorePack::parse(&bytes).expect("parse");
1392        assert_eq!(pack.manifest().target_names, vec!["a", "b"]);
1393    }
1394
1395    #[test]
1396    fn size_target_reasonable() {
1397        // Sanity: a moderate mesh + targets stays comfortably compact.
1398        let n = 2000usize;
1399        let positions: Vec<f32> = (0..n * 3).map(|i| ((i % 97) as f32) * 0.01).collect();
1400        let indices: Vec<u32> = (0..(n as u32 - 1) * 3).map(|i| i % n as u32).collect();
1401        let mut b = CorePackBuilder::new();
1402        b.set_manifest(CorePackManifest::new("size", "1"));
1403        b.set_base_mesh(&positions, &indices, None);
1404        for t in 0..40 {
1405            let sparse: Vec<(u32, [f32; 3])> = (0..50)
1406                .map(|k| ((t * 50 + k) as u32 % n as u32, [0.001, 0.001, 0.001]))
1407                .collect();
1408            b.add_target(format!("t{t}"), "body", &sparse);
1409        }
1410        let bytes = b.build().expect("build");
1411        assert!(bytes.len() < 2 * 1024 * 1024, "pack size {}", bytes.len());
1412    }
1413}