Merge pull request #1246 from storytold/prc-compressed

craft-prc: rebuild compressed tessellations whose normals are recalculated (#778)
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-13 22:27:44 -05:00
commit 2239b0afb0
5 files changed
+1063 -42

No files matched your search

-1
View File
@@ -69,7 +69,6 @@ impl<'a> BitReader<'a> {
}
/// Array elements this reader may still decode.
#[cfg(test)]
pub(crate) fn elements_left(&self) -> usize {
self.budget.elements
}
+141 -40
View File
@@ -2,12 +2,14 @@
//! (172) with its `TESS_Face`s (174), `TESS_3D_Wire` (175), `TESS_Markup` (176) and
//! `TESS_3D_Compressed` (173) [WD 7.3.7, 7.8].
//!
//! `TESS_3D_Compressed` is read field by field to its end [WD 7.8.9.7] so the entities after
//! it decode; its triangles are not rebuilt.
//! `TESS_3D_Compressed` is read field by field to its end [WD 7.8.9.7]. One whose normals are
//! recalculated is rebuilt into triangles ([`compressed`]); one that stores its normals, or
//! whose arrays do not fit the traversal, is not.
//!
//! The schema runs after the fields of each concrete type read here (201, 172, 173, 174, 175,
//! 176, 305), not for the abstract levels (`PRCBase`, `TESS`).
mod compressed;
mod triangulate;
use crate::arrays;
@@ -49,7 +51,8 @@ pub enum Tessellation {
Wire(Vec<Vec<[f64; 3]>>),
/// A `TESS_Markup` (PMI drawing), read past but not decoded.
Markup,
/// A `TESS_3D_Compressed` mesh, read past but not rebuilt.
/// A `TESS_3D_Compressed` mesh, read past but not rebuilt: it stores its normals, or its
/// arrays do not fit the traversal. One that is rebuilt is a [`Tessellation::Mesh`].
#[non_exhaustive]
Compressed {
/// The entity's triangle count.
@@ -65,7 +68,8 @@ pub struct TriangleMesh {
/// The coordinate array as points.
pub positions: Vec<[f64; 3]>,
/// Counter-clockwise triangles seen from the outside, as indices into
/// [`Self::positions`] [WD 7.8.5.2].
/// [`Self::positions`] [WD 7.8.5.2]. A rebuilt `TESS_3D_Compressed` keeps its traversal's
/// winding, which is not oriented.
pub triangles: Vec<[u32; 3]>,
/// `triangles` split by `TESS_Face`: face `i` is `triangles[faces[i].clone()]`.
pub faces: Vec<std::ops::Range<usize>>,
@@ -136,7 +140,7 @@ impl Ctx<'_, '_> {
self.markup()?;
Tessellation::Markup
}
TESS_3D_COMPRESSED => Tessellation::Compressed { triangles: self.tess_3d_compressed()? },
TESS_3D_COMPRESSED => self.tess_3d_compressed()?,
t => return Err(malformed(format!("entity type {t} in the tessellation array"))),
});
}
@@ -295,23 +299,25 @@ impl Ctx<'_, '_> {
self.schema.skip_added_fields(TESS_MARKUP, &mut self.r)
}
/// `TESS_3D_Compressed` (173) after its type code [WD 7.8.9.7], read past; returns its
/// triangle count. prc-rs: the field order and version gates follow its schema.
/// `TESS_3D_Compressed` (173) after its type code [WD 7.8.9.7]: rebuilt into a mesh when
/// its normals are recalculated and its arrays fit, else read past to its end. prc-rs: the
/// field order and version gates follow its schema.
///
/// Sizes the WD leaves unstated: the face count is the largest face index + 1,
/// `normal_is_reversed` is one bit per triangle, and the point-reference array's
/// compressed flag is implicit (more than three references).
fn tess_3d_compressed(&mut self) -> Result<usize, PrcError> {
fn tess_3d_compressed(&mut self) -> Result<Tessellation, PrcError> {
let r = &mut self.r;
r.bit()?; // is_calculated
r.bit()?; // has_faces
r.double()?; // tolerance
let tolerance = r.double()?;
let mut origin = [0.0f64; 3];
if self.version >= ORIGIN_FROM {
for _ in 0..3 {
r.float_as_bytes()?;
for o in &mut origin {
*o = f64::from(r.float_as_bytes()?);
}
}
arrays::compressed_integer_array(r)?; // point_array
let points = arrays::compressed_integer_array(r)?; // point_array
let edge_status = arrays::character_array(r, 2, None, false)?;
let face_of = arrays::compressed_indice_array(r, None)?;
let t = face_of.len();
@@ -320,42 +326,50 @@ impl Ctx<'_, '_> {
}
let faces = face_of.iter().max().map_or(0, |&m| (m as usize).saturating_add(1));
let n = r.unsigned_integer()? as usize;
let references = arrays::bool_array(r, n)?.iter().filter(|&&b| b).count();
let refs = arrays::compressed_indice_array(r, Some(references > 3))?;
if refs.len() != references {
return Err(malformed(format!("{} point references for {references} flagged points", refs.len())));
let is_reference = arrays::bool_array(r, n)?;
let flagged = is_reference.iter().filter(|&&b| b).count();
let references = arrays::compressed_indice_array(r, Some(flagged > 3))?;
if references.len() != flagged {
return Err(malformed(format!("{} point references for {flagged} flagged points", references.len())));
}
self.compressed_normals(t, faces)?;
let r = &mut self.r;
if r.bit()? {
arrays::bool_array(r, faces)?; // is_point_color_on_face
arrays::character_array(r, 8, None, false)?; // point_color_array
let recalc = self.compressed_normals(t, faces)?;
let attributes = self.compressed_attributes(faces)?;
if !recalc {
return Ok(Tessellation::Compressed { triangles: t });
}
if r.bit()? {
arrays::bool_array(r, faces)?; // is_multiple_line_attribute_on_face
}
arrays::short_array(r, 16)?; // line_attributes
if !r.bit()? {
self.compressed_texture_parameter()?;
if !self.r.bit()? {
arrays::bool_array(&mut self.r, faces)?; // face_has_texture
}
}
if self.r.bit()? {
arrays::character_array(&mut self.r, 8, None, false)?; // behaviours
}
self.schema.skip_added_fields(TESS_3D_COMPRESSED, &mut self.r)?;
Ok(t)
let arrays = compressed::Arrays {
tolerance,
origin,
points: &points,
edge_status: &edge_status,
triangles: t,
is_reference: &is_reference,
references: &references,
limit: self.r.elements_left(),
};
// A walk that would outgrow the allowance left stops there, and the arrays stay
// compressed, as do arrays the walk does not fit.
let Ok(m) = compressed::rebuild(&arrays) else {
return Ok(Tessellation::Compressed { triangles: t });
};
// The rebuilt triangles, their styles and every face range are charged once the arrays
// fit and before any of them is made, so arrays that do not fit spend nothing.
let styled = attributes.styled(faces);
let kept = m.triangles.len().saturating_mul(triangle_elements(false, 0, styled));
self.r.take_elements(kept.saturating_add(faces.saturating_mul(RANGE_ELEMENTS)))?;
let styles = if styled { attributes.styles(&face_of, faces) } else { Vec::new() };
Ok(Tessellation::Mesh(by_face(m, &face_of, faces, &styles)))
}
/// A compressed tessellation's normals, from `must_recalculate_normals` on, read past.
fn compressed_normals(&mut self, triangles: usize, faces: usize) -> Result<(), PrcError> {
/// A compressed tessellation's normals, from `must_recalculate_normals` on, read past;
/// returns whether they are recalculated.
fn compressed_normals(&mut self, triangles: usize, faces: usize) -> Result<bool, PrcError> {
let r = &mut self.r;
if r.bit()? {
arrays::bool_array(r, triangles)?; // normal_is_reversed
r.double()?; // crease_angle
r.character()?; // normal_recalculation_flags
return Ok(());
return Ok(true);
}
let angle_bits = u32::from(r.character()?);
if angle_bits > MAX_ANGLE_BITS {
@@ -365,7 +379,29 @@ impl Ctx<'_, '_> {
arrays::bool_array(r, n)?; // is_binary_normal
arrays::short_array(r, angle_bits)?; // normal angles
arrays::bool_array(r, faces)?; // is_face_planar
Ok(())
Ok(false)
}
/// A compressed tessellation's colours, line attributes, texture and behaviours, through
/// its added schema fields; returns what its faces' styles are made from.
fn compressed_attributes(&mut self, faces: usize) -> Result<FaceAttributes, PrcError> {
let r = &mut self.r;
if r.bit()? {
arrays::bool_array(r, faces)?; // is_point_color_on_face
arrays::character_array(r, 8, None, false)?; // point_color_array
}
// is_multiple_line_attribute_on_face
let multi = if r.bit()? { arrays::bool_array(r, faces)? } else { Vec::new() };
let line_attributes = arrays::short_array(r, 16)?;
if !r.bit()? {
self.compressed_texture_parameter()?;
if !self.r.bit()? {
arrays::bool_array(&mut self.r, faces)?; // face_has_texture
}
}
let behaviours = if self.r.bit()? { arrays::character_array(&mut self.r, 8, None, false)? } else { Vec::new() };
self.schema.skip_added_fields(TESS_3D_COMPRESSED, &mut self.r)?;
Ok(FaceAttributes { multi, line_attributes, behaviours })
}
/// `CompressedTextureParameter` [WD 7.8.9.8-7.8.9.9; pdf-issues #729, #749], read past.
@@ -387,6 +423,71 @@ impl Ctx<'_, '_> {
}
}
/// Budget elements one face range (`Range<usize>`) takes.
const RANGE_ELEMENTS: usize = std::mem::size_of::<std::ops::Range<usize>>().div_ceil(8);
/// What a compressed tessellation's faces are styled from [WD 7.8.9.5]: with no face flagged
/// `is_multiple_line_attribute_on_face` (`multi`), `line_attributes` holds one style index + 1
/// per face (0 keeps the owner's graphics) and `behaviours` one behaviour byte per face.
struct FaceAttributes {
multi: Vec<bool>,
line_attributes: Vec<i32>,
behaviours: Vec<i32>,
}
impl FaceAttributes {
/// Whether the faces give their triangles styles. The one-per-triangle layout of a flagged
/// face is not established, so such a tessellation keeps its owner's graphics, as does one
/// whose attributes and behaviours are all 0.
fn styled(&self, faces: usize) -> bool {
let behaviours = self.behaviours.len() == faces && self.behaviours.iter().any(|&b| b != 0);
!self.multi.iter().any(|&b| b) && self.line_attributes.len() == faces && (behaviours || self.line_attributes.iter().any(|&a| a != 0))
}
/// Per triangle, the style its face gives it; call only when [`Self::styled`].
fn styles(&self, face_of: &[u32], faces: usize) -> Vec<TriangleStyle> {
let behaviour = |f: usize| {
if self.behaviours.len() == faces { self.behaviours.get(f).and_then(|&b| u16::try_from(b).ok()).unwrap_or(0) } else { 0 }
};
face_of
.iter()
.map(|&f| {
let f = f as usize;
TriangleStyle { style: self.line_attributes.get(f).and_then(|&a| u32::try_from(a).ok()).unwrap_or(0), behaviour: behaviour(f) }
})
.collect()
}
}
/// A rebuilt compressed mesh with its triangles grouped by face, in walk order within each,
/// so face `i` is `triangles[faces[i].clone()]` as for a `TESS_3D`. `faces` is charged to the
/// element budget by the caller.
fn by_face(m: compressed::Rebuilt, face_of: &[u32], faces: usize, styles: &[TriangleStyle]) -> TriangleMesh {
let mut order: Vec<usize> = (0..m.triangles.len()).collect();
order.sort_by_key(|&i| face_of.get(i).copied().unwrap_or(0));
let mut mesh = TriangleMesh { positions: m.positions, normals_recalculated: true, ..TriangleMesh::default() };
mesh.faces = Vec::with_capacity(faces);
for &i in &order {
let f = face_of.get(i).map_or(0, |&f| f as usize);
while mesh.faces.len() <= f {
let at = mesh.triangles.len();
mesh.faces.push(at..at);
}
if let (Some(&t), Some(r)) = (m.triangles.get(i), mesh.faces.last_mut()) {
mesh.triangles.push(t);
r.end = mesh.triangles.len();
}
if let Some(&s) = styles.get(i) {
mesh.triangle_styles.push(s);
}
}
while mesh.faces.len() < faces {
let at = mesh.triangles.len();
mesh.faces.push(at..at);
}
mesh
}
/// The normal array and each triangle's corner normals, or both empty when any slot fails to
/// name a normal: a mesh shades from its stored normals wholly or not at all.
fn stored_normals(coords: &[f64], slots: &[[u32; 3]], triangles: usize) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
+353
View File
@@ -0,0 +1,353 @@
//! Triangle reconstruction for a `TESS_3D_Compressed` (173) whose normals are recalculated
//! [WD 7.8.9, 7.8.9.2, 7.8.9.7].
//!
//! The WD names the arrays but not the traversal that wrote them; the rules below are the ones
//! the arrays fit when every slot, reference and point is consumed exactly. They are inferred
//! from compressed tessellations and are not WD text.
//!
//! Traversal. A component starts with a seed triangle of three slots; every later triangle
//! takes one slot, its apex. A slot flagged in `point_is_reference` takes the next
//! `point_reference_array` value, a vertex number; any other slot makes the next vertex from
//! the next `point_array` triple. Triangle `[A B C]`, entered across `[A B]`, reads
//! `edge_status & 3`: bit 1 continues across `[C B]`, bit 0 across `[A C]`; with both,
//! `[C B]` is taken and `[A C]` pushed. With neither, pushed edges are popped, skipping any
//! already shared by two triangles; an empty stack starts the next component. A triangle
//! whose continuation edge is already shared by two triangles, while the other edge is open,
//! continues across the open one.
//!
//! Geometry, in units of `tolerance`:
//! - seed: `V0 = origin + d`, `V1 = V0 + d`, `V2 = mid(V0, V1) + d`;
//! - apex across `[P Q]` with the triangle's third vertex `W` [WD 7.8.9.2]: with
//! `O = (P + Q) * 0.5`, `L` and `H` the lower- and higher-numbered of `P`, `Q`,
//! `X = unitize(L - H)`, `Z = unitize((W - O) x X)` and `Y = unitize(Z x X)`, the apex is
//! `((O + d.x X) - d.y Y) - d.z Z`.
//!
//! Arithmetic. The encoder frames each apex on the points it already approximated [WD 7.8.9],
//! so the decoder repeats the encoder's floating point: a last-bit difference compounds from
//! apex to apex. `unitize` therefore matches the WD's `PrcPt::Unitize` pseudocode [WD 12.3] as
//! read, whose length is not exact, and the operations keep the order above.
//!
//! Fold. A triangle folded against its parent swaps bits 1 and 0 for its own continuation.
//! Without stored normals to orient it, a new apex lying in its parent's plane on the parent's
//! side (`d.z == 0`, `d.y > 0`) is folded. A component that does not fit is rewound and walked
//! again with the fold inverted at its second triangle, where an apex that is a reference
//! leaves no other trace of its fold.
//!
//! Only the three-statuses-per-triangle `edge_status` form is reconstructed (the first `T`
//! are read); the one-per-triangle form walks differently.
use std::collections::HashMap;
type V = [f64; 3];
/// The arrays of one `TESS_3D_Compressed` the walk reads.
pub(super) struct Arrays<'a> {
pub(super) tolerance: f64,
pub(super) origin: V,
pub(super) points: &'a [i64],
pub(super) edge_status: &'a [i32],
pub(super) triangles: usize,
pub(super) is_reference: &'a [bool],
pub(super) references: &'a [u32],
/// Budget elements the walk may hold at once (see [`Walk::held`]).
pub(super) limit: usize,
}
/// The rebuilt vertices and triangles, in walk order.
#[derive(Debug)]
pub(super) struct Rebuilt {
pub(super) positions: Vec<V>,
pub(super) triangles: Vec<[u32; 3]>,
}
/// Rebuilds the mesh, or says why the arrays do not fit the traversal.
pub(super) fn rebuild(a: &Arrays<'_>) -> Result<Rebuilt, &'static str> {
if a.edge_status.len() != a.triangles.saturating_mul(3) {
return Err("the one-status-per-triangle edge form is not reconstructed");
}
let mut w = Walk::new(a.triangles, a.limit);
w.walk(a)?;
if w.slot != a.is_reference.len() || w.ri != a.references.len() || w.pi != a.points.len() {
return Err("the decoded triangles do not use up the stored arrays");
}
Ok(Rebuilt { positions: w.pos, triangles: w.tris })
}
fn add(a: V, b: V) -> V {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn sub(a: V, b: V) -> V {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn scale(a: V, s: f64) -> V {
a.map(|c| c * s)
}
fn dot(a: V, b: V) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: V, b: V) -> V {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
fn key(a: u32, b: u32) -> (u32, u32) {
if a < b { (a, b) } else { (b, a) }
}
/// `PrcPt::Length` [WD 12.3]: the squared length summed in `f64` and rounded to `f32`, then
/// Newton's square root in `f64`, seeded with that `f32` with its exponent halved. The printed
/// loop never advances its iterate; it is read as iterating until two successive values are
/// equal, `None` (the WD's -1) when 100 steps do not settle.
fn wd_length(a: V) -> Option<f64> {
let squared = (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]) as f32;
let bits = squared.to_bits();
let exponent = (bits >> 23) & 0xff;
let seed = if exponent > 127 { f32::from_bits((bits & !(0xff << 23)) | (((exponent - 127) / 2 + 127) << 23)) } else { squared };
let squared = f64::from(squared);
let mut x = f64::from(seed);
for _ in 0..100 {
let next = 0.5 * (x + squared / x);
if next == x {
return Some(next);
}
x = next;
}
None
}
/// `PrcPt::Unitize` [WD 12.3]: each component divided by [`wd_length`]; `None` below
/// `FLT_EPSILON`.
fn unitize(a: V) -> Option<V> {
let l = wd_length(a).filter(|&l| l >= f64::from(f32::EPSILON))?;
Some([a[0] / l, a[1] / l, a[2] / l])
}
/// The `Z` of `PrcPt::MakeOrthoRep` [WD 12.3] on `X`: `unitize(X x (0, 1, 0))`, or
/// `X x (1, 0, 0)` when that is null, from `X` unitized again as the pseudocode does.
fn make_ortho_rep_z(x: V) -> Option<V> {
let x = unitize(x)?;
unitize(cross(x, [0.0, 1.0, 0.0])).or_else(|| unitize(cross(x, [1.0, 0.0, 0.0])))
}
/// The apex across `[lo hi]` (`lo` the lower-numbered vertex) opposite `w`, `d` already scaled
/// by the tolerance [WD 7.8.9.2]. A null `Z` or `Y` takes `MakeOrthoRep(X)`'s `Z` as the WD
/// prescribes, with `Y = unitize(Z x X)` from the frame's own `X` (not the copy `MakeOrthoRep`
/// unitized again, whose last bits differ), turned a half-turn about `X` when the residue of
/// `(W - O) x X` points against that `Z`. A null `X`, an edge the WD's non-degeneracy rule
/// excludes, is used as it is.
fn apex(lo: V, hi: V, w: V, d: V) -> V {
let o = scale(add(lo, hi), 0.5);
let x = sub(lo, hi);
let x = unitize(x).unwrap_or(x);
let residue = cross(sub(w, o), x);
let (y, z) = unitize(residue)
.and_then(|z| Some((unitize(cross(z, x))?, z)))
.or_else(|| {
let z = make_ortho_rep_z(x)?;
let y = unitize(cross(z, x))?;
Some(if dot(residue, z) < 0.0 { (scale(y, -1.0), scale(z, -1.0)) } else { (y, z) })
})
.unwrap_or_default();
sub(sub(add(o, scale(x, d[0])), scale(y, d[1])), scale(z, d[2]))
}
/// Traversal state; `log` records every edge-count increment so a component can be rewound.
/// Everything it holds is checked against [`Arrays::limit`] after each triangle.
#[derive(Default)]
struct Walk {
pos: Vec<V>,
tris: Vec<[u32; 3]>,
edges: HashMap<(u32, u32), u8>,
log: Vec<(u32, u32)>,
stack: Vec<(u32, u32, u32)>,
next: Option<(u32, u32, u32)>,
slot: usize,
ri: usize,
pi: usize,
}
/// Where a component starts, to rewind to.
#[derive(Clone, Copy)]
struct Mark {
tri: usize,
pos: usize,
log: usize,
slot: usize,
ri: usize,
pi: usize,
}
impl Walk {
/// Reserves room for `t` triangles, no more than `limit` elements' worth (two each).
fn new(t: usize, limit: usize) -> Self {
Walk { tris: Vec::with_capacity(t.min(limit / 2).min(1 << 20)), ..Walk::default() }
}
fn mark(&self, tri: usize) -> Mark {
Mark { tri, pos: self.pos.len(), log: self.log.len(), slot: self.slot, ri: self.ri, pi: self.pi }
}
fn rewind(&mut self, m: Mark) {
for k in self.log.drain(m.log..) {
if let Some(n) = self.edges.get_mut(&k) {
*n = n.saturating_sub(1);
}
}
self.pos.truncate(m.pos);
self.tris.truncate(m.tri);
self.stack.clear();
self.next = None;
(self.slot, self.ri, self.pi) = (m.slot, m.ri, m.pi);
}
/// One slot: `Some(v)` a reference to vertex `v`, `None` a new point.
fn take(&mut self, a: &Arrays<'_>) -> Result<Option<u32>, &'static str> {
let flagged = *a.is_reference.get(self.slot).ok_or("slots run out")?;
self.slot = self.slot.saturating_add(1);
if !flagged {
return Ok(None);
}
let v = *a.references.get(self.ri).ok_or("references run out")?;
self.ri = self.ri.saturating_add(1);
self.get(v)?;
Ok(Some(v))
}
fn point(&mut self, a: &Arrays<'_>) -> Result<V, &'static str> {
let Some(&[x, y, z]) = a.points.get(self.pi..self.pi.saturating_add(3)) else {
return Err("points run out");
};
self.pi = self.pi.saturating_add(3);
let tol = a.tolerance;
Ok([x as f64 * tol, y as f64 * tol, z as f64 * tol])
}
fn get(&self, v: u32) -> Result<V, &'static str> {
self.pos.get(v as usize).copied().ok_or("a triangle refers to a vertex not yet decoded")
}
fn push(&mut self, p: V) -> Result<u32, &'static str> {
let id = u32::try_from(self.pos.len()).map_err(|_| "too many vertices")?;
self.pos.push(p);
Ok(id)
}
/// The next triangle and its default fold: the seed, or the apex across the edge to
/// continue from.
fn triangle(&mut self, a: &Arrays<'_>) -> Result<([u32; 3], bool), &'static str> {
if let Some((p, q, w)) = self.next.take() {
if let Some(r) = self.take(a)? {
return Ok(([p, q, r], false));
}
let (pp, qq, ww) = (self.get(p)?, self.get(q)?, self.get(w)?);
let (lo, hi) = if p < q { (pp, qq) } else { (qq, pp) };
let fold = matches!(a.points.get(self.pi..self.pi.saturating_add(3)), Some(&[_, dy, 0]) if dy > 0);
let d = self.point(a)?;
let r = self.push(apex(lo, hi, ww, d))?;
return Ok(([p, q, r], fold));
}
let mut v = [0u32; 3];
for k in 0..3 {
let id = match self.take(a)? {
Some(r) => r,
None => {
let d = self.point(a)?;
let p = match k {
0 => add(a.origin, d),
1 => add(self.get(v[0])?, d),
_ => add(scale(add(self.get(v[0])?, self.get(v[1])?), 0.5), d),
};
self.push(p)?
}
};
if let Some(s) = v.get_mut(k) {
*s = id;
}
}
Ok((v, false))
}
/// Budget elements (8 bytes each) the walk's arrays hold, by length: growth can add up to
/// as much again in spare capacity.
fn held(&self) -> usize {
let pos = self.pos.len().saturating_mul(3);
let tris = self.tris.len().saturating_mul(2);
let edges = self.edges.len().saturating_mul(2);
let stack = self.stack.len().saturating_mul(2);
pos.saturating_add(tris).saturating_add(edges).saturating_add(self.log.len()).saturating_add(stack)
}
fn closed(&self, e: (u32, u32, u32)) -> bool {
self.edges.get(&key(e.0, e.1)).copied().unwrap_or(0) >= 2
}
/// Adds one triangle; `flip` inverts its fold.
fn step(&mut self, a: &Arrays<'_>, status: i32, flip: bool) -> Result<(), &'static str> {
let ([ta, tb, tc], fold) = self.triangle(a)?;
for (x, y) in [(ta, tb), (tb, tc), (tc, ta)] {
let n = self.edges.entry(key(x, y)).or_insert(0);
*n = n.saturating_add(1);
let shared = *n;
self.log.push(key(x, y));
if shared > 2 {
return Err("an edge is shared by more than two triangles");
}
}
self.tris.push([ta, tb, tc]);
if self.held() > a.limit {
return Err("the rebuild needs more than the decoded-element allowance left");
}
let (mut left, mut right) = ((tc, tb, ta), (ta, tc, tb));
if fold != flip {
std::mem::swap(&mut left, &mut right);
}
let (taken, other) = if status & 2 != 0 { (left, right) } else { (right, left) };
if status & 3 != 0 && self.closed(taken) && !self.closed(other) {
std::mem::swap(&mut left, &mut right);
}
match (status & 2 != 0, status & 1 != 0) {
(true, true) => {
self.next = Some(left);
self.stack.push(right);
}
(true, false) => self.next = Some(left),
(false, true) => self.next = Some(right),
(false, false) => {}
}
while self.next.is_none() {
let Some(e) = self.stack.pop() else { break };
if !self.closed(e) {
self.next = Some(e);
}
}
Ok(())
}
/// Walks every component. One that fails is rewound and walked once more with the fold
/// inverted at its second triangle, so the walk is at most twice the triangle count.
fn walk(&mut self, a: &Arrays<'_>) -> Result<(), &'static str> {
let mut start = self.mark(0);
let mut flip_at = None;
let mut i = 0;
while i < a.triangles {
if self.next.is_none() && !(flip_at.is_some() && i == start.tri) {
start = self.mark(i);
flip_at = None;
}
let status = a.edge_status.get(i).copied().unwrap_or(0);
match self.step(a, status, flip_at == Some(i)) {
Ok(()) => i = i.saturating_add(1),
Err(e) => {
if flip_at.is_some() || i == start.tri {
return Err(e);
}
self.rewind(start);
flip_at = Some(start.tri.saturating_add(1));
i = start.tri;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests;
+299
View File
@@ -0,0 +1,299 @@
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
const S: f64 = std::f64::consts::SQRT_2;
fn run(points: &[i64], status: &[i32], t: usize, is_ref: &[bool], refs: &[u32]) -> Result<Rebuilt, &'static str> {
rebuild(&Arrays {
tolerance: 0.5,
origin: [10.0, 0.0, 0.0],
points,
edge_status: status,
triangles: t,
is_reference: is_ref,
references: refs,
limit: usize::MAX,
})
}
fn close(a: V, b: V) -> bool {
(0..3).all(|i| (a[i] - b[i]).abs() < 1e-9)
}
/// Seed `[0 1 2]`, then the apex across `[2 1]`: `X` runs from vertex 2 to vertex 1, and
/// each of `d`'s three components lands on its own axis with its own sign. Tolerance 0.5
/// halves every step.
#[test]
fn a_seed_and_one_apex_rebuild() {
let pts = [0, 0, 0, 4, 0, 0, -2, 4, 0, 2, -6, 2];
let m = run(&pts, &[2, 0, 0, 0, 0, 0], 2, &[false; 4], &[]).unwrap();
assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3]]);
let p: Vec<V> = m.positions.iter().map(|v| [v[0] - 10.0, v[1], v[2]]).collect();
assert!(close(p[0], [0.0, 0.0, 0.0]) && close(p[1], [2.0, 0.0, 0.0]));
assert!(close(p[2], [0.0, 2.0, 0.0]), "{:?}", p[2]);
assert!(close(p[3], [1.0 + 2.0 * S, 1.0 + S, -1.0]), "{:?}", p[3]);
}
/// The walk stops once its arrays would hold more than `limit` elements: the seed-and-apex
/// pair ends holding four vertices (12), two triangles (4), five edges (10) and six logged
/// edges (6), 32 in all.
#[test]
fn a_walk_past_its_limit_stops() {
let pts = [0, 0, 0, 4, 0, 0, -2, 4, 0, 2, -6, 2];
let with = |limit| {
rebuild(&Arrays {
tolerance: 0.5,
origin: [0.0; 3],
points: &pts,
edge_status: &[2, 0, 0, 0, 0, 0],
triangles: 2,
is_reference: &[false; 4],
references: &[],
limit,
})
};
assert_eq!(with(32).unwrap().triangles.len(), 2);
assert_eq!(with(31).unwrap_err(), "the rebuild needs more than the decoded-element allowance left");
}
/// The triangles reserved up front fit the limit too.
#[test]
fn the_walk_reserves_within_its_limit() {
assert!(Walk::new(1 << 20, 10).tris.capacity() < 1 << 20);
assert!(Walk::new(1 << 20, 10).tris.capacity() <= 5);
assert!(Walk::new(4, usize::MAX).tris.capacity() >= 4);
}
/// Bit 0 alone continues across `[A C]`, keeping `W = B`.
#[test]
fn the_right_edge_is_entered_from_a_to_c() {
let pts = [0, 0, 0, 4, 0, 0, -2, 4, 0, 0, -2, 0];
let m = run(&pts, &[1, 0, 0, 0, 0, 0], 2, &[false; 4], &[]).unwrap();
assert_eq!(m.triangles[1], [0, 2, 3]);
}
/// A reference slot reuses a vertex; a closed pushed edge is skipped and the next component
/// starts from a fresh seed.
#[test]
fn references_and_components() {
let pts = [0; 9 + 9];
let is_ref = [false, false, false, true, false, false, false];
let m = run(&pts, &[3, 0, 0, 0, 0, 0, 0, 0, 0], 3, &is_ref, &[0]).unwrap();
assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 0], [3, 4, 5]]);
}
/// A double-sided panel: four coplanar vertices, four triangles. The second triangle's apex
/// folds back onto the seed's side, so its own continuation takes `[A C]` first; the last two
/// apexes are the seed's first two vertices.
#[test]
fn a_folded_apex_swaps_its_continuation() {
let is_ref = [false, false, false, false, true, true];
let status = [3, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let m = run(&[0, 0, 0, 4, 0, 0, -2, 4, 0, 1, 3, 0], &status, 4, &is_ref, &[0, 1]).unwrap();
assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3], [2, 3, 0], [0, 3, 1]]);
// The same panel unfolded is rescued by the component retry.
let m = run(&[0, 0, 0, 4, 0, 0, -2, 4, 0, 1, -3, 0], &status, 4, &is_ref, &[0, 1]).unwrap();
assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3], [2, 3, 0], [0, 3, 1]]);
}
/// The fourth apex `(1, 3, 0)` lies in its parent's plane on the parent's side, so its
/// triangle folds and continues across `[A C]` to the reference.
#[test]
fn a_fold_past_the_retry_point_is_detected() {
let pts = [0, 0, 0, 4, 0, 0, -2, 4, 0, 0, -3, 1, 1, 3, 0];
let is_ref = [false, false, false, false, false, true];
let m = run(&pts, &[3, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], 4, &is_ref, &[0]).unwrap();
assert_eq!(m.triangles[3], [3, 4, 0]);
}
/// A panel whose fold apex is a reference carries no fold signal: the unfolded walk closes an
/// edge it still needs, so the component is rewound and walked again folded at its second
/// triangle.
#[test]
fn a_reference_fold_is_found_by_retrying_the_component() {
let pts: Vec<i64> = (1..=30).collect();
let mut is_ref = vec![false; 9];
is_ref.extend([true, true, false, true, true, true]);
let mut status = vec![0; 21];
status[3..7].copy_from_slice(&[3, 3, 2, 0]);
let m = run(&pts, &status, 7, &is_ref, &[0, 3, 6, 0, 3]).unwrap();
assert_eq!(m.triangles[3..], [[0, 3, 9], [9, 3, 6], [9, 6, 0], [0, 6, 3]]);
}
/// A tetrahedron closed by two references: after `[3 1 0]` the continuation edge `[0 1]` is
/// already shared twice, so the walk takes the open `[3 0]` instead.
#[test]
fn a_closed_continuation_edge_gives_way_to_the_open_one() {
let pts = [0, 0, 0, 4, 0, 0, -2, 4, 0, 0, 0, 4];
let mut status = [0; 12];
status[..3].copy_from_slice(&[2, 2, 2]);
let mut is_ref = [false; 6];
is_ref[4..].copy_from_slice(&[true, true]);
let m = run(&pts, &status, 4, &is_ref, &[0, 2]).unwrap();
assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3], [3, 1, 0], [3, 0, 2]]);
}
#[test]
fn arrays_that_do_not_fit_are_refused() {
let pts = [0; 9];
let ok = [false; 3];
assert!(run(&pts, &[0, 0, 0], 1, &ok, &[]).is_ok());
assert!(run(&pts, &[0], 1, &ok, &[]).is_err(), "T form");
assert!(run(&[0; 12], &[0, 0, 0], 1, &ok, &[]).is_err(), "points left");
assert!(run(&[0; 6], &[0, 0, 0], 1, &ok, &[]).is_err(), "points short");
assert!(run(&pts, &[0, 0, 0], 1, &[false; 4], &[]).is_err(), "slots left");
assert!(run(&[0; 6], &[0, 0, 0], 1, &[false, false, true], &[7]).is_err(), "forward reference");
assert!(run(&[0; 9], &[0, 0, 0], 1, &[false, false, false, true], &[0]).is_err(), "references left");
// A third triangle on the seed's edge [0 1], each apex a reference.
let three = [false, false, false, true, true];
let shared = run(&[0; 9], &[2, 2, 0, 0, 0, 0, 0, 0, 0], 3, &three, &[0, 0]).map(|_| ());
assert_eq!(shared, Err("an edge is shared by more than two triangles"));
}
/// `PrcPt::Length` rounds the squared length to `f32` and takes Newton's root from a halved
/// exponent, which can settle an ulp off the correctly rounded root; `Unitize` refuses
/// lengths below `FLT_EPSILON` [WD 12.3].
#[test]
fn length_and_unitize_follow_the_wd_pseudocode() {
assert_eq!(wd_length([3.0, 4.0, 0.0]), Some(5.0));
let newton = f64::from_bits(S.to_bits() - 1);
assert_eq!(wd_length([1.0, 1.0, 0.0]), Some(newton));
let long = 1.0 + 2f64.powi(-30);
assert_eq!(wd_length([long, 0.0, 0.0]), Some(1.0));
assert_eq!(unitize([0.0, long, 0.0]), Some([0.0, long, 0.0]));
assert_eq!(unitize([1e-8, 0.0, 0.0]), None);
assert_eq!(unitize([0.0; 3]), None);
}
/// A `W` on the edge's line leaves `Z` null, so the frame comes from `MakeOrthoRep(X)`: `Z`
/// from `X x (0, 1, 0)`, or from `X x (1, 0, 0)` when `X` is along `Y` [WD 7.8.9.2, 12.3].
#[test]
fn a_null_z_takes_the_ortho_rep_of_x() {
let d = [1.0, 2.0, 3.0];
assert_eq!(apex([0.0; 3], [-2.0, 0.0, 0.0], [5.0, 0.0, 0.0], d), [0.0, -2.0, -3.0]);
assert_eq!(apex([0.0; 3], [0.0, -2.0, 0.0], [0.0, 5.0, 0.0], d), [-2.0, 0.0, 3.0]);
}
/// A degenerate frame is turned a half-turn about `X` when the sub-epsilon residue of
/// `(W - O) x X` points against `MakeOrthoRep`'s `Z`, and kept when it points along it.
#[test]
fn a_degenerate_frame_follows_its_residue() {
let (lo, hi, d) = ([0.0; 3], [-2.0, 0.0, 0.0], [1.0, 2.0, 3.0]);
assert_eq!(apex(lo, hi, [5.0, 1e-9, 0.0], d), [0.0, 2.0, 3.0]);
assert_eq!(apex(lo, hi, [5.0, -1e-9, 0.0], d), [0.0, -2.0, -3.0]);
}
/// A degenerate frame's `Y` comes from the frame's own `X`, whose length `unitize` left off by
/// a few 1e-8, not from the copy `MakeOrthoRep` unitized again: the two differ by that much,
/// and the walk would carry the difference into later apexes.
#[test]
fn a_degenerate_frame_takes_y_from_its_own_x() {
let (lo, hi) = ([0.1, 0.2, 0.3], [0.7, -0.4, 0.9]);
let o = scale(add(lo, hi), 0.5);
let x = unitize(sub(lo, hi)).unwrap();
let w = add(o, scale(x, 0.37));
let d = [1.0, 2.0, 3.0];
let z = make_ortho_rep_z(x).unwrap();
let p = apex(lo, hi, w, d);
let own = unitize(cross(z, x)).unwrap();
let again = unitize(cross(z, unitize(x).unwrap())).unwrap();
assert_ne!(own, again, "the probe must tell the two Ys apart");
let sign = if dot(cross(sub(w, o), x), z) < 0.0 { -1.0 } else { 1.0 };
let expect = |y: V| sub(sub(add(o, scale(x, d[0])), scale(y, sign * d[1])), scale(z, sign * d[2]));
assert_eq!(p, expect(own));
assert_ne!(p, expect(again));
}
/// A strip of thin, non-planar triangles compressed by an encoder written out from the WD:
/// each point is expressed in the frame of the working mesh, rounded to the tolerance and
/// reinjected [WD 7.8.9, 7.8.9.2], with `PrcPt::Length` and `Unitize` transcribed from the
/// pseudocode [WD 12.3] as read. The decoder must land exactly on that working mesh, which
/// keeps every vertex within the rounding bound of its source.
#[test]
fn a_strip_decodes_onto_the_encoders_working_mesh() {
fn length(v: V) -> f64 {
let f = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) as f32;
let mut bits = f.to_bits();
let exponent = (bits >> 23) & 0xff;
if exponent > 127 {
bits = (bits & 0x807f_ffff) | (((exponent - 127) / 2 + 127) << 23);
}
let (squared, mut x0) = (f64::from(f), f64::from(f32::from_bits(bits)));
for _ in 0..100 {
let xi = 0.5 * (x0 + squared / x0);
if xi == x0 {
return xi;
}
x0 = xi;
}
-1.0
}
fn unitized(v: V) -> V {
let l = length(v);
assert!(l >= f64::from(f32::EPSILON), "null axis");
[v[0] / l, v[1] / l, v[2] / l]
}
let (tol, n) = (1e-4, 48);
let mut state = 0x9e37_79b9_7f4a_7c15_u64;
let mut jitter = |scale: f64| {
state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
((state >> 11) as f64 / (1u64 << 53) as f64 - 0.5) * scale
};
// Two rails 4 apart, 0.05 between a rail's points, 0.02 of relief.
let source: Vec<V> = (0..n)
.map(|i| {
let (along, rail) = (i as f64 * 0.025, (i % 2) as f64 * 4.0);
[along + jitter(0.01), rail + jitter(0.01), jitter(0.02)]
})
.collect();
let round = |v: f64| (v / tol).round() as i64;
let (mut points, mut work) = (Vec::new(), Vec::<V>::new());
for (i, target) in source.iter().take(3).enumerate() {
let base = match i {
0 => [0.0; 3],
1 => work[0],
_ => scale(add(work[0], work[1]), 0.5),
};
let d = [0, 1, 2].map(|c| round(target[c] - base[c]));
points.extend(d);
work.push(add(base, d.map(|c| c as f64 * tol)));
}
// Triangle k enters across vertices k and k + 1, opposite k - 1, and continues right on
// odd k, left on even k, unless it folds.
let mut status = vec![0; 3 * (n - 2)];
status[0] = 2;
for k in 1..n - 2 {
let (lo, hi, w) = (work[k], work[k + 1], work[k - 1]);
let o = scale(add(hi, lo), 0.5);
let x = unitized(sub(lo, hi));
let z = unitized(cross(sub(w, o), x));
let y = unitized(cross(z, x));
let r = sub(source[k + 2], o);
let d = [round(dot(r, x)), round(-dot(r, y)), round(-dot(r, z))];
points.extend(d);
let [dx, dy, dz] = d.map(|c| c as f64 * tol);
work.push(sub(sub(add(o, scale(x, dx)), scale(y, dy)), scale(z, dz)));
if k < n - 3 {
let fold = d[2] == 0 && d[1] > 0;
status[k] = if (k % 2 == 1) != fold { 1 } else { 2 };
}
}
let m = rebuild(&Arrays {
tolerance: tol,
origin: [0.0; 3],
points: &points,
edge_status: &status,
triangles: n - 2,
is_reference: &vec![false; n],
references: &[],
limit: usize::MAX,
})
.unwrap();
assert_eq!(m.triangles[1..4], [[2, 1, 3], [2, 3, 4], [4, 3, 5]]);
assert_eq!(m.positions, work);
for (p, s) in m.positions.iter().zip(&source) {
let off = dot(sub(*p, *s), sub(*p, *s)).sqrt() / tol;
assert!(off < 0.87, "{off} tolerances off");
}
}
+270 -1
View File
@@ -529,12 +529,281 @@ fn a_compressed_mesh_is_read_to_its_end() {
let schema = Schema::default();
let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, v);
let t = ctx.file_structure_tessellation().unwrap();
assert!(matches!(t[..], [Tessellation::Compressed { triangles: 1, .. }, Tessellation::Mesh(_)]), "full={full} v={v}: {t:?}");
assert_eq!(ctx.r.position(), w.len(), "full={full} v={v}");
assert!(matches!(t[1], Tessellation::Mesh(_)), "full={full} v={v}: {t:?}");
// Stored normals and the one-status-per-triangle form are not rebuilt; the
// recalculated 3T seed is.
if full || !three_t {
assert!(matches!(t[0], Tessellation::Compressed { triangles: 1, .. }), "full={full} v={v}: {t:?}");
} else {
let m = mesh(&t[0]);
assert_eq!((m.triangles.as_slice(), m.faces.as_slice()), ([[0, 1, 2]].as_slice(), std::slice::from_ref(&(0..1))), "v={v}");
assert!(m.normals_recalculated && m.normals.is_empty());
assert_eq!(m.triangle_styles, [TriangleStyle { style: 1, behaviour: 0 }]);
// Each point component is 1, in units of the 0.001 tolerance, from the origin 0.
let want = [0.001, 0.002, 0.0025];
assert!(m.positions.iter().zip(want).all(|(p, c)| p.iter().all(|x| (x - c).abs() < 1e-12)), "{:?}", m.positions);
}
}
}
}
/// A `TESS_3D_Compressed` (authoring version 8137, origin 0) whose normals are recalculated,
/// with `points` in units of `tolerance`, the 3T `status`, each triangle's face, the slots'
/// reference flags, the references and one line attribute per face; every other optional
/// branch absent.
struct Packed<'a> {
tolerance: f64,
points: &'a [i64],
status: &'a [u8],
face_of: &'a [u32],
/// The width of each face index; 8 unless an index needs more.
face_bits: u32,
is_ref: &'a [bool],
refs: &'a [u32],
line_attributes: &'a [u16],
}
/// A `CompressedIndiceArray` [WD 11.9] of `bits`-wide deltas, `implicit` with no compressed
/// flag.
fn indices(w: &mut W, v: &[u32], implicit: bool, bits: u32) {
if !implicit {
w.bit(false);
}
w.uint(u32::try_from(v.len()).unwrap());
for i in 0..v.len() {
w.put(if i == 0 { u64::from(bits) } else { 0 }, 8);
}
let mut prev = 0i64;
for &x in v {
let d = i64::from(x) - prev;
w.bit(d < 0).put(d.unsigned_abs(), bits - 1);
prev = i64::from(x);
}
}
fn packed(w: &mut W, c: &Packed<'_>) {
w.uint(173).bit(false).bit(true).double(c.tolerance);
w.put(0, 32).put(0, 32).put(0, 32);
// point_array: 8-bit sign-magnitude integers.
w.bit(false).uint(u32::try_from(c.points.len()).unwrap());
for _ in c.points {
w.put(8, 8);
}
for &p in c.points {
w.bit(p < 0).put(p.unsigned_abs(), 7);
}
w.bit(false).uint(u32::try_from(c.status.len()).unwrap());
for &e in c.status {
w.put(u64::from(e), 8);
}
indices(w, c.face_of, false, c.face_bits);
w.uint(u32::try_from(c.is_ref.len()).unwrap());
for &b in c.is_ref {
w.bit(b);
}
assert!(c.refs.len() <= 3, "more than three references are Huffman-coded");
indices(w, c.refs, true, 8);
// must_recalculate_normals, normal_is_reversed, crease angle, flags; no colours, no
// multiple line attributes.
w.bit(true);
for _ in c.face_of {
w.bit(false);
}
w.double(0.5).put(0, 8).bit(false).bit(false);
w.bit(false).uint(u32::try_from(c.line_attributes.len()).unwrap());
for &a in c.line_attributes {
w.put(u64::from(a & 0xff), 8).put(u64::from(a >> 8), 8);
}
// No texture, no behaviours.
w.bit(true).bit(false);
}
/// A double-sided panel of two faces packed in walk order, its fold apex and its last two
/// apexes references: rebuilt into a mesh grouped by face, each triangle styled by its face.
#[test]
fn a_recalculated_compressed_mesh_is_rebuilt_by_face() {
let mut b = W::default();
packed(
&mut b,
&Packed {
tolerance: 0.5,
points: &[0, 0, 0, 4, 0, 0, -2, 4, 0, 1, 3, 0],
status: &[3, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
face_of: &[1, 0, 1, 0],
face_bits: 8,
is_ref: &[false, false, false, false, true, true],
refs: &[0, 1],
line_attributes: &[3, 0],
},
);
let w = section(1, &b);
let bytes = w.bytes();
let schema = Schema::default();
let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, 8137);
let t = ctx.file_structure_tessellation().unwrap();
assert_eq!(ctx.r.position(), w.len());
let m = mesh(&t[0]);
// Walk order [0 1 2] [2 1 3] [2 3 0] [0 3 1]; faces 1, 0, 1, 0.
assert_eq!(m.triangles, [[2, 1, 3], [0, 3, 1], [0, 1, 2], [2, 3, 0]]);
assert_eq!(m.faces, [0..2, 2..4]);
assert_eq!(m.positions[..3], [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]]);
let style = |style| TriangleStyle { style, behaviour: 0 };
assert_eq!(m.triangle_styles, [style(3), style(3), style(0), style(0)]);
assert!(m.normals_recalculated);
}
/// Arrays the walk cannot fit stay a [`Tessellation::Compressed`], read to their end.
#[test]
fn a_compressed_mesh_that_does_not_fit_is_left_compressed() {
let mut b = W::default();
// One point too many for a lone seed.
let c = Packed {
tolerance: 0.5,
points: &[0; 12],
status: &[0, 0, 0],
face_of: &[0],
face_bits: 8,
is_ref: &[false; 3],
refs: &[],
line_attributes: &[0],
};
packed(&mut b, &c);
let w = section(1, &b);
let t = decode(&w, &Schema::default(), 8137).unwrap();
assert!(matches!(t[..], [Tessellation::Compressed { triangles: 1, .. }]), "{t:?}");
}
/// A face index near `u32::MAX` claims four billion faces: once the triangles rebuild, they are
/// charged before any face range is made, so the decode stops at the element allowance instead
/// of allocating them.
#[test]
fn a_huge_face_index_is_charged_not_allocated() {
let mut b = W::default();
let c = Packed {
tolerance: 0.5,
points: &[0; 9],
status: &[0, 0, 0],
face_of: &[u32::MAX - 1],
face_bits: 33,
is_ref: &[false; 3],
refs: &[],
line_attributes: &[],
};
packed(&mut b, &c);
let r = decode(&section(1, &b), &Schema::default(), 8137);
assert!(matches!(r, Err(PrcError::TooManyElements { .. })), "{r:?}");
}
/// The same huge face index on arrays the walk cannot fit charges nothing for triangles or
/// faces: the tessellation stays compressed and the file reads on.
#[test]
fn arrays_that_do_not_fit_spend_no_triangles_or_faces() {
let mut b = W::default();
let c = Packed {
tolerance: 0.5,
points: &[0; 12],
status: &[0, 0, 0],
face_of: &[u32::MAX - 1],
face_bits: 33,
is_ref: &[false; 3],
refs: &[],
line_attributes: &[],
};
packed(&mut b, &c);
let t = decode(&section(1, &b), &Schema::default(), 8137).unwrap();
assert!(matches!(t[..], [Tessellation::Compressed { triangles: 1, .. }]), "{t:?}");
}
/// The two-face panel of [`a_recalculated_compressed_mesh_is_rebuilt_by_face`], its second
/// reference `last_ref`.
fn panel(last_ref: u32) -> W {
let mut b = W::default();
packed(
&mut b,
&Packed {
tolerance: 0.5,
points: &[0, 0, 0, 4, 0, 0, -2, 4, 0, 1, 3, 0],
status: &[3, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
face_of: &[1, 0, 1, 0],
face_bits: 8,
is_ref: &[false, false, false, false, true, true],
refs: &[0, last_ref],
line_attributes: &[3, 0],
},
);
section(1, &b)
}
/// A rebuilt mesh is charged its triangles, each with its style, and each face range at its
/// size; the same arrays left compressed (a reference to a vertex not yet made) are charged
/// neither.
#[test]
fn a_rebuilt_compressed_mesh_is_charged_its_triangles_and_faces() {
let fits = panel(1);
let t = decode(&fits, &Schema::default(), 8137).unwrap();
assert_eq!(mesh(&t[0]).triangles.len(), 4);
let left = panel(9);
let t = decode(&left, &Schema::default(), 8137).unwrap();
assert!(matches!(t[..], [Tessellation::Compressed { triangles: 4, .. }]), "{t:?}");
assert_eq!(spent(&fits) - spent(&left), 4 * triangle_elements(false, 0, true) + 2 * RANGE_ELEMENTS);
assert_eq!(triangle_elements(false, 0, true), 3);
#[cfg(target_pointer_width = "64")]
assert_eq!(RANGE_ELEMENTS, 2);
}
/// A rebuild whose walk would outgrow the allowance left keeps the arrays compressed, even
/// when the rebuilt mesh alone would have fit, and the stream reads on.
#[test]
fn a_compressed_rebuild_is_bounded_by_the_allowance_left() {
let fits = panel(1);
let bytes = fits.bytes();
let schema = Schema::default();
let read = |n| Ctx::new(BitReader::with_elements(&bytes, n), &schema, 8137).file_structure_tessellation();
let arrays = spent(&panel(9));
let mesh_alone = spent(&fits) - arrays;
let t = read(arrays + mesh_alone).unwrap();
assert!(matches!(t[..], [Tessellation::Compressed { triangles: 4, .. }]), "{t:?}");
assert!(matches!(read(crate::MAX_DECODED_ELEMENTS).unwrap()[..], [Tessellation::Mesh(_)]));
}
/// A face's nonzero behaviour styles its triangles even when its line attribute is 0 (the
/// owner's graphics); all zero, the faces give no styles.
#[test]
fn a_compressed_face_behaviour_alone_styles_its_triangles() {
let attributes = |behaviours: Vec<i32>| FaceAttributes { multi: Vec::new(), line_attributes: vec![0], behaviours };
let a = attributes(vec![0x30]);
assert!(a.styled(1));
assert_eq!(a.styles(&[0, 0], 1), [TriangleStyle { style: 0, behaviour: 0x30 }; 2]);
assert!(!attributes(vec![0]).styled(1));
assert!(!attributes(Vec::new()).styled(1));
}
fn rebuilt(triangles: usize) -> compressed::Rebuilt {
compressed::Rebuilt { positions: vec![[0.0; 3]; 3], triangles: (0..triangles).map(|i| [0, 1, u32::try_from(i).unwrap()]).collect() }
}
/// Triangles are grouped by face in walk order within each, faces no triangle names (between
/// and after) get empty ranges, and each style moves with its triangle.
#[test]
fn rebuilt_triangles_are_grouped_by_face_with_their_styles() {
let face_of = [2, 0, 2, 1, 0];
let m = by_face(rebuilt(5), &face_of, 5, &[]);
let apex: Vec<u32> = m.triangles.iter().map(|t| t[2]).collect();
assert_eq!(apex, [1, 4, 3, 0, 2]);
assert_eq!(m.faces, [0..2, 2..3, 3..5, 5..5, 5..5]);
assert!(m.triangle_styles.is_empty());
assert!(m.normals_recalculated);
let style = |style| TriangleStyle { style, behaviour: u16::try_from(style).unwrap() };
let styles: Vec<TriangleStyle> = face_of.iter().map(|&f| style(f + 1)).collect();
let m = by_face(rebuilt(5), &[2, 0, 2, 3, 0], 4, &styles);
let apex: Vec<u32> = m.triangles.iter().map(|t| t[2]).collect();
assert_eq!(apex, [1, 4, 0, 2, 3]);
assert_eq!(m.faces, [0..2, 2..2, 2..4, 4..5]);
assert_eq!(m.triangle_styles, [style(1), style(1), style(3), style(3), style(2)]);
}
#[test]
fn refusals_and_damage_are_errors() {
// Edge statuses neither T nor 3T long.