use super::{ChunkData, StrokeData, TerrainTextureKind};
use crate::core::algebra::Vector2;
use crate::fxhash::{FxHashMap, FxHashSet};
use crate::resource::texture::TextureResource;
use crate::scene::terrain::pixel_position_to_grid_position;
#[derive(Debug, Default)]
pub struct StrokeChunks {
chunk_size: Vector2<u32>,
kind: TerrainTextureKind,
written_pixels: FxHashMap<Vector2<i32>, FxHashSet<Vector2<u32>>>,
count: usize,
unused_chunks: Vec<FxHashSet<Vector2<u32>>>,
}
impl StrokeChunks {
#[inline]
pub fn count(&self) -> usize {
self.count
}
#[inline]
pub fn kind(&self) -> TerrainTextureKind {
self.kind
}
pub fn clear(&mut self) {
self.count = 0;
for mut c in self.written_pixels.drain().map(|(_, v)| v) {
c.clear();
self.unused_chunks.push(c);
}
}
pub fn set_layout(&mut self, kind: TerrainTextureKind, size: Vector2<u32>) {
self.kind = kind;
self.chunk_size = match kind {
TerrainTextureKind::Height => size.map(|x| x - 3),
TerrainTextureKind::Mask => size,
};
}
pub fn copy_texture_data(
&self,
textures: &FxHashMap<Vector2<i32>, TextureResource>,
saved_chunk_data: &mut Vec<ChunkData>,
) {
for (c, _) in self.written_pixels.iter() {
if saved_chunk_data.iter().any(|x| x.grid_position == *c) {
continue;
}
let Some(texture) = textures.get(c) else {
continue;
};
saved_chunk_data.push(ChunkData::from_texture(*c, texture));
}
}
pub fn apply<V>(
&self,
stroke: &StrokeData<V>,
textures: &FxHashMap<Vector2<i32>, TextureResource>,
) where
V: Clone,
{
for (c, pxs) in self.written_pixels.iter() {
let Some(texture) = textures.get(c) else {
continue;
};
let mut texture_data = texture.data_ref();
let mut modify = texture_data.modify();
let Some(data) = modify.data_mut_of_type::<V>() else {
continue;
};
let origin = self.chunk_to_origin(*c);
let row_size = self.row_size();
for p in pxs.iter() {
let position = match self.kind {
TerrainTextureKind::Mask => origin + p.map(|x| x as i32),
TerrainTextureKind::Height => origin + p.map(|x| x as i32 - 1),
};
let Some(value) = stroke.latest_pixel_value(position) else {
continue;
};
let index = p.x as usize + p.y as usize * row_size;
data[index].clone_from(value);
}
}
}
#[inline]
pub fn pixel_position_to_grid_position(&self, position: Vector2<i32>) -> Vector2<i32> {
pixel_position_to_grid_position(position, self.chunk_size)
}
pub fn chunk_to_origin(&self, grid_position: Vector2<i32>) -> Vector2<i32> {
Vector2::new(
grid_position.x * self.chunk_size.x as i32,
grid_position.y * self.chunk_size.y as i32,
)
}
pub fn row_size(&self) -> usize {
match self.kind {
TerrainTextureKind::Height => (self.chunk_size.x + 3) as usize,
TerrainTextureKind::Mask => self.chunk_size.x as usize,
}
}
pub fn pixel_index(&self, position: Vector2<i32>) -> usize {
if !self.is_valid_pixel(position) {
panic!(
"Invalid pixel position: ({}, {}) within ({}, {})",
position.x, position.y, self.chunk_size.x, self.chunk_size.y
);
}
let p = match self.kind {
TerrainTextureKind::Height => position.map(|x| (x + 1) as usize),
TerrainTextureKind::Mask => position.map(|x| x as usize),
};
p.x + p.y * self.row_size()
}
pub fn is_valid_pixel(&self, position: Vector2<i32>) -> bool {
let size = self.chunk_size.map(|x| x as i32);
match self.kind {
TerrainTextureKind::Height => {
(-1..=size.x + 1).contains(&position.x) && (-1..=size.y + 1).contains(&position.y)
}
TerrainTextureKind::Mask => {
(0..size.x).contains(&position.x) && (0..size.y).contains(&position.x)
}
}
}
pub fn write(&mut self, position: Vector2<i32>) {
let grid_pos = self.pixel_position_to_grid_position(position);
self.count += 1;
match self.kind {
TerrainTextureKind::Height => {
for x in grid_pos.x - 1..=grid_pos.x + 1 {
for y in grid_pos.y - 1..=grid_pos.y + 1 {
self.write_height(Vector2::new(x, y), position);
}
}
}
TerrainTextureKind::Mask => {
let origin = self.chunk_to_origin(grid_pos);
let pos = (position - origin).map(|x| x as u32);
self.write_to_chunk(grid_pos, pos);
}
}
}
fn write_height(&mut self, grid_pos: Vector2<i32>, position: Vector2<i32>) {
let origin = self.chunk_to_origin(grid_pos);
let pos = position - origin;
let size = self.chunk_size;
let (w, h) = (size.x as i32, size.y as i32);
if (-1..=w + 1).contains(&pos.x) && (-1..=h + 1).contains(&pos.y) {
self.write_to_chunk(grid_pos, pos.map(|x| (x + 1) as u32));
}
}
fn write_to_chunk(&mut self, grid_pos: Vector2<i32>, position: Vector2<u32>) {
let mut unused = std::mem::take(&mut self.unused_chunks);
self.written_pixels
.entry(grid_pos)
.or_insert_with(|| unused.pop().unwrap_or_default())
.insert(position);
self.unused_chunks = unused;
}
}
#[cfg(test)]
mod tests {
use super::*;
const RANDOM_POINTS: &[(i32, i32)] = &[
(0, 0),
(1, 1),
(2, 2),
(-1, -1),
(20, -123),
(-11, 22),
(42, 285),
(360, -180),
(123, -456),
(54, 32),
(-2, -3),
];
#[test]
fn chunk_to_origin() {
let mut chunks = StrokeChunks::default();
chunks.set_layout(TerrainTextureKind::Height, Vector2::new(7, 7));
assert_eq!(
chunks.chunk_to_origin(Vector2::new(0, 0)),
Vector2::new(0, 0)
);
assert_eq!(
chunks.chunk_to_origin(Vector2::new(1, 0)),
Vector2::new(4, 0)
);
assert_eq!(
chunks.chunk_to_origin(Vector2::new(-2, -1)),
Vector2::new(-8, -4)
);
}
#[test]
fn pixel_position_to_grid_position() {
let mut chunks = StrokeChunks::default();
chunks.set_layout(TerrainTextureKind::Height, Vector2::new(7, 7));
assert_eq!(
chunks.pixel_position_to_grid_position(Vector2::new(0, 0)),
Vector2::new(0, 0)
);
assert_eq!(
chunks.pixel_position_to_grid_position(Vector2::new(2, 3)),
Vector2::new(0, 0)
);
assert_eq!(
chunks.pixel_position_to_grid_position(Vector2::new(-1, -1)),
Vector2::new(-1, -1)
);
assert_eq!(
chunks.pixel_position_to_grid_position(Vector2::new(4, -4)),
Vector2::new(1, -1)
);
}
fn test_points_height(size: Vector2<u32>) {
let mut chunks = StrokeChunks::default();
chunks.set_layout(TerrainTextureKind::Height, size);
for p in RANDOM_POINTS.iter() {
let p = Vector2::new(p.0, p.1);
let grid_pos = chunks.pixel_position_to_grid_position(p);
let origin = chunks.chunk_to_origin(grid_pos);
let pixel = p - origin;
test_point(p, pixel, size.map(|x| x - 1));
}
let s = size.map(|x| x as i32);
for x in -s.x..=s.x * 2 {
for y in -s.y..=s.y * 2 {
let p = Vector2::new(x, y);
let grid_pos = chunks.pixel_position_to_grid_position(p);
let origin = chunks.chunk_to_origin(grid_pos);
let pixel = p - origin;
test_point(p, pixel, size.map(|x| x - 1));
}
}
}
fn test_points_mask(size: Vector2<u32>) {
let mut chunks = StrokeChunks::default();
chunks.set_layout(TerrainTextureKind::Mask, size);
for p in RANDOM_POINTS.iter() {
let p = Vector2::new(p.0, p.1);
let grid_pos = chunks.pixel_position_to_grid_position(p);
let origin = chunks.chunk_to_origin(grid_pos);
let pixel = p - origin;
test_point(p, pixel, size);
}
let s = size.map(|x| x as i32);
for x in -s.x..=s.x * 2 {
for y in -s.y..=s.y * 2 {
let p = Vector2::new(x, y);
let grid_pos = chunks.pixel_position_to_grid_position(p);
let origin = chunks.chunk_to_origin(grid_pos);
let pixel = p - origin;
test_point(p, pixel, size);
}
}
}
fn test_point(p: Vector2<i32>, pixel: Vector2<i32>, size: Vector2<u32>) {
assert!(
pixel.x >= 0,
"({}, {}) -> ({}, {})",
p.x,
p.y,
pixel.x,
pixel.y
);
assert!(
pixel.y >= 0,
"({}, {}) -> ({}, {})",
p.x,
p.y,
pixel.x,
pixel.y
);
assert!(
pixel.x < size.x as i32,
"({}, {}) -> ({}, {})",
p.x,
p.y,
pixel.x,
pixel.y
);
assert!(
pixel.y < size.y as i32,
"({}, {}) -> ({}, {})",
p.x,
p.y,
pixel.x,
pixel.y
);
}
#[test]
fn random_points_5x5() {
test_points_height(Vector2::new(5, 5));
test_points_mask(Vector2::new(5, 5));
}
#[test]
fn random_points_10x10() {
test_points_height(Vector2::new(10, 10));
test_points_mask(Vector2::new(10, 10));
}
#[test]
fn random_points_257x257() {
test_points_height(Vector2::new(257, 257));
test_points_mask(Vector2::new(257, 257));
}
}