Added Expression node to VoxelGeneratorGraph. Needs polishing.

Some things to do:
- Support function calls
- Show expression in the node GUI, eventually edit it there
- Optimize power function
This commit is contained in:
Marc Gilleron 2022-04-03 16:43:04 +01:00
parent 68f8aa6092
commit 646cbacd64
22 changed files with 2175 additions and 76 deletions

View File

@ -0,0 +1,82 @@
#ifndef ZYLANN_EDITOR_PROPERTY_TEXT_CHANGE_ON_SUBMIT_H
#define ZYLANN_EDITOR_PROPERTY_TEXT_CHANGE_ON_SUBMIT_H
#include <editor/editor_inspector.h>
namespace zylann {
// The default string editor of the inspector calls the setter of the edited object on every character typed.
// This is not always desired. Instead, this editor should emit a change only when enter is pressed, or when the
// editor looses focus.
// Note: Godot's default string editor for LineEdit is `EditorPropertyText`
class EditorPropertyTextChangeOnSubmit : public EditorProperty {
GDCLASS(EditorPropertyTextChangeOnSubmit, EditorProperty)
public:
EditorPropertyTextChangeOnSubmit() {
_line_edit = memnew(LineEdit);
add_child(_line_edit);
add_focusable(_line_edit);
_line_edit->connect(
"text_submitted", callable_mp(this, &EditorPropertyTextChangeOnSubmit::_on_line_edit_text_submitted));
_line_edit->connect(
"text_changed", callable_mp(this, &EditorPropertyTextChangeOnSubmit::_on_line_edit_text_changed));
_line_edit->connect(
"focus_exited", callable_mp(this, &EditorPropertyTextChangeOnSubmit::_on_line_edit_focus_exited));
_line_edit->connect(
"focus_entered", callable_mp(this, &EditorPropertyTextChangeOnSubmit::_on_line_edit_focus_entered));
}
void update_property() override {
Object *obj = get_edited_object();
ERR_FAIL_COND(obj == nullptr);
_ignore_changes = true;
_line_edit->set_text(obj->get(get_edited_property()));
_ignore_changes = false;
}
private:
void _on_line_edit_focus_entered() {
_changed = false;
}
void _on_line_edit_text_changed(String new_text) {
if (_ignore_changes) {
return;
}
_changed = true;
}
void _on_line_edit_text_submitted(String text) {
if (_ignore_changes) {
return;
}
// Same behavior as the default `EditorPropertyText`
if (_line_edit->has_focus()) {
_line_edit->release_focus();
}
}
void _on_line_edit_focus_exited() {
if (_changed) {
_changed = false;
Object *obj = get_edited_object();
ERR_FAIL_COND(obj == nullptr);
String prev_text = obj->get(get_edited_property());
String text = _line_edit->get_text();
if (prev_text != text) {
emit_changed(get_edited_property(), text);
}
}
}
LineEdit *_line_edit = nullptr;
bool _ignore_changes = false;
bool _changed = false;
};
} // namespace zylann
#endif // ZYLANN_EDITOR_PROPERTY_TEXT_CHANGE_ON_SUBMIT_H

View File

@ -255,15 +255,15 @@ void VoxelGraphEditor::create_node_gui(uint32_t node_id) {
_graph_edit->add_child(node_view);
}
void remove_connections_from_and_to(GraphEdit *graph_edit, StringName node_name) {
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);
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);
graph_edit.disconnect_node(con.from, con.from_port, con.to, con.to_port);
}
}
}
@ -276,7 +276,7 @@ static NodePath to_node_path(StringName sn) {
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);
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);
@ -297,6 +297,52 @@ void VoxelGraphEditor::remove_node_gui(StringName gui_node_name) {
// 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();
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);
}
}
}
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));
@ -605,7 +651,7 @@ void VoxelGraphEditor::update_range_analysis_previews() {
}
// Highlight only nodes that will actually run
Span<const int> execution_map = VoxelGeneratorGraph::get_last_execution_map_debug_from_current_thread();
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) {
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));
@ -812,6 +858,7 @@ void VoxelGraphEditor::_bind_methods() {
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, PropertyInfo(Variant::INT, "nothing_selected")));

View File

@ -34,6 +34,10 @@ public:
void set_undo_redo(UndoRedo *undo_redo);
void set_voxel_node(VoxelNode *node);
// To be called when the number of inputs in a node changes.
// Rebuilds the node's internal controls, and updates GUI connections going to it from the graph.
void update_node_layout(uint32_t node_id);
private:
void _notification(int p_what);
void _process(float delta);

View File

@ -1,5 +1,6 @@
#include "voxel_graph_editor_node.h"
#include "../../generators/graph/voxel_graph_node_db.h"
#include "../../util/godot/funcs.h"
#include "voxel_graph_editor_node_preview.h"
#include <editor/editor_scale.h>
@ -9,39 +10,93 @@
namespace zylann::voxel {
VoxelGraphEditorNode *VoxelGraphEditorNode::create(const VoxelGeneratorGraph &graph, uint32_t node_id) {
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);
VoxelGraphEditorNode *node_view = memnew(VoxelGraphEditorNode);
node_view->set_position_offset(graph.get_node_gui_position(node_id) * EDSCALE);
StringName node_name = graph.get_node_name(node_id);
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 StringName node_name = graph.get_node_name(node_id);
node_view->update_title(node_name, node_type.name);
node_view->_node_id = node_id;
//node_view.resizable = true
//node_view.rect_size = Vector2(200, 100)
node_view->update_layout(graph);
if (node_type_id == VoxelGeneratorGraph::NODE_SDF_PREVIEW) {
node_view->_preview = memnew(VoxelGraphEditorNodePreview);
node_view->add_child(node_view->_preview);
}
return node_view;
}
void VoxelGraphEditorNode::update_layout(const VoxelGeneratorGraph &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);
// We artificially hide output ports if the node is an output.
// These nodes have an output for implementation reasons, some outputs can process the data like any other node.
const bool hide_outputs = node_type.category == VoxelGraphNodeDB::CATEGORY_OUTPUT;
const unsigned int row_count = math::max(node_type.inputs.size(), hide_outputs ? 0 : node_type.outputs.size());
struct Input {
String name;
};
struct Output {
String name;
};
std::vector<Input> inputs;
std::vector<Output> outputs;
{
for (const VoxelGraphNodeDB::Port &port : node_type.inputs) {
inputs.push_back({ port.name });
}
for (const VoxelGraphNodeDB::Port &port : node_type.outputs) {
outputs.push_back({ port.name });
}
if (graph.get_node_type_id(_node_id) == VoxelGeneratorGraph::NODE_EXPRESSION) {
std::vector<std::string> names;
graph.get_expression_node_inputs(_node_id, names);
for (const std::string &s : names) {
inputs.push_back({ to_godot(s) });
}
}
}
const unsigned int row_count = math::max(inputs.size(), hide_outputs ? 0 : outputs.size());
const Color port_color(0.4, 0.4, 1.0);
const Color hint_label_modulate(0.6, 0.6, 0.6);
//const int middle_min_width = EDSCALE * 32.0;
// Temporarily remove preview if any
if (_preview != nullptr) {
remove_child(_preview);
}
// Clear previous inputs and outputs
for (Node *row : _rows) {
remove_child(row);
row->queue_delete();
}
_rows.clear();
clear_all_slots();
_input_hints.clear();
// Add inputs and outputs
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()) && !hide_outputs;
const bool has_left = i < inputs.size();
const bool has_right = (i < outputs.size()) && !hide_outputs;
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);
label->set_text(inputs[i].name);
property_control->add_child(label);
Label *hint_label = memnew(Label);
@ -52,7 +107,7 @@ VoxelGraphEditorNode *VoxelGraphEditorNode::create(const VoxelGeneratorGraph &gr
property_control->add_child(hint_label);
VoxelGraphEditorNode::InputHint input_hint;
input_hint.label = hint_label;
node_view->_input_hints.push_back(input_hint);
_input_hints.push_back(input_hint);
}
if (has_right) {
@ -64,24 +119,23 @@ VoxelGraphEditorNode *VoxelGraphEditorNode::create(const VoxelGeneratorGraph &gr
}
Label *label = memnew(Label);
label->set_text(node_type.outputs[i].name);
label->set_text(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);
_output_labels.push_back(label);
}
node_view->add_child(property_control);
node_view->set_slot(i, has_left, Variant::FLOAT, port_color, has_right, Variant::FLOAT, port_color);
add_child(property_control);
set_slot(i, has_left, Variant::FLOAT, port_color, has_right, Variant::FLOAT, port_color);
_rows.push_back(property_control);
}
if (node_type_id == VoxelGeneratorGraph::NODE_SDF_PREVIEW) {
node_view->_preview = memnew(VoxelGraphEditorNodePreview);
node_view->add_child(node_view->_preview);
// Re-add preview if any
if (_preview != nullptr) {
add_child(_preview);
}
return node_view;
}
void VoxelGraphEditorNode::update_title(StringName node_name, String node_type_name) {

View File

@ -23,6 +23,8 @@ public:
void update_range_analysis_tooltips(const VoxelGeneratorGraph &graph, const VoxelGraphRuntime::State &state);
void clear_range_analysis_tooltips();
void update_layout(const VoxelGeneratorGraph &graph);
bool has_outputs() const {
return _output_labels.size() > 0;
}
@ -46,6 +48,7 @@ private:
};
std::vector<InputHint> _input_hints;
std::vector<Node *> _rows;
};
} // namespace zylann::voxel

View File

