1use alloc::vec::Vec;
2use he3d_math::scalar::floor;
3
4use crate::{ColorRgba8, Error, Rgb};
5
6pub const MAX_TEXTURE_DIMENSION: u32 = 8192;
13
14#[derive(Clone, Debug, PartialEq)]
17pub struct Texture {
18 width: u32,
19 height: u32,
20 pixels: Vec<ColorRgba8>,
21}
22
23impl Texture {
24 pub fn new(width: u32, height: u32, pixels: Vec<ColorRgba8>) -> Result<Self, Error> {
32 if width == 0
33 || height == 0
34 || width > MAX_TEXTURE_DIMENSION
35 || height > MAX_TEXTURE_DIMENSION
36 {
37 return Err(Error::InvalidSize);
38 }
39 let expected = (width as usize)
40 .checked_mul(height as usize)
41 .ok_or(Error::Overflow)?;
42 if pixels.len() != expected {
43 return Err(Error::BufferMismatch);
44 }
45 Ok(Self {
46 width,
47 height,
48 pixels,
49 })
50 }
51
52 pub fn from_pixels(width: u32, height: u32, pixels: &[ColorRgba8]) -> Result<Self, Error> {
59 Self::new(width, height, pixels.to_vec())
60 }
61
62 #[must_use]
65 pub fn solid(color: ColorRgba8) -> Self {
66 Self {
67 width: 1,
68 height: 1,
69 pixels: alloc::vec![color],
70 }
71 }
72
73 #[must_use]
76 pub fn width(&self) -> u32 {
77 self.width
78 }
79
80 #[must_use]
83 pub fn height(&self) -> u32 {
84 self.height
85 }
86
87 #[must_use]
90 pub fn pixels(&self) -> &[ColorRgba8] {
91 &self.pixels
92 }
93
94 #[must_use]
100 pub fn sample(&self, u: f32, v: f32) -> ColorRgba8 {
101 let x = sample_axis(u, self.width);
102 let y = sample_axis(v, self.height);
103 self.pixels[y * self.width as usize + x]
104 }
105
106 #[must_use]
109 pub fn sample_linear(&self, u: f32, v: f32) -> Rgb {
110 self.sample(u, v).to_linear()
111 }
112}
113
114#[expect(
122 clippy::cast_possible_truncation,
123 clippy::cast_precision_loss,
124 clippy::cast_sign_loss
125)]
126fn sample_axis(coord: f32, size: u32) -> usize {
127 let wrapped = wrap_uv(coord);
128 let scaled = wrapped * size as f32;
129 let index = scaled as i64;
130 index.clamp(0, i64::from(size) - 1) as usize
131}
132
133fn wrap_uv(x: f32) -> f32 {
137 let frac = x - floor(x);
138 if frac < 0.0 { frac + 1.0 } else { frac }
139}
140
141#[cfg(test)]
142mod tests {
143 use alloc::vec;
144
145 use super::*;
146
147 fn checker() -> Texture {
148 let pixels = vec![
151 ColorRgba8::from_rgb(255, 0, 0),
152 ColorRgba8::from_rgb(0, 255, 0),
153 ColorRgba8::from_rgb(0, 0, 255),
154 ColorRgba8::from_rgb(255, 255, 255),
155 ];
156 Texture::new(2, 2, pixels).unwrap()
157 }
158
159 #[test]
160 fn samples_corners() {
161 let tex = checker();
162 assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(255, 0, 0));
163 assert_eq!(tex.sample(0.99, 0.99), ColorRgba8::from_rgb(255, 255, 255));
164 }
165
166 #[test]
167 fn wraps_out_of_range_coordinates() {
168 let tex = checker();
169 assert_eq!(tex.sample(1.5, 0.0), ColorRgba8::from_rgb(0, 255, 0));
172 assert_eq!(tex.sample(0.0, -0.5), tex.sample(0.0, 0.5));
175 }
176
177 #[test]
178 fn rejects_zero_dimension() {
179 assert_eq!(Texture::new(0, 2, Vec::new()), Err(Error::InvalidSize));
180 }
181
182 #[test]
183 fn rejects_oversized_dimension() {
184 assert_eq!(
185 Texture::new(MAX_TEXTURE_DIMENSION + 1, 2, Vec::new()),
186 Err(Error::InvalidSize)
187 );
188 }
189
190 #[test]
191 fn rejects_buffer_mismatch() {
192 let pixels = vec![ColorRgba8::BLACK; 3];
193 assert_eq!(Texture::new(2, 2, pixels), Err(Error::BufferMismatch));
194 }
195
196 #[test]
197 fn solid_samples_constant_color() {
198 let tex = Texture::solid(ColorRgba8::from_rgb(10, 20, 30));
199 assert_eq!(tex.width(), 1);
200 assert_eq!(tex.height(), 1);
201 assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(10, 20, 30));
202 assert_eq!(tex.sample(0.5, 0.5), ColorRgba8::from_rgb(10, 20, 30));
203 assert_eq!(tex.sample(1.5, -0.5), ColorRgba8::from_rgb(10, 20, 30));
204 }
205}