feat(shaders_lab): visual node editor (imgui-node-editor) + multi-source

- cpp/vendor/imgui-node-editor: vendorized thedmd/imgui-node-editor v0.9.4
- cpp/functions/gfx/dag_node_editor: new visual pipeline editor replacing dag_panel
- DagStep: source_ids[4] + editor_pos + editor_uid (multi-input support)
- DagNodeDef: num_inputs explicit; circle reclassified as Op
- dag_compile: N inputs per node, topological ordering preserved
- main: use node editor; destroy on shutdown
- patch imgui_extra_math.inl: guard operator*(float, ImVec2) for imgui >= 18955

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 21:55:43 +02:00
parent bf5011de93
commit 88fca7b128
89 changed files with 22289 additions and 41 deletions
@@ -0,0 +1,9 @@
add_example_executable(blueprints-example
blueprints-example.cpp
utilities/builders.h
utilities/drawing.h
utilities/widgets.h
utilities/builders.cpp
utilities/drawing.cpp
utilities/widgets.cpp
)
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

@@ -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 <imgui_internal.h>
//------------------------------------------------------------------------------
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<int>(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));
}
@@ -0,0 +1,81 @@
//------------------------------------------------------------------------------
// 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 <imgui_node_editor.h>
//------------------------------------------------------------------------------
namespace ax {
namespace NodeEditor {
namespace Utilities {
//------------------------------------------------------------------------------
struct BlueprintNodeBuilder
{
BlueprintNodeBuilder(ImTextureID texture = nullptr, 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 Editor
} // namespace ax
@@ -0,0 +1,252 @@
# define IMGUI_DEFINE_MATH_OPERATORS
# include "drawing.h"
# include <imgui_internal.h>
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<int>(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<int>(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);
}
}
}
@@ -0,0 +1,12 @@
# pragma once
# include <imgui.h>
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
@@ -0,0 +1,16 @@
# define IMGUI_DEFINE_MATH_OPERATORS
# include "widgets.h"
# include <imgui_internal.h>
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);
}
@@ -0,0 +1,13 @@
#pragma once
#include <imgui.h>
#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