use crate::Color;
use glam::{Affine2, Vec2};
use impeller_hal::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VertexMode {
#[default]
Triangles,
TriangleStrip,
TriangleFan,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Vertices {
positions: Vec<Vec2>,
texture_coords: Vec<Vec2>,
colors: Vec<Color>,
indices: Vec<u32>,
}
impl Vertices {
pub fn new(mode: VertexMode, positions: Vec<Vec2>) -> Result<Self> {
Self::build(mode, positions, Vec::new(), Vec::new(), None)
}
pub fn textured(
mode: VertexMode,
positions: Vec<Vec2>,
texture_coords: Vec<Vec2>,
) -> Result<Self> {
Self::build(mode, positions, texture_coords, Vec::new(), None)
}
pub fn colored(mode: VertexMode, positions: Vec<Vec2>, colors: Vec<Color>) -> Result<Self> {
Self::build(mode, positions, Vec::new(), colors, None)
}
pub fn indexed(
mode: VertexMode,
positions: Vec<Vec2>,
texture_coords: Vec<Vec2>,
indices: Vec<u32>,
) -> Result<Self> {
Self::build(mode, positions, texture_coords, Vec::new(), Some(indices))
}
pub fn full(
mode: VertexMode,
positions: Vec<Vec2>,
texture_coords: Vec<Vec2>,
colors: Vec<Color>,
indices: Vec<u32>,
) -> Result<Self> {
Self::build(mode, positions, texture_coords, colors, Some(indices))
}
fn build(
mode: VertexMode,
positions: Vec<Vec2>,
texture_coords: Vec<Vec2>,
colors: Vec<Color>,
indices: Option<Vec<u32>>,
) -> Result<Self> {
if !texture_coords.is_empty() && texture_coords.len() != positions.len() {
return Err(Error::Unsupported(
"a mesh with texture coordinates needs one per position",
));
}
if !colors.is_empty() && colors.len() != positions.len() {
return Err(Error::Unsupported(
"a mesh with colors needs one per position",
));
}
if let Some(indices) = &indices {
if let Some(bad) = indices.iter().find(|i| **i as usize >= positions.len()) {
let _ = bad;
return Err(Error::Unsupported(
"a mesh index names a position the mesh does not have",
));
}
}
if !positions.iter().all(|p| p.is_finite()) || !texture_coords.iter().all(|c| c.is_finite())
{
return Err(Error::Unsupported(
"a mesh position or texture coordinate is not a finite number",
));
}
let order: Vec<u32> = match indices {
Some(indices) => indices,
None => (0..positions.len() as u32).collect(),
};
let indices = expand(mode, &order);
Ok(Self {
positions,
texture_coords,
colors,
indices,
})
}
pub fn positions(&self) -> &[Vec2] {
&self.positions
}
pub fn texture_coords(&self) -> &[Vec2] {
&self.texture_coords
}
pub fn colors(&self) -> &[Color] {
&self.colors
}
pub fn indices(&self) -> &[u32] {
&self.indices
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sprite {
pub source: SourceRect,
pub color: Color,
pub transform: Affine2,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SourceRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl SourceRect {
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn is_finite(&self) -> bool {
self.x.is_finite()
&& self.y.is_finite()
&& self.width.is_finite()
&& self.height.is_finite()
}
}
impl Sprite {
pub fn new(source: SourceRect, transform: Affine2) -> Self {
Self {
source,
color: Color::WHITE,
transform,
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn at(source: SourceRect, position: Vec2) -> Self {
Self::new(source, Affine2::from_translation(position))
}
}
fn expand(mode: VertexMode, order: &[u32]) -> Vec<u32> {
let mut out = Vec::new();
match mode {
VertexMode::Triangles => {
for triangle in order.chunks_exact(3) {
out.extend_from_slice(triangle);
}
}
VertexMode::TriangleStrip => {
for (i, window) in order.windows(3).enumerate() {
if i % 2 == 0 {
out.extend_from_slice(&[window[0], window[1], window[2]]);
} else {
out.extend_from_slice(&[window[1], window[0], window[2]]);
}
}
}
VertexMode::TriangleFan => {
for window in order.windows(2).skip(1) {
out.extend_from_slice(&[order[0], window[0], window[1]]);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn winding(a: Vec2, b: Vec2, c: Vec2) -> f32 {
(b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)
}
#[test]
fn a_strip_comes_out_consistently_wound() {
let strip: Vec<Vec2> = (0..6)
.map(|i| Vec2::new(i as f32 * 10.0, if i % 2 == 0 { 0.0 } else { 12.0 }))
.collect();
let indices = expand(VertexMode::TriangleStrip, &[0, 1, 2, 3, 4, 5]);
assert_eq!(indices.len(), 4 * 3, "six points make four triangles");
let signs: Vec<f32> = indices
.chunks_exact(3)
.map(|t| {
winding(
strip[t[0] as usize],
strip[t[1] as usize],
strip[t[2] as usize],
)
.signum()
})
.collect();
assert!(
signs.iter().all(|s| *s == signs[0]),
"every triangle in a strip should wind the same way, got {signs:?}"
);
for (i, t) in indices.chunks_exact(3).enumerate() {
let mut got = [t[0], t[1], t[2]];
got.sort_unstable();
let want = [i as u32, i as u32 + 1, i as u32 + 2];
assert_eq!(got, want, "triangle {i} covers the wrong points");
}
}
fn points(n: usize) -> Vec<Vec2> {
(0..n).map(|i| Vec2::new(i as f32, 0.0)).collect()
}
#[test]
fn a_strip_alternates_winding_so_every_triangle_faces_the_same_way() {
let mesh = Vertices::new(VertexMode::TriangleStrip, points(4)).expect("strip");
assert_eq!(mesh.indices(), &[0, 1, 2, 2, 1, 3]);
}
#[test]
fn a_fan_shares_its_first_vertex_with_every_triangle() {
let mesh = Vertices::new(VertexMode::TriangleFan, points(4)).expect("fan");
assert_eq!(mesh.indices(), &[0, 1, 2, 0, 2, 3]);
}
#[test]
fn a_list_drops_a_partial_triangle_rather_than_inventing_a_vertex() {
let mesh = Vertices::new(VertexMode::Triangles, points(5)).expect("list");
assert_eq!(mesh.indices(), &[0, 1, 2]);
}
#[test]
fn too_few_positions_for_any_triangle_is_an_empty_mesh_rather_than_an_error() {
for mode in [
VertexMode::Triangles,
VertexMode::TriangleStrip,
VertexMode::TriangleFan,
] {
let mesh = Vertices::new(mode, points(2)).expect("two points");
assert!(mesh.is_empty(), "{mode:?} made a triangle from two points");
}
}
#[test]
fn an_index_past_the_end_is_refused_where_it_is_written() {
let error = Vertices::indexed(VertexMode::Triangles, points(3), Vec::new(), vec![0, 1, 3]);
assert!(error.is_err(), "an out-of-range index was accepted");
}
#[test]
fn texture_coordinates_have_to_match_the_positions_they_belong_to() {
let error = Vertices::textured(VertexMode::Triangles, points(3), points(2));
assert!(error.is_err(), "a short coordinate list was accepted");
}
}