Refactor structure, implement layers, add Erode/Fill/layer panel

- Restructure folder hierarchy: max 4 .rs per dir using subfolders
  (ops/operations -> brush/ + shaping/, model/ -> world/, io/* -> subtypes, etc.)
- Fix golden_test.rs comment, extract BrushApply helper, add World::with_heightmap
- Implement 6 layer data generators (Caves, River, Frost, Trees, Biome, Resources)
  with noise-based (OpenSimplex) and terrain-derived algorithms
- Fix clippy warnings, dead code cleanup, import path fixes
- GUI: add Erode&Fill tool modes with radio buttons/sliders/apply_tool
- GUI: add Layers panel with 6 checkboxes + CPU viewport overlay rendering
This commit is contained in:
loki5512344 2026-06-24 12:27:55 +02:00
parent 3d3798136d
commit bdd4564240
104 changed files with 5184 additions and 3912 deletions

View file

@ -1,5 +1,4 @@
//! Chunk data structures for Minecraft Anvil format.
use std::collections::HashMap;
/// A parsed chunk from an Anvil (.mca) region file.
@ -27,7 +26,6 @@ pub struct ChunkSection {
pub block_light: Option<Vec<i8>>,
pub sky_light: Option<Vec<i8>>,
}
impl Chunk {
/// Parse a chunk from an NBT Compound tag.
pub fn from_nbt(tag: &terrafier_nbt::Tag) -> Option<Self> {
@ -86,37 +84,7 @@ impl Chunk {
raw,
})
}
/// Serialize this chunk back to an NBT Compound tag.
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
let mut compound = self.raw.clone();
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(self.x));
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(self.z));
compound.insert(
"DataVersion".into(),
terrafier_nbt::Tag::Int(self.data_version),
);
let sections_list: Vec<terrafier_nbt::Tag> =
self.sections.iter().map(|s| s.to_nbt()).collect();
compound.insert("sections".into(), terrafier_nbt::Tag::List(sections_list));
if let Some(status) = &self.status {
compound.insert("Status".into(), terrafier_nbt::Tag::String(status.clone()));
}
if !self.heightmaps.is_empty() {
compound.insert(
"Heightmaps".into(),
terrafier_nbt::Tag::Compound(self.heightmaps.clone()),
);
}
terrafier_nbt::Tag::Compound(compound)
}
}
impl ChunkSection {
/// Parse a section from an NBT Compound tag.
pub fn from_nbt(tag: &terrafier_nbt::Tag) -> Option<Self> {
@ -216,64 +184,7 @@ impl ChunkSection {
sky_light,
})
}
/// Serialize section back to NBT Compound.
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
let mut compound = HashMap::new();
compound.insert("Y".into(), terrafier_nbt::Tag::Byte(self.section_y));
// Block states
let palette_list: Vec<terrafier_nbt::Tag> = self
.palette
.iter()
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
.collect();
let mut block_states = HashMap::new();
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_list));
if !self.block_data.is_empty() {
block_states.insert(
"data".into(),
terrafier_nbt::Tag::LongArray(self.block_data.clone()),
);
}
compound.insert(
"block_states".into(),
terrafier_nbt::Tag::Compound(block_states),
);
// Biomes
let biome_palette_list: Vec<terrafier_nbt::Tag> = self
.biome_palette
.iter()
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
.collect();
let mut biomes = HashMap::new();
biomes.insert(
"palette".into(),
terrafier_nbt::Tag::List(biome_palette_list),
);
if !self.biome_data.is_empty() {
biomes.insert(
"data".into(),
terrafier_nbt::Tag::LongArray(self.biome_data.clone()),
);
}
compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
if let Some(bl) = &self.block_light {
compound.insert(
"BlockLight".into(),
terrafier_nbt::Tag::ByteArray(bl.clone()),
);
}
if let Some(sl) = &self.sky_light {
compound.insert("SkyLight".into(), terrafier_nbt::Tag::ByteArray(sl.clone()));
}
terrafier_nbt::Tag::Compound(compound)
}
}
fn get_int(map: &HashMap<String, terrafier_nbt::Tag>, key: &str) -> Option<i32> {
map.get(key).and_then(|t| match t {
terrafier_nbt::Tag::Int(v) => Some(*v),

View file

@ -0,0 +1,90 @@
use std::collections::HashMap;
use super::chunk::{Chunk, ChunkSection};
impl Chunk {
/// Serialize this chunk back to an NBT Compound tag.
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
let mut compound = self.raw.clone();
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(self.x));
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(self.z));
compound.insert(
"DataVersion".into(),
terrafier_nbt::Tag::Int(self.data_version),
);
let sections_list: Vec<terrafier_nbt::Tag> =
self.sections.iter().map(|s| s.to_nbt()).collect();
compound.insert("sections".into(), terrafier_nbt::Tag::List(sections_list));
if let Some(status) = &self.status {
compound.insert("Status".into(), terrafier_nbt::Tag::String(status.clone()));
}
if !self.heightmaps.is_empty() {
compound.insert(
"Heightmaps".into(),
terrafier_nbt::Tag::Compound(self.heightmaps.clone()),
);
}
terrafier_nbt::Tag::Compound(compound)
}
}
impl ChunkSection {
/// Serialize section back to NBT Compound.
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
let mut compound = HashMap::new();
compound.insert("Y".into(), terrafier_nbt::Tag::Byte(self.section_y));
let palette_list: Vec<terrafier_nbt::Tag> = self
.palette
.iter()
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
.collect();
let mut block_states = HashMap::new();
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_list));
if !self.block_data.is_empty() {
block_states.insert(
"data".into(),
terrafier_nbt::Tag::LongArray(self.block_data.clone()),
);
}
compound.insert(
"block_states".into(),
terrafier_nbt::Tag::Compound(block_states),
);
let biome_palette_list: Vec<terrafier_nbt::Tag> = self
.biome_palette
.iter()
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
.collect();
let mut biomes = HashMap::new();
biomes.insert(
"palette".into(),
terrafier_nbt::Tag::List(biome_palette_list),
);
if !self.biome_data.is_empty() {
biomes.insert(
"data".into(),
terrafier_nbt::Tag::LongArray(self.biome_data.clone()),
);
}
compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
if let Some(bl) = &self.block_light {
compound.insert(
"BlockLight".into(),
terrafier_nbt::Tag::ByteArray(bl.clone()),
);
}
if let Some(sl) = &self.sky_light {
compound.insert("SkyLight".into(), terrafier_nbt::Tag::ByteArray(sl.clone()));
}
terrafier_nbt::Tag::Compound(compound)
}
}

View file

@ -0,0 +1,4 @@
mod chunk;
mod chunk_write;
pub use chunk::*;

View file

@ -1,4 +1,2 @@
//! Anvil format I/O — region file and chunk data structures.
pub mod chunk;
pub mod region;

View file

@ -0,0 +1,4 @@
mod region;
mod region_write;
pub use region::*;

View file

@ -28,7 +28,7 @@ pub type Result<T> = std::result::Result<T, RegionError>;
pub struct Region {
pub x: i32,
pub z: i32,
chunks: HashMap<(u8, u8), ChunkEntry>,
pub(crate) chunks: HashMap<(u8, u8), ChunkEntry>,
}
pub struct ChunkEntry {
@ -163,68 +163,5 @@ impl Region {
self.chunks.len()
}
/// Serialize region back to .mca bytes.
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let sector_size: u64 = 4096;
let mut locations = [0u32; 1024];
let mut timestamps = [0u32; 1024];
let mut sector_data: Vec<Vec<u8>> = Vec::new();
for i in 0..1024 {
let local_x = (i % 32) as u8;
let local_z = (i / 32) as u8;
if let Some(entry) = self.chunks.get(&(local_x, local_z)) {
timestamps[i] = entry.timestamp;
// Serialize NBT and compress with Zlib
let compressed = compression::compress(
entry.data.as_deref().unwrap_or_default(),
CompressionType::Zlib,
)?;
// Prepend: length (4 bytes BE) + compression type (1 byte)
let total_len = 1 + compressed.len();
let mut sector = Vec::with_capacity(4 + total_len);
sector.extend(&(total_len as u32).to_be_bytes());
sector.push(CompressionType::Zlib.id());
sector.extend(&compressed);
// Pad to sector boundary
while sector.len() % sector_size as usize != 0 {
sector.push(0);
}
let offset = 2 + sector_data.len() as u32;
locations[i] =
(offset << 8) | ((sector.len() / sector_size as usize) as u32 & 0xFF);
sector_data.push(sector);
}
}
// Build output: header + sector data
let mut output =
Vec::with_capacity(2 * 4096 + sector_data.iter().map(|s| s.len()).sum::<usize>());
// Location table
for loc in locations.iter() {
output.extend(&loc.to_be_bytes());
}
// Timestamp table
for ts in timestamps.iter() {
output.extend(&ts.to_be_bytes());
}
// Pad header to exactly 2 sectors
while output.len() < 2 * sector_size as usize {
output.push(0);
}
// Sector data
for sector in &sector_data {
output.extend(sector);
}
Ok(output)
}
}

View file

@ -0,0 +1,62 @@
use crate::compression::{self, CompressionType};
use super::region::{Region, Result};
impl Region {
/// Serialize region back to .mca bytes.
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let sector_size: u64 = 4096;
let mut locations = [0u32; 1024];
let mut timestamps = [0u32; 1024];
let mut sector_data: Vec<Vec<u8>> = Vec::new();
for i in 0..1024 {
let local_x = (i % 32) as u8;
let local_z = (i / 32) as u8;
if let Some(entry) = self.chunks.get(&(local_x, local_z)) {
timestamps[i] = entry.timestamp;
let compressed = compression::compress(
entry.data.as_deref().unwrap_or_default(),
CompressionType::Zlib,
)?;
let total_len = 1 + compressed.len();
let mut sector = Vec::with_capacity(4 + total_len);
sector.extend(&(total_len as u32).to_be_bytes());
sector.push(CompressionType::Zlib.id());
sector.extend(&compressed);
while sector.len() % sector_size as usize != 0 {
sector.push(0);
}
let offset = 2 + sector_data.len() as u32;
locations[i] =
(offset << 8) | ((sector.len() / sector_size as usize) as u32 & 0xFF);
sector_data.push(sector);
}
}
let mut output =
Vec::with_capacity(2 * 4096 + sector_data.iter().map(|s| s.len()).sum::<usize>());
for loc in locations.iter() {
output.extend(&loc.to_be_bytes());
}
for ts in timestamps.iter() {
output.extend(&ts.to_be_bytes());
}
while output.len() < 2 * sector_size as usize {
output.push(0);
}
for sector in &sector_data {
output.extend(sector);
}
Ok(output)
}
}