use super::ShadingKind;
use crate::color::{ColorSpace, Rgb};
use crate::function::{BitReader, Function};
use kurbo::Point;
use std::sync::Arc;
pub const MAX_COMPONENTS: usize = 8;
const VALID_COORD_BITS: [u32; 8] = [1, 2, 4, 8, 12, 16, 24, 32];
const VALID_COMPONENT_BITS: [u32; 6] = [1, 2, 4, 8, 12, 16];
const VALID_FLAG_BITS: [u32; 3] = [2, 4, 8];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vertex {
pub point: Point,
pub color: Rgb,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Triangle {
pub vertices: [Vertex; 3],
}
#[derive(Debug, Clone, PartialEq)]
pub struct Patch {
pub points: Box<[Point]>,
pub colors: [Rgb; 4],
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Mesh {
pub triangles: Vec<Triangle>,
pub patches: Vec<Patch>,
pub component_range: [f32; 2],
}
impl Mesh {
#[must_use]
pub fn bounds(&self) -> Option<kurbo::Rect> {
let mut rect: Option<kurbo::Rect> = None;
let mut add = |p: Point| {
let r = kurbo::Rect::from_points(p, p);
rect = Some(match rect {
Some(existing) => existing.union(r),
None => r,
});
};
for t in &self.triangles {
for v in t.vertices {
add(v.point);
}
}
for p in &self.patches {
for point in &p.points {
add(*point);
}
}
rect
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MeshParams {
pub coord_bits: u32,
pub component_bits: u32,
pub flag_bits: u32,
pub components: usize,
pub decode: Box<[f32]>,
pub coord_max: u32,
pub component_max: u32,
}
impl MeshParams {
#[must_use]
pub fn new(
coord_bits: u32,
component_bits: u32,
flag_bits: u32,
components: usize,
decode: &[f32],
kind: ShadingKind,
) -> Option<Self> {
if !VALID_COORD_BITS.contains(&coord_bits)
|| !VALID_COMPONENT_BITS.contains(&component_bits)
|| (kind.reads_edge_flags() && !VALID_FLAG_BITS.contains(&flag_bits))
|| components > MAX_COMPONENTS
{
return None;
}
if decode.len() != 4 + 2 * components {
return None;
}
Some(Self {
coord_bits,
component_bits,
flag_bits,
components,
decode: decode.into(),
coord_max: if coord_bits >= 32 {
u32::MAX
} else {
(1u32 << coord_bits) - 1
},
component_max: if component_bits >= 32 {
u32::MAX
} else {
(1u32 << component_bits) - 1
},
})
}
#[must_use]
pub fn component_range(&self) -> [f32; 2] {
[
self.decode.get(4).copied().unwrap_or(0.0),
self.decode.get(5).copied().unwrap_or(0.0),
]
}
}
pub struct MeshReader<'a> {
bits: BitReader<'a>,
params: &'a MeshParams,
space: &'a ColorSpace,
functions: &'a [Arc<Function>],
}
impl<'a> MeshReader<'a> {
#[must_use]
pub fn new(
data: &'a [u8],
params: &'a MeshParams,
space: &'a ColorSpace,
functions: &'a [Arc<Function>],
) -> Self {
Self {
bits: BitReader::new(data),
params,
space,
functions,
}
}
#[must_use]
pub fn can_read_flag(&self) -> bool {
self.bits.remaining() >= u64::from(self.params.flag_bits)
}
#[must_use]
pub fn can_read_coords(&self) -> bool {
self.bits.remaining() / 2 >= u64::from(self.params.coord_bits)
}
#[must_use]
pub fn can_read_color(&self) -> bool {
if self.params.component_bits == 0 {
return false;
}
self.bits.remaining() / u64::from(self.params.component_bits)
>= self.params.components as u64
}
pub fn read_flag(&mut self) -> u8 {
u8::try_from(self.bits.read(self.params.flag_bits) & 0x03).unwrap_or(0)
}
pub fn read_coords(&mut self) -> Point {
let decode = |raw: u32, min: f32, max: f32, max_raw: u32| -> f64 {
if self.params.coord_bits == 32 {
f64::from(min)
+ f64::from(raw) * (f64::from(max) - f64::from(min)) / f64::from(max_raw)
} else {
#[expect(
clippy::cast_precision_loss,
reason = "below 32 bits the raw value is exact in f32, matching the C++"
)]
let v = min + (raw as f32) * (max - min) / (max_raw as f32);
f64::from(v)
}
};
let at = |i: usize| self.params.decode.get(i).copied().unwrap_or(0.0);
let raw_x = self.bits.read(self.params.coord_bits);
let raw_y = self.bits.read(self.params.coord_bits);
Point::new(
decode(raw_x, at(0), at(1), self.params.coord_max),
decode(raw_y, at(2), at(3), self.params.coord_max),
)
}
pub fn read_color(&mut self) -> Rgb {
let mut comps = [0.0f32; MAX_COMPONENTS];
for i in 0..self.params.components.min(MAX_COMPONENTS) {
let raw = self.bits.read(self.params.component_bits);
let min = self.params.decode.get(4 + i * 2).copied().unwrap_or(0.0);
let max = self
.params
.decode
.get(4 + i * 2 + 1)
.copied()
.unwrap_or(0.0);
#[expect(
clippy::cast_precision_loss,
reason = "component widths cap at 16 bits, exact in f32"
)]
let v = min + (raw as f32) * (max - min) / (self.params.component_max as f32);
if let Some(slot) = comps.get_mut(i) {
*slot = v;
}
}
if self.functions.is_empty() {
return self
.space
.try_to_rgb(comps.get(..self.params.components).unwrap_or(&[]))
.unwrap_or(Rgb::BLACK);
}
Rgb {
r: comps.first().copied().unwrap_or(0.0),
g: 0.0,
b: 0.0,
}
}
pub fn read_vertex(&mut self) -> Option<(u8, Vertex)> {
if !self.can_read_flag() {
return None;
}
let flag = self.read_flag();
if !self.can_read_coords() {
return None;
}
let point = self.read_coords();
if !self.can_read_color() {
return None;
}
let color = self.read_color();
self.bits.byte_align();
Some((flag, Vertex { point, color }))
}
pub fn read_vertex_row(&mut self, count: usize) -> Vec<Vertex> {
let mut row = Vec::with_capacity(count);
for _ in 0..count {
if !self.can_read_coords() {
return Vec::new();
}
let point = self.read_coords();
if !self.can_read_color() {
return Vec::new();
}
let color = self.read_color();
self.bits.byte_align();
row.push(Vertex { point, color });
}
row
}
#[must_use]
pub fn read_free_form(&mut self) -> Vec<Triangle> {
let mut out = Vec::new();
let mut previous: [Option<Vertex>; 3] = [None; 3];
loop {
let Some((flag, vertex)) = self.read_vertex() else {
return out;
};
if flag == 0 {
let (Some((_, b)), Some((_, c))) = (self.read_vertex(), self.read_vertex()) else {
return out;
};
previous = [Some(vertex), Some(b), Some(c)];
} else {
let (Some(p0), Some(p1), Some(p2)) = (previous[0], previous[1], previous[2]) else {
return out;
};
previous = if flag == 1 {
[Some(p1), Some(p2), Some(vertex)]
} else {
[Some(p0), Some(p2), Some(vertex)]
};
}
let (Some(a), Some(b), Some(c)) = (previous[0], previous[1], previous[2]) else {
return out;
};
out.push(Triangle {
vertices: [a, b, c],
});
}
}
#[must_use]
pub fn read_lattice(&mut self, per_row: usize) -> Vec<Triangle> {
if per_row < 2 {
return Vec::new();
}
let mut out = Vec::new();
let mut previous = self.read_vertex_row(per_row);
if previous.is_empty() {
return out;
}
loop {
let row = self.read_vertex_row(per_row);
if row.is_empty() {
return out;
}
for i in 0..per_row - 1 {
let (Some(a), Some(b), Some(c), Some(d)) = (
previous.get(i),
previous.get(i + 1),
row.get(i),
row.get(i + 1),
) else {
continue;
};
out.push(Triangle {
vertices: [*a, *b, *c],
});
out.push(Triangle {
vertices: [*b, *d, *c],
});
}
previous = row;
}
}
#[must_use]
pub fn read_patches(&mut self, kind: ShadingKind) -> Vec<Patch> {
let point_count = if kind == ShadingKind::TensorMesh {
16
} else {
12
};
let mut out: Vec<Patch> = Vec::new();
let mut coords = vec![Point::ZERO; point_count];
let mut colors = [Rgb::BLACK; 4];
loop {
if !self.can_read_flag() {
return out;
}
let flag = self.read_flag();
let (start_point, start_color) = if flag == 0 { (0, 0) } else { (4, 2) };
if flag != 0 {
let Some(previous) = out.last() else {
return out;
};
for i in 0..4 {
let source = (usize::from(flag) * 3 + i) % 12;
if let (Some(slot), Some(p)) = (coords.get_mut(i), previous.points.get(source))
{
*slot = *p;
}
}
if let (Some(slot), Some(c)) =
(colors.first_mut(), previous.colors.get(usize::from(flag)))
{
*slot = *c;
}
let next = previous
.colors
.get((usize::from(flag) + 1) % 4)
.copied()
.unwrap_or(Rgb::BLACK);
if let Some(slot) = colors.get_mut(1) {
*slot = next;
}
}
for i in start_point..point_count {
if !self.can_read_coords() {
break;
}
let p = self.read_coords();
if let Some(slot) = coords.get_mut(i) {
*slot = p;
}
}
for i in start_color..4 {
if !self.can_read_color() {
break;
}
let c = self.read_color();
if let Some(slot) = colors.get_mut(i) {
*slot = c;
}
}
out.push(Patch {
points: coords.clone().into(),
colors,
});
}
}
}
#[must_use]
pub fn coons_interior(boundary: &[Point]) -> [Point; 4] {
let p = |i: usize| boundary.get(i).copied().unwrap_or(Point::ZERO);
let (p00, p01, p02, p03) = (p(0), p(1), p(2), p(3));
let (p13, p23) = (p(4), p(5));
let (p33, p32, p31, p30) = (p(6), p(7), p(8), p(9));
let (p20, p10) = (p(10), p(11));
let blend = |a: Point, b: Point, c: Point, d: Point, e: Point, f: Point, g: Point, h: Point| {
Point::new(
(-4.0 * a.x + 6.0 * (b.x + c.x) - 2.0 * (d.x + e.x) + 3.0 * (f.x + g.x) - h.x) / 9.0,
(-4.0 * a.y + 6.0 * (b.y + c.y) - 2.0 * (d.y + e.y) + 3.0 * (f.y + g.y) - h.y) / 9.0,
)
};
[
blend(p00, p01, p10, p03, p30, p31, p13, p33),
blend(p03, p02, p13, p00, p33, p32, p10, p30),
blend(p30, p31, p20, p33, p00, p01, p23, p03),
blend(p33, p32, p23, p30, p03, p02, p20, p00),
]
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{MAX_COMPONENTS, MeshParams, MeshReader, ShadingKind};
use crate::color::ColorSpace;
fn params(components: usize, decode: &[f32]) -> Option<MeshParams> {
MeshParams::new(8, 8, 8, components, decode, ShadingKind::FreeFormMesh)
}
#[test]
fn bit_widths_are_validated_per_field() {
assert!(MeshParams::new(24, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_some());
assert!(MeshParams::new(32, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_some());
assert!(MeshParams::new(8, 24, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
assert!(MeshParams::new(8, 32, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
assert!(MeshParams::new(3, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
assert!(MeshParams::new(8, 3, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
assert!(MeshParams::new(8, 8, 3, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
assert!(MeshParams::new(8, 8, 2, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_some());
assert!(MeshParams::new(8, 8, 3, 1, &[0.0; 6], ShadingKind::LatticeMesh).is_some());
}
#[test]
fn the_decode_length_must_be_exact() {
assert!(params(1, &[0.0; 6]).is_some());
assert!(params(1, &[0.0; 5]).is_none());
assert!(params(1, &[0.0; 7]).is_none());
assert!(params(3, &[0.0; 10]).is_some());
assert!(params(3, &[0.0; 8]).is_none());
}
#[test]
fn more_than_eight_components_is_refused() {
assert!(params(MAX_COMPONENTS, &[0.0; 20]).is_some());
assert!(params(MAX_COMPONENTS + 1, &[0.0; 22]).is_none());
}
#[test]
fn flags_are_masked_to_two_bits() {
let p = MeshParams::new(8, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).expect("params");
let data = [0xFFu8; 8];
let space = ColorSpace::DeviceGray;
let mut reader = MeshReader::new(&data, &p, &space, &[]);
assert_eq!(reader.read_flag(), 3);
}
#[test]
fn a_lattice_row_shorter_than_two_yields_nothing() {
let p = MeshParams::new(
8,
8,
8,
1,
&[0.0f32, 1.0, 0.0, 1.0, 0.0, 1.0],
ShadingKind::LatticeMesh,
)
.expect("params");
let data = [0u8; 64];
let space = ColorSpace::DeviceGray;
let mut reader = MeshReader::new(&data, &p, &space, &[]);
assert!(reader.read_lattice(1).is_empty());
assert!(reader.read_lattice(0).is_empty());
}
#[test]
fn free_form_flag_three_behaves_as_flag_two() {
let p = MeshParams::new(
8,
8,
8,
1,
&[0.0f32, 255.0, 0.0, 255.0, 0.0, 1.0],
ShadingKind::FreeFormMesh,
)
.expect("params");
let space = ColorSpace::DeviceGray;
let mut data = Vec::new();
for (flag, x, y) in [(0u8, 0u8, 0u8), (0, 10, 0), (0, 0, 10)] {
data.extend_from_slice(&[flag, x, y, 128]);
}
data.extend_from_slice(&[2, 20, 20, 128]);
let mut reader = MeshReader::new(&data, &p, &space, &[]);
let with_two = reader.read_free_form();
let mut data3 = Vec::new();
for (flag, x, y) in [(0u8, 0u8, 0u8), (0, 10, 0), (0, 0, 10)] {
data3.extend_from_slice(&[flag, x, y, 128]);
}
data3.extend_from_slice(&[3, 20, 20, 128]);
let mut reader = MeshReader::new(&data3, &p, &space, &[]);
let with_three = reader.read_free_form();
assert_eq!(with_two, with_three);
assert_eq!(with_two.len(), 2);
}
#[test]
fn a_truncated_stream_stops_rather_than_reading_past_the_end() {
let p = MeshParams::new(
8,
8,
8,
1,
&[0.0f32, 255.0, 0.0, 255.0, 0.0, 1.0],
ShadingKind::FreeFormMesh,
)
.expect("params");
let space = ColorSpace::DeviceGray;
let data = [0u8, 5];
let mut reader = MeshReader::new(&data, &p, &space, &[]);
assert!(reader.read_free_form().is_empty());
}
#[test]
fn coons_interiors_are_derived_from_the_boundary() {
let boundary: Vec<kurbo::Point> = (0..12)
.map(|i| {
let t = f64::from(i) / 12.0;
kurbo::Point::new(t, t)
})
.collect();
let interior = super::coons_interior(&boundary);
for p in interior {
assert!(p.x.is_finite() && p.y.is_finite());
}
}
}