mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
craft-prc: rebuild compressed tessellations whose normals are recalculated (#778)
This commit is contained in:
1 parent
427fa15899
commit
f72b69699f
5 files changed
+1063
-42
No files matched your search
@@ -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
@@ -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]>) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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(§ion(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(§ion(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.
|
||||
|
||||
Reference in new issue
Block a user