1use alloc::vec::Vec;
2use he3d_math::scalar::floor;
3
4use crate::{ColorRgba8, Error, Rgb};
5
6pub const MAX_TEXTURE_DIMENSION: u32 = 8192;
9
10#[derive(Clone, Debug, PartialEq)]
13pub struct Texture {
14 width: u32,
15 height: u32,
16 pixels: Vec<ColorRgba8>,
17}
18
19impl Texture {
20 pub fn new(width: u32, height: u32, pixels: Vec<ColorRgba8>) -> Result<Self, Error> {
23 if width == 0
24 || height == 0
25 || width > MAX_TEXTURE_DIMENSION
26 || height > MAX_TEXTURE_DIMENSION
27 {
28 return Err(Error::InvalidSize);
29 }
30 let expected = (width as usize)
31 .checked_mul(height as usize)
32 .ok_or(Error::Overflow)?;
33 if pixels.len() != expected {
34 return Err(Error::BufferMismatch);
35 }
36 Ok(Self {
37 width,
38 height,
39 pixels,
40 })
41 }
42
43 pub fn from_pixels(width: u32, height: u32, pixels: &[ColorRgba8]) -> Result<Self, Error> {
46 Self::new(width, height, pixels.to_vec())
47 }
48
49 pub fn solid(color: ColorRgba8) -> Self {
52 Self {
53 width: 1,
54 height: 1,
55 pixels: alloc::vec![color],
56 }
57 }
58
59 pub fn width(&self) -> u32 {
60 self.width
61 }
62
63 pub fn height(&self) -> u32 {
64 self.height
65 }
66
67 pub fn pixels(&self) -> &[ColorRgba8] {
68 &self.pixels
69 }
70
71 pub fn sample(&self, u: f32, v: f32) -> ColorRgba8 {
77 let x = self.sample_axis(u, self.width);
78 let y = self.sample_axis(v, self.height);
79 self.pixels[y * self.width as usize + x]
80 }
81
82 pub fn sample_linear(&self, u: f32, v: f32) -> Rgb {
85 self.sample(u, v).to_linear()
86 }
87
88 fn sample_axis(&self, coord: f32, size: u32) -> usize {
89 let wrapped = wrap_uv(coord);
90 let index = (wrapped * size as f32) as i64;
91 index.clamp(0, size as i64 - 1) as usize
92 }
93}
94
95fn wrap_uv(x: f32) -> f32 {
98 let frac = x - floor(x);
99 if frac < 0.0 { frac + 1.0 } else { frac }
100}
101
102#[cfg(test)]
103mod tests {
104 use alloc::vec;
105
106 use super::*;
107
108 fn checker() -> Texture {
109 let pixels = vec![
112 ColorRgba8::from_rgb(255, 0, 0),
113 ColorRgba8::from_rgb(0, 255, 0),
114 ColorRgba8::from_rgb(0, 0, 255),
115 ColorRgba8::from_rgb(255, 255, 255),
116 ];
117 Texture::new(2, 2, pixels).unwrap()
118 }
119
120 #[test]
121 fn samples_corners() {
122 let tex = checker();
123 assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(255, 0, 0));
124 assert_eq!(tex.sample(0.99, 0.99), ColorRgba8::from_rgb(255, 255, 255));
125 }
126
127 #[test]
128 fn wraps_out_of_range_coordinates() {
129 let tex = checker();
130 assert_eq!(tex.sample(1.5, 0.0), ColorRgba8::from_rgb(0, 255, 0));
133 assert_eq!(tex.sample(0.0, -0.5), tex.sample(0.0, 0.5));
136 }
137
138 #[test]
139 fn rejects_zero_dimension() {
140 assert_eq!(Texture::new(0, 2, Vec::new()), Err(Error::InvalidSize));
141 }
142
143 #[test]
144 fn rejects_oversized_dimension() {
145 assert_eq!(
146 Texture::new(MAX_TEXTURE_DIMENSION + 1, 2, Vec::new()),
147 Err(Error::InvalidSize)
148 );
149 }
150
151 #[test]
152 fn rejects_buffer_mismatch() {
153 let pixels = vec![ColorRgba8::BLACK; 3];
154 assert_eq!(Texture::new(2, 2, pixels), Err(Error::BufferMismatch));
155 }
156
157 #[test]
158 fn solid_samples_constant_color() {
159 let tex = Texture::solid(ColorRgba8::from_rgb(10, 20, 30));
160 assert_eq!(tex.width(), 1);
161 assert_eq!(tex.height(), 1);
162 assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(10, 20, 30));
163 assert_eq!(tex.sample(0.5, 0.5), ColorRgba8::from_rgb(10, 20, 30));
164 assert_eq!(tex.sample(1.5, -0.5), ColorRgba8::from_rgb(10, 20, 30));
165 }
166}