+ atomic build.sh

+ implemented editor buttons
This commit is contained in:
2026-03-16 18:36:37 +03:30
parent a47f1f9818
commit 880362cc03
6 changed files with 160 additions and 18 deletions

View File

@@ -13,16 +13,16 @@ DEST_WINDOWS="$GODOT_PROJECT/addons/fastvoxel/bin/windows"
echo "Building for Linux (native)..." echo "Building for Linux (native)..."
cargo build --release cargo build --release
# Copy the shared library # Copy the shared library
mkdir -p "$DEST_LINUX" TMP="$DEST_LINUX/lib${LIB_NAME}.so.tmp"
cp "target/release/lib${LIB_NAME}.so" "$DEST_LINUX/" cp "target/release/lib${LIB_NAME}.so" "$TMP"
echo "Copied to $DEST_LINUX" mv "$TMP" "$DEST_LINUX/lib${LIB_NAME}.so"
# Optional: Crosscompile for Windows (if needed) # Optional: Crosscompile for Windows (if needed)
# Uncomment the following lines if you have the Windows target installed # Uncomment the following lines if you have the Windows target installed
echo "Building for Windows (cross-compile)..." # echo "Building for Windows (cross-compile)..."
cargo build --release --target x86_64-pc-windows-gnu # cargo build --release --target x86_64-pc-windows-gnu
mkdir -p "$DEST_WINDOWS" # mkdir -p "$DEST_WINDOWS"
cp "target/x86_64-pc-windows-gnu/release/${LIB_NAME}.dll" "$DEST_WINDOWS/" # cp "target/x86_64-pc-windows-gnu/release/${LIB_NAME}.dll" "$DEST_WINDOWS/"
echo "Copied to $DEST_WINDOWS" # echo "Copied to $DEST_WINDOWS"
echo "Done." echo "Done."

View File

@@ -191,7 +191,7 @@ impl ChunkManager {
let start = Instant::now(); let start = Instant::now();
// Use render_chunk (with colors) instead of render_chunk_with_uvs // Use render_chunk (with colors) instead of render_chunk_with_uvs
let mesh_instance = self.renderer.render_chunk(chunk, &mesh); let mesh_instance = self.renderer.render_chunk_with_uvs(chunk, &mesh);
godot_print!("Rendering Took: {}μs", start.elapsed().as_micros()); godot_print!("Rendering Took: {}μs", start.elapsed().as_micros());
self.pending_mesh_instances.push(mesh_instance); self.pending_mesh_instances.push(mesh_instance);

125
src/editor/editor.rs Normal file
View File

@@ -0,0 +1,125 @@
use godot::classes::EditorInterface;
use godot::classes::EditorPlugin;
use godot::classes::IEditorPlugin;
use godot::classes::ImageTexture;
use godot::classes::MenuButton;
use godot::classes::ResourceLoader;
use godot::classes::Texture2D;
use godot::classes::editor_plugin::CustomControlContainer;
use godot::prelude::*;
use crate::world::World;
#[derive(GodotClass)]
#[class(init, base=EditorPlugin, tool)]
pub struct WorldPlugin {
#[base]
base: Base<EditorPlugin>,
toolbar_button: Option<Gd<MenuButton>>,
}
#[godot_api]
impl WorldPlugin {
// Called when a menu item is selected
#[func]
fn on_menu_item_pressed(&self, id: i32) {
let mut world = match self.find_world_node() {
Some(world) => world,
None => {
godot_error!(
"No World node found in the current scene. Make sure it's named 'World' and is a child of the scene root."
);
return;
}
};
match id {
0 => {
world.call("clear_terrain", &[]);
world.call("generate_terrain", &[]);
}
1 => {
world.call("clear_terrain", &[]);
}
_ => {
godot_print!("Unknown item");
}
}
// Here you can call your actual world management methods.
// For example:
// if let Some(world_manager) = self.get_world_manager() {
// match id {
// 0 => world_manager.call("rebuild_world", &[]),
// 1 => world_manager.call("clear_world", &[]),
// 2 => world_manager.call("reload_chunks", &[]),
// _ => (),
// }
// }
}
fn find_world_node(&self) -> Option<Gd<World>> {
let editor = EditorInterface::singleton();
let scene_root = editor.get_edited_scene_root()?;
// Assuming the World node is a direct child of the root and named "World"
scene_root.try_get_node_as::<World>("World")
}
}
#[godot_api]
impl IEditorPlugin for WorldPlugin {
fn enter_tree(&mut self) {
// Create the MenuButton
let mut menu_button = MenuButton::new_alloc();
menu_button.set_text("FastVoxel"); // Button label
// menu_button.set_flat(false); // Make it look like a normal button (optional)
// --- Load an SVG icon and set it ---
// Path to your SVG file (relative to the project, e.g., in addons/fastvoxel/fastvoxel.svg)
if let Some(icon) = ResourceLoader::singleton().load("res://addons/fastvoxel/fastvoxel.svg")
{
let icon_texture: Gd<Texture2D> = icon.cast(); // Cast to Texture2D
// Get image data, resize, create new texture
let mut image = icon_texture.get_image(); // Requires Texture2D to have image data
image
.as_mut()
.expect("failed to load the fastvoxel icon")
.resize(24, 24);
let small_texture = ImageTexture::create_from_image(image.as_ref());
menu_button.set_button_icon(small_texture.as_ref());
} else {
godot_error!("Failed to load plugin icon");
}
// --- Build the dropdown menu ---
let mut popup = menu_button.get_popup(); // Returns a PopupMenu
popup.as_mut().expect("no popup").add_item("Rebuild World"); // item text, id
popup.as_mut().expect("no popup").add_item("Clear World");
popup.as_mut().expect("no popup").add_item("Reload Chunks");
// You can also add separators, submenus, etc.
// Connect the popup's "id_pressed" signal to our handler
let callable = self.base().callable("on_menu_item_pressed");
popup.unwrap().connect("id_pressed", &callable);
// Add the button to the editor's main toolbar (top row, next to play buttons)
self.base_mut().add_control_to_container(
CustomControlContainer::SPATIAL_EDITOR_MENU,
menu_button.to_godot(),
);
// Store the button reference so we can remove it later
self.toolbar_button = Some(menu_button);
}
fn exit_tree(&mut self) {
// Clean up: remove the button from the toolbar
if let Some(button) = self.toolbar_button.take() {
self.base_mut().remove_control_from_container(
CustomControlContainer::SPATIAL_EDITOR_MENU,
button.to_godot(),
);
// The button will be freed automatically when no longer referenced
}
}
}

View File

@@ -1,3 +1,5 @@
pub mod editor;
pub mod voxel_registry; pub mod voxel_registry;
pub use editor::WorldPlugin;
pub use voxel_registry::VoxelRegistry; pub use voxel_registry::VoxelRegistry;

View File

@@ -3,7 +3,7 @@ use std::{collections::HashMap, time::Instant};
use crate::chunk::{self, Chunk, ChunkMesh}; use crate::chunk::{self, Chunk, ChunkMesh};
use godot::{ use godot::{
classes::{ classes::{
ArrayMesh, Material, MeshInstance3D, ResourceLoader, RenderingServer, StandardMaterial3D, ArrayMesh, Material, MeshInstance3D, RenderingServer, ResourceLoader, StandardMaterial3D,
base_material_3d::{CullMode, Flags, ShadingMode, Transparency}, base_material_3d::{CullMode, Flags, ShadingMode, Transparency},
geometry_instance_3d::ShadowCastingSetting, geometry_instance_3d::ShadowCastingSetting,
mesh::{ArrayType, PrimitiveType}, mesh::{ArrayType, PrimitiveType},
@@ -212,6 +212,13 @@ impl Renderer {
self.mesh_instances.clear(); self.mesh_instances.clear();
} }
pub fn clear_and_destroy(&mut self) {
for (_, instance) in self.mesh_instances.drain() {
instance.free();
}
self.mesh_instances.clear();
}
pub fn remove_chunk(&mut self, chunk_position: (i32, i32, i32)) { pub fn remove_chunk(&mut self, chunk_position: (i32, i32, i32)) {
if let Some(instance) = self.mesh_instances.remove(&chunk_position) { if let Some(instance) = self.mesh_instances.remove(&chunk_position) {
self.mesh_instance_pool.push(instance); self.mesh_instance_pool.push(instance);

View File

@@ -2,15 +2,13 @@ use godot::classes::{INode3D, VoxelGi};
use godot::prelude::*; use godot::prelude::*;
use crate::chunk::ChunkManager; use crate::chunk::ChunkManager;
use crate::chunk::chunk::CHUNK_SIZE;
use crate::editor::VoxelRegistry; use crate::editor::VoxelRegistry;
use crate::generation::{HeightmapGenerator, WorldGenerator}; use crate::generation::{HeightmapGenerator, WorldGenerator};
use crate::meshing::mesher::CullingMesher; use crate::meshing::{Mesher, TexturedMesher};
use crate::meshing::{BinaryGreedyMesher, Mesher};
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Node3D,tool)] #[class(base=Node3D,tool)]
struct World { pub struct World {
base: Base<Node3D>, base: Base<Node3D>,
// ===== RENDERING GROUP ===== // ===== RENDERING GROUP =====
@@ -55,7 +53,7 @@ impl INode3D for World {
godot_print!("🧊 Hello from FastVoxel"); godot_print!("🧊 Hello from FastVoxel");
let generator = Box::new(HeightmapGenerator::new(1234, 0.03, 5)); let generator = Box::new(HeightmapGenerator::new(1234, 0.03, 5));
let mesher = Box::new(CullingMesher::new()); let mesher = Box::new(TexturedMesher::new());
let chunk_manager = ChunkManager::new(); let chunk_manager = ChunkManager::new();
Self { Self {
@@ -108,7 +106,6 @@ impl INode3D for World {
#[godot_api] #[godot_api]
impl World { impl World {
#[func] #[func]
fn generate_terrain(&mut self) { fn generate_terrain(&mut self) {
match &self.registry { match &self.registry {
None => { None => {
@@ -134,17 +131,28 @@ impl World {
} }
} }
#[func]
fn regenerate_terrain(&mut self) { fn regenerate_terrain(&mut self) {
godot_print!("Regenerating terrain..."); godot_print!("Regenerating terrain...");
self.chunk_manager.clear(); self.chunk_manager.clear();
// Create textured mesher // Create textured mesher
self.mesher = Box::new(CullingMesher::new()); self.mesher = Box::new(TexturedMesher::new());
self.generate_terrain(); self.generate_terrain();
} }
#[func]
fn clear_terrain(&mut self) {
godot_print!("Clearing Terrain...");
self.chunk_manager.clear();
self.chunk_manager.pending_mesh_instances.clear();
self.chunk_manager.renderer.clear_and_destroy();
}
#[func] #[func]
fn update_from_gdscript(&mut self, player_world_pos: Vector3) { fn update_from_gdscript(&mut self, player_world_pos: Vector3) {
match &self.registry { match &self.registry {