diff --git a/.gitignore b/.gitignore index 3fdc272..f50ecc3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ install_manifest.txt compile_commands.json CTestTestfile.cmake _deps -/Build/ +Build +.idea diff --git a/CMakeLists.txt b/CMakeLists.txt index 174cf9f..7519508 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,15 @@ endif() include_directories(Include) include_directories(External) +# Configure ImGui +set(IMGUI_BACKEND_SDL2 ON) +set(IMGUI_BACKEND_OPENGL ON) +set(IMGUI_STDLIB ON) +set(IMGUI_FREETYPE ON) + +# Add ImGui and any extensions +add_subdirectory(External/imgui-docking) + add_executable(OpenShaderDesigner Source/Entry.cpp @@ -54,24 +63,42 @@ add_executable(OpenShaderDesigner # Nodes Source/Graph/Nodes/Math.cpp - - # ImGui - External/imgui-docking/imgui_demo.cpp - External/imgui-docking/imgui_draw.cpp - External/imgui-docking/imgui_tables.cpp - External/imgui-docking/imgui.cpp - External/imgui-docking/imgui_widgets.cpp - External/imgui-docking/backends/imgui_impl_sdl2.cpp - External/imgui-docking/backends/imgui_impl_opengl3.cpp - External/imgui-docking/misc/cpp/imgui_stdlib.cpp - External/imgui-docking/misc/freetype/imgui_freetype.cpp - Include/OpenGL/BufferObject.h ) - target_link_libraries(OpenShaderDesigner PRIVATE Freetype::Freetype GLEW::GLEW OpenGL::GL ${SDL2_LIBRARIES} -) \ No newline at end of file +) + +# DOXYGEN ============================================================================================================== +# https://vicrucann.github.io/tutorials/quick-cmake-doxygen/ + +find_package(Doxygen) + +if(DOXYGEN_FOUND) + set(DOXYGEN_CONFIG_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) + set(DOXYGEN_CONFIG_OUT ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile) + + configure_file(${DOXYGEN_CONFIG_IN} ${DOXYGEN_CONFIG_OUT} @ONLY) + message("Doxygen Build Started.") + + if(WIN32) + add_custom_target(doxygen ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_CONFIG_OUT} + COMMAND start firefox "${CMAKE_CURRENT_SOURCE_DIR}/Documentation/html/index.html" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating Doxygen Documentation" + VERBATIM) + else() + add_custom_target(doxygen ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_CONFIG_OUT} + COMMAND firefox "${CMAKE_CURRENT_SOURCE_DIR}/Documentation/html/index.html" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating Doxygen Documentation" + VERBATIM) + endif() +else() + message("Doxygen not found.") +endif() \ No newline at end of file diff --git a/External/imgui-docking/backends/imgui_impl_sdl2.cpp b/External/imgui-docking/backends/imgui_impl_sdl2.cpp index 1bb17eb..f8602d0 100644 --- a/External/imgui-docking/backends/imgui_impl_sdl2.cpp +++ b/External/imgui-docking/backends/imgui_impl_sdl2.cpp @@ -95,8 +95,8 @@ // SDL // (the multi-viewports feature requires SDL features supported from SDL 2.0.4+. SDL 2.0.5+ is highly recommended) -#include -#include +#include +#include #if defined(__APPLE__) #include #endif diff --git a/Include/Graph/Nodes/Math.h b/Include/Graph/Nodes/Math.h index 85f84da..04042c3 100644 --- a/Include/Graph/Nodes/Math.h +++ b/Include/Graph/Nodes/Math.h @@ -16,15 +16,36 @@ #ifndef MATH_H #define MATH_H +#include #include +#include -namespace OpenShaderDesigner +namespace OpenShaderDesigner::Nodes::Math { + inline static constexpr ImColor HeaderColor = ImColor(0x92, 0x16, 0x16); + + struct Constant : public Node + { + using ValueType = Any; + + Constant(ShaderGraph& graph, ImVec2 pos); + virtual ~Constant() = default; + + [[nodiscard]] Node* Copy(ShaderGraph& graph) const override; + void Inspect() override; + + ValueType Value; + }; + + RegisterNode("Math/Constant", Constant); struct Add : public Node { Add(ShaderGraph& graph, ImVec2 pos); + virtual ~Add() = default; + [[nodiscard]] Node* Copy(ShaderGraph& graph) const override; + void Inspect() override; }; RegisterNode("Math/Add", Add); diff --git a/Include/Graph/ShaderGraph.h b/Include/Graph/ShaderGraph.h index bb90147..db085b2 100644 --- a/Include/Graph/ShaderGraph.h +++ b/Include/Graph/ShaderGraph.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,26 @@ namespace OpenShaderDesigner using PinId = uint16_t; using NodeId = uint32_t; + struct PinPtr + { + struct Hash + { + size_t operator()(const PinPtr& p) const + { + return p.hash(); + } + }; + + NodeId Node; + PinId Pin; + bool Input; + + size_t hash() const { return (Input ? 0 : 0x8000000) | static_cast(Node) << 32 | static_cast(Pin & 0x7FFFFFFF); } + + bool operator<(const PinPtr& o) const { return hash() < o.hash(); } + bool operator==(const PinPtr& o) const { return hash() == o.hash(); } + }; + struct Pin { enum PinType @@ -45,9 +66,8 @@ namespace OpenShaderDesigner , UINT , FLOAT , VECTOR - , TEXTURE - , ANY + , ANY , COUNT }; @@ -62,10 +82,17 @@ namespace OpenShaderDesigner , ImColor(0x8C, 0xC0, 0x8C) , ImColor(0x37, 0x95, 0x85) , ImColor(0xE3, 0x7D, 0xDC) - , ImColor(0xD2, 0x6E, 0x46) +// , ImColor(0xD2, 0x6E, 0x46) , ImColor(0xD2, 0xD5, 0xD3) }; + inline const static std::string TypeNames[COUNT] = { + "Int" + , "Unsigned Int" + , "Float" + , "Vector" + }; + std::string Name; PinType Type; PinDirection Direction; @@ -73,6 +100,7 @@ namespace OpenShaderDesigner struct Node { + public: ImVec2 Position = { 0, 0 }; struct @@ -91,7 +119,7 @@ namespace OpenShaderDesigner struct { ImVec2 Size; - bool Const; + bool Const; } Info; Node( @@ -100,8 +128,10 @@ namespace OpenShaderDesigner , const std::vector& inputs, bool dyn_inputs , const std::vector& outputs , bool constant = false); + ~Node() = default; virtual Node* Copy(ShaderGraph& graph) const = 0; + virtual void Inspect() = 0; }; class ShaderGraph @@ -109,25 +139,6 @@ namespace OpenShaderDesigner { private: friend Node; - struct PinPtr - { - struct Hash - { - size_t operator()(const PinPtr& p) const - { - return p.hash(); - } - }; - - NodeId Node; - PinId Pin; - bool Input; - - size_t hash() const { return (Input ? 0 : 0x8000000) | static_cast(Node) << 32 | static_cast(Pin & 0x7FFFFFFF); } - - bool operator<(const PinPtr& o) const { return hash() < o.hash(); } - bool operator==(const PinPtr& o) const { return hash() == o.hash(); } - }; using Connection = std::pair; using ConnectionMap = std::unordered_multimap; @@ -145,6 +156,20 @@ namespace OpenShaderDesigner ConstructorPtr Constructor; }; + struct GraphState + { + ShaderGraph& Parent; + std::vector Nodes; + std::unordered_set Erased; + ConnectionMap Connections; + + GraphState(ShaderGraph& parent); + GraphState(const GraphState& other); + ~GraphState(); + + GraphState& operator=(const GraphState& other); + }; + using ContextMenuHierarchy = DirectedGraph; using ContextID = ContextMenuHierarchy::Node; inline static ContextMenuHierarchy ContextMenu; @@ -178,12 +203,19 @@ namespace OpenShaderDesigner void Paste(const ImVec2& location); void EraseSelection(); + // History Functionality + void PushState(); + void PopState(); + // Helper functions float BezierOffset(const ImVec2& out, const ImVec2& in); bool AABB(const ImVec2& a0, const ImVec2& a1, const ImVec2& b0, const ImVec2& b1); ImVec2 GridToScreen(const ImVec2& position); ImVec2 ScreenToGrid(const ImVec2& position); + ImVec2 SnapToGrid(const ImVec2& position); + + Pin& GetPin(const PinPtr& ptr); public: ShaderGraph(); @@ -194,12 +226,9 @@ namespace OpenShaderDesigner static void Register(const std::filesystem::path& path, ConstructorPtr constructor); - - private: - std::vector Nodes; - std::unordered_set Erased; - ConnectionMap Connections; + GraphState State; + std::stack History; struct { @@ -279,6 +308,22 @@ namespace OpenShaderDesigner bool Focused; ImVec2 ContextMenuPosition; + + friend class Inspector; + }; + + class Inspector + : public EditorWindow + { + public: + Inspector(); + + void DrawWindow() override; + + private: + ShaderGraph* Graph; + + friend class ShaderGraph; }; } diff --git a/LICENSE b/LICENSE index 261eeb9..ba0cc5b 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2024 Medusa Slockbower Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Core/Window.cpp b/Source/Core/Window.cpp index 2313035..dfe0680 100644 --- a/Source/Core/Window.cpp +++ b/Source/Core/Window.cpp @@ -56,8 +56,12 @@ Window::Window(const Configuration& config) SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 16); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 16); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 16); + SDL_GL_SetAttribute(SDL_GL_FLOATBUFFERS, 1); } + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); + Context = SDL_GL_CreateContext(Handle); if(Context == nullptr) diff --git a/Source/Editor/EditorSystem.cpp b/Source/Editor/EditorSystem.cpp index 6b68e20..3ebb16f 100644 --- a/Source/Editor/EditorSystem.cpp +++ b/Source/Editor/EditorSystem.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -117,12 +118,12 @@ void EditorSystem::Initialize() ImGuiIO& io = ImGui::GetIO(); - io.Fonts->AddFontFromFileTTF("./Assets/Fonts/FiraMono-Regular.ttf", 20.0f); static ImWchar ranges[] = { 0x1, static_cast(0x1FFFF), 0 }; static ImFontConfig cfg; - cfg.OversampleH = cfg.OversampleV = 1; + cfg.OversampleH = cfg.OversampleV = 2; cfg.MergeMode = true; cfg.FontBuilderFlags |= ImGuiFreeTypeBuilderFlags_LoadColor; + io.Fonts->AddFontFromFileTTF("./Assets/Fonts/FiraMono-Regular.ttf", 20.0f); io.Fonts->AddFontFromFileTTF("./Assets/Fonts/remixicon.ttf", 18.0f, &cfg, ranges); @@ -139,8 +140,8 @@ void EditorSystem::Initialize() void EditorSystem::Draw() { ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplSDL2_NewFrame(); + ImGui::NewFrame(); ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode); diff --git a/Source/Graph/Nodes/Math.cpp b/Source/Graph/Nodes/Math.cpp index 4245f1a..1dff91f 100644 --- a/Source/Graph/Nodes/Math.cpp +++ b/Source/Graph/Nodes/Math.cpp @@ -14,13 +14,74 @@ // ===================================================================================================================== #include +#include using namespace OpenShaderDesigner; +using namespace OpenShaderDesigner::Nodes::Math; + +Constant::Constant(ShaderGraph& graph, ImVec2 pos) + : Node( + graph, pos + , "Constant", HeaderColor + , { }, false + , { { "Out", Pin::FLOAT, Pin::OUTPUT } } + ) +{ +} + +Node* Constant::Copy(ShaderGraph& graph) const +{ + return new Constant(graph, Position); +} + +void Constant::Inspect() +{ + Pin::PinType& Type = IO.Outputs[0].Type; + + if(ImGui::BeginCombo("Type", Pin::TypeNames[Type].c_str())) + { + for(int i = 0; i < Pin::ANY; ++i) + { + Pin::PinType t = static_cast(i); + + if(ImGui::Selectable(Pin::TypeNames[t].c_str(), t == Type)) + { + Type = t; + } + } + + ImGui::EndCombo(); + } + + glm::vec4& v = Value; + + switch(Type) + { + case Pin::INT: + ImGui::InputInt("Value", Value); + break; + + case Pin::UINT: + ImGui::InputUInt("Value", Value); + break; + + case Pin::FLOAT: + ImGui::InputFloat("Value", Value); + break; + + case Pin::VECTOR: + ImGui::ColorEdit4("Value", &v.x); + break; + + default: + break; + } +} Add::Add(ShaderGraph& graph, ImVec2 pos) : Node( graph, pos - , "Add", ImColor(0x92, 0x16, 0x16) + , "Add", HeaderColor , { { "A", Pin::ANY, Pin::INPUT }, { "B", Pin::ANY, Pin::INPUT } }, true , { { "Out", Pin::ANY, Pin::OUTPUT } } ) @@ -30,3 +91,8 @@ Node* Add::Copy(ShaderGraph& graph) const { return new Add(graph, Position); } + +void Add::Inspect() +{ + +} diff --git a/Source/Graph/ShaderGraph.cpp b/Source/Graph/ShaderGraph.cpp index 0ac05d1..cd6a171 100644 --- a/Source/Graph/ShaderGraph.cpp +++ b/Source/Graph/ShaderGraph.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -31,6 +32,56 @@ ImColor operator*(const ImColor& c, float f) return ImVec4(c.Value.x * f, c.Value.y * f, c.Value.z * f, c.Value.w); } +ShaderGraph::GraphState::GraphState(ShaderGraph& parent) + : Parent(parent) +{ +} + +ShaderGraph::GraphState::GraphState(const GraphState& other) + : Parent(other.Parent) + , Nodes(other.Nodes.size(), nullptr) + , Connections(other.Connections) + , Erased(other.Erased) +{ + NodeId id = 0; + for(const Node* node : other.Nodes) + { + if(node) Nodes[id] = node->Copy(Parent); + ++id; + } +} + +ShaderGraph::GraphState::~GraphState() +{ + for(Node* node : Nodes) + { + if(node) delete node; + } +} + +ShaderGraph::GraphState& ShaderGraph::GraphState::operator=(const GraphState& other) +{ + for(Node* node : Nodes) + { + if(node) delete node; + } + + Nodes.clear(); + Nodes.resize(other.Nodes.size(), nullptr); + + NodeId id = 0; + for(const Node* node : other.Nodes) + { + if(node) Nodes[id] = node->Copy(Parent); + ++id; + } + + Connections = other.Connections; + Erased = other.Erased; + + return *this; +} + float ShaderGraph::CalculateWidth(Node& node) { const float GridSize = Style.FontSize + Style.Grid.Lines.Padding; @@ -50,7 +101,7 @@ float ShaderGraph::CalculateWidth(Node& node) OutputWidth = glm::max(OutputWidth, HeaderHeight + ImGui::CalcTextSize(pin.Name.c_str()).x); } - float Width = glm::max(InputWidth, HeaderWidth) + OutputWidth + 1 * HeaderHeight; + float Width = glm::max(InputWidth + OutputWidth, HeaderWidth) + HeaderHeight; Width += GridSize - std::fmod(1.0f + Style.Grid.Lines.Padding + Width, GridSize); return Width; @@ -90,6 +141,7 @@ Node::Node( ShaderGraph::ShaderGraph() : EditorWindow("\uED46 Shader Graph", 0) + , State(*this) , Style { .Grid @@ -166,6 +218,8 @@ void ShaderGraph::OnOpen() { Mouse.Location = ImGui::GetMousePos(); Camera.Scroll = Camera.Zoom = 1.0f; + + EditorSystem::Open()->Graph = this; } void ShaderGraph::DrawWindow() @@ -175,8 +229,9 @@ void ShaderGraph::DrawWindow() DrawGrid(); + NodeId uid = 0; - for(Node* node : Nodes) + for(Node* node : State.Nodes) { if(node == nullptr) { ++uid; continue; } DrawNode(*node, uid++); @@ -344,26 +399,23 @@ void ShaderGraph::DrawNode(Node& node, NodeId id) ImDrawList& DrawList = *ImGui::GetWindowDrawList(); // Draw Vars - const float HeaderHeight = Style.FontSize; - const ImVec2 Padding = { Style.Grid.Lines.Padding, Style.Grid.Lines.Padding }; - const ImVec2 NodePos = node.Position + Padding; - const ImVec2 NodeRoot = GridToScreen(NodePos); - const ImVec2 NodeEdge = GridToScreen(NodePos + node.Info.Size); - const ImVec2 HeaderEdge = GridToScreen(NodePos + ImVec2(node.Info.Size.x, HeaderHeight)); - const ImVec2 HeaderText = GridToScreen(NodePos + ImVec2(Style.Nodes.Rounding, 0) + Padding * 0.5f); - const ImVec2 InputRoot = GridToScreen(NodePos + ImVec2(Style.Nodes.Pins.Padding, HeaderHeight)); - const ImVec2 OutputRoot = GridToScreen(NodePos + ImVec2(node.Info.Size.x - HeaderHeight - Style.Nodes.Pins.Padding, HeaderHeight)); - - const bool HasLock = Mouse.FocusedNode(); - const bool NodeHovered = ImGui::IsMouseHoveringRect(NodeRoot, NodeEdge); - const bool HeaderHovered = ImGui::IsMouseHoveringRect(NodeRoot, HeaderEdge); - const ImColor HeaderColor = node.Header.Color * (HeaderHovered || HasLock ? 1.2f : 1.0f) * (HasLock ? 0.8f : 1.0f); - - const float Rounding = Style.Nodes.Rounding / Camera.Zoom; - const float PinSpacing = HeaderHeight / Camera.Zoom; + const float HeaderHeight = Style.FontSize; + const ImVec2 Padding = { Style.Grid.Lines.Padding, Style.Grid.Lines.Padding }; + const ImVec2 NodePos = node.Position + Padding; + const ImVec2 NodeRoot = GridToScreen(NodePos); + const ImVec2 NodeEdge = GridToScreen(NodePos + node.Info.Size); + const ImVec2 HeaderEdge = GridToScreen(NodePos + ImVec2(node.Info.Size.x, HeaderHeight)); + const ImVec2 HeaderText = GridToScreen(NodePos + ImVec2(Style.Nodes.Rounding, 0) + Padding * 0.5f); + const ImVec2 InputRoot = GridToScreen(NodePos + ImVec2(Style.Nodes.Pins.Padding, HeaderHeight)); + const ImVec2 OutputRoot = GridToScreen(NodePos + ImVec2(node.Info.Size.x - HeaderHeight - Style.Nodes.Pins.Padding, HeaderHeight)); + const bool HasLock = Mouse.FocusedNode(); + const bool NodeHovered = ImGui::IsMouseHoveringRect(NodeRoot, NodeEdge); + const bool HeaderHovered = ImGui::IsMouseHoveringRect(NodeRoot, HeaderEdge); + const ImColor HeaderColor = node.Header.Color * (HeaderHovered || HasLock ? 1.2f : 1.0f) * (HasLock ? 0.8f : 1.0f); + const float Rounding = Style.Nodes.Rounding / Camera.Zoom; + const float PinSpacing = HeaderHeight / Camera.Zoom; const float BorderThickness = Style.Nodes.Border.Thickness / Camera.Zoom; - const float GridSize = (Style.FontSize + Style.Grid.Lines.Padding); const bool Ctrl = ImGui::IsKeyDown(ImGuiKey_ModCtrl); const bool Shift = ImGui::IsKeyDown(ImGuiKey_ModShift); @@ -387,7 +439,7 @@ void ShaderGraph::DrawNode(Node& node, NodeId id) for(NodeId selected : Mouse.Selected) { - Mouse.Locks.emplace(selected, Mouse.Location - Nodes[selected]->Position); + Mouse.Locks.emplace(selected, Mouse.Location - State.Nodes[selected]->Position); } } } @@ -407,14 +459,16 @@ void ShaderGraph::DrawNode(Node& node, NodeId id) Mouse.Selected.insert(id); } - // Begin selection + // Begin Dragging if(Mouse.FocusedNode() && Mouse.Selected.contains(id) && !(Ctrl || Shift)) { + if(Mouse.LocksDragged == false) PushState(); + Mouse.LocksDragged = true; for(NodeId selected : Mouse.Selected) { - Mouse.Locks.emplace(selected, Mouse.Location - Nodes[selected]->Position); + Mouse.Locks.emplace(selected, Mouse.Location - State.Nodes[selected]->Position); } } @@ -495,7 +549,7 @@ void ShaderGraph::DrawNode(Node& node, NodeId id) if(Mouse.Locks.contains(id)) { node.Position = Mouse.Location - Mouse.Locks[id]; - node.Position = ImFloor(node.Position / GridSize + ImVec2(0.5f, 0.5f)) * GridSize; + node.Position = SnapToGrid(node.Position); } // Content ========================================================================================================= @@ -511,7 +565,7 @@ void ShaderGraph::DrawNode(Node& node, NodeId id) DrawList.AddText(NULL, Style.FontSize / Camera.Zoom, HeaderText, Style.Nodes.Title, node.Header.Title.c_str()); DrawList.PopClipRect(); - DrawList.AddLine(InputRoot, HeaderEdge, Style.Nodes.Border.Color, BorderThickness); + DrawList.AddLine(ImVec2(NodeRoot.x, HeaderEdge.y), HeaderEdge, Style.Nodes.Border.Color, BorderThickness); } // Border ========================================================================================================== @@ -551,26 +605,45 @@ void ShaderGraph::DrawPin(NodeId node_id, Pin& pin, PinId pin_id, ImVec2 locatio const ImVec2 PinCenter = PinRoot + Offset; const float PinPadding = Style.Nodes.Pins.Padding / Camera.Zoom; const float BorderThickness = Style.Nodes.Pins.BorderThickness / Camera.Zoom; - const float PinRadius = (HeaderHeight - PinPadding - 2.0f * Style.Nodes.Pins.BorderThickness) * 0.5f; + const float PinRadius = (HeaderHeight - PinPadding - 2.0f * BorderThickness) * 0.5f; const bool Hovered = ImGui::IsMouseHoveringRect(PinRoot, PinEdge); + const bool MouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); + const bool MouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); // Pin ============================================================================================================= // Input - if(ImGui::IsMouseClicked(ImGuiMouseButton_Left) && Hovered && !Mouse.NewConnection() && !ImGui::IsKeyDown(ImGuiKey_ModAlt)) + if(MouseClicked && Hovered && !Mouse.NewConnection() && !ImGui::IsKeyDown(ImGuiKey_ModAlt)) { StartConnection({ node_id, pin_id, input }); } // Circle PinPtr ptr = { node_id, pin_id, input }; - auto it = Connections.find(ptr); - if(Mouse.NewConnection() && *Mouse.NewConnection == ptr) DrawList.AddCircleFilled(PinCenter, PinRadius, Pin::Colors[pin.Type]); - else if(!input && it != Connections.end()) DrawList.AddCircleFilled(PinCenter, PinRadius, Pin::Colors[pin.Type] * (Hovered ? 0.8f : 1.0f)); - else if(it != Connections.end()) DrawList.AddCircleFilled(PinCenter, PinRadius, Pin::Colors[Nodes[it->second.Node]->IO.Outputs[it->second.Pin].Type] * (Hovered ? 0.8f : 1.0f)); - else if(Hovered) DrawList.AddCircleFilled(PinCenter, PinRadius, Pin::Colors[pin.Type]); - else DrawList.AddCircleFilled(PinCenter, PinRadius, Style.Nodes.Pins.Background); - DrawList.AddCircle(PinCenter, PinRadius, Pin::Colors[pin.Type], 0, BorderThickness); + auto it = State.Connections.find(ptr); + const bool Connected = (it != State.Connections.end()); + const bool NewConnectionRoot = Mouse.NewConnection() && *Mouse.NewConnection == ptr; + const bool NewConnectionNext = Mouse.NewConnection() && *Mouse.NewConnection != ptr && Hovered; + const bool Pressed = Hovered && MouseDown; + const bool Filled = Hovered || Connected || NewConnectionRoot || NewConnectionNext; + ImColor pinColor = Pin::Colors[pin.Type]; + ImColor fillColor = Style.Nodes.Pins.Background; + + if(input) + { + if(Connected) pinColor = Pin::Colors[GetPin(it->second).Type]; + else if(NewConnectionNext) + { + Pin& Next = GetPin(*Mouse.NewConnection); + if(pin.Type == Next.Type || pin.Type == Pin::ANY) pinColor = Pin::Colors[Next.Type]; + } + } + + if(Pressed && !NewConnectionNext) pinColor = pinColor * 0.8f; + if(Filled) fillColor = pinColor; + + DrawList.AddCircleFilled(PinCenter, PinRadius, fillColor); + DrawList.AddCircle(PinCenter, PinRadius, pinColor, 0, BorderThickness); // Text const ImVec2 TextOffset = location + ImVec2((input ? HeaderHeight : -ImGui::CalcTextSize(pin.Name.c_str()).x / Camera.Zoom), 0); @@ -615,7 +688,7 @@ void ShaderGraph::DrawContextMenu() // Create Nodes ImVec2 position = ContextMenuPosition; - position = ImFloor(position / GridSize) * GridSize; + position = SnapToGrid(position); std::stack context; context.push(0); @@ -694,32 +767,39 @@ void ShaderGraph::DrawConnections() // Connections ============================================================================================================= - for(const Connection& connection : Connections) + for(const Connection& connection : State.Connections) { DrawConnection(connection.first, connection.second); } if(Mouse.NewConnection()) { - const Node& Node = *Nodes[Mouse.NewConnection->Node]; + const Node& Node = *State.Nodes[Mouse.NewConnection->Node]; const Pin& Pin = Mouse.NewConnection->Input ? Node.IO.Inputs[Mouse.NewConnection->Pin] : Node.IO.Outputs[Mouse.NewConnection->Pin]; + const auto Connection = State.Connections.find(*Mouse.NewConnection); + const bool Connected = Connection != State.Connections.end(); const ImVec2 NodePos = Node.Position + Padding; const ImVec2 InputRoot = GridToScreen(NodePos + ImVec2(Style.Nodes.Pins.Padding, HeaderHeight)); const ImVec2 OutputRoot = GridToScreen(NodePos + ImVec2(Node.Info.Size.x - HeaderHeight - Style.Nodes.Pins.Padding, HeaderHeight)); - const ImVec2 Root = (Mouse.NewConnection->Input ? InputRoot : OutputRoot) + ImVec2(0, HeaderHeight) * (Mouse.NewConnection->Pin + 0.5f); - const ImVec2 A = Root + Offset + ImVec2(Mouse.NewConnection->Input ? -PinRadius : PinRadius, 0); - const ImVec2 D = ImGui::GetMousePos(); + const ImVec2 PinLoc = Root + Offset + ImVec2(Mouse.NewConnection->Input ? -PinRadius : PinRadius, 0); + const ImVec2 MouseLoc = ImGui::GetMousePos(); + + const ImVec2 A = Mouse.NewConnection->Input ? MouseLoc : PinLoc; + const ImVec2 D = Mouse.NewConnection->Input ? PinLoc : MouseLoc; const float Off = BezierOffset(A, D); - const ImVec2 B = ImVec2(A.x + (Mouse.NewConnection->Input ? -Off : Off), A.y); - const ImVec2 C = ImVec2(D.x + (Mouse.NewConnection->Input ? Off : -Off), D.y); + const ImVec2 B = ImVec2(A.x + Off, A.y); + const ImVec2 C = ImVec2(D.x - Off, D.y); + + const ImColor Color = Mouse.NewConnection->Input && Connected + ? Pin::Colors[GetPin(Connection->second).Type] : Pin::Colors[Pin.Type]; DrawList.AddBezierCubic( A, B, C, D - , Pin::Colors[Pin.Type], Style.Nodes.Pins.Connections.Thickness / Camera.Zoom + , Color, Style.Nodes.Pins.Connections.Thickness / Camera.Zoom ); } @@ -751,12 +831,12 @@ void ShaderGraph::DrawConnection(const PinPtr& a, const PinPtr& b) const PinPtr& In = a.Input ? a : b; const PinPtr& Out = a.Input ? b : a; - const Node& OutNode = *Nodes[Out.Node]; + const Node& OutNode = *State.Nodes[Out.Node]; const ImVec2 OutSize = OutNode.Info.Size / Camera.Zoom; const ImVec2 OutPosition = CanvasCenter - Camera.Location + OutNode.Position / Camera.Zoom + Padding; const ImVec2 OutputLoc = OutPosition + ImVec2(OutSize.x - HeaderHeight - Style.Nodes.Pins.Padding / Camera.Zoom, HeaderHeight); - const Node& InNode = *Nodes[In.Node]; + const Node& InNode = *State.Nodes[In.Node]; const ImVec2 InPosition = CanvasCenter - Camera.Location + InNode.Position / Camera.Zoom + Padding; const ImVec2 InputLoc = InPosition + ImVec2(Style.Nodes.Pins.Padding / Camera.Zoom, HeaderHeight); @@ -794,11 +874,11 @@ void ShaderGraph::CreateConnection(const PinPtr& a, const PinPtr& b) if(a.Node == b.Node) return; const PinPtr& In = a.Input ? a : b; - const Node& InNode = *Nodes[In.Node]; + const Node& InNode = *State.Nodes[In.Node]; const Pin& InPin = InNode.IO.Inputs[In.Pin]; const PinPtr& Out = a.Input ? b : a; - const Node& OutNode = *Nodes[Out.Node]; + const Node& OutNode = *State.Nodes[Out.Node]; const Pin& OutPin = OutNode.IO.Outputs[Out.Pin]; // Make sure valid typing @@ -809,28 +889,28 @@ void ShaderGraph::CreateConnection(const PinPtr& a, const PinPtr& b) if(b.Input) EraseConnections(b); // Add New Connections - Connections.emplace(a, b); - Connections.emplace(b, a); + State.Connections.emplace(a, b); + State.Connections.emplace(b, a); } void ShaderGraph::EraseConnection(const PinPtr& a, const PinPtr& b) { - auto range = Connections.equal_range(a); + auto range = State.Connections.equal_range(a); for(auto it = range.first; it != range.second; ++it) { if(it->second == b) { - Connections.erase(it); + State.Connections.erase(it); break; } } - range = Connections.equal_range(b); + range = State.Connections.equal_range(b); for(auto it = range.first; it != range.second; ++it) { if(it->second == a) { - Connections.erase(it); + State.Connections.erase(it); break; } } @@ -838,41 +918,41 @@ void ShaderGraph::EraseConnection(const PinPtr& a, const PinPtr& b) void ShaderGraph::EraseConnections(const PinPtr& a) { - auto it = Connections.find(a); - while(it != Connections.end()) + auto it = State.Connections.find(a); + while(it != State.Connections.end()) { - auto range = Connections.equal_range(it->second); + auto range = State.Connections.equal_range(it->second); for(auto match = range.first; match != range.second; ++match) { if(match->second == a) { - Connections.erase(match); + State.Connections.erase(match); break; } } - Connections.erase(it); - it = Connections.find(a); + State.Connections.erase(it); + it = State.Connections.find(a); } } NodeId ShaderGraph::AddNode(Node* node) { - if(Erased.empty()) + if(State.Erased.empty()) { - Nodes.push_back(node); - return static_cast(Nodes.size() - 1); + State.Nodes.push_back(node); + return static_cast(State.Nodes.size() - 1); } - NodeId id = *Erased.begin(); - Nodes[id] = node; - Erased.erase(id); + NodeId id = *State.Erased.begin(); + State.Nodes[id] = node; + State.Erased.erase(id); return id; } void ShaderGraph::RemoveNode(NodeId id) { - Node* node = Nodes[id]; + Node* node = State.Nodes[id]; if(node->Info.Const) return; PinId i = 0; @@ -881,9 +961,9 @@ void ShaderGraph::RemoveNode(NodeId id) i = 0; for(const auto& pin : node->IO.Outputs) EraseConnections({ id, i++, false }); - Erased.insert(id); + State.Erased.insert(id); delete node; - Nodes[id] = nullptr; + State.Nodes[id] = nullptr; } void ShaderGraph::ClearClipboard() @@ -899,7 +979,7 @@ void ShaderGraph::Copy() // Helper for connections std::unordered_map clipboardTransform; - ImVec2 min = Nodes[*Mouse.Selected.begin()]->Position; + ImVec2 min = State.Nodes[*Mouse.Selected.begin()]->Position; // Reset Clipboard ClearClipboard(); @@ -908,7 +988,7 @@ void ShaderGraph::Copy() // Copy nodes for(auto id : Mouse.Selected) { - Node* node = Nodes[id]; + Node* node = State.Nodes[id]; clipboardTransform[id] = static_cast(Clipboard.Nodes.size()); Clipboard.Nodes.push_back(node->Copy(*this)); min = ImMin(node->Position, min); @@ -921,7 +1001,7 @@ void ShaderGraph::Copy() } // Copy connections - for(const Connection& connection : Connections) + for(const Connection& connection : State.Connections) { if(!(Mouse.Selected.contains(connection.first.Node) && Mouse.Selected.contains(connection.second.Node))) continue; @@ -939,7 +1019,7 @@ void ShaderGraph::Paste(const ImVec2& location) // Helper for connections const float GridSize = (Style.FontSize + Style.Grid.Lines.Padding); std::unordered_map clipboardTransform; - ImVec2 root = ImFloor(location / GridSize + ImVec2(0.5f, 0.5f)) * GridSize; + ImVec2 root = SnapToGrid(location); Mouse.Selected.clear(); // Paste the nodes @@ -947,7 +1027,7 @@ void ShaderGraph::Paste(const ImVec2& location) for(Node* node : Clipboard.Nodes) { NodeId index = clipboardTransform[id++] = AddNode(node->Copy(*this)); - Nodes[index]->Position += root; + State.Nodes[index]->Position += root; Mouse.Selected.insert(index); } @@ -970,6 +1050,17 @@ void ShaderGraph::EraseSelection() Mouse.Selected.clear(); } +void ShaderGraph::PushState() +{ + History.push(State); +} + +void ShaderGraph::PopState() +{ + State = History.top(); + History.pop(); +} + float ShaderGraph::BezierOffset(const ImVec2& out, const ImVec2& in) { const float HeaderHeight = Style.FontSize / Camera.Zoom; @@ -1004,6 +1095,18 @@ ImVec2 ShaderGraph::ScreenToGrid(const ImVec2& position) return (position - CanvasCenter + Camera.Location) * Camera.Zoom; } +ImVec2 ShaderGraph::SnapToGrid(const ImVec2& position) +{ + const float GridSize = (Style.FontSize + Style.Grid.Lines.Padding); + return ImFloor(position / GridSize) * GridSize; +} + +Pin& ShaderGraph::GetPin(const PinPtr &ptr) +{ + Node* node = State.Nodes[ptr.Node]; + return (ptr.Input ? node->IO.Inputs : node->IO.Outputs)[ptr.Pin]; +} + void ShaderGraph::Register(const std::filesystem::path& path, ConstructorPtr constructor) { const std::string name = path.filename().string(); @@ -1042,3 +1145,20 @@ void ShaderGraph::Register(const std::filesystem::path& path, ConstructorPtr con ContextMenu.Insert({ name, constructor }, node); } + +Inspector::Inspector() + : EditorWindow("Inspector", 0) + , Graph(nullptr) +{ +} + +void Inspector::DrawWindow() +{ + if(Graph->Mouse.Selected.size() != 1) + { + ImGui::Text("Selected %d nodes.", Graph->Mouse.Selected.size()); + return; + } + + Graph->State.Nodes[*Graph->Mouse.Selected.begin()]->Inspect(); +}