use alloc::vec::Vec;
use he3d_math::scalar::floor;
use crate::{ColorRgba8, Error, Rgb};
pub const MAX_TEXTURE_DIMENSION: u32 = 8192;
#[derive(Clone, Debug, PartialEq)]
pub struct Texture {
width: u32,
height: u32,
pixels: Vec<ColorRgba8>,
}
impl Texture {
pub fn new(width: u32, height: u32, pixels: Vec<ColorRgba8>) -> Result<Self, Error> {
if width == 0
|| height == 0
|| width > MAX_TEXTURE_DIMENSION
|| height > MAX_TEXTURE_DIMENSION
{
return Err(Error::InvalidSize);
}
let expected = (width as usize)
.checked_mul(height as usize)
.ok_or(Error::Overflow)?;
if pixels.len() != expected {
return Err(Error::BufferMismatch);
}
Ok(Self {
width,
height,
pixels,
})
}
pub fn from_pixels(width: u32, height: u32, pixels: &[ColorRgba8]) -> Result<Self, Error> {
Self::new(width, height, pixels.to_vec())
}
pub fn solid(color: ColorRgba8) -> Self {
Self {
width: 1,
height: 1,
pixels: alloc::vec![color],
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn pixels(&self) -> &[ColorRgba8] {
&self.pixels
}
pub fn sample(&self, u: f32, v: f32) -> ColorRgba8 {
let x = self.sample_axis(u, self.width);
let y = self.sample_axis(v, self.height);
self.pixels[y * self.width as usize + x]
}
pub fn sample_linear(&self, u: f32, v: f32) -> Rgb {
self.sample(u, v).to_linear()
}
fn sample_axis(&self, coord: f32, size: u32) -> usize {
let wrapped = wrap_uv(coord);
let index = (wrapped * size as f32) as i64;
index.clamp(0, size as i64 - 1) as usize
}
}
fn wrap_uv(x: f32) -> f32 {
let frac = x - floor(x);
if frac < 0.0 { frac + 1.0 } else { frac }
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
fn checker() -> Texture {
let pixels = vec![
ColorRgba8::from_rgb(255, 0, 0),
ColorRgba8::from_rgb(0, 255, 0),
ColorRgba8::from_rgb(0, 0, 255),
ColorRgba8::from_rgb(255, 255, 255),
];
Texture::new(2, 2, pixels).unwrap()
}
#[test]
fn samples_corners() {
let tex = checker();
assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(255, 0, 0));
assert_eq!(tex.sample(0.99, 0.99), ColorRgba8::from_rgb(255, 255, 255));
}
#[test]
fn wraps_out_of_range_coordinates() {
let tex = checker();
assert_eq!(tex.sample(1.5, 0.0), ColorRgba8::from_rgb(0, 255, 0));
assert_eq!(tex.sample(0.0, -0.5), tex.sample(0.0, 0.5));
}
#[test]
fn rejects_zero_dimension() {
assert_eq!(Texture::new(0, 2, Vec::new()), Err(Error::InvalidSize));
}
#[test]
fn rejects_oversized_dimension() {
assert_eq!(
Texture::new(MAX_TEXTURE_DIMENSION + 1, 2, Vec::new()),
Err(Error::InvalidSize)
);
}
#[test]
fn rejects_buffer_mismatch() {
let pixels = vec![ColorRgba8::BLACK; 3];
assert_eq!(Texture::new(2, 2, pixels), Err(Error::BufferMismatch));
}
#[test]
fn solid_samples_constant_color() {
let tex = Texture::solid(ColorRgba8::from_rgb(10, 20, 30));
assert_eq!(tex.width(), 1);
assert_eq!(tex.height(), 1);
assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(10, 20, 30));
assert_eq!(tex.sample(0.5, 0.5), ColorRgba8::from_rgb(10, 20, 30));
assert_eq!(tex.sample(1.5, -0.5), ColorRgba8::from_rgb(10, 20, 30));
}
}