From 5a6ac92a48e627e61ed59850a1ab2c9f2cef1149 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sun, 16 Feb 2025 22:12:24 +0100 Subject: [PATCH] Using bits of the blueprints examples for the AnimGraphEditor. --- CMakeLists.txt | 14 +- media/fonts/Roboto-Medium.ttf | 0 src/AnimGraph/AnimGraphResource.cc | 3 - src/AnimGraph/AnimGraphResource.h | 2 - .../AnimGraphEditor.cc | 446 +++++++++++------- .../AnimGraphEditor.h | 0 src/AnimGraphEditor/utilities/builders.cpp | 310 ++++++++++++ src/AnimGraphEditor/utilities/builders.h | 76 +++ src/AnimGraphEditor/utilities/drawing.cpp | 252 ++++++++++ src/AnimGraphEditor/utilities/drawing.h | 12 + src/AnimGraphEditor/utilities/widgets.cpp | 16 + src/AnimGraphEditor/utilities/widgets.h | 13 + src/main.cc | 13 +- tests/AnimGraphEditorTests.cc | 2 +- tests/AnimGraphResourceTests.cc | 2 +- 15 files changed, 968 insertions(+), 193 deletions(-) create mode 100644 media/fonts/Roboto-Medium.ttf rename src/{AnimGraph => AnimGraphEditor}/AnimGraphEditor.cc (77%) rename src/{AnimGraph => AnimGraphEditor}/AnimGraphEditor.h (100%) create mode 100644 src/AnimGraphEditor/utilities/builders.cpp create mode 100644 src/AnimGraphEditor/utilities/builders.h create mode 100644 src/AnimGraphEditor/utilities/drawing.cpp create mode 100644 src/AnimGraphEditor/utilities/drawing.h create mode 100644 src/AnimGraphEditor/utilities/widgets.cpp create mode 100644 src/AnimGraphEditor/utilities/widgets.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 95ba565..66eb1a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,8 +80,14 @@ target_sources(AnimTestbed PRIVATE src/main.cc src/embedded_fonts.h src/SkinnedMeshRenderer.cc - src/AnimGraph/AnimGraphEditor.cc - src/AnimGraph/AnimGraphEditor.h + src/AnimGraphEditor/AnimGraphEditor.cc + src/AnimGraphEditor/AnimGraphEditor.h + src/AnimGraphEditor/utilities/builders.cpp + src/AnimGraphEditor/utilities/builders.h + src/AnimGraphEditor/utilities/drawing.cpp + src/AnimGraphEditor/utilities/drawing.h + src/AnimGraphEditor/utilities/widgets.cpp + src/AnimGraphEditor/utilities/widgets.h src/Camera.c src/SkinnedMesh.cc src/SkinnedMesh.h @@ -109,6 +115,10 @@ target_sources(AnimTestbed PRIVATE 3rdparty/imgui-node-editor/imgui_extra_math.inl 3rdparty/imgui-node-editor/crude_json.cpp 3rdparty/imgui-node-editor/crude_json.h + # ImGui Extension needed for blueprint nodes + 3rdparty/imgui/imgui_stacklayout.cpp + 3rdparty/imgui/imgui_stacklayout.h + 3rdparty/imgui/imgui_stacklayout_internal.h ) target_link_libraries(AnimTestbed AnimTestbedCode glfw ozz_base ozz_geometry ozz_animation ${OPENGL_LIBRARIES}) diff --git a/media/fonts/Roboto-Medium.ttf b/media/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..e69de29 diff --git a/src/AnimGraph/AnimGraphResource.cc b/src/AnimGraph/AnimGraphResource.cc index 5d5c235..4fe77f5 100644 --- a/src/AnimGraph/AnimGraphResource.cc +++ b/src/AnimGraph/AnimGraphResource.cc @@ -310,9 +310,6 @@ static bool sAnimGraphResourceBlendTreeFromJson( result_graph_resource->m_name = json_data["name"]; result_graph_resource->m_position[0] = json_data["position"][0]; result_graph_resource->m_position[1] = json_data["position"][1]; - assert(result_graph_resource->m_virtual_socket_accessor == nullptr); - result_graph_resource->m_virtual_socket_accessor = - AnimNodeDescriptorFactory("BlendTree", nullptr); // Load nodes for (size_t i = 0, n = json_data["nodes"].size(); i < n; i++) { diff --git a/src/AnimGraph/AnimGraphResource.h b/src/AnimGraph/AnimGraphResource.h index b23e32c..87e210d 100644 --- a/src/AnimGraph/AnimGraphResource.h +++ b/src/AnimGraph/AnimGraphResource.h @@ -56,8 +56,6 @@ struct BlendTreeResource { void CleanupNodes() { for (AnimNodeResource* node_resource : m_nodes) { - delete node_resource->m_virtual_socket_accessor; - node_resource->m_virtual_socket_accessor = nullptr; delete node_resource; } diff --git a/src/AnimGraph/AnimGraphEditor.cc b/src/AnimGraphEditor/AnimGraphEditor.cc similarity index 77% rename from src/AnimGraph/AnimGraphEditor.cc rename to src/AnimGraphEditor/AnimGraphEditor.cc index aa97a91..a5c954f 100644 --- a/src/AnimGraph/AnimGraphEditor.cc +++ b/src/AnimGraphEditor/AnimGraphEditor.cc @@ -7,11 +7,14 @@ #include #include "3rdparty/imgui-node-editor/imgui_node_editor.h" -#include "AnimGraphResource.h" +#include "AnimGraphEditor/utilities/builders.h" +#include "AnimGraphEditor/utilities/drawing.h" +#include "AnimGraphEditor/utilities/widgets.h" #include "SkinnedMesh.h" #include "imgui.h" #include "imnodes.h" #include "misc/cpp/imgui_stdlib.h" +#include "src/AnimGraph/AnimGraphResource.h" struct EditorState { AnimGraphResource* rootGraphResource = nullptr; @@ -25,6 +28,8 @@ struct EditorState { static EditorState sEditorState; +constexpr int cPinIconSize = 24; + ImNodesPinShape sGetSocketShapeFromSocketType(const SocketType& socket_type) { switch (socket_type) { case SocketType::SocketTypeAnimation: @@ -46,12 +51,47 @@ ImNodesPinShape sGetSocketShapeFromSocketType(const SocketType& socket_type) { return ImNodesPinShape_Quad; } +void DrawSocketIcon(const SocketType& socket_type, bool is_connected = false) { + ax::Drawing::IconType iconType; + ImColor color = ImColor(255, 255, 255); + color.Value.w = 1; + switch (socket_type) { + case SocketType::SocketTypeAnimation: + iconType = ax::Drawing::IconType::Flow; + break; + case SocketType::SocketTypeInt: + iconType = ax::Drawing::IconType::Circle; + break; + case SocketType::SocketTypeFloat: + iconType = ax::Drawing::IconType::Circle; + break; + case SocketType::SocketTypeVec3: + iconType = ax::Drawing::IconType::Circle; + break; + case SocketType::SocketTypeQuat: + iconType = ax::Drawing::IconType::Circle; + break; + case SocketType::SocketTypeBool: + iconType = ax::Drawing::IconType::Circle; + break; + default: + break; + } + + ax::Widgets::Icon( + ImVec2( + static_cast(cPinIconSize), + static_cast(cPinIconSize)), + iconType, + is_connected, + color, + ImColor(32, 32, 32, 1)); +} + bool NodeSocketEditor(Socket& socket) { bool modified = false; int mode_current = static_cast(socket.m_type); - if (ImGui::InputText("Name", &socket.m_name)) { - modified = true; - } + ImGui::Columns(3); if (ImGui::Combo( "Type", &mode_current, @@ -62,6 +102,13 @@ bool NodeSocketEditor(Socket& socket) { modified = true; } + ImGui::Columns(2); + if (ImGui::InputText("Name", &socket.m_name)) { + modified = true; + } + + ImGui::Columns(1); + return modified; } @@ -234,26 +281,39 @@ void AnimGraphEditorRenderSidebar( node_resource->m_virtual_socket_accessor->m_inputs; std::vector::iterator iter = outputs.begin(); - while (iter != outputs.end()) { - Socket& output = *iter; - ImGui::PushID(&output); - if (NodeSocketEditor(output)) { - AnimGraphResource* current_graph_resource = - sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]; - current_graph_resource->m_socket_accessor->m_inputs = outputs; - } + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + if (ImGui::BeginTable( + "split", + 2, + ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable)) { + while (iter != outputs.end()) { + Socket& socket = *iter; + ImGui::PushID(&socket); - if (ImGui::Button("X")) { - RemoveBlendTreeConnectionsForSocket( - blend_tree_resource, - node_resource, - output); - iter = outputs.erase(iter); - } else { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + bool node_open = ImGui::TreeNode(socket.m_name.c_str()); + ImGui::TableSetColumnIndex(1); + + if (node_open) { + int mode_current = static_cast(socket.m_type); + if (ImGui::Combo( + "Type", + &mode_current, + SocketTypeNames, + sizeof(SocketTypeNames) / sizeof(char*))) { + socket.m_type = static_cast(mode_current); + } + ImGui::TreePop(); + } + + ImGui::PopID(); iter++; } - ImGui::PopID(); + ImGui::EndTable(); } + ImGui::PopStyleVar(); if (ImGui::Button("+")) { AnimGraphResource* current_graph_resource = @@ -306,9 +366,8 @@ void AnimGraphEditorClear() { } if (sEditorState.rootGraphResource) { - delete sEditorState.rootGraphResource->m_virtual_socket_accessor; + delete sEditorState.rootGraphResource; } - delete sEditorState.rootGraphResource; sEditorState.rootGraphResource = new AnimGraphResource(); sEditorState.rootGraphResource->m_name = "Root"; @@ -324,12 +383,68 @@ void AnimGraphEditorClear() { sEditorState.hierarchyStackIndex = 0; } -void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) { - ax::NodeEditor::SetCurrentEditor(context); +void BlendTreeEditorNodePopup() { + const bool open_popup = ImGui::IsMouseReleased(ImGuiMouseButton_Right); - // - // Menu bar - // + if (open_popup && ImGui::IsWindowHovered()) { + ax::NodeEditor::Suspend(); + ImGui::OpenPopup("add node"); + ax::NodeEditor::Resume(); + sEditorState.mousePopupStart = ImGui::GetMousePos(); + } + + ax::NodeEditor::Suspend(); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); + if (ImGui::BeginPopup("add node")) { + std::string node_type_name = ""; + if (ImGui::MenuItem("AnimSampler")) { + node_type_name = "AnimSampler"; + } + + if (ImGui::MenuItem("Blend2")) { + node_type_name = "Blend2"; + } + + if (ImGui::MenuItem("SpeedScale")) { + node_type_name = "SpeedScale"; + } + + if (ImGui::MenuItem("LockTranslationNode")) { + node_type_name = "LockTranslationNode"; + } + + if (ImGui::MenuItem("MathAddNode")) { + node_type_name = "MathAddNode"; + } + + if (ImGui::MenuItem("MathFloatToVec3Node")) { + node_type_name = "MathFloatToVec3Node"; + } + + if (ImGui::MenuItem("ConstScalarNode")) { + node_type_name = "ConstScalarNode"; + } + + if (ImGui::MenuItem("BlendTree")) { + node_type_name = "BlendTree"; + } + + if (!node_type_name.empty()) { + AnimNodeResource* node_resource = AnimNodeResourceFactory(node_type_name); + ax::NodeEditor::SetNodePosition( + ax::NodeEditor::NodeId(node_resource), + sEditorState.mousePopupStart); + sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex] + ->m_blend_tree_resource.AddNode(node_resource); + } + + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + ax::NodeEditor::Resume(); +} + +void AnimGraphEditorMenuBar() { ImGui::BeginMenuBar(); if (ImGui::Button("Save")) { sEditorState.rootGraphResource->SaveToFile("editor_graph.json"); @@ -358,10 +473,9 @@ void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) { } ImGui::EndMenuBar(); +} - // - // Breadcrumb navigation - // +void AnimGraphEditorBreadcrumbNavigation() { for (size_t i = 0, n = sEditorState.hierarchyStack.size(); i < n; i++) { AnimGraphResource* graph_resource = dynamic_cast(sEditorState.hierarchyStack[i]); @@ -389,100 +503,10 @@ void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) { ImGui::SameLine(); } } +} - ImGui::Columns(2); - - // - // Node editor canvas - // - ax::NodeEditor::Begin("Graph Editor"); - - AnimGraphResource* current_graph = - sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]; - BlendTreeResource& current_blend_tree = current_graph->m_blend_tree_resource; - - for (size_t node_index = 0, n = current_blend_tree.GetNumNodes(); - node_index < n; - node_index++) { - AnimNodeResource* node_resource = current_blend_tree.GetNode(node_index); - ax::NodeEditor::NodeId node_id(node_resource); - - if (sEditorState.isGraphLoadedThisFrame) { - ax::NodeEditor::SetNodePosition( - node_id, - ImVec2(node_resource->m_position[0], node_resource->m_position[1])); - } - ax::NodeEditor::BeginNode(node_id); - ImGui::Text("%s", node_resource->m_node_type_name.c_str()); - - // Inputs - std::vector node_inputs = - current_blend_tree.GetNodeInputSockets(node_resource); - for (size_t j = 0, ni = node_inputs.size(); j < ni; j++) { - Socket& socket = node_inputs[j]; - ax::NodeEditor::BeginPin( - NodeIndexAndSocketIndexToInputPinId( - static_cast(node_index), - static_cast(j)), - ax::NodeEditor::PinKind::Input); - ImGui::Text("%s", socket.m_name.c_str()); - ax::NodeEditor::EndPin(); - } - - // Outputs - std::vector node_outputs = - current_blend_tree.GetNodeOutputSockets(node_resource); - for (size_t j = 0, ni = node_outputs.size(); j < ni; j++) { - Socket& socket = node_outputs[j]; - ax::NodeEditor::BeginPin( - NodeIndexAndSocketIndexToOutputPinId( - static_cast(node_index), - static_cast(j)), - ax::NodeEditor::PinKind::Output); - ImGui::Text("%s", socket.m_name.c_str()); - ax::NodeEditor::EndPin(); - } - - ax::NodeEditor::EndNode(); - - ImVec2 node_position = ax::NodeEditor::GetNodePosition(node_id); - node_resource->m_position[0] = node_position.x; - node_resource->m_position[1] = node_position.y; - } - - int link_id = 0; - for (size_t connection_id = 0, n = current_blend_tree.GetNumConnections(); - connection_id < n; - connection_id++) { - const BlendTreeConnectionResource* connection_resource = - current_blend_tree.GetConnection(connection_id); - - const AnimNodeResource* source_node_resource = - current_blend_tree.GetNode(connection_resource->source_node_index); - int source_socket_index = - source_node_resource->m_virtual_socket_accessor->GetOutputIndex( - connection_resource->source_socket_name.c_str()); - - const AnimNodeResource* target_node_resource = - current_blend_tree.GetNode(connection_resource->target_node_index); - int target_socket_index = - target_node_resource->m_virtual_socket_accessor->GetInputIndex( - connection_resource->target_socket_name.c_str()); - - int source_socket_pin_id = NodeIndexAndSocketIndexToOutputPinId( - static_cast(connection_resource->source_node_index), - source_socket_index); - int target_socket_pin_id = NodeIndexAndSocketIndexToInputPinId( - static_cast(connection_resource->target_node_index), - target_socket_index); - - ax::NodeEditor::Link( - ax::NodeEditor::LinkId(connection_resource), - source_socket_pin_id, - target_socket_pin_id); - } - - // Create Connections +void HandleConnectionCreation( + BlendTreeResource& current_blend_tree) { // Create Connections if (ax::NodeEditor::BeginCreate()) { ax::NodeEditor::PinId input_pin_id, output_pin_id; if (ax::NodeEditor::QueryNewLink(&input_pin_id, &output_pin_id)) { @@ -549,69 +573,137 @@ void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) { } } ax::NodeEditor::EndCreate(); +} - // Popup menu - { - const bool open_popup = ImGui::IsMouseReleased(ImGuiMouseButton_Right); +void BlendTreeRenderNodes( + BlendTreeResource& current_blend_tree, + ax::NodeEditor::Utilities::BlueprintNodeBuilder& builder) { + for (size_t node_index = 0, n = current_blend_tree.GetNumNodes(); + node_index < n; + node_index++) { + AnimNodeResource* node_resource = current_blend_tree.GetNode(node_index); - if (open_popup && ImGui::IsWindowHovered()) { - ax::NodeEditor::Suspend(); - ImGui::OpenPopup("add node"); - ax::NodeEditor::Resume(); - sEditorState.mousePopupStart = ImGui::GetMousePos(); + ax::NodeEditor::NodeId node_id(node_resource); + + builder.Begin(node_id); + + if (sEditorState.isGraphLoadedThisFrame) { + ax::NodeEditor::SetNodePosition( + node_id, + ImVec2(node_resource->m_position[0], node_resource->m_position[1])); } - ax::NodeEditor::Suspend(); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); - if (ImGui::BeginPopup("add node")) { - std::string node_type_name = ""; - if (ImGui::MenuItem("AnimSampler")) { - node_type_name = "AnimSampler"; - } + builder.Header(); + ImGui::Text("%s", node_resource->m_node_type_name.c_str()); + ImGui::Spring(0); + builder.EndHeader(); - if (ImGui::MenuItem("Blend2")) { - node_type_name = "Blend2"; - } + // Inputs + std::vector node_inputs = + current_blend_tree.GetNodeInputSockets(node_resource); + for (size_t j = 0, ni = node_inputs.size(); j < ni; j++) { + Socket& socket = node_inputs[j]; + builder.Input(NodeIndexAndSocketIndexToInputPinId( + static_cast(node_index), + static_cast(j))); - if (ImGui::MenuItem("SpeedScale")) { - node_type_name = "SpeedScale"; - } + DrawSocketIcon( + socket.m_type, + current_blend_tree.IsSocketConnected(node_resource, socket.m_name)); + ImGui::Spring(0); - if (ImGui::MenuItem("LockTranslationNode")) { - node_type_name = "LockTranslationNode"; - } - - if (ImGui::MenuItem("MathAddNode")) { - node_type_name = "MathAddNode"; - } - - if (ImGui::MenuItem("MathFloatToVec3Node")) { - node_type_name = "MathFloatToVec3Node"; - } - - if (ImGui::MenuItem("ConstScalarNode")) { - node_type_name = "ConstScalarNode"; - } - - if (ImGui::MenuItem("BlendTree")) { - node_type_name = "BlendTree"; - } - - if (!node_type_name.empty()) { - AnimNodeResource* node_resource = - AnimNodeResourceFactory(node_type_name); - ax::NodeEditor::SetNodePosition( - ax::NodeEditor::NodeId(node_resource), - sEditorState.mousePopupStart); - sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex] - ->m_blend_tree_resource.AddNode(node_resource); - } - - ImGui::EndPopup(); + //ImGui::PushItemWidth(100.0f); + ImGui::Text("%s", socket.m_name.c_str()); + //ImGui::PopItemWidth(); + builder.EndInput(); } - ImGui::PopStyleVar(); - ax::NodeEditor::Resume(); + + // Outputs + std::vector node_outputs = + current_blend_tree.GetNodeOutputSockets(node_resource); + for (size_t j = 0, ni = node_outputs.size(); j < ni; j++) { + Socket& socket = node_outputs[j]; + builder.Output(NodeIndexAndSocketIndexToOutputPinId( + static_cast(node_index), + static_cast(j))); + + ImGui::Text("%s", socket.m_name.c_str()); + + ImGui::Spring(0); + DrawSocketIcon( + socket.m_type, + current_blend_tree.IsSocketConnected(node_resource, socket.m_name)); + + builder.EndOutput(); + } + + ImVec2 node_position = ax::NodeEditor::GetNodePosition(node_id); + node_resource->m_position[0] = node_position.x; + node_resource->m_position[1] = node_position.y; + + builder.End(); } +} + +void BlendTreeRenderConnections(BlendTreeResource& current_blend_tree) { + for (size_t connection_id = 0, n = current_blend_tree.GetNumConnections(); + connection_id < n; + connection_id++) { + const BlendTreeConnectionResource* connection_resource = + current_blend_tree.GetConnection(connection_id); + + const AnimNodeResource* source_node_resource = + current_blend_tree.GetNode(connection_resource->source_node_index); + int source_socket_index = + source_node_resource->m_virtual_socket_accessor->GetOutputIndex( + connection_resource->source_socket_name.c_str()); + + const AnimNodeResource* target_node_resource = + current_blend_tree.GetNode(connection_resource->target_node_index); + int target_socket_index = + target_node_resource->m_virtual_socket_accessor->GetInputIndex( + connection_resource->target_socket_name.c_str()); + + int source_socket_pin_id = NodeIndexAndSocketIndexToOutputPinId( + static_cast(connection_resource->source_node_index), + source_socket_index); + int target_socket_pin_id = NodeIndexAndSocketIndexToInputPinId( + static_cast(connection_resource->target_node_index), + target_socket_index); + + ax::NodeEditor::Link( + ax::NodeEditor::LinkId(connection_resource), + source_socket_pin_id, + target_socket_pin_id); + } +} +void AnimGraphEditorUpdate(ax::NodeEditor::EditorContext* context) { + ax::NodeEditor::SetCurrentEditor(context); + + AnimGraphEditorMenuBar(); + + AnimGraphEditorBreadcrumbNavigation(); + + ImGui::Columns(2); + + // + // Node editor canvas + // + ax::NodeEditor::Begin("Graph Editor"); + + AnimGraphResource* current_graph = + sEditorState.hierarchyStack[sEditorState.hierarchyStackIndex]; + BlendTreeResource& current_blend_tree = current_graph->m_blend_tree_resource; + + ax::NodeEditor::Utilities::BlueprintNodeBuilder builder; + + BlendTreeRenderNodes(current_blend_tree, builder); + + BlendTreeRenderConnections(current_blend_tree); + + HandleConnectionCreation(current_blend_tree); + + BlendTreeEditorNodePopup(); ax::NodeEditor::End(); diff --git a/src/AnimGraph/AnimGraphEditor.h b/src/AnimGraphEditor/AnimGraphEditor.h similarity index 100% rename from src/AnimGraph/AnimGraphEditor.h rename to src/AnimGraphEditor/AnimGraphEditor.h diff --git a/src/AnimGraphEditor/utilities/builders.cpp b/src/AnimGraphEditor/utilities/builders.cpp new file mode 100644 index 0000000..e8ae020 --- /dev/null +++ b/src/AnimGraphEditor/utilities/builders.cpp @@ -0,0 +1,310 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +# define IMGUI_DEFINE_MATH_OPERATORS +# include "builders.h" +# include + + +//------------------------------------------------------------------------------ +namespace ed = ax::NodeEditor; +namespace util = ax::NodeEditor::Utilities; + +util::BlueprintNodeBuilder::BlueprintNodeBuilder(ImTextureID texture, int textureWidth, int textureHeight): + HeaderTextureId(texture), + HeaderTextureWidth(textureWidth), + HeaderTextureHeight(textureHeight), + CurrentNodeId(0), + CurrentStage(Stage::Invalid), + HasHeader(false) +{ +} + +void util::BlueprintNodeBuilder::Begin(ed::NodeId id) +{ + HasHeader = false; + HeaderMin = HeaderMax = ImVec2(); + + ed::PushStyleVar(StyleVar_NodePadding, ImVec4(8, 4, 8, 8)); + + ed::BeginNode(id); + + ImGui::PushID(id.AsPointer()); + CurrentNodeId = id; + + SetStage(Stage::Begin); +} + +void util::BlueprintNodeBuilder::End() +{ + SetStage(Stage::End); + + ed::EndNode(); + + if (ImGui::IsItemVisible()) + { + auto alpha = static_cast(255 * ImGui::GetStyle().Alpha); + + auto drawList = ed::GetNodeBackgroundDrawList(CurrentNodeId); + + const auto halfBorderWidth = ed::GetStyle().NodeBorderWidth * 0.5f; + + auto headerColor = IM_COL32(0, 0, 0, alpha) | (HeaderColor & IM_COL32(255, 255, 255, 0)); + if ((HeaderMax.x > HeaderMin.x) && (HeaderMax.y > HeaderMin.y) && HeaderTextureId) + { + const auto uv = ImVec2( + (HeaderMax.x - HeaderMin.x) / (float)(4.0f * HeaderTextureWidth), + (HeaderMax.y - HeaderMin.y) / (float)(4.0f * HeaderTextureHeight)); + + drawList->AddImageRounded(HeaderTextureId, + HeaderMin - ImVec2(8 - halfBorderWidth, 4 - halfBorderWidth), + HeaderMax + ImVec2(8 - halfBorderWidth, 0), + ImVec2(0.0f, 0.0f), uv, +#if IMGUI_VERSION_NUM > 18101 + headerColor, GetStyle().NodeRounding, ImDrawFlags_RoundCornersTop); +#else + headerColor, GetStyle().NodeRounding, 1 | 2); +#endif + + if (ContentMin.y > HeaderMax.y) + { + drawList->AddLine( + ImVec2(HeaderMin.x - (8 - halfBorderWidth), HeaderMax.y - 0.5f), + ImVec2(HeaderMax.x + (8 - halfBorderWidth), HeaderMax.y - 0.5f), + ImColor(255, 255, 255, 96 * alpha / (3 * 255)), 1.0f); + } + } + } + + CurrentNodeId = 0; + + ImGui::PopID(); + + ed::PopStyleVar(); + + SetStage(Stage::Invalid); +} + +void util::BlueprintNodeBuilder::Header(const ImVec4& color) +{ + HeaderColor = ImColor(color); + SetStage(Stage::Header); +} + +void util::BlueprintNodeBuilder::EndHeader() +{ + SetStage(Stage::Content); +} + +void util::BlueprintNodeBuilder::Input(ed::PinId id) +{ + if (CurrentStage == Stage::Begin) + SetStage(Stage::Content); + + const auto applyPadding = (CurrentStage == Stage::Input); + + SetStage(Stage::Input); + + if (applyPadding) + ImGui::Spring(0); + + Pin(id, PinKind::Input); + + ImGui::BeginHorizontal(id.AsPointer()); +} + +void util::BlueprintNodeBuilder::EndInput() +{ + ImGui::EndHorizontal(); + + EndPin(); +} + +void util::BlueprintNodeBuilder::Middle() +{ + if (CurrentStage == Stage::Begin) + SetStage(Stage::Content); + + SetStage(Stage::Middle); +} + +void util::BlueprintNodeBuilder::Output(ed::PinId id) +{ + if (CurrentStage == Stage::Begin) + SetStage(Stage::Content); + + const auto applyPadding = (CurrentStage == Stage::Output); + + SetStage(Stage::Output); + + if (applyPadding) + ImGui::Spring(0); + + Pin(id, PinKind::Output); + + ImGui::BeginHorizontal(id.AsPointer()); +} + +void util::BlueprintNodeBuilder::EndOutput() +{ + ImGui::EndHorizontal(); + + EndPin(); +} + +bool util::BlueprintNodeBuilder::SetStage(Stage stage) +{ + if (stage == CurrentStage) + return false; + + auto oldStage = CurrentStage; + CurrentStage = stage; + + ImVec2 cursor; + switch (oldStage) + { + case Stage::Begin: + break; + + case Stage::Header: + ImGui::EndHorizontal(); + HeaderMin = ImGui::GetItemRectMin(); + HeaderMax = ImGui::GetItemRectMax(); + + // spacing between header and content + ImGui::Spring(0, ImGui::GetStyle().ItemSpacing.y * 2.0f); + + break; + + case Stage::Content: + break; + + case Stage::Input: + ed::PopStyleVar(2); + + ImGui::Spring(1, 0); + ImGui::EndVertical(); + + // #debug + // ImGui::GetWindowDrawList()->AddRect( + // ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255, 0, 0, 255)); + + break; + + case Stage::Middle: + ImGui::EndVertical(); + + // #debug + // ImGui::GetWindowDrawList()->AddRect( + // ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255, 0, 0, 255)); + + break; + + case Stage::Output: + ed::PopStyleVar(2); + + ImGui::Spring(1, 0); + ImGui::EndVertical(); + + // #debug + // ImGui::GetWindowDrawList()->AddRect( + // ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255, 0, 0, 255)); + + break; + + case Stage::End: + break; + + case Stage::Invalid: + break; + } + + switch (stage) + { + case Stage::Begin: + ImGui::BeginVertical("node"); + break; + + case Stage::Header: + HasHeader = true; + + ImGui::BeginHorizontal("header"); + break; + + case Stage::Content: + if (oldStage == Stage::Begin) + ImGui::Spring(0); + + ImGui::BeginHorizontal("content"); + ImGui::Spring(0, 0); + break; + + case Stage::Input: + ImGui::BeginVertical("inputs", ImVec2(0, 0), 0.0f); + + ed::PushStyleVar(ed::StyleVar_PivotAlignment, ImVec2(0, 0.5f)); + ed::PushStyleVar(ed::StyleVar_PivotSize, ImVec2(0, 0)); + + if (!HasHeader) + ImGui::Spring(1, 0); + break; + + case Stage::Middle: + ImGui::Spring(1); + ImGui::BeginVertical("middle", ImVec2(0, 0), 1.0f); + break; + + case Stage::Output: + if (oldStage == Stage::Middle || oldStage == Stage::Input) + ImGui::Spring(1); + else + ImGui::Spring(1, 0); + ImGui::BeginVertical("outputs", ImVec2(0, 0), 1.0f); + + ed::PushStyleVar(ed::StyleVar_PivotAlignment, ImVec2(1.0f, 0.5f)); + ed::PushStyleVar(ed::StyleVar_PivotSize, ImVec2(0, 0)); + + if (!HasHeader) + ImGui::Spring(1, 0); + break; + + case Stage::End: + if (oldStage == Stage::Input) + ImGui::Spring(1, 0); + if (oldStage != Stage::Begin) + ImGui::EndHorizontal(); + ContentMin = ImGui::GetItemRectMin(); + ContentMax = ImGui::GetItemRectMax(); + + //ImGui::Spring(0); + ImGui::EndVertical(); + NodeMin = ImGui::GetItemRectMin(); + NodeMax = ImGui::GetItemRectMax(); + break; + + case Stage::Invalid: + break; + } + + return true; +} + +void util::BlueprintNodeBuilder::Pin(ed::PinId id, ed::PinKind kind) +{ + ed::BeginPin(id, kind); +} + +void util::BlueprintNodeBuilder::EndPin() +{ + ed::EndPin(); + + // #debug + // ImGui::GetWindowDrawList()->AddRectFilled( + // ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255, 0, 0, 64)); +} diff --git a/src/AnimGraphEditor/utilities/builders.h b/src/AnimGraphEditor/utilities/builders.h new file mode 100644 index 0000000..a00d426 --- /dev/null +++ b/src/AnimGraphEditor/utilities/builders.h @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +// LICENSE +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// CREDITS +// Written by Michal Cichon +//------------------------------------------------------------------------------ +#pragma once + +//------------------------------------------------------------------------------ +#include "3rdparty/imgui-node-editor/imgui_node_editor.h" + +//------------------------------------------------------------------------------ +namespace ax { +namespace NodeEditor { +namespace Utilities { + +//------------------------------------------------------------------------------ +struct BlueprintNodeBuilder { + BlueprintNodeBuilder( + ImTextureID texture = 0, + int textureWidth = 0, + int textureHeight = 0); + + void Begin(NodeId id); + void End(); + + void Header(const ImVec4& color = ImVec4(1, 1, 1, 1)); + void EndHeader(); + + void Input(PinId id); + void EndInput(); + + void Middle(); + + void Output(PinId id); + void EndOutput(); + + private: + enum class Stage { + Invalid, + Begin, + Header, + Content, + Input, + Output, + Middle, + End + }; + + bool SetStage(Stage stage); + + void Pin(PinId id, ax::NodeEditor::PinKind kind); + void EndPin(); + + ImTextureID HeaderTextureId; + int HeaderTextureWidth; + int HeaderTextureHeight; + NodeId CurrentNodeId; + Stage CurrentStage; + ImU32 HeaderColor; + ImVec2 NodeMin; + ImVec2 NodeMax; + ImVec2 HeaderMin; + ImVec2 HeaderMax; + ImVec2 ContentMin; + ImVec2 ContentMax; + bool HasHeader; +}; + +//------------------------------------------------------------------------------ +} // namespace Utilities +} // namespace NodeEditor +} // namespace ax \ No newline at end of file diff --git a/src/AnimGraphEditor/utilities/drawing.cpp b/src/AnimGraphEditor/utilities/drawing.cpp new file mode 100644 index 0000000..1f255ee --- /dev/null +++ b/src/AnimGraphEditor/utilities/drawing.cpp @@ -0,0 +1,252 @@ +# define IMGUI_DEFINE_MATH_OPERATORS +# include "drawing.h" +# include + +void ax::Drawing::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor) +{ + auto rect = ImRect(a, b); + auto rect_x = rect.Min.x; + auto rect_y = rect.Min.y; + auto rect_w = rect.Max.x - rect.Min.x; + auto rect_h = rect.Max.y - rect.Min.y; + auto rect_center_x = (rect.Min.x + rect.Max.x) * 0.5f; + auto rect_center_y = (rect.Min.y + rect.Max.y) * 0.5f; + auto rect_center = ImVec2(rect_center_x, rect_center_y); + const auto outline_scale = rect_w / 24.0f; + const auto extra_segments = static_cast(2 * outline_scale); // for full circle + + if (type == IconType::Flow) + { + const auto origin_scale = rect_w / 24.0f; + + const auto offset_x = 1.0f * origin_scale; + const auto offset_y = 0.0f * origin_scale; + const auto margin = (filled ? 2.0f : 2.0f) * origin_scale; + const auto rounding = 0.1f * origin_scale; + const auto tip_round = 0.7f; // percentage of triangle edge (for tip) + //const auto edge_round = 0.7f; // percentage of triangle edge (for corner) + const auto canvas = ImRect( + rect.Min.x + margin + offset_x, + rect.Min.y + margin + offset_y, + rect.Max.x - margin + offset_x, + rect.Max.y - margin + offset_y); + const auto canvas_x = canvas.Min.x; + const auto canvas_y = canvas.Min.y; + const auto canvas_w = canvas.Max.x - canvas.Min.x; + const auto canvas_h = canvas.Max.y - canvas.Min.y; + + const auto left = canvas_x + canvas_w * 0.5f * 0.3f; + const auto right = canvas_x + canvas_w - canvas_w * 0.5f * 0.3f; + const auto top = canvas_y + canvas_h * 0.5f * 0.2f; + const auto bottom = canvas_y + canvas_h - canvas_h * 0.5f * 0.2f; + const auto center_y = (top + bottom) * 0.5f; + //const auto angle = AX_PI * 0.5f * 0.5f * 0.5f; + + const auto tip_top = ImVec2(canvas_x + canvas_w * 0.5f, top); + const auto tip_right = ImVec2(right, center_y); + const auto tip_bottom = ImVec2(canvas_x + canvas_w * 0.5f, bottom); + + drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding)); + drawList->PathBezierCubicCurveTo( + ImVec2(left, top), + ImVec2(left, top), + ImVec2(left, top) + ImVec2(rounding, 0)); + drawList->PathLineTo(tip_top); + drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round); + drawList->PathBezierCubicCurveTo( + tip_right, + tip_right, + tip_bottom + (tip_right - tip_bottom) * tip_round); + drawList->PathLineTo(tip_bottom); + drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0)); + drawList->PathBezierCubicCurveTo( + ImVec2(left, bottom), + ImVec2(left, bottom), + ImVec2(left, bottom) - ImVec2(0, rounding)); + + if (!filled) + { + if (innerColor & 0xFF000000) + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } + else + drawList->PathFillConvex(color); + } + else + { + auto triangleStart = rect_center_x + 0.32f * rect_w; + + auto rect_offset = -static_cast(rect_w * 0.25f * 0.25f); + + rect.Min.x += rect_offset; + rect.Max.x += rect_offset; + rect_x += rect_offset; + rect_center_x += rect_offset * 0.5f; + rect_center.x += rect_offset * 0.5f; + + if (type == IconType::Circle) + { + const auto c = rect_center; + + if (!filled) + { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + + if (innerColor & 0xFF000000) + drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments); + drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale); + } + else + { + drawList->AddCircleFilled(c, 0.5f * rect_w / 2.0f, color, 12 + extra_segments); + } + } + + if (type == IconType::Square) + { + if (filled) + { + const auto r = 0.5f * rect_w / 2.0f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + +#if IMGUI_VERSION_NUM > 18101 + drawList->AddRectFilled(p0, p1, color, 0, ImDrawFlags_RoundCornersAll); +#else + drawList->AddRectFilled(p0, p1, color, 0, 15); +#endif + } + else + { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + { +#if IMGUI_VERSION_NUM > 18101 + drawList->AddRectFilled(p0, p1, innerColor, 0, ImDrawFlags_RoundCornersAll); +#else + drawList->AddRectFilled(p0, p1, innerColor, 0, 15); +#endif + } + +#if IMGUI_VERSION_NUM > 18101 + drawList->AddRect(p0, p1, color, 0, ImDrawFlags_RoundCornersAll, 2.0f * outline_scale); +#else + drawList->AddRect(p0, p1, color, 0, 15, 2.0f * outline_scale); +#endif + } + } + + if (type == IconType::Grid) + { + const auto r = 0.5f * rect_w / 2.0f; + const auto w = ceilf(r / 3.0f); + + const auto baseTl = ImVec2(floorf(rect_center_x - w * 2.5f), floorf(rect_center_y - w * 2.5f)); + const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w)); + + auto tl = baseTl; + auto br = baseBr; + for (int i = 0; i < 3; ++i) + { + tl.x = baseTl.x; + br.x = baseBr.x; + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + if (i != 1 || filled) + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + drawList->AddRectFilled(tl, br, color); + + tl.y += w * 2; + br.y += w * 2; + } + + triangleStart = br.x + w + 1.0f / 24.0f * rect_w; + } + + if (type == IconType::RoundSquare) + { + if (filled) + { + const auto r = 0.5f * rect_w / 2.0f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + +#if IMGUI_VERSION_NUM > 18101 + drawList->AddRectFilled(p0, p1, color, cr, ImDrawFlags_RoundCornersAll); +#else + drawList->AddRectFilled(p0, p1, color, cr, 15); +#endif + } + else + { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + { +#if IMGUI_VERSION_NUM > 18101 + drawList->AddRectFilled(p0, p1, innerColor, cr, ImDrawFlags_RoundCornersAll); +#else + drawList->AddRectFilled(p0, p1, innerColor, cr, 15); +#endif + } + +#if IMGUI_VERSION_NUM > 18101 + drawList->AddRect(p0, p1, color, cr, ImDrawFlags_RoundCornersAll, 2.0f * outline_scale); +#else + drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale); +#endif + } + } + else if (type == IconType::Diamond) + { + if (filled) + { + const auto r = 0.607f * rect_w / 2.0f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2( 0, -r)); + drawList->PathLineTo(c + ImVec2( r, 0)); + drawList->PathLineTo(c + ImVec2( 0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + drawList->PathFillConvex(color); + } + else + { + const auto r = 0.607f * rect_w / 2.0f - 0.5f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2( 0, -r)); + drawList->PathLineTo(c + ImVec2( r, 0)); + drawList->PathLineTo(c + ImVec2( 0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + + if (innerColor & 0xFF000000) + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } + } + else + { + const auto triangleTip = triangleStart + rect_w * (0.45f - 0.32f); + + drawList->AddTriangleFilled( + ImVec2(ceilf(triangleTip), rect_y + rect_h * 0.5f), + ImVec2(triangleStart, rect_center_y + 0.15f * rect_h), + ImVec2(triangleStart, rect_center_y - 0.15f * rect_h), + color); + } + } +} diff --git a/src/AnimGraphEditor/utilities/drawing.h b/src/AnimGraphEditor/utilities/drawing.h new file mode 100644 index 0000000..4387c58 --- /dev/null +++ b/src/AnimGraphEditor/utilities/drawing.h @@ -0,0 +1,12 @@ +# pragma once +# include + +namespace ax { +namespace Drawing { + +enum class IconType: ImU32 { Flow, Circle, Square, Grid, RoundSquare, Diamond }; + +void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor); + +} // namespace Drawing +} // namespace ax \ No newline at end of file diff --git a/src/AnimGraphEditor/utilities/widgets.cpp b/src/AnimGraphEditor/utilities/widgets.cpp new file mode 100644 index 0000000..202c8e2 --- /dev/null +++ b/src/AnimGraphEditor/utilities/widgets.cpp @@ -0,0 +1,16 @@ +# define IMGUI_DEFINE_MATH_OPERATORS +# include "widgets.h" +# include + +void ax::Widgets::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color/* = ImVec4(1, 1, 1, 1)*/, const ImVec4& innerColor/* = ImVec4(0, 0, 0, 0)*/) +{ + if (ImGui::IsRectVisible(size)) + { + auto cursorPos = ImGui::GetCursorScreenPos(); + auto drawList = ImGui::GetWindowDrawList(); + ax::Drawing::DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor)); + } + + ImGui::Dummy(size); +} + diff --git a/src/AnimGraphEditor/utilities/widgets.h b/src/AnimGraphEditor/utilities/widgets.h new file mode 100644 index 0000000..09b3946 --- /dev/null +++ b/src/AnimGraphEditor/utilities/widgets.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include "drawing.h" + +namespace ax { +namespace Widgets { + +using Drawing::IconType; + +void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0)); + +} // namespace Widgets +} // namespace ax \ No newline at end of file diff --git a/src/main.cc b/src/main.cc index 123f265..b868b91 100644 --- a/src/main.cc +++ b/src/main.cc @@ -22,13 +22,13 @@ #include "3rdparty/json/json.hpp" #include "AnimGraph/AnimGraphBlendTree.h" #include "AnimGraph/AnimGraphData.h" +#include "AnimGraphEditor/AnimGraphEditor.h" #include "Camera.h" #include "GLFW/glfw3.h" #include "SkinnedMesh.h" #include "SkinnedMeshRenderer.h" #include "SkinnedMeshResource.h" #include "embedded_fonts.h" -#include "src/AnimGraph/AnimGraphEditor.h" const int Width = 1024; const int Height = 768; @@ -159,12 +159,6 @@ struct Viewport { offscreen_pass_desc.label = "offscreen-pass"; this->pass = sg_make_pass(&offscreen_pass_desc); - - sg_pipeline_desc gl_pipeline_desc = { - .depth = {.compare = SG_COMPAREFUNC_LESS_EQUAL, .write_enabled = true}, - .cull_mode = SG_CULLMODE_BACK, - .sample_count = cMSAASampleCount}; - // this->pip = sg_make_pipeline(gl_pipeline_desc); } }; @@ -1063,7 +1057,12 @@ int main() { /* cleanup */ ax::NodeEditor::DestroyEditor(gApplicationConfig.graph_editor.context); ImNodes::DestroyContext(); + + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + sgl_shutdown(); sg_shutdown(); glfwTerminate(); diff --git a/tests/AnimGraphEditorTests.cc b/tests/AnimGraphEditorTests.cc index 04244ec..22d1910 100644 --- a/tests/AnimGraphEditorTests.cc +++ b/tests/AnimGraphEditorTests.cc @@ -1,4 +1,4 @@ -#include "AnimGraph/AnimGraphEditor.h" +#include "AnimGraphEditor/AnimGraphEditor.h" #include "catch.hpp" TEST_CASE("Node Socket To InputPin Conversion", "[animGraphEditor]") { diff --git a/tests/AnimGraphResourceTests.cc b/tests/AnimGraphResourceTests.cc index 3a60648..cb60f0f 100644 --- a/tests/AnimGraphResourceTests.cc +++ b/tests/AnimGraphResourceTests.cc @@ -3,9 +3,9 @@ // #include "AnimGraph/AnimGraphBlendTree.h" -#include "AnimGraph/AnimGraphEditor.h" #include "AnimGraph/AnimGraphNodes.h" #include "AnimGraph/AnimGraphResource.h" +#include "AnimGraphEditor/AnimGraphEditor.h" #include "catch.hpp" #include "ozz/base/io/archive.h" #include "ozz/base/io/stream.h"