From 8a2df7f75845488bad5cc6ebbb563ca5b13fbaa3 Mon Sep 17 00:00:00 2001 From: gk646 Date: Fri, 24 May 2024 00:00:57 +0200 Subject: [PATCH] version bump 1.0.8 added persistence of node groups node groups are also loaded when importing other files --- README.md | 24 +- examples/{ => logic}/1BitAdder.rn | 0 examples/{ => logic}/1BitFullAdder.rn | 12 +- examples/logic/2BitFullAdder.rn | 65 ++++++ examples/quest/SimpleQuest.rn | 40 ++++ src/external/cxstructs/include/cxutil/cxio.h | 16 +- .../cxstructs/include/cxutil/cxstring.h | 10 + src/raynodes/application/EditorContext.h | 2 +- src/raynodes/application/NodeEditor.cpp | 2 +- .../application/context/ContextCore.h | 2 +- .../application/context/ContextPersist.h | 2 + .../{ContextTemplates.h => ContextTemplate.h} | 0 .../application/context/ContextTerminal.h | 28 +++ .../application/context/impl/ContextCore.cpp | 6 +- .../application/context/impl/ContextLogic.cpp | 20 +- .../context/impl/ContextPersist.cpp | 205 +++++++++++++++--- .../application/editor/EditorControls.h | 1 + src/raynodes/application/editor/EditorDraw.h | 2 + src/raynodes/application/elements/Action.cpp | 1 - src/raynodes/blocks/NodeGroup.cpp | 48 ++-- src/raynodes/blocks/NodeGroup.h | 16 +- src/raynodes/shared/defines.h | 14 +- src/raynodes/shared/fwd.h | 7 +- src/raynodes/shared/types.h | 13 +- src/raynodes/shared/uiutil.h | 5 + src/raynodes/ui/elements/ActionMenu.h | 1 + src/raynodes/ui/elements/NodeCreateMenu.cpp | 2 +- test/ActionTest.cpp | 4 +- test/PersistTest.cpp | 12 +- 29 files changed, 451 insertions(+), 109 deletions(-) rename examples/{ => logic}/1BitAdder.rn (100%) rename examples/{ => logic}/1BitFullAdder.rn (75%) create mode 100644 examples/logic/2BitFullAdder.rn create mode 100644 examples/quest/SimpleQuest.rn rename src/raynodes/application/context/{ContextTemplates.h => ContextTemplate.h} (100%) create mode 100644 src/raynodes/application/context/ContextTerminal.h diff --git a/README.md b/README.md index cab944e..27b0cf1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # raynodes -`raynodes` is a standalone 2D node editor made using [raylib](https://github.com/raysan5/raylib) and [raygui](https://github.com/raysan5/raygui) with a focus +`raynodes` is a standalone 2D node editor made using [raylib](https://github.com/raysan5/raylib) +and [raygui](https://github.com/raysan5/raygui) with a focus extensibility. It aims to be an attractive tool for any node based task, and supports being integrated into bigger projects like games, editors... In many cases it comes close to being a node-editor SDK of sorts. @@ -12,14 +13,16 @@ A small showcase of its major features: - Custom filetype optimized for **low export size** - **Fast and optimized** import header included for working with project exports! - Supports both **Windows and Linux** (and possibly macOS) -- **Plugin interface** and capabilities +- **Plugin interface** and capabilities which allows extending the node library - **Unit testing** of critical parts (importing, persistence...) -- Fully-fledged **User created** nodes in-editor without touching the source code! +- Supports **user-created** nodes inside the editor without touching the source code! - **Modern code base** using many C++20 and above For more infos on the design choices go to [Software Design](#Software-Design) For more information on how to use the editor look at the [raynodes-wiki](https://github.com/gk646/raynodes/wiki). -The other dependencies are [cxstructs](https://github.com/gk646/cxstructs), [tinyfiledialogs](https://sourceforge.net/projects/tinyfiledialogs/) for file dialogs and [catch2](https://github.com/catchorg/Catch2) for testing. +The other dependencies +are [cxstructs](https://github.com/gk646/cxstructs), [tinyfiledialogs](https://sourceforge.net/projects/tinyfiledialogs/) +for file dialogs and [catch2](https://github.com/catchorg/Catch2) for testing. ![Image](.github/fullEditor.png) @@ -34,7 +37,8 @@ The other dependencies are [cxstructs](https://github.com/gk646/cxstructs), [tin ### For Users -Just download the .zip for your operating system from the most recent release in the [release page](https://github.com/gk646/raynodes/releases). +Just download the .zip for your operating system from the most recent release in +the [release page](https://github.com/gk646/raynodes/releases). Unzip and start the executable! Have fun using `raynodes`. ### For Developers @@ -49,7 +53,8 @@ To build the project locally you just need to do 4 simple steps: 4. Build the project from inside the build directory with `make ..` All external dependencies are included in the source! -This model is chosen based on their combined low size (only **14mb**) and the provided simplicity for sharing and integrated testing. +This model is chosen based on their combined low size (only **14mb**) and the provided simplicity for sharing and +integrated testing. ## Editor Features: @@ -61,7 +66,6 @@ Files starting with `.` (dot) will be interpreted as relative paths: Else it will be interpreted as absolute path: `./raynodes.exe C:\Users\Me\Documents\MyAbsoluteFile.rn` - **On Windows you can also set it as the default executable for ".rn" files, then double click to open any such file!** ### Controls @@ -74,10 +78,12 @@ the [shortcuts](https://github.com/gk646/raynodes/wiki/Controls) page in the wik The user interface takes inspiration from other editors like paint.net. For a comprehensive list checkout the [user-interface](https://github.com/gk646/raynodes/wiki/User-Interface) page of the wiki! -### User Defined Templates +### User Defined Templates `raynodes` gives you the ability to create your own nodes without even interacting with the source code! -Open the `Node Creator` window via the button on the left and add a new node by clicking `+Add`. For more in-depth information check out the [User-Created-Nodes](https://github.com/gk646/raynodes/wiki/User-Created-Nodes) page of the wiki! +Open the `Node Creator` window via the button on the left and add a new node by clicking `+Add`. For more in-depth +information check out the [User-Created-Nodes](https://github.com/gk646/raynodes/wiki/User-Created-Nodes) page of the +wiki! ## Custom Components! diff --git a/examples/1BitAdder.rn b/examples/logic/1BitAdder.rn similarity index 100% rename from examples/1BitAdder.rn rename to examples/logic/1BitAdder.rn diff --git a/examples/1BitFullAdder.rn b/examples/logic/1BitFullAdder.rn similarity index 75% rename from examples/1BitFullAdder.rn rename to examples/logic/1BitFullAdder.rn index 70a6830..59c0f16 100644 --- a/examples/1BitFullAdder.rn +++ b/examples/logic/1BitFullAdder.rn @@ -1,5 +1,5 @@ --EditorData-- -810-117.000-90.0001.000 +810-155.000-306.0000.800 --Templates-- 4 0XOR GateGate255 @@ -7,11 +7,11 @@ 2AND GateGate255 3OR GateGate255 --Nodes-- -20-309-81 -01-305-214 -12-616-218 -13-619-85 -14-62174 +20-20150 +01-197-83 +12-508-87 +13-51146 +14-196-256 05181-177 2625-35 37176100 diff --git a/examples/logic/2BitFullAdder.rn b/examples/logic/2BitFullAdder.rn new file mode 100644 index 0000000..713c098 --- /dev/null +++ b/examples/logic/2BitFullAdder.rn @@ -0,0 +1,65 @@ +--EditorData-- +2328-359.301-355.3030.950 +--Templates-- +5 +0XOR GateGate255 +1Bool DisplayDisplay255 +2BoolBool255 +3AND GateGate255 +4OR GateGate255 +--Nodes-- +30-13-52 +01-9-185 +22-319-189 +23-322-56 +24-154-289 +05211-302 +36218-174 +47351-53 +38-251-280 +09-247-413 +210-555-425 +211-561-284 +212-436-544 +013-56-540 +314-47-385 +41580-281 +217-880-513 +218-884-367 +219-887-237 +216-877-633 +1212-271 +1220-549 +1233-412 +--Connections-- +200100 +300000 +200001 +300101 +100500 +400601 +400501 +000701 +600700 +100600 +1100800 +1000801 +1000900 +1100901 +9001300 +12001301 +9001400 +12001401 +14001500 +8001501 +16001000 +17001100 +1800200 +1900300 +13002200 +1500400 +5002300 +7002100 +--Groups-- +-596-579Node Group010911814131512 +-357-337Node Group021360574 diff --git a/examples/quest/SimpleQuest.rn b/examples/quest/SimpleQuest.rn new file mode 100644 index 0000000..2288b81 --- /dev/null +++ b/examples/quest/SimpleQuest.rn @@ -0,0 +1,40 @@ +--EditorData-- +1120303.237-465.6130.850 +--Templates-- +3 +0Dialog ChoiceNPCTypeDisplayTextChoice1Choice2Choice3Choice4255 +1DialogNPCTypeDisplayText255 +2QuestHeaderNameDescriptionZoneLevel255 +--Nodes-- +20TestTestDTEST_ROOM1-807-462 +01ARIAWhats your name?NumericNormal nameMehLast one-493-547 +02ARIAWhich number?1234-166-862 +13ARIA-134-521 +14ARIA-126-392 +05ARIALast choice huh?Noo never!MaybeIts okOKOKOKOK-124-254 +16ARIACool number!235-909 +17ARIACool number2238-778 +18ARIALooks like your number is too big hehe!224-646 +19ARIAOkay, but i gotta go now!774-452 +110ARIAWas nice to see you!1140-455 +--Connections-- +0-101-10 +1202-10 +1303-10 +1404-10 +1505-10 +2206-10 +2307-10 +2408-10 +2508-10 +8-109-10 +7-109-10 +6-109-10 +4-109-10 +3-109-10 +3-109-10 +5209-10 +5309-10 +5509-10 +5409-10 +9-1010-10 diff --git a/src/external/cxstructs/include/cxutil/cxio.h b/src/external/cxstructs/include/cxutil/cxio.h index bf25fce..eb5f9fd 100644 --- a/src/external/cxstructs/include/cxutil/cxio.h +++ b/src/external/cxstructs/include/cxutil/cxio.h @@ -189,7 +189,7 @@ inline void io_load_skip_separator(FILE* file) { } } inline bool io_load_inside_section(FILE* file, const char* section) { - long currentPos = ftell(file); + const long currentPos = ftell(file); if (currentPos == -1) return false; // Error - we return false char ch; @@ -206,8 +206,8 @@ inline bool io_load_inside_section(FILE* file, const char* section) { char buffer[MAX_SECTION_SIZE] = {}; int count = 0; - int sectionLength = manual_strlen(section); - while (fread(&ch, 1, 1, file) == 1 && count < sectionLength && count < MAX_SECTION_SIZE - 1) { + const int sectionLength = manual_strlen(section); + while (fread(&ch, 1, 1, file) == 1 && ch != '-' && count < sectionLength && count < MAX_SECTION_SIZE - 1) { buffer[count++] = ch; } buffer[count] = '\0'; // Null-terminate the string @@ -215,14 +215,22 @@ inline bool io_load_inside_section(FILE* file, const char* section) { io_load_newline(file, false); return true; // Found same section } - io_load_newline(file, false); + return false; // Found new section } fseek(file, currentPos, SEEK_SET); return true; // Still inside same section } +// Returns true if the next byte is a newline - useful when loading a undefined amount in one line +inline bool io_load_is_newline(FILE* file) { + char c; + fread(&c, 1, 1, file); + const auto result = c == '\n'; + fseek(file, -1, SEEK_CUR); + return result; +} // include to use # if defined(_STRING_) || defined(_GLIBCXX_STRING) inline void io_load(FILE* file, std::string& s) { diff --git a/src/external/cxstructs/include/cxutil/cxstring.h b/src/external/cxstructs/include/cxutil/cxstring.h index 8c61172..a6bd71f 100644 --- a/src/external/cxstructs/include/cxutil/cxstring.h +++ b/src/external/cxstructs/include/cxutil/cxstring.h @@ -84,6 +84,16 @@ inline int str_cmpn_case(const char* s1, const char* s2, int maxCount) { } return tolower(*s1) - tolower(*s2); } +// Case insensitive! - Compares to string with a while loop - stops at max count +inline bool str_cmp_case(const char* s1, const char* s2) { + while (*s1 && *s2) { + const int diff = tolower(*s1) - tolower(*s2); + if (diff != 0) return false; + ++s1; + ++s2; + } + return tolower(*s1) - tolower(*s2) == 0; +} // Case insensitive! - Tries to find and return the first occurrence of sequence in string inline const char* str_substr_case(const char* string, const char* sequence) { if (!*sequence) return string; diff --git a/src/raynodes/application/EditorContext.h b/src/raynodes/application/EditorContext.h index d72d772..f53c463 100644 --- a/src/raynodes/application/EditorContext.h +++ b/src/raynodes/application/EditorContext.h @@ -52,7 +52,7 @@ #include "context/ContextLogic.h" #include "context/ContextPersist.h" #include "context/ContextInput.h" -#include "context/ContextTemplates.h" +#include "context/ContextTemplate.h" #include "context/ContextPlugin.h" // We actually wanna keep this as small as possible diff --git a/src/raynodes/application/NodeEditor.cpp b/src/raynodes/application/NodeEditor.cpp index 7adf23c..7b616b9 100644 --- a/src/raynodes/application/NodeEditor.cpp +++ b/src/raynodes/application/NodeEditor.cpp @@ -51,7 +51,7 @@ bool NodeEditor::start() { c + context.ui.loadUI(context); // Only load file if path is given - automatically opens picker - if (!context.persist.openedFilePath.empty()) { c + context.persist.importProject(context); } + if (!context.persist.openedFilePath.empty()) c + context.persist.importProject(context); return c.holds(); } diff --git a/src/raynodes/application/context/ContextCore.h b/src/raynodes/application/context/ContextCore.h index 9c41dd8..fc2908a 100644 --- a/src/raynodes/application/context/ContextCore.h +++ b/src/raynodes/application/context/ContextCore.h @@ -61,7 +61,7 @@ struct EXPORT Core final { if (it != nodeMap.end()) return it->second; return nullptr; } - Node* createNode(EditorContext& ec, const char* name, Vector2 worldPos, uint16_t hint = UINT16_MAX); + Node* createAddNode(EditorContext& ec, const char* name, Vector2 worldPos, uint16_t hint = UINT16_MAX); void insertNode(EditorContext& ec, Node& node); void removeNode(EditorContext& ec, NodeID id); void moveToFront(Node* node) { diff --git a/src/raynodes/application/context/ContextPersist.h b/src/raynodes/application/context/ContextPersist.h index e9382f5..a3b86ee 100644 --- a/src/raynodes/application/context/ContextPersist.h +++ b/src/raynodes/application/context/ContextPersist.h @@ -29,6 +29,8 @@ struct EXPORT Persist { bool importProject(EditorContext& ec); bool saveProject(EditorContext& ec, bool saveAsMode = false); bool saveUserTemplates(EditorContext& ec); + // Imports only the nodes from another project and calls func for each + bool importNodesFromProject(EditorContext& ec); }; #endif //RAYNODES_SRC_APPLICATION_CONTEXT_CONTEXTPERSIST_H_ \ No newline at end of file diff --git a/src/raynodes/application/context/ContextTemplates.h b/src/raynodes/application/context/ContextTemplate.h similarity index 100% rename from src/raynodes/application/context/ContextTemplates.h rename to src/raynodes/application/context/ContextTemplate.h diff --git a/src/raynodes/application/context/ContextTerminal.h b/src/raynodes/application/context/ContextTerminal.h new file mode 100644 index 0000000..c0883bf --- /dev/null +++ b/src/raynodes/application/context/ContextTerminal.h @@ -0,0 +1,28 @@ +// Copyright (c) 2024 gk646 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef CONTEXTTERMINAL_H +#define CONTEXTTERMINAL_H + +struct Terminal { + +}; + +#endif //CONTEXTTERMINAL_H \ No newline at end of file diff --git a/src/raynodes/application/context/impl/ContextCore.cpp b/src/raynodes/application/context/impl/ContextCore.cpp index c80c9d7..fc6a452 100644 --- a/src/raynodes/application/context/impl/ContextCore.cpp +++ b/src/raynodes/application/context/impl/ContextCore.cpp @@ -162,7 +162,7 @@ void Core::resetEditor(EditorContext& ec) { hasUnsavedChanges = false; } -Node* Core::createNode(EditorContext& ec, const char* name, const Vector2 worldPos, uint16_t hint) { +Node* Core::createAddNode(EditorContext& ec, const char* name, const Vector2 worldPos, uint16_t hint) { if (name == nullptr) return nullptr; //Use the hint when provided @@ -176,6 +176,7 @@ Node* Core::createNode(EditorContext& ec, const char* name, const Vector2 worldP return newNode; } + void Core::insertNode(EditorContext& ec, Node& node) { if (nodeMap.contains(node.uID)) return; nodes.push_back(&node); @@ -185,10 +186,13 @@ void Core::insertNode(EditorContext& ec, Node& node) { c->onAddedToScreen(ec, node); } } + // Only call with a valid id void Core::removeNode(EditorContext& ec, const NodeID id) { if (!nodeMap.contains(id)) return; const auto node = nodeMap[id]; + + if (node->isInGroup) [[unlikely]] { NodeGroup::InvokeDelete(ec, *node); } nodeMap.erase(id); std::erase(nodes, node); diff --git a/src/raynodes/application/context/impl/ContextLogic.cpp b/src/raynodes/application/context/impl/ContextLogic.cpp index 10f2d8e..ed1b53a 100644 --- a/src/raynodes/application/context/impl/ContextLogic.cpp +++ b/src/raynodes/application/context/impl/ContextLogic.cpp @@ -132,7 +132,7 @@ void Logic::registerNodeContextActions(EditorContext& ec) { // Node Menu { ec.ui.nodeContextMenu.registerAction( - "Remove all connections", + "Remove connections", [](EditorContext& ec, Node& node) { // Cant fully test for connections unless we iterate connections // -> Outputs dont track connections! @@ -184,6 +184,13 @@ void Logic::registerNodeContextActions(EditorContext& ec) { // Node Group Menu { + ec.ui.nodeGroupContextMenu.registerQickAction( + "Refresh", + [](EditorContext& ec, NodeGroup& ng) { + ng.onConnectionAdded(ec,*ng.nodes[0]); + }, + 222); + ec.ui.nodeGroupContextMenu.registerAction( "Remove node group", [](EditorContext& ec, NodeGroup& ng) { @@ -199,17 +206,20 @@ void Logic::registerNodeContextActions(EditorContext& ec) { ec.ui.canvasContextMenu.registerQickAction( "Open node menu", [](EditorContext& ec, Vector2& _) { - ec.ui.canvasContextMenu.isVisible = false; + ec.ui.canvasContextMenu.hide(); ec.logic.contextMenuPos = ec.logic.mouse; ec.ui.nodeCreateMenu.show(); }, 227); ec.ui.canvasContextMenu.registerAction( - "Import nodes here", + "Add comment", [](EditorContext& ec, Vector2& _) { - //TODO add import nodes from other projects + }, - 209); + 30); + + ec.ui.canvasContextMenu.registerAction( + "Import nodes here", [](EditorContext& ec, Vector2& _) { ec.persist.importNodesFromProject(ec); }, 209); } } \ No newline at end of file diff --git a/src/raynodes/application/context/impl/ContextPersist.cpp b/src/raynodes/application/context/impl/ContextPersist.cpp index 7871468..f1dc3e8 100644 --- a/src/raynodes/application/context/impl/ContextPersist.cpp +++ b/src/raynodes/application/context/impl/ContextPersist.cpp @@ -19,6 +19,7 @@ // SOFTWARE. #include "application/EditorContext.h" +#include "application/elements/Action.h" #include #include @@ -26,23 +27,19 @@ #include #include -//TODO make save format that combines both saving and loading -//-> should be easy as its symmetric - function wrapper and boolean for loading-saving // Errors that we should detect: -// - Non Unique Node labels when loading templates (DONE) -// - Symbol name is too long (component or node) (DONE) -// - Non Unique Component Labels when loading templates (per node) +// - Non Unique Node labels when loading templates (DONE) +// - Symbol name is too long (component or node) (DONE) +// - Non Unique Component Labels when loading templates (per node) (DONE) +// - Missing definitions for loaded component (not present in factory maps) (DONE) // - Missing definitions for loaded node (not present in factory maps) -// - Missing definitions for loaded component (not present in factory maps) - -using PersistFunc = bool (*)(EditorContext&, FILE*); static constexpr int MAX_UNIQUE_LABELS = 512; // One hot encoding the labels struct ComponentIndices { char storage[PLG_MAX_NAME_LEN * MAX_UNIQUE_LABELS] = {}; uint16_t count = 0; - int add(const char* name, int index = -1) { + int add(const char* name, const int index = -1) { if (name == nullptr || count >= MAX_UNIQUE_LABELS || get(name) != -1) return -1; int addIndex = index == -1 ? count : index; char* ptr = storage + addIndex * PLG_MAX_NAME_LEN; @@ -61,7 +58,7 @@ struct ComponentIndices { } return -1; } - [[nodiscard]] const char* getName(int index) const { + [[nodiscard]] const char* getName(const int index) const { if (index == -1 || index > count || index >= MAX_UNIQUE_LABELS) return nullptr; return storage + index * PLG_MAX_NAME_LEN; } @@ -84,7 +81,7 @@ bool IsValidConnection(int maxNodeID, int fromNode, int from, int out, int toNod const bool correctToComponent = (to >= 0 && to < COMPS_PER_NODE) || to == -1; return correctNode && correctFromComponent && correctToComponent && out != -1 && in != -1; } -void CreateNewConnection(EditorContext& ec, int fromID, int fromI, int outI, int toID, int toI, int inI) { +Connection* CreateNewConnection(EditorContext& ec, int fromID, int fromI, int outI, int toID, int toI, int inI) { const auto& nodeMap = ec.core.nodeMap; // We use int8_t as size_type to save space Node& fromNode = *nodeMap.at(static_cast(fromID)); @@ -108,7 +105,9 @@ void CreateNewConnection(EditorContext& ec, int fromID, int fromI, int outI, int in = &toNode.nodeIn; } - ec.core.addConnection(new Connection(fromNode, from, *out, toNode, to, *in)); + const auto conn = new Connection(fromNode, from, *out, toNode, to, *in); + ec.core.addConnection(conn); + return conn; } void SaveEditorData(FILE* file, EditorContext& ec) { io_save_section(file, "EditorData"); @@ -185,6 +184,22 @@ int SaveConnections(FILE* file, EditorContext& ec) { } return count; } +void SaveGroups(FILE* file, EditorContext& ec) { + io_save_section(file, "Groups"); + for (const auto& ng : ec.core.nodeGroups) { + io_save(file, static_cast(ng.pos.x)); + io_save(file, static_cast(ng.pos.y)); + io_save(file, ng.name); + io_save(file, ng.expanded); + for (const auto n : ng.nodes) { + io_save(file, n->uID); + } + io_save_newline(file); + } +} +void SaveComments(FILE* file, EditorContext& ec) { + io_save_section(file, "Comments"); +} void LoadEditorData(FILE* file, EditorContext& ec) { io_load_newline(file, true); //Skip the Editor section io_load_skip_separator(file); // Node count @@ -194,7 +209,7 @@ void LoadEditorData(FILE* file, EditorContext& ec) { io_load(file, ec.display.camera.zoom); io_load_newline(file); } -void LoadTemplates(FILE* file, EditorContext& ec) { +void LoadTemplates(FILE* file) { io_load_newline(file, false); int amount = 0; io_load(file, amount); @@ -220,7 +235,7 @@ int LoadNodes(FILE* file, EditorContext& ec) { int id; io_load(file, id); auto* nodeName = compIndices.getName(index); - const auto newNode = ec.core.createNode(ec, nodeName, {0, 0}, static_cast(id)); + const auto newNode = ec.core.createAddNode(ec, nodeName, {0, 0}, static_cast(id)); if (!newNode) { io_load_newline(file, true); continue; @@ -254,9 +269,36 @@ int LoadConnections(FILE* file, EditorContext& ec) { } return count; } +void LoadGroups(FILE* file, EditorContext& ec) { + while (io_load_inside_section(file, "Groups")) { + int x, y; + char buff[PLG_MAX_NAME_LEN]; + bool expanded; + io_load(file, x); + io_load(file, y); + io_load(file, buff, PLG_MAX_NAME_LEN); + io_load(file, expanded); + auto& ng = ec.core.nodeGroups.emplace_back(static_cast(x), static_cast(y), buff, expanded); + while (!io_load_is_newline(file)) { + int id; + io_load(file, id); + auto* node = ec.core.getNode(static_cast(id)); + // To get the correct dimensions + if (node) { + Node::Draw(ec, *node); + Node::Update(ec, *node); + ng.addNode(ec, *node); + } + } + io_load_newline(file, true); + } +} +void LoadComments(FILE* file, EditorContext& ec) { + io_load_newline(file, true); +} } // namespace -bool Persist::saveProject(EditorContext& ec, bool saveAsMode) { +bool Persist::saveProject(EditorContext& ec, const bool saveAsMode) { // Strictly enforce this to limit saving -> Actions need to be accurate if (!ec.core.hasUnsavedChanges && !saveAsMode) return true; @@ -281,6 +323,7 @@ bool Persist::saveProject(EditorContext& ec, bool saveAsMode) { SaveTemplates(file, ec); nodes = SaveNodes(file, ec); connections = SaveConnections(file, ec); + SaveGroups(file, ec); }); if (!res) { @@ -299,7 +342,7 @@ bool Persist::saveProject(EditorContext& ec, bool saveAsMode) { bool Persist::importProject(EditorContext& ec) { if (openedFilePath.empty()) { //TODO save and reuse default path - auto* res = tinyfd_openFileDialog("Open File", nullptr, 1, Info::fileFilter, Info::fileDescription, 0); + const auto* res = tinyfd_openFileDialog("Open File", nullptr, 1, Info::fileFilter, Info::fileDescription, 0); if (res != nullptr) openedFilePath = res; } @@ -320,9 +363,10 @@ bool Persist::importProject(EditorContext& ec) { int nodes = 0; int connections = 0; LoadEditorData(file, ec); - LoadTemplates(file, ec); + LoadTemplates(file); nodes = LoadNodes(file, ec); connections = LoadConnections(file, ec); + LoadGroups(file, ec); //printf("Loaded %s nodes\n", ec.string.getPaddedNum(nodes)); //printf("Loaded %s connections\n", ec.string.getPaddedNum(connections)); @@ -339,7 +383,9 @@ bool Persist::importProject(EditorContext& ec) { //-----------USER_FILES-----------// namespace { -bool LoadFromFile(EditorContext& ec, const char* path, PersistFunc func) { +// Generic reusable method +using PersistFunc = bool (*)(EditorContext&, FILE*); +bool LoadFromFile(EditorContext& ec, const char* path, const PersistFunc func) { FILE* file = fopen(path, "rb"); if (file == nullptr) { @@ -347,14 +393,13 @@ bool LoadFromFile(EditorContext& ec, const char* path, PersistFunc func) { return true; } - auto res = func(ec, file); + const auto res = func(ec, file); if (!res) return false; if (fclose(file) != 0) return false; return true; } - bool LoadUserTemplates(EditorContext& ec, FILE* file) { io_load_newline(file, true); int size; @@ -378,7 +423,7 @@ bool LoadUserTemplates(EditorContext& ec, FILE* file) { } // Node create func - const auto createFunc = [](const NodeTemplate& nt, Vec2 p, NodeID id) { + const auto createFunc = [](const NodeTemplate& nt, const Vec2 p, const NodeID id) { return new Node(nt, p, id); }; @@ -388,7 +433,6 @@ bool LoadUserTemplates(EditorContext& ec, FILE* file) { } return true; } - bool SaveUserTemplatesImpl(EditorContext& ec, FILE* file) { io_save_section(file, "Templates"); io_save(file, static_cast(ec.templates.userDefinedNodes.size())); @@ -405,7 +449,6 @@ bool SaveUserTemplatesImpl(EditorContext& ec, FILE* file) { } return true; } - } // namespace bool Persist::loadUserFiles(EditorContext& ec) { @@ -423,8 +466,11 @@ bool Persist::saveUserTemplates(EditorContext& ec) { constexpr int sizePerTemplate = 250; const int size = std::max(static_cast(ec.templates.userDefinedNodes.size()), 1) * sizePerTemplate; - const auto res = - io_save_buffered_write(Info::userTemplates, size, [&](FILE* file) { SaveUserTemplatesImpl(ec, file); }); + const auto saveFunc = [&](FILE* file) { + SaveUserTemplatesImpl(ec, file); + }; + + const auto res = io_save_buffered_write(Info::userTemplates, size, saveFunc); if (!res) { fprintf(stderr, "Error saving to %s", Info::userTemplates); @@ -432,4 +478,113 @@ bool Persist::saveUserTemplates(EditorContext& ec) { } return true; +} + +bool Persist::importNodesFromProject(EditorContext& ec) { + const auto title = "Select project to import nodes from"; + const auto* res = tinyfd_openFileDialog(title, nullptr, 1, Info::fileFilter, Info::fileDescription, 0); + if (res == nullptr) return false; + + const auto loadFunc = [](EditorContext& ec, FILE* file) { + compIndices.reset(); + // Skip Editor Data + io_load_newline(file, true); + io_load_newline(file, true); + + // Load templates + LoadTemplates(file); + + auto* action = new NodeCreateAction(10); + + // Adding the current to the used id allows us to map back connections uniquely + const auto startID = ec.core.UID; + + // Nodes are imported such that import point is the top left corner + const Vector2 contextWorldPos = GetScreenToWorld2D(ec.logic.contextMenuPos, ec.display.camera); + float minX = FLT_MAX; + float minY = FLT_MAX; + + // Load the nodes + while (io_load_inside_section(file, "Nodes")) { + int index = -1; + io_load(file, index); + if (index == -1) { + io_load_newline(file, true); + continue; + } + int id; + io_load(file, id); + auto* nodeName = compIndices.getName(index); + const auto newNode = ec.core.createAddNode(ec, nodeName, {0, 0}, startID + id); + if (!newNode) { + io_load_newline(file, true); + continue; + } + Node::LoadState(file, *newNode); + + io_load_newline(file); + action->createdNodes.push_back(newNode); + + // Update the minimum position + if (newNode->x < minX) minX = newNode->x; + if (newNode->y < minY) minY = newNode->y; + } + + // Load the connections + while (io_load_inside_section(file, "Connections")) { + int fromNode, from, out; + int toNode, to, in; + //Output + io_load(file, fromNode); + io_load(file, from); + io_load(file, out); + //Input + io_load(file, toNode); + io_load(file, to); + io_load(file, in); + fromNode += startID; + toNode += startID; + if (IsValidConnection(UINT16_MAX, fromNode, from, out, toNode, to, in)) { + auto* conn = CreateNewConnection(ec, fromNode, from, out, toNode, to, in); + action->createdConnection.push_back(conn); + } + io_load_newline(file); + } + + // Offset the nodes so they are positioned as specified above + const Vector2 offset = {contextWorldPos.x - minX, contextWorldPos.y - minY}; + for (auto* newNode : action->createdNodes) { + newNode->x += offset.x; + newNode->y += offset.y; + } + + while (io_load_inside_section(file, "Groups")) { + int x, y; + char buff[PLG_MAX_NAME_LEN]; + bool expanded; + io_load(file, x); + io_load(file, y); + io_load(file, buff, PLG_MAX_NAME_LEN); + io_load(file, expanded); + auto& ng = ec.core.nodeGroups.emplace_back(static_cast(x), static_cast(y), buff, expanded); + while (!io_load_is_newline(file)) { + int id; + io_load(file, id); + auto* node = ec.core.getNode(static_cast(startID + id)); + // To get the correct dimensions + if (node) { + Node::Draw(ec, *node); + Node::Update(ec, *node); + ng.addNode(ec, *node); + } + } + io_load_newline(file, true); + } + + ec.core.UID = static_cast(startID + action->createdNodes.size()); + ec.core.addEditorAction(ec, action); + return true; + }; + + return LoadFromFile(ec, res, loadFunc); } \ No newline at end of file diff --git a/src/raynodes/application/editor/EditorControls.h b/src/raynodes/application/editor/EditorControls.h index efadad8..3ab72c1 100644 --- a/src/raynodes/application/editor/EditorControls.h +++ b/src/raynodes/application/editor/EditorControls.h @@ -82,6 +82,7 @@ inline void PollControls(EditorContext& ec) { // Node Search menu if (ec.input.isKeyPressed(KEY_TAB)) { ec.logic.contextMenuPos = ec.logic.mouse; + ec.ui.canvasContextMenu.hide(); ec.ui.nodeCreateMenu.show(); } diff --git a/src/raynodes/application/editor/EditorDraw.h b/src/raynodes/application/editor/EditorDraw.h index e50570f..8e3aa39 100644 --- a/src/raynodes/application/editor/EditorDraw.h +++ b/src/raynodes/application/editor/EditorDraw.h @@ -50,6 +50,8 @@ inline void DrawConnections(EditorContext& ec, const bool isCTRLDown) { if (delNodes && CheckCollisionBezierRect(fromPos, toPos, selectRect)) { action->deletedConnections.push_back(conn); + NodeGroup::InvokeConnection(ec,conn->toNode); + NodeGroup::InvokeConnection(ec,conn->fromNode); ec.core.removeConnection(conn); } } diff --git a/src/raynodes/application/elements/Action.cpp b/src/raynodes/application/elements/Action.cpp index 9d34540..385c62b 100644 --- a/src/raynodes/application/elements/Action.cpp +++ b/src/raynodes/application/elements/Action.cpp @@ -41,7 +41,6 @@ NodeDeleteAction::NodeDeleteAction(EditorContext& ec, const std::unordered_mapisInGroup) [[unlikely]] { NodeGroup::InvokeDelete(ec, *node); } } } diff --git a/src/raynodes/blocks/NodeGroup.cpp b/src/raynodes/blocks/NodeGroup.cpp index d36ba43..61c3689 100644 --- a/src/raynodes/blocks/NodeGroup.cpp +++ b/src/raynodes/blocks/NodeGroup.cpp @@ -54,17 +54,24 @@ void HandleDrag(EditorContext& ec, NodeGroup& ng) { ng.isHovered = false; } } -bool HasNoInputs(const Component& c) { - for (const auto& in : c.inputs) { - if (in.connection != nullptr) return false; +// Checks if the given node has no input connection with any of the given nodes +bool HasNoInputs(const Node& search, const std::vector& others) { + for (const auto c : search.components) { + for (const auto& in : c->inputs) { + if (in.connection != nullptr) { + if (std::ranges::contains(others, &in.connection->fromNode)) return false; + } + } } return true; } -bool HasNoOutputs(const Node* search, const std::vector& others) { + +// Checks if the given node has no output connection with any of the given nodes +bool HasNoOutputs(const Node& search, const std::vector& others) { for (const auto n : others) { - for (const auto& c : n->components) { + for (const auto c : n->components) { for (const auto& in : c->inputs) { - if (in.connection != nullptr && &in.connection->fromNode == search) return false; + if (in.connection != nullptr && &in.connection->fromNode == &search) return false; } } } @@ -90,7 +97,7 @@ void DrawRename(EditorContext& ec, NodeGroup& ng) { } } // namespace -NodeGroup::NodeGroup(EditorContext& ec, const char* name, std::unordered_map selectedNodes) +NodeGroup::NodeGroup(const EditorContext& ec, const char* name, std::unordered_map selectedNodes) : foldedDims(), pos(), dims(), name(cxstructs::str_dup(name)) { usedPins.reserve(5); nodes.reserve(selectedNodes.size() + 1); @@ -102,10 +109,8 @@ NodeGroup::NodeGroup(EditorContext& ec, const char* name, std::unordered_mapcomponents) { - if (HasNoInputs(*comp)) { + // All inputs that are not used from other nodes inside the group are projected out + if (HasNoInputs(*node, nodes)) { + for (const auto comp : node->components) { for (auto& in : comp->inputs) { usedPins.emplace_back(node, comp, &in); } } } - // Draw outputs - if (HasNoOutputs(node, nodes)) { + // All outputs that are not used from other nodes inside the group are projected out + if (HasNoOutputs(*node, nodes)) { for (const auto comp : node->components) { for (auto& out : comp->outputs) { usedPins.emplace_back(node, comp, &out); @@ -312,21 +318,21 @@ Rectangle NodeGroup::getBounds() const { } // Invoke the event methods -void NodeGroup::InvokeConnection(EditorContext&ec, Node& node) { - for(auto& g : ec.core.nodeGroups) { +void NodeGroup::InvokeConnection(EditorContext& ec, Node& node) { + for (auto& g : ec.core.nodeGroups) { g.onConnectionAdded(ec, node); } } void NodeGroup::InvokeMoved(EditorContext& ec, Node& node) { const auto bounds = node.getBounds(); - for(auto& g : ec.core.nodeGroups) { - g.onNodeMoved(ec, node,bounds); + for (auto& g : ec.core.nodeGroups) { + g.onNodeMoved(ec, node, bounds); } } void NodeGroup::InvokeDelete(EditorContext& ec, Node& node) { - for(auto& g : ec.core.nodeGroups) { + for (auto& g : ec.core.nodeGroups) { g.removeNode(ec, node); } } \ No newline at end of file diff --git a/src/raynodes/blocks/NodeGroup.h b/src/raynodes/blocks/NodeGroup.h index 6fc7f84..446394b 100644 --- a/src/raynodes/blocks/NodeGroup.h +++ b/src/raynodes/blocks/NodeGroup.h @@ -9,12 +9,6 @@ #include #include -struct StandalonePin { - Node* node; - Component* component; - Pin* pin; -}; - struct EXPORT NodeGroup { bool isRenaming = false; bool isHovered = false; @@ -27,8 +21,8 @@ struct EXPORT NodeGroup { std::vector nodes; // Nodes that make up this group std::vector usedPins; // Used in or out pins - NodeGroup(EditorContext& ec, const char* name, std::unordered_map selectedNodes); - NodeGroup(EditorContext& ec, const char* name); + NodeGroup(const EditorContext& ec, const char* name, std::unordered_map selectedNodes); + NodeGroup(float x, float y, const char* name, bool expanded); NodeGroup(NodeGroup&& other) noexcept : isHovered(other.isHovered), isDragged(other.isDragged), expanded(other.expanded), foldedDims(other.foldedDims), pos(other.pos), dims(other.dims), name(other.name), @@ -51,16 +45,16 @@ struct EXPORT NodeGroup { } ~NodeGroup(); bool operator==(const NodeGroup& other) const { return this == &other; } + [[nodiscard]] Rectangle getBounds() const; + // Core void draw(EditorContext& ec); void update(EditorContext& ec); - [[nodiscard]] Rectangle getBounds() const; - NodeGroup clone(Vector2 pos); + // Event functions void removeNode(EditorContext& ec, Node& node); void addNode(EditorContext& ec, Node& node); - void onNodeMoved(EditorContext& ec, Node& node, Rectangle nBounds); void onConnectionAdded(EditorContext& ec, Node& node); diff --git a/src/raynodes/shared/defines.h b/src/raynodes/shared/defines.h index 5630a44..0743041 100644 --- a/src/raynodes/shared/defines.h +++ b/src/raynodes/shared/defines.h @@ -21,13 +21,13 @@ #ifndef DEFINES_H #define DEFINES_H -#define COMPS_PER_NODE 6 // Max components per node -#define INPUT_PINS 3 // Max input pins for each component -#define OUTPUT_PINS 3 // Max output pins for each component -#define NODE_OUTPUT_PINS 3 // Max output pins for each node on the top level -#define START_FPS 90 -#define PLG_MAX_NAME_LEN 17 //16 with terminator ('\0') -#define NEW_LINE_SUB '\034' +#define COMPS_PER_NODE 6 // Max components per node +#define INPUT_PINS 3 // Max input pins for each component +#define OUTPUT_PINS 3 // Max output pins for each component +#define NODE_OUTPUT_PINS 3 // Max output pins for each node on the top level +#define START_FPS 90 // display refresh AND logic tickrate of the editor +#define PLG_MAX_NAME_LEN 17 // 16 with terminator ('\0') - Max length for node group, plugins, components, nodes +#define NEW_LINE_SUB '\034' // Substitute charcter for newline '\n' on the disk // Defines for the plugin exports #ifdef _WIN32 diff --git a/src/raynodes/shared/fwd.h b/src/raynodes/shared/fwd.h index 99011a5..26adad8 100644 --- a/src/raynodes/shared/fwd.h +++ b/src/raynodes/shared/fwd.h @@ -21,8 +21,8 @@ #ifndef RAYNODES_SRC_SHARED_FWD_H_ #define RAYNODES_SRC_SHARED_FWD_H_ +#include #include "shared/defines.h" -#include "shared/types.h" enum PinType : uint8_t; // Datatype of connection pins enum MOperation : uint8_t; // Type of math operation @@ -46,9 +46,12 @@ struct EditorContext; // Central backend data holder struct PluginContainer; // Holds the dll instance and the plugin instance class Window; // UI class struct TextField; // UI class +struct Vec2; // Vector2 replacement +struct ComponentTemplate; // Building plan for a component using ComponentCreateFunc = Component* (*)(ComponentTemplate); // Takes a name and returns a new Component using NodeCreateFunc = Node* (*)(const NodeTemplate&, Vec2, NodeID); // Creates a new node +using NodeFunc = void (*)(EditorContext&, Node*); // Generic function called on a node //Raylib types struct Color; @@ -56,4 +59,6 @@ struct Rectangle; struct Font; struct Vector2; +#include "shared/types.h" + #endif //RAYNODES_SRC_SHARED_FWD_H_ \ No newline at end of file diff --git a/src/raynodes/shared/types.h b/src/raynodes/shared/types.h index 4853fdb..29e1ab6 100644 --- a/src/raynodes/shared/types.h +++ b/src/raynodes/shared/types.h @@ -21,9 +21,6 @@ #ifndef TYPES_H #define TYPES_H -#include - -struct RaynodesPluginI; struct PluginContainer { void* handle; const char* name; @@ -31,7 +28,7 @@ struct PluginContainer { void free(); }; -struct Uints8{ +struct Uints8 { uint8_t x; uint8_t y; }; @@ -78,6 +75,12 @@ struct Color4 { unsigned char a; // Color alpha value }; +struct StandalonePin { + Node* node; + Component* component; + Pin* pin; +}; + struct ComponentTemplate { const char* label = nullptr; const char* component = nullptr; @@ -89,8 +92,6 @@ struct NodeTemplate { ComponentTemplate components[COMPS_PER_NODE]; // Current limit }; -struct Node; -enum NodeID : uint16_t; using NodeCreateFunc = Node* (*)(const NodeTemplate&, Vec2, NodeID); // Creates a new node struct NodeInfo { diff --git a/src/raynodes/shared/uiutil.h b/src/raynodes/shared/uiutil.h index 34dc6ad..7dbd9ec 100644 --- a/src/raynodes/shared/uiutil.h +++ b/src/raynodes/shared/uiutil.h @@ -35,6 +35,11 @@ using SortVector = cxstructs::StackVector; template void StringFilter(T* arg, const char* search, cxstructs::StackVector& collector, GetString getString) { const char* name = getString(arg); + if (cxstructs::str_cmp_case(name, search)) { + collector.clear(); + collector.push_back(arg); + return; + } if (cxstructs::str_substr_case(name, search) != nullptr) { collector.push_back(arg); } } diff --git a/src/raynodes/ui/elements/ActionMenu.h b/src/raynodes/ui/elements/ActionMenu.h index 723e778..b38b163 100644 --- a/src/raynodes/ui/elements/ActionMenu.h +++ b/src/raynodes/ui/elements/ActionMenu.h @@ -65,6 +65,7 @@ struct ActionMenu { quickActions.push_back({action, tooltip, iconID}); } void show() { isVisible = true; } + void hide() { isVisible = false; } }; #endif //ACTIONMENU_H \ No newline at end of file diff --git a/src/raynodes/ui/elements/NodeCreateMenu.cpp b/src/raynodes/ui/elements/NodeCreateMenu.cpp index 257df65..9e80e62 100644 --- a/src/raynodes/ui/elements/NodeCreateMenu.cpp +++ b/src/raynodes/ui/elements/NodeCreateMenu.cpp @@ -30,7 +30,7 @@ namespace { void HandleNewNode(EditorContext& ec, const Vector2 pos, const char* name) { - const auto newN = ec.core.createNode(ec, name, GetScreenToWorld2D(pos, ec.display.camera)); + const auto newN = ec.core.createAddNode(ec, name, GetScreenToWorld2D(pos, ec.display.camera)); if (!newN) return; const auto action = new NodeCreateAction(2); action->createdNodes.push_back(newN); diff --git a/test/ActionTest.cpp b/test/ActionTest.cpp index 1a5ac85..ba2b8d6 100644 --- a/test/ActionTest.cpp +++ b/test/ActionTest.cpp @@ -20,7 +20,7 @@ TEST_CASE("Fuzzy Test", "[Actions]") { }; auto createNode = [](EditorContext& ec) { - ec.core.createNode(ec, "Vec3", {}); + ec.core.createAddNode(ec, "Vec3", {}); }; auto undo = [](EditorContext& ec) { @@ -34,7 +34,7 @@ TEST_CASE("Fuzzy Test", "[Actions]") { const std::vector funcs{createNode, createNode, copyNode, deleteNode, pasteNode, undo, redo}; // Select a few nodes - TestUtil::fuzzTest(ec, funcs, 1000, [](EditorContext& ec) { + TestUtil::fuzzTest(ec, funcs, 10000, [](EditorContext& ec) { ec.core.selectedNodes.clear(); int size = ec.core.nodes.size(); if (size == 0) return; diff --git a/test/PersistTest.cpp b/test/PersistTest.cpp index 08be8de..bae0823 100644 --- a/test/PersistTest.cpp +++ b/test/PersistTest.cpp @@ -30,7 +30,7 @@ TEST_CASE("Test correct file creation and count", "[Persist]") { constexpr int testSize = 10; for (int i = 0; i < testSize; ++i) { - ec.core.createNode(ec, "Text", {0, 0}); + ec.core.createAddNode(ec, "Text", {0, 0}); } REQUIRE(ec.core.nodes.size() == testSize); @@ -60,11 +60,11 @@ TEST_CASE("Teset correct data persistence", "[Persist]") { // Create nodes { - ec.core.createNode(ec, "Text", {})->getComponent>("Text")->textField.buffer = testString; - ec.core.createNode(ec, "Int", {})->getComponent("Int")->selectedMode = testInt; - auto vec2 = ec.core.createNode(ec, "Vec2", {})->getComponent>("Vec2")->textFields; + ec.core.createAddNode(ec, "Text", {})->getComponent>("Text")->textField.buffer = testString; + ec.core.createAddNode(ec, "Int", {})->getComponent("Int")->selectedMode = testInt; + auto vec2 = ec.core.createAddNode(ec, "Vec2", {})->getComponent>("Vec2")->textFields; vec2[0].buffer = testFloat, vec2[1].buffer = testFloat; - auto vec3 = ec.core.createNode(ec, "Vec3", {})->getComponent>("Vec3")->textFields; + auto vec3 = ec.core.createAddNode(ec, "Vec3", {})->getComponent>("Vec3")->textFields; vec3[0].buffer = testFloat, vec3[1].buffer = testFloat, vec3[2].buffer = testFloat; } @@ -99,7 +99,7 @@ TEST_CASE("Benchmark saving and loading", "[Persist]") { constexpr int testSize = 1000; for (int i = 0; i < testSize; ++i) { - ec.core.createNode(ec, "Text", {0, 0}); + ec.core.createAddNode(ec, "Text", {0, 0}); } REQUIRE(ec.core.nodes.size() == testSize);