godot_voxel/editor/graph/voxel_graph_editor.cpp
Marc Gilleron 03bfc09e99 Implemented execution map to optimize out nodes locally
- Added optional optimization of execution map
- Added visualization of skipped nodes in the editor debug tool
2021-03-21 18:48:00 +00:00

954 lines
33 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/macros.h"
#include <core/core_string_names.h>
#include <core/os/os.h>
#include <core/undo_redo.h>
#include <editor/editor_scale.h>
#include <scene/gui/check_box.h>
#include <scene/gui/dialogs.h>
#include <scene/gui/graph_edit.h>
#include <scene/gui/grid_container.h>
#include <scene/gui/label.h>
const char *VoxelGraphEditor::SIGNAL_NODE_SELECTED = "node_selected";
const char *VoxelGraphEditor::SIGNAL_NOTHING_SELECTED = "nothing_selected";
// Shows a 2D slice of the 3D set of values coming from an output port
class VoxelGraphEditorNodePreview : public VBoxContainer {
GDCLASS(VoxelGraphEditorNodePreview, VBoxContainer)
public:
static const int RESOLUTION = 64;
VoxelGraphEditorNodePreview() {
_image.instance();
_image->create(RESOLUTION, RESOLUTION, false, Image::FORMAT_L8);
_texture.instance();
update_texture();
_texture_rect = memnew(TextureRect);
_texture_rect->set_stretch_mode(TextureRect::STRETCH_SCALE);
_texture_rect->set_custom_minimum_size(Vector2(RESOLUTION, RESOLUTION));
_texture_rect->set_texture(_texture);
add_child(_texture_rect);
}
Ref<Image> get_image() const {
return _image;
}
void update_texture() {
_texture->create_from_image(_image, 0);
}
private:
TextureRect *_texture_rect = nullptr;
Ref<ImageTexture> _texture;
Ref<Image> _image;
};
// Graph node with a few custom data attached.
class VoxelGraphEditorNode : public GraphNode {
GDCLASS(VoxelGraphEditorNode, GraphNode)
public:
uint32_t node_id = 0;
VoxelGraphEditorNodePreview *preview = nullptr;
Vector<Control *> output_labels;
};
class VoxelRangeAnalysisDialog : public AcceptDialog {
GDCLASS(VoxelRangeAnalysisDialog, AcceptDialog)
public:
VoxelRangeAnalysisDialog() {
set_title(TTR("Debug Range Analysis"));
set_custom_minimum_size(EDSCALE * Vector2(300, 280));
VBoxContainer *vb = memnew(VBoxContainer);
//vb->set_anchors_preset(Control::PRESET_TOP_WIDE);
enabled_checkbox = memnew(CheckBox);
enabled_checkbox->set_text(TTR("Enabled"));
enabled_checkbox->connect("toggled", this, "_on_enabled_checkbox_toggled");
vb->add_child(enabled_checkbox);
Label *tip = memnew(Label);
// TODO Had to use `\n` and disable autowrap, otherwise the popup height becomes crazy high
// See https://github.com/godotengine/godot/issues/47005
tip->set_text(TTR("When enabled, hover node output labels to\ninspect their "
"estimated range within the\nconfigured area.\n"
"Nodes that may be optimized out locally will be greyed out."));
//tip->set_autowrap(true);
tip->set_modulate(Color(1.f, 1.f, 1.f, 0.8f));
vb->add_child(tip);
GridContainer *gc = memnew(GridContainer);
gc->set_anchors_preset(Control::PRESET_TOP_WIDE);
gc->set_columns(2);
add_row(TTR("Position X"), pos_x_spinbox, gc, 0);
add_row(TTR("Position Y"), pos_y_spinbox, gc, 0);
add_row(TTR("Position Z"), pos_z_spinbox, gc, 0);
add_row(TTR("Size X"), size_x_spinbox, gc, 100);
add_row(TTR("Size Y"), size_y_spinbox, gc, 100);
add_row(TTR("Size Z"), size_z_spinbox, gc, 100);
vb->add_child(gc);
add_child(vb);
}
AABB get_aabb() const {
const Vector3 center(pos_x_spinbox->get_value(), pos_y_spinbox->get_value(), pos_z_spinbox->get_value());
const Vector3 size(size_x_spinbox->get_value(), size_y_spinbox->get_value(), size_z_spinbox->get_value());
return AABB(center - 0.5f * size, size);
}
bool is_analysis_enabled() const {
return enabled_checkbox->is_pressed();
}
private:
void _on_enabled_checkbox_toggled(bool enabled) {
emit_signal("analysis_toggled", enabled);
}
void _on_area_spinbox_value_changed(float value) {
emit_signal("area_changed");
}
void add_row(String text, SpinBox *&sb, GridContainer *parent, float defval) {
sb = memnew(SpinBox);
sb->set_min(-99999.f);
sb->set_max(99999.f);
sb->set_value(defval);
sb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
Label *label = memnew(Label);
label->set_text(text);
parent->add_child(label);
parent->add_child(sb);
sb->connect("value_changed", this, "_on_area_spinbox_value_changed");
}
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("_on_enabled_checkbox_toggled", "enabled"),
&VoxelRangeAnalysisDialog::_on_enabled_checkbox_toggled);
ClassDB::bind_method(D_METHOD("_on_area_spinbox_value_changed", "value"),
&VoxelRangeAnalysisDialog::_on_area_spinbox_value_changed);
ADD_SIGNAL(MethodInfo("analysis_toggled", PropertyInfo(Variant::BOOL, "enabled")));
ADD_SIGNAL(MethodInfo("area_changed"));
}
CheckBox *enabled_checkbox;
SpinBox *pos_x_spinbox;
SpinBox *pos_y_spinbox;
SpinBox *pos_z_spinbox;
SpinBox *size_x_spinbox;
SpinBox *size_y_spinbox;
SpinBox *size_z_spinbox;
};
VoxelGraphEditor::VoxelGraphEditor() {
VBoxContainer *vbox_container = memnew(VBoxContainer);
vbox_container->set_anchors_and_margins_preset(Control::PRESET_WIDE);
{
HBoxContainer *toolbar = memnew(HBoxContainer);
Button *update_previews_button = memnew(Button);
update_previews_button->set_text("Update Previews");
update_previews_button->connect("pressed", this, "_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", this, "_on_profile_button_pressed");
toolbar->add_child(profile_button);
_profile_label = memnew(Label);
toolbar->add_child(_profile_label);
Button *range_analysis_button = memnew(Button);
range_analysis_button->set_text("Analyze Range...");
range_analysis_button->connect("pressed", this, "_on_analyze_range_button_pressed");
toolbar->add_child(range_analysis_button);
vbox_container->add_child(toolbar);
}
_graph_edit = memnew(GraphEdit);
_graph_edit->set_anchors_preset(Control::PRESET_WIDE);
_graph_edit->set_right_disconnects(true);
_graph_edit->set_v_size_flags(Control::SIZE_EXPAND_FILL);
_graph_edit->connect("gui_input", this, "_on_graph_edit_gui_input");
_graph_edit->connect("connection_request", this, "_on_graph_edit_connection_request");
_graph_edit->connect("delete_nodes_request", this, "_on_graph_edit_delete_nodes_request");
_graph_edit->connect("disconnection_request", this, "_on_graph_edit_disconnection_request");
_graph_edit->connect("node_selected", this, "_on_graph_edit_node_selected");
_graph_edit->connect("node_unselected", this, "_on_graph_edit_node_unselected");
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", this, "_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", this, "_on_range_analysis_toggled");
_range_analysis_dialog->connect("area_changed", this, "_on_range_analysis_area_changed");
add_child(_range_analysis_dialog);
}
void VoxelGraphEditor::set_graph(Ref<VoxelGeneratorGraph> graph) {
if (_graph == graph) {
return;
}
if (_graph.is_valid()) {
_graph->disconnect(CoreStringNames::get_singleton()->changed, this, "_on_graph_changed");
_graph->disconnect(VoxelGeneratorGraph::SIGNAL_NODE_NAME_CHANGED, this, "_on_graph_node_name_changed");
}
_graph = graph;
if (_graph.is_valid()) {
_graph->connect(CoreStringNames::get_singleton()->changed, this, "_on_graph_changed");
_graph->connect(VoxelGeneratorGraph::SIGNAL_NODE_NAME_CHANGED, this, "_on_graph_node_name_changed");
}
_debug_renderer.clear();
build_gui_from_graph();
}
void VoxelGraphEditor::set_undo_redo(UndoRedo *undo_redo) {
_undo_redo = undo_redo;
}
void VoxelGraphEditor::set_voxel_node(VoxelNode *node) {
_voxel_node = node;
if (_voxel_node == nullptr) {
PRINT_VERBOSE("Reference node for VoxelGraph previews: null");
_debug_renderer.set_world(nullptr);
} else {
PRINT_VERBOSE(String("Reference node for VoxelGraph previews: {0}").format(varray(node->get_path())));
_debug_renderer.set_world(_voxel_node->get_world().ptr());
}
}
void VoxelGraphEditor::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_INTERNAL_PROCESS:
_process(get_tree()->get_idle_process_time());
break;
case NOTIFICATION_VISIBILITY_CHANGED:
set_process_internal(is_visible());
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();
}
}
}
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
PoolIntArray node_ids = graph.get_node_ids();
{
PoolIntArray::Read node_ids_read = node_ids.read();
for (int i = 0; i < node_ids.size(); ++i) {
const uint32_t node_id = node_ids_read[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);
}
}
static void update_node_view_title(VoxelGraphEditorNode *node_view, StringName node_name, String node_type_name) {
if (node_name == StringName()) {
node_view->set_title(node_type_name);
} else {
node_view->set_title(String("{0} ({1})").format(varray(node_name, node_type_name)));
}
}
void VoxelGraphEditor::create_node_gui(uint32_t node_id) {
// Build one GUI node
CRASH_COND(_graph.is_null());
const VoxelGeneratorGraph &graph = **_graph;
const uint32_t node_type_id = graph.get_node_type_id(node_id);
const VoxelGraphNodeDB::NodeType &node_type = VoxelGraphNodeDB::get_singleton()->get_type(node_type_id);
const String ui_node_name = node_to_gui_name(node_id);
ERR_FAIL_COND(_graph_edit->has_node(ui_node_name));
VoxelGraphEditorNode *node_view = memnew(VoxelGraphEditorNode);
node_view->set_offset(graph.get_node_gui_position(node_id) * EDSCALE);
StringName node_name = graph.get_node_name(node_id);
update_node_view_title(node_view, node_name, node_type.name);
node_view->set_name(ui_node_name);
node_view->node_id = node_id;
node_view->connect("dragged", this, "_on_graph_node_dragged", varray(node_id));
//node_view.resizable = true
//node_view.rect_size = Vector2(200, 100)
const unsigned int row_count = max(node_type.inputs.size(), node_type.outputs.size());
const Color port_color(0.4, 0.4, 1.0);
for (unsigned int i = 0; i < row_count; ++i) {
const bool has_left = i < node_type.inputs.size();
const bool has_right = i < node_type.outputs.size();
HBoxContainer *property_control = memnew(HBoxContainer);
property_control->set_custom_minimum_size(Vector2(0, 24 * EDSCALE));
if (has_left) {
Label *label = memnew(Label);
label->set_text(node_type.inputs[i].name);
property_control->add_child(label);
}
if (has_right) {
if (property_control->get_child_count() < 2) {
Control *spacer = memnew(Control);
spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
property_control->add_child(spacer);
}
Label *label = memnew(Label);
label->set_text(node_type.outputs[i].name);
// Pass filter is required to allow tooltips to work
label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
property_control->add_child(label);
node_view->output_labels.push_back(label);
}
node_view->add_child(property_control);
node_view->set_slot(i, has_left, Variant::REAL, port_color, has_right, Variant::REAL, port_color);
}
if (node_type_id == VoxelGeneratorGraph::NODE_SDF_PREVIEW) {
node_view->preview = memnew(VoxelGraphEditorNodePreview);
node_view->add_child(node_view->preview);
}
_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);
}
}
}
static NodePath to_node_path(StringName sn) {
Vector<StringName> path;
path.push_back(sn);
return NodePath(path, false);
}
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;
// }
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() == BUTTON_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);
//print("Connection attempt from ", from, ":", from_slot, " to ", to, ":", to_slot)
if (_graph->can_connect(src_node_view->node_id, from_slot, dst_node_view->node_id, to_slot)) {
_undo_redo->create_action(TTR("Connect Nodes"));
_undo_redo->add_do_method(
*_graph, "add_connection", src_node_view->node_id, from_slot, dst_node_view->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_view->node_id, from_slot, dst_node_view->node_id, to_slot);
_undo_redo->add_undo_method(_graph_edit, "disconnect_node", from_node_name, from_slot, 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);
_undo_redo->create_action(TTR("Disconnect Nodes"));
_undo_redo->add_do_method(
*_graph, "remove_connection", src_node_view->node_id, from_slot, dst_node_view->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_view->node_id, from_slot, dst_node_view->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() {
std::vector<VoxelGraphEditorNode *> to_erase;
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->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, 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();
}
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_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->node_id);
}
void VoxelGraphEditor::_on_graph_edit_node_unselected(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() {
if (_graph.is_null()) {
return;
}
clear_range_analysis_tooltips();
reset_modulates(*_graph_edit);
uint64_t time_before = OS::get_singleton()->get_ticks_usec();
const VoxelGraphRuntime::CompilationResult result = _graph->compile();
if (!result.success) {
ERR_PRINT(String("Voxel graph compilation failed: {0}").format(varray(result.message)));
// TODO Enhance reporting in the UI
return;
}
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();
}
uint64_t time_taken = OS::get_singleton()->get_ticks_usec() - time_before;
PRINT_VERBOSE(String("Previews generated in {0} us").format(varray(time_taken)));
}
void VoxelGraphEditor::update_range_analysis_previews() {
PRINT_VERBOSE("Updating range analysis previews");
ERR_FAIL_COND(!_graph->is_good());
const AABB aabb = _range_analysis_dialog->get_aabb();
_graph->analyze_range(aabb.position, aabb.position + aabb.size, true, 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->output_labels.size() == 0) {
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);
for (int port_index = 0; port_index < node_view->output_labels.size(); ++port_index) {
ProgramGraph::PortLocation loc;
loc.node_id = node_view->node_id;
loc.port_index = port_index;
uint32_t address;
if (!_graph->try_get_output_port_address(loc, address)) {
continue;
}
const Interval range = state.get_range(address);
Control *label = node_view->output_labels[port_index];
label->set_tooltip(String("Min: {0}\nMax: {1}").format(varray(range.min, range.max)));
}
}
// Highlight only nodes that will actually run
ArraySlice<const int> execution_map = state.get_debug_execution_map();
for (unsigned int i = 0; i < execution_map.size(); ++i) {
String node_view_path = node_to_gui_name(execution_map[i]);
VoxelGraphEditorNode *node_view =
Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(node_view_path));
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 Transform parent_transform = _voxel_node->get_global_transform();
const AABB aabb = _range_analysis_dialog->get_aabb();
_debug_renderer.begin();
_debug_renderer.draw_box(
parent_transform * Transform(Basis().scaled(aabb.size), aabb.position),
VoxelDebug::ID_VOXEL_GRAPH_DEBUG_BOUNDS);
_debug_renderer.end();
}
void VoxelGraphEditor::update_slice_previews() {
// TODO Use a thread?
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;
for (int i = 0; i < _graph_edit->get_child_count(); ++i) {
VoxelGraphEditorNode *node = Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_child(i));
if (node == nullptr || node->preview == nullptr) {
continue;
}
ProgramGraph::PortLocation dst;
dst.node_id = node->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->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);
}
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;
std::vector<float> sdf_vec;
x_vec.resize(buffer_size);
y_vec.resize(buffer_size);
z_vec.resize(buffer_size);
sdf_vec.resize(buffer_size);
const Vector3 min_pos(-preview_size_x / 2, -preview_size_y / 2, 0);
const Vector3 max_pos = min_pos + Vector3(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;
}
}
}
_graph->generate_set(
ArraySlice<float>(x_vec, 0, x_vec.size()),
ArraySlice<float>(y_vec, 0, y_vec.size()),
ArraySlice<float>(z_vec, 0, z_vec.size()),
ArraySlice<float>(sdf_vec, 0, sdf_vec.size()));
const VoxelGraphRuntime::State &last_state = VoxelGeneratorGraph::get_last_state_from_current_thread();
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));
im.lock();
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 = clamp((v - info.min_value) * info.value_scale, 0.f, 1.f);
im.set_pixel(x, y, Color(g, g, g));
++i;
}
}
im.unlock();
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;
}
for (int i = 0; i < node_view->output_labels.size(); ++i) {
Control *oc = node_view->output_labels[i];
oc->set_tooltip("");
}
}
}
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) {
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));
update_node_view_title(node_view, node_name, node_type_name);
}
void VoxelGraphEditor::_on_update_previews_button_pressed() {
update_previews();
}
void VoxelGraphEditor::_on_profile_button_pressed() {
if (_graph.is_null()) {
return;
}
const float us = _graph->debug_measure_microseconds_per_voxel(false);
_profile_label->set_text(String("{0} microseconds per voxel").format(varray(us)));
}
void VoxelGraphEditor::_on_analyze_range_button_pressed() {
_range_analysis_dialog->popup_centered_minsize();
}
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::_bind_methods() {
ClassDB::bind_method(D_METHOD("_on_graph_edit_gui_input", "event"), &VoxelGraphEditor::_on_graph_edit_gui_input);
ClassDB::bind_method(
D_METHOD("_on_graph_edit_connection_request", "from_node_name", "from_slot", "to_node_name", "to_slot"),
&VoxelGraphEditor::_on_graph_edit_connection_request);
ClassDB::bind_method(
D_METHOD("_on_graph_edit_disconnection_request", "from_node_name", "from_slot", "to_node_name", "to_slot"),
&VoxelGraphEditor::_on_graph_edit_disconnection_request);
ClassDB::bind_method(D_METHOD("_on_graph_edit_delete_nodes_request"),
&VoxelGraphEditor::_on_graph_edit_delete_nodes_request);
ClassDB::bind_method(D_METHOD("_on_graph_edit_node_selected"), &VoxelGraphEditor::_on_graph_edit_node_selected);
ClassDB::bind_method(D_METHOD("_on_graph_edit_node_unselected"), &VoxelGraphEditor::_on_graph_edit_node_unselected);
ClassDB::bind_method(D_METHOD("_on_graph_node_dragged", "from", "to", "id"),
&VoxelGraphEditor::_on_graph_node_dragged);
ClassDB::bind_method(D_METHOD("_on_context_menu_id_pressed", "id"), &VoxelGraphEditor::_on_context_menu_id_pressed);
ClassDB::bind_method(D_METHOD("_on_graph_changed"), &VoxelGraphEditor::_on_graph_changed);
ClassDB::bind_method(D_METHOD("_on_graph_node_name_changed"), &VoxelGraphEditor::_on_graph_node_name_changed);
ClassDB::bind_method(D_METHOD("_on_update_previews_button_pressed"),
&VoxelGraphEditor::_on_update_previews_button_pressed);
ClassDB::bind_method(D_METHOD("_on_profile_button_pressed"), &VoxelGraphEditor::_on_profile_button_pressed);
ClassDB::bind_method(D_METHOD("_on_analyze_range_button_pressed"),
&VoxelGraphEditor::_on_analyze_range_button_pressed);
ClassDB::bind_method(D_METHOD("_on_range_analysis_toggled", "enabled"),
&VoxelGraphEditor::_on_range_analysis_toggled);
ClassDB::bind_method(D_METHOD("_on_range_analysis_area_changed"),
&VoxelGraphEditor::_on_range_analysis_area_changed);
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);
ADD_SIGNAL(MethodInfo(SIGNAL_NODE_SELECTED, PropertyInfo(Variant::INT, "node_id")));
ADD_SIGNAL(MethodInfo(SIGNAL_NOTHING_SELECTED, PropertyInfo(Variant::INT, "nothing_selected")));
}