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:
parent
68f8aa6092
commit
646cbacd64
82
editor/graph/editor_property_text_change_on_submit.h
Normal file
82
editor/graph/editor_property_text_change_on_submit.h
Normal 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
|
@ -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")));
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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...
|
||||
|
@ -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)));
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
202
tests/tests.cpp
202
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<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
829
util/expression_parser.cpp
Normal 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
131
util/expression_parser.h
Normal 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
|
44
util/funcs.h
44
util/funcs.h
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user