@ -1,6 +1,7 @@
#include "voxel_graph_editor_plugin.h"
#include "../../generators/graph/voxel_generator_graph.h"
#include "../../terrain/voxel_node.h"
#include "editor_property_text_change_on_submit.h"
#include "voxel_graph_editor.h"
#include "voxel_graph_node_inspector_wrapper.h"
@ -9,6 +10,27 @@
namespace zylann::voxel {
// Changes string editors of the inspector to call setters only when enter key is pressed, similar to Unreal.
// Because the default behavior of `EditorPropertyText` is to call the setter on every character typed, which is a
// nightmare when editing an Expression node: inputs change constantly as the code is written which has much higher
// chance of messing up existing connections, and creates individual UndoRedo actions as well.
class VoxelGraphEditorInspectorPlugin : public EditorInspectorPlugin {
GDCLASS(VoxelGraphEditorInspectorPlugin, EditorInspectorPlugin)
public:
bool can_handle(Object *obj) override {
return obj != nullptr && Object::cast_to<VoxelGraphNodeInspectorWrapper>(obj) != nullptr;
}
bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint,
const String &p_hint_text, const uint32_t p_usage, const bool p_wide = false) override {
if (p_type == Variant::STRING) {
add_property_editor(p_path, memnew(EditorPropertyTextChangeOnSubmit));
return true;
}
return false;
}
};
VoxelGraphEditorPlugin::VoxelGraphEditorPlugin() {
//EditorInterface *ed = get_editor_interface();
_graph_editor = memnew(VoxelGraphEditor);
@ -21,6 +43,10 @@ VoxelGraphEditorPlugin::VoxelGraphEditorPlugin() {
callable_mp(this, &VoxelGraphEditorPlugin::_on_graph_editor_nodes_deleted));
_bottom_panel_button = add_control_to_bottom_panel(_graph_editor, TTR("Voxel Graph"));
_bottom_panel_button->hide();
Ref<VoxelGraphEditorInspectorPlugin> inspector_plugin;
inspector_plugin.instantiate();
add_inspector_plugin(inspector_plugin);
}
bool VoxelGraphEditorPlugin::handles(Object *p_object) const {
@ -98,7 +124,7 @@ void VoxelGraphEditorPlugin::_hide_deferred() {
void VoxelGraphEditorPlugin::_on_graph_editor_node_selected(uint32_t node_id) {
Ref<VoxelGraphNodeInspectorWrapper> wrapper;
wrapper.instantiate();
wrapper->setup(_graph_editor->get_graph(), node_id, &get_undo_redo());
wrapper->setup(_graph_editor->get_graph(), node_id, &get_undo_redo(), _graph_editor);
// Note: it's neither explicit nor documented, but the reference will stay alive due to EditorHistory::_add_object
get_editor_interface()->inspect_object(*wrapper);
// TODO Absurd situation here...

View File

@ -1,14 +1,18 @@
#include "voxel_graph_node_inspector_wrapper.h"
#include "../../generators/graph/voxel_graph_node_db.h"
#include "../../util/godot/funcs.h"
#include "../../util/macros.h"
#include "voxel_graph_editor.h"
#include <core/object/undo_redo.h>
namespace zylann::voxel {
void VoxelGraphNodeInspectorWrapper::setup(Ref<VoxelGeneratorGraph> p_graph, uint32_t p_node_id, UndoRedo *ur) {
void VoxelGraphNodeInspectorWrapper::setup(
Ref<VoxelGeneratorGraph> p_graph, uint32_t p_node_id, UndoRedo *ur, VoxelGraphEditor *ed) {
_graph = p_graph;
_node_id = p_node_id;
_undo_redo = ur;
_graph_editor = ed;
}
void VoxelGraphNodeInspectorWrapper::_get_property_list(List<PropertyInfo> *p_list) const {
@ -63,21 +67,99 @@ void VoxelGraphNodeInspectorWrapper::_get_property_list(List<PropertyInfo> *p_li
}
}
// Automatically updates the list of inputs from variable names used in the expression.
// Contrary to VisualScript (for which this has to be done manually to the user), submitting the text field containing
// the expression's code also changes dynamic inputs of the node and reconnects existing connections, all as one
// UndoRedo action.
static void update_expression_inputs(
VoxelGeneratorGraph &generator, uint32_t node_id, String code, UndoRedo &ur, VoxelGraphEditor &graph_editor) {
//
const CharString code_utf8 = code.utf8();
std::vector<std::string_view> new_input_names;
if (!VoxelGeneratorGraph::get_expression_variables(code_utf8.get_data(), new_input_names)) {
// Error, the action will not include node input changes
return;
}
std::vector<std::string> old_input_names;
generator.get_expression_node_inputs(node_id, old_input_names);
struct Connection {
ProgramGraph::PortLocation src;
uint32_t dst_port_index;
};
// Find what we'll disconnect
std::vector<Connection> to_disconnect;
for (uint32_t port_index = 0; port_index < old_input_names.size(); ++port_index) {
ProgramGraph::PortLocation src;
if (generator.try_get_connection_to({ node_id, port_index }, src)) {
to_disconnect.push_back({ { src.node_id, src.port_index }, port_index });
}
}
// Find what we'll reconnect
std::vector<Connection> to_reconnect;
for (uint32_t port_index = 0; port_index < old_input_names.size(); ++port_index) {
const std::string_view old_name = old_input_names[port_index];
auto new_input_name_it = std::find(new_input_names.begin(), new_input_names.end(), old_name);
if (new_input_name_it != new_input_names.end()) {
ProgramGraph::PortLocation src;
if (generator.try_get_connection_to({ node_id, port_index }, src)) {
const uint32_t dst_port_index = new_input_name_it - new_input_names.begin();
to_reconnect.push_back({ src, dst_port_index });
}
}
}
// Do
for (size_t i = 0; i < to_disconnect.size(); ++i) {
const Connection con = to_disconnect[i];
ur.add_do_method(
&generator, "remove_connection", con.src.node_id, con.src.port_index, node_id, con.dst_port_index);
}
ur.add_do_method(&generator, "set_expression_node_inputs", node_id, to_godot(new_input_names));
for (size_t i = 0; i < to_reconnect.size(); ++i) {
const Connection con = to_reconnect[i];
ur.add_do_method(
&generator, "add_connection", con.src.node_id, con.src.port_index, node_id, con.dst_port_index);
}
// Undo
for (size_t i = 0; i < to_reconnect.size(); ++i) {
const Connection con = to_reconnect[i];
ur.add_undo_method(
&generator, "remove_connection", con.src.node_id, con.src.port_index, node_id, con.dst_port_index);
}
ur.add_undo_method(&generator, "set_expression_node_inputs", node_id, to_godot(old_input_names));
for (size_t i = 0; i < to_disconnect.size(); ++i) {
const Connection con = to_disconnect[i];
ur.add_undo_method(
&generator, "add_connection", con.src.node_id, con.src.port_index, node_id, con.dst_port_index);
}
ur.add_do_method(&graph_editor, "update_node_layout", node_id);
ur.add_undo_method(&graph_editor, "update_node_layout", node_id);
}
bool VoxelGraphNodeInspectorWrapper::_set(const StringName &p_name, const Variant &p_value) {
Ref<VoxelGeneratorGraph> graph = get_graph();
ERR_FAIL_COND_V(graph.is_null(), false);
ERR_FAIL_COND_V(_undo_redo == nullptr, false);
UndoRedo *ur = _undo_redo;
UndoRedo &ur = *_undo_redo;
if (p_name == "name") {
String previous_name = graph->get_node_name(_node_id);
ur->create_action("Set VoxelGeneratorGraph node name");
ur->add_do_method(graph.ptr(), "set_node_name", _node_id, p_value);
ur->add_undo_method(graph.ptr(), "set_node_name", _node_id, previous_name);
ur.create_action("Set VoxelGeneratorGraph node name");
ur.add_do_method(graph.ptr(), "set_node_name", _node_id, p_value);
ur.add_undo_method(graph.ptr(), "set_node_name", _node_id, previous_name);
// ur->add_do_method(this, "notify_property_list_changed");
// ur->add_undo_method(this, "notify_property_list_changed");
ur->commit_action();
ur.commit_action();
return true;
}
@ -88,21 +170,29 @@ bool VoxelGraphNodeInspectorWrapper::_set(const StringName &p_name, const Varian
uint32_t index;
if (db->try_get_param_index_from_name(node_type_id, p_name, index)) {
Variant previous_value = graph->get_node_param(_node_id, index);
ur->create_action("Set VoxelGeneratorGraph node parameter");
ur->add_do_method(graph.ptr(), "set_node_param", _node_id, index, p_value);
ur->add_undo_method(graph.ptr(), "set_node_param", _node_id, index, previous_value);
ur->add_do_method(this, "notify_property_list_changed");
ur->add_undo_method(this, "notify_property_list_changed");
ur->commit_action();
ur.create_action("Set VoxelGeneratorGraph node parameter");
ur.add_do_method(graph.ptr(), "set_node_param", _node_id, index, p_value);
ur.add_undo_method(graph.ptr(), "set_node_param", _node_id, index, previous_value);
if (node_type_id == VoxelGeneratorGraph::NODE_EXPRESSION) {
update_expression_inputs(**graph, _node_id, p_value, ur, *_graph_editor);
// TODO Default inputs cannot be set after adding variables!
// It requires calling `notify_property_list_changed`, however that makes the LineEdit in the inspector to
// reset its cursor position, making string parameter edition a nightmare. Only workaround is to deselect
// and re-select the node...
} else {
ur.add_do_method(this, "notify_property_list_changed");
ur.add_undo_method(this, "notify_property_list_changed");
}
ur.commit_action();
} else if (db->try_get_input_index_from_name(node_type_id, p_name, index)) {
Variant previous_value = graph->get_node_default_input(_node_id, index);
ur->create_action("Set VoxelGeneratorGraph node default input");
ur->add_do_method(graph.ptr(), "set_node_default_input", _node_id, index, p_value);
ur->add_undo_method(graph.ptr(), "set_node_default_input", _node_id, index, previous_value);
ur->add_do_method(this, "notify_property_list_changed");
ur->add_undo_method(this, "notify_property_list_changed");
ur->commit_action();
ur.create_action("Set VoxelGeneratorGraph node default input");
ur.add_do_method(graph.ptr(), "set_node_default_input", _node_id, index, p_value);
ur.add_undo_method(graph.ptr(), "set_node_default_input", _node_id, index, previous_value);
ur.add_do_method(this, "notify_property_list_changed");
ur.add_undo_method(this, "notify_property_list_changed");
ur.commit_action();
} else {
ERR_PRINT(String("Invalid param name {0}").format(varray(p_name)));

View File

@ -8,13 +8,15 @@ class UndoRedo;
namespace zylann::voxel {
class VoxelGraphEditor;
// Nodes aren't resources so this translates them into a form the inspector can understand.
// This makes it easier to support undo/redo and sub-resources.
// WARNING: `AnimationPlayer` will allow to keyframe properties, but there really is no support for that.
class VoxelGraphNodeInspectorWrapper : public RefCounted {
GDCLASS(VoxelGraphNodeInspectorWrapper, RefCounted)
public:
void setup(Ref<VoxelGeneratorGraph> p_graph, uint32_t p_node_id, UndoRedo *ur);
void setup(Ref<VoxelGeneratorGraph> p_graph, uint32_t p_node_id, UndoRedo *ur, VoxelGraphEditor *ed);
inline Ref<VoxelGeneratorGraph> get_graph() const {
return _graph;
}
@ -31,6 +33,7 @@ private:
Ref<VoxelGeneratorGraph> _graph;
uint32_t _node_id = ProgramGraph::NULL_ID;
UndoRedo *_undo_redo = nullptr;
VoxelGraphEditor *_graph_editor = nullptr;
};
} // namespace zylann::voxel

View File

@ -39,6 +39,17 @@ uint32_t ProgramGraph::Node::find_output_connection(uint32_t output_port_index,
return ProgramGraph::NULL_INDEX;
}
bool ProgramGraph::Node::find_input_port_by_name(std::string_view name, unsigned int &out_i) const {
for (unsigned int i = 0; i < inputs.size(); ++i) {
const ProgramGraph::Port &port = inputs[i];
if (port.dynamic_name == name) {
out_i = i;
return true;
}
}
return false;
}
ProgramGraph::Node *ProgramGraph::create_node(uint32_t type_id, uint32_t id) {
if (id == NULL_ID) {
id = generate_node_id();

View File

@ -1,19 +1,25 @@
#ifndef PROGRAM_GRAPH_H
#define PROGRAM_GRAPH_H
#include "../../util/non_copyable.h"
#include <core/math/vector2.h>
#include <core/templates/hashfuncs.h>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace zylann {
// Generic graph representing a program
class ProgramGraph {
class ProgramGraph : NonCopyable {
public:
static const uint32_t NULL_ID = 0;
static const uint32_t NULL_INDEX = -1;
// TODO Use typedef to make things explicit
//typedef uint32_t NodeID;
struct PortLocation {
uint32_t node_id;
uint32_t port_index;
@ -24,15 +30,16 @@ public:
PortLocation dst;
};
struct PortLocationHasher {
static inline uint32_t hash(const PortLocation &v) {
const uint32_t hash = hash_djb2_one_32(v.node_id);
return hash_djb2_one_32(v.port_index, hash);
}
};
struct Port {
std::vector<PortLocation> connections;
// Dynamic ports are ports that are not inherited from `type_id`, they exist solely for this node.
// Because it can't be deduced from `type_id`, they must be given a name.
// Initially needed for expression nodes.
std::string dynamic_name;
inline bool is_dynamic() const {
return dynamic_name.size() > 0;
}
};
struct Node {
@ -47,9 +54,12 @@ public:
uint32_t find_input_connection(PortLocation src, uint32_t input_port_index) const;
uint32_t find_output_connection(uint32_t output_port_index, PortLocation dst) const;
bool find_input_port_by_name(std::string_view name, unsigned int &out_i) const;
};
Node *create_node(uint32_t type_id, uint32_t id = NULL_ID);
// TODO Return a reference, this function is not allowed to fail
Node *get_node(uint32_t id) const;
Node *try_get_node(uint32_t id) const;
void remove_node(uint32_t id);
@ -100,6 +110,7 @@ public:
}
void copy_from(const ProgramGraph &other, bool copy_subresources);
void get_connections(std::vector<ProgramGraph::Connection> &connections) const;
//void get_connections_from_and_to(std::vector<ProgramGraph::Connection> &connections, uint32_t node_id) const;
@ -120,6 +131,24 @@ inline bool operator==(const ProgramGraph::PortLocation &a, const ProgramGraph::
return a.node_id == b.node_id && a.port_index == b.port_index;
}
// For Godot
struct ProgramGraphPortLocationHasher {
static inline uint32_t hash(const ProgramGraph::PortLocation &v) {
const uint32_t hash = hash_djb2_one_32(v.node_id);
return hash_djb2_one_32(v.port_index, hash);
}
};
} // namespace zylann
// For STL
namespace std {
template <>
struct hash<zylann::ProgramGraph::PortLocation> {
size_t operator()(const zylann::ProgramGraph::PortLocation &v) const {
return zylann::ProgramGraphPortLocationHasher::hash(v);
}
};
} // namespace std
#endif // PROGRAM_GRAPH_H

View File

@ -1,5 +1,6 @@
#include "voxel_generator_graph.h"
#include "../../storage/voxel_buffer_internal.h"
#include "../../util/expression_parser.h"
#include "../../util/godot/funcs.h"
#include "../../util/macros.h"
#include "../../util/profiling.h"
@ -29,7 +30,7 @@ void VoxelGeneratorGraph::clear() {
}
}
static ProgramGraph::Node *create_node_internal(
ProgramGraph::Node *create_node_internal(
ProgramGraph &graph, VoxelGeneratorGraph::NodeTypeID type_id, Vector2 position, uint32_t id) {
const VoxelGraphNodeDB::NodeType &type = VoxelGraphNodeDB::get_singleton()->get_type(type_id);
@ -179,6 +180,65 @@ void VoxelGeneratorGraph::set_node_param(uint32_t node_id, uint32_t param_index,
}
}
bool VoxelGeneratorGraph::get_expression_variables(std::string_view code, std::vector<std::string_view> &vars) {
// TODO Support functions
Span<const ExpressionParser::Function> functions;
ExpressionParser::Result result = ExpressionParser::parse(code, functions);
if (result.error.id == ExpressionParser::ERROR_NONE) {
if (result.root != nullptr) {
ExpressionParser::find_variables(*result.root, vars);
}
if (result.root != nullptr) {
memdelete(result.root);
}
return true;
} else {
return false;
}
}
void VoxelGeneratorGraph::get_expression_node_inputs(uint32_t node_id, std::vector<std::string> &out_names) const {
ProgramGraph::Node *node = _graph.try_get_node(node_id);
ERR_FAIL_COND(node == nullptr);
ERR_FAIL_COND(node->type_id != NODE_EXPRESSION);
for (unsigned int i = 0; i < node->inputs.size(); ++i) {
const ProgramGraph::Port &port = node->inputs[i];
ERR_FAIL_COND(!port.is_dynamic());
out_names.push_back(port.dynamic_name);
}
}
inline bool has_duplicate(const PackedStringArray &sa) {
return find_duplicate(Span<const String>(sa.ptr(), sa.size())) != sa.size();
}
void VoxelGeneratorGraph::set_expression_node_inputs(uint32_t node_id, PackedStringArray names) {
ProgramGraph::Node *node = _graph.try_get_node(node_id);
// Validate
ERR_FAIL_COND(node == nullptr);
ERR_FAIL_COND(node->type_id != NODE_EXPRESSION);
for (int i = 0; i < names.size(); ++i) {
const String name = names[i];
ERR_FAIL_COND(!name.is_valid_identifier());
}
ERR_FAIL_COND(has_duplicate(names));
for (unsigned int i = 0; i < node->inputs.size(); ++i) {
const ProgramGraph::Port &port = node->inputs[i];
// Sounds annoying if you call this from a script, but this is supposed to be editor functionality for now
ERR_FAIL_COND_MSG(port.connections.size() > 0,
TTR("Cannot change input ports if connections exist, disconnect them first."));
}
node->inputs.resize(names.size());
node->default_inputs.resize(names.size());
for (int i = 0; i < names.size(); ++i) {
const String name = names[i];
const CharString name_utf8 = name.utf8();
node->inputs[i].dynamic_name = name_utf8.get_data();
}
}
Variant VoxelGeneratorGraph::get_node_param(uint32_t node_id, uint32_t param_index) const {
const ProgramGraph::Node *node = _graph.try_get_node(node_id);
ERR_FAIL_COND_V(node == nullptr, Variant());
@ -989,7 +1049,7 @@ const VoxelGraphRuntime::State &VoxelGeneratorGraph::get_last_state_from_current
return _cache.state;
}
Span<const int> VoxelGeneratorGraph::get_last_execution_map_debug_from_current_thread() {
Span<const uint32_t> VoxelGeneratorGraph::get_last_execution_map_debug_from_current_thread() {
return to_span_const(_cache.optimized_execution_map.debug_nodes);
}
@ -1369,6 +1429,7 @@ static Dictionary get_graph_as_variant_data(const ProgramGraph &graph) {
node_data[param.name] = node->params[j];
}
// Static inputs
for (size_t j = 0; j < type.inputs.size(); ++j) {
if (node->inputs[j].connections.size() == 0) {
const VoxelGraphNodeDB::Port &port = type.inputs[j];
@ -1376,6 +1437,23 @@ static Dictionary get_graph_as_variant_data(const ProgramGraph &graph) {
}
}
// Dynamic inputs. Order matters.
Array dynamic_inputs_data;
for (size_t j = 0; j < node->inputs.size(); ++j) {
const ProgramGraph::Port &port = node->inputs[j];
if (port.is_dynamic()) {
Array d;
d.resize(2);
d[0] = String(port.dynamic_name.c_str());
d[1] = node->default_inputs[j];
dynamic_inputs_data.append(d);
}
}
if (dynamic_inputs_data.size() > 0) {
node_data["dynamic_inputs"] = dynamic_inputs_data;
}
String key = String::num_uint64(node_id);
nodes_data[key] = node_data;
});
@ -1444,6 +1522,26 @@ static bool load_graph_from_variant_data(ProgramGraph &graph, Dictionary data) {
if (param_name == "gui_position") {
continue;
}
if (param_name == "dynamic_inputs") {
const Array dynamic_inputs_data = node_data[*param_key];
for (int dpi = 0; dpi < dynamic_inputs_data.size(); ++dpi) {
const Array d = dynamic_inputs_data[dpi];
ERR_FAIL_COND_V(d.size() != 2, false);
const String dynamic_param_name = d[0];
ProgramGraph::Port dport;
CharString dynamic_param_name_utf8 = dynamic_param_name.utf8();
dport.dynamic_name = dynamic_param_name_utf8.get_data();
const Variant defval = d[1];
node->default_inputs.push_back(defval);
node->inputs.push_back(dport);
}
continue;
}
uint32_t param_index;
if (type_db.try_get_param_index_from_name(type_id, param_name, param_index)) {
node->params[param_index] = node_data[*param_key];
@ -1771,6 +1869,8 @@ void VoxelGeneratorGraph::_bind_methods() {
D_METHOD("set_node_gui_position", "node_id", "position"), &VoxelGeneratorGraph::set_node_gui_position);
ClassDB::bind_method(D_METHOD("get_node_name", "node_id"), &VoxelGeneratorGraph::get_node_name);
ClassDB::bind_method(D_METHOD("set_node_name", "node_id", "name"), &VoxelGeneratorGraph::set_node_name);
ClassDB::bind_method(D_METHOD("set_expression_node_inputs", "node_id", "names"),
&VoxelGeneratorGraph::set_expression_node_inputs);
ClassDB::bind_method(D_METHOD("set_sdf_clip_threshold", "threshold"), &VoxelGeneratorGraph::set_sdf_clip_threshold);
ClassDB::bind_method(D_METHOD("get_sdf_clip_threshold"), &VoxelGeneratorGraph::get_sdf_clip_threshold);
@ -1882,6 +1982,9 @@ void VoxelGeneratorGraph::_bind_methods() {
BIND_ENUM_CONSTANT(NODE_FAST_NOISE_2_3D);
#endif
BIND_ENUM_CONSTANT(NODE_OUTPUT_SINGLE_TEXTURE);
BIND_ENUM_CONSTANT(NODE_EXPRESSION);
BIND_ENUM_CONSTANT(NODE_POWI);
BIND_ENUM_CONSTANT(NODE_POW);
BIND_ENUM_CONSTANT(NODE_TYPE_COUNT);
}

View File

@ -68,6 +68,9 @@ public:
NODE_FAST_NOISE_2_3D,
#endif
NODE_OUTPUT_SINGLE_TEXTURE,
NODE_EXPRESSION,
NODE_POWI, // pow(x, constant positive integer)
NODE_POW, // pow(x, y)
NODE_TYPE_COUNT
};
@ -110,6 +113,10 @@ public:
Variant get_node_param(uint32_t node_id, uint32_t param_index) const;
void set_node_param(uint32_t node_id, uint32_t param_index, Variant value);
static bool get_expression_variables(std::string_view code, std::vector<std::string_view> &vars);
void get_expression_node_inputs(uint32_t node_id, std::vector<std::string> &out_names) const;
void set_expression_node_inputs(uint32_t node_id, PackedStringArray names);
Variant get_node_default_input(uint32_t node_id, uint32_t input_index) const;
void set_node_default_input(uint32_t node_id, uint32_t input_index, Variant value);
@ -173,7 +180,7 @@ public:
// Returns state from the last generator used in the current thread
static const VoxelGraphRuntime::State &get_last_state_from_current_thread();
static Span<const int> get_last_execution_map_debug_from_current_thread();
static Span<const uint32_t> get_last_execution_map_debug_from_current_thread();
bool try_get_output_port_address(ProgramGraph::PortLocation port, uint32_t &out_address) const;
@ -281,6 +288,9 @@ private:
static thread_local Cache _cache;
};
ProgramGraph::Node *create_node_internal(
ProgramGraph &graph, VoxelGeneratorGraph::NodeTypeID type_id, Vector2 position, uint32_t id);
} // namespace zylann::voxel
VARIANT_ENUM_CAST(zylann::voxel::VoxelGeneratorGraph::NodeTypeID)

View File

@ -1292,6 +1292,7 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() {
t.params.push_back(Param("min_value", Variant::FLOAT, -1.f));
t.params.push_back(Param("max_value", Variant::FLOAT, 1.f));
t.debug_only = true;
t.is_pseudo_node = true;
}
{
struct Params {
@ -1786,6 +1787,80 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() {
};
}
#endif // VOXEL_ENABLE_FAST_NOISE_2
{
NodeType &t = types[VoxelGeneratorGraph::NODE_EXPRESSION];
t.name = "Expression";
t.category = CATEGORY_MATH;
t.params.push_back(Param("expression", Variant::STRING, "0"));
t.outputs.push_back(Port("out"));
t.compile_func = [](CompileContext &ctx) { ctx.make_error("Internal error, expression wasn't expanded"); };
t.is_pseudo_node = true;
}
{
struct Params {
unsigned int power;
};
NodeType &t = types[VoxelGeneratorGraph::NODE_POWI];
t.name = "Powi";
t.category = CATEGORY_MATH;
t.inputs.push_back(Port("x"));
t.params.push_back(Param("power", Variant::INT, 2));
t.outputs.push_back(Port("out"));
t.compile_func = [](CompileContext &ctx) {
const int power = ctx.get_param(0).operator int();
if (power < 0) {
ctx.make_error("Power cannot be negative");
} else {
Params p;
p.power = power;
ctx.set_params(p);
}
};
t.process_buffer_func = [](ProcessBufferContext &ctx) {
const VoxelGraphRuntime::Buffer &x = ctx.get_input(0);
VoxelGraphRuntime::Buffer &out = ctx.get_output(0);
const unsigned int power = ctx.get_params<Params>().power;
for (unsigned int i = 0; i < out.size; ++i) {
float v = x.data[i];
for (unsigned int p = 0; p < power; ++p) {
v *= v;
}
out.data[i] = v;
}
};
t.range_analysis_func = [](RangeAnalysisContext &ctx) {
const Interval x = ctx.get_input(0);
const unsigned int power = ctx.get_params<Params>().power;
ctx.set_output(0, powi(x, power));
};
}
{
NodeType &t = types[VoxelGeneratorGraph::NODE_POW];
t.name = "Pow";
t.category = CATEGORY_MATH;
t.inputs.push_back(Port("x"));
t.inputs.push_back(Port("p", 2.f));
t.outputs.push_back(Port("out"));
t.process_buffer_func = [](ProcessBufferContext &ctx) {
const VoxelGraphRuntime::Buffer &x = ctx.get_input(0);
const VoxelGraphRuntime::Buffer &p = ctx.get_input(1);
VoxelGraphRuntime::Buffer &out = ctx.get_output(0);
for (unsigned int i = 0; i < out.size; ++i) {
out.data[i] = Math::pow(x.data[i], p.data[i]);
}
};
t.range_analysis_func = [](RangeAnalysisContext &ctx) {
const Interval x = ctx.get_input(0);
const Interval y = ctx.get_input(1);
ctx.set_output(0, pow(x, y));
};
}
for (unsigned int i = 0; i < _types.size(); ++i) {
NodeType &t = _types[i];
@ -1810,6 +1885,7 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() {
break;
case Variant::OBJECT:
case Variant::STRING:
break;
default:

View File

@ -46,7 +46,10 @@ public:
struct NodeType {
String name;
// Debug-only nodes are ignored in non-debug compilation.
bool debug_only = false;
// Pseudo nodes are replaced during compilation with one or multiple real nodes, they have no logic on their own
bool is_pseudo_node = false;
Category category;
std::vector<Port> inputs;
std::vector<Port> outputs;
@ -60,6 +63,7 @@ public:
VoxelGraphNodeDB();
// TODO Return a reference, it should never be null or should crash
static VoxelGraphNodeDB *get_singleton();
static void create_singleton();
static void destroy_singleton();

View File

@ -1,4 +1,5 @@
#include "voxel_graph_runtime.h"
#include "../../util/expression_parser.h"
#include "../../util/funcs.h"
#include "../../util/macros.h"
#include "../../util/profiling.h"
@ -25,15 +26,297 @@ void VoxelGraphRuntime::clear() {
_program.clear();
}
VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::compile(const ProgramGraph &graph, bool debug) {
struct ToConnect {
std::string_view var_name;
ProgramGraph::PortLocation dst;
};
static uint32_t expand_node(ProgramGraph &graph, const ExpressionParser::Node &ep_node, const VoxelGraphNodeDB &db,
std::vector<ToConnect> &to_connect, std::vector<uint32_t> &expanded_node_ids);
static bool expand_input(ProgramGraph &graph, const ExpressionParser::Node &arg, ProgramGraph::Node &pg_node,
uint32_t pg_node_input_index, const VoxelGraphNodeDB &db, std::vector<ToConnect> &to_connect,
std::vector<uint32_t> &expanded_node_ids) {
switch (arg.type) {
case ExpressionParser::Node::NUMBER: {
const ExpressionParser::NumberNode &arg_nn = reinterpret_cast<const ExpressionParser::NumberNode &>(arg);
pg_node.default_inputs[pg_node_input_index] = arg_nn.value;
} break;
case ExpressionParser::Node::VARIABLE: {
const ExpressionParser::VariableNode &arg_vn =
reinterpret_cast<const ExpressionParser::VariableNode &>(arg);
to_connect.push_back({ arg_vn.name, { pg_node.id, pg_node_input_index } });
} break;
case ExpressionParser::Node::OPERATOR:
case ExpressionParser::Node::FUNCTION: {
const uint32_t dependency_pg_node_id = expand_node(graph, arg, db, to_connect, expanded_node_ids);
ERR_FAIL_COND_V(dependency_pg_node_id == ProgramGraph::NULL_ID, false);
graph.connect({ dependency_pg_node_id, 0 }, { pg_node.id, pg_node_input_index });
} break;
default:
return false;
}
return true;
}
static ProgramGraph::Node &create_node(
ProgramGraph &graph, const VoxelGraphNodeDB &db, VoxelGeneratorGraph::NodeTypeID node_type_id) {
ProgramGraph::Node *node = create_node_internal(graph, node_type_id, Vector2(), ProgramGraph::NULL_ID);
CRASH_COND(node == nullptr);
return *node;
}
static uint32_t expand_node(ProgramGraph &graph, const ExpressionParser::Node &ep_node, const VoxelGraphNodeDB &db,
std::vector<ToConnect> &to_connect, std::vector<uint32_t> &expanded_node_ids) {
switch (ep_node.type) {
case ExpressionParser::Node::NUMBER: {
// Note, this code should only run if the whole expression is only a number.
// Constant node inputs don't create a constant node, they just set the default value of the input.
ProgramGraph::Node &pg_node = create_node(graph, db, VoxelGeneratorGraph::NODE_CONSTANT);
const ExpressionParser::NumberNode &nn = reinterpret_cast<const ExpressionParser::NumberNode &>(ep_node);
CRASH_COND(pg_node.params.size() != 1);
pg_node.params[0] = nn.value;
expanded_node_ids.push_back(pg_node.id);
return pg_node.id;
}
case ExpressionParser::Node::VARIABLE: {
// Note, this code should only run if the whole expression is only a variable.
// Variable node inputs don't create a node each time, they are turned into connections in a later pass.
// Here we need a pass-through node, so let's use `var + 0`. It's not a common case anyways.
ProgramGraph::Node &pg_node = create_node(graph, db, VoxelGeneratorGraph::NODE_ADD);
const ExpressionParser::VariableNode &vn =
reinterpret_cast<const ExpressionParser::VariableNode &>(ep_node);
to_connect.push_back({ vn.name, { pg_node.id, 0 } });
CRASH_COND(pg_node.default_inputs.size() != 2);
pg_node.default_inputs[1] = 0;
expanded_node_ids.push_back(pg_node.id);
return pg_node.id;
}
case ExpressionParser::Node::OPERATOR: {
const ExpressionParser::OperatorNode &on =
reinterpret_cast<const ExpressionParser::OperatorNode &>(ep_node);
VoxelGeneratorGraph::NodeTypeID node_type_id;
switch (on.op) {
case ExpressionParser::OperatorNode::ADD:
node_type_id = VoxelGeneratorGraph::NODE_ADD;
break;
case ExpressionParser::OperatorNode::SUBTRACT:
node_type_id = VoxelGeneratorGraph::NODE_SUBTRACT;
break;
case ExpressionParser::OperatorNode::MULTIPLY:
node_type_id = VoxelGeneratorGraph::NODE_MULTIPLY;
break;
case ExpressionParser::OperatorNode::DIVIDE:
node_type_id = VoxelGeneratorGraph::NODE_DIVIDE;
break;
case ExpressionParser::OperatorNode::POWER:
// TODO Optimize: if exponent is constant we can use POWI, SQRT, DIVIDE or MULTIPLY
node_type_id = VoxelGeneratorGraph::NODE_POW;
break;
default:
CRASH_NOW();
break;
}
ProgramGraph::Node &pg_node = create_node(graph, db, node_type_id);
expanded_node_ids.push_back(pg_node.id);
CRASH_COND(on.n0 == nullptr);
ERR_FAIL_COND_V(
!expand_input(graph, *on.n0, pg_node, 0, db, to_connect, expanded_node_ids), ProgramGraph::NULL_ID);
CRASH_COND(on.n1 == nullptr);
ERR_FAIL_COND_V(
!expand_input(graph, *on.n1, pg_node, 1, db, to_connect, expanded_node_ids), ProgramGraph::NULL_ID);
return pg_node.id;
}
case ExpressionParser::Node::FUNCTION: {
// TODO Functions support
// const ExpressionParser::FunctionNode &fn =
// reinterpret_cast<const ExpressionParser::FunctionNode &>(ep_node);
// const ExpressionParser::Function *f = ExpressionParser::find_function_by_id(fn.function_id, functions);
// CRASH_COND(f == nullptr);
// const unsigned int arg_count = f->argument_count;
// for (unsigned int arg_index = 0; arg_index < arg_count; ++arg_index) {
// //...
// }
return ProgramGraph::NULL_ID;
}
default:
return ProgramGraph::NULL_ID;
}
}
static VoxelGraphRuntime::CompilationResult expand_expression_node(ProgramGraph &graph, uint32_t original_node_id,
ProgramGraph::PortLocation &expanded_output_port, std::vector<uint32_t> &expanded_nodes) {
VOXEL_PROFILE_SCOPE();
const ProgramGraph::Node *original_node = graph.get_node(original_node_id);
CRASH_COND(original_node->params.size() == 0);
const String code = original_node->params[0];
const CharString code_utf8 = code.utf8();
// TODO Have functions
Span<const ExpressionParser::Function> functions;
// Extract the AST, so we can convert it into graph nodes,
// and benefit from all features of range analysis and buffer processing
ExpressionParser::Result parse_result = ExpressionParser::parse(code_utf8.get_data(), functions);
if (parse_result.error.id != ExpressionParser::ERROR_NONE) {
if (parse_result.root != nullptr) {
memdelete(parse_result.root);
}
const std::string error_message_utf8 = ExpressionParser::to_string(parse_result.error);
VoxelGraphRuntime::CompilationResult result;
result.success = false;
result.node_id = original_node_id;
result.message = String(error_message_utf8.c_str());
return result;
}
if (parse_result.root == nullptr) {
VoxelGraphRuntime::CompilationResult result;
result.success = false;
result.node_id = original_node_id;
result.message = "Expression is empty";
return result;
}
std::vector<ToConnect> to_connect;
const uint32_t expanded_root_node_id =
expand_node(graph, *parse_result.root, *VoxelGraphNodeDB::get_singleton(), to_connect, expanded_nodes);
if (expanded_root_node_id == ProgramGraph::NULL_ID) {
if (parse_result.root != nullptr) {
memdelete(parse_result.root);
}
VoxelGraphRuntime::CompilationResult result;
result.success = false;
result.node_id = original_node_id;
result.message = "Internal error";
return result;
}
expanded_output_port = { expanded_root_node_id, 0 };
for (unsigned int i = 0; i < to_connect.size(); ++i) {
const ToConnect tc = to_connect[i];
unsigned int original_port_index;
if (!original_node->find_input_port_by_name(tc.var_name, original_port_index)) {
if (parse_result.root != nullptr) {
memdelete(parse_result.root);
}
VoxelGraphRuntime::CompilationResult result;
result.success = false;
result.node_id = original_node_id;
result.message = "Could not resolve expression variable from input ports";
return result;
}
const ProgramGraph::Port &original_port = original_node->inputs[original_port_index];
for (unsigned int j = 0; j < original_port.connections.size(); ++j) {
const ProgramGraph::PortLocation src = original_port.connections[j];
graph.connect(src, tc.dst);
}
}
graph.remove_node(original_node_id);
if (parse_result.root != nullptr) {
memdelete(parse_result.root);
}
VoxelGraphRuntime::CompilationResult result;
result.success = true;
return result;
}
struct PortRemap {
ProgramGraph::PortLocation original;
ProgramGraph::PortLocation expanded;
};
struct ExpandedNodeRemap {
uint32_t expanded_node_id;
uint32_t original_node_id;
};
static VoxelGraphRuntime::CompilationResult expand_expression_nodes(ProgramGraph &graph,
std::vector<PortRemap> &user_to_expanded_ports, std::vector<ExpandedNodeRemap> &expanded_to_user_node_ids) {
VOXEL_PROFILE_SCOPE();
// Gather expression node IDs first, as expansion could invalidate the iterator
std::vector<uint32_t> expression_node_ids;
graph.for_each_node([&expression_node_ids](ProgramGraph::Node &node) {
if (node.type_id == VoxelGeneratorGraph::NODE_EXPRESSION) {
expression_node_ids.push_back(node.id);
}
});
std::vector<uint32_t> expanded_node_ids;
for (auto it = expression_node_ids.begin(); it != expression_node_ids.end(); ++it) {
const uint32_t node_id = *it;
ProgramGraph::PortLocation expanded_output_port;
expanded_node_ids.clear();
VoxelGraphRuntime::CompilationResult result =
expand_expression_node(graph, node_id, expanded_output_port, expanded_node_ids);
if (!result.success) {
return result;
}
user_to_expanded_ports.push_back({ { node_id, 0 }, expanded_output_port });
for (auto it2 = expanded_node_ids.begin(); it2 != expanded_node_ids.end(); ++it2) {
expanded_to_user_node_ids.push_back({ *it2, node_id });
}
}
VoxelGraphRuntime::CompilationResult result;
result.success = true;
return result;
}
VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::compile(const ProgramGraph &p_graph, bool debug) {
VOXEL_PROFILE_SCOPE();
ProgramGraph graph;
graph.copy_from(p_graph, false);
// TODO Store a remapping to allow debugging with the expanded graph
std::vector<PortRemap> user_to_expanded_ports;
std::vector<ExpandedNodeRemap> expanded_to_user_node_ids;
VoxelGraphRuntime::CompilationResult expand_result =
expand_expression_nodes(graph, user_to_expanded_ports, expanded_to_user_node_ids);
if (!expand_result.success) {
return expand_result;
}
VoxelGraphRuntime::CompilationResult result = _compile(graph, debug);
if (!result.success) {
clear();
}
for (PortRemap r : user_to_expanded_ports) {
_program.user_port_to_expanded_port.insert({ r.original, r.expanded });
}
for (ExpandedNodeRemap r : expanded_to_user_node_ids) {
_program.expanded_node_id_to_user_node_id.insert({ r.expanded_node_id, r.original_node_id });
}
return result;
}
VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGraph &graph, bool debug) {
VOXEL_PROFILE_SCOPE();
clear();
std::vector<uint32_t> order;
@ -498,11 +781,25 @@ void VoxelGraphRuntime::generate_optimized_execution_map(
}
if (debug) {
std::vector<uint32_t> &debug_nodes = execution_map.debug_nodes;
for (unsigned int node_index = 0; node_index < graph.nodes.size(); ++node_index) {
const ProcessResult res = results[node_index];
const DependencyGraph::Node &node = graph.nodes[node_index];
if (res == REQUIRED) {
execution_map.debug_nodes.push_back(node.debug_node_id);
uint32_t debug_node_id = node.debug_node_id;
auto it = _program.expanded_node_id_to_user_node_id.find(debug_node_id);
if (it != _program.expanded_node_id_to_user_node_id.end()) {
debug_node_id = it->second;
if (std::find(debug_nodes.begin(), debug_nodes.end(), debug_node_id) != debug_nodes.end()) {
// Ignore duplicates. Some nodes can have been expanded into multiple ones.
continue;
}
}
debug_nodes.push_back(node.debug_node_id);
}
}
}
@ -857,6 +1154,10 @@ void VoxelGraphRuntime::analyze_range(State &state, Vector3i min_pos, Vector3i m
}
bool VoxelGraphRuntime::try_get_output_port_address(ProgramGraph::PortLocation port, uint16_t &out_address) const {
auto it = _program.user_port_to_expanded_port.find(port);
if (it != _program.user_port_to_expanded_port.end()) {
port = it->second;
}
const uint16_t *aptr = _program.output_port_addresses.getptr(port);
if (aptr == nullptr) {
// This port did not take part of the compiled result

View File

@ -51,7 +51,7 @@ public:
// TODO Typo?
std::vector<uint16_t> operation_adresses;
// Stores node IDs referring to the user-facing graph
std::vector<int> debug_nodes;
std::vector<uint32_t> debug_nodes;
// From which index in the adress list operations will start depending on Y
unsigned int xzy_start_index = 0;
@ -116,7 +116,7 @@ public:
~VoxelGraphRuntime();
void clear();
CompilationResult compile(const ProgramGraph &graph, bool debug);
CompilationResult compile(const ProgramGraph &p_graph, bool debug);
// Call this before you use a state with generation functions.
// You need to call it once, until you want to use a different graph, buffer size or buffer count.
@ -372,7 +372,8 @@ private:
uint16_t end_dependency;
uint16_t op_address;
bool is_input;
int debug_node_id;
// Node ID from the expanded ProgramGraph (non user-provided, so may need remap)
uint32_t debug_node_id;
};
// Indexes to the `nodes` array
@ -448,9 +449,16 @@ private:
// Buffers are needed to hold values of arguments and outputs for each operation.
unsigned int buffer_count = 0;
// Associates a high-level port to its corresponding address within the compiled program.
// Associates a port from the input graph to its corresponding address within the compiled program.
// This is used for debugging intermediate values.
HashMap<ProgramGraph::PortLocation, uint16_t, ProgramGraph::PortLocationHasher> output_port_addresses;
HashMap<ProgramGraph::PortLocation, uint16_t, ProgramGraphPortLocationHasher> output_port_addresses;
// If you have a port location from the original user graph, before querying `output_port_addresses`, remap
// it first, in case it got expanded to different nodes during compilation.
std::unordered_map<ProgramGraph::PortLocation, ProgramGraph::PortLocation> user_port_to_expanded_port;
// Associates expanded graph ID to user graph node IDs.
std::unordered_map<uint32_t, uint32_t> expanded_node_id_to_user_node_id;
// Result of the last compilation attempt. The program should not be run if it failed.
CompilationResult compilation_result;
@ -461,6 +469,8 @@ private:
xzy_start_op_address = 0;
default_execution_map.clear();
output_port_addresses.clear();
user_port_to_expanded_port.clear();
expanded_node_id_to_user_node_id.clear();
dependency_graph.clear();
x_input_address = -1;
y_input_address = -1;

View File

@ -1,5 +1,6 @@
#include "tests.h"
#include "../edition/voxel_tool_terrain.h"
#include "../generators/graph/expression_parser.h"
#include "../generators/graph/range_utility.h"
#include "../generators/graph/voxel_generator_graph.h"
#include "../meshers/blocky/voxel_blocky_library.h"
@ -349,6 +350,33 @@ void test_voxel_graph_generator_default_graph_compilation() {
result.success, String("Failed to compile graph: {0}: {1}").format(varray(result.node_id, result.message)));
}
void test_voxel_graph_generator_expressions() {
Ref<VoxelGeneratorGraph> generator;
generator.instantiate();
const uint32_t in_x = generator->create_node(VoxelGeneratorGraph::NODE_INPUT_X, Vector2(0, 0));
const uint32_t in_y = generator->create_node(VoxelGeneratorGraph::NODE_INPUT_Y, Vector2(0, 0));
const uint32_t in_z = generator->create_node(VoxelGeneratorGraph::NODE_INPUT_Z, Vector2(0, 0));
const uint32_t out_sdf = generator->create_node(VoxelGeneratorGraph::NODE_OUTPUT_SDF, Vector2(0, 0));
const uint32_t n_expression = generator->create_node(VoxelGeneratorGraph::NODE_EXPRESSION, Vector2());
generator->set_node_param(n_expression, 0, "0.1 * x + 0.2 * z + y");
PackedStringArray var_names;
var_names.push_back("x");
var_names.push_back("y");
var_names.push_back("z");
generator->set_expression_node_inputs(n_expression, var_names);
generator->add_connection(in_x, 0, n_expression, 0);
generator->add_connection(in_y, 0, n_expression, 1);
generator->add_connection(in_z, 0, n_expression, 2);
generator->add_connection(n_expression, 0, out_sdf, 0);
VoxelGraphRuntime::CompilationResult result = generator->compile();
ZYLANN_TEST_ASSERT_MSG(
result.success, String("Failed to compile graph: {0}: {1}").format(varray(result.node_id, result.message)));
}
void test_voxel_graph_generator_texturing() {
Ref<VoxelGeneratorGraph> generator;
generator.instantiate();
@ -1462,6 +1490,178 @@ void test_flat_map() {
}
}
void test_expression_parser() {
using namespace ExpressionParser;
{
Result result = parse("", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse(" ", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("42", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root != nullptr);
ZYLANN_TEST_ASSERT(result.root->type == Node::NUMBER);
const NumberNode *nn = reinterpret_cast<NumberNode *>(result.root);
ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 42.f));
memdelete(result.root);
}
{
Result result = parse("()", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("((()))", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("(42)", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root != nullptr);
ZYLANN_TEST_ASSERT(result.root->type == Node::NUMBER);
const NumberNode *nn = reinterpret_cast<NumberNode *>(result.root);
ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 42.f));
memdelete(result.root);
}
{
Result result = parse("(", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_UNCLOSED_PARENTHESIS);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("(666", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_UNCLOSED_PARENTHESIS);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("1+", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_MISSING_OPERAND_ARGUMENTS);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("++", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_MISSING_OPERAND_ARGUMENTS);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("1 2 3", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_MULTIPLE_OPERANDS);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("???", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_INVALID_TOKEN);
ZYLANN_TEST_ASSERT(result.root == nullptr);
}
{
Result result = parse("1+2-3*4/5", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root != nullptr);
ZYLANN_TEST_ASSERT(result.root->type == Node::NUMBER);
const NumberNode *nn = reinterpret_cast<NumberNode *>(result.root);
ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 0.6f));
memdelete(result.root);
}
{
Result result = parse("1*2-3/4+5", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root != nullptr);
ZYLANN_TEST_ASSERT(result.root->type == Node::NUMBER);
const NumberNode *nn = reinterpret_cast<NumberNode *>(result.root);
ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 6.25f));
memdelete(result.root);
}
{
Result result = parse("(5 - 3)^2 + 2.5/(4 + 6)", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root != nullptr);
ZYLANN_TEST_ASSERT(result.root->type == Node::NUMBER);
const NumberNode *nn = reinterpret_cast<NumberNode *>(result.root);
ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 4.25f));
memdelete(result.root);
}
{
/*
-
/ \
/ \
/ \
* -
/ \ / \
4 ^ c d
/ \
+ 2
/ \
a b
*/
VariableNode *node_a = memnew(VariableNode("a"));
VariableNode *node_b = memnew(VariableNode("b"));
OperatorNode *node_add = memnew(OperatorNode(OperatorNode::ADD, node_a, node_b));
NumberNode *node_two = memnew(NumberNode(2));
OperatorNode *node_power = memnew(OperatorNode(OperatorNode::POWER, node_add, node_two));
NumberNode *node_four = memnew(NumberNode(4));
OperatorNode *node_mul = memnew(OperatorNode(OperatorNode::MULTIPLY, node_four, node_power));
VariableNode *node_c = memnew(VariableNode("c"));
VariableNode *node_d = memnew(VariableNode("d"));
OperatorNode *node_sub = memnew(OperatorNode(OperatorNode::SUBTRACT, node_c, node_d));
OperatorNode *expected_root = memnew(OperatorNode(OperatorNode::SUBTRACT, node_mul, node_sub));
Result result = parse("4*(a+b)^2-(c-d)", Span<const Function>());
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root != nullptr);
// {
// const std::string s1 = tree_to_string(*expected_root, Span<const Function>());
// print_line(String(s1.c_str()));
// print_line("---");
// const std::string s2 = tree_to_string(*result.root, Span<const Function>());
// print_line(String(s2.c_str()));
// }
ZYLANN_TEST_ASSERT(is_tree_equal(*result.root, *expected_root, Span<const Function>()));
memdelete(result.root);
memdelete(expected_root);
}
{
FixedArray<Function, 2> functions;
{
Function f;
f.name = "sqrt";
f.id = 0;
f.argument_count = 1;
f.func = [](Span<const float> args) { //
return Math::sqrt(args[0]);
};
functions[0] = f;
}
{
Function f;
f.name = "clamp";
f.id = 1;
f.argument_count = 3;
f.func = [](Span<const float> args) { //
return math::clamp(args[0], args[1], args[2]);
};
functions[1] = f;
}
Result result = parse("clamp(sqrt(20 + sqrt(25)), 1, 2.0 * 2.0)", to_span_const(functions));
ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE);
ZYLANN_TEST_ASSERT(result.root != nullptr);
ZYLANN_TEST_ASSERT(result.root->type == Node::NUMBER);
const NumberNode *nn = reinterpret_cast<NumberNode *>(result.root);
ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 4.f));
memdelete(result.root);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#define VOXEL_TEST(fname) \
@ -1479,6 +1679,7 @@ void run_voxel_tests() {
VOXEL_TEST(test_encode_weights_packed_u16);
VOXEL_TEST(test_copy_3d_region_zxy);
VOXEL_TEST(test_voxel_graph_generator_default_graph_compilation);
VOXEL_TEST(test_voxel_graph_generator_expressions);
VOXEL_TEST(test_voxel_graph_generator_texturing);
VOXEL_TEST(test_island_finder);
VOXEL_TEST(test_unordered_remove_if);
@ -1497,6 +1698,7 @@ void run_voxel_tests() {
#endif
VOXEL_TEST(test_run_blocky_random_tick);
VOXEL_TEST(test_flat_map);
VOXEL_TEST(test_expression_parser);
print_line("------------ Voxel tests end -------------");
}

829
util/expression_parser.cpp Normal file
View File

@ -0,0 +1,829 @@
#include "expression_parser.h"
#include <core/math/math_funcs.h>
#include <core/os/memory.h>
#include <sstream>
#include <vector>
namespace zylann {
namespace ExpressionParser {
OperatorNode::~OperatorNode() {
if (n0 != nullptr) {
memdelete(n0);
}
if (n1 != nullptr) {
memdelete(n1);
}
}
FunctionNode::~FunctionNode() {
for (unsigned int i = 0; i < args.size(); ++i) {
if (args[i] != nullptr) {
memdelete(args[i]);
}
}
}
struct StringView {
const char *ptr;
size_t size;
};
struct Token {
enum Type { //
NUMBER,
NAME,
PLUS,
MINUS,
DIVIDE,
MULTIPLY,
PARENTHESIS_OPEN,
PARENTHESIS_CLOSE,
POWER,
COMMA,
COUNT,
INVALID
};
Type type;
union Data {
// Can't put a std::string_view inside a union, not sure why
StringView str;
float number;
} data;
};
StringView pack(std::string_view text) {
return StringView{ text.data(), text.size() };
}
std::string_view unpack(StringView sv) {
return std::string_view(sv.ptr, sv.size);
}
bool is_name_starter(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
bool is_digit(char c) {
return (c >= '0' && c <= '9');
}
bool is_name_char(char c) {
return is_name_starter(c) || is_digit(c);
}
std::string_view get_name(const std::string_view text, unsigned int &pos) {
unsigned int begin_pos = pos;
while (pos < text.size()) {
const char c = text[pos];
if (!is_name_char(c)) {
return text.substr(begin_pos, pos - begin_pos);
}
++pos;
}
return text.substr(begin_pos);
}
bool get_number_token(const std::string_view text, unsigned int &pos, Token &out_token, bool negative) {
const unsigned int begin_pos = pos;
// Integer part
int64_t n = 0;
char c;
while (pos < text.size()) {
c = text[pos];
if (!is_digit(c)) {
break;
}
n = n * 10 + (c - '0');
++pos;
}
if (negative) {
n = -n;
}
// Decimal part
double f;
bool is_float = false;
if (c == '.') {
++pos;
int64_t d = 0;
int64_t p = 1;
while (pos < text.size()) {
c = text[pos];
if (!is_digit(c)) {
break;
}
d = d * 10 + (c - '0');
p *= 10;
++pos;
}
f = n + double(d) / double(p);
if (negative) {
f = -f;
}
is_float = true;
}
if (!is_name_starter(c)) {
if (is_float) {
out_token.type = Token::NUMBER;
out_token.data.number = f;
} else {
out_token.type = Token::NUMBER;
out_token.data.number = n;
}
return true;
}
return false;
}
class Tokenizer {
public:
Tokenizer(std::string_view text) : _text(text), _error(ERROR_NONE), _position(0) {}
bool get_next(Token &out_token) {
struct CharToken {
const char character;
Token::Type type;
};
static const CharToken s_char_tokens[] = {
{ '(', Token::PARENTHESIS_OPEN }, //
{ ')', Token::PARENTHESIS_CLOSE }, //
{ ',', Token::COMMA }, //
{ '+', Token::PLUS }, //
{ '-', Token::MINUS }, //
{ '*', Token::MULTIPLY }, //
{ '/', Token::DIVIDE }, //
{ '^', Token::POWER }, //
{ 0, Token::INVALID } //
};
while (_position < _text.size()) {
const char c = _text[_position];
if (c == ' ' || c == '\t') {
++_position;
continue;
}
{
const CharToken *ct = s_char_tokens;
while (ct->character != 0) {
if (ct->character == c) {
Token token;
token.type = ct->type;
out_token = token;
++_position;
return true;
}
++ct;
}
}
if (is_name_starter(c)) {
Token token;
token.type = Token::NAME;
token.data.str = pack(get_name(_text, _position));
CRASH_COND(token.data.str.size == 0);
out_token = token;
return true;
}
// TODO Unary operator `-`
/*if (c == '-') {
++_position;
if (_position >= _text.size()) {
_error = ERROR_UNEXPECTED_END;
return false;
}
if (!get_number_token(_text, _position, out_token, true)) {
_error = ERROR_INVALID_NUMBER;
return false;
}
return true;
}*/
if (is_digit(c)) {
if (!get_number_token(_text, _position, out_token, false)) {
_error = ERROR_INVALID_NUMBER;
return false;
}
return true;
}
_error = ERROR_INVALID_TOKEN;
return false;
}
return false;
}
ErrorID get_error() const {
return _error;
}
unsigned int get_position() const {
return _position;
}
private:
std::string_view _text;
ErrorID _error;
unsigned int _position;
};
bool as_operator(Token::Type token_type, OperatorNode::Operation &op) {
switch (token_type) {
case Token::PLUS:
op = OperatorNode::ADD;
return true;
case Token::MULTIPLY:
op = OperatorNode::MULTIPLY;
return true;
case Token::MINUS:
op = OperatorNode::SUBTRACT;
return true;
case Token::DIVIDE:
op = OperatorNode::DIVIDE;
return true;
case Token::POWER:
op = OperatorNode::POWER;
return true;
default:
return false;
}
}
const int MAX_PRECEDENCE = 100;
int get_operator_precedence(OperatorNode::Operation op) {
switch (op) {
case OperatorNode::ADD:
case OperatorNode::SUBTRACT:
return 1;
case OperatorNode::MULTIPLY:
case OperatorNode::DIVIDE:
return 2;
case OperatorNode::POWER:
return 3;
default:
CRASH_NOW();
return 0;
}
}
struct OpEntry {
int precedence;
// TODO Use unique ptr
OperatorNode *node;
};
template <typename T>
inline T pop(std::vector<T> &stack) {
CRASH_COND(stack.size() == 0);
T t = stack.back();
stack.pop_back();
return t;
}
unsigned int get_operator_argument_count(OperatorNode::Operation op_type) {
switch (op_type) {
case OperatorNode::ADD:
case OperatorNode::SUBTRACT:
case OperatorNode::MULTIPLY:
case OperatorNode::DIVIDE:
case OperatorNode::POWER:
return 2;
default:
CRASH_NOW();
return 0;
}
}
ErrorID pop_expression_operator(std::vector<OpEntry> &operations_stack, std::vector<Node *> &operand_stack) {
OpEntry last_op = pop(operations_stack);
CRASH_COND(last_op.node == nullptr);
CRASH_COND(last_op.node->type != Node::OPERATOR);
OperatorNode *last_node = last_op.node;
const unsigned int argc = get_operator_argument_count(last_node->op);
CRASH_COND(argc < 1 || argc > 2);
if (operand_stack.size() < argc) {
return ERROR_MISSING_OPERAND_ARGUMENTS;
}
if (argc == 1) {
last_node->n0 = pop(operand_stack);
} else {
Node *right = pop(operand_stack);
Node *left = pop(operand_stack);
last_node->n0 = left;
last_node->n1 = right;
}
// Push result back to stack
operand_stack.push_back(last_node);
return ERROR_NONE;
}
bool is_operand(const Token &token) {
return token.type == Token::NAME || token.type == Token::NUMBER;
}
Node *operand_to_node(const Token token) {
switch (token.type) {
case Token::NUMBER: {
NumberNode *node = memnew(NumberNode(token.data.number));
return node;
}
case Token::NAME: {
VariableNode *node = memnew(VariableNode(unpack(token.data.str)));
return node;
}
default:
CRASH_NOW_MSG("Token not handled");
return nullptr;
}
}
const Function *find_function_by_name(std::string_view name, Span<const Function> functions) {
for (unsigned int i = 0; i < functions.size(); ++i) {
const Function &f = functions[i];
if (f.name == name) {
return &f;
}
}
return nullptr;
}
Result parse_expression(Tokenizer &tokenizer, bool in_argument_list, Span<const Function> functions);
Result parse_function(Tokenizer &tokenizer, std::vector<Node *> &operand_stack, Span<const Function> functions) {
std::string_view fname;
{
// We'll replace the variable with a function call node
Node *top = operand_stack.back();
CRASH_COND(top->type != Node::VARIABLE);
VariableNode *node = reinterpret_cast<VariableNode *>(top);
fname = node->name;
memdelete(node);
operand_stack.pop_back();
}
const Function *fn = find_function_by_name(fname, functions);
if (fn == nullptr) {
Result result;
result.error.id = ERROR_UNKNOWN_FUNCTION;
result.error.position = tokenizer.get_position();
result.error.symbol = fname;
return result;
}
FunctionNode *fnode = memnew(FunctionNode);
fnode->function_id = fn->id;
CRASH_COND(fn->argument_count >= fnode->args.size());
for (unsigned int arg_index = 0; arg_index < fn->argument_count; ++arg_index) {
Result arg_result = parse_expression(tokenizer, true, functions);
if (arg_result.error.id != ERROR_NONE) {
memdelete(fnode);
return arg_result;
}
fnode->args[arg_index] = arg_result.root;
}
Result result;
result.root = fnode;
operand_stack.push_back(fnode);
return result;
}
void free_nodes(std::vector<OpEntry> &operations_stack, std::vector<Node *> operand_stack) {
for (unsigned int i = 0; i < operations_stack.size(); ++i) {
memdelete(operations_stack[i].node);
}
for (unsigned int i = 0; i < operand_stack.size(); ++i) {
memdelete(operand_stack[i]);
}
}
Result parse_expression(Tokenizer &tokenizer, bool in_argument_list, Span<const Function> functions) {
Token token;
std::vector<OpEntry> operations_stack;
// TODO Use unique ptr
std::vector<Node *> operand_stack;
int precedence_base = 0;
bool previous_was_operand = false;
while (tokenizer.get_next(token)) {
if (in_argument_list && token.type == Token::COMMA) {
break;
}
bool current_is_operand = false;
OperatorNode::Operation op_type;
if (as_operator(token.type, op_type)) {
OpEntry op;
op.precedence = precedence_base + get_operator_precedence(op_type);
// Operands will be assigned when we pop operations from the stack
op.node = memnew(OperatorNode(op_type, nullptr, nullptr));
while (operations_stack.size() > 0) {
const OpEntry last_op = operations_stack.back();
// While the current operator has lower precedence, pop last operand
if (op.precedence <= last_op.precedence) {
const ErrorID err = pop_expression_operator(operations_stack, operand_stack);
if (err != ERROR_NONE) {
free_nodes(operations_stack, operand_stack);
Result result;
result.error.id = err;
result.error.position = tokenizer.get_position();
return result;
}
} else {
break;
}
}
operations_stack.push_back(op);
} else if (is_operand(token)) {
if (previous_was_operand) {
free_nodes(operations_stack, operand_stack);
Result result;
result.error.id = ERROR_MULTIPLE_OPERANDS;
result.error.position = tokenizer.get_position();
return result;
}
operand_stack.push_back(operand_to_node(token));
current_is_operand = true;
} else if (token.type == Token::PARENTHESIS_OPEN) {
if (operand_stack.size() > 0 && operand_stack.back()->type == Node::VARIABLE) {
Result fn_result = parse_function(tokenizer, operand_stack, functions);
if (fn_result.error.id != ERROR_NONE) {
free_nodes(operations_stack, operand_stack);
return fn_result;
}
} else {
// Increase precedence for what will go inside parenthesis
precedence_base += MAX_PRECEDENCE;
}
} else if (token.type == Token::PARENTHESIS_CLOSE) {
if (in_argument_list && precedence_base < MAX_PRECEDENCE) {
break;
}
precedence_base -= MAX_PRECEDENCE;
CRASH_COND(precedence_base < 0);
} else {
free_nodes(operations_stack, operand_stack);
Result result;
result.error.id = ERROR_UNEXPECTED_TOKEN;
result.error.position = tokenizer.get_position();
return result;
}
previous_was_operand = current_is_operand;
}
Result result;
result.error.id = tokenizer.get_error();
if (result.error.id != ERROR_NONE) {
free_nodes(operations_stack, operand_stack);
result.error.position = tokenizer.get_position();
return result;
}
if (precedence_base != 0) {
free_nodes(operations_stack, operand_stack);
result.error.id = ERROR_UNCLOSED_PARENTHESIS;
result.error.position = tokenizer.get_position();
return result;
}
// All remaining operations should end up with ascending precedence,
// so popping them should be correct
// Note: will not work correctly if precedences are equal
while (operations_stack.size() > 0) {
const ErrorID err = pop_expression_operator(operations_stack, operand_stack);
if (err != ERROR_NONE) {
free_nodes(operations_stack, operand_stack);
Result result;
result.error.id = err;
result.error.position = tokenizer.get_position();
return result;
}
}
CRASH_COND(operand_stack.size() > 1);
// The stack can be empty if the expression was empty
if (operand_stack.size() > 0) {
result.root = operand_stack.back();
}
return result;
}
void find_variables(const Node &node, std::vector<std::string_view> &variables) {
switch (node.type) {
case Node::NUMBER:
break;
case Node::VARIABLE: {
const VariableNode &vnode = reinterpret_cast<const VariableNode &>(node);
// A variable can appear multiple times, only get it once
if (std::find(variables.begin(), variables.end(), vnode.name) == variables.end()) {
variables.push_back(vnode.name);
}
} break;
case Node::OPERATOR: {
const OperatorNode &onode = reinterpret_cast<const OperatorNode &>(node);
if (onode.n0 != nullptr) {
find_variables(*onode.n0, variables);
}
if (onode.n1 != nullptr) {
find_variables(*onode.n1, variables);
}
} break;
case Node::FUNCTION: {
const FunctionNode &fnode = reinterpret_cast<const FunctionNode &>(node);
for (unsigned int i = 0; i < fnode.args.size(); ++i) {
if (fnode.args[i] != nullptr) {
find_variables(*fnode.args[i], variables);
}
}
} break;
default:
CRASH_NOW();
}
}
// Returns true if the passed node is constant (or gets changed into a constant).
// `out_number` is the value of the node if it is constant.
bool precompute_constants(Node *&node, float &out_number, Span<const Function> functions) {
CRASH_COND(node == nullptr);
switch (node->type) {
case Node::NUMBER: {
const NumberNode *nn = reinterpret_cast<NumberNode *>(node);
out_number = nn->value;
return true;
}
case Node::VARIABLE:
return false;
case Node::OPERATOR: {
OperatorNode *onode = reinterpret_cast<OperatorNode *>(node);
if (onode->n0 != nullptr && onode->n1 != nullptr) {
float n0;
float n1;
const bool constant0 = precompute_constants(onode->n0, n0, functions);
const bool constant1 = precompute_constants(onode->n1, n1, functions);
if (constant0 && constant1) {
switch (onode->op) {
case OperatorNode::ADD:
out_number = n0 + n1;
break;
case OperatorNode::SUBTRACT:
out_number = n0 - n1;
break;
case OperatorNode::MULTIPLY:
out_number = n0 * n1;
break;
case OperatorNode::DIVIDE:
out_number = n0 / n1;
break;
case OperatorNode::POWER:
out_number = powf(n0, n1);
break;
default:
CRASH_NOW();
}
memdelete(node);
node = memnew(NumberNode(out_number));
return true;
}
// TODO Unary operators
}
return false;
} break;
case Node::FUNCTION: {
FunctionNode *fnode = reinterpret_cast<FunctionNode *>(node);
bool all_constant = true;
FixedArray<float, 4> constant_args;
const Function *f = find_function_by_id(fnode->function_id, functions);
for (unsigned int i = 0; i < f->argument_count; ++i) {
if (!precompute_constants(fnode->args[i], constant_args[i], functions)) {
all_constant = false;
}
}
if (all_constant) {
CRASH_COND(f == nullptr);
CRASH_COND(f->func == nullptr);
out_number = f->func(to_span_const(constant_args, f->argument_count));
memdelete(node);
node = memnew(NumberNode(out_number));
return true;
}
return false;
} break;
default:
CRASH_NOW();
return false;
}
}
Result parse(std::string_view text, Span<const Function> functions) {
for (unsigned int i = 0; i < functions.size(); ++i) {
const Function &f = functions[i];
CRASH_COND(f.name == "");
CRASH_COND(f.func == nullptr);
}
Tokenizer tokenizer(text);
Result result = parse_expression(tokenizer, false, functions);
if (result.error.id != ERROR_NONE) {
return result;
}
if (result.root != nullptr) {
float _;
precompute_constants(result.root, _, functions);
}
return result;
}
bool is_tree_equal(const Node *a, const Node *b, Span<const Function> functions) {
if (a->type != b->type) {
return false;
}
switch (a->type) {
case Node::NUMBER: {
const NumberNode *nn_a = reinterpret_cast<const NumberNode *>(a);
const NumberNode *nn_b = reinterpret_cast<const NumberNode *>(b);
return Math::is_equal_approx(nn_a->value, nn_b->value);
}
case Node::VARIABLE: {
const VariableNode *va = reinterpret_cast<const VariableNode *>(a);
const VariableNode *vb = reinterpret_cast<const VariableNode *>(b);
return va->name == vb->name;
}
case Node::OPERATOR: {
const OperatorNode *oa = reinterpret_cast<const OperatorNode *>(a);
const OperatorNode *ob = reinterpret_cast<const OperatorNode *>(b);
if (oa->op != ob->op) {
return false;
}
CRASH_COND(oa->n0 == nullptr);
CRASH_COND(ob->n0 == nullptr);
if (oa->n1 == nullptr && ob->n1 == nullptr) {
return is_tree_equal(oa->n0, ob->n0, functions);
}
CRASH_COND(oa->n1 == nullptr);
CRASH_COND(ob->n1 == nullptr);
return is_tree_equal(oa->n0, ob->n0, functions) && is_tree_equal(oa->n1, ob->n1, functions);
}
case Node::FUNCTION: {
const FunctionNode *fa = reinterpret_cast<const FunctionNode *>(a);
const FunctionNode *fb = reinterpret_cast<const FunctionNode *>(b);
if (fa->function_id != fb->function_id) {
return false;
}
const Function *f = find_function_by_id(fa->function_id, functions);
CRASH_COND(f == nullptr);
for (unsigned int i = 0; i < f->argument_count; ++i) {
CRASH_COND(fa->args[i] == nullptr);
CRASH_COND(fb->args[i] == nullptr);
if (!is_tree_equal(fa->args[i], fb->args[i], functions)) {
return false;
}
}
return true;
}
default:
CRASH_NOW();
return false;
}
}
bool is_tree_equal(const Node &root_a, const Node &root_b, Span<const Function> functions) {
return is_tree_equal(&root_a, &root_b, functions);
}
const char *to_string(OperatorNode::Operation op) {
switch (op) {
case OperatorNode::ADD:
return "+";
case OperatorNode::SUBTRACT:
return "-";
case OperatorNode::MULTIPLY:
return "*";
case OperatorNode::DIVIDE:
return "/";
case OperatorNode::POWER:
return "^";
default:
CRASH_NOW();
return "?";
}
}
void tree_to_string(const Node *node, int depth, std::stringstream &output, Span<const Function> functions) {
for (int i = 0; i < depth; ++i) {
output << " ";
}
switch (node->type) {
case Node::NUMBER: {
const NumberNode *nn = reinterpret_cast<const NumberNode *>(node);
output << nn->value;
} break;
case Node::VARIABLE: {
const VariableNode *vn = reinterpret_cast<const VariableNode *>(node);
output << vn->name;
} break;
case Node::OPERATOR: {
const OperatorNode *on = reinterpret_cast<const OperatorNode *>(node);
output << to_string(on->op);
output << '\n';
CRASH_COND(on->n0 == nullptr);
if (on->n1 == nullptr) {
tree_to_string(on->n0, depth + 1, output, functions);
} else {
CRASH_COND(on->n1 == nullptr);
tree_to_string(on->n0, depth + 1, output, functions);
output << '\n';
tree_to_string(on->n1, depth + 1, output, functions);
}
} break;
case Node::FUNCTION: {
const FunctionNode *fn = reinterpret_cast<const FunctionNode *>(node);
const Function *f = find_function_by_id(fn->function_id, functions);
CRASH_COND(f == nullptr);
output << f->name << "()";
for (unsigned int i = 0; i < f->argument_count; ++i) {
CRASH_COND(fn->args[i] == nullptr);
output << '\n';
tree_to_string(fn->args[i], depth + 1, output, functions);
}
} break;
default:
CRASH_NOW();
}
}
std::string tree_to_string(const Node &node, Span<const Function> functions) {
std::stringstream ss;
tree_to_string(&node, 0, ss, functions);
return ss.str();
}
std::string to_string(const Error error) {
switch (error.id) {
case ERROR_NONE:
return "";
case ERROR_INVALID:
return "Invalid expression";
case ERROR_UNEXPECTED_END:
return "Unexpected end of expression";
case ERROR_INVALID_NUMBER:
return "Invalid number";
case ERROR_INVALID_TOKEN:
return "Invalid token";
case ERROR_UNEXPECTED_TOKEN:
return "Unexpected token";
case ERROR_UNKNOWN_FUNCTION: {
std::string s = "Unknown function: '";
s += error.symbol;
s += '\'';
return s;
}
case ERROR_UNCLOSED_PARENTHESIS:
return "Non-closed parenthesis";
case ERROR_MISSING_OPERAND_ARGUMENTS:
return "Missing operand arguments";
case ERROR_MULTIPLE_OPERANDS:
return "Multiple operands";
default:
return "Unknown error";
}
}
} //namespace ExpressionParser
} //namespace zylann

131
util/expression_parser.h Normal file
View File

@ -0,0 +1,131 @@
#ifndef ZYLANN_EXPRESSION_PARSER_H
#define ZYLANN_EXPRESSION_PARSER_H
#include "fixed_array.h"
#include "span.h"
#include <string_view>
namespace zylann {
namespace ExpressionParser {
struct Node {
enum Type { //
NUMBER,
VARIABLE,
OPERATOR,
FUNCTION,
TYPE_COUNT,
INVALID
};
Type type = INVALID;
virtual ~Node() {}
};
struct NumberNode : Node {
float value;
NumberNode(float p_value) : value(p_value) {
type = Node::NUMBER;
}
};
struct VariableNode : Node {
std::string_view name;
VariableNode(std::string_view p_name) : name(p_name) {
type = Node::VARIABLE;
}
};
struct OperatorNode : Node {
enum Operation { //
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE,
POWER,
OP_COUNT
};
Operation op;
// TODO Use unique ptr
Node *n0 = nullptr;
Node *n1 = nullptr;
OperatorNode(Operation p_op, Node *a, Node *b) : op(p_op), n0(a), n1(b) {
type = OPERATOR;
}
~OperatorNode();
};
struct FunctionNode : Node {
unsigned int function_id;
// TODO Use unique ptr
FixedArray<Node *, 4> args;
FunctionNode() {
type = Node::FUNCTION;
args.fill(nullptr);
}
~FunctionNode();
};
enum ErrorID { //
ERROR_NONE,
ERROR_INVALID,
ERROR_UNEXPECTED_END,
ERROR_INVALID_NUMBER,
ERROR_INVALID_TOKEN,
ERROR_UNEXPECTED_TOKEN,
ERROR_UNKNOWN_FUNCTION,
ERROR_UNCLOSED_PARENTHESIS,
ERROR_MISSING_OPERAND_ARGUMENTS,
ERROR_MULTIPLE_OPERANDS,
ERROR_COUNT
};
struct Error {
ErrorID id = ERROR_NONE;
std::string_view symbol;
unsigned int position = 0;
};
struct Result {
// TODO Use unique ptr
Node *root = nullptr;
Error error;
};
struct Function {
std::string_view name;
unsigned int argument_count = 0;
unsigned int id = 0;
float (*func)(Span<const float> args) = nullptr;
};
// TODO `text` should be `const`
Result parse(std::string_view text, Span<const Function> functions);
bool is_tree_equal(const Node &root_a, const Node &root_b, Span<const Function> functions);
std::string tree_to_string(const Node &node, Span<const Function> functions);
std::string to_string(const Error error);
void find_variables(const Node &node, std::vector<std::string_view> &variables);
// TODO Just use indices in the span? Or pointers?
inline const Function *find_function_by_id(unsigned int id, Span<const Function> functions) {
for (unsigned int i = 0; i < functions.size(); ++i) {
const Function &f = functions[i];
if (f.id == id) {
return &f;
}
}
return nullptr;
}
} // namespace ExpressionParser
} // namespace zylann
#endif // ZYLANN_EXPRESSION_PARSER_H

View File

@ -1,6 +1,7 @@
#ifndef HEADER_VOXEL_UTILITY_H
#define HEADER_VOXEL_UTILITY_H
#include "span.h"
#include <core/string/ustring.h>
#include <core/templates/vector.h>
#include <utility>
@ -66,21 +67,34 @@ inline void append_array(std::vector<T> &dst, const std::vector<T> &src) {
// Removes all items satisfying the given predicate.
// This can reduce the size of the container. Items are moved to preserve order.
//template <typename T, typename F>
//inline void remove_if(std::vector<T> &vec, F predicate) {
// unsigned int j = 0;
// for (unsigned int i = 0; i < vec.size(); ++i) {
// if (predicate(vec[i])) {
// continue;
// } else {
// if (i != j) {
// vec[j] = vec[i];
// }
// ++j;
// }
// }
// vec.resize(j);
//}
// template <typename T, typename F>
// inline void remove_if(std::vector<T> &vec, F predicate) {
// unsigned int j = 0;
// for (unsigned int i = 0; i < vec.size(); ++i) {
// if (predicate(vec[i])) {
// continue;
// } else {
// if (i != j) {
// vec[j] = vec[i];
// }
// ++j;
// }
// }
// vec.resize(j);
// }
template <typename T>
size_t find_duplicate(Span<const T> items) {
for (unsigned int i = 0; i < items.size(); ++i) {
const T &a = items[i];
for (unsigned int j = i + 1; j < items.size(); ++j) {
if (items[j] == a) {
return j;
}
}
}
return items.size();
}
inline String ptr2s(const void *p) {
return String::num_uint64((uint64_t)p, 16);

View File

@ -115,6 +115,28 @@ inline Vector3f to_vec3f(Vector3 v) {
return Vector3f(v.x, v.y, v.z);
}
inline String to_godot(const std::string_view sv) {
return String::utf8(sv.data(), sv.size());
}
static PackedStringArray to_godot(const std::vector<std::string_view> &svv) {
PackedStringArray psa;
psa.resize(svv.size());
for (unsigned int i = 0; i < svv.size(); ++i) {
psa.write[i] = to_godot(svv[i]);
}
return psa;
}
static PackedStringArray to_godot(const std::vector<std::string> &sv) {
PackedStringArray psa;
psa.resize(sv.size());
for (unsigned int i = 0; i < sv.size(); ++i) {
psa.write[i] = to_godot(sv[i]);
}
return psa;
}
} // namespace zylann
#endif // VOXEL_UTILITY_GODOT_FUNCS_H

View File

@ -6,6 +6,8 @@
namespace zylann::math {
// TODO Optimization: make template, I don't always need `real_t`, sometimes it uses doubles unnecessarily
// For interval arithmetic
struct Interval {
// Both inclusive
@ -430,6 +432,52 @@ inline Interval get_length(const Interval &x, const Interval &y, const Interval
return sqrt(squared(x) + squared(y) + squared(z));
}
inline Interval powi(Interval x, int pi) {
const real_t pf = pi;
if (pi >= 0) {
if (pi % 2 == 1) {
// Positive odd powers: ascending
return Interval{ Math::pow(x.min, pf), Math::pow(x.max, pf) };
} else {
// Positive even powers: parabola
if (x.min < 0.f && x.max > 0.f) {
// The interval includes 0
return Interval{ 0.f, max(Math::pow(x.min, pf), Math::pow(x.max, pf)) };
}
// The interval is only on one side of the parabola
if (x.max <= 0.f) {
// Negative side: monotonic descending
return Interval{ Math::pow(x.max, pf), Math::pow(x.min, pf) };
} else {
// Positive side: monotonic ascending
return Interval{ Math::pow(x.min, pf), Math::pow(x.max, pf) };
}
}
} else {
// TODO Negative integer powers
return Interval::from_infinity();
}
}
inline Interval pow(Interval x, float pf) {
const int pi = pf;
if (Math::is_equal_approx(pi, pf)) {
return powi(x, pi);
} else {
// TODO Decimal powers
return Interval::from_infinity();
}
}
inline Interval pow(Interval x, Interval p) {
if (p.is_single_value()) {
return pow(x, p.min);
} else {
// TODO Varying powers
return Interval::from_infinity();
}
}
} //namespace zylann::math
#endif // INTERVAL_H