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