WASM plugins, scripting, i18n, GUI pan/zoom, cargo-dist CI/CD
- WASM plugin host (wasmtime 46, 17 host fns) + example noisemap plugin - Rhai scripting engine (12 API fns, `terrafier run` cmd) - i18n: en/ru translations, auto-detect, t! macro - GUI: pan (click-drag) + zoom (scroll wheel) with viewport transform - cargo-dist 0.31: MSI/pkg/shell installers, both binaries - CI: .github/workflows/ci.yml + release.yml - Security: wasmtime 30→46, bincode 1.3→2.0, quick-xml CVE docs - Code review: SAFETY comments, unwrap→match cleanup - Fixes: clippy approx_constant, needless_borrows, rustfmt
This commit is contained in:
parent
bdd4564240
commit
18613260a8
100 changed files with 4262 additions and 585 deletions
|
|
@ -74,6 +74,8 @@ pub struct TerrafierApp {
|
|||
pub renderer: Option<GpuRenderer>,
|
||||
pub show_gpu_render: bool,
|
||||
pub gpu_status: GpuStatus,
|
||||
pub view_offset: egui::Vec2,
|
||||
pub view_zoom: f32,
|
||||
pub cpu_texture_handle: Option<egui::TextureHandle>,
|
||||
pub layer_visible: [bool; 6],
|
||||
}
|
||||
|
|
@ -96,7 +98,10 @@ impl TerrafierApp {
|
|||
gpu_renderer.register_texture(&mut renderer_lock);
|
||||
(Some(gpu_renderer), GpuStatus::Enabled)
|
||||
} else {
|
||||
(None, GpuStatus::FallbackCpu("GPU init failed — no wgpu adapter found"))
|
||||
(
|
||||
None,
|
||||
GpuStatus::FallbackCpu("GPU init failed — no wgpu adapter found"),
|
||||
)
|
||||
};
|
||||
|
||||
Self {
|
||||
|
|
@ -124,9 +129,10 @@ impl TerrafierApp {
|
|||
renderer,
|
||||
show_gpu_render: true,
|
||||
gpu_status,
|
||||
view_offset: egui::Vec2::ZERO,
|
||||
view_zoom: 1.0,
|
||||
cpu_texture_handle: None,
|
||||
layer_visible: [false; 6],
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,20 @@ impl TerrafierApp {
|
|||
pub fn undo(&mut self) {
|
||||
if let Some(op) = self.undo_stack.pop_back() {
|
||||
if let Some(ref mut world) = self.world {
|
||||
let dim = world.overworld_mut().expect("world has no overworld");
|
||||
let inv = op.inverse();
|
||||
if let Err(e) = inv.apply(dim) {
|
||||
self.status_message = format!("Undo error: {:?}", e);
|
||||
self.undo_stack.push_back(op);
|
||||
if let Some(dim) = world.overworld_mut() {
|
||||
let inv = op.inverse();
|
||||
if let Err(e) = inv.apply(dim) {
|
||||
self.status_message = format!("Undo error: {:?}", e);
|
||||
self.undo_stack.push_back(op);
|
||||
return;
|
||||
}
|
||||
self.redo_stack.push(op);
|
||||
self.status_message = "Undo".to_string();
|
||||
return;
|
||||
}
|
||||
self.redo_stack.push(op);
|
||||
self.status_message = "Undo".to_string();
|
||||
}
|
||||
self.undo_stack.push_back(op);
|
||||
self.status_message = "Undo error: world has no overworld".to_string();
|
||||
} else {
|
||||
self.status_message = "Nothing to undo".to_string();
|
||||
}
|
||||
|
|
@ -32,15 +36,19 @@ impl TerrafierApp {
|
|||
pub fn redo(&mut self) {
|
||||
if let Some(op) = self.redo_stack.pop() {
|
||||
if let Some(ref mut world) = self.world {
|
||||
let dim = world.overworld_mut().expect("world has no overworld");
|
||||
if let Err(e) = op.apply(dim) {
|
||||
self.status_message = format!("Redo error: {:?}", e);
|
||||
self.redo_stack.push(op);
|
||||
if let Some(dim) = world.overworld_mut() {
|
||||
if let Err(e) = op.apply(dim) {
|
||||
self.status_message = format!("Redo error: {:?}", e);
|
||||
self.redo_stack.push(op);
|
||||
return;
|
||||
}
|
||||
self.undo_stack.push_back(op);
|
||||
self.status_message = "Redo".to_string();
|
||||
return;
|
||||
}
|
||||
self.undo_stack.push_back(op);
|
||||
self.status_message = "Redo".to_string();
|
||||
}
|
||||
self.redo_stack.push(op);
|
||||
self.status_message = "Redo error: world has no overworld".to_string();
|
||||
} else {
|
||||
self.status_message = "Nothing to redo".to_string();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ impl eframe::App for TerrafierApp {
|
|||
&& let Some(tile) = world.dimensions[0].tiles.values().next()
|
||||
{
|
||||
renderer.render(
|
||||
&tile.heightmap.as_slice().try_into().expect("heightmap size"),
|
||||
&tile.terrain.as_slice().try_into().expect("terrain size"),
|
||||
&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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,57 +47,103 @@ impl GpuRenderer {
|
|||
needed_storage,
|
||||
);
|
||||
}
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("terrain_bind_group_layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
||||
wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
||||
wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
||||
wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: false }, has_dynamic_offset: false, min_binding_size: None }, count: None },
|
||||
],
|
||||
});
|
||||
let shader_module =
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("terrain_shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
let pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("terrain_pipeline_layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let compute_pipeline =
|
||||
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("terrain_compute_pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
module: &shader_module,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
cache: None,
|
||||
});
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("terrain_bind_group_layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: false },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("terrain_shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("terrain_pipeline_layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("terrain_compute_pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
module: &shader_module,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
cache: None,
|
||||
});
|
||||
let heightmap_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("heightmap_buffer"), size: 8192 * 4,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
|
||||
label: Some("heightmap_buffer"),
|
||||
size: 8192 * 4,
|
||||
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,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
|
||||
label: Some("terrain_buffer"),
|
||||
size: 4096 * 4,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("uniform_buffer"), size: std::mem::size_of::<Uniforms>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
|
||||
label: Some("uniform_buffer"),
|
||||
size: std::mem::size_of::<Uniforms>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let output_size = (width * height * 4) as u64;
|
||||
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("output_buffer"), size: output_size,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false,
|
||||
label: Some("output_buffer"),
|
||||
size: output_size,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let output_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("output_texture"),
|
||||
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2,
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
|
|
@ -107,16 +153,54 @@ impl GpuRenderer {
|
|||
label: Some("terrain_bind_group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &uniform_buffer, offset: 0, size: None }) },
|
||||
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &heightmap_buffer, offset: 0, size: None }) },
|
||||
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &terrain_buffer, offset: 0, size: None }) },
|
||||
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &output_buffer, offset: 0, size: None }) },
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &uniform_buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &heightmap_buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &terrain_buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &output_buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
Self {
|
||||
device, queue, compute_pipeline, heightmap_buffer, terrain_buffer,
|
||||
uniform_buffer, output_buffer, output_texture, output_texture_view, bind_group,
|
||||
texture_id: None, width, height,
|
||||
device,
|
||||
queue,
|
||||
compute_pipeline,
|
||||
heightmap_buffer,
|
||||
terrain_buffer,
|
||||
uniform_buffer,
|
||||
output_buffer,
|
||||
output_texture,
|
||||
output_texture_view,
|
||||
bind_group,
|
||||
texture_id: None,
|
||||
width,
|
||||
height,
|
||||
last_heightmap: vec![0i16; 16384],
|
||||
last_terrain: vec![0u8; 16384],
|
||||
last_show_heightmap: false,
|
||||
|
|
@ -125,16 +209,25 @@ impl GpuRenderer {
|
|||
}
|
||||
|
||||
pub(crate) fn pack_heightmap(heightmap: &[i16; 16384]) -> Vec<u32> {
|
||||
heightmap.chunks(2).map(|chunk| {
|
||||
let low = chunk[0] as u32 & 0xFFFF;
|
||||
let high = chunk[1] as u32 & 0xFFFF;
|
||||
low | (high << 16)
|
||||
}).collect()
|
||||
heightmap
|
||||
.chunks(2)
|
||||
.map(|chunk| {
|
||||
let low = chunk[0] as u32 & 0xFFFF;
|
||||
let high = chunk[1] as u32 & 0xFFFF;
|
||||
low | (high << 16)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn pack_terrain(terrain: &[u8; 16384]) -> Vec<u32> {
|
||||
terrain.chunks(4).map(|chunk| {
|
||||
chunk[0] as u32 | (chunk[1] as u32) << 8 | (chunk[2] as u32) << 16 | (chunk[3] as u32) << 24
|
||||
}).collect()
|
||||
terrain
|
||||
.chunks(4)
|
||||
.map(|chunk| {
|
||||
chunk[0] as u32
|
||||
| (chunk[1] as u32) << 8
|
||||
| (chunk[2] as u32) << 16
|
||||
| (chunk[3] as u32) << 24
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,15 @@ use crate::renderer::GpuRenderer;
|
|||
use crate::renderer::Uniforms;
|
||||
|
||||
fn as_u8_slice<T>(data: &[T]) -> &[u8] {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
data.as_ptr() as *const u8,
|
||||
std::mem::size_of_val(data),
|
||||
)
|
||||
}
|
||||
// SAFETY: Reinterprets `&[T]` as `&[u8]` for GPU buffer upload.
|
||||
// T is a primitive type with no padding, and the byte layout is stable.
|
||||
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
|
||||
}
|
||||
|
||||
fn as_u8_value<T>(data: &T) -> &[u8] {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
data as *const T as *const u8,
|
||||
std::mem::size_of::<T>(),
|
||||
)
|
||||
}
|
||||
// SAFETY: Reinterprets a single `&T` as `&[u8]` for GPU buffer upload.
|
||||
// T is a primitive type with no padding, and the byte layout is stable.
|
||||
unsafe { std::slice::from_raw_parts(data as *const T as *const u8, std::mem::size_of::<T>()) }
|
||||
}
|
||||
|
||||
impl GpuRenderer {
|
||||
|
|
@ -39,8 +33,8 @@ impl GpuRenderer {
|
|||
self.last_show_heightmap = show_heightmap;
|
||||
self.needs_upload = false;
|
||||
|
||||
let min_height = *heightmap.iter().min().unwrap();
|
||||
let max_height = *heightmap.iter().max().unwrap();
|
||||
let min_height = *heightmap.iter().min().unwrap_or(&0);
|
||||
let max_height = *heightmap.iter().max().unwrap_or(&255);
|
||||
|
||||
let packed_heightmap = Self::pack_heightmap(heightmap);
|
||||
self.queue
|
||||
|
|
|
|||
|
|
@ -1,21 +1,45 @@
|
|||
use crate::app::{
|
||||
LAYER_BIOME_IDX, LAYER_CAVES_IDX, LAYER_FROST_IDX, LAYER_RESOURCES_IDX, LAYER_RIVER_IDX,
|
||||
LAYER_TREES_IDX,
|
||||
};
|
||||
use egui::{Color32, ColorImage, TextureOptions, Vec2};
|
||||
use terrafier_core::model::tile::{TILE_SIZE, LayerBuffer};
|
||||
use terrafier_core::model::layers::{LAYER_CAVES, LAYER_RIVER, LAYER_FROST, LAYER_TREES, LAYER_BIOME, LAYER_RESOURCES};
|
||||
use crate::app::{LAYER_CAVES_IDX, LAYER_RIVER_IDX, LAYER_FROST_IDX, LAYER_TREES_IDX, LAYER_BIOME_IDX, LAYER_RESOURCES_IDX};
|
||||
use terrafier_core::model::layers::{
|
||||
LAYER_BIOME, LAYER_CAVES, LAYER_FROST, LAYER_RESOURCES, LAYER_RIVER, LAYER_TREES,
|
||||
};
|
||||
use terrafier_core::model::tile::{LayerBuffer, TILE_SIZE};
|
||||
|
||||
const LAYER_IDS: [u32; 6] = [LAYER_CAVES, LAYER_RIVER, LAYER_FROST, LAYER_TREES, LAYER_BIOME, LAYER_RESOURCES];
|
||||
const LAYER_IDS: [u32; 6] = [
|
||||
LAYER_CAVES,
|
||||
LAYER_RIVER,
|
||||
LAYER_FROST,
|
||||
LAYER_TREES,
|
||||
LAYER_BIOME,
|
||||
LAYER_RESOURCES,
|
||||
];
|
||||
|
||||
fn layer_value(tile: &terrafier_core::model::tile::Tile, idx: usize, layer_idx: usize) -> Option<f32> {
|
||||
fn layer_value(
|
||||
tile: &terrafier_core::model::tile::Tile,
|
||||
idx: usize,
|
||||
layer_idx: usize,
|
||||
) -> Option<f32> {
|
||||
let layer_id = LAYER_IDS[layer_idx];
|
||||
let buf = tile.layer_data.get(&layer_id)?;
|
||||
let val = match buf {
|
||||
LayerBuffer::Bit(data) => {
|
||||
let word = data.get(idx / 64)?;
|
||||
Some(if (word >> (idx % 64)) & 1 != 0 { 1.0 } else { 0.0 })
|
||||
Some(if (word >> (idx % 64)) & 1 != 0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
})
|
||||
}
|
||||
LayerBuffer::Nibble(data) => {
|
||||
let byte = *data.get(idx / 2)?;
|
||||
let nib = if idx % 2 == 0 { byte & 0x0F } else { (byte >> 4) & 0x0F };
|
||||
let nib = if idx % 2 == 0 {
|
||||
byte & 0x0F
|
||||
} else {
|
||||
(byte >> 4) & 0x0F
|
||||
};
|
||||
Some(nib as f32 / 15.0)
|
||||
}
|
||||
LayerBuffer::Byte(data) => {
|
||||
|
|
@ -65,7 +89,9 @@ const TERRAIN_COLORS: [Color32; 7] = [
|
|||
];
|
||||
|
||||
pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
||||
let Some(ref world) = app.world else { return; };
|
||||
let Some(ref world) = app.world else {
|
||||
return;
|
||||
};
|
||||
let dim = &world.dimensions[0];
|
||||
|
||||
let mut min_tx = i32::MAX;
|
||||
|
|
@ -79,20 +105,22 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
max_tz = max_tz.max(tz);
|
||||
}
|
||||
|
||||
let display_size = 64u32;
|
||||
let grid_w = ((max_tx - min_tx + 1) * display_size as i32) as usize;
|
||||
let grid_h = ((max_tz - min_tz + 1) * display_size as i32) as usize;
|
||||
let base_size = 64u32;
|
||||
let zoom = app.view_zoom;
|
||||
|
||||
let grid_w = ((max_tx - min_tx + 1) * base_size as i32) as usize;
|
||||
let grid_h = ((max_tz - min_tz + 1) * base_size as i32) as usize;
|
||||
|
||||
let mut img_data = vec![0u8; grid_w * grid_h * 4];
|
||||
|
||||
for (&(tx, tz), tile) in &dim.tiles {
|
||||
let px = ((tx - min_tx) * display_size as i32) as usize;
|
||||
let pz = ((tz - min_tz) * display_size as i32) as usize;
|
||||
let px = ((tx - min_tx) * base_size as i32) as usize;
|
||||
let pz = ((tz - min_tz) * base_size as i32) as usize;
|
||||
|
||||
for ly in 0..display_size as usize {
|
||||
for lx in 0..display_size as usize {
|
||||
let sx = (lx * TILE_SIZE / display_size as usize).min(TILE_SIZE - 1);
|
||||
let sz = (ly * TILE_SIZE / display_size as usize).min(TILE_SIZE - 1);
|
||||
for ly in 0..base_size as usize {
|
||||
for lx in 0..base_size as usize {
|
||||
let sx = (lx * TILE_SIZE / base_size as usize).min(TILE_SIZE - 1);
|
||||
let sz = (ly * TILE_SIZE / base_size as usize).min(TILE_SIZE - 1);
|
||||
let idx = sz * TILE_SIZE + sx;
|
||||
|
||||
let terrain_type = tile.terrain[idx] as usize;
|
||||
|
|
@ -127,7 +155,11 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
if app.layer_visible[li] {
|
||||
if let Some(val) = layer_value(tile, idx, li) {
|
||||
if let Some(over) = layer_overlay(li, val) {
|
||||
let alpha = if val > 0.0 { (val * 0.6 + 0.2).min(0.8) } else { 0.0 };
|
||||
let alpha = if val > 0.0 {
|
||||
(val * 0.6 + 0.2).min(0.8)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
(r, g, b) = blend_overlay((r, g, b), over, alpha);
|
||||
}
|
||||
}
|
||||
|
|
@ -156,13 +188,35 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
id
|
||||
};
|
||||
|
||||
let desired_size = Vec2::new(grid_w as f32, grid_h as f32);
|
||||
let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::click());
|
||||
// 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(),
|
||||
);
|
||||
|
||||
// Handle scroll-wheel zoom
|
||||
let scroll_delta = ui.input(|i| i.raw_scroll_delta);
|
||||
if scroll_delta.y != 0.0 {
|
||||
app.view_zoom = (app.view_zoom * (1.0 + scroll_delta.y * 0.002)).clamp(0.1, 10.0);
|
||||
}
|
||||
|
||||
// Handle drag-to-pan
|
||||
if response.dragged() {
|
||||
app.view_offset += response.drag_delta();
|
||||
}
|
||||
|
||||
// Recalculate scaled size after potential zoom change
|
||||
let scaled_size = base_size as f32 * app.view_zoom;
|
||||
let offset = app.view_offset;
|
||||
|
||||
let rect = response.rect;
|
||||
painter.image(
|
||||
texture_id,
|
||||
rect,
|
||||
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::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
Color32::WHITE,
|
||||
);
|
||||
|
|
@ -173,10 +227,10 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
&& sel_tz >= min_tz
|
||||
&& sel_tz <= max_tz
|
||||
{
|
||||
let bx = rect.min.x + (sel_tx - min_tx) as f32 * display_size as f32;
|
||||
let bz = rect.min.y + (sel_tz - min_tz) as f32 * display_size as f32;
|
||||
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(display_size as f32));
|
||||
egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(scaled_size));
|
||||
painter.rect_stroke(
|
||||
border_rect,
|
||||
0.0,
|
||||
|
|
@ -188,13 +242,13 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) =
|
||||
(app.selected_tile, app.brush_local_x, app.brush_local_z)
|
||||
{
|
||||
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size as f32;
|
||||
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size as f32;
|
||||
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * scaled_size;
|
||||
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * scaled_size;
|
||||
let brush_x =
|
||||
rect.min.x + tile_x_in_pixels + (bx as f32 * display_size as f32 / TILE_SIZE as f32);
|
||||
rect.min.x + offset.x + tile_x_in_pixels + (bx as f32 * scaled_size / TILE_SIZE as f32);
|
||||
let brush_z =
|
||||
rect.min.y + tile_z_in_pixels + (bz as f32 * display_size as f32 / TILE_SIZE as f32);
|
||||
let brush_radius_px = app.brush_radius as f32 * display_size as f32 / TILE_SIZE as f32;
|
||||
rect.min.y + offset.y + tile_z_in_pixels + (bz as f32 * scaled_size / TILE_SIZE as f32);
|
||||
let brush_radius_px = app.brush_radius as f32 * scaled_size / TILE_SIZE as f32;
|
||||
painter.circle_stroke(
|
||||
egui::pos2(brush_x, brush_z),
|
||||
brush_radius_px.max(2.0),
|
||||
|
|
@ -202,20 +256,21 @@ pub fn render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
);
|
||||
}
|
||||
|
||||
// Click-to-select with inverse transform
|
||||
if response.clicked()
|
||||
&& let Some(pos) = response.interact_pointer_pos()
|
||||
{
|
||||
let lx = pos.x - rect.min.x;
|
||||
let lz = pos.y - rect.min.y;
|
||||
let lx = pos.x - rect.min.x - offset.x;
|
||||
let lz = pos.y - rect.min.y - offset.y;
|
||||
if lx >= 0.0 && lz >= 0.0 {
|
||||
let tx = (lx / display_size as f32).floor() as i32 + min_tx;
|
||||
let tz = (lz / display_size as f32).floor() as i32 + min_tz;
|
||||
let tx = (lx / scaled_size).floor() as i32 + min_tx;
|
||||
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 % display_size) * TILE_SIZE as u32 / display_size).min(127);
|
||||
((lx as u32 % scaled_size as u32) * TILE_SIZE as u32 / scaled_size as u32).min(127);
|
||||
let local_z =
|
||||
((lz as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127);
|
||||
((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!(
|
||||
|
|
|
|||
|
|
@ -11,37 +11,64 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
|||
&& 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());
|
||||
let (response, painter) = ui.allocate_painter(
|
||||
desired_size,
|
||||
egui::Sense::click_and_drag(),
|
||||
);
|
||||
let rect = response.rect;
|
||||
|
||||
// Handle scroll-wheel zoom
|
||||
let scroll_delta = ui.input(|i| i.raw_scroll_delta);
|
||||
if scroll_delta.y != 0.0 {
|
||||
app.view_zoom = (app.view_zoom * (1.0 + scroll_delta.y * 0.002)).clamp(0.1, 10.0);
|
||||
}
|
||||
|
||||
// Handle drag-to-pan
|
||||
if response.dragged() {
|
||||
app.view_offset += response.drag_delta();
|
||||
}
|
||||
|
||||
let zoom = app.view_zoom;
|
||||
let offset = app.view_offset;
|
||||
|
||||
let base_display_size = rect.width() / 4.0;
|
||||
let display_size = base_display_size * zoom;
|
||||
|
||||
// Find tile bounds for positioning
|
||||
let mut min_tx = i32::MAX;
|
||||
let mut max_tx = i32::MIN;
|
||||
let mut min_tz = i32::MAX;
|
||||
let mut max_tz = i32::MIN;
|
||||
for &(tx, tz) in dim.tiles.keys() {
|
||||
min_tx = min_tx.min(tx);
|
||||
max_tx = max_tx.max(tx);
|
||||
min_tz = min_tz.min(tz);
|
||||
max_tz = max_tz.max(tz);
|
||||
}
|
||||
|
||||
// Draw the GPU-rendered tile texture at the correct position
|
||||
// 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_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(tex_origin_x, tex_origin_y),
|
||||
Vec2::splat(display_size),
|
||||
);
|
||||
painter.image(
|
||||
texture_id,
|
||||
rect,
|
||||
tex_rect,
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
Color32::WHITE,
|
||||
);
|
||||
|
||||
let display_size = rect.width() / 4.0;
|
||||
|
||||
if let Some((sel_tx, sel_tz)) = app.selected_tile {
|
||||
let mut min_tx = i32::MAX;
|
||||
let mut max_tx = i32::MIN;
|
||||
let mut min_tz = i32::MAX;
|
||||
let mut max_tz = i32::MIN;
|
||||
for &(tx, tz) in dim.tiles.keys() {
|
||||
min_tx = min_tx.min(tx);
|
||||
max_tx = max_tx.max(tx);
|
||||
min_tz = min_tz.min(tz);
|
||||
max_tz = max_tz.max(tz);
|
||||
}
|
||||
|
||||
let bx = rect.min.x;
|
||||
let bz = rect.min.y;
|
||||
let sel_bx = bx + (sel_tx - min_tx) as f32 * display_size;
|
||||
let sel_bz = bz + (sel_tz - min_tz) as f32 * display_size;
|
||||
let border_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(sel_bx, sel_bz),
|
||||
Vec2::splat(display_size),
|
||||
);
|
||||
let bx = rect.min.x + offset.x + (sel_tx - min_tx) as f32 * display_size;
|
||||
let bz = rect.min.y + offset.y + (sel_tz - min_tz) as f32 * display_size;
|
||||
let border_rect =
|
||||
egui::Rect::from_min_size(egui::pos2(bx, bz), Vec2::splat(display_size));
|
||||
painter.rect_stroke(
|
||||
border_rect,
|
||||
0.0,
|
||||
|
|
@ -53,18 +80,12 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
|||
if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) =
|
||||
(app.selected_tile, app.brush_local_x, app.brush_local_z)
|
||||
{
|
||||
let mut min_tx = i32::MAX;
|
||||
let mut min_tz = i32::MAX;
|
||||
for &(tx, tz) in dim.tiles.keys() {
|
||||
min_tx = min_tx.min(tx);
|
||||
min_tz = min_tz.min(tz);
|
||||
}
|
||||
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 + tile_x_in_pixels
|
||||
+ (bx as f32 * display_size / TILE_SIZE as f32);
|
||||
let brush_z = rect.min.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),
|
||||
|
|
@ -76,23 +97,19 @@ pub fn try_render(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) -> bool
|
|||
if response.clicked()
|
||||
&& let Some(pos) = response.interact_pointer_pos()
|
||||
{
|
||||
let lx = pos.x - rect.min.x;
|
||||
let lz = pos.y - rect.min.y;
|
||||
let lx = pos.x - rect.min.x - offset.x;
|
||||
let lz = pos.y - rect.min.y - offset.y;
|
||||
if lx >= 0.0 && lz >= 0.0 {
|
||||
let mut min_tx = i32::MAX;
|
||||
let mut min_tz = i32::MAX;
|
||||
for &(tx, tz) in dim.tiles.keys() {
|
||||
min_tx = min_tx.min(tx);
|
||||
min_tz = min_tz.min(tz);
|
||||
}
|
||||
let tx = (lx / display_size).floor() as i32 + min_tx;
|
||||
let tz = (lz / display_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 % display_size as u32) * TILE_SIZE as u32 / display_size as u32).min(127);
|
||||
let local_z =
|
||||
((lz as u32 % display_size as u32) * TILE_SIZE as u32 / display_size as u32).min(127);
|
||||
let local_x = ((lx as u32 % display_size as u32) * TILE_SIZE as u32
|
||||
/ display_size as u32)
|
||||
.min(127);
|
||||
let local_z = ((lz as u32 % display_size as u32) * TILE_SIZE as u32
|
||||
/ display_size as u32)
|
||||
.min(127);
|
||||
app.brush_local_x = Some(local_x);
|
||||
app.brush_local_z = Some(local_z);
|
||||
app.status_message = format!(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use terrafier_core::Terrain;
|
||||
use terrafier_core::model::types::SymmetricBrush;
|
||||
use terrafier_core::model::tile::TILE_SIZE;
|
||||
use terrafier_core::model::types::SymmetricBrush;
|
||||
use terrafier_core::ops::operations::{
|
||||
ErodeOperation, FillOperation, FlattenOperation, HeightOperation, MultiTileOperation,
|
||||
Operation, PaintOperation, SmoothOperation,
|
||||
};
|
||||
|
||||
use crate::app::{TerrafierApp, ToolMode, LAYER_NAMES};
|
||||
use crate::app::{LAYER_NAMES, TerrafierApp, ToolMode};
|
||||
|
||||
pub fn show_tools_panel(ui: &mut egui::Ui, app: &mut TerrafierApp) {
|
||||
ui.heading("Tools");
|
||||
|
|
@ -83,7 +83,10 @@ fn apply_tool(app: &mut TerrafierApp) {
|
|||
let Some(ref mut world) = app.world else {
|
||||
return;
|
||||
};
|
||||
let dim = world.overworld_mut().expect("no overworld");
|
||||
let Some(dim) = world.overworld_mut() else {
|
||||
app.status_message = "World has no overworld".to_string();
|
||||
return;
|
||||
};
|
||||
|
||||
let center = app.brush_local_x.unwrap_or(64);
|
||||
let center_z = app.brush_local_z.unwrap_or(64);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue