use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct RasterBuf {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
impl RasterBuf {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
pixels: vec![0; (width * height * 4) as usize],
}
}
pub fn filled(width: u32, height: u32, rgba: [u8; 4]) -> Self {
let mut s = Self::new(width, height);
for px in s.pixels.chunks_exact_mut(4) {
px.copy_from_slice(&rgba);
}
s
}
pub fn is_blank(&self) -> bool {
let (head, mid, tail) = unsafe { self.pixels.align_to::<u128>() };
head.iter().all(|&b| b == 0) && mid.iter().all(|&w| w == 0) && tail.iter().all(|&b| b == 0)
}
pub fn blank_shared(width: u32, height: u32) -> Arc<RasterBuf> {
let mut pool = blank_pool().lock().unwrap_or_else(|e| e.into_inner());
Arc::clone(
pool.entry((width, height))
.or_insert_with(|| Arc::new(RasterBuf::new(width, height))),
)
}
pub fn is_interned_blank(buf: &Arc<RasterBuf>) -> bool {
let pool = blank_pool().lock().unwrap_or_else(|e| e.into_inner());
pool.get(&(buf.width, buf.height))
.is_some_and(|shared| Arc::ptr_eq(shared, buf))
}
pub fn pixel(&self, x: u32, y: u32) -> [u8; 4] {
let i = ((y * self.width + x) * 4) as usize;
[
self.pixels[i],
self.pixels[i + 1],
self.pixels[i + 2],
self.pixels[i + 3],
]
}
}
type BlankPool = std::sync::Mutex<HashMap<(u32, u32), Arc<RasterBuf>>>;
fn blank_pool() -> &'static BlankPool {
static POOL: std::sync::OnceLock<BlankPool> = std::sync::OnceLock::new();
POOL.get_or_init(|| std::sync::Mutex::new(HashMap::new()))
}
#[derive(Debug, Clone, Default)]
pub struct SpriteRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub pixel_ratio: f32,
pub stretch_x: Vec<[u32; 2]>,
pub stretch_y: Vec<[u32; 2]>,
pub content: Option<[u32; 4]>,
}
#[derive(Debug)]
pub struct SpriteSheet {
pub atlas: RasterBuf,
pub icons: HashMap<String, SpriteRect>,
}
impl SpriteSheet {
pub fn crop(&self, name: &str) -> Option<RasterBuf> {
let r = self.icons.get(name)?;
if r.width == 0
|| r.height == 0
|| r.x + r.width > self.atlas.width
|| r.y + r.height > self.atlas.height
{
return None;
}
let mut out = RasterBuf::new(r.width, r.height);
let aw = self.atlas.width as usize;
for row in 0..r.height {
let src = (((r.y + row) as usize * aw) + r.x as usize) * 4;
let dst = (row as usize * r.width as usize) * 4;
let n = r.width as usize * 4;
out.pixels[dst..dst + n].copy_from_slice(&self.atlas.pixels[src..src + n]);
}
Some(out)
}
}
pub type OpaqueValue = Arc<dyn Any + Send + Sync>;
#[derive(Debug, Clone)]
pub struct ScalarField {
pub width: u32,
pub height: u32,
pub values: Arc<[f32]>,
pub nodata: Option<f32>,
pub geo_scale: Option<GeoScale>,
}
#[derive(Debug, Clone, Copy)]
pub struct GeoScale {
pub metres_per_pixel_x: f32,
pub metres_per_pixel_y: f32,
}
impl ScalarField {
pub fn sample(&self, x: u32, y: u32) -> f32 {
self.values[(y * self.width + x) as usize]
}
pub fn metres_per_pixel_x(&self) -> f32 {
self.geo_scale.map(|g| g.metres_per_pixel_x).unwrap_or(1.0)
}
pub fn metres_per_pixel_y(&self) -> f32 {
self.geo_scale.map(|g| g.metres_per_pixel_y).unwrap_or(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sprite_crop_extracts_named_rect() {
let mut atlas = RasterBuf::new(4, 2);
for y in 0..2 {
for x in 0..4 {
let i = ((y * 4 + x) * 4) as usize;
let c = if x < 2 {
[255, 0, 0, 255]
} else {
[0, 255, 0, 255]
};
atlas.pixels[i..i + 4].copy_from_slice(&c);
}
}
let mut icons = HashMap::new();
icons.insert(
"left".to_string(),
SpriteRect {
x: 0,
y: 0,
width: 2,
height: 2,
pixel_ratio: 1.0,
..SpriteRect::default()
},
);
icons.insert(
"right".to_string(),
SpriteRect {
x: 2,
y: 0,
width: 2,
height: 2,
pixel_ratio: 1.0,
..SpriteRect::default()
},
);
icons.insert(
"oob".to_string(),
SpriteRect {
x: 3,
y: 0,
width: 2,
height: 2,
pixel_ratio: 1.0,
..SpriteRect::default()
},
);
let sheet = SpriteSheet { atlas, icons };
let right = sheet.crop("right").expect("named icon");
assert_eq!((right.width, right.height), (2, 2));
assert!(right.pixels.chunks_exact(4).all(|p| p == [0, 255, 0, 255]));
let left = sheet.crop("left").unwrap();
assert!(left.pixels.chunks_exact(4).all(|p| p == [255, 0, 0, 255]));
assert!(sheet.crop("missing").is_none());
assert!(sheet.crop("oob").is_none());
}
}