godot_voxel/editor/graph/voxel_graph_editor.cpp

1041 lines
37 KiB
C++

#include "voxel_graph_editor.h"
#include "../../generators/graph/voxel_generator_graph.h"
#include "../../generators/graph/voxel_graph_node_db.h"
#include "../../terrain/voxel_node.h"
#include "../../util/godot/funcs.h"
#include "../../util/log.h"
#include "../../util/macros.h"
#include "../../util/math/conv.h"
#include "../../util/profiling.h"
#include "../../util/string_funcs.h"
#include "voxel_graph_editor_node.h"
#include "voxel_graph_editor_node_preview.h"
#include "voxel_graph_editor_shader_dialog.h"
#include "voxel_range_analysis_dialog.h"
#include <core/core_string_names.h>
#include <core/object/undo_redo.h>
#include <core/os/time.h>
#include <editor/editor_scale.h>
#include <scene/gui/check_box.h>
#include <scene/gui/code_edit.h>
#include <scene/gui/graph_edit.h>
#include <scene/gui/label.h>
#include <scene/gui/option_button.h>
namespace zylann::voxel {
const char *VoxelGraphEditor::SIGNAL_NODE_SELECTED = "node_selected";
const char *VoxelGraphEditor::SIGNAL_NOTHING_SELECTED = "nothing_selected";
const char *VoxelGraphEditor::SIGNAL_NODES_DELETED = "nodes_deleted";
const char *VoxelGraphEditor::SIGNAL_REGENERATE_REQUESTED = "regenerate_requested";
const char *VoxelGraphEditor::SIGNAL_POPOUT_REQUESTED = "popout_requested";
static NodePath to_node_path(StringName sn) {
Vector<StringName> path;
path.push_back(sn);
return NodePath(path, false);
}
VoxelGraphEditor::VoxelGraphEditor() {
VBoxContainer *vbox_container = memnew(VBoxContainer);
vbox_container->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
{
HBoxContainer *toolbar = memnew(HBoxContainer);
Button *update_previews_button = memnew(Button);
update_previews_button->set_text("Update Previews");
update_previews_button->connect(
"pressed", callable_mp(this, &VoxelGraphEditor::_on_update_previews_button_pressed));
toolbar->add_child(update_previews_button);
Button *profile_button = memnew(Button);
profile_button->set_text("Profile");
profile_button->connect("pressed", callable_mp(this, &VoxelGraphEditor::_on_profile_button_pressed));
toolbar->add_child(profile_button);
_profile_label = memnew(Label);
toolbar->add_child(_profile_label);
_compile_result_label = memnew(Label);
_compile_result_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
_compile_result_label->set_clip_text(true);
_compile_result_label->hide();
toolbar->add_child(_compile_result_label);
Button *range_analysis_button = memnew(Button);
range_analysis_button->set_text("Analyze Range...");
range_analysis_button->connect(
"pressed", callable_mp(this, &VoxelGraphEditor::_on_analyze_range_button_pressed));
toolbar->add_child(range_analysis_button);
OptionButton *preview_axes_menu = memnew(OptionButton);
preview_axes_menu->add_item("Preview XY", PREVIEW_XY);
preview_axes_menu->add_item("Preview XZ", PREVIEW_XZ);
preview_axes_menu->get_popup()->connect(
"id_pressed", callable_mp(this, &VoxelGraphEditor::_on_preview_axes_menu_id_pressed));
toolbar->add_child(preview_axes_menu);
Button *generate_shader_button = memnew(Button);
generate_shader_button->set_text(TTR("Generate shader"));
generate_shader_button->connect(
"pressed", callable_mp(this, &VoxelGraphEditor::_on_generate_shader_button_pressed));
toolbar->add_child(generate_shader_button);
CheckBox *live_update_checkbox = memnew(CheckBox);
live_update_checkbox->set_text(TTR("Live Update"));
live_update_checkbox->set_tooltip_text(
TTR("Automatically re-generate the terrain when the generator is modified"));
live_update_checkbox->set_pressed(_live_update_enabled);
live_update_checkbox->connect("toggled", callable_mp(this, &VoxelGraphEditor::_on_live_update_toggled));
toolbar->add_child(live_update_checkbox);
Control *spacer = memnew(Control);
spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
toolbar->add_child(spacer);
_pin_button = memnew(Button);
_pin_button->set_flat(true);
_pin_button->set_toggle_mode(true);
_pin_button->set_tooltip_text(TTR("Pin VoxelGraphEditor"));
toolbar->add_child(_pin_button);
_popout_button = memnew(Button);
_popout_button->set_flat(true);
_popout_button->set_tooltip_text(TTR("Pop-out as separate window"));
_popout_button->connect("pressed", callable_mp(this, &VoxelGraphEditor::_on_popout_button_pressed));
toolbar->add_child(_popout_button);
vbox_container->add_child(toolbar);
}
_graph_edit = memnew(GraphEdit);
_graph_edit->set_anchors_preset(Control::PRESET_FULL_RECT);
_graph_edit->set_right_disconnects(true);
_graph_edit->set_v_size_flags(Control::SIZE_EXPAND_FILL);
_graph_edit->connect("gui_input", callable_mp(this, &VoxelGraphEditor::_on_graph_edit_gui_input));
_graph_edit->connect("connection_request", callable_mp(this, &VoxelGraphEditor::_on_graph_edit_connection_request));
_graph_edit->connect(
"delete_nodes_request", callable_mp(this, &VoxelGraphEditor::_on_graph_edit_delete_nodes_request));
_graph_edit->connect(
"disconnection_request", callable_mp(this, &VoxelGraphEditor::_on_graph_edit_disconnection_request));
_graph_edit->connect("node_selected", callable_mp(this, &VoxelGraphEditor::_on_graph_edit_node_selected));
_graph_edit->connect("node_deselected", callable_mp(this, &VoxelGraphEditor::_on_graph_edit_node_deselected));
vbox_container->add_child(_graph_edit);
add_child(vbox_container);
_context_menu = memnew(PopupMenu);
FixedArray<PopupMenu *, VoxelGraphNodeDB::CATEGORY_COUNT> category_menus;
for (unsigned int i = 0; i < category_menus.size(); ++i) {
String name = VoxelGraphNodeDB::get_category_name(VoxelGraphNodeDB::Category(i));
PopupMenu *menu = memnew(PopupMenu);
menu->set_name(name);
menu->connect("id_pressed", callable_mp(this, &VoxelGraphEditor::_on_context_menu_id_pressed));
_context_menu->add_child(menu);
_context_menu->add_submenu_item(name, name, i);
category_menus[i] = menu;
}
for (int i = 0; i < VoxelGraphNodeDB::get_singleton().get_type_count(); ++i) {
const VoxelGraphNodeDB::NodeType &node_type = VoxelGraphNodeDB::get_singleton().get_type(i);
PopupMenu *menu = category_menus[node_type.category];
menu->add_item(node_type.name, i);
}
_context_menu->hide();
add_child(_context_menu);
_range_analysis_dialog = memnew(VoxelRangeAnalysisDialog);
_range_analysis_dialog->connect(
"analysis_toggled", callable_mp(this, &VoxelGraphEditor::_on_range_analysis_toggled));
_range_analysis_dialog->connect(
"area_changed", callable_mp(this, &VoxelGraphEditor::_on_range_analysis_area_changed));
add_child(_range_analysis_dialog);
_shader_dialog = memnew(VoxelGraphEditorShaderDialog);
add_child(_shader_dialog);
}
void VoxelGraphEditor::set_graph(Ref<VoxelGeneratorGraph> graph) {
if (_graph == graph) {
return;
}
if (_graph.is_valid()) {
_graph->disconnect(
CoreStringNames::get_singleton()->changed, callable_mp(this, &VoxelGraphEditor::_on_graph_changed));
_graph->disconnect(VoxelGeneratorGraph::SIGNAL_NODE_NAME_CHANGED,
callable_mp(this, &VoxelGraphEditor::_on_graph_node_name_changed));
}
_graph = graph;
if (_graph.is_valid()) {
// Load a default preset when creating new graphs.
// TODO Downside is, an empty graph cannot be seen.
// But Godot doesnt let us know if the resource has been created from the inspector or not
if (_graph->get_nodes_count() == 0) {
_graph->load_plane_preset();
}
_graph->connect(
CoreStringNames::get_singleton()->changed, callable_mp(this, &VoxelGraphEditor::_on_graph_changed));
_graph->connect(VoxelGeneratorGraph::SIGNAL_NODE_NAME_CHANGED,
callable_mp(this, &VoxelGraphEditor::_on_graph_node_name_changed));
}
_debug_renderer.clear();
build_gui_from_graph();
schedule_preview_update();
}
void VoxelGraphEditor::set_undo_redo(Ref<EditorUndoRedoManager> undo_redo) {
_undo_redo = undo_redo;
}
void VoxelGraphEditor::set_voxel_node(VoxelNode *node) {
_voxel_node = node;
if (_voxel_node == nullptr) {
ZN_PRINT_VERBOSE("Reference node for VoxelGraph gizmos: null");
_debug_renderer.set_world(nullptr);
} else {
ZN_PRINT_VERBOSE(format("Reference node for VoxelGraph gizmos: {}", String(node->get_path())));
_debug_renderer.set_world(_voxel_node->get_world_3d().ptr());
}
}
void VoxelGraphEditor::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_INTERNAL_PROCESS:
_process(get_tree()->get_process_time());
break;
case NOTIFICATION_VISIBILITY_CHANGED:
set_process_internal(is_visible());
break;
case NOTIFICATION_THEME_CHANGED:
_pin_button->set_icon(get_theme_icon(SNAME("Pin"), SNAME("EditorIcons")));
_popout_button->set_icon(get_theme_icon(SNAME("ExternalLink"), SNAME("EditorIcons")));
break;
}
}
void VoxelGraphEditor::_process(float delta) {
if (_time_before_preview_update > 0.f) {
_time_before_preview_update -= delta;
if (_time_before_preview_update < 0.f) {
update_previews(true);
}
}
// I decided to do by polling to display some things on graph nodes, so all the code is here and there is no faffing
// around with signals.
if (_graph.is_valid() && is_visible_in_tree()) {
for (int child_node_index = 0; child_node_index < _graph_edit->get_child_count(); ++child_node_index) {
Node *node = _graph_edit->get_child(child_node_index);
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(node);
if (node_view != nullptr) {
node_view->poll(**_graph);
}
}
}
}
void VoxelGraphEditor::clear() {
_graph_edit->clear_connections();
for (int i = 0; i < _graph_edit->get_child_count(); ++i) {
Node *node = _graph_edit->get_child(i);
GraphNode *node_view = Object::cast_to<GraphNode>(node);
if (node_view != nullptr) {
memdelete(node_view);
--i;
}
}
}
inline String node_to_gui_name(uint32_t node_id) {
return String("{0}").format(varray(node_id));
}
void VoxelGraphEditor::build_gui_from_graph() {
// Rebuild the entire graph GUI
clear();
if (_graph.is_null()) {
return;
}
const VoxelGeneratorGraph &graph = **_graph;
// Nodes
PackedInt32Array node_ids = graph.get_node_ids();
for (int i = 0; i < node_ids.size(); ++i) {
const uint32_t node_id = node_ids[i];
create_node_gui(node_id);
}
// Connections
std::vector<ProgramGraph::Connection> connections;
graph.get_connections(connections);
for (size_t i = 0; i < connections.size(); ++i) {
const ProgramGraph::Connection &con = connections[i];
const String from_node_name = node_to_gui_name(con.src.node_id);
const String to_node_name = node_to_gui_name(con.dst.node_id);
VoxelGraphEditorNode *to_node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(to_node_name));
ERR_FAIL_COND(to_node_view == nullptr);
const Error err = _graph_edit->connect_node(
from_node_name, con.src.port_index, to_node_view->get_name(), con.dst.port_index);
ERR_FAIL_COND(err != OK);
}
}
void VoxelGraphEditor::create_node_gui(uint32_t node_id) {
// Build one GUI node
CRASH_COND(_graph.is_null());
const String ui_node_name = node_to_gui_name(node_id);
ERR_FAIL_COND(_graph_edit->has_node(ui_node_name));
VoxelGraphEditorNode *node_view = VoxelGraphEditorNode::create(**_graph, node_id);
node_view->set_name(ui_node_name);
node_view->connect("dragged", callable_mp(this, &VoxelGraphEditor::_on_graph_node_dragged).bind(node_id));
_graph_edit->add_child(node_view);
}
void remove_connections_from_and_to(GraphEdit &graph_edit, StringName node_name) {
// Get copy of connection list
List<GraphEdit::Connection> connections;
graph_edit.get_connection_list(&connections);
for (List<GraphEdit::Connection>::Element *E = connections.front(); E; E = E->next()) {
const GraphEdit::Connection &con = E->get();
if (con.from == node_name || con.to == node_name) {
graph_edit.disconnect_node(con.from, con.from_port, con.to, con.to_port);
}
}
}
void VoxelGraphEditor::remove_node_gui(StringName gui_node_name) {
// Remove connections from the UI, because GraphNode doesn't do it...
remove_connections_from_and_to(*_graph_edit, gui_node_name);
Node *node_view = _graph_edit->get_node(to_node_path(gui_node_name));
ERR_FAIL_COND(Object::cast_to<GraphNode>(node_view) == nullptr);
memdelete(node_view);
}
// There is no API for this (and no internal function either), so like the implementation, I copy/pasted it
// static const GraphNode *get_graph_node_under_mouse(const GraphEdit *graph_edit) {
// for (int i = graph_edit->get_child_count() - 1; i >= 0; i--) {
// const GraphNode *gn = Object::cast_to<GraphNode>(graph_edit->get_child(i));
// if (gn != nullptr) {
// Rect2 r = gn->get_rect();
// r.size *= graph_edit->get_zoom();
// if (r.has_point(graph_edit->get_local_mouse_position())) {
// return gn;
// }
// }
// }
// return nullptr;
// }
void VoxelGraphEditor::update_node_layout(uint32_t node_id) {
ERR_FAIL_COND(_graph.is_null());
GraphEdit &graph_edit = *_graph_edit;
const String view_name = node_to_gui_name(node_id);
VoxelGraphEditorNode *view = Object::cast_to<VoxelGraphEditorNode>(graph_edit.get_node(view_name));
ERR_FAIL_COND(view == nullptr);
// Remove all GUI connections going to the node
List<GraphEdit::Connection> old_connections;
graph_edit.get_connection_list(&old_connections);
for (List<GraphEdit::Connection>::Element *e = old_connections.front(); e; e = e->next()) {
const GraphEdit::Connection &con = e->get();
const NodePath to = to_node_path(con.to);
const VoxelGraphEditorNode *to_view = Object::cast_to<VoxelGraphEditorNode>(graph_edit.get_node(to));
if (to_view == nullptr) {
continue;
}
if (to_view == view) {
graph_edit.disconnect_node(con.from, con.from_port, con.to, con.to_port);
}
}
// Update node layout
view->update_layout(**_graph);
// TODO What about output connections?
// Currently assuming there is always only one for expression nodes, therefore it might be ok?
// Add connections back by reading the graph
// TODO Optimize: the graph stores an adjacency list, we could use that
std::vector<ProgramGraph::Connection> connections;
_graph->get_connections(connections);
for (size_t i = 0; i < connections.size(); ++i) {
const ProgramGraph::Connection &con = connections[i];
if (con.dst.node_id == node_id) {
graph_edit.connect_node(node_to_gui_name(con.src.node_id), con.src.port_index,
node_to_gui_name(con.dst.node_id), con.dst.port_index);
}
}
}
bool VoxelGraphEditor::is_pinned_hint() const {
return _pin_button->is_pressed();
}
static bool is_nothing_selected(GraphEdit *graph_edit) {
for (int i = 0; i < graph_edit->get_child_count(); ++i) {
GraphNode *node = Object::cast_to<GraphNode>(graph_edit->get_child(i));
if (node != nullptr && node->is_selected()) {
return false;
}
}
return true;
}
void VoxelGraphEditor::_on_graph_edit_gui_input(Ref<InputEvent> event) {
Ref<InputEventMouseButton> mb = event;
if (mb.is_valid()) {
if (mb->is_pressed()) {
if (mb->get_button_index() == MouseButton::RIGHT) {
_click_position = mb->get_position();
_context_menu->set_position(get_global_mouse_position());
_context_menu->popup();
}
}
}
}
void VoxelGraphEditor::_on_graph_edit_connection_request(
String from_node_name, int from_slot, String to_node_name, int to_slot) {
//
VoxelGraphEditorNode *src_node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(from_node_name));
VoxelGraphEditorNode *dst_node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(to_node_name));
ERR_FAIL_COND(src_node_view == nullptr);
ERR_FAIL_COND(dst_node_view == nullptr);
const uint32_t src_node_id = src_node_view->get_generator_node_id();
const uint32_t dst_node_id = dst_node_view->get_generator_node_id();
//print("Connection attempt from ", from, ":", from_slot, " to ", to, ":", to_slot)
if (!_graph->is_valid_connection(src_node_id, from_slot, dst_node_id, to_slot)) {
ZN_PRINT_VERBOSE("Connection is invalid");
return;
}
_undo_redo->create_action(TTR("Connect Nodes"));
ProgramGraph::PortLocation prev_src_port;
String prev_src_node_name;
const bool replacing =
_graph->try_get_connection_to(ProgramGraph::PortLocation{ dst_node_id, uint32_t(to_slot) }, prev_src_port);
if (replacing) {
// Remove existing connection so we can replace with the new one
prev_src_node_name = node_to_gui_name(prev_src_port.node_id);
_undo_redo->add_do_method(
*_graph, "remove_connection", prev_src_port.node_id, prev_src_port.port_index, dst_node_id, to_slot);
_undo_redo->add_do_method(
_graph_edit, "disconnect_node", prev_src_node_name, prev_src_port.port_index, to_node_name, to_slot);
}
_undo_redo->add_do_method(*_graph, "add_connection", src_node_id, from_slot, dst_node_id, to_slot);
_undo_redo->add_do_method(_graph_edit, "connect_node", from_node_name, from_slot, to_node_name, to_slot);
_undo_redo->add_undo_method(*_graph, "remove_connection", src_node_id, from_slot, dst_node_id, to_slot);
_undo_redo->add_undo_method(_graph_edit, "disconnect_node", from_node_name, from_slot, to_node_name, to_slot);
if (replacing) {
// After undoing the connection we added, put back the connection we replaced
_undo_redo->add_undo_method(
*_graph, "add_connection", prev_src_port.node_id, prev_src_port.port_index, dst_node_id, to_slot);
_undo_redo->add_undo_method(
_graph_edit, "connect_node", prev_src_node_name, prev_src_port.port_index, to_node_name, to_slot);
}
_undo_redo->commit_action();
}
void VoxelGraphEditor::_on_graph_edit_disconnection_request(
String from_node_name, int from_slot, String to_node_name, int to_slot) {
VoxelGraphEditorNode *src_node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(from_node_name));
VoxelGraphEditorNode *dst_node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(to_node_name));
ERR_FAIL_COND(src_node_view == nullptr);
ERR_FAIL_COND(dst_node_view == nullptr);
const uint32_t src_node_id = src_node_view->get_generator_node_id();
const uint32_t dst_node_id = dst_node_view->get_generator_node_id();
_undo_redo->create_action(TTR("Disconnect Nodes"));
_undo_redo->add_do_method(*_graph, "remove_connection", src_node_id, from_slot, dst_node_id, to_slot);
_undo_redo->add_do_method(_graph_edit, "disconnect_node", from_node_name, from_slot, to_node_name, to_slot);
_undo_redo->add_undo_method(*_graph, "add_connection", src_node_id, from_slot, dst_node_id, to_slot);
_undo_redo->add_undo_method(_graph_edit, "connect_node", from_node_name, from_slot, to_node_name, to_slot);
_undo_redo->commit_action();
}
void VoxelGraphEditor::_on_graph_edit_delete_nodes_request(TypedArray<StringName> node_names) {
std::vector<VoxelGraphEditorNode *> to_erase;
// The `node_names` argument is the result of Godot issue #61112. While it is less convenient than just getting
// the nodes themselves, it also has the downside of being always empty if you choose to not show "close" buttons
// on every graph node corner, even if you have nodes selected.
// So... I keep doing it the old way.
for (int i = 0; i < _graph_edit->get_child_count(); ++i) {
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_child(i));
if (node_view != nullptr) {
if (node_view->is_selected()) {
to_erase.push_back(node_view);
}
}
}
_undo_redo->create_action(TTR("Delete Nodes"));
std::vector<ProgramGraph::Connection> connections;
_graph->get_connections(connections);
for (size_t i = 0; i < to_erase.size(); ++i) {
const VoxelGraphEditorNode *node_view = to_erase[i];
const uint32_t node_id = node_view->get_generator_node_id();
const uint32_t node_type_id = _graph->get_node_type_id(node_id);
_undo_redo->add_do_method(*_graph, "remove_node", node_id);
_undo_redo->add_do_method(this, "remove_node_gui", node_view->get_name());
_undo_redo->add_undo_method(
*_graph, "create_node", node_type_id, _graph->get_node_gui_position(node_id), node_id);
// Params undo
const size_t param_count = VoxelGraphNodeDB::get_singleton().get_type(node_type_id).params.size();
for (size_t j = 0; j < param_count; ++j) {
Variant param_value = _graph->get_node_param(node_id, j);
_undo_redo->add_undo_method(*_graph, "set_node_param", node_id, ZN_SIZE_T_TO_VARIANT(j), param_value);
}
_undo_redo->add_undo_method(this, "create_node_gui", node_id);
// Connections undo
for (size_t j = 0; j < connections.size(); ++j) {
const ProgramGraph::Connection &con = connections[j];
if (con.src.node_id == node_id || con.dst.node_id == node_id) {
_undo_redo->add_undo_method(*_graph, "add_connection", con.src.node_id, con.src.port_index,
con.dst.node_id, con.dst.port_index);
const String src_node_name = node_to_gui_name(con.src.node_id);
const String dst_node_name = node_to_gui_name(con.dst.node_id);
_undo_redo->add_undo_method(_graph_edit, "connect_node", src_node_name, con.src.port_index,
dst_node_name, con.dst.port_index);
}
}
}
_undo_redo->commit_action();
emit_signal(SIGNAL_NODES_DELETED);
}
void VoxelGraphEditor::_on_graph_node_dragged(Vector2 from, Vector2 to, int id) {
_undo_redo->create_action(TTR("Move nodes"));
_undo_redo->add_do_method(this, "set_node_position", id, to);
_undo_redo->add_undo_method(this, "set_node_position", id, from);
_undo_redo->commit_action();
// I haven't the faintest idea how VisualScriptEditor magically makes this work,
// neither using `create_action` nor `commit_action`.
}
void VoxelGraphEditor::set_node_position(int id, Vector2 offset) {
String node_name = node_to_gui_name(id);
GraphNode *node_view = Object::cast_to<GraphNode>(_graph_edit->get_node(node_name));
if (node_view != nullptr) {
node_view->set_position_offset(offset);
}
_graph->set_node_gui_position(id, offset / EDSCALE);
}
Vector2 get_graph_offset_from_mouse(const GraphEdit *graph_edit, const Vector2 local_mouse_pos) {
// TODO Ask for a method, or at least documentation about how it's done
Vector2 offset = graph_edit->get_scroll_ofs() + local_mouse_pos;
if (graph_edit->is_using_snap()) {
const int snap = graph_edit->get_snap();
offset = offset.snapped(Vector2(snap, snap));
}
offset /= EDSCALE;
offset /= graph_edit->get_zoom();
return offset;
}
void VoxelGraphEditor::_on_context_menu_id_pressed(int id) {
const Vector2 pos = get_graph_offset_from_mouse(_graph_edit, _click_position);
const uint32_t node_type_id = id;
const uint32_t node_id = _graph->generate_node_id();
const StringName node_name = node_to_gui_name(node_id);
_undo_redo->create_action(TTR("Create Node"));
_undo_redo->add_do_method(*_graph, "create_node", node_type_id, pos, node_id);
_undo_redo->add_do_method(this, "create_node_gui", node_id);
_undo_redo->add_undo_method(*_graph, "remove_node", node_id);
_undo_redo->add_undo_method(this, "remove_node_gui", node_name);
_undo_redo->commit_action();
}
void VoxelGraphEditor::_on_graph_edit_node_selected(Node *p_node) {
VoxelGraphEditorNode *node = Object::cast_to<VoxelGraphEditorNode>(p_node);
emit_signal(SIGNAL_NODE_SELECTED, node->get_generator_node_id());
}
void VoxelGraphEditor::_on_graph_edit_node_deselected(Node *p_node) {
// Just checking if nothing is selected _now_ is unreliable, because the user could have just selected another
// node, and I don't know when `GraphEdit` will update the `selected` flags in the current call stack.
// GraphEdit doesn't have an API giving us enough context to guess that, so have to rely on dirty workaround.
if (!_nothing_selected_check_scheduled) {
_nothing_selected_check_scheduled = true;
call_deferred("_check_nothing_selected");
}
}
void VoxelGraphEditor::_check_nothing_selected() {
_nothing_selected_check_scheduled = false;
if (is_nothing_selected(_graph_edit)) {
emit_signal(SIGNAL_NOTHING_SELECTED);
}
}
void reset_modulates(GraphEdit &graph_edit) {
for (int child_index = 0; child_index < graph_edit.get_child_count(); ++child_index) {
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(graph_edit.get_child(child_index));
if (node_view == nullptr) {
continue;
}
node_view->set_modulate(Color(1, 1, 1));
}
}
void VoxelGraphEditor::update_previews(bool with_live_update) {
if (_graph.is_null()) {
return;
}
clear_range_analysis_tooltips();
hide_profiling_ratios();
reset_modulates(*_graph_edit);
const uint64_t time_before = Time::get_singleton()->get_ticks_usec();
const VoxelGraphRuntime::CompilationResult result = _graph->compile(true);
if (!result.success) {
ERR_PRINT(String("Voxel graph compilation failed: {0}").format(varray(result.message)));
_compile_result_label->set_text(result.message);
_compile_result_label->set_tooltip_text(result.message);
_compile_result_label->set_modulate(Color(1, 0.3, 0.1));
_compile_result_label->show();
if (result.node_id >= 0) {
String node_view_path = node_to_gui_name(result.node_id);
VoxelGraphEditorNode *node_view =
Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(node_view_path));
node_view->set_modulate(Color(1, 0.3, 0.1));
}
return;
} else {
_compile_result_label->hide();
}
if (!_graph->is_good()) {
return;
}
// We assume no other thread will try to modify the graph and compile something not good
update_slice_previews();
if (_range_analysis_dialog->is_analysis_enabled()) {
update_range_analysis_previews();
}
const uint64_t time_taken = Time::get_singleton()->get_ticks_usec() - time_before;
ZN_PRINT_VERBOSE(format("Previews generated in {} us", time_taken));
if (_live_update_enabled && with_live_update) {
// Check if the graph changed in a way that actually changes the output,
// because re-generating all voxels is expensive.
// Note, sub-resouces can be involved, not just node connections and properties.
const uint64_t hash = _graph->get_output_graph_hash();
if (hash != _last_output_graph_hash) {
_last_output_graph_hash = hash;
// Not calling into `_voxel_node` directly because the editor could be pinned and the terrain not actually
// selected. In this situation the plugin may reset the node to null. But it is desirable for terrains
// using the current graph to update if they are in the edited scene, so this may be delegated to the editor
// plugin. There isn't enough context from here to do this cleanly.
emit_signal(SIGNAL_REGENERATE_REQUESTED);
}
}
}
void VoxelGraphEditor::update_range_analysis_previews() {
ZN_PRINT_VERBOSE("Updating range analysis previews");
ERR_FAIL_COND(_graph.is_null());
ERR_FAIL_COND(!_graph->is_good());
const AABB aabb = _range_analysis_dialog->get_aabb();
_graph->debug_analyze_range(math::floor_to_int(aabb.position), math::floor_to_int(aabb.position + aabb.size), true);
const VoxelGraphRuntime::State &state = _graph->get_last_state_from_current_thread();
const Color greyed_out_color(1, 1, 1, 0.5);
for (int child_index = 0; child_index < _graph_edit->get_child_count(); ++child_index) {
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_child(child_index));
if (node_view == nullptr) {
continue;
}
if (!node_view->has_outputs()) {
continue;
}
// Assume the node won't run for now
// TODO Would be nice if GraphEdit's minimap would take such coloring into account...
node_view->set_modulate(greyed_out_color);
node_view->update_range_analysis_tooltips(**_graph, state);
}
// Highlight only nodes that will actually run.
// Note, some nodes can appear twice in this map due to internal expansion.
Span<const uint32_t> execution_map = VoxelGeneratorGraph::get_last_execution_map_debug_from_current_thread();
for (unsigned int i = 0; i < execution_map.size(); ++i) {
const uint32_t node_id = execution_map[i];
// Some returned nodes might not be in the user-facing graph because they get generated during compilation
if (!_graph->has_node(node_id)) {
ZN_PRINT_VERBOSE(
format("Ignoring node {} from range analysis results, not present in user graph", node_id));
continue;
}
const String node_view_path = node_to_gui_name(node_id);
Node *node = _graph_edit->get_node(node_view_path);
ZN_ASSERT_CONTINUE(node != nullptr);
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(node);
ZN_ASSERT_CONTINUE(node_view != nullptr);
node_view->set_modulate(Color(1, 1, 1));
}
}
void VoxelGraphEditor::update_range_analysis_gizmo() {
if (!_range_analysis_dialog->is_analysis_enabled()) {
_debug_renderer.clear();
return;
}
if (_voxel_node == nullptr) {
return;
}
const Transform3D parent_transform = _voxel_node->get_global_transform();
const AABB aabb = _range_analysis_dialog->get_aabb();
_debug_renderer.begin();
_debug_renderer.draw_box(parent_transform * Transform3D(Basis().scaled(aabb.size), aabb.position),
DebugColors::ID_VOXEL_GRAPH_DEBUG_BOUNDS);
_debug_renderer.end();
}
void VoxelGraphEditor::update_slice_previews() {
// TODO Use a thread?
ZN_PRINT_VERBOSE("Updating slice previews");
ERR_FAIL_COND(!_graph->is_good());
struct PreviewInfo {
VoxelGraphEditorNodePreview *control;
uint32_t address;
float min_value;
float value_scale;
};
std::vector<PreviewInfo> previews;
// Gather preview nodes
for (int i = 0; i < _graph_edit->get_child_count(); ++i) {
const VoxelGraphEditorNode *node = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_child(i));
if (node == nullptr || node->get_preview() == nullptr) {
continue;
}
ProgramGraph::PortLocation dst;
dst.node_id = node->get_generator_node_id();
dst.port_index = 0;
ProgramGraph::PortLocation src;
if (!_graph->try_get_connection_to(dst, src)) {
// Not connected?
continue;
}
PreviewInfo info;
info.control = node->get_preview();
if (!_graph->try_get_output_port_address(src, info.address)) {
// Not part of the compiled result
continue;
}
info.min_value = _graph->get_node_param(dst.node_id, 0);
const float max_value = _graph->get_node_param(dst.node_id, 1);
info.value_scale = 1.f / (max_value - info.min_value);
previews.push_back(info);
}
// Generate data
{
const int preview_size_x = VoxelGraphEditorNodePreview::RESOLUTION;
const int preview_size_y = VoxelGraphEditorNodePreview::RESOLUTION;
const int buffer_size = preview_size_x * preview_size_y;
std::vector<float> x_vec;
std::vector<float> y_vec;
std::vector<float> z_vec;
x_vec.resize(buffer_size);
y_vec.resize(buffer_size);
z_vec.resize(buffer_size);
const Vector3f min_pos(-preview_size_x / 2, -preview_size_y / 2, 0);
const Vector3f max_pos = min_pos + Vector3f(preview_size_x, preview_size_x, 0);
int i = 0;
for (int iy = 0; iy < preview_size_x; ++iy) {
const float y = Math::lerp(min_pos.y, max_pos.y, static_cast<float>(iy) / preview_size_y);
for (int ix = 0; ix < preview_size_y; ++ix) {
const float x = Math::lerp(min_pos.x, max_pos.x, static_cast<float>(ix) / preview_size_x);
x_vec[i] = x;
y_vec[i] = y;
z_vec[i] = min_pos.z;
++i;
}
}
Span<float> x_coords = to_span(x_vec);
Span<float> y_coords;
Span<float> z_coords;
if (_preview_axes == PREVIEW_XY) {
y_coords = to_span(y_vec);
z_coords = to_span(z_vec);
} else {
y_coords = to_span(z_vec);
z_coords = to_span(y_vec);
}
_graph->generate_set(x_coords, y_coords, z_coords);
}
const VoxelGraphRuntime::State &last_state = VoxelGeneratorGraph::get_last_state_from_current_thread();
// Update previews
for (size_t preview_index = 0; preview_index < previews.size(); ++preview_index) {
PreviewInfo &info = previews[preview_index];
const VoxelGraphRuntime::Buffer &buffer = last_state.get_buffer(info.address);
Image &im = **info.control->get_image();
ERR_FAIL_COND(im.get_width() * im.get_height() != static_cast<int>(buffer.size));
// TODO Support debugging inputs
ERR_CONTINUE_MSG(buffer.data == nullptr,
buffer.is_binding ? "Plugging a debug view on an input is not supported yet."
: "Didn't expect buffer to be null");
unsigned int i = 0;
for (int y = 0; y < im.get_height(); ++y) {
for (int x = 0; x < im.get_width(); ++x) {
const float v = buffer.data[i];
const float g = math::clamp((v - info.min_value) * info.value_scale, 0.f, 1.f);
im.set_pixel(x, im.get_height() - y - 1, Color(g, g, g));
++i;
}
}
info.control->update_texture();
}
}
void VoxelGraphEditor::clear_range_analysis_tooltips() {
for (int child_index = 0; child_index < _graph_edit->get_child_count(); ++child_index) {
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_child(child_index));
if (node_view == nullptr) {
continue;
}
node_view->clear_range_analysis_tooltips();
}
}
void VoxelGraphEditor::schedule_preview_update() {
_time_before_preview_update = 0.5f;
}
void VoxelGraphEditor::_on_graph_changed() {
schedule_preview_update();
}
void VoxelGraphEditor::_on_graph_node_name_changed(int node_id) {
ERR_FAIL_COND(_graph.is_null());
StringName node_name = _graph->get_node_name(node_id);
const uint32_t node_type_id = _graph->get_node_type_id(node_id);
String node_type_name = VoxelGraphNodeDB::get_singleton().get_type(node_type_id).name;
const String ui_node_name = node_to_gui_name(node_id);
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(ui_node_name));
ERR_FAIL_COND(node_view == nullptr);
if (node_type_id != VoxelGeneratorGraph::NODE_EXPRESSION) {
node_view->update_title(node_name, node_type_name);
}
}
void VoxelGraphEditor::_on_update_previews_button_pressed() {
update_previews(false);
}
void VoxelGraphEditor::_on_profile_button_pressed() {
if (_graph.is_null() || !_graph->is_good()) {
return;
}
std::vector<VoxelGeneratorGraph::NodeProfilingInfo> nodes_profiling_info;
const float us = _graph->debug_measure_microseconds_per_voxel(false, &nodes_profiling_info);
_profile_label->set_text(String("{0} microseconds per voxel").format(varray(us)));
struct NodeRatio {
uint32_t node_id;
float ratio;
};
std::vector<NodeRatio> node_ratios;
// Deduplicate entries and get maximum
float max_individual_time = 0.f;
for (const VoxelGeneratorGraph::NodeProfilingInfo &info : nodes_profiling_info) {
unsigned int i = 0;
for (; i < node_ratios.size(); ++i) {
if (node_ratios[i].node_id == info.node_id) {
break;
}
}
if (i == node_ratios.size()) {
node_ratios.push_back(NodeRatio{ info.node_id, float(info.microseconds) });
} else {
node_ratios[i].ratio += info.microseconds;
}
max_individual_time = math::max(max_individual_time, float(info.microseconds));
}
if (max_individual_time > 0.f) {
for (NodeRatio &nr : node_ratios) {
nr.ratio = math::clamp(nr.ratio / max_individual_time, 0.f, 1.f);
}
} else {
for (NodeRatio &nr : node_ratios) {
nr.ratio = 1.f;
}
}
for (const NodeRatio &nr : node_ratios) {
// Some nodes generated during compilation aren't present in the user-facing graph
if (!_graph->has_node(nr.node_id)) {
ZN_PRINT_VERBOSE(format("Ignoring node {} from profiling results, not present in user graph", nr.node_id));
continue;
}
const String ui_node_name = node_to_gui_name(nr.node_id);
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(ui_node_name));
ERR_CONTINUE(node_view == nullptr);
node_view->set_profiling_ratio_visible(true);
node_view->set_profiling_ratio(nr.ratio);
}
}
void VoxelGraphEditor::hide_profiling_ratios() {
for (int child_index = 0; child_index < _graph_edit->get_child_count(); ++child_index) {
VoxelGraphEditorNode *node_view = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_child(child_index));
if (node_view == nullptr) {
continue;
}
node_view->set_profiling_ratio_visible(false);
}
}
void VoxelGraphEditor::_on_analyze_range_button_pressed() {
_range_analysis_dialog->popup_centered();
}
void VoxelGraphEditor::_on_range_analysis_toggled(bool enabled) {
schedule_preview_update();
update_range_analysis_gizmo();
}
void VoxelGraphEditor::_on_range_analysis_area_changed() {
schedule_preview_update();
update_range_analysis_gizmo();
}
void VoxelGraphEditor::_on_preview_axes_menu_id_pressed(int id) {
ERR_FAIL_COND(id < 0 || id >= PREVIEW_AXES_OPTIONS_COUNT);
_preview_axes = PreviewAxes(id);
schedule_preview_update();
}
void VoxelGraphEditor::_on_generate_shader_button_pressed() {
ERR_FAIL_COND(_graph.is_null());
const String code = _graph->generate_shader();
if (code == "") {
return;
}
_shader_dialog->set_shader_code(code);
_shader_dialog->popup_centered();
}
void VoxelGraphEditor::_on_live_update_toggled(bool enabled) {
_live_update_enabled = enabled;
}
void VoxelGraphEditor::_on_popout_button_pressed() {
emit_signal(SIGNAL_POPOUT_REQUESTED);
}
void VoxelGraphEditor::set_popout_button_enabled(bool enable) {
_popout_button->set_visible(enable);
}
void VoxelGraphEditor::_bind_methods() {
ClassDB::bind_method(D_METHOD("_check_nothing_selected"), &VoxelGraphEditor::_check_nothing_selected);
ClassDB::bind_method(D_METHOD("create_node_gui", "node_id"), &VoxelGraphEditor::create_node_gui);
ClassDB::bind_method(D_METHOD("remove_node_gui", "node_name"), &VoxelGraphEditor::remove_node_gui);
ClassDB::bind_method(D_METHOD("set_node_position", "node_id", "offset"), &VoxelGraphEditor::set_node_position);
ClassDB::bind_method(D_METHOD("update_node_layout", "node_id"), &VoxelGraphEditor::update_node_layout);
ADD_SIGNAL(MethodInfo(SIGNAL_NODE_SELECTED, PropertyInfo(Variant::INT, "node_id")));
ADD_SIGNAL(MethodInfo(SIGNAL_NOTHING_SELECTED));
ADD_SIGNAL(MethodInfo(SIGNAL_NODES_DELETED));
ADD_SIGNAL(MethodInfo(SIGNAL_REGENERATE_REQUESTED));
ADD_SIGNAL(MethodInfo(SIGNAL_POPOUT_REQUESTED));
}
} // namespace zylann::voxel