From 646cbacd6471754b0710615bf72805b1ab22ca1b Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Sun, 3 Apr 2022 16:43:04 +0100 Subject: [PATCH] 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 --- .../editor_property_text_change_on_submit.h | 82 ++ editor/graph/voxel_graph_editor.cpp | 57 +- editor/graph/voxel_graph_editor.h | 4 + editor/graph/voxel_graph_editor_node.cpp | 90 +- editor/graph/voxel_graph_editor_node.h | 3 + editor/graph/voxel_graph_editor_plugin.cpp | 28 +- .../voxel_graph_node_inspector_wrapper.cpp | 126 ++- .../voxel_graph_node_inspector_wrapper.h | 5 +- generators/graph/program_graph.cpp | 11 + generators/graph/program_graph.h | 45 +- generators/graph/voxel_generator_graph.cpp | 107 ++- generators/graph/voxel_generator_graph.h | 12 +- generators/graph/voxel_graph_node_db.cpp | 76 ++ generators/graph/voxel_graph_node_db.h | 4 + generators/graph/voxel_graph_runtime.cpp | 305 ++++++- generators/graph/voxel_graph_runtime.h | 20 +- tests/tests.cpp | 202 +++++ util/expression_parser.cpp | 829 ++++++++++++++++++ util/expression_parser.h | 131 +++ util/funcs.h | 44 +- util/godot/funcs.h | 22 + util/math/interval.h | 48 + 22 files changed, 2175 insertions(+), 76 deletions(-) create mode 100644 editor/graph/editor_property_text_change_on_submit.h create mode 100644 util/expression_parser.cpp create mode 100644 util/expression_parser.h diff --git a/editor/graph/editor_property_text_change_on_submit.h b/editor/graph/editor_property_text_change_on_submit.h new file mode 100644 index 00000000..d48a2ae8 --- /dev/null +++ b/editor/graph/editor_property_text_change_on_submit.h @@ -0,0 +1,82 @@ +#ifndef ZYLANN_EDITOR_PROPERTY_TEXT_CHANGE_ON_SUBMIT_H +#define ZYLANN_EDITOR_PROPERTY_TEXT_CHANGE_ON_SUBMIT_H + +#include + +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 diff --git a/editor/graph/voxel_graph_editor.cpp b/editor/graph/voxel_graph_editor.cpp index 77eb5f77..3d1d6b20 100644 --- a/editor/graph/voxel_graph_editor.cpp +++ b/editor/graph/voxel_graph_editor.cpp @@ -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 connections; - graph_edit->get_connection_list(&connections); + graph_edit.get_connection_list(&connections); for (List::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(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(graph_edit.get_node(view_name)); + ERR_FAIL_COND(view == nullptr); + + // Remove all GUI connections going to the node + + List old_connections; + graph_edit.get_connection_list(&old_connections); + + for (List::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(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 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(graph_edit->get_child(i)); @@ -605,7 +651,7 @@ void VoxelGraphEditor::update_range_analysis_previews() { } // Highlight only nodes that will actually run - Span execution_map = VoxelGeneratorGraph::get_last_execution_map_debug_from_current_thread(); + Span 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(_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"))); diff --git a/editor/graph/voxel_graph_editor.h b/editor/graph/voxel_graph_editor.h index 8c060f9b..7b54e4ac 100644 --- a/editor/graph/voxel_graph_editor.h +++ b/editor/graph/voxel_graph_editor.h @@ -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); diff --git a/editor/graph/voxel_graph_editor_node.cpp b/editor/graph/voxel_graph_editor_node.cpp index 2f19a4de..15cd2949 100644 --- a/editor/graph/voxel_graph_editor_node.cpp +++ b/editor/graph/voxel_graph_editor_node.cpp @@ -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 @@ -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 inputs; + std::vector 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 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) { diff --git a/editor/graph/voxel_graph_editor_node.h b/editor/graph/voxel_graph_editor_node.h index 04e21bc8..77425648 100644 --- a/editor/graph/voxel_graph_editor_node.h +++ b/editor/graph/voxel_graph_editor_node.h @@ -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 _input_hints; + std::vector _rows; }; } // namespace zylann::voxel diff --git a/editor/graph/voxel_graph_editor_plugin.cpp b/editor/graph/voxel_graph_editor_plugin.cpp index e9977b82..e5ad274e 100644 --- a/editor/graph/voxel_graph_editor_plugin.cpp +++ b/editor/graph/voxel_graph_editor_plugin.cpp @@ -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(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 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 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... diff --git a/editor/graph/voxel_graph_node_inspector_wrapper.cpp b/editor/graph/voxel_graph_node_inspector_wrapper.cpp index 31737af8..8589679f 100644 --- a/editor/graph/voxel_graph_node_inspector_wrapper.cpp +++ b/editor/graph/voxel_graph_node_inspector_wrapper.cpp @@ -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 namespace zylann::voxel { -void VoxelGraphNodeInspectorWrapper::setup(Ref p_graph, uint32_t p_node_id, UndoRedo *ur) { +void VoxelGraphNodeInspectorWrapper::setup( + Ref 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 *p_list) const { @@ -63,21 +67,99 @@ void VoxelGraphNodeInspectorWrapper::_get_property_list(List *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 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 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 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 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 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))); diff --git a/editor/graph/voxel_graph_node_inspector_wrapper.h b/editor/graph/voxel_graph_node_inspector_wrapper.h index b814823a..7a26326a 100644 --- a/editor/graph/voxel_graph_node_inspector_wrapper.h +++ b/editor/graph/voxel_graph_node_inspector_wrapper.h @@ -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 p_graph, uint32_t p_node_id, UndoRedo *ur); + void setup(Ref p_graph, uint32_t p_node_id, UndoRedo *ur, VoxelGraphEditor *ed); inline Ref get_graph() const { return _graph; } @@ -31,6 +33,7 @@ private: Ref _graph; uint32_t _node_id = ProgramGraph::NULL_ID; UndoRedo *_undo_redo = nullptr; + VoxelGraphEditor *_graph_editor = nullptr; }; } // namespace zylann::voxel diff --git a/generators/graph/program_graph.cpp b/generators/graph/program_graph.cpp index 4db44309..12eea078 100644 --- a/generators/graph/program_graph.cpp +++ b/generators/graph/program_graph.cpp @@ -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(); diff --git a/generators/graph/program_graph.h b/generators/graph/program_graph.h index ed76be61..debcecac 100644 --- a/generators/graph/program_graph.h +++ b/generators/graph/program_graph.h @@ -1,19 +1,25 @@ #ifndef PROGRAM_GRAPH_H #define PROGRAM_GRAPH_H +#include "../../util/non_copyable.h" #include #include +#include +#include #include #include 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 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 &connections) const; //void get_connections_from_and_to(std::vector &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 { + size_t operator()(const zylann::ProgramGraph::PortLocation &v) const { + return zylann::ProgramGraphPortLocationHasher::hash(v); + } +}; +} // namespace std + #endif // PROGRAM_GRAPH_H diff --git a/generators/graph/voxel_generator_graph.cpp b/generators/graph/voxel_generator_graph.cpp index 1a25b5af..cfde9e0d 100644 --- a/generators/graph/voxel_generator_graph.cpp +++ b/generators/graph/voxel_generator_graph.cpp @@ -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 &vars) { + // TODO Support functions + Span 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 &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(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 VoxelGeneratorGraph::get_last_execution_map_debug_from_current_thread() { +Span 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); } diff --git a/generators/graph/voxel_generator_graph.h b/generators/graph/voxel_generator_graph.h index f830d596..ec53ad61 100644 --- a/generators/graph/voxel_generator_graph.h +++ b/generators/graph/voxel_generator_graph.h @@ -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 &vars); + void get_expression_node_inputs(uint32_t node_id, std::vector &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 get_last_execution_map_debug_from_current_thread(); + static Span 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) diff --git a/generators/graph/voxel_graph_node_db.cpp b/generators/graph/voxel_graph_node_db.cpp index 42042c51..7362dfe2 100644 --- a/generators/graph/voxel_graph_node_db.cpp +++ b/generators/graph/voxel_graph_node_db.cpp @@ -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().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().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: diff --git a/generators/graph/voxel_graph_node_db.h b/generators/graph/voxel_graph_node_db.h index 623a1fb8..ce38fad5 100644 --- a/generators/graph/voxel_graph_node_db.h +++ b/generators/graph/voxel_graph_node_db.h @@ -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 inputs; std::vector 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(); diff --git a/generators/graph/voxel_graph_runtime.cpp b/generators/graph/voxel_graph_runtime.cpp index 80f0c59e..7ce9bdc2 100644 --- a/generators/graph/voxel_graph_runtime.cpp +++ b/generators/graph/voxel_graph_runtime.cpp @@ -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 &to_connect, std::vector &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 &to_connect, + std::vector &expanded_node_ids) { + switch (arg.type) { + case ExpressionParser::Node::NUMBER: { + const ExpressionParser::NumberNode &arg_nn = reinterpret_cast(arg); + pg_node.default_inputs[pg_node_input_index] = arg_nn.value; + } break; + + case ExpressionParser::Node::VARIABLE: { + const ExpressionParser::VariableNode &arg_vn = + reinterpret_cast(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 &to_connect, std::vector &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(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(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(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(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 &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 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 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 &user_to_expanded_ports, std::vector &expanded_to_user_node_ids) { + VOXEL_PROFILE_SCOPE(); + // Gather expression node IDs first, as expansion could invalidate the iterator + std::vector 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 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 user_to_expanded_ports; + std::vector 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 order; @@ -498,11 +781,25 @@ void VoxelGraphRuntime::generate_optimized_execution_map( } if (debug) { + std::vector &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 diff --git a/generators/graph/voxel_graph_runtime.h b/generators/graph/voxel_graph_runtime.h index 0553ae0c..6bafebbc 100644 --- a/generators/graph/voxel_graph_runtime.h +++ b/generators/graph/voxel_graph_runtime.h @@ -51,7 +51,7 @@ public: // TODO Typo? std::vector operation_adresses; // Stores node IDs referring to the user-facing graph - std::vector debug_nodes; + std::vector 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 output_port_addresses; + HashMap 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 user_port_to_expanded_port; + + // Associates expanded graph ID to user graph node IDs. + std::unordered_map 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; diff --git a/tests/tests.cpp b/tests/tests.cpp index 0cf261c5..65ec27f6 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -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 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 generator; generator.instantiate(); @@ -1462,6 +1490,178 @@ void test_flat_map() { } } +void test_expression_parser() { + using namespace ExpressionParser; + + { + Result result = parse("", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse(" ", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("42", Span()); + 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(result.root); + ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 42.f)); + memdelete(result.root); + } + { + Result result = parse("()", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("((()))", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("(42)", Span()); + 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(result.root); + ZYLANN_TEST_ASSERT(Math::is_equal_approx(nn->value, 42.f)); + memdelete(result.root); + } + { + Result result = parse("(", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_UNCLOSED_PARENTHESIS); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("(666", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_UNCLOSED_PARENTHESIS); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("1+", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_MISSING_OPERAND_ARGUMENTS); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("++", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_MISSING_OPERAND_ARGUMENTS); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("1 2 3", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_MULTIPLE_OPERANDS); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("???", Span()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_INVALID_TOKEN); + ZYLANN_TEST_ASSERT(result.root == nullptr); + } + { + Result result = parse("1+2-3*4/5", Span()); + 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(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()); + 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(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()); + 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(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()); + ZYLANN_TEST_ASSERT(result.error.id == ERROR_NONE); + ZYLANN_TEST_ASSERT(result.root != nullptr); + // { + // const std::string s1 = tree_to_string(*expected_root, Span()); + // print_line(String(s1.c_str())); + // print_line("---"); + // const std::string s2 = tree_to_string(*result.root, Span()); + // print_line(String(s2.c_str())); + // } + ZYLANN_TEST_ASSERT(is_tree_equal(*result.root, *expected_root, Span())); + memdelete(result.root); + memdelete(expected_root); + } + { + FixedArray functions; + + { + Function f; + f.name = "sqrt"; + f.id = 0; + f.argument_count = 1; + f.func = [](Span args) { // + return Math::sqrt(args[0]); + }; + functions[0] = f; + } + { + Function f; + f.name = "clamp"; + f.id = 1; + f.argument_count = 3; + f.func = [](Span 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(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 -------------"); } diff --git a/util/expression_parser.cpp b/util/expression_parser.cpp new file mode 100644 index 00000000..04382952 --- /dev/null +++ b/util/expression_parser.cpp @@ -0,0 +1,829 @@ +#include "expression_parser.h" +#include +#include +#include +#include + +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 +inline T pop(std::vector &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 &operations_stack, std::vector &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 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 functions); + +Result parse_function(Tokenizer &tokenizer, std::vector &operand_stack, Span 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(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 &operations_stack, std::vector 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 functions) { + Token token; + + std::vector operations_stack; + // TODO Use unique ptr + std::vector 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 &variables) { + switch (node.type) { + case Node::NUMBER: + break; + + case Node::VARIABLE: { + const VariableNode &vnode = reinterpret_cast(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(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(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 functions) { + CRASH_COND(node == nullptr); + switch (node->type) { + case Node::NUMBER: { + const NumberNode *nn = reinterpret_cast(node); + out_number = nn->value; + return true; + } + + case Node::VARIABLE: + return false; + + case Node::OPERATOR: { + OperatorNode *onode = reinterpret_cast(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(node); + bool all_constant = true; + FixedArray 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 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 functions) { + if (a->type != b->type) { + return false; + } + switch (a->type) { + case Node::NUMBER: { + const NumberNode *nn_a = reinterpret_cast(a); + const NumberNode *nn_b = reinterpret_cast(b); + return Math::is_equal_approx(nn_a->value, nn_b->value); + } + case Node::VARIABLE: { + const VariableNode *va = reinterpret_cast(a); + const VariableNode *vb = reinterpret_cast(b); + return va->name == vb->name; + } + case Node::OPERATOR: { + const OperatorNode *oa = reinterpret_cast(a); + const OperatorNode *ob = reinterpret_cast(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(a); + const FunctionNode *fb = reinterpret_cast(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 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 functions) { + for (int i = 0; i < depth; ++i) { + output << " "; + } + switch (node->type) { + case Node::NUMBER: { + const NumberNode *nn = reinterpret_cast(node); + output << nn->value; + } break; + + case Node::VARIABLE: { + const VariableNode *vn = reinterpret_cast(node); + output << vn->name; + } break; + + case Node::OPERATOR: { + const OperatorNode *on = reinterpret_cast(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(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 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 diff --git a/util/expression_parser.h b/util/expression_parser.h new file mode 100644 index 00000000..4e9007d5 --- /dev/null +++ b/util/expression_parser.h @@ -0,0 +1,131 @@ +#ifndef ZYLANN_EXPRESSION_PARSER_H +#define ZYLANN_EXPRESSION_PARSER_H + +#include "fixed_array.h" +#include "span.h" +#include + +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 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 args) = nullptr; +}; + +// TODO `text` should be `const` +Result parse(std::string_view text, Span functions); +bool is_tree_equal(const Node &root_a, const Node &root_b, Span functions); +std::string tree_to_string(const Node &node, Span functions); +std::string to_string(const Error error); +void find_variables(const Node &node, std::vector &variables); + +// TODO Just use indices in the span? Or pointers? +inline const Function *find_function_by_id(unsigned int id, Span 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 diff --git a/util/funcs.h b/util/funcs.h index f0df4699..605b0d81 100644 --- a/util/funcs.h +++ b/util/funcs.h @@ -1,6 +1,7 @@ #ifndef HEADER_VOXEL_UTILITY_H #define HEADER_VOXEL_UTILITY_H +#include "span.h" #include #include #include @@ -66,21 +67,34 @@ inline void append_array(std::vector &dst, const std::vector &src) { // Removes all items satisfying the given predicate. // This can reduce the size of the container. Items are moved to preserve order. -//template -//inline void remove_if(std::vector &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 +// inline void remove_if(std::vector &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 +size_t find_duplicate(Span 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); diff --git a/util/godot/funcs.h b/util/godot/funcs.h index 9df89e12..57050d05 100644 --- a/util/godot/funcs.h +++ b/util/godot/funcs.h @@ -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 &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 &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 diff --git a/util/math/interval.h b/util/math/interval.h index fadf82a3..46f9815d 100644 --- a/util/math/interval.h +++ b/util/math/interval.h @@ -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