1use std::collections::HashMap;
33use std::io::Cursor;
34use std::sync::{Arc, OnceLock};
35
36use image::imageops::FilterType;
37use image::{ImageReader, RgbaImage};
38
39use super::math::V3;
40
41pub const MAX_SIDE: u32 = 4096;
43pub const BUDGET: usize = 64 * 1024 * 1024;
45const DECODE_LIMIT: u64 = 512 * 1024 * 1024;
48
49#[derive(Clone, PartialEq, Eq)]
52pub struct Image {
53 pub name: String,
55 pub width: u32,
56 pub height: u32,
57 pub rgba: Vec<u8>,
58}
59
60impl std::fmt::Debug for Image {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "Image({:?}, {}x{})", self.name, self.width, self.height)
63 }
64}
65
66impl Image {
67 fn texel(&self, x: u32, y: u32) -> [u8; 4] {
69 let i = (y as usize * self.width as usize + x as usize) * 4;
70 [self.rgba[i], self.rgba[i + 1], self.rgba[i + 2], self.rgba[i + 3]]
71 }
72
73 pub fn average(&self) -> Option<V3> {
76 let (sx, sy) = ((self.width / 64).max(1), (self.height / 64).max(1));
77 let (mut sum, mut weight) = ([0.0f64; 3], 0.0f64);
78 for y in (0..self.height).step_by(sy as usize) {
79 for x in (0..self.width).step_by(sx as usize) {
80 let p = self.texel(x, y);
81 let a = f64::from(p[3]) / 255.0;
82 for k in 0..3 {
83 sum[k] += f64::from(p[k]) / 255.0 * a;
84 }
85 weight += a;
86 }
87 }
88 (weight > 0.0).then(|| sum.map(|s| (s / weight) as f32))
89 }
90}
91
92#[derive(Clone, Debug, PartialEq)]
94pub struct Texture {
95 pub image: Arc<Image>,
96 pub tint: V3,
100 pub alpha_cutoff: Option<f32>,
103}
104
105impl Texture {
106 pub fn new(image: Arc<Image>) -> Texture {
108 Texture { image, tint: [1.0; 3], alpha_cutoff: None }
109 }
110
111 fn around(&self, uv: [f32; 2]) -> [(u32, u32, f32); 4] {
113 let img = &*self.image;
114 let (w, h) = (img.width.max(1), img.height.max(1));
115 let fx = wrap_coord(uv[0]) * w as f32 - 0.5;
117 let fy = wrap_coord(uv[1]) * h as f32 - 0.5;
118 let (x0, y0) = (fx.floor(), fy.floor());
119 let (tx, ty) = (fx - x0, fy - y0);
120 let pick = |i: i64, n: u32| i.rem_euclid(i64::from(n)) as u32;
121 let (x0, y0) = (x0 as i64, y0 as i64);
122 let xs = [pick(x0, w), pick(x0 + 1, w)];
123 let ys = [pick(y0, h), pick(y0 + 1, h)];
124 [(xs[0], ys[0], (1.0 - tx) * (1.0 - ty)), (xs[1], ys[0], tx * (1.0 - ty)), (xs[0], ys[1], (1.0 - tx) * ty), (xs[1], ys[1], tx * ty)]
125 }
126
127 pub fn sample(&self, uv: [f32; 2]) -> [f32; 4] {
131 let lut = linear_lut();
132 let mut acc = [0.0f32; 4];
133 for (x, y, wt) in self.around(uv) {
134 let p = self.image.texel(x, y);
135 let a = f32::from(p[3]) / 255.0;
136 for k in 0..3 {
139 acc[k] += lut[p[k] as usize] * a * wt;
140 }
141 acc[3] += a * wt;
142 }
143 if acc[3] <= 0.0 {
144 return [0.0; 4];
145 }
146 [linear_to_srgb(acc[0] / acc[3]), linear_to_srgb(acc[1] / acc[3]), linear_to_srgb(acc[2] / acc[3]), acc[3]]
147 }
148
149 pub fn sample_data(&self, uv: [f32; 2]) -> V3 {
153 let mut acc = [0.0f32; 3];
154 for (x, y, wt) in self.around(uv) {
155 let p = self.image.texel(x, y);
156 for k in 0..3 {
157 acc[k] += f32::from(p[k]) / 255.0 * wt;
158 }
159 }
160 acc
161 }
162}
163
164pub fn modulate(color: V3, rgb: [f32; 4]) -> V3 {
168 std::array::from_fn(|k| linear_to_srgb(srgb_to_linear(color[k]) * srgb_to_linear(rgb[k])))
169}
170
171pub fn wrap_coord(t: f32) -> f32 {
173 if !t.is_finite() {
174 return 0.0;
175 }
176 t - t.floor()
177}
178
179pub fn srgb_to_linear(c: f32) -> f32 {
180 let c = c.clamp(0.0, 1.0);
181 if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
182}
183
184pub fn linear_to_srgb(c: f32) -> f32 {
185 let c = c.clamp(0.0, 1.0);
186 if c <= 0.003_130_8 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
187}
188
189fn linear_lut() -> &'static [f32; 256] {
191 static LUT: OnceLock<[f32; 256]> = OnceLock::new();
192 LUT.get_or_init(|| std::array::from_fn(|i| srgb_to_linear(i as f32 / 255.0)))
193}
194
195pub fn fit_size(w: u32, h: u32, max_side: u32, max_bytes: usize) -> Option<(u32, u32)> {
199 if w == 0 || h == 0 || max_bytes < 4 {
200 return None;
201 }
202 let s = (f64::from(max_side) / f64::from(w.max(h))).min(1.0);
203 let (mut tw, mut th) = (((f64::from(w) * s).round() as u32).max(1), ((f64::from(h) * s).round() as u32).max(1));
204 while tw as usize * th as usize * 4 > max_bytes {
205 tw = (tw / 2).max(1);
206 th = (th / 2).max(1);
207 }
208 Some((tw, th))
209}
210
211pub fn decode(data: &[u8], max_side: u32, max_bytes: usize) -> Option<RgbaImage> {
216 let size = ImageReader::new(Cursor::new(data)).with_guessed_format().ok().and_then(|r| r.into_dimensions().ok());
217 let img = match size {
218 Some((w, h)) => {
219 let (tw, th) = fit_size(w, h, max_side, max_bytes)?;
220 if (tw, th) == (w, h) {
221 decode_full(data)?
222 } else {
223 match crate::imageio::decode_bytes_within(data, tw.max(th)) {
224 Ok(img) => img,
225 Err(_) => decode_full(data)?,
226 }
227 }
228 }
229 None => crate::imageio::decode_bytes_within(data, max_side).ok()?,
230 };
231 let (tw, th) = fit_size(img.width(), img.height(), max_side, max_bytes)?;
232 Some(if (tw, th) == img.dimensions() { img } else { image::imageops::resize(&img, tw, th, FilterType::Triangle) })
233}
234
235fn decode_full(data: &[u8]) -> Option<RgbaImage> {
236 let mut reader = ImageReader::new(Cursor::new(data)).with_guessed_format().ok()?;
237 let mut limits = image::Limits::default();
238 limits.max_alloc = Some(DECODE_LIMIT);
239 reader.limits(limits);
240 Some(reader.decode().ok()?.into_rgba8())
241}
242
243pub fn candidates(path: &str) -> Vec<String> {
248 let p = path.trim().replace('\\', "/");
249 let p = p.strip_prefix("file://").map_or(p.as_str(), |r| r.strip_prefix("localhost").unwrap_or(r));
250 let drive = p.len() >= 2 && p.as_bytes()[1] == b':' && p.as_bytes()[0].is_ascii_alphabetic();
251 let absolute = p.starts_with('/') || drive;
252 let parts: Vec<&str> = p.split('/').filter(|s| !s.is_empty() && *s != ".").collect();
253 let Some(&name) = parts.last() else { return Vec::new() };
254 let name = if drive && parts.len() == 1 { &name[2..] } else { name };
255 if name.is_empty() || name == ".." {
256 return Vec::new();
257 }
258 let mut out = Vec::new();
259 if !absolute && !parts.contains(&"..") {
260 out.push(parts.join("/"));
261 }
262 if out.first().is_none_or(|f| f != name) {
263 out.push(name.to_string());
264 }
265 out
266}
267
268pub type Side<'a> = &'a dyn Fn(&str) -> Option<Vec<u8>>;
270
271pub struct Loader<'a> {
275 side: Side<'a>,
276 left: usize,
277 share: usize,
279 by_path: HashMap<String, Option<Arc<Image>>>,
281 by_hash: HashMap<[u8; 32], Option<Arc<Image>>>,
282}
283
284impl<'a> Loader<'a> {
285 pub fn new(side: Side<'a>) -> Loader<'a> {
286 Loader::with_budget(side, BUDGET)
287 }
288
289 pub fn with_budget(side: Side<'a>, budget: usize) -> Loader<'a> {
291 Loader { side, left: budget, share: 1, by_path: HashMap::new(), by_hash: HashMap::new() }
292 }
293
294 pub fn for_map<T>(&mut self, read: impl FnOnce(&mut Self) -> T) -> T {
297 self.share = 2;
298 let out = read(self);
299 self.share = 1;
300 out
301 }
302
303 pub fn left(&self) -> usize {
305 self.left
306 }
307
308 pub fn file(&mut self, path: &str) -> Option<Arc<Image>> {
310 self.file_in("", path)
311 }
312
313 pub fn file_in(&mut self, dir: &str, path: &str) -> Option<Arc<Image>> {
317 let mut tries = Vec::new();
318 let dir = dir.trim_matches('/');
319 if !dir.is_empty() {
320 for c in candidates(path) {
321 tries.extend(candidates(&format!("{dir}/{c}")).into_iter().take(1));
322 }
323 }
324 tries.extend(candidates(path));
325 for p in tries {
326 if let Some(hit) = self.by_path.get(&p) {
327 if hit.is_some() {
328 return hit.clone();
329 }
330 continue;
331 }
332 let Some(bytes) = (self.side)(&p) else {
333 self.by_path.insert(p, None);
334 continue;
335 };
336 let name = p.rsplit('/').next().unwrap_or(&p).to_string();
337 let img = self.embedded(&name, &bytes);
338 self.by_path.insert(p, img.clone());
339 if img.is_some() {
340 return img;
341 }
342 }
343 None
344 }
345
346 pub fn embedded(&mut self, name: &str, data: &[u8]) -> Option<Arc<Image>> {
349 let hash = *blake3::hash(data).as_bytes();
350 if let Some(hit) = self.by_hash.get(&hash) {
351 return hit.clone();
352 }
353 let img = decode(data, MAX_SIDE, self.left / self.share).map(|img| {
354 let (width, height) = img.dimensions();
355 Arc::new(Image { name: name.to_string(), width, height, rgba: img.into_raw() })
356 });
357 if let Some(i) = &img {
358 self.left -= i.rgba.len();
359 }
360 self.by_hash.insert(hash, img.clone());
361 img
362 }
363}
364
365pub fn mip_chain(img: &Image) -> Vec<(u32, u32, Vec<u8>)> {
370 let lut = linear_lut();
371 let mut out: Vec<(u32, u32, Vec<u8>)> = Vec::new();
372 let (mut w, mut h) = (img.width, img.height);
373 while w > 1 || h > 1 {
374 let (pw, ph) = (w, h);
375 let src: &[u8] = out.last().map_or(&img.rgba, |l| &l.2);
376 let (nw, nh) = ((w / 2).max(1), (h / 2).max(1));
377 let mut level = vec![0u8; nw as usize * nh as usize * 4];
378 for y in 0..nh {
379 for x in 0..nw {
380 let mut acc = [0.0f32; 4];
381 let mut n = 0.0f32;
382 for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
383 let (sx, sy) = ((x * 2 + dx).min(pw - 1), (y * 2 + dy).min(ph - 1));
384 let i = (sy as usize * pw as usize + sx as usize) * 4;
385 let a = f32::from(src[i + 3]) / 255.0;
386 for k in 0..3 {
387 acc[k] += lut[src[i + k] as usize] * a;
388 }
389 acc[3] += a;
390 n += 1.0;
391 }
392 let o = (y as usize * nw as usize + x as usize) * 4;
393 for k in 0..3 {
394 let v = if acc[3] > 0.0 { linear_to_srgb(acc[k] / acc[3]) } else { 0.0 };
395 level[o + k] = (v * 255.0).round() as u8;
396 }
397 level[o + 3] = (acc[3] / n * 255.0).round() as u8;
398 }
399 }
400 out.push((nw, nh, level));
401 (w, h) = (nw, nh);
402 }
403 out
404}
405
406pub fn mip_chain_data(img: &Image) -> Vec<(u32, u32, Vec<u8>)> {
409 let mut out: Vec<(u32, u32, Vec<u8>)> = Vec::new();
410 let (mut w, mut h) = (img.width, img.height);
411 while w > 1 || h > 1 {
412 let src: &[u8] = out.last().map_or(&img.rgba, |l| &l.2);
413 let (nw, nh) = ((w / 2).max(1), (h / 2).max(1));
414 let mut level = vec![0u8; nw as usize * nh as usize * 4];
415 for y in 0..nh {
416 for x in 0..nw {
417 let mut acc = [0u32; 4];
418 for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
419 let (sx, sy) = ((x * 2 + dx).min(w - 1), (y * 2 + dy).min(h - 1));
420 let i = (sy as usize * w as usize + sx as usize) * 4;
421 for k in 0..4 {
422 acc[k] += u32::from(src[i + k]);
423 }
424 }
425 let o = (y as usize * nw as usize + x as usize) * 4;
426 for k in 0..4 {
427 level[o + k] = ((acc[k] + 2) / 4) as u8;
428 }
429 }
430 }
431 out.push((nw, nh, level));
432 (w, h) = (nw, nh);
433 }
434 out
435}
436
437#[cfg(test)]
438pub(crate) mod tests {
439 use super::*;
440
441 pub(crate) fn png(w: u32, h: u32, rgba: [u8; 4]) -> Vec<u8> {
443 let mut out = Vec::new();
444 RgbaImage::from_pixel(w, h, image::Rgba(rgba)).write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
445 out
446 }
447
448 pub(crate) fn png_2x2() -> Vec<u8> {
450 let mut img = RgbaImage::new(2, 2);
451 img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));
452 img.put_pixel(1, 0, image::Rgba([0, 255, 0, 255]));
453 img.put_pixel(0, 1, image::Rgba([0, 0, 255, 255]));
454 img.put_pixel(1, 1, image::Rgba([255, 255, 255, 255]));
455 let mut out = Vec::new();
456 img.write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
457 out
458 }
459
460 pub(crate) fn huge_png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
465 fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
466 out.extend_from_slice(&(data.len() as u32).to_be_bytes());
467 let start = out.len();
468 out.extend_from_slice(kind);
469 out.extend_from_slice(data);
470 let crc = crc32(&out[start..]);
471 out.extend_from_slice(&crc.to_be_bytes());
472 }
473 fn crc32(b: &[u8]) -> u32 {
474 let mut c = !0u32;
475 for &x in b {
476 c ^= u32::from(x);
477 for _ in 0..8 {
478 c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
479 }
480 }
481 !c
482 }
483 struct Bits {
484 out: Vec<u8>,
485 acc: u64,
486 n: u32,
487 }
488 impl Bits {
489 fn put(&mut self, v: u32, len: u32) {
490 self.acc |= u64::from(v) << self.n;
491 self.n += len;
492 while self.n >= 8 {
493 self.out.push(self.acc as u8);
494 self.acc >>= 8;
495 self.n -= 8;
496 }
497 }
498 fn code(&mut self, v: u32, len: u32) {
500 let rev = v.reverse_bits() >> (32 - len);
501 self.put(rev, len);
502 }
503 }
504 let total = u64::from(h) * (u64::from(w) + 1);
505 let mut z = vec![0x78, 0x01];
506 let mut bits = Bits { out: Vec::new(), acc: 0, n: 0 };
507 bits.put(1, 1); bits.put(1, 2); bits.code(0x30, 8); let mut left = total - 1;
511 while left >= 3 {
512 let len = left.min(258);
513 let len = if left - len > 0 && left - len < 3 { len - 3 } else { len };
515 match len {
516 258 => bits.code(0b1100_0101, 8), l => {
518 let (code, base, extra) = length_code(l as u32);
520 if code < 280 {
521 bits.code(code - 256, 7);
522 } else {
523 bits.code(0b1100_0000 + (code - 280), 8);
524 }
525 bits.put(l as u32 - base, extra);
526 }
527 }
528 bits.code(0, 5); left -= len;
530 }
531 for _ in 0..left {
532 bits.code(0x30, 8);
533 }
534 bits.code(0, 7); if bits.n > 0 {
536 bits.out.push(bits.acc as u8);
537 }
538 z.extend_from_slice(&bits.out);
539 let (a, b) = (1u64, total % 65521);
541 z.extend_from_slice(&(((b << 16) | a) as u32).to_be_bytes());
542 let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
543 let mut ihdr = Vec::new();
544 ihdr.extend_from_slice(&w.to_be_bytes());
545 ihdr.extend_from_slice(&h.to_be_bytes());
546 ihdr.extend_from_slice(&[8, 3, 0, 0, 0]);
547 chunk(&mut out, b"IHDR", &ihdr);
548 chunk(&mut out, b"PLTE", &rgb);
549 chunk(&mut out, b"IDAT", &z);
550 chunk(&mut out, b"IEND", &[]);
551 out
552 }
553
554 fn length_code(l: u32) -> (u32, u32, u32) {
555 const BASE: [u32; 29] = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258];
556 const EXTRA: [u32; 29] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0];
557 let i = BASE.iter().rposition(|&b| b <= l).unwrap();
558 (257 + i as u32, BASE[i], EXTRA[i])
559 }
560
561 fn image(w: u32, h: u32, px: &[[u8; 4]]) -> Arc<Image> {
562 Arc::new(Image { name: "t".into(), width: w, height: h, rgba: px.iter().flatten().copied().collect() })
563 }
564
565 #[test]
566 fn paths_stay_next_to_the_model() {
567 assert_eq!(candidates("tex/wood.png"), ["tex/wood.png", "wood.png"]);
568 assert_eq!(candidates("./wood.png"), ["wood.png"]);
569 assert_eq!(candidates("tex\\wood.png"), ["tex/wood.png", "wood.png"]);
570 assert_eq!(candidates("../x.png"), ["x.png"]);
572 assert_eq!(candidates("a/../../x.png"), ["x.png"]);
573 assert_eq!(candidates("/etc/x.png"), ["x.png"]);
574 assert_eq!(candidates("C:\\work\\x.png"), ["x.png"]);
575 assert_eq!(candidates("C:x.png"), ["x.png"]);
576 assert_eq!(candidates("file:///C:/work/g.png"), ["g.png"]);
577 assert!(candidates("..").is_empty() && candidates("").is_empty() && candidates("/").is_empty());
578 }
579
580 #[test]
581 fn traversal_and_absolute_paths_are_not_read() {
582 let dir = tempfile::tempdir().unwrap();
583 let model = dir.path().join("m");
584 std::fs::create_dir_all(model.join("sub")).unwrap();
585 std::fs::write(dir.path().join("x.png"), png(1, 1, [255, 0, 0, 255])).unwrap();
586 std::fs::write(model.join("sub/y.png"), png(1, 1, [0, 255, 0, 255])).unwrap();
587 let asked = std::cell::RefCell::new(Vec::new());
588 let root = model.clone();
589 let side = |n: &str| {
590 asked.borrow_mut().push(n.to_string());
591 std::fs::read(root.join(n)).ok()
592 };
593 let mut l = Loader::new(&side);
594 assert!(l.file("../x.png").is_none(), "above the model");
595 assert!(l.file(dir.path().join("x.png").to_str().unwrap()).is_none(), "absolute");
596 assert!(asked.borrow().iter().all(|n| n == "x.png"), "only the bare name was asked for: {:?}", asked.borrow());
597 assert_eq!(l.file("sub/y.png").unwrap().texel(0, 0), [0, 255, 0, 255]);
599 assert!(l.file_in("sub", "y.png").is_some());
600 }
601
602 #[test]
603 fn the_same_picture_decodes_once() {
604 let data = png(4, 4, [9, 9, 9, 255]);
605 let side = |n: &str| (n == "a.png" || n == "b.png").then(|| data.clone());
606 let mut l = Loader::new(&side);
607 let a = l.file("a.png").unwrap();
608 let b = l.file("b.png").unwrap();
609 assert!(Arc::ptr_eq(&a, &b), "same bytes, one picture");
610 assert_eq!(l.left(), BUDGET - 64);
611 assert!(l.file("missing.png").is_none());
612 assert!(l.embedded("junk", b"not a picture").is_none());
613 assert_eq!(l.left(), BUDGET - 64);
614 let q = l.embedded("q", &png_2x2()).unwrap();
615 assert_eq!((q.width, q.height, q.texel(0, 1)), (2, 2, [0, 0, 255, 255]));
616 }
617
618 #[test]
619 fn huge_pictures_are_scaled_down_within_the_budget() {
620 let big = huge_png(10_000, 10_000, [200, 10, 20]);
622 assert!(big.len() < 8_000_000);
623 let side = |_: &str| None;
624 let mut l = Loader::new(&side);
625 let img = l.embedded("big", &big).unwrap();
626 assert_eq!((img.width, img.height), (MAX_SIDE, MAX_SIDE));
627 let mid = img.texel(2000, 2000);
628 assert!(mid[0] > 190 && mid[1] < 30 && mid[3] == 255, "{mid:?}");
629 assert_eq!(l.left(), 0);
632 assert!(l.embedded("other", &huge_png(5000, 5000, [1, 2, 3])).is_none());
633 let mut l = Loader::with_budget(&side, BUDGET);
635 let a = l.embedded("a", &huge_png(4096, 2048, [1, 1, 1])).unwrap();
636 assert_eq!((a.width, a.height), (4096, 2048));
637 let b = l.embedded("b", &huge_png(4096, 4096, [2, 2, 2])).unwrap();
638 assert_eq!((b.width, b.height), (2048, 2048));
639 assert_eq!(l.left(), BUDGET - (4096 * 2048 + 2048 * 2048) * 4);
640 }
641
642 #[test]
643 fn fitting_sizes() {
644 assert_eq!(fit_size(100, 50, 4096, BUDGET), Some((100, 50)));
645 assert_eq!(fit_size(10_000, 5_000, 4096, BUDGET), Some((4096, 2048)));
646 assert_eq!(fit_size(4096, 4096, 4096, BUDGET / 2), Some((2048, 2048)));
647 assert_eq!(fit_size(8, 8, 4096, 3), None);
648 assert_eq!(fit_size(0, 8, 4096, BUDGET), None);
649 }
650
651 #[test]
652 fn coordinates_repeat() {
653 assert_eq!(wrap_coord(1.25), 0.25);
654 assert_eq!(wrap_coord(-0.25), 0.75);
655 assert_eq!(wrap_coord(f32::NAN), 0.0);
656 }
657
658 fn close(a: [f32; 4], b: [f32; 4]) -> bool {
659 a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-4)
660 }
661
662 #[test]
663 fn sampling_is_bilinear_and_wraps() {
664 let t = Texture::new(image(2, 2, &[[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255, 255, 255, 255]]));
665 assert!(close(t.sample([0.25, 0.25]), [1.0, 0.0, 0.0, 1.0]));
667 assert!(close(t.sample([0.75, 0.75]), [1.0; 4]));
668 assert!(close(t.sample([0.25, 0.75]), [0.0, 0.0, 1.0, 1.0]), "v runs down the picture");
669 let mid = t.sample([0.5, 0.25]);
672 assert!((mid[0] - mid[1]).abs() < 1e-5 && mid[0] > 0.7 && mid[2] == 0.0, "{mid:?}");
673 assert!(close(t.sample([1.25, 0.25]), t.sample([0.25, 0.25])));
675 assert!(!close(t.sample([0.0, 0.0]), [1.0, 0.0, 0.0, 1.0]));
677 let a = Texture::new(image(2, 1, &[[255, 0, 0, 255], [0, 0, 0, 0]]));
679 assert!(close(a.sample([0.5, 0.5]), [1.0, 0.0, 0.0, 0.5]));
680 }
681
682 #[test]
683 fn modulating_multiplies_in_linear_light() {
684 assert_eq!(modulate([1.0; 3], [0.2, 0.4, 0.6, 1.0]).map(|c| (c * 1000.0).round()), [200.0, 400.0, 600.0]);
685 let half = modulate([0.5; 3], [0.5, 0.5, 0.5, 1.0]);
686 assert!(half[0] < 0.25 + 0.2 && half[0] > 0.2, "{half:?}");
687 }
688
689 #[test]
690 fn mipmaps_halve_to_one_pixel() {
691 let img = Image { name: String::new(), width: 4, height: 2, rgba: [[255u8, 0, 0, 255], [0, 0, 255, 255]].repeat(4).concat() };
692 let chain = mip_chain(&img);
693 assert_eq!(chain.iter().map(|l| (l.0, l.1)).collect::<Vec<_>>(), [(2, 1), (1, 1)]);
694 let p = &chain[1].2;
696 assert!(p[0] > 180 && p[0] == p[2] && p[1] == 0 && p[3] == 255, "{p:?}");
697 assert!(mip_chain(&Image { name: String::new(), width: 1, height: 1, rgba: vec![0; 4] }).is_empty());
698 }
699
700 #[test]
701 fn data_mipmaps_average_the_stored_values() {
702 let img = Image { name: String::new(), width: 2, height: 1, rgba: vec![0, 100, 200, 0, 100, 100, 100, 255] };
703 let chain = mip_chain_data(&img);
704 assert_eq!(chain.len(), 1);
705 assert_eq!(chain[0].2, [50, 100, 150, 128]);
707 assert!(mip_chain_data(&Image { name: String::new(), width: 1, height: 1, rgba: vec![0; 4] }).is_empty());
708 }
709
710 #[test]
711 fn data_sampling_keeps_the_stored_values() {
712 let t = Texture::new(image(2, 1, &[[0, 0, 0, 0], [255, 51, 255, 255]]));
713 let mid = t.sample_data([0.5, 0.5]);
714 assert!((mid[0] - 0.5).abs() < 1e-5 && (mid[1] - 0.1).abs() < 1e-5, "{mid:?}");
715 }
716
717 #[test]
718 fn maps_take_at_most_half_of_what_is_left() {
719 let side = |_: &str| None;
720 let mut l = Loader::with_budget(&side, 64 * 64 * 4);
721 let big = png(64, 64, [1, 2, 3, 255]);
722 let map = l.for_map(|l| l.embedded("n", &big)).unwrap();
723 assert_eq!((map.width, map.height), (32, 32), "halved to fit half the budget");
724 let rest = l.embedded("b", &png(64, 32, [4, 5, 6, 255])).unwrap();
726 assert_eq!((rest.width, rest.height), (64, 32));
727 assert_eq!(l.left(), 64 * 64 * 4 - 32 * 32 * 4 - 64 * 32 * 4);
728 }
729
730 #[test]
731 fn averages_weigh_alpha() {
732 let img = image(2, 1, &[[0, 0, 255, 255], [255, 0, 0, 0]]);
733 assert_eq!(img.average(), Some([0.0, 0.0, 1.0]));
734 assert_eq!(image(1, 1, &[[1, 2, 3, 0]]).average(), None);
735 }
736}