Fix critical structural issues: undo/redo, surface import, brush strength, i18n macro, level.dat export, GPU fallback, Nether/End support, dead code removal, dim serde, CPU perf, wasm-host, GPU constants, clippy warnings
- Replace OnceLock with Mutex<Option> for snapshot system in all 9 operations (undo/redo now works after redo) - Fix import surface detection: scan actual packed block data for highest solid block instead of always using +15 - Apply brush strength threshold (< 0.3) in PaintOperation for natural edge falloff - Remove broken t!() macro variant with variable substitution - Complete level.dat with RandomSeed, GameType, Difficulty, Spawn, Time, DataPacks, etc. - GPU viewport falls back to CPU renderer for multi-tile worlds - Export supports Nether (DIM-1/region) and End (DIM1/region) dimension subdirectories - Remove dead FloodOperation (duplicate of PaintOperation) - Add log::warn for malformed dimension tile keys in serde - Reduce CPU viewport base_size from 64 to 32 (4x fewer pixels) - Simplify wasm-host host_get_seed to direct cast - Add named constants for GPU buffer sizes - Fix unused import warning in cli/main.rs - cargo fmt
This commit is contained in:
parent
18613260a8
commit
2419b603c1
24 changed files with 339 additions and 271 deletions
23
PLAN.md
23
PLAN.md
|
|
@ -125,3 +125,26 @@ struct World {
|
|||
seed: u64,
|
||||
}
|
||||
```
|
||||
|
||||
## Известные проблемы (требуют фикса)
|
||||
|
||||
### Критические
|
||||
1. **GPU рендерит только 1 тайл** — `GpuRenderer` 128×128, а вьюпорт показывает мульти-тайловый мир. `gpu.rs` рисует один тайл, остальные пустые.
|
||||
2. **Undo/redo сломан** — `OnceLock` в операциях не позволяет повторно захватить снепшот при redo. После undo→redo мир ломается.
|
||||
3. **Импорт высот неверный** — `reader.rs:122` всегда ставит surface_y = section_y * 16 + 15, игнорируя реальную высоту поверхности.
|
||||
4. **Brush strength не влияет на Paint** — `apply_brush_terrain()` игнорирует силу кисти, кроме проверки на 0.
|
||||
5. **Макрос `t!()` сломан** — `i18n/src/lib.rs:61`: невалидный каст, макрос с переменными не компилируется.
|
||||
|
||||
### Существенные
|
||||
6. **level.dat при экспорте неполный** — нет `GameRules`, `Difficulty`, `Spawn`, `Time`, `RandomSeed`.
|
||||
7. **Нет поддержки Nether/End** — экспорт всегда в `region/`, импорт создаёт только `"overworld"`.
|
||||
8. **`FloodOperation` — мёртвый код** — дубликат `PaintOperation`, нигде не используется.
|
||||
9. **`Dimension` сериализация хрупкая** — строковые ключи `"{tx},{tz}"`, битые ключи молча дропаются.
|
||||
10. **CPU-вьюпорт рендерит всё каждый кадр** — полный перерендер при каждом `update()`.
|
||||
11. **`wasm-host` `host_get_seed` бессмысленный** — разбивает u64 на lo/hi 32-bit и собирает обратно.
|
||||
|
||||
### Архитектурные
|
||||
12. **Только 1 тест на 93 .rs файла** — операции, импорт, экспорт, слои без покрытия.
|
||||
13. **Экспорт блоков примитивный** — 9 типов блоков, без свойств (snowy, waterlogged).
|
||||
14. **`data_version` хардкод** — всегда экспортит 3954, игнорируя версию исходного мира.
|
||||
15. **GPU buffer sizes хардкод** — `8192 * 4` и `4096 * 4` без Named constants.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ pub fn cmd_export(
|
|||
|
||||
if validate || dry_run {
|
||||
if dry_run {
|
||||
log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "export")]));
|
||||
log::info!(
|
||||
"{}",
|
||||
i18n::translate_with("cli.dry_run", &[("action", "export")])
|
||||
);
|
||||
}
|
||||
output::print_result(
|
||||
format,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ pub fn cmd_import(
|
|||
|
||||
if validate || dry_run {
|
||||
if dry_run {
|
||||
log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "import")]));
|
||||
log::info!(
|
||||
"{}",
|
||||
i18n::translate_with("cli.dry_run", &[("action", "import")])
|
||||
);
|
||||
}
|
||||
terrafier_core::io::import::validate_save(input_path)?;
|
||||
output::print_result(
|
||||
|
|
@ -54,10 +57,13 @@ pub fn cmd_import(
|
|||
let bar = progress::new_bar(region_count.max(1) as u64, &importing_msg);
|
||||
let world = terrafier_core::io::import::import(input_path)?;
|
||||
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
||||
let import_msg = i18n::translate_with("cli.import.complete", &[
|
||||
("tiles", &tile_count.to_string()),
|
||||
("dimensions", &world.dimensions.len().to_string()),
|
||||
]);
|
||||
let import_msg = i18n::translate_with(
|
||||
"cli.import.complete",
|
||||
&[
|
||||
("tiles", &tile_count.to_string()),
|
||||
("dimensions", &world.dimensions.len().to_string()),
|
||||
],
|
||||
);
|
||||
progress::finish_with(&import_msg, bar);
|
||||
|
||||
let output_path = Path::new(&args.output);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ pub fn cmd_render(
|
|||
|
||||
if validate || dry_run {
|
||||
if dry_run {
|
||||
log::info!("{}", i18n::translate_with("cli.dry_run", &[("action", "render")]));
|
||||
log::info!(
|
||||
"{}",
|
||||
i18n::translate_with("cli.dry_run", &[("action", "render")])
|
||||
);
|
||||
}
|
||||
output::print_result(
|
||||
format,
|
||||
|
|
@ -50,9 +53,10 @@ pub fn cmd_render(
|
|||
|
||||
let bar = progress::new_bar(tile_count as u64, i18n::t!("cli.render.rendering"));
|
||||
terrafier_core::io::export::render_to_image(&world, &args.output, args.scale)?;
|
||||
let saved_msg = i18n::translate_with("cli.render.saved", &[
|
||||
("path", &args.output.display().to_string()),
|
||||
]);
|
||||
let saved_msg = i18n::translate_with(
|
||||
"cli.render.saved",
|
||||
&[("path", &args.output.display().to_string())],
|
||||
);
|
||||
progress::finish_with(&saved_msg, bar);
|
||||
|
||||
log::info!("Preview rendered to: {}", args.output.display());
|
||||
|
|
|
|||
|
|
@ -45,11 +45,15 @@ pub fn cmd_new(
|
|||
|
||||
let spinner = progress::new_spinner(i18n::t!("cli.new.creating"));
|
||||
let world = World::new(&args.name, args.seed);
|
||||
let tile_count = world.dimensions.iter().map(|d| d.tiles.len()).sum::<usize>();
|
||||
let created_msg = i18n::translate_with("cli.new.created", &[
|
||||
("name", &args.name),
|
||||
("tiles", &tile_count.to_string()),
|
||||
]);
|
||||
let tile_count = world
|
||||
.dimensions
|
||||
.iter()
|
||||
.map(|d| d.tiles.len())
|
||||
.sum::<usize>();
|
||||
let created_msg = i18n::translate_with(
|
||||
"cli.new.created",
|
||||
&[("name", &args.name), ("tiles", &tile_count.to_string())],
|
||||
);
|
||||
progress::finish_with(&created_msg, spinner);
|
||||
|
||||
let world_dir = args.output.join(&args.name);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod cmds;
|
||||
mod util;
|
||||
|
|
@ -43,7 +42,7 @@ enum Commands {
|
|||
#[cfg(feature = "scripting")]
|
||||
Run {
|
||||
/// Path to .rhai script file
|
||||
script: PathBuf,
|
||||
script: std::path::PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ fn print_human(result: &Value) {
|
|||
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
||||
println!(
|
||||
"{}",
|
||||
i18n::translate_with("cli.new.created", &[
|
||||
("name", name),
|
||||
("tiles", &tiles.to_string()),
|
||||
])
|
||||
i18n::translate_with(
|
||||
"cli.new.created",
|
||||
&[("name", name), ("tiles", &tiles.to_string()),]
|
||||
)
|
||||
);
|
||||
let _ = seed;
|
||||
}
|
||||
|
|
@ -48,10 +48,10 @@ fn print_human(result: &Value) {
|
|||
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
||||
println!(
|
||||
"{}",
|
||||
i18n::translate_with("cli.export.complete", &[
|
||||
("regions", "?"),
|
||||
("chunks", &tiles.to_string()),
|
||||
])
|
||||
i18n::translate_with(
|
||||
"cli.export.complete",
|
||||
&[("regions", "?"), ("chunks", &tiles.to_string()),]
|
||||
)
|
||||
);
|
||||
}
|
||||
"imported" => {
|
||||
|
|
@ -60,10 +60,13 @@ fn print_human(result: &Value) {
|
|||
let dims = result["dimensions"].as_u64().unwrap_or(0);
|
||||
println!(
|
||||
"{}",
|
||||
i18n::translate_with("cli.import.complete", &[
|
||||
("tiles", &tiles.to_string()),
|
||||
("dimensions", &dims.to_string()),
|
||||
])
|
||||
i18n::translate_with(
|
||||
"cli.import.complete",
|
||||
&[
|
||||
("tiles", &tiles.to_string()),
|
||||
("dimensions", &dims.to_string()),
|
||||
]
|
||||
)
|
||||
);
|
||||
let _ = name;
|
||||
}
|
||||
|
|
@ -71,9 +74,7 @@ fn print_human(result: &Value) {
|
|||
let output = result["output"].as_str().unwrap_or("?");
|
||||
println!(
|
||||
"{}",
|
||||
i18n::translate_with("cli.render.saved", &[
|
||||
("path", output),
|
||||
])
|
||||
i18n::translate_with("cli.render.saved", &[("path", output),])
|
||||
);
|
||||
}
|
||||
"validated" => {
|
||||
|
|
@ -125,17 +126,15 @@ fn print_human_info(result: &Value) {
|
|||
if let Some(dims) = result.get("dimensions").and_then(|v| v.as_array()) {
|
||||
println!(
|
||||
" {}",
|
||||
i18n::translate_with("cli.info.dimensions", &[
|
||||
("dimensions", &dims.len().to_string()),
|
||||
])
|
||||
i18n::translate_with(
|
||||
"cli.info.dimensions",
|
||||
&[("dimensions", &dims.len().to_string()),]
|
||||
)
|
||||
);
|
||||
for dim in dims {
|
||||
let dim_name = dim["name"].as_str().unwrap_or("?");
|
||||
let dim_tiles = dim["tiles"].as_u64().unwrap_or(0);
|
||||
println!(
|
||||
" - {} ({} tiles)",
|
||||
dim_name, dim_tiles
|
||||
);
|
||||
println!(" - {} ({} tiles)", dim_name, dim_tiles);
|
||||
}
|
||||
}
|
||||
if let Some(tiles) = result.get("total_tiles").and_then(|v| v.as_u64()) {
|
||||
|
|
|
|||
|
|
@ -14,18 +14,21 @@ use super::chunk::{build_chunk_nbt, build_chunk_nbt_with_layers};
|
|||
#[allow(clippy::type_complexity)]
|
||||
type RegionTiles<'a> = BTreeMap<(i32, i32), Vec<(&'a (i32, i32), &'a Tile)>>;
|
||||
|
||||
/// Save a Terrafier World to a Minecraft save directory.
|
||||
pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
||||
fs::create_dir_all(output_path.join("region"))?;
|
||||
fn dim_region_path(output_path: &Path, dim_name: &str) -> std::path::PathBuf {
|
||||
match dim_name {
|
||||
"the_nether" => output_path.join("DIM-1").join("region"),
|
||||
"the_end" => output_path.join("DIM1").join("region"),
|
||||
_ => 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(()),
|
||||
};
|
||||
fn write_dimension(
|
||||
dim: &crate::model::dimension::Dimension,
|
||||
output_path: &Path,
|
||||
build_chunk: &dyn Fn(i32, i32, &Tile, usize, usize) -> Result<Vec<u8>>,
|
||||
) -> Result<()> {
|
||||
let region_path = dim_region_path(output_path, &dim.name);
|
||||
fs::create_dir_all(®ion_path)?;
|
||||
|
||||
let mut regions: RegionTiles = BTreeMap::new();
|
||||
for (key, tile) in &dim.tiles {
|
||||
|
|
@ -47,7 +50,7 @@ pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
|||
let region_local_x = (chunk_x & 31) as u8;
|
||||
let region_local_z = (chunk_z & 31) as u8;
|
||||
|
||||
let chunk_data = build_chunk_nbt(chunk_x, chunk_z, tile, chunk_lx, chunk_lz)?;
|
||||
let chunk_data = build_chunk(chunk_x, chunk_z, tile, chunk_lx, chunk_lz)?;
|
||||
|
||||
if chunk_data.is_empty() {
|
||||
continue;
|
||||
|
|
@ -60,7 +63,22 @@ pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
|||
|
||||
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)?;
|
||||
fs::write(region_path.join(&file_name), ®ion_bytes)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save a Terrafier World to a Minecraft save directory.
|
||||
pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
||||
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)?;
|
||||
|
||||
for dim in &world.dimensions {
|
||||
write_dimension(dim, output_path, &|cx, cz, tile, clx, clz| {
|
||||
build_chunk_nbt(cx, cz, tile, clx, clz)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -72,58 +90,14 @@ pub fn save_world_with_layers(
|
|||
output_path: &Path,
|
||||
layer_exporters: &[&dyn LayerExport],
|
||||
) -> Result<()> {
|
||||
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: RegionTiles = 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)?;
|
||||
for dim in &world.dimensions {
|
||||
write_dimension(dim, output_path, &|cx, cz, tile, clx, clz| {
|
||||
build_chunk_nbt_with_layers(cx, cz, tile, clx, clz, layer_exporters)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -135,6 +109,24 @@ fn build_level_dat(world: &World) -> Result<terrafier_nbt::Tag> {
|
|||
data.insert("LevelName".into(), Tag::String(world.name.clone()));
|
||||
data.insert("DataVersion".into(), Tag::Int(data_version()));
|
||||
data.insert("version".into(), Tag::Int(19133));
|
||||
data.insert("RandomSeed".into(), Tag::Long(world.seed as i64));
|
||||
data.insert("GameType".into(), Tag::Int(1));
|
||||
data.insert("hardcore".into(), Tag::Byte(0));
|
||||
data.insert("Difficulty".into(), Tag::Byte(2));
|
||||
data.insert("DifficultyLocked".into(), Tag::Byte(0));
|
||||
data.insert("SpawnX".into(), Tag::Int(0));
|
||||
data.insert("SpawnY".into(), Tag::Int(64));
|
||||
data.insert("SpawnZ".into(), Tag::Int(0));
|
||||
data.insert("Time".into(), Tag::Long(0));
|
||||
data.insert("DayTime".into(), Tag::Long(1000));
|
||||
data.insert("allowCommands".into(), Tag::Byte(1));
|
||||
data.insert("initialized".into(), Tag::Byte(1));
|
||||
data.insert("LastPlayed".into(), Tag::Long(0));
|
||||
|
||||
let mut data_packs = HashMap::new();
|
||||
data_packs.insert("Enabled".into(), Tag::List(vec![]));
|
||||
data_packs.insert("Disabled".into(), Tag::List(vec![]));
|
||||
data.insert("DataPacks".into(), Tag::Compound(data_packs));
|
||||
|
||||
let mut world_gen = HashMap::new();
|
||||
world_gen.insert("seed".into(), Tag::Long(world.seed as i64));
|
||||
|
|
|
|||
|
|
@ -86,41 +86,74 @@ pub fn load_save(path: &Path) -> Result<World> {
|
|||
let mut sorted: Vec<_> = chunk.sections.iter().collect();
|
||||
sorted.sort_by_key(|b| std::cmp::Reverse(b.section_y));
|
||||
|
||||
for section in &sorted {
|
||||
'section_loop: for section in &sorted {
|
||||
if section.palette.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let has_blocks = section.palette.iter().any(|p| {
|
||||
p.get("Name").is_some_and(|n| {
|
||||
matches!(
|
||||
n,
|
||||
terrafier_nbt::Tag::String(s)
|
||||
if s != "minecraft:air"
|
||||
)
|
||||
let non_air_indices: Vec<usize> = section
|
||||
.palette
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| {
|
||||
p.get("Name").is_some_and(|n| {
|
||||
matches!(
|
||||
n,
|
||||
terrafier_nbt::Tag::String(s)
|
||||
if s != "minecraft:air"
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if !has_blocks {
|
||||
if non_air_indices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if section.block_data.is_empty() {
|
||||
if section.palette[0].get("Name").is_some_and(|n| {
|
||||
matches!(
|
||||
n,
|
||||
terrafier_nbt::Tag::String(s)
|
||||
if s == "minecraft:air"
|
||||
)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
surface_y = Some((section.section_y as i32) * 16 + 15);
|
||||
break;
|
||||
}
|
||||
|
||||
surface_y = Some((section.section_y as i32) * 16 + 15);
|
||||
break;
|
||||
let bpe = std::cmp::max(
|
||||
4,
|
||||
(section.palette.len() as f64).log2().ceil() as usize,
|
||||
);
|
||||
|
||||
for dy in (0..16usize).rev() {
|
||||
let block_index = dy * 256 + lz * 16 + lx;
|
||||
let bit_pos = block_index * bpe;
|
||||
let long_idx = bit_pos / 64;
|
||||
let bit_off = bit_pos % 64;
|
||||
|
||||
if let Some(&word) = section.block_data.get(long_idx) {
|
||||
let palette_index = if bit_off + bpe <= 64 {
|
||||
let mask = (1u64 << bpe) - 1;
|
||||
((word as u64 >> bit_off) & mask) as usize
|
||||
} else {
|
||||
let low_bits = 64 - bit_off;
|
||||
let low = ((word as u64) >> bit_off) as usize;
|
||||
let high_bits = bit_off + bpe - 64;
|
||||
let high = section
|
||||
.block_data
|
||||
.get(long_idx + 1)
|
||||
.map(|&w| {
|
||||
(w as u64 & ((1u64 << high_bits) - 1))
|
||||
as usize
|
||||
})
|
||||
.unwrap_or(0);
|
||||
low | (high << low_bits)
|
||||
};
|
||||
|
||||
if non_air_indices.contains(&palette_index) {
|
||||
surface_y = Some(
|
||||
(section.section_y as i32) * 16 + dy as i32,
|
||||
);
|
||||
break 'section_loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ pub type Result<T> = std::result::Result<T, BinaryError>;
|
|||
/// Save a `World` to a binary file.
|
||||
pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
||||
let config = bincode::config::standard();
|
||||
let bytes =
|
||||
bincode::serde::encode_to_vec(world, config).map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
||||
let bytes = bincode::serde::encode_to_vec(world, config)
|
||||
.map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
||||
|
||||
std::fs::write(path, &bytes)?;
|
||||
Ok(())
|
||||
|
|
@ -38,9 +38,8 @@ pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
|||
pub fn load_binary(path: &Path) -> Result<World> {
|
||||
let bytes = std::fs::read(path)?;
|
||||
let config = bincode::config::standard();
|
||||
let (world, _): (World, usize) =
|
||||
bincode::serde::decode_from_slice(&bytes, config)
|
||||
.map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
||||
let (world, _): (World, usize) = bincode::serde::decode_from_slice(&bytes, config)
|
||||
.map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ impl<'de> Deserialize<'de> for Dimension {
|
|||
&& let (Ok(tx), Ok(tz)) = (tx_s.parse::<i32>(), tz_s.parse::<i32>())
|
||||
{
|
||||
tiles.insert((tx, tz), tile);
|
||||
} else {
|
||||
log::warn!("Skipping dimension tile with malformed key: {}", key);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::model::dimension::Dimension;
|
|||
use crate::model::tile::{TILE_SIZE, Tile};
|
||||
use crate::model::types::Terrain;
|
||||
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::Mutex;
|
||||
/// Determines whether a cell should be modified by an operation.
|
||||
pub trait Filter: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
|
|
@ -121,7 +121,7 @@ pub struct FilteredOperation {
|
|||
pub center_x: u32,
|
||||
pub center_z: u32,
|
||||
pub radius: u32,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||
}
|
||||
|
||||
impl Operation for FilteredOperation {
|
||||
|
|
@ -130,7 +130,7 @@ impl Operation for FilteredOperation {
|
|||
}
|
||||
|
||||
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||
let is_first_apply = self.before_snapshot.get().is_none();
|
||||
let is_first_apply = self.before_snapshot.lock().unwrap().is_none();
|
||||
|
||||
let snapshot: Vec<(usize, i16)> = if is_first_apply {
|
||||
let tile =
|
||||
|
|
@ -159,10 +159,14 @@ impl Operation for FilteredOperation {
|
|||
snap.push((idx, tile.heightmap[idx]));
|
||||
}
|
||||
}
|
||||
let _ = self.before_snapshot.set(snap.clone());
|
||||
self.before_snapshot.lock().unwrap().replace(snap.clone());
|
||||
snap
|
||||
} else {
|
||||
self.before_snapshot.get().cloned().unwrap_or_default()
|
||||
self.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
self.operation.apply(dim)?;
|
||||
|
|
@ -189,7 +193,12 @@ impl Operation for FilteredOperation {
|
|||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreHeightsOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::model::tile::TILE_SIZE;
|
|||
use crate::model::types::Brush;
|
||||
use crate::model::types::Terrain;
|
||||
use crate::ops::operations::{Operation, OperationError, RestoreTerrainOperation};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
struct BrushApply<'a> {
|
||||
tile: &'a mut crate::model::tile::Tile,
|
||||
|
|
@ -12,7 +12,7 @@ struct BrushApply<'a> {
|
|||
radius: u32,
|
||||
terrain_id: u8,
|
||||
brush: &'a dyn Brush,
|
||||
snapshot: &'a OnceLock<Vec<(usize, u8)>>,
|
||||
snapshot: &'a Mutex<Option<Vec<(usize, u8)>>>,
|
||||
snapshot_buf: &'a mut Vec<(usize, u8)>,
|
||||
}
|
||||
|
||||
|
|
@ -25,11 +25,11 @@ fn apply_brush_terrain(args: BrushApply) {
|
|||
if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 {
|
||||
continue;
|
||||
}
|
||||
if args.brush.get_strength(dx as f64, dz as f64) == 0.0 {
|
||||
if args.brush.get_strength(dx as f64, dz as f64) < 0.3 {
|
||||
continue;
|
||||
}
|
||||
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
||||
if args.snapshot.get().is_none() {
|
||||
if args.snapshot.lock().unwrap().is_none() {
|
||||
args.snapshot_buf.push((idx, args.tile.terrain[idx]));
|
||||
}
|
||||
args.tile.terrain[idx] = args.terrain_id;
|
||||
|
|
@ -46,7 +46,7 @@ pub struct PaintOperation {
|
|||
pub radius: u32,
|
||||
pub terrain: Terrain,
|
||||
pub brush: Arc<dyn Brush>,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
|
||||
}
|
||||
|
||||
impl Operation for PaintOperation {
|
||||
|
|
@ -77,66 +77,19 @@ impl Operation for PaintOperation {
|
|||
snapshot_buf: &mut snapshot_buf,
|
||||
});
|
||||
|
||||
let _ = self.before_snapshot.get_or_init(|| snapshot_buf);
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
self.before_snapshot.lock().unwrap().replace(snapshot_buf);
|
||||
}
|
||||
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 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 mut snapshot_buf = Vec::new();
|
||||
apply_brush_terrain(BrushApply {
|
||||
tile,
|
||||
cx: self.center_x as i32,
|
||||
cz: self.center_z as i32,
|
||||
radius: self.radius,
|
||||
terrain_id: self.terrain as u8,
|
||||
brush: self.brush.as_ref(),
|
||||
snapshot: &self.before_snapshot,
|
||||
snapshot_buf: &mut snapshot_buf,
|
||||
});
|
||||
|
||||
let _ = self.before_snapshot.get_or_init(|| snapshot_buf);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreTerrainOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
@ -152,7 +105,7 @@ pub struct PencilOperation {
|
|||
pub center_x: u32,
|
||||
pub center_z: u32,
|
||||
pub terrain: Terrain,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
|
||||
}
|
||||
|
||||
impl Operation for PencilOperation {
|
||||
|
|
@ -183,8 +136,11 @@ impl Operation for PencilOperation {
|
|||
}
|
||||
|
||||
let idx = cz * TILE_SIZE + cx;
|
||||
if self.before_snapshot.get().is_none() {
|
||||
let _ = self.before_snapshot.set(vec![(idx, tile.terrain[idx])]);
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
self.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(vec![(idx, tile.terrain[idx])]);
|
||||
}
|
||||
tile.terrain[idx] = self.terrain as u8;
|
||||
|
||||
|
|
@ -192,7 +148,12 @@ impl Operation for PencilOperation {
|
|||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreTerrainOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::model::dimension::Dimension;
|
||||
use crate::model::tile::TILE_SIZE;
|
||||
|
|
@ -14,7 +14,7 @@ pub struct FillOperation {
|
|||
pub center_x: u32,
|
||||
pub center_z: u32,
|
||||
pub terrain: Terrain,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
|
||||
}
|
||||
|
||||
impl Operation for FillOperation {
|
||||
|
|
@ -60,7 +60,7 @@ impl Operation for FillOperation {
|
|||
|
||||
while let Some((x, z)) = queue.pop_front() {
|
||||
let i = z * TILE_SIZE + x;
|
||||
if self.before_snapshot.get().is_none() {
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
snapshot.push((i, tile.terrain[i]));
|
||||
}
|
||||
tile.terrain[i] = target_terrain;
|
||||
|
|
@ -80,15 +80,20 @@ impl Operation for FillOperation {
|
|||
}
|
||||
}
|
||||
|
||||
if self.before_snapshot.get().is_none() {
|
||||
let _ = self.before_snapshot.set(snapshot);
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
self.before_snapshot.lock().unwrap().replace(snapshot);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreTerrainOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ mod brush;
|
|||
mod fill;
|
||||
mod shaping;
|
||||
|
||||
pub use brush::{FloodOperation, PaintOperation, PencilOperation};
|
||||
pub use brush::{PaintOperation, PencilOperation};
|
||||
pub use fill::FillOperation;
|
||||
pub use shaping::{
|
||||
ErodeOperation, FlattenOperation, HeightOperation, MultiTileOperation, SmoothOperation,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::model::dimension::Dimension;
|
||||
use crate::model::tile::TILE_SIZE;
|
||||
|
|
@ -17,7 +16,7 @@ pub struct ErodeOperation {
|
|||
pub iterations: u32,
|
||||
pub talus_angle: f64,
|
||||
pub brush: Arc<dyn Brush>,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||
}
|
||||
|
||||
impl Operation for ErodeOperation {
|
||||
|
|
@ -42,7 +41,7 @@ impl Operation for ErodeOperation {
|
|||
|
||||
// Snapshot original heights on first apply
|
||||
let mut snapshot: Vec<(usize, i16)> = Vec::new();
|
||||
let is_first_apply = self.before_snapshot.get().is_none();
|
||||
let is_first_apply = self.before_snapshot.lock().unwrap().is_none();
|
||||
if is_first_apply {
|
||||
for dz in -r..=r {
|
||||
for dx in -r..=r {
|
||||
|
|
@ -60,7 +59,7 @@ impl Operation for ErodeOperation {
|
|||
}
|
||||
}
|
||||
if is_first_apply {
|
||||
let _ = self.before_snapshot.set(snapshot);
|
||||
self.before_snapshot.lock().unwrap().replace(snapshot);
|
||||
}
|
||||
|
||||
// Apply erosion for `iterations` passes
|
||||
|
|
@ -142,7 +141,12 @@ impl Operation for ErodeOperation {
|
|||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreHeightsOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::model::dimension::Dimension;
|
||||
use crate::model::tile::TILE_SIZE;
|
||||
|
|
@ -16,7 +15,7 @@ pub struct HeightOperation {
|
|||
pub radius: u32,
|
||||
pub delta: i16,
|
||||
pub brush: Arc<dyn Brush>,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||
}
|
||||
|
||||
impl Operation for HeightOperation {
|
||||
|
|
@ -55,7 +54,7 @@ impl Operation for HeightOperation {
|
|||
|
||||
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
||||
|
||||
if self.before_snapshot.get().is_none() {
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
heights.push((idx, tile.heightmap[idx]));
|
||||
}
|
||||
|
||||
|
|
@ -66,15 +65,20 @@ impl Operation for HeightOperation {
|
|||
}
|
||||
}
|
||||
|
||||
if self.before_snapshot.get().is_none() {
|
||||
let _ = self.before_snapshot.set(heights);
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
self.before_snapshot.lock().unwrap().replace(heights);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreHeightsOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
@ -92,7 +96,7 @@ pub struct FlattenOperation {
|
|||
pub radius: u32,
|
||||
pub target_height: i16,
|
||||
pub brush: Arc<dyn Brush>,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||
}
|
||||
|
||||
impl Operation for FlattenOperation {
|
||||
|
|
@ -131,7 +135,7 @@ impl Operation for FlattenOperation {
|
|||
|
||||
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
||||
|
||||
if self.before_snapshot.get().is_none() {
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
heights.push((idx, tile.heightmap[idx]));
|
||||
}
|
||||
|
||||
|
|
@ -145,15 +149,20 @@ impl Operation for FlattenOperation {
|
|||
}
|
||||
}
|
||||
|
||||
if self.before_snapshot.get().is_none() {
|
||||
let _ = self.before_snapshot.set(heights);
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
self.before_snapshot.lock().unwrap().replace(heights);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreHeightsOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::model::dimension::Dimension;
|
||||
use crate::model::tile::TILE_SIZE;
|
||||
|
|
@ -16,7 +15,7 @@ pub struct SmoothOperation {
|
|||
pub radius: u32,
|
||||
pub iterations: u32,
|
||||
pub brush: Arc<dyn Brush>,
|
||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
||||
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||
}
|
||||
|
||||
impl Operation for SmoothOperation {
|
||||
|
|
@ -51,7 +50,7 @@ impl Operation for SmoothOperation {
|
|||
if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
if self.before_snapshot.get().is_none() {
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
snapshot.push((
|
||||
(az as usize) * TILE_SIZE + (ax as usize),
|
||||
tile.heightmap[(az as usize) * TILE_SIZE + (ax as usize)],
|
||||
|
|
@ -59,8 +58,8 @@ impl Operation for SmoothOperation {
|
|||
}
|
||||
}
|
||||
}
|
||||
if self.before_snapshot.get().is_none() {
|
||||
let _ = self.before_snapshot.set(snapshot);
|
||||
if self.before_snapshot.lock().unwrap().is_none() {
|
||||
self.before_snapshot.lock().unwrap().replace(snapshot);
|
||||
}
|
||||
|
||||
// Apply smoothing for `iterations` passes
|
||||
|
|
@ -110,7 +109,12 @@ impl Operation for SmoothOperation {
|
|||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
let snapshot = self
|
||||
.before_snapshot
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(RestoreHeightsOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
mod lang;
|
||||
pub use lang::Language;
|
||||
|
|
@ -57,11 +57,6 @@ macro_rules! t {
|
|||
($key:expr) => {
|
||||
$crate::translate($key)
|
||||
};
|
||||
($key:expr, $($var:ident = $val:expr),+ $(,)?) => {{
|
||||
let vars: &[(&str, &str)] = &[$((stringify!($var), &$val as &dyn std::fmt::Display as &str)),+];
|
||||
// This won't work with runtime format. Use translate_with instead.
|
||||
$crate::translate_with($key, &[$((stringify!($var), &$val.as_ref() as &str)),+])
|
||||
}};
|
||||
}
|
||||
|
||||
/// Detect system language from environment variables (LANG, LANGUAGE, LC_ALL).
|
||||
|
|
@ -106,10 +101,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_translate_with_vars() {
|
||||
let result = translate_with("cli.new.created", &[
|
||||
("name", "test"),
|
||||
("tiles", "9"),
|
||||
]);
|
||||
let result = translate_with("cli.new.created", &[("name", "test"), ("tiles", "9")]);
|
||||
assert!(result.contains("test"));
|
||||
assert!(result.contains("9"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,11 +46,7 @@ fn host_log(mut caller: Caller<'_, WorldContext>, level: i32, ptr: i32, len: i32
|
|||
}
|
||||
|
||||
fn host_get_seed(caller: Caller<'_, WorldContext>) -> i64 {
|
||||
let (lo, hi) = (
|
||||
(caller.data().seed & 0xFFFF_FFFF) as i64,
|
||||
((caller.data().seed >> 32) & 0xFFFF_FFFF) as i64,
|
||||
);
|
||||
lo | (hi << 32)
|
||||
caller.data().seed as i64
|
||||
}
|
||||
|
||||
fn host_tile_size(_caller: Caller<'_, WorldContext>) -> i32 {
|
||||
|
|
|
|||
|
|
@ -71,8 +71,16 @@ impl eframe::App for TerrafierApp {
|
|||
&& let Some(tile) = world.dimensions[0].tiles.values().next()
|
||||
{
|
||||
renderer.render(
|
||||
&tile.heightmap.as_slice().try_into().unwrap_or_else(|_| [0; 16384]),
|
||||
&tile.terrain.as_slice().try_into().unwrap_or_else(|_| [0; 16384]),
|
||||
&tile
|
||||
.heightmap
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| [0; 16384]),
|
||||
&tile
|
||||
.terrain
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| [0; 16384]),
|
||||
self.show_heightmap,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
const TILE_SIZE: u32 = 128;
|
||||
const TILE_CELLS: u32 = TILE_SIZE * TILE_SIZE;
|
||||
const HEIGHTMAP_PACKED_SIZE: u64 = (TILE_CELLS / 2) as u64 * 4;
|
||||
const TERRAIN_PACKED_SIZE: u64 = (TILE_CELLS / 4) as u64 * 4;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Uniforms {
|
||||
|
|
@ -111,13 +116,13 @@ impl GpuRenderer {
|
|||
});
|
||||
let heightmap_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("heightmap_buffer"),
|
||||
size: 8192 * 4,
|
||||
size: HEIGHTMAP_PACKED_SIZE,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let terrain_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("terrain_buffer"),
|
||||
size: 4096 * 4,
|
||||
size: TERRAIN_PACKED_SIZE,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
max_tz = max_tz.max(tz);
|
||||
}
|
||||
|
||||
let base_size = 64u32;
|
||||
let base_size = 32u32;
|
||||
let zoom = app.view_zoom;
|
||||
|
||||
let grid_w = ((max_tx - min_tx + 1) * base_size as i32) as usize;
|
||||
|
|
@ -190,10 +190,7 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
|
||||
// Viewport: allocate a large area for the world with scroll
|
||||
let viewport_size = Vec2::new(grid_w as f32 * zoom, grid_h as f32 * zoom);
|
||||
let (response, painter) = ui.allocate_painter(
|
||||
viewport_size,
|
||||
egui::Sense::click_and_drag(),
|
||||
);
|
||||
let (response, painter) = ui.allocate_painter(viewport_size, egui::Sense::click_and_drag());
|
||||
|
||||
// Handle scroll-wheel zoom
|
||||
let scroll_delta = ui.input(|i| i.raw_scroll_delta);
|
||||
|
|
@ -215,7 +212,10 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
texture_id,
|
||||
egui::Rect::from_min_max(
|
||||
egui::pos2(rect.min.x + offset.x, rect.min.y + offset.y),
|
||||
egui::pos2(rect.min.x + offset.x + grid_w as f32 * app.view_zoom, rect.min.y + offset.y + grid_h as f32 * app.view_zoom),
|
||||
egui::pos2(
|
||||
rect.min.x + offset.x + grid_w as f32 * app.view_zoom,
|
||||
rect.min.y + offset.y + grid_h as f32 * app.view_zoom,
|
||||
),
|
||||
),
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
Color32::WHITE,
|
||||
|
|
@ -229,8 +229,7 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
{
|
||||
let bx = rect.min.x + offset.x + (sel_tx - min_tx) as f32 * scaled_size;
|
||||
let bz = rect.min.y + offset.y + (sel_tz - min_tz) as f32 * scaled_size;
|
||||
let border_rect =
|
||||
egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size));
|
||||
let border_rect = egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size));
|
||||
painter.rect_stroke(
|
||||
border_rect,
|
||||
0.0,
|
||||
|
|
@ -267,10 +266,12 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
let tz = (lz / scaled_size).floor() as i32 + min_tz;
|
||||
if dim.tiles.contains_key(&(tx, tz)) {
|
||||
app.selected_tile = Some((tx, tz));
|
||||
let local_x =
|
||||
((lx as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127);
|
||||
let local_z =
|
||||
((lz as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127);
|
||||
let local_x = ((lx as u32 % scaled_size as u32) * TILE_SIZE as u32
|
||||
/ scaled_size as u32)
|
||||
.min(127);
|
||||
let local_z = ((lz as u32 % scaled_size as u32) * TILE_SIZE as u32
|
||||
/ scaled_size as u32)
|
||||
.min(127);
|
||||
app.brush_local_x = Some(local_x);
|
||||
app.brush_local_z = Some(local_z);
|
||||
app.status_message = format!(
|
||||
|
|
|
|||
|
|
@ -5,16 +5,20 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
|||
let Some(ref world) = app.world else {
|
||||
return false;
|
||||
};
|
||||
let dim = &world.dimensions[0];
|
||||
let Some(dim) = world.dimensions.first() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// If world has more than one tile, fall back to CPU renderer
|
||||
if dim.tiles.len() > 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(ref renderer) = app.renderer
|
||||
&& let Some(texture_id) = renderer.texture_id
|
||||
{
|
||||
let desired_size = Vec2::new(512.0, 512.0);
|
||||
let (response, painter) = ui.allocate_painter(
|
||||
desired_size,
|
||||
egui::Sense::click_and_drag(),
|
||||
);
|
||||
let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::click_and_drag());
|
||||
let rect = response.rect;
|
||||
|
||||
// Handle scroll-wheel zoom
|
||||
|
|
@ -50,8 +54,10 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
|||
// GPU only renders the first tile - position it correctly
|
||||
// Use a default position for the rendered tile
|
||||
let first_tile_pos = dim.tiles.keys().next().copied().unwrap_or((0, 0));
|
||||
let tex_origin_x = rect.min.x + offset.x + (first_tile_pos.0 - min_tx) as f32 * display_size;
|
||||
let tex_origin_y = rect.min.y + offset.y + (first_tile_pos.1 - min_tz) as f32 * display_size;
|
||||
let tex_origin_x =
|
||||
rect.min.x + offset.x + (first_tile_pos.0 - min_tx) as f32 * display_size;
|
||||
let tex_origin_y =
|
||||
rect.min.y + offset.y + (first_tile_pos.1 - min_tz) as f32 * display_size;
|
||||
|
||||
let tex_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(tex_origin_x, tex_origin_y),
|
||||
|
|
@ -82,10 +88,14 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
|||
{
|
||||
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size;
|
||||
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size;
|
||||
let brush_x =
|
||||
rect.min.x + offset.x + tile_x_in_pixels + (bx as f32 * display_size / TILE_SIZE as f32);
|
||||
let brush_z =
|
||||
rect.min.y + offset.y + tile_z_in_pixels + (bz as f32 * display_size / TILE_SIZE as f32);
|
||||
let brush_x = rect.min.x
|
||||
+ offset.x
|
||||
+ tile_x_in_pixels
|
||||
+ (bx as f32 * display_size / TILE_SIZE as f32);
|
||||
let brush_z = rect.min.y
|
||||
+ offset.y
|
||||
+ tile_z_in_pixels
|
||||
+ (bz as f32 * display_size / TILE_SIZE as f32);
|
||||
let brush_radius_px = app.brush_radius as f32 * display_size / TILE_SIZE as f32;
|
||||
painter.circle_stroke(
|
||||
egui::pos2(brush_x, brush_z),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue