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
|
|
@ -20,6 +20,7 @@ serde-big-array.workspace = true
|
|||
bincode.workspace = true
|
||||
|
||||
image.workspace = true
|
||||
flate2.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::io::layer_export::LayerExport;
|
||||
use crate::io::minecraft::MinecraftIOError;
|
||||
use crate::model::world::World;
|
||||
|
||||
|
|
@ -26,6 +27,16 @@ pub fn export_to_save(world: &World, output_path: &Path) -> Result<()> {
|
|||
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).
|
||||
pub fn render_to_image(world: &World, output_path: &Path, scale: u32) -> Result<()> {
|
||||
// Determine bounds from tiles
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::path::Path;
|
|||
use thiserror::Error;
|
||||
|
||||
use crate::io::minecraft::MinecraftIOError;
|
||||
use crate::io::wp_import;
|
||||
use crate::model::world::World;
|
||||
|
||||
#[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))
|
||||
}
|
||||
|
|
|
|||
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::platform::Platform;
|
||||
use crate::io::layer_export::{apply_layers, biome_name, LayerExport};
|
||||
use crate::model::tile::Tile;
|
||||
use crate::model::world::World;
|
||||
|
||||
|
|
@ -496,6 +497,199 @@ fn build_chunk_nbt(
|
|||
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) {
|
||||
let default = || ("Unknown".to_string(), 0u64, 3954i32);
|
||||
match root {
|
||||
|
|
|
|||
|
|
@ -3,4 +3,6 @@
|
|||
pub mod binary;
|
||||
pub mod export;
|
||||
pub mod import;
|
||||
pub mod layer_export;
|
||||
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) {
|
||||
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.
|
||||
|
||||
pub mod filters;
|
||||
pub mod heightmap;
|
||||
pub mod operations;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
//!
|
||||
//! Each operation implements the Operation trait and supports undo.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::OnceLock;
|
||||
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.
|
||||
pub struct MultiTileOperation {
|
||||
pub operations: Vec<Box<dyn Operation>>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue