diff --git a/doc/classes/Terrain3DEditor.xml b/doc/classes/Terrain3DEditor.xml index 777d94ee2..a6838311f 100644 --- a/doc/classes/Terrain3DEditor.xml +++ b/doc/classes/Terrain3DEditor.xml @@ -18,6 +18,11 @@ + + + + + @@ -71,6 +76,19 @@ + + + + + This signal is emitted whenever the editor is used to: + - add or remove a region, + - alter a region map with a brush tool, + - undo or redo any of the above operations. + + The parameter contains the axis-aligned bounding box of the area edited. + + + diff --git a/project/addons/terrain_3d/editor/editor.gd b/project/addons/terrain_3d/editor/editor.gd index 9039fc1a1..a79c729d4 100644 --- a/project/addons/terrain_3d/editor/editor.gd +++ b/project/addons/terrain_3d/editor/editor.gd @@ -23,7 +23,7 @@ var mouse_global_position: Vector3 = Vector3.ZERO func _enter_tree() -> void: - editor = Terrain3DEditor.new() + editor = Terrain3DEditor.get_singleton() ui = UI.new() ui.plugin = self add_child(ui) @@ -44,7 +44,7 @@ func _exit_tree() -> void: remove_control_from_container(texture_dock_container, texture_dock) texture_dock.queue_free() ui.queue_free() - editor.free() + editor = null func _handles(p_object: Object) -> bool: diff --git a/src/register_types.cpp b/src/register_types.cpp index 346a952ed..bc3bb7667 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -23,12 +23,16 @@ void initialize_terrain_3d(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + + Terrain3DEditor::create(); } void uninitialize_terrain_3d(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } + + Terrain3DEditor::free(); } extern "C" { diff --git a/src/terrain_3d_editor.cpp b/src/terrain_3d_editor.cpp index 1dde9b4c5..9c36268a6 100644 --- a/src/terrain_3d_editor.cpp +++ b/src/terrain_3d_editor.cpp @@ -7,6 +7,8 @@ #include "terrain_3d_editor.h" #include "util.h" +Terrain3DEditor *Terrain3DEditor::_singleton = nullptr; + /////////////////////////// // Subclass Functions /////////////////////////// @@ -43,21 +45,49 @@ void Terrain3DEditor::Brush::set_data(Dictionary p_data) { // Private Functions /////////////////////////// +void Terrain3DEditor::_region_modified(Vector3 p_global_position, Vector2 p_height_range) { + Vector2i region_offset = _terrain->get_storage()->get_region_offset(p_global_position); + Terrain3DStorage::RegionSize region_size = _terrain->get_storage()->get_region_size(); + + AABB edited_area; + edited_area.position = Vector3(region_offset.x * region_size, p_height_range.x, region_offset.y * region_size); + edited_area.size = Vector3(region_size, p_height_range.y - p_height_range.x, region_size); + + if (_modified) { + _modified_area = _modified_area.merge(edited_area); + } else { + _modified_area = edited_area; + } + _modified = true; + emit_signal("terrain_edited", edited_area); +} + void Terrain3DEditor::_operate_region(Vector3 p_global_position) { bool has_region = _terrain->get_storage()->has_region(p_global_position); + bool modified = false; + Vector2 height_range; if (_operation == ADD) { if (!has_region) { _terrain->get_storage()->add_region(p_global_position); - _modified = true; + modified = true; + } } if (_operation == SUBTRACT) { if (has_region) { + int region_index = _terrain->get_storage()->get_region_index(p_global_position); + Ref height_map = _terrain->get_storage()->get_map_region(Terrain3DStorage::TYPE_HEIGHT, region_index); + height_range = Util::get_min_max(height_map); + _terrain->get_storage()->remove_region(p_global_position); - _modified = true; + modified = true; } } + + if (modified) { + _region_modified(p_global_position, height_range); + } } void Terrain3DEditor::_operate_map(Vector3 p_global_position, real_t p_camera_direction) { @@ -72,13 +102,13 @@ void Terrain3DEditor::_operate_map(Vector3 p_global_position, real_t p_camera_di } else { LOG(DEBUG, "No region to operate on, attempting to add"); storage->add_region(p_global_position); - _modified = true; region_size = storage->get_region_size(); region_index = storage->get_region_index(p_global_position); if (region_index == -1) { LOG(ERROR, "Failed to add region, no region to operate on"); return; } + _region_modified(p_global_position); } } if (_tool < 0 || _tool >= REGION) { @@ -123,6 +153,10 @@ void Terrain3DEditor::_operate_map(Vector3 p_global_position, real_t p_camera_di } Object::cast_to(_terrain->get_plugin()->get("ui"))->call("set_decal_rotation", rot); + AABB edited_area; + edited_area.position = p_global_position - Vector3(brush_size, 0.0, brush_size) / 2; + edited_area.size = Vector3(brush_size, 0.0, brush_size); + for (int x = 0; x < brush_size; x++) { for (int y = 0; y < brush_size; y++) { Vector2i brush_offset = Vector2i(x, y) - (Vector2i(brush_size, brush_size) / 2); @@ -141,6 +175,7 @@ void Terrain3DEditor::_operate_map(Vector3 p_global_position, real_t p_camera_di continue; } new_region_index = storage->get_region_index(brush_global_position); + _region_modified(brush_global_position); } if (new_region_index != region_index) { @@ -160,6 +195,10 @@ void Terrain3DEditor::_operate_map(Vector3 p_global_position, real_t p_camera_di continue; } + Vector3 edited_position = brush_global_position; + edited_position.y = storage->get_height(edited_position); + edited_area = edited_area.expand(edited_position); + // Start brushing on the map real_t brush_alpha = real_t(Math::pow(double(_brush.get_alpha(brush_pixel_position)), double(gamma))); Color src = map->get_pixelv(map_pixel_position); @@ -208,6 +247,9 @@ void Terrain3DEditor::_operate_map(Vector3 p_global_position, real_t p_camera_di dest = Color(destf, 0.0f, 0.0f, 1.0f); storage->update_heights(destf); + edited_position.y = destf; + edited_area = edited_area.expand(edited_position); + } else if (map_type == Terrain3DStorage::TYPE_CONTROL) { // Get bit field from pixel uint32_t base_id = Util::get_base(src.r); @@ -307,8 +349,14 @@ void Terrain3DEditor::_operate_map(Vector3 p_global_position, real_t p_camera_di } } } + if (_modified) { + _modified_area = _modified_area.merge(edited_area); + } else { + _modified_area = edited_area; + } _modified = true; storage->force_update_maps(map_type); + emit_signal("terrain_edited", edited_area); } bool Terrain3DEditor::_is_in_bounds(Vector2i p_position, Vector2i p_max_position) { @@ -336,6 +384,7 @@ Vector2 Terrain3DEditor::_rotate_uv(Vector2 p_uv, real_t p_angle) { * 0-2: map 0,1,2 * 3: Region offsets * 4: height range + * 5: edited AABB */ void Terrain3DEditor::_setup_undo() { ERR_FAIL_COND_MSG(_terrain == nullptr, "terrain is null, returning"); @@ -345,7 +394,7 @@ void Terrain3DEditor::_setup_undo() { } LOG(INFO, "Setting up undo snapshot..."); _undo_set.clear(); - _undo_set.resize(Terrain3DStorage::TYPE_MAX + 2); + _undo_set.resize(Terrain3DStorage::TYPE_MAX + 3); for (int i = 0; i < Terrain3DStorage::TYPE_MAX; i++) { _undo_set[i] = _terrain->get_storage()->get_maps_copy(static_cast(i)); LOG(DEBUG, "maps ", i, "(", static_cast>(_undo_set[i]).size(), "): ", _undo_set[i]); @@ -353,6 +402,8 @@ void Terrain3DEditor::_setup_undo() { _undo_set[Terrain3DStorage::TYPE_MAX] = _terrain->get_storage()->get_region_offsets().duplicate(); LOG(DEBUG, "region_offsets(", static_cast>(_undo_set[Terrain3DStorage::TYPE_MAX]).size(), "): ", _undo_set[Terrain3DStorage::TYPE_MAX]); _undo_set[Terrain3DStorage::TYPE_MAX + 1] = _terrain->get_storage()->get_height_range(); + + _undo_set[Terrain3DStorage::TYPE_MAX + 2] = _modified_area; } void Terrain3DEditor::_store_undo() { @@ -368,12 +419,15 @@ void Terrain3DEditor::_store_undo() { LOG(DEBUG, "Creating undo action: '", action_name, "'"); undo_redo->create_action(action_name); + LOG(DEBUG, "Updating undo snapshot modified area: ", _modified_area); + _undo_set[Terrain3DStorage::TYPE_MAX + 2] = _modified_area; + LOG(DEBUG, "Storing undo snapshot: ", _undo_set); undo_redo->add_undo_method(this, "apply_undo", _undo_set.duplicate()); // Must be duplicated LOG(DEBUG, "Setting up redo snapshot..."); Array redo_set; - redo_set.resize(Terrain3DStorage::TYPE_MAX + 2); + redo_set.resize(Terrain3DStorage::TYPE_MAX + 3); for (int i = 0; i < Terrain3DStorage::TYPE_MAX; i++) { redo_set[i] = _terrain->get_storage()->get_maps_copy(static_cast(i)); LOG(DEBUG, "maps ", i, "(", static_cast>(redo_set[i]).size(), "): ", redo_set[i]); @@ -382,6 +436,9 @@ void Terrain3DEditor::_store_undo() { LOG(DEBUG, "region_offsets(", static_cast>(redo_set[Terrain3DStorage::TYPE_MAX]).size(), "): ", redo_set[Terrain3DStorage::TYPE_MAX]); redo_set[Terrain3DStorage::TYPE_MAX + 1] = _terrain->get_storage()->get_height_range(); + LOG(DEBUG, "Storing modified area: ", _modified_area); + redo_set[Terrain3DStorage::TYPE_MAX + 2] = _modified_area; + LOG(DEBUG, "Storing redo snapshot: ", redo_set); undo_redo->add_do_method(this, "apply_undo", redo_set); @@ -406,13 +463,14 @@ void Terrain3DEditor::_apply_undo(const Array &p_set) { LOG(DEBUG, "Calling GDScript update_grid()"); _terrain->get_plugin()->call("update_grid"); } + _pending_undo = false; _modified = false; -} + _modified_area = AABB(); -/////////////////////////// -// Public Functions -/////////////////////////// + AABB edited_area = p_set[Terrain3DStorage::TYPE_MAX + 2]; + emit_signal("terrain_edited", edited_area); +} Terrain3DEditor::Terrain3DEditor() { } @@ -420,6 +478,10 @@ Terrain3DEditor::Terrain3DEditor() { Terrain3DEditor::~Terrain3DEditor() { } +/////////////////////////// +// Public Functions +/////////////////////////// + void Terrain3DEditor::set_brush_data(Dictionary p_data) { if (p_data.is_empty()) { return; @@ -437,6 +499,7 @@ void Terrain3DEditor::start_operation(Vector3 p_global_position) { _setup_undo(); _pending_undo = true; _modified = false; + _modified_area = AABB(); if (_tool == REGION) { _operate_region(p_global_position); } @@ -468,6 +531,7 @@ void Terrain3DEditor::stop_operation() { _store_undo(); _pending_undo = false; _modified = false; + _modified_area = AABB(); } } @@ -494,6 +558,8 @@ void Terrain3DEditor::_bind_methods() { BIND_ENUM_CONSTANT(REGION); BIND_ENUM_CONSTANT(TOOL_MAX); + ClassDB::bind_static_method("Terrain3DEditor", D_METHOD("get_singleton"), &Terrain3DEditor::get_singleton); + ClassDB::bind_method(D_METHOD("set_terrain", "terrain"), &Terrain3DEditor::set_terrain); ClassDB::bind_method(D_METHOD("get_terrain"), &Terrain3DEditor::get_terrain); @@ -507,4 +573,6 @@ void Terrain3DEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("stop_operation"), &Terrain3DEditor::stop_operation); ClassDB::bind_method(D_METHOD("apply_undo", "maps"), &Terrain3DEditor::_apply_undo); + + ADD_SIGNAL(MethodInfo("terrain_edited", PropertyInfo(Variant::AABB, "edited_area"))); } diff --git a/src/terrain_3d_editor.h b/src/terrain_3d_editor.h index 9f4ca7987..90de0ad15 100644 --- a/src/terrain_3d_editor.h +++ b/src/terrain_3d_editor.h @@ -114,8 +114,10 @@ class Terrain3DEditor : public Object { real_t _operation_interval = 0.0f; bool _pending_undo = false; bool _modified = false; - Array _undo_set; // 0-2: map 0,1,2, 3: Region offsets, 4: height range + AABB _modified_area; + Array _undo_set; // 0-2: map 0,1,2, 3: Region offsets, 4: height range, 5: edited AABB + void _region_modified(Vector3 p_global_position, Vector2 p_height_range = Vector2()); void _operate_region(Vector3 p_global_position); void _operate_map(Vector3 p_global_position, real_t p_camera_direction); bool _is_in_bounds(Vector2i p_position, Vector2i p_max_position); @@ -126,10 +128,24 @@ class Terrain3DEditor : public Object { void _store_undo(); void _apply_undo(const Array &p_set); -public: + // Singleton + friend void initialize_terrain_3d(ModuleInitializationLevel p_level); + friend void uninitialize_terrain_3d(ModuleInitializationLevel p_level); + + static Terrain3DEditor *_singleton; + + static void create() { _singleton = memnew(Terrain3DEditor); } + static void free() { + memdelete(_singleton); + _singleton = nullptr; + } + Terrain3DEditor(); ~Terrain3DEditor(); +public: + static Terrain3DEditor *get_singleton() { return _singleton; } + void set_terrain(Terrain3D *p_terrain) { _terrain = p_terrain; } Terrain3D *get_terrain() const { return _terrain; } @@ -150,4 +166,4 @@ class Terrain3DEditor : public Object { VARIANT_ENUM_CAST(Terrain3DEditor::Operation); VARIANT_ENUM_CAST(Terrain3DEditor::Tool); -#endif // TERRAIN3D_EDITOR_CLASS_H \ No newline at end of file +#endif // TERRAIN3D_EDITOR_CLASS_H