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,
|
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 validate || dry_run {
|
||||||
if 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(
|
output::print_result(
|
||||||
format,
|
format,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ pub fn cmd_import(
|
||||||
|
|
||||||
if validate || dry_run {
|
if validate || dry_run {
|
||||||
if 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)?;
|
terrafier_core::io::import::validate_save(input_path)?;
|
||||||
output::print_result(
|
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 bar = progress::new_bar(region_count.max(1) as u64, &importing_msg);
|
||||||
let world = terrafier_core::io::import::import(input_path)?;
|
let world = terrafier_core::io::import::import(input_path)?;
|
||||||
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
let tile_count: usize = world.dimensions.iter().map(|d| d.tiles.len()).sum();
|
||||||
let import_msg = i18n::translate_with("cli.import.complete", &[
|
let import_msg = i18n::translate_with(
|
||||||
("tiles", &tile_count.to_string()),
|
"cli.import.complete",
|
||||||
("dimensions", &world.dimensions.len().to_string()),
|
&[
|
||||||
]);
|
("tiles", &tile_count.to_string()),
|
||||||
|
("dimensions", &world.dimensions.len().to_string()),
|
||||||
|
],
|
||||||
|
);
|
||||||
progress::finish_with(&import_msg, bar);
|
progress::finish_with(&import_msg, bar);
|
||||||
|
|
||||||
let output_path = Path::new(&args.output);
|
let output_path = Path::new(&args.output);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ pub fn cmd_render(
|
||||||
|
|
||||||
if validate || dry_run {
|
if validate || dry_run {
|
||||||
if 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(
|
output::print_result(
|
||||||
format,
|
format,
|
||||||
|
|
@ -50,9 +53,10 @@ pub fn cmd_render(
|
||||||
|
|
||||||
let bar = progress::new_bar(tile_count as u64, i18n::t!("cli.render.rendering"));
|
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)?;
|
terrafier_core::io::export::render_to_image(&world, &args.output, args.scale)?;
|
||||||
let saved_msg = i18n::translate_with("cli.render.saved", &[
|
let saved_msg = i18n::translate_with(
|
||||||
("path", &args.output.display().to_string()),
|
"cli.render.saved",
|
||||||
]);
|
&[("path", &args.output.display().to_string())],
|
||||||
|
);
|
||||||
progress::finish_with(&saved_msg, bar);
|
progress::finish_with(&saved_msg, bar);
|
||||||
|
|
||||||
log::info!("Preview rendered to: {}", args.output.display());
|
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 spinner = progress::new_spinner(i18n::t!("cli.new.creating"));
|
||||||
let world = World::new(&args.name, args.seed);
|
let world = World::new(&args.name, args.seed);
|
||||||
let tile_count = world.dimensions.iter().map(|d| d.tiles.len()).sum::<usize>();
|
let tile_count = world
|
||||||
let created_msg = i18n::translate_with("cli.new.created", &[
|
.dimensions
|
||||||
("name", &args.name),
|
.iter()
|
||||||
("tiles", &tile_count.to_string()),
|
.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);
|
progress::finish_with(&created_msg, spinner);
|
||||||
|
|
||||||
let world_dir = args.output.join(&args.name);
|
let world_dir = args.output.join(&args.name);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
mod cmds;
|
mod cmds;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
@ -43,7 +42,7 @@ enum Commands {
|
||||||
#[cfg(feature = "scripting")]
|
#[cfg(feature = "scripting")]
|
||||||
Run {
|
Run {
|
||||||
/// Path to .rhai script file
|
/// 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);
|
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
i18n::translate_with("cli.new.created", &[
|
i18n::translate_with(
|
||||||
("name", name),
|
"cli.new.created",
|
||||||
("tiles", &tiles.to_string()),
|
&[("name", name), ("tiles", &tiles.to_string()),]
|
||||||
])
|
)
|
||||||
);
|
);
|
||||||
let _ = seed;
|
let _ = seed;
|
||||||
}
|
}
|
||||||
|
|
@ -48,10 +48,10 @@ fn print_human(result: &Value) {
|
||||||
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
let tiles = result["tiles"].as_u64().unwrap_or(0);
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
i18n::translate_with("cli.export.complete", &[
|
i18n::translate_with(
|
||||||
("regions", "?"),
|
"cli.export.complete",
|
||||||
("chunks", &tiles.to_string()),
|
&[("regions", "?"), ("chunks", &tiles.to_string()),]
|
||||||
])
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
"imported" => {
|
"imported" => {
|
||||||
|
|
@ -60,10 +60,13 @@ fn print_human(result: &Value) {
|
||||||
let dims = result["dimensions"].as_u64().unwrap_or(0);
|
let dims = result["dimensions"].as_u64().unwrap_or(0);
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
i18n::translate_with("cli.import.complete", &[
|
i18n::translate_with(
|
||||||
("tiles", &tiles.to_string()),
|
"cli.import.complete",
|
||||||
("dimensions", &dims.to_string()),
|
&[
|
||||||
])
|
("tiles", &tiles.to_string()),
|
||||||
|
("dimensions", &dims.to_string()),
|
||||||
|
]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
let _ = name;
|
let _ = name;
|
||||||
}
|
}
|
||||||
|
|
@ -71,9 +74,7 @@ fn print_human(result: &Value) {
|
||||||
let output = result["output"].as_str().unwrap_or("?");
|
let output = result["output"].as_str().unwrap_or("?");
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
i18n::translate_with("cli.render.saved", &[
|
i18n::translate_with("cli.render.saved", &[("path", output),])
|
||||||
("path", output),
|
|
||||||
])
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
"validated" => {
|
"validated" => {
|
||||||
|
|
@ -125,17 +126,15 @@ fn print_human_info(result: &Value) {
|
||||||
if let Some(dims) = result.get("dimensions").and_then(|v| v.as_array()) {
|
if let Some(dims) = result.get("dimensions").and_then(|v| v.as_array()) {
|
||||||
println!(
|
println!(
|
||||||
" {}",
|
" {}",
|
||||||
i18n::translate_with("cli.info.dimensions", &[
|
i18n::translate_with(
|
||||||
("dimensions", &dims.len().to_string()),
|
"cli.info.dimensions",
|
||||||
])
|
&[("dimensions", &dims.len().to_string()),]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
for dim in dims {
|
for dim in dims {
|
||||||
let dim_name = dim["name"].as_str().unwrap_or("?");
|
let dim_name = dim["name"].as_str().unwrap_or("?");
|
||||||
let dim_tiles = dim["tiles"].as_u64().unwrap_or(0);
|
let dim_tiles = dim["tiles"].as_u64().unwrap_or(0);
|
||||||
println!(
|
println!(" - {} ({} tiles)", dim_name, dim_tiles);
|
||||||
" - {} ({} tiles)",
|
|
||||||
dim_name, dim_tiles
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(tiles) = result.get("total_tiles").and_then(|v| v.as_u64()) {
|
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)]
|
#[allow(clippy::type_complexity)]
|
||||||
type RegionTiles<'a> = BTreeMap<(i32, i32), Vec<(&'a (i32, i32), &'a Tile)>>;
|
type RegionTiles<'a> = BTreeMap<(i32, i32), Vec<(&'a (i32, i32), &'a Tile)>>;
|
||||||
|
|
||||||
/// Save a Terrafier World to a Minecraft save directory.
|
fn dim_region_path(output_path: &Path, dim_name: &str) -> std::path::PathBuf {
|
||||||
pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
match dim_name {
|
||||||
fs::create_dir_all(output_path.join("region"))?;
|
"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)?;
|
fn write_dimension(
|
||||||
let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?;
|
dim: &crate::model::dimension::Dimension,
|
||||||
fs::write(output_path.join("level.dat"), &level_bytes)?;
|
output_path: &Path,
|
||||||
|
build_chunk: &dyn Fn(i32, i32, &Tile, usize, usize) -> Result<Vec<u8>>,
|
||||||
let dim = match world.dimensions.first() {
|
) -> Result<()> {
|
||||||
Some(d) => d,
|
let region_path = dim_region_path(output_path, &dim.name);
|
||||||
None => return Ok(()),
|
fs::create_dir_all(®ion_path)?;
|
||||||
};
|
|
||||||
|
|
||||||
let mut regions: RegionTiles = BTreeMap::new();
|
let mut regions: RegionTiles = BTreeMap::new();
|
||||||
for (key, tile) in &dim.tiles {
|
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_x = (chunk_x & 31) as u8;
|
||||||
let region_local_z = (chunk_z & 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() {
|
if chunk_data.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -60,7 +63,22 @@ pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
||||||
|
|
||||||
let region_bytes = region.to_bytes()?;
|
let region_bytes = region.to_bytes()?;
|
||||||
let file_name = format!("r.{}.{}.mca", rx, rz);
|
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(())
|
Ok(())
|
||||||
|
|
@ -72,58 +90,14 @@ pub fn save_world_with_layers(
|
||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
layer_exporters: &[&dyn LayerExport],
|
layer_exporters: &[&dyn LayerExport],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
fs::create_dir_all(output_path.join("region"))?;
|
|
||||||
|
|
||||||
let level_tag = build_level_dat(world)?;
|
let level_tag = build_level_dat(world)?;
|
||||||
let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?;
|
let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?;
|
||||||
fs::write(output_path.join("level.dat"), &level_bytes)?;
|
fs::write(output_path.join("level.dat"), &level_bytes)?;
|
||||||
|
|
||||||
let dim = match world.dimensions.first() {
|
for dim in &world.dimensions {
|
||||||
Some(d) => d,
|
write_dimension(dim, output_path, &|cx, cz, tile, clx, clz| {
|
||||||
None => return Ok(()),
|
build_chunk_nbt_with_layers(cx, cz, tile, clx, clz, layer_exporters)
|
||||||
};
|
})?;
|
||||||
|
|
||||||
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)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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("LevelName".into(), Tag::String(world.name.clone()));
|
||||||
data.insert("DataVersion".into(), Tag::Int(data_version()));
|
data.insert("DataVersion".into(), Tag::Int(data_version()));
|
||||||
data.insert("version".into(), Tag::Int(19133));
|
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();
|
let mut world_gen = HashMap::new();
|
||||||
world_gen.insert("seed".into(), Tag::Long(world.seed as i64));
|
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();
|
let mut sorted: Vec<_> = chunk.sections.iter().collect();
|
||||||
sorted.sort_by_key(|b| std::cmp::Reverse(b.section_y));
|
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() {
|
if section.palette.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_blocks = section.palette.iter().any(|p| {
|
let non_air_indices: Vec<usize> = section
|
||||||
p.get("Name").is_some_and(|n| {
|
.palette
|
||||||
matches!(
|
.iter()
|
||||||
n,
|
.enumerate()
|
||||||
terrafier_nbt::Tag::String(s)
|
.filter(|(_, p)| {
|
||||||
if s != "minecraft:air"
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if section.block_data.is_empty() {
|
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);
|
surface_y = Some((section.section_y as i32) * 16 + 15);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
surface_y = Some((section.section_y as i32) * 16 + 15);
|
let bpe = std::cmp::max(
|
||||||
break;
|
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.
|
/// Save a `World` to a binary file.
|
||||||
pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
||||||
let config = bincode::config::standard();
|
let config = bincode::config::standard();
|
||||||
let bytes =
|
let bytes = bincode::serde::encode_to_vec(world, config)
|
||||||
bincode::serde::encode_to_vec(world, config).map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
.map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
||||||
|
|
||||||
std::fs::write(path, &bytes)?;
|
std::fs::write(path, &bytes)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -38,9 +38,8 @@ pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
||||||
pub fn load_binary(path: &Path) -> Result<World> {
|
pub fn load_binary(path: &Path) -> Result<World> {
|
||||||
let bytes = std::fs::read(path)?;
|
let bytes = std::fs::read(path)?;
|
||||||
let config = bincode::config::standard();
|
let config = bincode::config::standard();
|
||||||
let (world, _): (World, usize) =
|
let (world, _): (World, usize) = bincode::serde::decode_from_slice(&bytes, config)
|
||||||
bincode::serde::decode_from_slice(&bytes, config)
|
.map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
||||||
.map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
|
||||||
Ok(world)
|
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>())
|
&& let (Ok(tx), Ok(tz)) = (tx_s.parse::<i32>(), tz_s.parse::<i32>())
|
||||||
{
|
{
|
||||||
tiles.insert((tx, tz), tile);
|
tiles.insert((tx, tz), tile);
|
||||||
|
} else {
|
||||||
|
log::warn!("Skipping dimension tile with malformed key: {}", key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::{TILE_SIZE, Tile};
|
use crate::model::tile::{TILE_SIZE, Tile};
|
||||||
use crate::model::types::Terrain;
|
use crate::model::types::Terrain;
|
||||||
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
|
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.
|
/// Determines whether a cell should be modified by an operation.
|
||||||
pub trait Filter: Send + Sync {
|
pub trait Filter: Send + Sync {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
@ -121,7 +121,7 @@ pub struct FilteredOperation {
|
||||||
pub center_x: u32,
|
pub center_x: u32,
|
||||||
pub center_z: u32,
|
pub center_z: u32,
|
||||||
pub radius: u32,
|
pub radius: u32,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for FilteredOperation {
|
impl Operation for FilteredOperation {
|
||||||
|
|
@ -130,7 +130,7 @@ impl Operation for FilteredOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
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 snapshot: Vec<(usize, i16)> = if is_first_apply {
|
||||||
let tile =
|
let tile =
|
||||||
|
|
@ -159,10 +159,14 @@ impl Operation for FilteredOperation {
|
||||||
snap.push((idx, tile.heightmap[idx]));
|
snap.push((idx, tile.heightmap[idx]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = self.before_snapshot.set(snap.clone());
|
self.before_snapshot.lock().unwrap().replace(snap.clone());
|
||||||
snap
|
snap
|
||||||
} else {
|
} else {
|
||||||
self.before_snapshot.get().cloned().unwrap_or_default()
|
self.before_snapshot
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.operation.apply(dim)?;
|
self.operation.apply(dim)?;
|
||||||
|
|
@ -189,7 +193,12 @@ impl Operation for FilteredOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
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 {
|
Box::new(RestoreHeightsOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::model::tile::TILE_SIZE;
|
||||||
use crate::model::types::Brush;
|
use crate::model::types::Brush;
|
||||||
use crate::model::types::Terrain;
|
use crate::model::types::Terrain;
|
||||||
use crate::ops::operations::{Operation, OperationError, RestoreTerrainOperation};
|
use crate::ops::operations::{Operation, OperationError, RestoreTerrainOperation};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
struct BrushApply<'a> {
|
struct BrushApply<'a> {
|
||||||
tile: &'a mut crate::model::tile::Tile,
|
tile: &'a mut crate::model::tile::Tile,
|
||||||
|
|
@ -12,7 +12,7 @@ struct BrushApply<'a> {
|
||||||
radius: u32,
|
radius: u32,
|
||||||
terrain_id: u8,
|
terrain_id: u8,
|
||||||
brush: &'a dyn Brush,
|
brush: &'a dyn Brush,
|
||||||
snapshot: &'a OnceLock<Vec<(usize, u8)>>,
|
snapshot: &'a Mutex<Option<Vec<(usize, u8)>>>,
|
||||||
snapshot_buf: &'a mut 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 {
|
if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 {
|
||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
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.snapshot_buf.push((idx, args.tile.terrain[idx]));
|
||||||
}
|
}
|
||||||
args.tile.terrain[idx] = args.terrain_id;
|
args.tile.terrain[idx] = args.terrain_id;
|
||||||
|
|
@ -46,7 +46,7 @@ pub struct PaintOperation {
|
||||||
pub radius: u32,
|
pub radius: u32,
|
||||||
pub terrain: Terrain,
|
pub terrain: Terrain,
|
||||||
pub brush: Arc<dyn Brush>,
|
pub brush: Arc<dyn Brush>,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for PaintOperation {
|
impl Operation for PaintOperation {
|
||||||
|
|
@ -77,66 +77,19 @@ impl Operation for PaintOperation {
|
||||||
snapshot_buf: &mut snapshot_buf,
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
fn inverse(&self) -> Box<dyn Operation> {
|
||||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
let snapshot = self
|
||||||
Box::new(RestoreTerrainOperation {
|
.before_snapshot
|
||||||
tile_x: self.tile_x,
|
.lock()
|
||||||
tile_z: self.tile_z,
|
.unwrap()
|
||||||
snapshot,
|
.clone()
|
||||||
})
|
.unwrap_or_default();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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();
|
|
||||||
Box::new(RestoreTerrainOperation {
|
Box::new(RestoreTerrainOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
@ -152,7 +105,7 @@ pub struct PencilOperation {
|
||||||
pub center_x: u32,
|
pub center_x: u32,
|
||||||
pub center_z: u32,
|
pub center_z: u32,
|
||||||
pub terrain: Terrain,
|
pub terrain: Terrain,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for PencilOperation {
|
impl Operation for PencilOperation {
|
||||||
|
|
@ -183,8 +136,11 @@ impl Operation for PencilOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
let idx = cz * TILE_SIZE + cx;
|
let idx = cz * TILE_SIZE + cx;
|
||||||
if self.before_snapshot.get().is_none() {
|
if self.before_snapshot.lock().unwrap().is_none() {
|
||||||
let _ = self.before_snapshot.set(vec![(idx, tile.terrain[idx])]);
|
self.before_snapshot
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.replace(vec![(idx, tile.terrain[idx])]);
|
||||||
}
|
}
|
||||||
tile.terrain[idx] = self.terrain as u8;
|
tile.terrain[idx] = self.terrain as u8;
|
||||||
|
|
||||||
|
|
@ -192,7 +148,12 @@ impl Operation for PencilOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
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 {
|
Box::new(RestoreTerrainOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::OnceLock;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
|
@ -14,7 +14,7 @@ pub struct FillOperation {
|
||||||
pub center_x: u32,
|
pub center_x: u32,
|
||||||
pub center_z: u32,
|
pub center_z: u32,
|
||||||
pub terrain: Terrain,
|
pub terrain: Terrain,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, u8)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, u8)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for FillOperation {
|
impl Operation for FillOperation {
|
||||||
|
|
@ -60,7 +60,7 @@ impl Operation for FillOperation {
|
||||||
|
|
||||||
while let Some((x, z)) = queue.pop_front() {
|
while let Some((x, z)) = queue.pop_front() {
|
||||||
let i = z * TILE_SIZE + x;
|
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]));
|
snapshot.push((i, tile.terrain[i]));
|
||||||
}
|
}
|
||||||
tile.terrain[i] = target_terrain;
|
tile.terrain[i] = target_terrain;
|
||||||
|
|
@ -80,15 +80,20 @@ impl Operation for FillOperation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.before_snapshot.get().is_none() {
|
if self.before_snapshot.lock().unwrap().is_none() {
|
||||||
let _ = self.before_snapshot.set(snapshot);
|
self.before_snapshot.lock().unwrap().replace(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
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 {
|
Box::new(RestoreTerrainOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ mod brush;
|
||||||
mod fill;
|
mod fill;
|
||||||
mod shaping;
|
mod shaping;
|
||||||
|
|
||||||
pub use brush::{FloodOperation, PaintOperation, PencilOperation};
|
pub use brush::{PaintOperation, PencilOperation};
|
||||||
pub use fill::FillOperation;
|
pub use fill::FillOperation;
|
||||||
pub use shaping::{
|
pub use shaping::{
|
||||||
ErodeOperation, FlattenOperation, HeightOperation, MultiTileOperation, SmoothOperation,
|
ErodeOperation, FlattenOperation, HeightOperation, MultiTileOperation, SmoothOperation,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
|
@ -17,7 +16,7 @@ pub struct ErodeOperation {
|
||||||
pub iterations: u32,
|
pub iterations: u32,
|
||||||
pub talus_angle: f64,
|
pub talus_angle: f64,
|
||||||
pub brush: Arc<dyn Brush>,
|
pub brush: Arc<dyn Brush>,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for ErodeOperation {
|
impl Operation for ErodeOperation {
|
||||||
|
|
@ -42,7 +41,7 @@ impl Operation for ErodeOperation {
|
||||||
|
|
||||||
// Snapshot original heights on first apply
|
// Snapshot original heights on first apply
|
||||||
let mut snapshot: Vec<(usize, i16)> = Vec::new();
|
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 {
|
if is_first_apply {
|
||||||
for dz in -r..=r {
|
for dz in -r..=r {
|
||||||
for dx in -r..=r {
|
for dx in -r..=r {
|
||||||
|
|
@ -60,7 +59,7 @@ impl Operation for ErodeOperation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if is_first_apply {
|
if is_first_apply {
|
||||||
let _ = self.before_snapshot.set(snapshot);
|
self.before_snapshot.lock().unwrap().replace(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply erosion for `iterations` passes
|
// Apply erosion for `iterations` passes
|
||||||
|
|
@ -142,7 +141,12 @@ impl Operation for ErodeOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
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 {
|
Box::new(RestoreHeightsOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
|
@ -16,7 +15,7 @@ pub struct HeightOperation {
|
||||||
pub radius: u32,
|
pub radius: u32,
|
||||||
pub delta: i16,
|
pub delta: i16,
|
||||||
pub brush: Arc<dyn Brush>,
|
pub brush: Arc<dyn Brush>,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for HeightOperation {
|
impl Operation for HeightOperation {
|
||||||
|
|
@ -55,7 +54,7 @@ impl Operation for HeightOperation {
|
||||||
|
|
||||||
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
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]));
|
heights.push((idx, tile.heightmap[idx]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,15 +65,20 @@ impl Operation for HeightOperation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.before_snapshot.get().is_none() {
|
if self.before_snapshot.lock().unwrap().is_none() {
|
||||||
let _ = self.before_snapshot.set(heights);
|
self.before_snapshot.lock().unwrap().replace(heights);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
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 {
|
Box::new(RestoreHeightsOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
@ -92,7 +96,7 @@ pub struct FlattenOperation {
|
||||||
pub radius: u32,
|
pub radius: u32,
|
||||||
pub target_height: i16,
|
pub target_height: i16,
|
||||||
pub brush: Arc<dyn Brush>,
|
pub brush: Arc<dyn Brush>,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for FlattenOperation {
|
impl Operation for FlattenOperation {
|
||||||
|
|
@ -131,7 +135,7 @@ impl Operation for FlattenOperation {
|
||||||
|
|
||||||
let idx = (az as usize) * TILE_SIZE + (ax as usize);
|
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]));
|
heights.push((idx, tile.heightmap[idx]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,15 +149,20 @@ impl Operation for FlattenOperation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.before_snapshot.get().is_none() {
|
if self.before_snapshot.lock().unwrap().is_none() {
|
||||||
let _ = self.before_snapshot.set(heights);
|
self.before_snapshot.lock().unwrap().replace(heights);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
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 {
|
Box::new(RestoreHeightsOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use crate::model::dimension::Dimension;
|
use crate::model::dimension::Dimension;
|
||||||
use crate::model::tile::TILE_SIZE;
|
use crate::model::tile::TILE_SIZE;
|
||||||
|
|
@ -16,7 +15,7 @@ pub struct SmoothOperation {
|
||||||
pub radius: u32,
|
pub radius: u32,
|
||||||
pub iterations: u32,
|
pub iterations: u32,
|
||||||
pub brush: Arc<dyn Brush>,
|
pub brush: Arc<dyn Brush>,
|
||||||
pub before_snapshot: OnceLock<Vec<(usize, i16)>>,
|
pub before_snapshot: Mutex<Option<Vec<(usize, i16)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for SmoothOperation {
|
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 {
|
if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if self.before_snapshot.get().is_none() {
|
if self.before_snapshot.lock().unwrap().is_none() {
|
||||||
snapshot.push((
|
snapshot.push((
|
||||||
(az as usize) * TILE_SIZE + (ax as usize),
|
(az as usize) * TILE_SIZE + (ax as usize),
|
||||||
tile.heightmap[(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() {
|
if self.before_snapshot.lock().unwrap().is_none() {
|
||||||
let _ = self.before_snapshot.set(snapshot);
|
self.before_snapshot.lock().unwrap().replace(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply smoothing for `iterations` passes
|
// Apply smoothing for `iterations` passes
|
||||||
|
|
@ -110,7 +109,12 @@ impl Operation for SmoothOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse(&self) -> Box<dyn Operation> {
|
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 {
|
Box::new(RestoreHeightsOperation {
|
||||||
tile_x: self.tile_x,
|
tile_x: self.tile_x,
|
||||||
tile_z: self.tile_z,
|
tile_z: self.tile_z,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
|
||||||
mod lang;
|
mod lang;
|
||||||
pub use lang::Language;
|
pub use lang::Language;
|
||||||
|
|
@ -57,11 +57,6 @@ macro_rules! t {
|
||||||
($key:expr) => {
|
($key:expr) => {
|
||||||
$crate::translate($key)
|
$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).
|
/// Detect system language from environment variables (LANG, LANGUAGE, LC_ALL).
|
||||||
|
|
@ -106,10 +101,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_translate_with_vars() {
|
fn test_translate_with_vars() {
|
||||||
let result = translate_with("cli.new.created", &[
|
let result = translate_with("cli.new.created", &[("name", "test"), ("tiles", "9")]);
|
||||||
("name", "test"),
|
|
||||||
("tiles", "9"),
|
|
||||||
]);
|
|
||||||
assert!(result.contains("test"));
|
assert!(result.contains("test"));
|
||||||
assert!(result.contains("9"));
|
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 {
|
fn host_get_seed(caller: Caller<'_, WorldContext>) -> i64 {
|
||||||
let (lo, hi) = (
|
caller.data().seed as i64
|
||||||
(caller.data().seed & 0xFFFF_FFFF) as i64,
|
|
||||||
((caller.data().seed >> 32) & 0xFFFF_FFFF) as i64,
|
|
||||||
);
|
|
||||||
lo | (hi << 32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn host_tile_size(_caller: Caller<'_, WorldContext>) -> i32 {
|
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()
|
&& let Some(tile) = world.dimensions[0].tiles.values().next()
|
||||||
{
|
{
|
||||||
renderer.render(
|
renderer.render(
|
||||||
&tile.heightmap.as_slice().try_into().unwrap_or_else(|_| [0; 16384]),
|
&tile
|
||||||
&tile.terrain.as_slice().try_into().unwrap_or_else(|_| [0; 16384]),
|
.heightmap
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or_else(|_| [0; 16384]),
|
||||||
|
&tile
|
||||||
|
.terrain
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or_else(|_| [0; 16384]),
|
||||||
self.show_heightmap,
|
self.show_heightmap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
use std::sync::Arc;
|
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)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Uniforms {
|
pub struct Uniforms {
|
||||||
|
|
@ -111,13 +116,13 @@ impl GpuRenderer {
|
||||||
});
|
});
|
||||||
let heightmap_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let heightmap_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("heightmap_buffer"),
|
label: Some("heightmap_buffer"),
|
||||||
size: 8192 * 4,
|
size: HEIGHTMAP_PACKED_SIZE,
|
||||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
let terrain_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let terrain_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("terrain_buffer"),
|
label: Some("terrain_buffer"),
|
||||||
size: 4096 * 4,
|
size: TERRAIN_PACKED_SIZE,
|
||||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
mapped_at_creation: false,
|
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);
|
max_tz = max_tz.max(tz);
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_size = 64u32;
|
let base_size = 32u32;
|
||||||
let zoom = app.view_zoom;
|
let zoom = app.view_zoom;
|
||||||
|
|
||||||
let grid_w = ((max_tx - min_tx + 1) * base_size as i32) as usize;
|
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
|
// 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 viewport_size = Vec2::new(grid_w as f32 * zoom, grid_h as f32 * zoom);
|
||||||
let (response, painter) = ui.allocate_painter(
|
let (response, painter) = ui.allocate_painter(viewport_size, egui::Sense::click_and_drag());
|
||||||
viewport_size,
|
|
||||||
egui::Sense::click_and_drag(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle scroll-wheel zoom
|
// Handle scroll-wheel zoom
|
||||||
let scroll_delta = ui.input(|i| i.raw_scroll_delta);
|
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,
|
texture_id,
|
||||||
egui::Rect::from_min_max(
|
egui::Rect::from_min_max(
|
||||||
egui::pos2(rect.min.x + offset.x, rect.min.y + offset.y),
|
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)),
|
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||||
Color32::WHITE,
|
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 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 bz = rect.min.y + offset.y + (sel_tz - min_tz) as f32 * scaled_size;
|
||||||
let border_rect =
|
let border_rect = egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size));
|
||||||
egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size));
|
|
||||||
painter.rect_stroke(
|
painter.rect_stroke(
|
||||||
border_rect,
|
border_rect,
|
||||||
0.0,
|
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;
|
let tz = (lz / scaled_size).floor() as i32 + min_tz;
|
||||||
if dim.tiles.contains_key(&(tx, tz)) {
|
if dim.tiles.contains_key(&(tx, tz)) {
|
||||||
app.selected_tile = Some((tx, tz));
|
app.selected_tile = Some((tx, tz));
|
||||||
let local_x =
|
let local_x = ((lx as u32 % scaled_size as u32) * TILE_SIZE as u32
|
||||||
((lx as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127);
|
/ scaled_size as u32)
|
||||||
let local_z =
|
.min(127);
|
||||||
((lz 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_x = Some(local_x);
|
||||||
app.brush_local_z = Some(local_z);
|
app.brush_local_z = Some(local_z);
|
||||||
app.status_message = format!(
|
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 {
|
let Some(ref world) = app.world else {
|
||||||
return false;
|
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
|
if let Some(ref renderer) = app.renderer
|
||||||
&& let Some(texture_id) = renderer.texture_id
|
&& let Some(texture_id) = renderer.texture_id
|
||||||
{
|
{
|
||||||
let desired_size = Vec2::new(512.0, 512.0);
|
let desired_size = Vec2::new(512.0, 512.0);
|
||||||
let (response, painter) = ui.allocate_painter(
|
let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::click_and_drag());
|
||||||
desired_size,
|
|
||||||
egui::Sense::click_and_drag(),
|
|
||||||
);
|
|
||||||
let rect = response.rect;
|
let rect = response.rect;
|
||||||
|
|
||||||
// Handle scroll-wheel zoom
|
// 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
|
// GPU only renders the first tile - position it correctly
|
||||||
// Use a default position for the rendered tile
|
// Use a default position for the rendered tile
|
||||||
let first_tile_pos = dim.tiles.keys().next().copied().unwrap_or((0, 0));
|
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_x =
|
||||||
let tex_origin_y = rect.min.y + offset.y + (first_tile_pos.1 - min_tz) as f32 * display_size;
|
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(
|
let tex_rect = egui::Rect::from_min_size(
|
||||||
egui::pos2(tex_origin_x, tex_origin_y),
|
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_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 tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size;
|
||||||
let brush_x =
|
let brush_x = rect.min.x
|
||||||
rect.min.x + offset.x + tile_x_in_pixels + (bx as f32 * display_size / TILE_SIZE as f32);
|
+ offset.x
|
||||||
let brush_z =
|
+ tile_x_in_pixels
|
||||||
rect.min.y + offset.y + tile_z_in_pixels + (bz as f32 * display_size / TILE_SIZE as f32);
|
+ (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;
|
let brush_radius_px = app.brush_radius as f32 * display_size / TILE_SIZE as f32;
|
||||||
painter.circle_stroke(
|
painter.circle_stroke(
|
||||||
egui::pos2(brush_x, brush_z),
|
egui::pos2(brush_x, brush_z),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue