Add operations (erode, fill, flood, pencil), filter system, layer system with exporters, and WorldPainter .world import
- New operations: ErodeOperation (thermal erosion), FillOperation (flood-fill), FloodOperation (hard replace), PencilOperation (single-point) - Filter system: Filter trait with TerrainFilter, HeightFilter, SlopeFilter, CompositeFilter, InvertFilter, FilteredOperation wrapper - Layer system: Layer trait + 6 implementations (Caves, River, Frost, Trees, Biome, Resources) + layer data helpers on Tile - Layer exporters: LayerExport trait + 6 implementations that modify blocks during export (caves carve air, rivers create water channels, frost adds snow/ice, trees place logs, resources add ores, biomes set biome palette) - WorldPainter .world import: Java serialization stream parser with full TC_* token support, tile extraction, format auto-detection
This commit is contained in:
parent
2b2f968d70
commit
fa05187f1c
16 changed files with 2218 additions and 13 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3571,6 +3571,7 @@ name = "terrafier-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"flate2",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
serde-big-array = "0.5"
|
serde-big-array = "0.5"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
|
flate2 = "1.1"
|
||||||
|
|
||||||
[workspace.metadata.dist]
|
[workspace.metadata.dist]
|
||||||
# Инициализируется при первом запуске cargo dist init
|
# Инициализируется при первом запуске cargo dist init
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ serde-big-array.workspace = true
|
||||||
bincode.workspace = true
|
bincode.workspace = true
|
||||||
|
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
|
flate2.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::io::layer_export::LayerExport;
|
||||||
use crate::io::minecraft::MinecraftIOError;
|
use crate::io::minecraft::MinecraftIOError;
|
||||||
use crate::model::world::World;
|
use crate::model::world::World;
|
||||||
|
|
||||||
|
|
@ -26,6 +27,16 @@ pub fn export_to_save(world: &World, output_path: &Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Export a Terrafier World to a Minecraft save directory, applying layer exporters.
|
||||||
|
pub fn export_to_save_with_layers(
|
||||||
|
world: &World,
|
||||||
|
output_path: &Path,
|
||||||
|
layer_exporters: &[&dyn LayerExport],
|
||||||
|
) -> Result<()> {
|
||||||
|
crate::io::minecraft::save_world_with_layers(world, output_path, layer_exporters)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Render a Terrafier world to a PNG image (top-down view).
|
/// Render a Terrafier world to a PNG image (top-down view).
|
||||||
pub fn render_to_image(world: &World, output_path: &Path, scale: u32) -> Result<()> {
|
pub fn render_to_image(world: &World, output_path: &Path, scale: u32) -> Result<()> {
|
||||||
// Determine bounds from tiles
|
// Determine bounds from tiles
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use std::path::Path;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::io::minecraft::MinecraftIOError;
|
use crate::io::minecraft::MinecraftIOError;
|
||||||
|
use crate::io::wp_import;
|
||||||
use crate::model::world::World;
|
use crate::model::world::World;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
@ -72,5 +73,10 @@ pub fn import(path: &Path) -> Result<World> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wp_import::is_world_file(path) {
|
||||||
|
return wp_import::import_world_file(path)
|
||||||
|
.map_err(|e| ImportError::UnsupportedFormat(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
Err(ImportError::UnsupportedFormat(path_str))
|
Err(ImportError::UnsupportedFormat(path_str))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
313
core/src/io/layer_export.rs
Normal file
313
core/src/io/layer_export.rs
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
//! Layer export system — applies layer data to modify blocks during Minecraft export.
|
||||||
|
|
||||||
|
use crate::coords::TILE_SIZE;
|
||||||
|
use crate::model::layers::{
|
||||||
|
LAYER_BIOME, LAYER_CAVES, LAYER_FROST, LAYER_RESOURCES, LAYER_RIVER, LAYER_TREES,
|
||||||
|
};
|
||||||
|
use crate::model::tile::{LayerBuffer, Tile};
|
||||||
|
|
||||||
|
/// A layer exporter knows how to modify blocks based on layer data.
|
||||||
|
pub trait LayerExport: Send + Sync {
|
||||||
|
fn layer_id(&self) -> u32;
|
||||||
|
fn modify_block(
|
||||||
|
&self,
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
y: i32,
|
||||||
|
surface_y: i32,
|
||||||
|
block_name: &str,
|
||||||
|
) -> Option<&'static str>;
|
||||||
|
|
||||||
|
/// Optional biome override for this position.
|
||||||
|
fn biome_name(&self, tile: &Tile, local_x: usize, local_z: usize) -> Option<&'static str> {
|
||||||
|
let _ = (tile, local_x, local_z);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply all layer exporters in order. The first exporter that returns a replacement wins.
|
||||||
|
pub fn apply_layers<'a>(
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
y: i32,
|
||||||
|
surface_y: i32,
|
||||||
|
block_name: &'a str,
|
||||||
|
layer_exporters: &[&dyn LayerExport],
|
||||||
|
) -> &'a str {
|
||||||
|
for exporter in layer_exporters {
|
||||||
|
if let Some(replacement) =
|
||||||
|
exporter.modify_block(tile, local_x, local_z, y, surface_y, block_name)
|
||||||
|
{
|
||||||
|
return replacement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block_name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the biome name from layer exporters, falling back to plains.
|
||||||
|
pub fn biome_name(
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
layer_exporters: &[&dyn LayerExport],
|
||||||
|
) -> &'static str {
|
||||||
|
for exporter in layer_exporters {
|
||||||
|
if let Some(name) = exporter.biome_name(tile, local_x, local_z) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"minecraft:plains"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Built-in layer exporter implementations
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct CavesLayerExport;
|
||||||
|
impl LayerExport for CavesLayerExport {
|
||||||
|
fn layer_id(&self) -> u32 {
|
||||||
|
LAYER_CAVES
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_block(
|
||||||
|
&self,
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
y: i32,
|
||||||
|
surface_y: i32,
|
||||||
|
block_name: &str,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let idx = local_z * TILE_SIZE + local_x;
|
||||||
|
let data = tile.get_layer_data(LAYER_CAVES)?;
|
||||||
|
if let LayerBuffer::Nibble(nibbles) = data {
|
||||||
|
let byte = nibbles[idx / 2];
|
||||||
|
let value = if idx % 2 == 0 { byte >> 4 } else { byte & 0xF };
|
||||||
|
if value > 0
|
||||||
|
&& y < surface_y
|
||||||
|
&& y >= surface_y - 12
|
||||||
|
&& y > -60
|
||||||
|
{
|
||||||
|
match block_name {
|
||||||
|
"minecraft:stone"
|
||||||
|
| "minecraft:deepslate"
|
||||||
|
| "minecraft:dirt"
|
||||||
|
| "minecraft:grass_block"
|
||||||
|
| "minecraft:sand"
|
||||||
|
| "minecraft:sandstone"
|
||||||
|
| "minecraft:gravel" => Some("minecraft:air"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RiverLayerExport;
|
||||||
|
impl LayerExport for RiverLayerExport {
|
||||||
|
fn layer_id(&self) -> u32 {
|
||||||
|
LAYER_RIVER
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_block(
|
||||||
|
&self,
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
y: i32,
|
||||||
|
surface_y: i32,
|
||||||
|
block_name: &str,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let idx = local_z * TILE_SIZE + local_x;
|
||||||
|
let data = tile.get_layer_data(LAYER_RIVER)?;
|
||||||
|
if let LayerBuffer::Byte(bytes) = data {
|
||||||
|
let value = bytes[idx];
|
||||||
|
if value > 0 {
|
||||||
|
let depth = value as i32;
|
||||||
|
if y <= surface_y && y > surface_y - depth {
|
||||||
|
if y >= surface_y - 1 {
|
||||||
|
Some("minecraft:water")
|
||||||
|
} else {
|
||||||
|
match block_name {
|
||||||
|
"minecraft:stone"
|
||||||
|
| "minecraft:deepslate"
|
||||||
|
| "minecraft:dirt"
|
||||||
|
| "minecraft:grass_block"
|
||||||
|
| "minecraft:sand"
|
||||||
|
| "minecraft:sandstone"
|
||||||
|
| "minecraft:gravel" => Some("minecraft:air"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FrostLayerExport;
|
||||||
|
impl LayerExport for FrostLayerExport {
|
||||||
|
fn layer_id(&self) -> u32 {
|
||||||
|
LAYER_FROST
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_block(
|
||||||
|
&self,
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
y: i32,
|
||||||
|
surface_y: i32,
|
||||||
|
_block_name: &str,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let idx = local_z * TILE_SIZE + local_x;
|
||||||
|
let data = tile.get_layer_data(LAYER_FROST)?;
|
||||||
|
if let LayerBuffer::Bit(bits) = data {
|
||||||
|
let word = bits[idx / 64];
|
||||||
|
let bit = (word >> (idx % 64)) & 1;
|
||||||
|
if bit == 1 && y == surface_y {
|
||||||
|
if _block_name == "minecraft:water" {
|
||||||
|
Some("minecraft:ice")
|
||||||
|
} else if _block_name == "minecraft:grass_block" {
|
||||||
|
Some("minecraft:snow_block")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TreesLayerExport;
|
||||||
|
impl LayerExport for TreesLayerExport {
|
||||||
|
fn layer_id(&self) -> u32 {
|
||||||
|
LAYER_TREES
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_block(
|
||||||
|
&self,
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
y: i32,
|
||||||
|
surface_y: i32,
|
||||||
|
block_name: &str,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let idx = local_z * TILE_SIZE + local_x;
|
||||||
|
let data = tile.get_layer_data(LAYER_TREES)?;
|
||||||
|
if let LayerBuffer::Byte(bytes) = data {
|
||||||
|
let value = bytes[idx];
|
||||||
|
if value > 0
|
||||||
|
&& block_name == "minecraft:air"
|
||||||
|
&& y > surface_y
|
||||||
|
&& y <= surface_y + value as i32
|
||||||
|
{
|
||||||
|
Some("minecraft:oak_log")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResourcesLayerExport;
|
||||||
|
impl LayerExport for ResourcesLayerExport {
|
||||||
|
fn layer_id(&self) -> u32 {
|
||||||
|
LAYER_RESOURCES
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_block(
|
||||||
|
&self,
|
||||||
|
tile: &Tile,
|
||||||
|
local_x: usize,
|
||||||
|
local_z: usize,
|
||||||
|
y: i32,
|
||||||
|
_surface_y: i32,
|
||||||
|
block_name: &str,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let idx = local_z * TILE_SIZE + local_x;
|
||||||
|
let data = tile.get_layer_data(LAYER_RESOURCES)?;
|
||||||
|
if let LayerBuffer::Nibble(nibbles) = data {
|
||||||
|
let byte = nibbles[idx / 2];
|
||||||
|
let value = if idx % 2 == 0 { byte >> 4 } else { byte & 0xF };
|
||||||
|
if value > 0 && block_name == "minecraft:stone" && y > -60 {
|
||||||
|
let ore = match value {
|
||||||
|
1 => "minecraft:coal_ore",
|
||||||
|
2 => "minecraft:iron_ore",
|
||||||
|
3 => "minecraft:copper_ore",
|
||||||
|
4 => "minecraft:gold_ore",
|
||||||
|
5 => "minecraft:redstone_ore",
|
||||||
|
6 => "minecraft:lapis_ore",
|
||||||
|
7 => "minecraft:diamond_ore",
|
||||||
|
8 => "minecraft:emerald_ore",
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(ore)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BiomeLayerExport;
|
||||||
|
impl LayerExport for BiomeLayerExport {
|
||||||
|
fn layer_id(&self) -> u32 {
|
||||||
|
LAYER_BIOME
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_block(
|
||||||
|
&self,
|
||||||
|
_tile: &Tile,
|
||||||
|
_local_x: usize,
|
||||||
|
_local_z: usize,
|
||||||
|
_y: i32,
|
||||||
|
_surface_y: i32,
|
||||||
|
_block_name: &str,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn biome_name(&self, tile: &Tile, local_x: usize, local_z: usize) -> Option<&'static str> {
|
||||||
|
let idx = local_z * TILE_SIZE + local_x;
|
||||||
|
let data = tile.get_layer_data(LAYER_BIOME)?;
|
||||||
|
if let LayerBuffer::Byte(bytes) = data {
|
||||||
|
Some(match bytes[idx] {
|
||||||
|
0 => "minecraft:plains",
|
||||||
|
1 => "minecraft:desert",
|
||||||
|
2 => "minecraft:forest",
|
||||||
|
3 => "minecraft:taiga",
|
||||||
|
4 => "minecraft:swamp",
|
||||||
|
5 => "minecraft:snowy_plains",
|
||||||
|
6 => "minecraft:jungle",
|
||||||
|
7 => "minecraft:badlands",
|
||||||
|
8 => "minecraft:ocean",
|
||||||
|
9 => "minecraft:river",
|
||||||
|
10 => "minecraft:mushroom_fields",
|
||||||
|
_ => "minecraft:plains",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ use terrafier_nbt::io::reader::read_gzip;
|
||||||
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::platform::Platform;
|
use crate::model::platform::Platform;
|
||||||
|
use crate::io::layer_export::{apply_layers, biome_name, LayerExport};
|
||||||
use crate::model::tile::Tile;
|
use crate::model::tile::Tile;
|
||||||
use crate::model::world::World;
|
use crate::model::world::World;
|
||||||
|
|
||||||
|
|
@ -496,6 +497,199 @@ fn build_chunk_nbt(
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a single chunk's NBT data, applying layer exporters for block/biome overrides.
|
||||||
|
fn build_chunk_nbt_with_layers(
|
||||||
|
chunk_x: i32,
|
||||||
|
chunk_z: i32,
|
||||||
|
tile: &Tile,
|
||||||
|
chunk_lx: usize,
|
||||||
|
chunk_lz: usize,
|
||||||
|
layer_exporters: &[&dyn LayerExport],
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let mut compound = HashMap::new();
|
||||||
|
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(chunk_x));
|
||||||
|
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(chunk_z));
|
||||||
|
compound.insert("DataVersion".into(), terrafier_nbt::Tag::Int(3954));
|
||||||
|
compound.insert("Status".into(), terrafier_nbt::Tag::String("full".into()));
|
||||||
|
|
||||||
|
const BLOCK_SET: &[&str] = &[
|
||||||
|
"minecraft:air",
|
||||||
|
"minecraft:grass_block",
|
||||||
|
"minecraft:dirt",
|
||||||
|
"minecraft:stone",
|
||||||
|
"minecraft:bedrock",
|
||||||
|
"minecraft:sand",
|
||||||
|
"minecraft:sandstone",
|
||||||
|
"minecraft:water",
|
||||||
|
"minecraft:deepslate",
|
||||||
|
"minecraft:snow_block",
|
||||||
|
"minecraft:ice",
|
||||||
|
"minecraft:oak_log",
|
||||||
|
"minecraft:coal_ore",
|
||||||
|
"minecraft:iron_ore",
|
||||||
|
"minecraft:copper_ore",
|
||||||
|
"minecraft:gold_ore",
|
||||||
|
"minecraft:redstone_ore",
|
||||||
|
"minecraft:lapis_ore",
|
||||||
|
"minecraft:diamond_ore",
|
||||||
|
"minecraft:emerald_ore",
|
||||||
|
];
|
||||||
|
|
||||||
|
let min_sec = (tile.min_height as i32 >> 4).max(-4);
|
||||||
|
let max_sec = (tile.max_height as i32 >> 4).min(20);
|
||||||
|
|
||||||
|
let mut sections: Vec<terrafier_nbt::Tag> = Vec::new();
|
||||||
|
|
||||||
|
for sec_y in min_sec..=max_sec {
|
||||||
|
let sec_base = sec_y * 16;
|
||||||
|
|
||||||
|
let mut indices = Vec::with_capacity(4096);
|
||||||
|
for y_rel in 0..16 {
|
||||||
|
let global_y = sec_base + y_rel;
|
||||||
|
for lz in 0..16 {
|
||||||
|
for lx in 0..16 {
|
||||||
|
let tile_lx = chunk_lx * 16 + lx;
|
||||||
|
let tile_lz = chunk_lz * 16 + lz;
|
||||||
|
|
||||||
|
let idx = if tile_lx >= 128 || tile_lz >= 128 {
|
||||||
|
0u16
|
||||||
|
} else {
|
||||||
|
let surface_y = tile.heightmap[tile_lz * 128 + tile_lx] as i32;
|
||||||
|
let terrain_id = tile.terrain[tile_lz * 128 + tile_lx];
|
||||||
|
let base_name = block_name(terrain_id, global_y, surface_y);
|
||||||
|
let name = apply_layers(tile, tile_lx, tile_lz, global_y, surface_y, base_name, layer_exporters);
|
||||||
|
BLOCK_SET.iter().position(|s| *s == name).unwrap_or(0) as u16
|
||||||
|
};
|
||||||
|
indices.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !indices.iter().any(|&i| i != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let used_blocks: Vec<&str> = {
|
||||||
|
let mut seen = std::collections::BTreeSet::new();
|
||||||
|
let mut names = Vec::new();
|
||||||
|
for &i in &indices {
|
||||||
|
if i < BLOCK_SET.len() as u16 && seen.insert(i) {
|
||||||
|
names.push(BLOCK_SET[i as usize]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names
|
||||||
|
};
|
||||||
|
|
||||||
|
let bits = bits_needed(used_blocks.len());
|
||||||
|
let mut palette_map: std::collections::HashMap<&str, u16> = HashMap::new();
|
||||||
|
for (pi, name) in used_blocks.iter().enumerate() {
|
||||||
|
palette_map.insert(name, pi as u16);
|
||||||
|
}
|
||||||
|
let remapped: Vec<u16> = indices.iter().map(|&i| {
|
||||||
|
if i < BLOCK_SET.len() as u16 {
|
||||||
|
palette_map[BLOCK_SET[i as usize]]
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
let packed = pack_indices(&remapped, bits);
|
||||||
|
|
||||||
|
let palette_tags: Vec<terrafier_nbt::Tag> = used_blocks.iter().map(|name| {
|
||||||
|
let mut entry = HashMap::new();
|
||||||
|
entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string()));
|
||||||
|
terrafier_nbt::Tag::Compound(entry)
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let mut block_states = HashMap::new();
|
||||||
|
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_tags));
|
||||||
|
block_states.insert("data".into(), terrafier_nbt::Tag::LongArray(packed));
|
||||||
|
|
||||||
|
let biome_at = biome_name(tile, chunk_lx * 16, chunk_lz * 16, layer_exporters);
|
||||||
|
let biome_palette = vec![{
|
||||||
|
let mut b = HashMap::new();
|
||||||
|
b.insert("Name".into(), terrafier_nbt::Tag::String(biome_at.into()));
|
||||||
|
terrafier_nbt::Tag::Compound(b)
|
||||||
|
}];
|
||||||
|
let mut biomes = HashMap::new();
|
||||||
|
biomes.insert("palette".into(), terrafier_nbt::Tag::List(biome_palette));
|
||||||
|
|
||||||
|
let mut sec_compound = HashMap::new();
|
||||||
|
sec_compound.insert("Y".into(), terrafier_nbt::Tag::Byte(sec_y as i8));
|
||||||
|
sec_compound.insert("block_states".into(), terrafier_nbt::Tag::Compound(block_states));
|
||||||
|
sec_compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
|
||||||
|
|
||||||
|
sections.push(terrafier_nbt::Tag::Compound(sec_compound));
|
||||||
|
}
|
||||||
|
|
||||||
|
if sections.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
compound.insert("sections".into(), terrafier_nbt::Tag::List(sections));
|
||||||
|
|
||||||
|
let chunk_tag = terrafier_nbt::Tag::Compound(compound);
|
||||||
|
let bytes = terrafier_nbt::io::writer::to_bytes(&chunk_tag)?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a Terrafier World to a Minecraft save directory, applying layer exporters.
|
||||||
|
pub fn save_world_with_layers(
|
||||||
|
world: &World,
|
||||||
|
output_path: &Path,
|
||||||
|
layer_exporters: &[&dyn LayerExport],
|
||||||
|
) -> Result<()> {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
fs::create_dir_all(output_path.join("region"))?;
|
||||||
|
|
||||||
|
let level_tag = build_level_dat(world)?;
|
||||||
|
let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?;
|
||||||
|
fs::write(output_path.join("level.dat"), &level_bytes)?;
|
||||||
|
|
||||||
|
let dim = match world.dimensions.first() {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut regions: BTreeMap<(i32, i32), Vec<(&(i32, i32), &Tile)>> = BTreeMap::new();
|
||||||
|
for (key, tile) in &dim.tiles {
|
||||||
|
let (tx, tz) = key;
|
||||||
|
let rx = tx >> 2;
|
||||||
|
let rz = tz >> 2;
|
||||||
|
regions.entry((rx, rz)).or_default().push((key, tile));
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((rx, rz), tile_refs) in ®ions {
|
||||||
|
let mut region = terrafier_fastanvil::io::region::Region::new(*rx, *rz);
|
||||||
|
|
||||||
|
for (_key, tile) in tile_refs {
|
||||||
|
for chunk_lx in 0..8usize {
|
||||||
|
for chunk_lz in 0..8usize {
|
||||||
|
let chunk_x = tile.x * 8 + chunk_lx as i32;
|
||||||
|
let chunk_z = tile.z * 8 + chunk_lz as i32;
|
||||||
|
|
||||||
|
let region_local_x = (chunk_x & 31) as u8;
|
||||||
|
let region_local_z = (chunk_z & 31) as u8;
|
||||||
|
|
||||||
|
let chunk_data = build_chunk_nbt_with_layers(chunk_x, chunk_z, tile, chunk_lx, chunk_lz, layer_exporters)?;
|
||||||
|
|
||||||
|
if chunk_data.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.set_chunk_data(region_local_x, region_local_z, chunk_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let region_bytes = region.to_bytes()?;
|
||||||
|
let file_name = format!("r.{}.{}.mca", rx, rz);
|
||||||
|
fs::write(output_path.join("region").join(&file_name), ®ion_bytes)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_level_dat(root: &terrafier_nbt::Tag) -> (String, u64, i32) {
|
fn parse_level_dat(root: &terrafier_nbt::Tag) -> (String, u64, i32) {
|
||||||
let default = || ("Unknown".to_string(), 0u64, 3954i32);
|
let default = || ("Unknown".to_string(), 0u64, 3954i32);
|
||||||
match root {
|
match root {
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,6 @@
|
||||||
pub mod binary;
|
pub mod binary;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
|
pub mod layer_export;
|
||||||
pub mod minecraft;
|
pub mod minecraft;
|
||||||
|
pub mod wp_import;
|
||||||
|
|
|
||||||
1012
core/src/io/wp_import.rs
Normal file
1012
core/src/io/wp_import.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +0,0 @@
|
||||||
pub trait Layer: Send + Sync {
|
|
||||||
fn id(&self) -> &'static str;
|
|
||||||
fn name(&self) -> &'static str;
|
|
||||||
fn data_size(&self) -> DataSize;
|
|
||||||
fn priority(&self) -> i32;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum DataSize {
|
|
||||||
Bit,
|
|
||||||
Nibble,
|
|
||||||
Byte,
|
|
||||||
Int,
|
|
||||||
}
|
|
||||||
49
core/src/model/layers/builtin.rs
Normal file
49
core/src/model/layers/builtin.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use super::{DataSize, Layer};
|
||||||
|
|
||||||
|
pub struct CavesLayer;
|
||||||
|
impl Layer for CavesLayer {
|
||||||
|
fn id(&self) -> &'static str { "caves" }
|
||||||
|
fn name(&self) -> &'static str { "Caves" }
|
||||||
|
fn data_size(&self) -> DataSize { DataSize::Nibble }
|
||||||
|
fn priority(&self) -> i32 { 10 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RiverLayer;
|
||||||
|
impl Layer for RiverLayer {
|
||||||
|
fn id(&self) -> &'static str { "river" }
|
||||||
|
fn name(&self) -> &'static str { "River" }
|
||||||
|
fn data_size(&self) -> DataSize { DataSize::Byte }
|
||||||
|
fn priority(&self) -> i32 { 20 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FrostLayer;
|
||||||
|
impl Layer for FrostLayer {
|
||||||
|
fn id(&self) -> &'static str { "frost" }
|
||||||
|
fn name(&self) -> &'static str { "Frost" }
|
||||||
|
fn data_size(&self) -> DataSize { DataSize::Bit }
|
||||||
|
fn priority(&self) -> i32 { 30 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TreesLayer;
|
||||||
|
impl Layer for TreesLayer {
|
||||||
|
fn id(&self) -> &'static str { "trees" }
|
||||||
|
fn name(&self) -> &'static str { "Trees" }
|
||||||
|
fn data_size(&self) -> DataSize { DataSize::Byte }
|
||||||
|
fn priority(&self) -> i32 { 40 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BiomeLayer;
|
||||||
|
impl Layer for BiomeLayer {
|
||||||
|
fn id(&self) -> &'static str { "biome" }
|
||||||
|
fn name(&self) -> &'static str { "Biome" }
|
||||||
|
fn data_size(&self) -> DataSize { DataSize::Byte }
|
||||||
|
fn priority(&self) -> i32 { 50 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResourcesLayer;
|
||||||
|
impl Layer for ResourcesLayer {
|
||||||
|
fn id(&self) -> &'static str { "resources" }
|
||||||
|
fn name(&self) -> &'static str { "Resources" }
|
||||||
|
fn data_size(&self) -> DataSize { DataSize::Nibble }
|
||||||
|
fn priority(&self) -> i32 { 60 }
|
||||||
|
}
|
||||||
24
core/src/model/layers/mod.rs
Normal file
24
core/src/model/layers/mod.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
//! Layer system — defines the Layer trait and built-in layer types.
|
||||||
|
|
||||||
|
pub mod builtin;
|
||||||
|
|
||||||
|
pub trait Layer: Send + Sync {
|
||||||
|
fn id(&self) -> &'static str;
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
fn data_size(&self) -> DataSize;
|
||||||
|
fn priority(&self) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DataSize {
|
||||||
|
Bit,
|
||||||
|
Nibble,
|
||||||
|
Byte,
|
||||||
|
Int,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const LAYER_CAVES: u32 = 1;
|
||||||
|
pub const LAYER_RIVER: u32 = 2;
|
||||||
|
pub const LAYER_FROST: u32 = 3;
|
||||||
|
pub const LAYER_TREES: u32 = 4;
|
||||||
|
pub const LAYER_BIOME: u32 = 5;
|
||||||
|
pub const LAYER_RESOURCES: u32 = 6;
|
||||||
|
|
@ -59,4 +59,26 @@ impl Tile {
|
||||||
pub fn set_terrain(&mut self, lx: usize, lz: usize, terrain: u8) {
|
pub fn set_terrain(&mut self, lx: usize, lz: usize, terrain: u8) {
|
||||||
self.terrain[lz * TILE_SIZE + lx] = terrain;
|
self.terrain[lz * TILE_SIZE + lx] = terrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get layer data buffer for a given layer ID.
|
||||||
|
pub fn get_layer_data(&self, layer_id: u32) -> Option<&LayerBuffer> {
|
||||||
|
self.layer_data.get(&layer_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set layer data buffer for a given layer ID.
|
||||||
|
pub fn set_layer_data(&mut self, layer_id: u32, data: LayerBuffer) {
|
||||||
|
self.layer_data.insert(layer_id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure a layer buffer exists, creating an empty one if needed.
|
||||||
|
pub fn ensure_layer(&mut self, layer_id: u32, data_size: crate::model::layers::DataSize) -> &mut LayerBuffer {
|
||||||
|
use crate::model::layers::DataSize;
|
||||||
|
let total = TILE_SIZE * TILE_SIZE;
|
||||||
|
self.layer_data.entry(layer_id).or_insert_with(|| match data_size {
|
||||||
|
DataSize::Bit => LayerBuffer::Bit(vec![0u64; (total + 63) / 64]),
|
||||||
|
DataSize::Nibble => LayerBuffer::Nibble(vec![0u8; (total + 1) / 2]),
|
||||||
|
DataSize::Byte => LayerBuffer::Byte(vec![0u8; total]),
|
||||||
|
DataSize::Int => LayerBuffer::Int(vec![0i32; total]),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
219
core/src/ops/filters.rs
Normal file
219
core/src/ops/filters.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
//! Filter system — restrict which cells an operation affects based on terrain, height, or slope.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use crate::model::dimension::Dimension;
|
||||||
|
use crate::model::terrain::Terrain;
|
||||||
|
use crate::model::tile::{Tile, TILE_SIZE};
|
||||||
|
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
||||||
|
|
||||||
|
/// Determines whether a cell should be modified by an operation.
|
||||||
|
pub trait Filter: Send + Sync {
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only apply where terrain matches a specific type.
|
||||||
|
pub struct TerrainFilter {
|
||||||
|
pub terrain: Terrain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for TerrainFilter {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TerrainFilter"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool {
|
||||||
|
tile.get_terrain(local_x, local_z) == self.terrain as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only apply where height is within a range.
|
||||||
|
pub struct HeightFilter {
|
||||||
|
pub min: i16,
|
||||||
|
pub max: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for HeightFilter {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"HeightFilter"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool {
|
||||||
|
let h = tile.get_height(local_x, local_z);
|
||||||
|
h >= self.min && h <= self.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only apply where slope (max height difference to 4 neighbors) is within a range.
|
||||||
|
pub struct SlopeFilter {
|
||||||
|
pub min: f64,
|
||||||
|
pub max: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for SlopeFilter {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SlopeFilter"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool {
|
||||||
|
let center = tile.get_height(local_x, local_z);
|
||||||
|
let mut max_diff = 0i16;
|
||||||
|
for (dx, dz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] {
|
||||||
|
let nx = local_x as i32 + dx;
|
||||||
|
let nz = local_z as i32 + dz;
|
||||||
|
if nx >= 0 && nx < TILE_SIZE as i32 && nz >= 0 && nz < TILE_SIZE as i32 {
|
||||||
|
let nh = tile.get_height(nx as usize, nz as usize);
|
||||||
|
let diff = (center - nh).abs();
|
||||||
|
if diff > max_diff {
|
||||||
|
max_diff = diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let slope = max_diff as f64;
|
||||||
|
slope >= self.min && slope <= self.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How multiple filters are combined.
|
||||||
|
pub enum FilterCombinator {
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine multiple filters with AND/OR logic.
|
||||||
|
pub struct CompositeFilter {
|
||||||
|
pub filters: Vec<Box<dyn Filter>>,
|
||||||
|
pub combinator: FilterCombinator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for CompositeFilter {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CompositeFilter"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool {
|
||||||
|
match self.combinator {
|
||||||
|
FilterCombinator::And => self
|
||||||
|
.filters
|
||||||
|
.iter()
|
||||||
|
.all(|f| f.should_apply(tile, local_x, local_z)),
|
||||||
|
FilterCombinator::Or => self
|
||||||
|
.filters
|
||||||
|
.iter()
|
||||||
|
.any(|f| f.should_apply(tile, local_x, local_z)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Negate a filter.
|
||||||
|
pub struct InvertFilter {
|
||||||
|
pub filter: Box<dyn Filter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for InvertFilter {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"InvertFilter"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool {
|
||||||
|
!self.filter.should_apply(tile, local_x, local_z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps any operation and restricts its effect to cells that pass the filter.
|
||||||
|
///
|
||||||
|
/// On apply:
|
||||||
|
/// 1. Snapshots all cell heights in the brush area.
|
||||||
|
/// 2. Runs the inner operation.
|
||||||
|
/// 3. Restores heights of cells that do NOT pass the filter.
|
||||||
|
///
|
||||||
|
/// Inverse restores all cells from the snapshot.
|
||||||
|
pub struct FilteredOperation {
|
||||||
|
pub operation: Box<dyn Operation>,
|
||||||
|
pub filter: Box<dyn Filter>,
|
||||||
|
pub tile_x: i32,
|
||||||
|
pub tile_z: i32,
|
||||||
|
pub center_x: u32,
|
||||||
|
pub center_z: u32,
|
||||||
|
pub radius: u32,
|
||||||
|
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation for FilteredOperation {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
self.operation.name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||||
|
let is_first_apply = self.before_snapshot.get().is_none();
|
||||||
|
|
||||||
|
// Phase 1: snapshot all cells in brush area (if first apply)
|
||||||
|
let snapshot: Vec<(usize, i16)> = if is_first_apply {
|
||||||
|
let tile = dim.tiles.get(&(self.tile_x, self.tile_z)).ok_or(
|
||||||
|
OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: 0,
|
||||||
|
z: 0,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let r = self.radius as i32;
|
||||||
|
let cx = self.center_x as i32;
|
||||||
|
let cz = self.center_z as i32;
|
||||||
|
|
||||||
|
let mut snap = Vec::new();
|
||||||
|
for dz in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
let ax = cx + dx;
|
||||||
|
let az = cz + dz;
|
||||||
|
if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
||||||
|
snap.push((idx, tile.heightmap[idx]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = self.before_snapshot.set(snap.clone());
|
||||||
|
snap
|
||||||
|
} else {
|
||||||
|
self.before_snapshot
|
||||||
|
.get()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Phase 2: apply inner operation
|
||||||
|
self.operation.apply(dim)?;
|
||||||
|
|
||||||
|
// Phase 3: restore cells that don't pass the filter
|
||||||
|
let tile = dim.tiles.get_mut(&(self.tile_x, self.tile_z)).ok_or(
|
||||||
|
OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: 0,
|
||||||
|
z: 0,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for &(idx, original_h) in &snapshot {
|
||||||
|
let lx = idx % TILE_SIZE;
|
||||||
|
let lz = idx / TILE_SIZE;
|
||||||
|
if !self.filter.should_apply(tile, lx, lz) {
|
||||||
|
tile.heightmap[idx] = original_h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inverse(&self) -> Box<dyn Operation> {
|
||||||
|
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||||
|
Box::new(RestoreHeightsOperation {
|
||||||
|
tile_x: self.tile_x,
|
||||||
|
tile_z: self.tile_z,
|
||||||
|
snapshot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! Editing operations — terrain modification, brush operations, height maps.
|
//! Editing operations — terrain modification, brush operations, height maps.
|
||||||
|
|
||||||
|
pub mod filters;
|
||||||
pub mod heightmap;
|
pub mod heightmap;
|
||||||
pub mod operations;
|
pub mod operations;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Each operation implements the Operation trait and supports undo.
|
//! Each operation implements the Operation trait and supports undo.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -443,6 +444,367 @@ impl Operation for SmoothOperation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulate thermal erosion — material moves from high to low when slope exceeds threshold.
|
||||||
|
pub struct ErodeOperation {
|
||||||
|
pub tile_x: i32,
|
||||||
|
pub tile_z: i32,
|
||||||
|
pub center_x: u32,
|
||||||
|
pub center_z: u32,
|
||||||
|
pub radius: u32,
|
||||||
|
pub iterations: u32,
|
||||||
|
pub talus_angle: f64,
|
||||||
|
pub brush: Arc<dyn crate::model::brush::Brush>,
|
||||||
|
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation for ErodeOperation {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Erode"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||||
|
let tile = dim
|
||||||
|
.tiles
|
||||||
|
.get_mut(&(self.tile_x, self.tile_z))
|
||||||
|
.ok_or(OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: 0,
|
||||||
|
z: 0,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let r = self.radius as i32;
|
||||||
|
let cx = self.center_x as i32;
|
||||||
|
let cz = self.center_z as i32;
|
||||||
|
|
||||||
|
// Snapshot original heights on first apply
|
||||||
|
let mut snapshot: Vec<(usize, i16)> = Vec::new();
|
||||||
|
let is_first_apply = self.before_snapshot.get().is_none();
|
||||||
|
if is_first_apply {
|
||||||
|
for dz in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
let ax = cx + dx;
|
||||||
|
let az = cz + dz;
|
||||||
|
if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
||||||
|
snapshot.push((idx, tile.heightmap[idx]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_first_apply {
|
||||||
|
let _ = self.before_snapshot.set(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply erosion for `iterations` passes
|
||||||
|
let tile_size = TILE_SIZE;
|
||||||
|
for _ in 0..self.iterations.max(1) {
|
||||||
|
let mut deltas = vec![0i32; tile_size * tile_size];
|
||||||
|
|
||||||
|
for dz in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
let ax = cx + dx;
|
||||||
|
let az = cz + dz;
|
||||||
|
if ax < 0 || az < 0 || ax >= tile_size as i32 || az >= tile_size as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = (az as usize) * tile_size + (ax as usize);
|
||||||
|
let current = tile.heightmap[idx] as i32;
|
||||||
|
|
||||||
|
// Find lowest neighbor that is also in brush area
|
||||||
|
let mut lowest_h = current;
|
||||||
|
let mut lowest_idx = None;
|
||||||
|
for (ndx, ndz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] {
|
||||||
|
let bx = ax + ndx;
|
||||||
|
let bz = az + ndz;
|
||||||
|
if bx < 0 || bx >= tile_size as i32 || bz < 0 || bz >= tile_size as i32
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let n_dx = bx - cx;
|
||||||
|
let n_dz = bz - cz;
|
||||||
|
if self.brush.get_strength(n_dx as f64, n_dz as f64) <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let nidx = (bz as usize) * tile_size + (bx as usize);
|
||||||
|
let nh = tile.heightmap[nidx] as i32;
|
||||||
|
if nh < lowest_h {
|
||||||
|
lowest_h = nh;
|
||||||
|
lowest_idx = Some(nidx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(lidx) = lowest_idx {
|
||||||
|
let diff = current - lowest_h;
|
||||||
|
if (diff as f64) > self.talus_angle {
|
||||||
|
let excess = diff as f64 - self.talus_angle;
|
||||||
|
let move_amount = (excess * 0.5).round() as i32;
|
||||||
|
deltas[idx] -= move_amount;
|
||||||
|
deltas[lidx] += move_amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply deltas
|
||||||
|
for dz in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
let ax = cx + dx;
|
||||||
|
let az = cz + dz;
|
||||||
|
if ax < 0 || az < 0 || ax >= tile_size as i32 || az >= tile_size as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let idx = (az as usize) * tile_size + (ax as usize);
|
||||||
|
if deltas[idx] != 0 {
|
||||||
|
let new_h = (tile.heightmap[idx] as i32 + deltas[idx])
|
||||||
|
.clamp(tile.min_height as i32, tile.max_height as i32)
|
||||||
|
as i16;
|
||||||
|
tile.heightmap[idx] = new_h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inverse(&self) -> Box<dyn Operation> {
|
||||||
|
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||||
|
Box::new(RestoreHeightsOperation {
|
||||||
|
tile_x: self.tile_x,
|
||||||
|
tile_z: self.tile_z,
|
||||||
|
snapshot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill a contiguous area (flood-fill from center) with a terrain type.
|
||||||
|
pub struct FillOperation {
|
||||||
|
pub tile_x: i32,
|
||||||
|
pub tile_z: i32,
|
||||||
|
pub center_x: u32,
|
||||||
|
pub center_z: u32,
|
||||||
|
pub terrain: Terrain,
|
||||||
|
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation for FillOperation {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||||
|
let tile = dim
|
||||||
|
.tiles
|
||||||
|
.get_mut(&(self.tile_x, self.tile_z))
|
||||||
|
.ok_or(OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: 0,
|
||||||
|
z: 0,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let cx = self.center_x as usize;
|
||||||
|
let cz = self.center_z as usize;
|
||||||
|
if cx >= TILE_SIZE || cz >= TILE_SIZE {
|
||||||
|
return Err(OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: self.center_x,
|
||||||
|
z: self.center_z,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = cz * TILE_SIZE + cx;
|
||||||
|
let original_terrain = tile.terrain[idx];
|
||||||
|
let target_terrain = self.terrain as u8;
|
||||||
|
|
||||||
|
if original_terrain == target_terrain {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut snapshot: Vec<(usize, u8)> = Vec::new();
|
||||||
|
let mut visited = vec![false; TILE_SIZE * TILE_SIZE];
|
||||||
|
let mut queue = VecDeque::new();
|
||||||
|
queue.push_back((cx, cz));
|
||||||
|
visited[idx] = true;
|
||||||
|
|
||||||
|
while let Some((x, z)) = queue.pop_front() {
|
||||||
|
let i = z * TILE_SIZE + x;
|
||||||
|
if self.before_snapshot.get().is_none() {
|
||||||
|
snapshot.push((i, tile.terrain[i]));
|
||||||
|
}
|
||||||
|
tile.terrain[i] = target_terrain;
|
||||||
|
|
||||||
|
for (ndx, ndz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] {
|
||||||
|
let nx = x as i32 + ndx;
|
||||||
|
let nz = z as i32 + ndz;
|
||||||
|
if nx >= 0 && nx < TILE_SIZE as i32 && nz >= 0 && nz < TILE_SIZE as i32 {
|
||||||
|
let nxu = nx as usize;
|
||||||
|
let nzu = nz as usize;
|
||||||
|
let ni = nzu * TILE_SIZE + nxu;
|
||||||
|
if !visited[ni] && tile.terrain[ni] == original_terrain {
|
||||||
|
visited[ni] = true;
|
||||||
|
queue.push_back((nxu, nzu));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.before_snapshot.get().is_none() {
|
||||||
|
let _ = self.before_snapshot.set(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inverse(&self) -> Box<dyn Operation> {
|
||||||
|
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||||
|
Box::new(RestoreTerrainOperation {
|
||||||
|
tile_x: self.tile_x,
|
||||||
|
tile_z: self.tile_z,
|
||||||
|
snapshot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set all terrain within brush area to a single type (hard replace regardless of strength).
|
||||||
|
pub struct FloodOperation {
|
||||||
|
pub tile_x: i32,
|
||||||
|
pub tile_z: i32,
|
||||||
|
pub center_x: u32,
|
||||||
|
pub center_z: u32,
|
||||||
|
pub radius: u32,
|
||||||
|
pub terrain: Terrain,
|
||||||
|
pub brush: Arc<dyn crate::model::brush::Brush>,
|
||||||
|
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation for FloodOperation {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Flood"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||||
|
let tile = dim
|
||||||
|
.tiles
|
||||||
|
.get_mut(&(self.tile_x, self.tile_z))
|
||||||
|
.ok_or(OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: 0,
|
||||||
|
z: 0,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let r = self.radius as i32;
|
||||||
|
let cx = self.center_x as i32;
|
||||||
|
let cz = self.center_z as i32;
|
||||||
|
let terrain_id = self.terrain as u8;
|
||||||
|
|
||||||
|
let mut snapshot: Vec<(usize, u8)> = Vec::new();
|
||||||
|
|
||||||
|
for dz in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
let ax = cx + dx;
|
||||||
|
let az = cz + dz;
|
||||||
|
if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.brush.get_strength(dx as f64, dz as f64) == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
||||||
|
if self.before_snapshot.get().is_none() {
|
||||||
|
snapshot.push((idx, tile.terrain[idx]));
|
||||||
|
}
|
||||||
|
tile.terrain[idx] = terrain_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.before_snapshot.get().is_none() {
|
||||||
|
let _ = self.before_snapshot.set(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inverse(&self) -> Box<dyn Operation> {
|
||||||
|
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||||
|
Box::new(RestoreTerrainOperation {
|
||||||
|
tile_x: self.tile_x,
|
||||||
|
tile_z: self.tile_z,
|
||||||
|
snapshot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single-point terrain paint (radius 1, no brush falloff).
|
||||||
|
pub struct PencilOperation {
|
||||||
|
pub tile_x: i32,
|
||||||
|
pub tile_z: i32,
|
||||||
|
pub center_x: u32,
|
||||||
|
pub center_z: u32,
|
||||||
|
pub terrain: Terrain,
|
||||||
|
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation for PencilOperation {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Pencil"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||||
|
let tile = dim
|
||||||
|
.tiles
|
||||||
|
.get_mut(&(self.tile_x, self.tile_z))
|
||||||
|
.ok_or(OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: 0,
|
||||||
|
z: 0,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let cx = self.center_x as usize;
|
||||||
|
let cz = self.center_z as usize;
|
||||||
|
if cx >= TILE_SIZE || cz >= TILE_SIZE {
|
||||||
|
return Err(OperationError::OutOfBounds {
|
||||||
|
tx: self.tile_x,
|
||||||
|
tz: self.tile_z,
|
||||||
|
x: self.center_x,
|
||||||
|
z: self.center_z,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = cz * TILE_SIZE + cx;
|
||||||
|
if self.before_snapshot.get().is_none() {
|
||||||
|
let _ = self.before_snapshot.set(vec![(idx, tile.terrain[idx])]);
|
||||||
|
}
|
||||||
|
tile.terrain[idx] = self.terrain as u8;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inverse(&self) -> Box<dyn Operation> {
|
||||||
|
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||||
|
Box::new(RestoreTerrainOperation {
|
||||||
|
tile_x: self.tile_x,
|
||||||
|
tile_z: self.tile_z,
|
||||||
|
snapshot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Applies multiple sub-operations, supporting brushes that span multiple tiles.
|
/// Applies multiple sub-operations, supporting brushes that span multiple tiles.
|
||||||
pub struct MultiTileOperation {
|
pub struct MultiTileOperation {
|
||||||
pub operations: Vec<Box<dyn Operation>>,
|
pub operations: Vec<Box<dyn Operation>>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue