deargui-vpl/applications/nodehub/app-render.cpp
2026-02-03 18:25:25 +01:00

3509 lines
302 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#define IMGUI_DEFINE_MATH_OPERATORS
#include "app.h"
#include "utilities/node_renderer_base.h"
#include "utilities/pathfinding.h"
#include "blocks/block.h"
#include "blocks/parameter_node.h"
#include "blocks/block_edit_dialog.h"
#include "blocks/parameter_edit_dialog.h"
#include "blocks/group_block.h" // Include to register Group block
#include "blocks/logic_blocks.h" // Include to register Logic blocks
#include "blocks/parameter_operation.h" // Include to register Parameter Operation block
#include "Logging.h" // Integrated logger system
#include <imgui_node_editor.h>
#include <imgui_node_editor_internal.h>
#include <imgui_internal.h>
#include <algorithm>
namespace ed = ax::NodeEditor;
using namespace ax;
using ax::NodeRendering::IconType;
extern ed::EditorContext *m_Editor;
static inline ImRect ImGui_GetItemRect()
{
return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
}
static inline ImRect ImRect_Expanded(const ImRect &rect, float x, float y)
{
auto result = rect;
result.Min.x -= x;
result.Min.y -= y;
result.Max.x += x;
result.Max.y += y;
return result;
}
static bool Splitter(bool split_vertically, float thickness, float *size1, float *size2, float min_size1, float min_size2, float splitter_long_axis_size = -1.0f)
{
using namespace ImGui;
ImGuiContext &g = *GImGui;
ImGuiWindow *window = g.CurrentWindow;
ImGuiID id = window->GetID("##Splitter");
ImRect bb;
bb.Min = window->DC.CursorPos + (split_vertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1));
bb.Max = bb.Min + CalcItemSize(split_vertically ? ImVec2(thickness, splitter_long_axis_size) : ImVec2(splitter_long_axis_size, thickness), 0.0f, 0.0f);
return SplitterBehavior(bb, id, split_vertically ? ImGuiAxis_X : ImGuiAxis_Y, size1, size2, min_size1, min_size2, 0.0f);
}
// Helper function to get actual pin position based on node type and pin layout
static ImVec2 GetPinPosition(Pin *pin, const ImVec2 &nodePos, const ImVec2 &nodeSize)
{
// Use stored position data from PinRenderer if available
if (pin->HasPositionData)
{
// Get the current node position to calculate relative offset
ImVec2 currentNodePos = ed::GetNodePosition(pin->Node->ID);
ImVec2 relativeOffset = pin->LastPivotPosition - currentNodePos;
// Apply this offset to the requested node position
// This handles cases where nodePos != currentNodePos (e.g., during creation)
return nodePos + relativeOffset;
}
// Fallback to approximation for legacy/unrendered pins
if (pin->Node->Type == NodeType::Parameter)
{
// Parameter node: pin is centered at bottom for output
return ImVec2(nodePos.x + nodeSize.x * 0.5f, nodePos.y + nodeSize.y);
}
else if (pin->Node->IsBlockBased() && pin->Type != PinType::Flow)
{
// Block input parameter pins: APPROXIMATION (only used as fallback)
// Count non-flow input pins
int pinIndex = -1;
int nonFlowIndex = -1;
for (auto &nodePin : pin->Node->Inputs)
{
pinIndex++;
if (nodePin.Type == PinType::Flow)
continue;
nonFlowIndex++;
if (nodePin.ID.Get() == pin->ID.Get())
break;
}
if (nonFlowIndex >= 0)
{
int totalNonFlowPins = 0;
for (auto &nodePin : pin->Node->Inputs)
{
if (nodePin.Type != PinType::Flow)
totalNonFlowPins++;
}
// Calculate based on percentage that matches the measurement
float offsetPercent = 0.15f; // 15% for first pin when there are multiple pins
float xPos = nodePos.x + nodeSize.x * offsetPercent * (nonFlowIndex + 1);
return ImVec2(xPos, nodePos.y);
}
else
{
// Fallback to center
return ImVec2(nodePos.x + nodeSize.x * 0.5f, nodePos.y);
}
}
else if (pin->Kind == PinKind::Output && pin->Type == PinType::Flow)
{
return ImVec2(nodePos.x + nodeSize.x, nodePos.y + nodeSize.y * 0.5f);
}
else
{
return ImVec2(nodePos.x, nodePos.y + nodeSize.y * 0.5f);
}
}
// Helper function to get pin direction based on node type and pin kind
static ImVec2 GetPinDirection(Pin *pin)
{
if (pin->Node->Type == NodeType::Parameter)
{
// Parameter nodes:
// - Input is at top, accepts from above (direction downward = 0, 1)
// - Output is at bottom, flows upward (direction upward = 0, -1)
if (pin->Kind == PinKind::Input)
return ImVec2(0, -1); // Input accepts from above (direction downward)
else
return ImVec2(0, 1); // Output flows upward
}
else if (pin->Node->IsBlockBased() && pin->Type != PinType::Flow)
{
// Block parameter pins:
// - Inputs are at top, accept from above (flow upward)
// - Outputs are at bottom, flow downward
if (pin->Kind == PinKind::Input)
return ImVec2(0, -1); // Input accepts from above
else
return ImVec2(0, 1); // Output flows downward
}
else if (pin->Kind == PinKind::Output && pin->Type == PinType::Flow)
{
return ImVec2(1, 0);
}
else
{
return ImVec2(-1, 0);
}
}
void App::DrawPinIcon(const Pin &pin, bool connected, int alpha)
{
IconType iconType;
ImColor color = GetIconColor(pin.Type);
color.Value.w = alpha / 255.0f;
switch (pin.Type)
{
case PinType::Flow:
iconType = IconType::Flow;
break;
case PinType::Bool:
iconType = IconType::Circle;
break;
case PinType::Int:
iconType = IconType::Circle;
break;
case PinType::Float:
iconType = IconType::Circle;
break;
case PinType::String:
iconType = IconType::Circle;
break;
case PinType::Object:
iconType = IconType::Circle;
break;
case PinType::Function:
iconType = IconType::Circle;
break;
case PinType::Delegate:
iconType = IconType::Square;
break;
default:
return;
}
ax::NodeRendering::NodeRendererBase::Icon(ImVec2(static_cast<float>(m_PinIconSize), static_cast<float>(m_PinIconSize)), iconType, false, color, ImColor(32, 32, 32, alpha));
}
void App::RenderDebugInfo()
{
auto &io = ImGui::GetIO();
static bool showMetrics = false;
static bool showDebugLog = false;
ImGui::BeginGroup();
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Links: SPACE=cycle mode (Auto/Straight/Guided) | Double-click=add/remove waypoint");
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.5f, 1.0f), "Nodes: SPACE=cycle mode | ARROWS=move (Shift=10x) | DEL=delete");
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "F12=Screenshot");
ImGui::Text("FPS: %.2f (%.2gms)", io.Framerate, io.Framerate ? 1000.0f / io.Framerate : 0.0f);
// Mouse and canvas info
auto mousePos = io.MousePos;
auto canvasMousePos = ed::ScreenToCanvas(mousePos);
auto zoom = ed::GetCurrentZoom();
ImGui::Text("Mouse: Screen(%.0f, %.0f) Canvas(%.1f, %.1f)", mousePos.x, mousePos.y, canvasMousePos.x, canvasMousePos.y);
ImGui::Text("Zoom: %.2fx", zoom);
ImGui::SameLine();
if (ImGui::SmallButton(showMetrics ? "Hide Metrics" : "Show Metrics"))
showMetrics = !showMetrics;
ImGui::SameLine();
if (ImGui::SmallButton(showDebugLog ? "Hide Debug Log" : "Show Debug Log"))
showDebugLog = !showDebugLog;
ImGui::EndGroup();
ImGui::Separator();
if (showMetrics)
ImGui::ShowMetricsWindow(&showMetrics);
if (showDebugLog)
{
// ShowDebugLogWindow is not available in this ImGui version
if (ImGui::Begin("Debug Log", &showDebugLog))
ImGui::Text("Debug log window not available in this ImGui build");
ImGui::End();
}
}
void App::ShowStyleEditor(bool *show)
{
if (!ImGui::Begin("Style", show))
{
ImGui::End();
return;
}
auto paneWidth = ImGui::GetContentRegionAvail().x;
auto &editorStyle = ed::GetStyle();
ImGui::BeginHorizontal("Style buttons", ImVec2(paneWidth, 0), 1.0f);
ImGui::TextUnformatted("Values");
ImGui::Spring();
if (ImGui::Button("Reset to defaults"))
editorStyle = ed::Style();
ImGui::EndHorizontal();
ImGui::Spacing();
ImGui::DragFloat4("Node Padding", &editorStyle.NodePadding.x, 0.1f, 0.0f, 40.0f);
ImGui::DragFloat("Node Rounding", &editorStyle.NodeRounding, 0.1f, 0.0f, 40.0f);
ImGui::DragFloat("Node Border Width", &editorStyle.NodeBorderWidth, 0.1f, 0.0f, 15.0f);
ImGui::DragFloat("Hovered Node Border Width", &editorStyle.HoveredNodeBorderWidth, 0.1f, 0.0f, 15.0f);
ImGui::DragFloat("Hovered Node Border Offset", &editorStyle.HoverNodeBorderOffset, 0.1f, -40.0f, 40.0f);
ImGui::DragFloat("Selected Node Border Width", &editorStyle.SelectedNodeBorderWidth, 0.1f, 0.0f, 15.0f);
ImGui::DragFloat("Selected Node Border Offset", &editorStyle.SelectedNodeBorderOffset, 0.1f, -40.0f, 40.0f);
ImGui::DragFloat("Pin Rounding", &editorStyle.PinRounding, 0.1f, 0.0f, 40.0f);
ImGui::DragFloat("Pin Border Width", &editorStyle.PinBorderWidth, 0.1f, 0.0f, 15.0f);
ImGui::DragFloat("Link Strength", &editorStyle.LinkStrength, 1.0f, 0.0f, 500.0f);
ImGui::DragFloat("Scroll Duration", &editorStyle.ScrollDuration, 0.001f, 0.0f, 2.0f);
ImGui::DragFloat("Flow Marker Distance", &editorStyle.FlowMarkerDistance, 1.0f, 1.0f, 200.0f);
ImGui::DragFloat("Flow Speed", &editorStyle.FlowSpeed, 1.0f, 1.0f, 2000.0f);
ImGui::DragFloat("Flow Duration", &editorStyle.FlowDuration, 0.001f, 0.0f, 5.0f);
ImGui::DragFloat("Group Rounding", &editorStyle.GroupRounding, 0.1f, 0.0f, 40.0f);
ImGui::DragFloat("Group Border Width", &editorStyle.GroupBorderWidth, 0.1f, 0.0f, 15.0f);
ImGui::Separator();
ImGui::Separator();
// StyleManager section - custom node styles
if (ImGui::CollapsingHeader("Node Styles (StyleManager)", ImGuiTreeNodeFlags_DefaultOpen))
{
auto& styleMgr = m_StyleManager;
ImGui::BeginHorizontal("StyleManager buttons", ImVec2(paneWidth, 0), 1.0f);
ImGui::TextUnformatted("Custom Node Styles");
ImGui::Spring();
if (ImGui::Button("Save Styles"))
{
if (styleMgr.SaveToFile("styles.json"))
LOG_INFO("[StyleEditor] Styles saved to styles.json");
else
LOG_WARN("[StyleEditor] Failed to save styles (not yet implemented)");
}
ImGui::EndHorizontal();
ImGui::Spacing();
// Block Style
if (ImGui::TreeNode("Block Style"))
{
ImGui::ColorEdit4("BG Color##block", &styleMgr.BlockStyle.BgColor.Value.x);
ImGui::ColorEdit4("Border Color##block", &styleMgr.BlockStyle.BorderColor.Value.x);
ImGui::ColorEdit4("Border Running##block", &styleMgr.BlockStyle.BorderColorRunning.Value.x);
ImGui::DragFloat("Rounding##block", &styleMgr.BlockStyle.Rounding, 0.1f, 0.0f, 20.0f);
ImGui::DragFloat("Border Width##block", &styleMgr.BlockStyle.BorderWidth, 0.1f, 0.0f, 10.0f);
ImGui::DragFloat("Border Width Running##block", &styleMgr.BlockStyle.BorderWidthRunning, 0.1f, 0.0f, 10.0f);
ImGui::DragFloat4("Padding##block", &styleMgr.BlockStyle.Padding.x, 0.5f, 0.0f, 30.0f);
ImGui::TreePop();
}
// Group Style
if (ImGui::TreeNode("Group Style"))
{
ImGui::ColorEdit4("BG Color##group", &styleMgr.GroupStyle.BgColor.Value.x);
ImGui::ColorEdit4("Border Color##group", &styleMgr.GroupStyle.BorderColor.Value.x);
ImGui::ColorEdit4("Border Running##group", &styleMgr.GroupStyle.BorderColorRunning.Value.x);
ImGui::DragFloat("Rounding##group", &styleMgr.GroupStyle.Rounding, 0.1f, 0.0f, 20.0f);
ImGui::DragFloat("Border Width##group", &styleMgr.GroupStyle.BorderWidth, 0.1f, 0.0f, 10.0f);
ImGui::DragFloat("Border Width Running##group", &styleMgr.GroupStyle.BorderWidthRunning, 0.1f, 0.0f, 10.0f);
ImGui::DragFloat4("Padding##group", &styleMgr.GroupStyle.Padding.x, 0.5f, 0.0f, 30.0f);
ImGui::DragFloat("Resize Grip Size", &styleMgr.GroupResizeGripSize, 0.5f, 8.0f, 32.0f);
ImGui::DragFloat("Resize Grip Line Spacing", &styleMgr.GroupResizeGripLineSpacing, 0.1f, 1.0f, 10.0f);
ImGui::ColorEdit4("Resize Grip Color", &styleMgr.GroupResizeGripColor.Value.x);
ImGui::TreePop();
}
// Parameter Style
if (ImGui::TreeNode("Parameter Style"))
{
ImGui::ColorEdit4("BG Color##param", &styleMgr.ParameterStyle.BgColor.Value.x);
ImGui::ColorEdit4("Border Color##param", &styleMgr.ParameterStyle.BorderColor.Value.x);
ImGui::ColorEdit4("Border Source##param", &styleMgr.ParamBorderColorSource.Value.x);
ImGui::ColorEdit4("Border Shortcut##param", &styleMgr.ParamBorderColorShortcut.Value.x);
ImGui::DragFloat("Rounding##param", &styleMgr.ParameterStyle.Rounding, 0.1f, 0.0f, 20.0f);
ImGui::DragFloat("Border Width##param", &styleMgr.ParameterStyle.BorderWidth, 0.1f, 0.0f, 10.0f);
ImGui::DragFloat("Border Width Source", &styleMgr.ParamBorderWidthSource, 0.1f, 0.0f, 10.0f);
ImGui::TreePop();
}
// Pin Styles
if (ImGui::TreeNode("Pin Styles"))
{
ImGui::Text("Pin Colors:");
ImGui::ColorEdit4("Input##pin", &styleMgr.PinColor_Input.Value.x);
ImGui::ColorEdit4("Output##pin", &styleMgr.PinColor_Output.Value.x);
ImGui::ColorEdit4("Running##pin", &styleMgr.PinColor_Running.Value.x);
ImGui::ColorEdit4("Deactivated##pin", &styleMgr.PinColor_Deactivated.Value.x);
ImGui::ColorEdit4("Error##pin", &styleMgr.PinColor_Error.Value.x);
ImGui::ColorEdit4("Warning##pin", &styleMgr.PinColor_Warning.Value.x);
ImGui::Separator();
ImGui::Text("Pin Sizes:");
ImGui::DragFloat("Flow Pin Size", &styleMgr.FlowPinSize, 0.5f, 4.0f, 20.0f);
ImGui::DragFloat("Parameter Pin Width", &styleMgr.ParameterPinWidth, 0.5f, 4.0f, 20.0f);
ImGui::DragFloat("Parameter Pin Height", &styleMgr.ParameterPinHeight, 0.5f, 2.0f, 20.0f);
ImGui::DragFloat("Parameter Pin Edge Offset", &styleMgr.ParameterPinEdgeOffset, 0.5f, -10.0f, 10.0f);
ImGui::DragFloat("Flow Pin Edge Offset", &styleMgr.FlowPinEdgeOffset, 0.5f, -10.0f, 10.0f);
ImGui::TreePop();
}
// Link Styles
if (ImGui::TreeNode("Link Styles"))
{
ImGui::Text("Link Thickness:");
ImGui::DragFloat("Normal", &styleMgr.LinkThicknessNormal, 0.1f, 0.5f, 10.0f);
ImGui::DragFloat("Selected/Highlighted", &styleMgr.LinkThicknessSelected, 0.1f, 0.5f, 10.0f);
ImGui::DragFloat("Parameter Normal", &styleMgr.LinkThicknessParameterNormal, 0.1f, 0.5f, 10.0f);
ImGui::DragFloat("Parameter Selected", &styleMgr.LinkThicknessParameterSelected, 0.1f, 0.5f, 10.0f);
ImGui::Separator();
ImGui::Text("Link Colors:");
ImGui::ColorEdit4("Highlighted", &styleMgr.LinkColorHighlighted.Value.x);
ImGui::TreePop();
}
// Waypoint/Control Point Styles
if (ImGui::TreeNode("Waypoint Styles"))
{
bool waypointChanged = false;
ImGui::Text("Waypoint Properties:");
if (ImGui::DragFloat("Radius", &styleMgr.WaypointRadius, 0.5f, 2.0f, 20.0f))
waypointChanged = true;
if (ImGui::DragFloat("Border Width", &styleMgr.WaypointBorderWidth, 0.1f, 0.5f, 5.0f))
waypointChanged = true;
ImGui::Separator();
ImGui::Text("Waypoint Colors:");
if (ImGui::ColorEdit4("Normal", &styleMgr.WaypointColor.Value.x))
waypointChanged = true;
if (ImGui::ColorEdit4("Border", &styleMgr.WaypointBorderColor.Value.x))
waypointChanged = true;
if (ImGui::ColorEdit4("Hovered", &styleMgr.WaypointColorHovered.Value.x))
waypointChanged = true;
if (ImGui::ColorEdit4("Selected/Dragged", &styleMgr.WaypointColorSelected.Value.x))
waypointChanged = true;
// Apply waypoint styles to editor immediately when changed
if (waypointChanged)
{
auto& style = editorStyle;
style.WaypointRadius = styleMgr.WaypointRadius;
style.WaypointBorderWidth = styleMgr.WaypointBorderWidth;
style.WaypointColor = styleMgr.WaypointColor;
style.WaypointBorderColor = styleMgr.WaypointBorderColor;
style.WaypointColorHovered = styleMgr.WaypointColorHovered;
style.WaypointColorSelected = styleMgr.WaypointColorSelected;
}
ImGui::Separator();
ImGui::Text("Preview Waypoints (while dragging):");
ImGui::DragFloat("Preview Radius", &styleMgr.WaypointPreviewRadius, 0.5f, 2.0f, 20.0f);
ImGui::ColorEdit4("Preview Color", &styleMgr.WaypointPreviewColor.Value.x);
ImGui::ColorEdit4("Preview Border", &styleMgr.WaypointPreviewBorderColor.Value.x);
ImGui::TreePop();
}
// Layout
if (ImGui::TreeNode("Layout"))
{
ImGui::DragFloat("Min Node Width", &styleMgr.MinNodeWidth, 1.0f, 40.0f, 200.0f);
ImGui::DragFloat("Min Node Height", &styleMgr.MinNodeHeight, 1.0f, 20.0f, 200.0f);
ImGui::DragFloat("Min Group Size", &styleMgr.MinGroupSize, 1.0f, 50.0f, 300.0f);
ImGui::TreePop();
}
}
ImGui::Separator();
static ImGuiColorEditFlags edit_mode = ImGuiColorEditFlags_DisplayRGB;
ImGui::BeginHorizontal("Color Mode", ImVec2(paneWidth, 0), 1.0f);
ImGui::TextUnformatted("Filter Colors");
ImGui::Spring();
ImGui::RadioButton("RGB", &edit_mode, ImGuiColorEditFlags_DisplayRGB);
ImGui::Spring(0);
ImGui::RadioButton("HSV", &edit_mode, ImGuiColorEditFlags_DisplayHSV);
ImGui::Spring(0);
ImGui::RadioButton("HEX", &edit_mode, ImGuiColorEditFlags_DisplayHex);
ImGui::EndHorizontal();
static ImGuiTextFilter filter;
filter.Draw("##filter", paneWidth);
ImGui::Spacing();
ImGui::PushItemWidth(-160);
for (int i = 0; i < ed::StyleColor_Count; ++i)
{
auto name = ed::GetStyleColorName((ed::StyleColor)i);
if (!filter.PassFilter(name))
continue;
ImGui::ColorEdit4(name, &editorStyle.Colors[i].x, edit_mode);
}
ImGui::PopItemWidth();
ImGui::End();
}
void App::ShowLeftPane(float paneWidth)
{
auto &io = ImGui::GetIO();
ImGui::BeginChild("Selection", ImVec2(paneWidth, 0));
paneWidth = ImGui::GetContentRegionAvail().x;
static bool showStyleEditor = false;
ImGui::BeginHorizontal("Style Editor", ImVec2(paneWidth, 0));
ImGui::Spring(0.0f, 0.0f);
if (ImGui::Button("Zoom to Content"))
ed::NavigateToContent();
ImGui::Spring(0.0f);
if (ImGui::Button("Show Flow"))
{
// Get links from active root container
if (GetActiveRootContainer())
{
auto links = GetActiveRootContainer()->GetAllLinks();
for (auto* link : links)
{
if (link)
ed::Flow(link->ID);
}
}
}
ImGui::Spring();
if (ImGui::Button("Edit Style"))
showStyleEditor = true;
ImGui::EndHorizontal();
ImGui::Checkbox("Show Ordinals", &m_ShowOrdinals);
if (showStyleEditor)
ShowStyleEditor(&showStyleEditor);
std::vector<ed::NodeId> selectedNodes;
std::vector<ed::LinkId> selectedLinks;
selectedNodes.resize(ed::GetSelectedObjectCount());
selectedLinks.resize(ed::GetSelectedObjectCount());
int nodeCount = ed::GetSelectedNodes(selectedNodes.data(), static_cast<int>(selectedNodes.size()));
int linkCount = ed::GetSelectedLinks(selectedLinks.data(), static_cast<int>(selectedLinks.size()));
selectedNodes.resize(nodeCount);
selectedLinks.resize(linkCount);
int saveIconWidth = GetTextureWidth(m_SaveIcon);
int saveIconHeight = GetTextureWidth(m_SaveIcon);
int restoreIconWidth = GetTextureWidth(m_RestoreIcon);
int restoreIconHeight = GetTextureWidth(m_RestoreIcon);
ImGui::GetWindowDrawList()->AddRectFilled(
ImGui::GetCursorScreenPos(),
ImGui::GetCursorScreenPos() + ImVec2(paneWidth, ImGui::GetTextLineHeight()),
ImColor(ImGui::GetStyle().Colors[ImGuiCol_HeaderActive]), ImGui::GetTextLineHeight() * 0.25f);
ImGui::Spacing();
ImGui::SameLine();
ImGui::TextUnformatted("Nodes");
ImGui::Indent();
// Get nodes from active root container
std::vector<Node*> nodesToDisplay;
if (GetActiveRootContainer())
{
nodesToDisplay = GetActiveRootContainer()->GetAllNodes();
}
for (auto* nodePtr : nodesToDisplay)
{
if (!nodePtr) continue;
auto &node = *nodePtr;
ImGui::PushID(node.ID.AsPointer());
auto start = ImGui::GetCursorScreenPos();
if (const auto progress = GetTouchProgress(node.ID))
{
ImGui::GetWindowDrawList()->AddLine(
start + ImVec2(-8, 0),
start + ImVec2(-8, ImGui::GetTextLineHeight()),
IM_COL32(255, 0, 0, 255 - (int)(255 * progress)), 4.0f);
}
bool isSelected = std::find(selectedNodes.begin(), selectedNodes.end(), node.ID) != selectedNodes.end();
#if IMGUI_VERSION_NUM >= 18967
ImGui::SetNextItemAllowOverlap();
#endif
if (ImGui::Selectable((node.Name + "##" + std::to_string(reinterpret_cast<uintptr_t>(node.ID.AsPointer()))).c_str(), &isSelected))
{
if (io.KeyCtrl)
{
if (isSelected)
ed::SelectNode(node.ID, true);
else
ed::DeselectNode(node.ID);
}
else
ed::SelectNode(node.ID, false);
ed::NavigateToSelection();
}
if (ImGui::IsItemHovered() && !node.State.empty())
ImGui::SetTooltip("State: %s", node.State.c_str());
auto id = std::string("(") + std::to_string(reinterpret_cast<uintptr_t>(node.ID.AsPointer())) + ")";
auto textSize = ImGui::CalcTextSize(id.c_str(), nullptr);
auto iconPanelPos = start + ImVec2(
paneWidth - ImGui::GetStyle().FramePadding.x - ImGui::GetStyle().IndentSpacing - saveIconWidth - restoreIconWidth - ImGui::GetStyle().ItemInnerSpacing.x * 1,
(ImGui::GetTextLineHeight() - saveIconHeight) / 2);
ImGui::GetWindowDrawList()->AddText(
ImVec2(iconPanelPos.x - textSize.x - ImGui::GetStyle().ItemInnerSpacing.x, start.y),
IM_COL32(255, 255, 255, 255), id.c_str(), nullptr);
auto drawList = ImGui::GetWindowDrawList();
ImGui::SetCursorScreenPos(iconPanelPos);
#if IMGUI_VERSION_NUM < 18967
ImGui::SetItemAllowOverlap();
#else
ImGui::SetNextItemAllowOverlap();
#endif
if (node.SavedState.empty())
{
if (ImGui::InvisibleButton("save", ImVec2((float)saveIconWidth, (float)saveIconHeight)))
node.SavedState = node.State;
if (ImGui::IsItemActive())
drawList->AddImage(m_SaveIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 96));
else if (ImGui::IsItemHovered())
drawList->AddImage(m_SaveIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
else
drawList->AddImage(m_SaveIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 160));
}
else
{
ImGui::Dummy(ImVec2((float)saveIconWidth, (float)saveIconHeight));
drawList->AddImage(m_SaveIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 32));
}
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
#if IMGUI_VERSION_NUM < 18967
ImGui::SetItemAllowOverlap();
#else
ImGui::SetNextItemAllowOverlap();
#endif
if (!node.SavedState.empty())
{
if (ImGui::InvisibleButton("restore", ImVec2((float)restoreIconWidth, (float)restoreIconHeight)))
{
node.State = node.SavedState;
ed::RestoreNodeState(node.ID);
node.SavedState.clear();
}
if (ImGui::IsItemActive())
drawList->AddImage(m_RestoreIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 96));
else if (ImGui::IsItemHovered())
drawList->AddImage(m_RestoreIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
else
drawList->AddImage(m_RestoreIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 160));
}
else
{
ImGui::Dummy(ImVec2((float)restoreIconWidth, (float)restoreIconHeight));
drawList->AddImage(m_RestoreIcon, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 32));
}
ImGui::SameLine(0, 0);
#if IMGUI_VERSION_NUM < 18967
ImGui::SetItemAllowOverlap();
#endif
ImGui::Dummy(ImVec2(0, (float)restoreIconHeight));
ImGui::PopID();
}
ImGui::Unindent();
static int changeCount = 0;
ImGui::GetWindowDrawList()->AddRectFilled(
ImGui::GetCursorScreenPos(),
ImGui::GetCursorScreenPos() + ImVec2(paneWidth, ImGui::GetTextLineHeight()),
ImColor(ImGui::GetStyle().Colors[ImGuiCol_HeaderActive]), ImGui::GetTextLineHeight() * 0.25f);
ImGui::Spacing();
ImGui::SameLine();
ImGui::TextUnformatted("Selection");
ImGui::BeginHorizontal("Selection Stats", ImVec2(paneWidth, 0));
ImGui::Text("Changed %d time%s", changeCount, changeCount > 1 ? "s" : "");
ImGui::Spring();
if (ImGui::Button("Deselect All"))
ed::ClearSelection();
ImGui::EndHorizontal();
ImGui::Indent();
for (int i = 0; i < nodeCount; ++i)
ImGui::Text("Node (%p)", selectedNodes[i].AsPointer());
for (int i = 0; i < linkCount; ++i)
ImGui::Text("Link (%p)", selectedLinks[i].AsPointer());
ImGui::Unindent();
ImGui::Separator();
ImGui::TextUnformatted("Logger Level");
ImGui::PushID("LoggerLevel");
static const char* levelLabels[] = {"Trace", "Debug", "Info", "Warn", "Error", "Critical", "Off"};
static const spdlog::level::level_enum levelValues[] = {
spdlog::level::trace,
spdlog::level::debug,
spdlog::level::info,
spdlog::level::warn,
spdlog::level::err,
spdlog::level::critical,
spdlog::level::off
};
int currentLevelIndex = 2; // default to info
if (g_logger)
{
auto currentLevel = g_logger->level();
for (int i = 0; i < IM_ARRAYSIZE(levelValues); ++i)
{
if (levelValues[i] == currentLevel)
{
currentLevelIndex = i;
break;
}
}
}
if (!g_logger)
{
ImGui::TextDisabled("Logger not initialized");
}
else
{
int levelSelection = currentLevelIndex;
if (ImGui::Combo("##logLevel", &levelSelection, levelLabels, IM_ARRAYSIZE(levelLabels)))
{
SetLoggerLevel(levelValues[levelSelection]);
LOG_INFO("Logger level set to {}", levelLabels[levelSelection]);
}
}
ImGui::PopID();
if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Z)))
{
// Get links from active root container
if (GetActiveRootContainer())
{
auto links = GetActiveRootContainer()->GetAllLinks();
for (auto* link : links)
{
if (link)
ed::Flow(link->ID);
}
}
}
if (ed::HasSelectionChanged())
++changeCount;
// In-app logger
float loggerHeight = 150.0f; // Fixed height for logger
float remainingHeight = ImGui::GetContentRegionAvail().y - loggerHeight;
if (remainingHeight > 0)
{
ImGui::Dummy(ImVec2(0, remainingHeight));
}
ImGui::EndChild();
}
void App::RenderNodes(Pin *newLinkPin)
{
// Render nodes from active root container
auto *container = GetActiveRootContainer();
// Make a copy of the node pointers to avoid iterator invalidation when removing nodes
std::vector<Node *> nodesToRender;
if (container)
{
// Use GetNodes() to resolve IDs to pointers (safe from reallocation)
nodesToRender = container->GetNodes(this);
}
else
{
// No container - get nodes from active root container
if (GetActiveRootContainer())
{
nodesToRender = GetActiveRootContainer()->GetAllNodes();
}
}
// Track orphaned nodes to remove after iteration
std::vector<Node *> orphanedNodes;
// Render block-based nodes (blocks render themselves)
for (auto *nodePtr : nodesToRender)
{
if (!nodePtr)
{
LOG_WARN("[WARNING] RenderNodes: null node pointer in container!");
continue;
}
// CRITICAL: Skip parameter nodes FIRST - before accessing any member variables
// Parameter nodes render themselves in a separate loop below
// Use try-catch to safely access Type field
NodeType nodeType;
try
{
nodeType = nodePtr->Type;
}
catch (...)
{
// Node pointer is corrupted - mark for removal
LOG_ERROR("[ERROR] RenderNodes: Cannot access node Type field - marking for removal");
orphanedNodes.push_back(nodePtr);
continue;
}
if (nodeType == NodeType::Parameter)
continue;
// Only process block-based nodes
bool isBlockBased = false;
try
{
isBlockBased = nodePtr->IsBlockBased();
}
catch (...)
{
LOG_ERROR("[ERROR] RenderNodes: Cannot access IsBlockBased - marking for removal");
orphanedNodes.push_back(nodePtr);
continue;
}
if (!isBlockBased)
continue;
// Validate node pointer itself is reasonable
uintptr_t nodePtrValue = reinterpret_cast<uintptr_t>(nodePtr);
if (nodePtrValue < 0x1000 ||
nodePtrValue == 0xFFFFFFFFFFFFFFFFULL ||
nodePtrValue == 0xDDDDDDDDDDDDDDDDULL)
{
LOG_ERROR("[ERROR] RenderNodes: Node pointer itself is corrupted: 0x%llx - marking for removal",
(unsigned long long)nodePtrValue);
// Mark corrupted pointer for removal
orphanedNodes.push_back(nodePtr);
continue;
}
// Use safe getter and validate BlockInstance pointer matches node ID
Block *blockInstance = nodePtr->GetBlockInstance();
if (!blockInstance)
{
// Safe access to node ID for logging
int nodeId = -1;
const char *blockTypeStr = "(unknown)";
try
{
nodeId = ToRuntimeId(nodePtr->ID);
blockTypeStr = nodePtr->BlockType.empty() ? "(empty)" : nodePtr->BlockType.c_str();
}
catch (...)
{
// Node is corrupted, can't access members - mark for removal
LOG_ERROR("[ERROR] RenderNodes: Cannot access node fields - node is corrupted");
orphanedNodes.push_back(nodePtr);
continue;
}
// Mark orphaned node for removal (remove after iteration to avoid iterator invalidation)
LOG_WARN("[DELETE] RenderNodes: Marking node {} (ptr={:p}) for removal - null BlockInstance",
nodeId, static_cast<void*>(nodePtr));
LOG_WARN("[DELETE] RenderNodes: Node details - Type={}, IsBlockBased={}, BlockType='{}'",
static_cast<int>(nodePtr->Type), nodePtr->IsBlockBased() ? "true" : "false",
nodePtr->BlockType.empty() ? "(empty)" : nodePtr->BlockType.c_str());
orphanedNodes.push_back(nodePtr);
continue;
}
// Check if BlockInstance pointer is clearly corrupted (before accessing any members)
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(blockInstance);
if (ptrValue < 0x1000 || ptrValue > 0x7FFFFFFFFFFFFFFFULL)
{
// Corrupted pointer - mark for removal
int nodeId = -1;
try
{
nodeId = nodePtr->ID.Get();
}
catch (...)
{
nodeId = -1;
}
LOG_ERROR("[ERROR] RenderNodes: Node {} has corrupted BlockInstance pointer: 0x{:016X} - marking for removal",
nodeId, static_cast<unsigned long long>(ptrValue));
orphanedNodes.push_back(nodePtr);
continue;
}
// Critical validation: Check if BlockInstance ID matches node ID
// If IDs don't match, the block was deleted and ID was reused (dangling pointer)
int blockId = -1;
try
{
blockId = blockInstance->GetID();
}
catch (...)
{
int nodeId = -1;
try
{
nodeId = nodePtr->ID.Get();
}
catch (...)
{
nodeId = -1;
}
LOG_ERROR("[ERROR] RenderNodes: Failed to call GetID() on BlockInstance for node {} - marking for removal",
nodeId);
orphanedNodes.push_back(nodePtr);
continue;
}
int nodeId = -1;
try
{
nodeId = (int)nodePtr->ID.Get();
}
catch (...)
{
orphanedNodes.push_back(nodePtr);
continue;
}
if (blockId != nodeId)
{
// ID mismatch = dangling pointer, mark for removal
LOG_ERROR("[ERROR] RenderNodes: Node {} has BlockInstance with mismatched ID! Node ID={}, Block ID={} - marking for removal",
nodeId, nodeId, blockId);
orphanedNodes.push_back(nodePtr);
continue;
}
// Call Render with validated pointer
blockInstance->Render(*nodePtr, this, newLinkPin);
}
// Remove orphaned nodes from container (after iteration to avoid iterator invalidation)
if (!orphanedNodes.empty() && GetActiveRootContainer())
{
for (auto *orphanedNode : orphanedNodes)
{
// Try to get node ID for logging (may fail if node is corrupted)
int nodeId = -1;
try
{
if (orphanedNode)
nodeId = (int)orphanedNode->ID.Get();
}
catch (...)
{
nodeId = -1;
}
LOG_WARN("[DELETE] RenderNodes: Removing orphaned node {} (ptr={:p}) from container",
nodeId, static_cast<void*>(orphanedNode));
// RemoveNode uses ID-based lookup, safe even if pointer is invalidated
if (orphanedNode)
GetActiveRootContainer()->RemoveNode(orphanedNode->ID);
}
LOG_INFO("[DELETE] RenderNodes: Cleaned up {} orphaned node(s) from container",
orphanedNodes.size());
}
// Render parameter nodes (parameters render themselves)
for (auto *nodePtr : nodesToRender)
{
if (!nodePtr || nodePtr->Type != NodeType::Parameter)
continue;
// Use safe getter for ParameterInstance
auto *paramInstance = nodePtr->GetParameterInstance();
if (!paramInstance)
continue;
paramInstance->Render(*nodePtr, this, newLinkPin);
}
}
void App::VisualizeRuntimeExecution(const std::vector<Node *> &executedNodes,
const std::vector<ed::LinkId> &affectedLinks)
{
// Only visualize if we're in rendering mode (not headless)
// This function can be called from ExecuteRuntimeStep but will be a no-op in headless mode
// In rendering mode, it marks nodes and links for visual feedback
// Guard: Check if ImGui context is available (won't be in headless mode)
if (!ImGui::GetCurrentContext())
return;
float currentTime = (float)ImGui::GetTime();
float highlightDuration = 0.6f; // 600ms
// Mark executed nodes as running (red border)
for (Node *node : executedNodes)
{
if (node && node->IsBlockBased())
{
m_RunningNodes[node->ID] = currentTime + highlightDuration;
}
}
// Mark affected links for red highlight
for (auto linkId : affectedLinks)
{
m_HighlightedLinks[linkId] = currentTime + highlightDuration;
}
}
void App::RenderLinks()
{
// Update highlighted links (remove expired ones)
float currentTime = (float)ImGui::GetTime();
for (auto it = m_HighlightedLinks.begin(); it != m_HighlightedLinks.end();)
{
if (currentTime >= it->second)
{
it = m_HighlightedLinks.erase(it);
}
else
{
++it;
}
}
// Update running nodes (remove expired ones)
for (auto it = m_RunningNodes.begin(); it != m_RunningNodes.end();)
{
if (currentTime >= it->second)
{
it = m_RunningNodes.erase(it);
}
else
{
++it;
}
}
// Draw links with special styling for parameter connections
// Use links from active root container
auto *container = GetActiveRootContainer();
std::vector<Link *> linksToRender;
if (container)
{
// Use GetLinks() to resolve IDs to pointers (safe from reallocation)
linksToRender = container->GetLinks(this);
}
else
{
// No container - get links from active root container
if (GetActiveRootContainer())
{
linksToRender = GetActiveRootContainer()->GetAllLinks();
}
}
for (auto *linkPtr : linksToRender)
{
if (!linkPtr)
continue;
auto &link = *linkPtr;
// Check if link is selected for visual feedback
bool isSelected = ed::IsLinkSelected(link.ID);
// Check if link is highlighted (from Run() execution)
bool isHighlighted = m_HighlightedLinks.find(link.ID) != m_HighlightedLinks.end();
// Get style manager for link styling
auto& styleMgr = m_StyleManager;
// Use highlighted color if highlighted, otherwise use link's normal color
ImColor linkColor = isHighlighted ? styleMgr.LinkColorHighlighted : link.Color;
// Get link mode and render accordingly
auto linkMode = ed::GetLinkMode(link.ID);
// Calculate thickness based on selection/highlight state from StyleManager
float thickness;
if (link.IsParameterLink)
{
thickness = (isSelected || isHighlighted)
? styleMgr.LinkThicknessParameterSelected
: styleMgr.LinkThicknessParameterNormal;
}
else
{
thickness = (isSelected || isHighlighted)
? styleMgr.LinkThicknessSelected
: styleMgr.LinkThicknessNormal;
}
// Render based on link mode
switch (linkMode)
{
case ax::NodeEditor::LinkMode::Auto:
RenderAutoLink(link, linkColor, thickness);
break;
case ax::NodeEditor::LinkMode::Straight:
RenderStraightLink(link, linkColor, thickness);
break;
case ax::NodeEditor::LinkMode::Guided:
RenderGuidedLink(link, linkColor, thickness);
break;
}
}
// Show delay on hover and render edit box if editing
RenderLinkDelay();
}
// Static state for delay editing (shared between StartEditLinkDelay and RenderLinkDelay)
static ed::LinkId g_EditingLinkId = 0;
static char g_DelayBuffer[64] = "";
static bool g_IsEditingDelay = false;
static ImVec2 g_EditBoxPos = ImVec2(0, 0);
static ed::LinkId g_HoveredLinkForTooltip = 0; // For deferred tooltip rendering
void App::StartEditLinkDelay(ed::LinkId linkId)
{
auto* link = FindLink(linkId);
if (!link)
return;
g_EditingLinkId = linkId;
snprintf(g_DelayBuffer, sizeof(g_DelayBuffer), "%.3f", link->Delay);
g_IsEditingDelay = true;
// Capture mouse position at start of editing (fixed position)
g_EditBoxPos = ImGui::GetMousePos() + ImVec2(10, 10);
}
void App::RenderLinkDelay()
{
// Check if a link is hovered
auto hoveredLinkId = ed::GetHoveredLink();
// If editing, show input box at fixed position
if (g_IsEditingDelay && g_EditingLinkId)
{
auto* link = FindLink(g_EditingLinkId);
if (!link)
{
// Link was deleted, cancel editing
g_IsEditingDelay = false;
g_EditingLinkId = 0;
}
else
{
// Use a popup window instead of tooltip for stable positioning
ImGui::SetNextWindowPos(g_EditBoxPos, ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(40, 0), ImGuiCond_Once);
if (ImGui::Begin("##DelayEdit", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Delay:");
ImGui::SetNextItemWidth(120.0f);
bool inputActive = ImGui::InputText("##delay", g_DelayBuffer, sizeof(g_DelayBuffer),
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_EnterReturnsTrue);
if (inputActive || ImGui::IsItemDeactivatedAfterEdit())
{
// Parse and update delay
float newDelay = 0.0f;
if (sscanf(g_DelayBuffer, "%f", &newDelay) == 1)
{
link->Delay = newDelay;
}
g_IsEditingDelay = false;
g_EditingLinkId = 0;
ImGui::CloseCurrentPopup();
}
// Cancel editing on escape
if (ImGui::IsKeyPressed(ImGuiKey_Escape))
{
g_IsEditingDelay = false;
g_EditingLinkId = 0;
ImGui::CloseCurrentPopup();
}
// Cancel editing on click outside
if (ImGui::IsMouseClicked(0) && !ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow))
{
g_IsEditingDelay = false;
g_EditingLinkId = 0;
ImGui::CloseCurrentPopup();
}
// Keep focus on input
if (ImGui::IsWindowAppearing())
ImGui::SetKeyboardFocusHere(-1);
}
ImGui::End();
}
}
else if (hoveredLinkId && !g_IsEditingDelay)
{
// Show delay on hover (only if delay is non-zero or we want to show it always)
auto* link = FindLink(hoveredLinkId);
if (link && (link->Delay != 0.0f || true)) // Show even if zero for now
{
// Store hovered link for deferred tooltip rendering (outside editor context)
// This will be rendered in RenderDeferredTooltips() where we have proper screen coordinates
g_HoveredLinkForTooltip = hoveredLinkId;
}
}
}
void App::RenderAutoLink(Link& link, ImColor linkColor, float thickness)
{
if (link.IsParameterLink)
{
// Dotted straight line for parameter connections (Auto mode)
ed::PushStyleVar(ed::StyleVar_LinkStrength, 0.0f); // Straight
ed::Link(link.ID, link.StartPinID, link.EndPinID, linkColor, thickness);
ed::PopStyleVar();
}
else
{
// Normal curved (bezier) link for flow connections
ed::Link(link.ID, link.StartPinID, link.EndPinID, linkColor, thickness);
}
}
void App::RenderStraightLink(Link& link, ImColor linkColor, float thickness)
{
// Straight line - use link strength 0 for straight rendering
ed::PushStyleVar(ed::StyleVar_LinkStrength, 0.0f);
ed::Link(link.ID, link.StartPinID, link.EndPinID, linkColor, thickness);
ed::PopStyleVar();
}
void App::RenderGuidedLink(Link& link, ImColor linkColor, float thickness)
{
// Guided link - uses control points for custom path
// The editor will automatically use the control points stored in the link
ed::Link(link.ID, link.StartPinID, link.EndPinID, linkColor, thickness);
}
void App::ApplyPendingGuidedLinks()
{
static bool firstCall = true;
if (firstCall)
{
LOG_TRACE("[CHECKPOINT] ApplyPendingGuidedLinks: Beginning, pending modes={}, pending control points={}",
m_PendingLinkModes.size(), m_PendingGuidedLinks.size());
firstCall = false;
}
// Apply pending link modes first (Straight or Guided)
for (auto& pair : m_PendingLinkModes)
{
ed::SetLinkMode(pair.first, pair.second);
}
m_PendingLinkModes.clear();
// Apply pending guided link control points (only for Guided mode links)
if (m_PendingGuidedLinks.empty())
return;
for (auto &pair : m_PendingGuidedLinks)
{
// Mode should already be set to Guided from above, but ensure it's set
ed::SetLinkMode(pair.first, ax::NodeEditor::LinkMode::Guided);
// Preserve the loaded UserManipulatedWaypoints flag before AddLinkControlPoint
// (which will mark it as user-manipulated via callback)
auto link = FindLink(pair.first);
bool preserveUserManipulated = link && link->UserManipulatedWaypoints;
for (const auto &pt : pair.second)
{
ed::AddLinkControlPoint(pair.first, pt);
}
// Restore the loaded flag if it was true (user-manipulated waypoints from file)
// If it was false (default), keep it false (auto-generated waypoints)
if (link)
{
if (preserveUserManipulated)
{
// Restore the flag that was loaded from file (user-manipulated)
link->UserManipulatedWaypoints = true;
}
else
{
// Flag was false (default), so mark as not user-manipulated (auto-generated waypoints)
// Note: AddLinkControlPoint will mark as user-manipulated, but we override it here
link->UserManipulatedWaypoints = false;
}
}
}
m_PendingGuidedLinks.clear();
}
void App::AutoAdjustLinkWaypoints()
{
static bool firstCall = true;
if (firstCall)
{
// Count links from active root container
auto* activeRoot = GetActiveRootContainer();
size_t linkCount = activeRoot ? activeRoot->m_Links.size() : 0;
LOG_TRACE("[CHECKPOINT] AutoAdjustLinkWaypoints: Beginning, links.size()={}", linkCount);
firstCall = false;
}
// Auto-adjust waypoints for links when connected nodes ACTUALLY move
// BUT: Don't adjust while user is editing the link!
// Use links from active root container
auto *container = GetActiveRootContainer();
std::vector<Link *> linksToProcess;
if (container)
{
// Use GetLinks() to resolve IDs to pointers (safe from reallocation)
linksToProcess = container->GetLinks(this);
}
else
{
// No container - get links from active root container
if (GetActiveRootContainer())
{
linksToProcess = GetActiveRootContainer()->GetAllLinks();
}
}
if (firstCall)
{
LOG_TRACE("[CHECKPOINT] AutoAdjustLinkWaypoints: About to iterate {} links", linksToProcess.size());
}
for (auto *linkPtr : linksToProcess)
{
if (!linkPtr)
continue;
auto &link = *linkPtr;
// Skip auto-adjustment if user has manually manipulated waypoints
if (link.UserManipulatedWaypoints || !ed::IsLinkGuided(link.ID))
continue;
// Skip if this link is currently selected (user might be editing it)
if (ed::IsLinkSelected(link.ID))
continue;
auto startPin = FindPin(link.StartPinID);
auto endPin = FindPin(link.EndPinID);
if (!startPin || !startPin->Node || !endPin || !endPin->Node)
continue;
// Check if either connected node moved or resized
auto startNodePos = ed::GetNodePosition(startPin->Node->ID);
auto startNodeSize = ed::GetNodeSize(startPin->Node->ID);
auto endNodePos = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSize = ed::GetNodeSize(endPin->Node->ID);
bool startChanged = false, endChanged = false;
// Check position changes
if (m_LastNodePositions.count(startPin->Node->ID))
{
auto &lastPos = m_LastNodePositions[startPin->Node->ID];
float threshold = 1.0f; // Only trigger if moved more than 1 pixel
if (std::abs(lastPos.x - startNodePos.x) > threshold || std::abs(lastPos.y - startNodePos.y) > threshold)
startChanged = true;
}
if (m_LastNodePositions.count(endPin->Node->ID))
{
auto &lastPos = m_LastNodePositions[endPin->Node->ID];
float threshold = 1.0f;
if (std::abs(lastPos.x - endNodePos.x) > threshold || std::abs(lastPos.y - endNodePos.y) > threshold)
endChanged = true;
}
// Check size changes (e.g. when toggling display mode)
if (m_LastNodeSizes.count(startPin->Node->ID))
{
auto &lastSize = m_LastNodeSizes[startPin->Node->ID];
float threshold = 1.0f;
if (std::abs(lastSize.x - startNodeSize.x) > threshold || std::abs(lastSize.y - startNodeSize.y) > threshold)
startChanged = true;
}
if (m_LastNodeSizes.count(endPin->Node->ID))
{
auto &lastSize = m_LastNodeSizes[endPin->Node->ID];
float threshold = 1.0f;
if (std::abs(lastSize.x - endNodeSize.x) > threshold || std::abs(lastSize.y - endNodeSize.y) > threshold)
endChanged = true;
}
// Only regenerate if a node actually moved or resized
if (!startChanged && !endChanged)
continue;
// Regenerate waypoints based on current node positions and sizes
// (This ensures paths stay clean when nodes are moved or resized)
// Clear existing waypoints
ed::ClearLinkControlPoints(link.ID);
// Mark as NOT user-manipulated (auto-generated waypoints)
// (ClearLinkControlPoints will mark as user-manipulated, but we override it here)
link.UserManipulatedWaypoints = false;
// Get pin positions and directions using helper functions
ImVec2 startPos = GetPinPosition(startPin, startNodePos, startNodeSize);
ImVec2 endPos = GetPinPosition(endPin, endNodePos, endNodeSize);
ImVec2 startDir = GetPinDirection(startPin);
ImVec2 endDir = GetPinDirection(endPin);
ImVec2 startNodeMin = startNodePos;
ImVec2 startNodeMax = startNodePos + startNodeSize;
ImVec2 endNodeMin = endNodePos;
ImVec2 endNodeMax = endNodePos + endNodeSize;
// Build RoutingContext
PathFinding::RoutingContext ctx;
ctx.StartPin.Position = startPos;
ctx.StartPin.Direction = startDir;
ctx.StartPin.Type = startPin->Type;
ctx.StartPin.Kind = startPin->Kind;
ctx.StartPin.IsFlowPin = (startPin->Type == PinType::Flow);
ctx.StartPin.IsParameterPin = !ctx.StartPin.IsFlowPin;
ctx.EndPin.Position = endPos;
ctx.EndPin.Direction = endDir;
ctx.EndPin.Type = endPin->Type;
ctx.EndPin.Kind = endPin->Kind;
ctx.EndPin.IsFlowPin = (endPin->Type == PinType::Flow);
ctx.EndPin.IsParameterPin = !ctx.EndPin.IsFlowPin;
ctx.StartNode.Min = startNodeMin;
ctx.StartNode.Max = startNodeMax;
ctx.StartNode.Type = startPin->Node ? startPin->Node->Type : NodeType::Blueprint;
ctx.StartNode.BlockType = startPin->Node && startPin->Node->IsBlockBased() ? startPin->Node->BlockType : std::string();
ctx.StartNode.IsGroup = false; // Can be enhanced later
ctx.StartNode.ZPosition = 0.0f; // Can be enhanced later
ctx.EndNode.Min = endNodeMin;
ctx.EndNode.Max = endNodeMax;
ctx.EndNode.Type = endPin->Node ? endPin->Node->Type : NodeType::Blueprint;
ctx.EndNode.BlockType = endPin->Node && endPin->Node->IsBlockBased() ? endPin->Node->BlockType : std::string();
ctx.EndNode.IsGroup = false;
ctx.EndNode.ZPosition = 0.0f;
ctx.Margin = 40.0f;
ctx.Obstacles = {}; // Can be enhanced to populate obstacles later
ctx.EditorContext = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(m_Editor);
ctx.Container = GetActiveRootContainer();
// Add refinement passes (they handle feature flags internally)
ctx.AddRefinementPass(PathFinding::RefinementPass_SmoothPath, nullptr, "Link Fitting");
ctx.AddRefinementPass(PathFinding::RefinementPass_AutoCollapse, nullptr, "Auto Collapse");
// Generate fresh waypoints with refinement
std::vector<ImVec2> waypoints = PathFinding::GenerateWaypoints(ctx);
// Auto-collapse: If refinement pass returned empty waypoints, switch to Straight mode
if (waypoints.empty())
{
ed::SetLinkMode(link.ID, ax::NodeEditor::LinkMode::Straight);
link.UserManipulatedWaypoints = false;
LOG_DEBUG("[AutoAdjustLinkWaypoints] Link {} collapsed to Straight mode (empty waypoints)",
static_cast<long long>(link.ID.Get()));
}
else
{
// Add waypoints
for (const auto &wp : waypoints)
{
ed::AddLinkControlPoint(link.ID, wp);
}
// Mark as NOT user-manipulated (auto-generated waypoints)
// (AddLinkControlPoint will mark as user-manipulated, but we override it here)
link.UserManipulatedWaypoints = false;
}
}
}
void App::UpdateLastNodeSize(ed::NodeId nodeId)
{
// Update the last known size for a specific node
// This is used when display mode changes to ensure AutoAdjustLinkWaypoints can detect the change
m_LastNodeSizes[nodeId] = ed::GetNodeSize(nodeId);
}
void App::UpdateLastNodePositions()
{
static bool firstCall = true;
if (firstCall)
{
LOG_TRACE("[CHECKPOINT] UpdateLastNodePositions: Beginning");
}
// Update last known positions and sizes for all nodes (for next frame's change detection)
// Use nodes from active root container if available, otherwise use m_Nodes
auto *container = GetActiveRootContainer();
std::vector<Node *> nodesToProcess;
if (container)
{
// Use GetNodes() to resolve IDs to pointers (safe from reallocation)
nodesToProcess = container->GetNodes(this);
}
else
{
// No container - get nodes from active root container
auto* activeRoot = GetActiveRootContainer();
if (activeRoot)
{
nodesToProcess = activeRoot->GetAllNodes();
}
}
if (firstCall)
{
LOG_TRACE("[CHECKPOINT] UpdateLastNodePositions: About to iterate {} nodes", nodesToProcess.size());
}
for (auto *nodePtr : nodesToProcess)
{
if (!nodePtr)
continue;
auto &node = *nodePtr;
m_LastNodePositions[node.ID] = ed::GetNodePosition(node.ID);
m_LastNodeSizes[node.ID] = ed::GetNodeSize(node.ID);
}
if (firstCall)
{
LOG_TRACE("[CHECKPOINT] UpdateLastNodePositions: Complete");
firstCall = false;
}
}
void App::UpdateGuidedLinks()
{
static bool firstCall = true;
try
{
ApplyPendingGuidedLinks();
}
catch (...)
{
throw;
}
try
{
AutoAdjustLinkWaypoints();
}
catch (...)
{
throw;
}
try
{
UpdateLastNodePositions();
}
catch (...)
{
throw;
}
if (firstCall)
{
firstCall = false;
}
}
void App::RenderOrdinals()
{
if (!m_ShowOrdinals)
return;
auto editorMin = ImGui::GetItemRectMin();
auto editorMax = ImGui::GetItemRectMax();
int nodeCount = ed::GetNodeCount();
std::vector<ed::NodeId> orderedNodeIds;
orderedNodeIds.resize(static_cast<size_t>(nodeCount));
ed::GetOrderedNodeIds(orderedNodeIds.data(), nodeCount);
auto drawList = ImGui::GetWindowDrawList();
drawList->PushClipRect(editorMin, editorMax);
int ordinal = 0;
for (auto &nodeId : orderedNodeIds)
{
auto p0 = ed::GetNodePosition(nodeId);
auto p1 = p0 + ed::GetNodeSize(nodeId);
p0 = ed::CanvasToScreen(p0);
p1 = ed::CanvasToScreen(p1);
ImGuiTextBuffer builder;
builder.appendf("#%d", ordinal++);
auto textSize = ImGui::CalcTextSize(builder.c_str());
auto padding = ImVec2(2.0f, 2.0f);
auto widgetSize = textSize + padding * 2;
auto widgetPosition = ImVec2(p1.x, p0.y) + ImVec2(0.0f, -widgetSize.y);
drawList->AddRectFilled(widgetPosition, widgetPosition + widgetSize, IM_COL32(100, 80, 80, 190), 3.0f, ImDrawFlags_RoundCornersAll);
drawList->AddRect(widgetPosition, widgetPosition + widgetSize, IM_COL32(200, 160, 160, 190), 3.0f, ImDrawFlags_RoundCornersAll);
drawList->AddText(widgetPosition + padding, IM_COL32(255, 255, 255, 255), builder.c_str());
}
drawList->PopClipRect();
}
void App::DeleteNodeAndInstances(Node &node)
{
int nodeId = (int)node.ID.Get();
LOG_INFO("[DELETE] DeleteNodeAndInstances: Starting deletion of node %d (ptr=%p)",
nodeId, (void *)&node);
// Clear any dangling pin pointers that might reference pins in this node
for (auto &pin : node.Inputs)
{
if (m_HoveredPin == &pin)
m_HoveredPin = nullptr;
if (m_NewLinkPin == &pin)
m_NewLinkPin = nullptr;
if (m_NewNodeLinkPin == &pin)
m_NewNodeLinkPin = nullptr;
}
for (auto &pin : node.Outputs)
{
if (m_HoveredPin == &pin)
m_HoveredPin = nullptr;
if (m_NewLinkPin == &pin)
m_NewLinkPin = nullptr;
if (m_NewNodeLinkPin == &pin)
m_NewNodeLinkPin = nullptr;
}
// Clean up owned instances (full class definitions are available here)
// This properly calls destructors
// Validate pointer before deletion to prevent double-free/corruption
if (node.BlockInstance)
{
// Validate pointer is not a sentinel/corrupted value
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(node.BlockInstance);
if (ptrValue != 0xFFFFFFFFFFFFFFFFULL && ptrValue > 0x1000) // Basic sanity check
{
LOG_INFO("[DELETE] DeleteNodeAndInstances: Deleting BlockInstance for node %d (ptr=%p)",
nodeId, (void*)node.BlockInstance);
try {
delete node.BlockInstance;
} catch (...) {
LOG_ERROR("[DELETE] DeleteNodeAndInstances: Exception during BlockInstance deletion for node %d", nodeId);
}
}
else
{
LOG_ERROR("[DELETE] DeleteNodeAndInstances: BlockInstance pointer invalid/corrupted for node %d (0x%llx)",
nodeId, (unsigned long long)ptrValue);
}
node.BlockInstance = nullptr;
}
if (node.ParameterInstance)
{
// If this is a source node, clean up all shortcuts that reference it
if (node.ParameterInstance->IsSource())
{
LOG_INFO("[DELETE] DeleteNodeAndInstances: Source node %d being deleted, cleaning up shortcuts", nodeId);
// Get all nodes from active root container
auto* container = GetActiveRootContainer();
if (container)
{
auto allNodes = container->GetNodes(this);
// Find all shortcuts that reference this source
for (Node* otherNode : allNodes)
{
if (!otherNode || otherNode->ID == node.ID)
continue;
if (otherNode->Type == NodeType::Parameter && otherNode->ParameterInstance)
{
// Check if this node is a shortcut referencing the deleted source
if (otherNode->ParameterInstance->GetSourceID() == nodeId)
{
LOG_INFO("[DELETE] DeleteNodeAndInstances: Clearing shortcut reference in node %d",
otherNode->ID.Get());
// Clear shortcut reference (becomes independent)
otherNode->ParameterInstance->SetSourceID(0);
otherNode->ParameterInstance->SetIsSource(false);
}
}
}
}
}
// Validate pointer is not a sentinel/corrupted value
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(node.ParameterInstance);
if (ptrValue != 0xFFFFFFFFFFFFFFFFULL && ptrValue > 0x1000) // Basic sanity check
{
LOG_INFO("[DELETE] DeleteNodeAndInstances: Deleting ParameterInstance for node %d (ptr=%p)",
nodeId, (void*)node.ParameterInstance);
try {
delete node.ParameterInstance;
} catch (...) {
LOG_ERROR("[DELETE] DeleteNodeAndInstances: Exception during ParameterInstance deletion for node %d", nodeId);
}
}
else
{
LOG_ERROR("[DELETE] DeleteNodeAndInstances: ParameterInstance pointer invalid/corrupted for node %d (0x%llx)",
nodeId, (unsigned long long)ptrValue);
}
node.ParameterInstance = nullptr;
}
LOG_INFO("[DELETE] DeleteNodeAndInstances: Completed deletion of node %d", nodeId);
}
void App::HandleKeyboardShortcuts(std::vector<ed::NodeId> &selectedNodes, std::vector<ed::LinkId> &selectedLinks)
{
auto &io = ImGui::GetIO();
static bool loggedShortcutState = false;
bool shortcutsEnabled = ed::AreShortcutsEnabled();
bool wantTextInput = io.WantTextInput;
if (!loggedShortcutState)
{
LOG_DEBUG("HandleKeyboardShortcuts: shortcutsEnabled={}, wantTextInput={}",
shortcutsEnabled ? 1 : 0, wantTextInput ? 1 : 0);
loggedShortcutState = true;
}
// Only process shortcuts when enabled and not editing text
if (!shortcutsEnabled || wantTextInput)
return;
// Get selected objects
selectedNodes.resize(ed::GetSelectedObjectCount());
selectedLinks.resize(ed::GetSelectedObjectCount());
int nodeCount = ed::GetSelectedNodes(selectedNodes.data(), static_cast<int>(selectedNodes.size()));
int linkCount = ed::GetSelectedLinks(selectedLinks.data(), static_cast<int>(selectedLinks.size()));
selectedNodes.resize(nodeCount);
selectedLinks.resize(linkCount);
// Arrow keys: Move selected nodes
ImVec2 moveOffset(0, 0);
const float moveStep = io.KeyShift ? 10.0f : 1.0f; // Shift = 10x faster
if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow)))
moveOffset.x = -moveStep;
if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow)))
moveOffset.x = moveStep;
if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow)))
moveOffset.y = -moveStep;
if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow)))
moveOffset.y = moveStep;
if (moveOffset.x != 0 || moveOffset.y != 0)
{
for (auto nodeId : selectedNodes)
{
auto currentPos = ed::GetNodePosition(nodeId);
ed::SetNodePosition(nodeId, currentPos + moveOffset);
}
}
// F12 key: Take screenshot
// Try both old and new ImGui key APIs
bool f12Pressed = false;
#if IMGUI_VERSION_NUM >= 18800
f12Pressed = ImGui::IsKeyPressed(ImGuiKey_F12);
#else
f12Pressed = ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_F12));
#endif
// Also test with ASCII key code as fallback
bool f12PressedAlt = ImGui::IsKeyPressed(0x7B); // VK_F12 = 0x7B
if (f12Pressed || f12PressedAlt)
{
LOG_INFO("App: F12 key detected (f12Pressed=%d, f12PressedAlt=%d), calling TakeScreenshot()",
f12Pressed ? 1 : 0, f12PressedAlt ? 1 : 0);
TakeScreenshot();
}
// Ctrl-S: Save graph (nodes & links)
// Check both Ctrl and Super (Cmd on Mac) for cross-platform support
bool ctrlHeld = io.KeyCtrl || io.KeySuper;
#if IMGUI_VERSION_NUM >= 18800
bool sPressed = ImGui::IsKeyPressed(ImGuiKey_S, false);
#else
bool sPressed = ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_S)) || ImGui::IsKeyPressed('S');
#endif
if (ctrlHeld && sPressed && !io.WantTextInput)
{
LOG_INFO("App: Ctrl-S detected, saving graph...");
auto* activeContainer = GetActiveRootContainer();
if (activeContainer)
{
SaveGraph(m_GraphFilename, activeContainer);
}
LOG_INFO("App: Graph saved successfully");
}
// E key: Edit selected blocks, parameters, or links
if (ImGui::IsKeyPressed('E') && !io.WantTextInput)
{
// First check if a link is selected (priority for delay editing)
if (!selectedLinks.empty())
{
auto linkId = selectedLinks[0]; // Get first selected link
StartEditLinkDelay(linkId);
}
// Otherwise, edit selected nodes (blocks or parameters)
else
{
for (auto nodeId : selectedNodes)
{
auto node = FindNode(nodeId);
if (!node)
continue;
// Edit parameter nodes
if (node->Type == NodeType::Parameter && node->ParameterInstance)
{
OpenParameterEditDialog(node, this);
break; // Only open one dialog at a time
}
// Edit block instances
else if (node->IsBlockBased() && node->BlockInstance)
{
OpenBlockEditDialog(node, this);
break; // Only open one dialog at a time
}
}
}
}
// ALT+I: Add flow input to Group block
if (io.KeyAlt && !io.KeyCtrl && !io.KeyShift && ImGui::IsKeyPressed('I'))
{
if (!selectedNodes.empty())
{
auto nodeId = selectedNodes[0]; // Get first selected node
auto* node = FindNode(nodeId);
if (node && node->IsBlockBased() && node->BlockInstance && node->BlockType == "Group")
{
auto* groupBlock = dynamic_cast<GroupBlock*>(node->BlockInstance);
if (groupBlock)
{
// Count existing flow inputs to generate unique name
int flowInputCount = 0;
for (const auto& pin : node->Inputs)
{
if (pin.Type == PinType::Flow)
flowInputCount++;
}
std::string pinName = (flowInputCount == 0) ? "Execute" : "Execute" + std::to_string(flowInputCount + 1);
groupBlock->AddFlowInput(pinName);
// Rebuild node structure
node->Inputs.clear();
node->Outputs.clear();
groupBlock->Build(*node, this);
}
}
}
}
// ALT+O: Add flow output to Group block
if (io.KeyAlt && !io.KeyCtrl && !io.KeyShift && ImGui::IsKeyPressed('O'))
{
if (!selectedNodes.empty())
{
auto nodeId = selectedNodes[0]; // Get first selected node
auto* node = FindNode(nodeId);
if (node && node->IsBlockBased() && node->BlockInstance && node->BlockType == "Group")
{
auto* groupBlock = dynamic_cast<GroupBlock*>(node->BlockInstance);
if (groupBlock)
{
// Count existing flow outputs to generate unique name
int flowOutputCount = 0;
for (const auto& pin : node->Outputs)
{
if (pin.Type == PinType::Flow)
flowOutputCount++;
}
std::string pinName = (flowOutputCount == 0) ? "Done" : "Done" + std::to_string(flowOutputCount + 1);
groupBlock->AddFlowOutput(pinName);
// Rebuild node structure
node->Inputs.clear();
node->Outputs.clear();
groupBlock->Build(*node, this);
}
}
}
}
// R key: Run selected nodes (blocks or parameter nodes)
// Two modes:
// - R (pressed): Slower execution using key repeat rate
// - Shift+R (held): Fast continuous execution every frame (60fps)
bool shouldRun = false;
bool shiftHeld = io.KeyShift;
if (shiftHeld && ImGui::IsKeyDown('R'))
{
// Shift+R: Fast mode - execute every frame
shouldRun = true;
}
else if (!shiftHeld && ImGui::IsKeyPressed('R', true))
{
// R alone: Slow mode - uses system key repeat rate
shouldRun = true;
}
if (shouldRun)
{
for (auto nodeId : selectedNodes)
{
auto node = FindNode(nodeId);
if (!node)
continue;
// Run block instances
if (node->IsBlockBased() && node->BlockInstance)
{
// Find all output links (flow and parameter) to visualize execution
std::vector<ed::LinkId> affectedLinks;
// Find flow output links (execution flow)
for (const auto &pin : node->Outputs)
{
if (pin.Type == PinType::Flow)
{
auto* activeRoot = GetActiveRootContainer();
if (activeRoot)
{
auto links = activeRoot->GetAllLinks();
for (auto* linkPtr : links)
{
if (linkPtr && linkPtr->StartPinID == pin.ID)
{
affectedLinks.push_back(linkPtr->ID);
}
}
}
}
}
// Find parameter output links (data flow)
for (const auto &pin : node->Outputs)
{
if (pin.Type != PinType::Flow)
{
auto* activeRoot = GetActiveRootContainer();
if (activeRoot)
{
auto links = activeRoot->GetAllLinks();
for (auto* linkPtr : links)
{
if (linkPtr && linkPtr->StartPinID == pin.ID)
{
affectedLinks.push_back(linkPtr->ID);
}
}
}
}
}
// Run the block
int result = node->BlockInstance->Run(*node, this);
node->BlockInstance->ActivateOutput(0, true);
// Verify it was activated
bool isActive = node->BlockInstance->IsOutputActive(0);
// Visualize affected links with red highlight (no bubble animation)
float currentTime = ImGui::GetTime();
float highlightDuration = 0.6f; // 600ms
// Mark node as running (red border)
m_RunningNodes[node->ID] = currentTime + highlightDuration;
// Set red highlight for all affected links
for (auto linkId : affectedLinks)
{
m_HighlightedLinks[linkId] = currentTime + highlightDuration;
}
}
// Run parameter nodes
else if (node->Type == NodeType::Parameter && node->ParameterInstance)
{
int result = node->ParameterInstance->Run(*node, this);
LOG_INFO("Parameter '{}' (ID: {}) Run() returned: {}",
node->Name, node->ID.Get(), result);
}
}
}
// Space key: Cycle display mode (nodes) or cycle link modes (links)
if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Space)))
{
// Cycle link modes for selected links: Auto → Straight → Guided → Auto
for (auto linkId : selectedLinks)
{
auto currentMode = ed::GetLinkMode(linkId);
if (currentMode == ax::NodeEditor::LinkMode::Auto)
{
// Auto → Straight
ed::SetLinkMode(linkId, ax::NodeEditor::LinkMode::Straight);
auto link = FindLink(linkId);
if (link)
link->UserManipulatedWaypoints = false; // Straight mode is not user-manipulated
}
else if (currentMode == ax::NodeEditor::LinkMode::Straight)
{
// Straight → Guided (auto-generate waypoints)
ed::SetLinkMode(linkId, ax::NodeEditor::LinkMode::Guided);
auto linkPtr = FindLink(linkId);
if (linkPtr)
{
linkPtr->UserManipulatedWaypoints = false; // Auto-generated waypoints
auto startPin = FindPin(linkPtr->StartPinID);
auto endPin = FindPin(linkPtr->EndPinID);
if (startPin && startPin->Node && endPin && endPin->Node)
{
auto startNodePos = ed::GetNodePosition(startPin->Node->ID);
auto startNodeSize = ed::GetNodeSize(startPin->Node->ID);
auto endNodePos = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSize = ed::GetNodeSize(endPin->Node->ID);
ImVec2 startPos, endPos, startDir, endDir;
// Determine pin positions and directions using helper functions
startPos = GetPinPosition(startPin, startNodePos, startNodeSize);
startDir = GetPinDirection(startPin);
endPos = GetPinPosition(endPin, endNodePos, endNodeSize);
endDir = GetPinDirection(endPin);
ImVec2 startNodeMin = startNodePos;
ImVec2 startNodeMax = startNodePos + startNodeSize;
ImVec2 endNodeMin = endNodePos;
ImVec2 endNodeMax = endNodePos + endNodeSize;
// Validate node bounds before calling GenerateWaypoints
const float MAX_REASONABLE_COORD = 100000.0f;
bool startNodeValid = (std::abs(startNodePos.x) < MAX_REASONABLE_COORD &&
std::abs(startNodePos.y) < MAX_REASONABLE_COORD &&
startNodeSize.x > 0 && startNodeSize.y > 0);
bool endNodeValid = (std::abs(endNodePos.x) < MAX_REASONABLE_COORD &&
std::abs(endNodePos.y) < MAX_REASONABLE_COORD &&
endNodeSize.x > 0 && endNodeSize.y > 0);
if (!startNodeValid || !endNodeValid)
{
LOG_WARN("[GenerateWaypoints VALIDATION] HandleKeyboardShortcuts (Space key): Skipping - invalid node bounds");
LOG_WARN(" startNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
static_cast<long long>(startPin->Node ? startPin->Node->ID.Get() : -1),
startNodePos.x, startNodePos.y, startNodeSize.x, startNodeSize.y, startNodeValid);
LOG_WARN(" endNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
static_cast<long long>(endPin->Node ? endPin->Node->ID.Get() : -1),
endNodePos.x, endNodePos.y, endNodeSize.x, endNodeSize.y, endNodeValid);
}
else
{
// Build RoutingContext
PathFinding::RoutingContext ctx;
ctx.StartPin.Position = startPos;
ctx.StartPin.Direction = startDir;
ctx.StartPin.Type = startPin->Type;
ctx.StartPin.Kind = startPin->Kind;
ctx.StartPin.IsFlowPin = (startPin->Type == PinType::Flow);
ctx.StartPin.IsParameterPin = !ctx.StartPin.IsFlowPin;
ctx.EndPin.Position = endPos;
ctx.EndPin.Direction = endDir;
ctx.EndPin.Type = endPin->Type;
ctx.EndPin.Kind = endPin->Kind;
ctx.EndPin.IsFlowPin = (endPin->Type == PinType::Flow);
ctx.EndPin.IsParameterPin = !ctx.EndPin.IsFlowPin;
ctx.StartNode.Min = startNodeMin;
ctx.StartNode.Max = startNodeMax;
ctx.StartNode.Type = startPin->Node ? startPin->Node->Type : NodeType::Blueprint;
ctx.StartNode.BlockType = startPin->Node && startPin->Node->IsBlockBased() ? startPin->Node->BlockType : std::string();
ctx.StartNode.IsGroup = false;
ctx.StartNode.ZPosition = 0.0f;
ctx.EndNode.Min = endNodeMin;
ctx.EndNode.Max = endNodeMax;
ctx.EndNode.Type = endPin->Node ? endPin->Node->Type : NodeType::Blueprint;
ctx.EndNode.BlockType = endPin->Node && endPin->Node->IsBlockBased() ? endPin->Node->BlockType : std::string();
ctx.EndNode.IsGroup = false;
ctx.EndNode.ZPosition = 0.0f;
ctx.Margin = 40.0f;
ctx.Obstacles = {};
ctx.EditorContext = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(m_Editor);
ctx.Container = GetActiveRootContainer();
// Add refinement passes (they handle feature flags internally)
ctx.AddRefinementPass(PathFinding::RefinementPass_SmoothPath, nullptr, "Link Fitting");
ctx.AddRefinementPass(PathFinding::RefinementPass_AutoCollapse, nullptr, "Auto Collapse");
// Generate waypoints using pathfinding
auto waypoints = PathFinding::GenerateWaypoints(ctx);
// Auto-collapse: If refinement pass returned empty waypoints, switch to Straight mode
if (waypoints.empty())
{
ed::SetLinkMode(linkId, ax::NodeEditor::LinkMode::Straight);
if (linkPtr)
linkPtr->UserManipulatedWaypoints = false;
LOG_DEBUG("[HandleKeyboardShortcuts] Link {} collapsed to Straight mode (empty waypoints)",
static_cast<long long>(linkId.Get()));
}
else
{
// Add waypoints to the link
for (const auto &wp : waypoints)
{
ed::AddLinkControlPoint(linkId, wp);
}
// Mark as NOT user-manipulated (auto-generated waypoints)
// (AddLinkControlPoint will mark as user-manipulated, but we override it here)
if (linkPtr)
linkPtr->UserManipulatedWaypoints = false;
}
}
}
}
}
else // LinkMode::Guided
{
// Guided → Auto (clear waypoints)
ed::SetLinkMode(linkId, ax::NodeEditor::LinkMode::Auto);
auto link = FindLink(linkId);
if (link)
link->UserManipulatedWaypoints = false; // Auto mode is not user-manipulated
}
}
// Cycle display mode for selected nodes
for (auto nodeId : selectedNodes)
{
auto node = FindNode(nodeId);
if (!node)
continue;
// Parameter nodes
if (node->Type == NodeType::Parameter && node->ParameterInstance)
{
node->ParameterInstance->CycleDisplayMode();
// Notify editor that display mode changed (triggers link auto-adjustment)
ed::NotifyBlockDisplayModeChanged(node->ID);
}
// Group blocks - toggle between Collapsed and Expanded
else if (node->IsBlockBased() && node->BlockType == "Group" && node->BlockInstance)
{
auto* groupBlock = dynamic_cast<GroupBlock*>(node->BlockInstance);
if (groupBlock)
{
groupBlock->ToggleDisplayMode();
// Notify editor that display mode changed (triggers link auto-adjustment)
ed::NotifyBlockDisplayModeChanged(node->ID);
}
}
// Other block nodes
else if (node->IsBlockBased())
{
if (node->BlockDisplay == BlockDisplayMode::NameOnly)
node->BlockDisplay = BlockDisplayMode::NameAndParameters;
else
node->BlockDisplay = BlockDisplayMode::NameOnly;
// Notify editor that display mode changed (triggers link auto-adjustment)
ed::NotifyBlockDisplayModeChanged(node->ID);
}
}
}
}
void App::HandleLinkCreationAndDeletion()
{
if (!m_CreateNewNode)
{
if (ed::BeginCreate(ImColor(255, 255, 255), 2.0f))
{
auto showLabel = [](const char *label, ImColor color)
{
ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight());
auto size = ImGui::CalcTextSize(label);
auto padding = ImGui::GetStyle().FramePadding;
auto spacing = ImGui::GetStyle().ItemSpacing;
ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(spacing.x, -spacing.y));
auto rectMin = ImGui::GetCursorScreenPos() - padding;
auto rectMax = ImGui::GetCursorScreenPos() + size + padding;
auto drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(rectMin, rectMax, color, size.y * 0.15f);
ImGui::TextUnformatted(label);
};
ed::PinId startPinId = 0, endPinId = 0;
if (ed::QueryNewLink(&startPinId, &endPinId))
{
auto startPin = FindPin(startPinId);
auto endPin = FindPin(endPinId);
m_NewLinkPin = startPin ? startPin : endPin;
if (startPin && startPin->Kind == PinKind::Input)
{
LOG_DEBUG("[LINK_DRAG] Swapping pins (startPin is Input)");
std::swap(startPin, endPin);
std::swap(startPinId, endPinId);
LOG_DEBUG("[LINK_DRAG] After swap: startPin={:p} (Kind={}), endPin={:p} (Kind={})",
static_cast<void*>(startPin),
startPin ? (startPin->Kind == PinKind::Output ? "Output" : "Input") : "null",
static_cast<void*>(endPin),
endPin ? (endPin->Kind == PinKind::Output ? "Output" : "Input") : "null");
}
// Draw preview waypoints while dragging (even before dropping on target pin)
if (startPin && startPin->Node)
{
// Calculate preview waypoints from start pin to mouse or end pin
auto startNodePos = ed::GetNodePosition(startPin->Node->ID);
auto startNodeSize = ed::GetNodeSize(startPin->Node->ID);
// Use helper functions to get correct pin positions
ImVec2 startPos = GetPinPosition(startPin, startNodePos, startNodeSize);
ImVec2 startDir = GetPinDirection(startPin);
ImVec2 endPos, endDir;
ImVec2 endNodeMin, endNodeMax;
// End position: use end pin if valid, otherwise use mouse position
if (endPin && endPin->Node)
{
auto endNodePos = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSize = ed::GetNodeSize(endPin->Node->ID);
endPos = GetPinPosition(endPin, endNodePos, endNodeSize);
endDir = GetPinDirection(endPin);
endNodeMin = endNodePos;
endNodeMax = endNodePos + endNodeSize;
}
else
{
// Dragging to empty space - use mouse position as end point
endPos = ed::ScreenToCanvas(ImGui::GetMousePos());
// Infer direction based on start direction (opposite for proper connection)
endDir = ImVec2(-startDir.x, -startDir.y);
// No end node, so create dummy bounds around mouse
endNodeMin = endPos - ImVec2(10, 10);
endNodeMax = endPos + ImVec2(10, 10);
}
ImVec2 startNodeMin = startNodePos;
ImVec2 startNodeMax = startNodePos + startNodeSize;
// Validate node bounds before calling GenerateWaypoints
const float MAX_REASONABLE_COORD_PREVIEW = 100000.0f;
bool startNodeValidPreview = (std::abs(startNodePos.x) < MAX_REASONABLE_COORD_PREVIEW &&
std::abs(startNodePos.y) < MAX_REASONABLE_COORD_PREVIEW &&
startNodeSize.x > 0 && startNodeSize.y > 0);
bool endNodeValidPreview = true;
if (endPin && endPin->Node)
{
auto endNodePosForValidation = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSizeForValidation = ed::GetNodeSize(endPin->Node->ID);
endNodeValidPreview = (std::abs(endNodePosForValidation.x) < MAX_REASONABLE_COORD_PREVIEW &&
std::abs(endNodePosForValidation.y) < MAX_REASONABLE_COORD_PREVIEW &&
endNodeSizeForValidation.x > 0 && endNodeSizeForValidation.y > 0);
}
// Allow preview when dragging to empty space (endPin is null or endPin->Node is null)
if (!startNodeValidPreview || !endNodeValidPreview)
{
LOG_WARN("[GenerateWaypoints VALIDATION] HandleLinkCreationAndDeletion (preview): Skipping - invalid node bounds");
LOG_WARN(" startNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
startPin->Node->ID.Get(), startNodePos.x, startNodePos.y,
startNodeSize.x, startNodeSize.y, startNodeValidPreview);
if (endPin && endPin->Node)
{
auto endNodePosForValidation = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSizeForValidation = ed::GetNodeSize(endPin->Node->ID);
LOG_WARN(" endNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
endPin->Node->ID.Get(), endNodePosForValidation.x, endNodePosForValidation.y,
endNodeSizeForValidation.x, endNodeSizeForValidation.y, endNodeValidPreview);
}
}
else
{
// Build RoutingContext
PathFinding::RoutingContext ctx;
ctx.StartPin.Position = startPos;
ctx.StartPin.Direction = startDir;
ctx.StartPin.Type = startPin->Type;
ctx.StartPin.Kind = startPin->Kind;
ctx.StartPin.IsFlowPin = (startPin->Type == PinType::Flow);
ctx.StartPin.IsParameterPin = !ctx.StartPin.IsFlowPin;
ctx.EndPin.Position = endPos;
ctx.EndPin.Direction = endDir;
ctx.EndPin.Type = endPin ? endPin->Type : PinType::Flow;
ctx.EndPin.Kind = endPin ? endPin->Kind : PinKind::Input;
ctx.EndPin.IsFlowPin = (endPin && endPin->Type == PinType::Flow);
ctx.EndPin.IsParameterPin = !ctx.EndPin.IsFlowPin;
ctx.StartNode.Min = startNodeMin;
ctx.StartNode.Max = startNodeMax;
ctx.StartNode.Type = startPin->Node ? startPin->Node->Type : NodeType::Blueprint;
ctx.StartNode.BlockType = startPin->Node && startPin->Node->IsBlockBased() ? startPin->Node->BlockType : std::string();
ctx.StartNode.IsGroup = false;
ctx.StartNode.ZPosition = 0.0f;
ctx.EndNode.Min = endNodeMin;
ctx.EndNode.Max = endNodeMax;
ctx.EndNode.Type = (endPin && endPin->Node) ? endPin->Node->Type : NodeType::Blueprint;
ctx.EndNode.BlockType = (endPin && endPin->Node && endPin->Node->IsBlockBased()) ? endPin->Node->BlockType : std::string();
ctx.EndNode.IsGroup = false;
ctx.EndNode.ZPosition = 0.0f;
ctx.Margin = 40.0f;
ctx.Obstacles = {};
ctx.EditorContext = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(m_Editor);
ctx.Container = GetActiveRootContainer();
// Generate preview waypoints
auto previewWaypoints = PathFinding::GenerateWaypoints(ctx);
// Draw preview waypoints as small circles
if (!previewWaypoints.empty())
{
auto drawList = ImGui::GetWindowDrawList();
// Use waypoint preview styling from StyleManager
auto& styleMgr = m_StyleManager;
const float wpRadius = styleMgr.WaypointPreviewRadius;
const ImU32 wpColor = styleMgr.WaypointPreviewColor;
const ImU32 wpBorderColor = styleMgr.WaypointPreviewBorderColor;
for (const auto &wp : previewWaypoints)
{
auto screenPos = ed::CanvasToScreen(wp);
drawList->AddCircleFilled(screenPos, wpRadius, wpColor);
drawList->AddCircle(screenPos, wpRadius, wpBorderColor, 0, 1.5f);
}
}
}
}
if (startPin && endPin)
{
if (endPin == startPin)
{
LOG_DEBUG("[LINK_DRAG] REJECT: Same pin");
ed::RejectNewItem(ImColor(255, 0, 0), 2.0f);
}
else if (endPin->Kind == startPin->Kind)
{
LOG_DEBUG("[LINK_DRAG] REJECT: Same pin kind (both {})",
startPin->Kind == PinKind::Output ? "Output" : "Input");
showLabel("x Incompatible Pin Kind", ImColor(45, 32, 32, 180));
ed::RejectNewItem(ImColor(255, 0, 0), 2.0f);
}
else if (endPin->Type != startPin->Type)
{
LOG_DEBUG("[LINK_DRAG] REJECT: Different pin types (start={}, end={})",
static_cast<int>(startPin->Type), static_cast<int>(endPin->Type));
showLabel("x Incompatible Pin Type", ImColor(45, 32, 32, 180));
ed::RejectNewItem(ImColor(255, 128, 128), 1.0f);
}
else
{
// Check for duplicate link
if (IsLinkDuplicate(startPinId, endPinId))
{
LOG_DEBUG("[LINK_DRAG] REJECT: Duplicate link exists");
showLabel("x Duplicate Link", ImColor(45, 32, 32, 180));
ed::RejectNewItem(ImColor(255, 128, 128), 2.0f);
}
else
{
showLabel("+ Create Link", ImColor(32, 45, 32, 180));
if (ed::AcceptNewItem(ImColor(128, 255, 128), 4.0f))
{
// Get active root container for ID generation
auto *container = GetActiveRootContainer();
if (!container)
{
LOG_ERROR("[LINK_DRAG] ERROR: No active root container for ID generation!");
// Can't create link without container - skip this iteration
}
else
{
// Use container's ID generator (same as nodes) to prevent ID collisions
ed::LinkId newLinkId = ed::LinkId(container->GetNextId());
LOG_DEBUG("[LINK_DRAG] Generated link ID={} from container (nextId will be {})",
static_cast<long long>(newLinkId.Get()), container->GetCurrentId());
// Create link in container's map indexed by ID
auto* rootContainer = dynamic_cast<RootContainer*>(container);
if (!rootContainer)
{
LOG_ERROR("[LINK_DRAG] ERROR: Container is not RootContainer");
// Skip link creation
}
else
{
// Use AddLink method instead of direct map access for safety
Link newLink(newLinkId, startPinId, endPinId);
// Generate UUID for link
newLink.UUID = m_UuidIdManager.GenerateUuid();
Link *createdLink = rootContainer->AddLink(newLink);
if (!createdLink)
{
LOG_ERROR("[LINK_DRAG] ERROR - link with ID {} already exists or insertion failed",
static_cast<long long>(newLinkId.Get()));
// Skip rest of link creation if insertion failed
}
else
{
// Register link UUID mapping
m_UuidIdManager.RegisterLink(createdLink->UUID, ToRuntimeId(createdLink->ID));
createdLink->Color = GetIconColor(startPin->Type);
LOG_DEBUG("[LINK_DRAG] Link created in container, ptr={:p}, ID={}",
static_cast<void*>(createdLink), static_cast<long long>(newLinkId.Get()));
// Note: AddLink already adds to m_LinkIds, no need to add again
LOG_DEBUG("[LINK_DRAG] Link successfully added to container");
// Mark as parameter link if connecting parameter to block
if (startPin->Node && startPin->Node->Type == NodeType::Parameter &&
endPin->Node && endPin->Node->IsBlockBased())
{
createdLink->IsParameterLink = true;
LOG_DEBUG("[LINK_DRAG] Marked as parameter link");
}
LOG_DEBUG("[LINK_DRAG] Link creation complete!");
// Auto-create guided waypoints for new links
ed::SetLinkGuided(createdLink->ID, true);
createdLink->UserManipulatedWaypoints = false; // Auto-generated waypoints
// Generate initial waypoints based on current node positions
if (startPin->Node && endPin->Node)
{
auto startNodePos = ed::GetNodePosition(startPin->Node->ID);
auto startNodeSize = ed::GetNodeSize(startPin->Node->ID);
auto endNodePos = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSize = ed::GetNodeSize(endPin->Node->ID);
// Use helper functions to get correct pin positions
ImVec2 startPos = GetPinPosition(startPin, startNodePos, startNodeSize);
ImVec2 endPos = GetPinPosition(endPin, endNodePos, endNodeSize);
ImVec2 startDir = GetPinDirection(startPin);
ImVec2 endDir = GetPinDirection(endPin);
ImVec2 startNodeMin = startNodePos;
ImVec2 startNodeMax = startNodePos + startNodeSize;
ImVec2 endNodeMin = endNodePos;
ImVec2 endNodeMax = endNodePos + endNodeSize;
// Validate node bounds before calling GenerateWaypoints
const float MAX_REASONABLE_COORD_NEWLINK = 100000.0f;
bool startNodeValidNewLink = (std::abs(startNodePos.x) < MAX_REASONABLE_COORD_NEWLINK &&
std::abs(startNodePos.y) < MAX_REASONABLE_COORD_NEWLINK &&
startNodeSize.x > 0 && startNodeSize.y > 0);
bool endNodeValidNewLink = (std::abs(endNodePos.x) < MAX_REASONABLE_COORD_NEWLINK &&
std::abs(endNodePos.y) < MAX_REASONABLE_COORD_NEWLINK &&
endNodeSize.x > 0 && endNodeSize.y > 0);
if (!startNodeValidNewLink || !endNodeValidNewLink)
{
auto endNodePosForValidation = ed::GetNodePosition(endPin->Node->ID);
LOG_WARN("[GenerateWaypoints VALIDATION] HandleLinkCreationAndDeletion (new link): Skipping - invalid node bounds");
LOG_WARN(" startNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
startPin->Node->ID.Get(), startNodePos.x, startNodePos.y,
startNodeSize.x, startNodeSize.y, startNodeValidNewLink);
LOG_WARN(" endNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
endPin->Node->ID.Get(), endNodePos.x, endNodePos.y,
endNodeSize.x, endNodeSize.y, endNodeValidNewLink);
}
else
{
// Build RoutingContext
PathFinding::RoutingContext ctx;
ctx.StartPin.Position = startPos;
ctx.StartPin.Direction = startDir;
ctx.StartPin.Type = startPin->Type;
ctx.StartPin.Kind = startPin->Kind;
ctx.StartPin.IsFlowPin = (startPin->Type == PinType::Flow);
ctx.StartPin.IsParameterPin = !ctx.StartPin.IsFlowPin;
ctx.EndPin.Position = endPos;
ctx.EndPin.Direction = endDir;
ctx.EndPin.Type = endPin->Type;
ctx.EndPin.Kind = endPin->Kind;
ctx.EndPin.IsFlowPin = (endPin->Type == PinType::Flow);
ctx.EndPin.IsParameterPin = !ctx.EndPin.IsFlowPin;
ctx.StartNode.Min = startNodeMin;
ctx.StartNode.Max = startNodeMax;
ctx.StartNode.Type = startPin->Node ? startPin->Node->Type : NodeType::Blueprint;
ctx.StartNode.BlockType = startPin->Node && startPin->Node->IsBlockBased() ? startPin->Node->BlockType : std::string();
ctx.StartNode.IsGroup = false;
ctx.StartNode.ZPosition = 0.0f;
ctx.EndNode.Min = endNodeMin;
ctx.EndNode.Max = endNodeMax;
ctx.EndNode.Type = endPin->Node ? endPin->Node->Type : NodeType::Blueprint;
ctx.EndNode.BlockType = endPin->Node && endPin->Node->IsBlockBased() ? endPin->Node->BlockType : std::string();
ctx.EndNode.IsGroup = false;
ctx.EndNode.ZPosition = 0.0f;
ctx.Margin = 40.0f;
ctx.Obstacles = {};
ctx.EditorContext = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(m_Editor);
ctx.Container = GetActiveRootContainer();
// Add refinement passes (they handle feature flags internally)
ctx.AddRefinementPass(PathFinding::RefinementPass_SmoothPath, nullptr, "Link Fitting");
ctx.AddRefinementPass(PathFinding::RefinementPass_AutoCollapse, nullptr, "Auto Collapse");
// Generate waypoints using pathfinding
auto waypoints = PathFinding::GenerateWaypoints(ctx);
// Auto-collapse: If refinement pass returned empty waypoints, switch to Straight mode
if (waypoints.empty())
{
ed::SetLinkMode(createdLink->ID, ax::NodeEditor::LinkMode::Straight);
createdLink->UserManipulatedWaypoints = false;
LOG_DEBUG("[HandleLinkCreationAndDeletion] Link {} collapsed to Straight mode (empty waypoints)",
static_cast<long long>(createdLink->ID.Get()));
}
else
{
// Add waypoints to the new link
for (const auto &wp : waypoints)
{
ed::AddLinkControlPoint(createdLink->ID, wp);
}
// Reset flag after adding waypoints (AddLinkControlPoint triggers callback)
createdLink->UserManipulatedWaypoints = false;
}
}
}
} // Close else block for linkInserted
} // Close else block for rootContainer
} // Close else block for container
} // Close if (ed::AcceptNewItem)
} // Close else block for duplicate check
} // Close else block for incompatible pin types
} // Close else block for startPin == endPin check
} // Close while (ed::QueryNewLink)
ed::EndCreate();
ed::PinId pinId = 0;
if (ed::QueryNewNode(&pinId))
{
m_NewLinkPin = FindPin(pinId);
if (m_NewLinkPin)
showLabel("+ Create Node", ImColor(32, 45, 32, 180));
if (ed::AcceptNewItem())
{
m_CreateNewNode = true;
m_NewNodeLinkPin = FindPin(pinId);
m_NewLinkPin = nullptr;
ed::Suspend();
ImGui::OpenPopup("Create New Node");
ed::Resume();
}
}
}
else
m_NewLinkPin = nullptr;
ed::EndCreate();
if (ed::BeginDelete())
{
ed::NodeId nodeId = 0;
while (ed::QueryDeletedNode(&nodeId))
{
if (ed::AcceptDeletedItem())
{
// Find node in active root container (nodes are stored there)
Node *nodePtr = nullptr;
if (GetActiveRootContainer())
{
nodePtr = GetActiveRootContainer()->FindNode(nodeId);
}
if (nodePtr)
{
int nodeIdInt = nodePtr->ID.Get();
LOG_INFO("[DELETE] OnFrame: User deleted node {} (ptr={:p}) via editor",
nodeIdInt, static_cast<void*>(nodePtr));
// Remove from active root container FIRST (before deleting)
// CRITICAL: Delete instances BEFORE removing node from map
// RemoveNode erases the node from the map, which destroys the Node object
// So we must clean up instances while the node still exists
LOG_DEBUG("[DELETE] OnFrame: Deleting instances for node {}", nodeIdInt);
DeleteNodeAndInstances(*nodePtr);
// Now remove node from container (this will destroy the Node object)
LOG_DEBUG("[DELETE] OnFrame: Removing node {} from container", nodeIdInt);
GetActiveRootContainer()->RemoveNode(nodeId);
LOG_INFO("[DELETE] OnFrame: Node {} fully deleted", nodeIdInt);
}
else
{
LOG_WARN("[DELETE] OnFrame: Node {} not found in active root container", nodeId.Get());
}
}
}
ed::LinkId linkId = 0;
while (ed::QueryDeletedLink(&linkId))
{
if (ed::AcceptDeletedItem())
{
LOG_INFO("[DELETE] OnFrame: User deleted link {} via editor", static_cast<long long>(linkId.Get()));
// Links are now stored in RootContainer, so just remove from container
if (GetActiveRootContainer())
{
LOG_DEBUG("[DELETE] OnFrame: Removing link {} from container", static_cast<long long>(linkId.Get()));
GetActiveRootContainer()->RemoveLink(linkId);
LOG_INFO("[DELETE] OnFrame: Link {} fully deleted", static_cast<long long>(linkId.Get()));
}
else
{
LOG_WARN("[DELETE] OnFrame: Link {} not found in m_Links!", static_cast<long long>(linkId.Get()));
}
}
}
}
ed::EndDelete();
}
}
void App::RenderDeferredTooltips()
{
// Deferred tooltips (must be in Suspend to use screen coordinates)
ed::Suspend();
// Check if any pin is being hovered and show value tooltip
// Iterate through all nodes and check if their pins are being hovered
auto* container = GetActiveRootContainer();
if (container && !m_HoveredPin)
{
auto nodes = container->GetAllNodes();
ImVec2 mousePos = ImGui::GetMousePos();
ImVec2 mouseCanvasPos = ed::ScreenToCanvas(mousePos);
for (auto* node : nodes)
{
if (!node) continue;
// Check all input pins
for (auto& pin : node->Inputs)
{
if (pin.Type == PinType::Flow) continue; // Skip flow pins
if (pin.HasPositionData && pin.LastRenderBounds.Contains(mouseCanvasPos))
{
// Pin is hovered - show tooltip with value
char tooltip[512];
std::string valueStr;
// Get pin value
if (node->Type == NodeType::Parameter && node->ParameterInstance)
{
// Parameter node - get value from node structure
switch (pin.Type)
{
case PinType::Bool: valueStr = node->BoolValue ? "true" : "false"; break;
case PinType::Int: valueStr = std::to_string(node->IntValue); break;
case PinType::Float:
{
char buf[32];
snprintf(buf, sizeof(buf), "%.3f", node->FloatValue);
valueStr = buf;
break;
}
case PinType::String: valueStr = node->StringValue; break;
default: valueStr = "?"; break;
}
}
else if (node->IsBlockBased())
{
// Block node - get value from UnconnectedParamValues or connected source
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node->UnconnectedParamValues;
// Check if connected
auto* link = FindLinkConnectedToPin(pin.ID);
if (link && link->EndPinID == pin.ID)
{
auto* sourcePin = FindPin(link->StartPinID);
if (sourcePin && sourcePin->Node && sourcePin->Node->Type == NodeType::Parameter)
{
// Get value from source parameter node
switch (pin.Type)
{
case PinType::Bool: valueStr = sourcePin->Node->BoolValue ? "true" : "false"; break;
case PinType::Int: valueStr = std::to_string(sourcePin->Node->IntValue); break;
case PinType::Float:
{
char buf[32];
snprintf(buf, sizeof(buf), "%.3f", sourcePin->Node->FloatValue);
valueStr = buf;
break;
}
case PinType::String: valueStr = sourcePin->Node->StringValue; break;
default: valueStr = "[Connected]"; break;
}
}
else
{
valueStr = "[Connected]";
}
}
else if (paramValues.find(pinId) != paramValues.end())
{
valueStr = paramValues[pinId];
}
else
{
// Default value
switch (pin.Type)
{
case PinType::Bool: valueStr = "false"; break;
case PinType::Int: valueStr = "0"; break;
case PinType::Float: valueStr = "0.0"; break;
case PinType::String: valueStr = ""; break;
default: valueStr = "?"; break;
}
}
}
snprintf(tooltip, sizeof(tooltip), "%s (%s)\nValue: %s",
pin.Name.c_str(),
pin.Type == PinType::Bool ? "Bool" :
pin.Type == PinType::Int ? "Int" :
pin.Type == PinType::Float ? "Float" :
pin.Type == PinType::String ? "String" : "Unknown",
valueStr.c_str());
ImGui::SetTooltip("%s", tooltip);
goto tooltip_done; // Exit after showing one tooltip
}
}
// Check all output pins
for (auto& pin : node->Outputs)
{
if (pin.Type == PinType::Flow) continue; // Skip flow pins
if (pin.HasPositionData && pin.LastRenderBounds.Contains(mouseCanvasPos))
{
// Pin is hovered - show tooltip with value
char tooltip[512];
std::string valueStr;
// Get pin value
if (node->Type == NodeType::Parameter && node->ParameterInstance)
{
// Parameter node - get value from node structure
switch (pin.Type)
{
case PinType::Bool: valueStr = node->BoolValue ? "true" : "false"; break;
case PinType::Int: valueStr = std::to_string(node->IntValue); break;
case PinType::Float:
{
char buf[32];
snprintf(buf, sizeof(buf), "%.3f", node->FloatValue);
valueStr = buf;
break;
}
case PinType::String: valueStr = node->StringValue; break;
default: valueStr = "?"; break;
}
}
else if (node->IsBlockBased())
{
// Block node - get value from UnconnectedParamValues (output values)
const int pinId = ToRuntimeId(pin.ID);
auto& paramValues = node->UnconnectedParamValues;
if (paramValues.find(pinId) != paramValues.end())
{
valueStr = paramValues[pinId];
}
else
{
// Default value
switch (pin.Type)
{
case PinType::Bool: valueStr = "false"; break;
case PinType::Int: valueStr = "0"; break;
case PinType::Float: valueStr = "0.0"; break;
case PinType::String: valueStr = ""; break;
default: valueStr = "?"; break;
}
}
}
snprintf(tooltip, sizeof(tooltip), "%s (%s)\nValue: %s",
pin.Name.c_str(),
pin.Type == PinType::Bool ? "Bool" :
pin.Type == PinType::Int ? "Int" :
pin.Type == PinType::Float ? "Float" :
pin.Type == PinType::String ? "String" : "Unknown",
valueStr.c_str());
ImGui::SetTooltip("%s", tooltip);
goto tooltip_done; // Exit after showing one tooltip
}
}
}
}
tooltip_done:
// Legacy tooltip system (still used by some renderers)
if (m_HoveredPin && !m_HoveredPinTooltip.empty())
{
ImGui::SetTooltip("%s", m_HoveredPinTooltip.c_str());
}
// Render delay tooltip if link is hovered
if (g_HoveredLinkForTooltip && !g_IsEditingDelay)
{
auto* link = FindLink(g_HoveredLinkForTooltip);
if (link && (link->Delay != 0.0f || true)) // Show even if zero for now
{
// Position tooltip slightly left and up from mouse cursor (in screen space)
ImVec2 mousePos = ImGui::GetMousePos();
ImVec2 tooltipPos = mousePos + ImVec2(-20, -80); // Left and up offset (moved up by 50)
ImGui::SetNextWindowPos(tooltipPos);
ImGui::BeginTooltip();
char delayText[64];
snprintf(delayText, sizeof(delayText), "Delay: %.3f\nPress E to edit", link->Delay);
ImGui::TextUnformatted(delayText);
ImGui::EndTooltip();
}
}
ed::Resume();
// Clear for next frame
m_HoveredPin = nullptr;
m_HoveredPinTooltip.clear();
g_HoveredLinkForTooltip = 0;
}
void App::RenderContextMenus()
{
#if 1
// Don't show context menus if edit dialog is open
if (IsBlockEditDialogOpen() || IsParameterEditDialogOpen())
return;
auto openPopupPosition = ImGui::GetMousePos();
ed::Suspend();
if (ed::ShowNodeContextMenu(&m_ContextNodeId))
ImGui::OpenPopup("Node Context Menu");
else if (ed::ShowPinContextMenu(&m_ContextPinId))
ImGui::OpenPopup("Pin Context Menu");
else if (ed::ShowLinkContextMenu(&m_ContextLinkId))
ImGui::OpenPopup("Link Context Menu");
else if (ed::ShowBackgroundContextMenu())
{
ImGui::OpenPopup("Create New Node");
m_NewNodeLinkPin = nullptr;
}
ed::Resume();
ed::Suspend();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
if (ImGui::BeginPopup("Node Context Menu"))
{
auto node = FindNode(m_ContextNodeId);
ImGui::TextUnformatted("Node Context Menu");
ImGui::Separator();
if (node)
{
ImGui::Text("ID: %p", node->ID.AsPointer());
const char *typeStr = "Unknown";
if (node->Type == NodeType::Blueprint)
typeStr = "Blueprint";
else if (node->Type == NodeType::Group)
typeStr = "Group";
else if (node->Type == NodeType::Tree)
typeStr = "Tree";
else if (node->Type == NodeType::Comment)
typeStr = "Comment";
else if (node->Type == NodeType::Parameter)
typeStr = "Parameter";
else if (node->Type == NodeType::Houdini)
typeStr = "Houdini";
ImGui::Text("Type: %s", typeStr);
ImGui::Text("Inputs: %d", (int)node->Inputs.size());
ImGui::Text("Outputs: %d", (int)node->Outputs.size());
// Let nodes add their own menu items
if (node->Type == NodeType::Parameter && node->ParameterInstance)
{
node->ParameterInstance->OnMenu(*node, this);
// Add Edit option for parameter nodes
ImGui::Separator();
if (ImGui::MenuItem("Edit (E)"))
{
OpenParameterEditDialog(node, this);
ImGui::CloseCurrentPopup();
}
}
else if (node->IsBlockBased() && node->BlockInstance)
{
node->BlockInstance->OnMenu(*node, this);
// Add Edit option for blocks
ImGui::Separator();
if (ImGui::MenuItem("Edit (E)"))
{
// Note: OpenPopup needs to be called before closing current popup
// Set up the dialog state first
OpenBlockEditDialog(node, this);
// Close the context menu popup
ImGui::CloseCurrentPopup();
}
}
}
else
ImGui::Text("Unknown node: %p", m_ContextNodeId.AsPointer());
ImGui::Separator();
if (ImGui::MenuItem("Delete"))
ed::DeleteNode(m_ContextNodeId);
ImGui::EndPopup();
}
if (ImGui::BeginPopup("Pin Context Menu"))
{
auto pin = FindPin(m_ContextPinId);
ImGui::TextUnformatted("Pin Context Menu");
ImGui::Separator();
if (pin)
{
ImGui::Text("ID: %p", pin->ID.AsPointer());
if (pin->Node)
ImGui::Text("Node: %p", pin->Node->ID.AsPointer());
else
ImGui::Text("Node: %s", "<none>");
}
else
ImGui::Text("Unknown pin: %p", m_ContextPinId.AsPointer());
ImGui::EndPopup();
}
if (ImGui::BeginPopup("Link Context Menu"))
{
auto link = FindLink(m_ContextLinkId);
ImGui::TextUnformatted("Link Context Menu");
ImGui::Separator();
if (link)
{
ImGui::Text("ID: %p", link->ID.AsPointer());
ImGui::Text("From: %p", link->StartPinID.AsPointer());
ImGui::Text("To: %p", link->EndPinID.AsPointer());
ImGui::Separator();
// Link mode selection
auto currentMode = ed::GetLinkMode(m_ContextLinkId);
const char *modeNames[] = {"Auto (Bezier)", "Straight", "Guided (Waypoints)"};
int currentModeInt = static_cast<int>(currentMode);
ImGui::Text("Mode: %s", modeNames[currentModeInt]);
ImGui::Text("(Press Space to cycle)");
ImGui::Separator();
// Edit delay menu item
char delayText[64];
snprintf(delayText, sizeof(delayText), "Edit Delay (E) - %.3f", link->Delay);
if (ImGui::MenuItem(delayText))
{
StartEditLinkDelay(m_ContextLinkId);
ImGui::CloseCurrentPopup();
}
ImGui::Separator();
// Mode selection menu items
if (ImGui::MenuItem("Auto (Bezier)", nullptr, currentMode == ax::NodeEditor::LinkMode::Auto))
{
ed::SetLinkMode(m_ContextLinkId, ax::NodeEditor::LinkMode::Auto);
}
if (ImGui::MenuItem("Straight Line", nullptr, currentMode == ax::NodeEditor::LinkMode::Straight))
{
ed::SetLinkMode(m_ContextLinkId, ax::NodeEditor::LinkMode::Straight);
}
if (ImGui::MenuItem("Guided (Empty)", nullptr, currentMode == ax::NodeEditor::LinkMode::Guided))
{
ed::SetLinkMode(m_ContextLinkId, ax::NodeEditor::LinkMode::Guided);
}
if (currentMode == ax::NodeEditor::LinkMode::Guided)
{
ImGui::Separator();
int cpCount = ed::GetLinkControlPointCount(m_ContextLinkId);
ImGui::Text("Control Points: %d", cpCount);
if (cpCount > 0 && ImGui::MenuItem("Clear Control Points"))
{
ed::ClearLinkControlPoints(m_ContextLinkId);
// Mark as user-manipulated (user cleared waypoints)
auto linkPtr = FindLink(m_ContextLinkId);
if (linkPtr)
linkPtr->UserManipulatedWaypoints = true;
}
if (ImGui::MenuItem("Auto Guide (Generate Waypoints)"))
{
// Convert to guided and auto-place waypoints for rectangular routing
ed::SetLinkGuided(m_ContextLinkId, true);
// Get the link to calculate waypoints
auto linkPtr = FindLink(m_ContextLinkId);
if (linkPtr)
{
linkPtr->UserManipulatedWaypoints = false; // Auto-generated waypoints
auto startPin = FindPin(linkPtr->StartPinID);
auto endPin = FindPin(linkPtr->EndPinID);
if (startPin && startPin->Node && endPin && endPin->Node)
{
// Get node positions and sizes
auto startNodePos = ed::GetNodePosition(startPin->Node->ID);
auto startNodeSize = ed::GetNodeSize(startPin->Node->ID);
auto endNodePos = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSize = ed::GetNodeSize(endPin->Node->ID);
// Determine pin positions and directions using helper functions
ImVec2 startPos, endPos, startDir, endDir;
startPos = GetPinPosition(startPin, startNodePos, startNodeSize);
startDir = GetPinDirection(startPin);
endPos = GetPinPosition(endPin, endNodePos, endNodeSize);
endDir = GetPinDirection(endPin);
// Calculate node bounds for proper routing
ImVec2 startNodeMin = startNodePos;
ImVec2 startNodeMax = startNodePos + startNodeSize;
ImVec2 endNodeMin = endNodePos;
ImVec2 endNodeMax = endNodePos + endNodeSize;
// Validate node bounds before calling GenerateWaypoints
const float MAX_REASONABLE_COORD_CONTEXT = 100000.0f;
bool startNodeValidContext = (std::abs(startNodePos.x) < MAX_REASONABLE_COORD_CONTEXT &&
std::abs(startNodePos.y) < MAX_REASONABLE_COORD_CONTEXT &&
startNodeSize.x > 0 && startNodeSize.y > 0);
bool endNodeValidContext = (std::abs(endNodePos.x) < MAX_REASONABLE_COORD_CONTEXT &&
std::abs(endNodePos.y) < MAX_REASONABLE_COORD_CONTEXT &&
endNodeSize.x > 0 && endNodeSize.y > 0);
if (!startNodeValidContext || !endNodeValidContext)
{
LOG_WARN("[GenerateWaypoints VALIDATION] RenderContextMenus (Auto Guide): Skipping - invalid node bounds");
LOG_WARN(" startNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
startPin->Node->ID.Get(), startNodePos.x, startNodePos.y,
startNodeSize.x, startNodeSize.y, startNodeValidContext);
LOG_WARN(" endNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
endPin->Node->ID.Get(), endNodePos.x, endNodePos.y,
endNodeSize.x, endNodeSize.y, endNodeValidContext);
}
else
{
// Build RoutingContext
PathFinding::RoutingContext ctx;
ctx.StartPin.Position = startPos;
ctx.StartPin.Direction = startDir;
ctx.StartPin.Type = startPin->Type;
ctx.StartPin.Kind = startPin->Kind;
ctx.StartPin.IsFlowPin = (startPin->Type == PinType::Flow);
ctx.StartPin.IsParameterPin = !ctx.StartPin.IsFlowPin;
ctx.EndPin.Position = endPos;
ctx.EndPin.Direction = endDir;
ctx.EndPin.Type = endPin->Type;
ctx.EndPin.Kind = endPin->Kind;
ctx.EndPin.IsFlowPin = (endPin->Type == PinType::Flow);
ctx.EndPin.IsParameterPin = !ctx.EndPin.IsFlowPin;
ctx.StartNode.Min = startNodeMin;
ctx.StartNode.Max = startNodeMax;
ctx.StartNode.Type = startPin->Node ? startPin->Node->Type : NodeType::Blueprint;
ctx.StartNode.BlockType = startPin->Node && startPin->Node->IsBlockBased() ? startPin->Node->BlockType : std::string();
ctx.StartNode.IsGroup = false;
ctx.StartNode.ZPosition = 0.0f;
ctx.EndNode.Min = endNodeMin;
ctx.EndNode.Max = endNodeMax;
ctx.EndNode.Type = endPin->Node ? endPin->Node->Type : NodeType::Blueprint;
ctx.EndNode.BlockType = endPin->Node && endPin->Node->IsBlockBased() ? endPin->Node->BlockType : std::string();
ctx.EndNode.IsGroup = false;
ctx.EndNode.ZPosition = 0.0f;
ctx.Margin = 40.0f;
ctx.Obstacles = {};
ctx.EditorContext = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(m_Editor);
ctx.Container = GetActiveRootContainer();
// Add refinement passes (they handle feature flags internally)
ctx.AddRefinementPass(PathFinding::RefinementPass_SmoothPath, nullptr, "Link Fitting");
ctx.AddRefinementPass(PathFinding::RefinementPass_AutoCollapse, nullptr, "Auto Collapse");
// Generate waypoints using pathfinding with node bounds
auto waypoints = PathFinding::GenerateWaypoints(ctx);
// Auto-collapse: If refinement pass returned empty waypoints, switch to Straight mode
if (waypoints.empty())
{
ed::SetLinkMode(m_ContextLinkId, ax::NodeEditor::LinkMode::Straight);
linkPtr->UserManipulatedWaypoints = false;
LOG_DEBUG("[RenderContextMenus] Link {} collapsed to Straight mode (empty waypoints)",
static_cast<long long>(m_ContextLinkId.Get()));
}
else
{
// Add waypoints to the link
for (const auto &wp : waypoints)
{
ed::AddLinkControlPoint(m_ContextLinkId, wp);
}
// Reset flag after adding waypoints (AddLinkControlPoint triggers callback)
linkPtr->UserManipulatedWaypoints = false;
}
}
}
}
}
}
}
else
ImGui::Text("Unknown link: %p", m_ContextLinkId.AsPointer());
ImGui::Separator();
if (ImGui::MenuItem("Delete"))
{
LOG_INFO("[DELETE] ContextMenu: Deleting link {}", static_cast<long long>(m_ContextLinkId.Get()));
// Remove from container (links are stored in RootContainer)
if (GetActiveRootContainer())
{
LOG_DEBUG("[DELETE] ContextMenu: Removing link from container");
GetActiveRootContainer()->RemoveLink(m_ContextLinkId);
}
// Tell editor to delete the link
ed::DeleteLink(m_ContextLinkId);
LOG_INFO("[DELETE] ContextMenu: Link deletion complete");
}
ImGui::EndPopup();
}
if (ImGui::BeginPopup("Create New Node"))
{
auto newNodePostion = openPopupPosition;
Node *node = nullptr;
// Parameter nodes (standalone values)
if (ImGui::BeginMenu("Parameter"))
{
if (ImGui::MenuItem("Bool"))
node = SpawnParameterNode(PinType::Bool);
if (ImGui::MenuItem("Int"))
node = SpawnParameterNode(PinType::Int);
if (ImGui::MenuItem("Float"))
node = SpawnParameterNode(PinType::Float);
if (ImGui::MenuItem("String"))
node = SpawnParameterNode(PinType::String);
ImGui::EndMenu();
}
// Start block (entry point)
ImGui::Separator();
if (ImGui::MenuItem("Start"))
node = SpawnBlockNode("Start");
// Log block (logging utility)
if (ImGui::MenuItem("Log"))
node = SpawnBlockNode("Log");
// Group block (top level)
ImGui::Separator();
if (ImGui::MenuItem("Group"))
node = SpawnBlockNode("Group");
// Parameter Operation block
ImGui::Separator();
if (ImGui::MenuItem("Parameter Operation"))
node = SpawnBlockNode("ParamOp");
// Block-based nodes
ImGui::Separator();
if (ImGui::BeginMenu("Math"))
{
if (ImGui::MenuItem("Add"))
node = SpawnBlockNode("Math.Add");
if (ImGui::MenuItem("Multiply"))
node = SpawnBlockNode("Math.Multiply");
if (ImGui::MenuItem("Compare"))
node = SpawnBlockNode("Math.Compare");
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Logic"))
{
if (ImGui::MenuItem("Test"))
node = SpawnBlockNode("Logic.Test");
ImGui::EndMenu();
}
if (node)
{
// BuildNodes() is not needed here - SpawnBlockNode already calls BuildNode()
// BuildNodes(); // REMOVED: Redundant and potentially causes issues
m_CreateNewNode = false;
ed::SetNodePosition(node->ID, newNodePostion);
if (auto startPin = m_NewNodeLinkPin)
{
auto &pins = startPin->Kind == PinKind::Input ? node->Outputs : node->Inputs;
for (auto &pin : pins)
{
if (CanCreateLink(startPin, &pin))
{
auto endPin = &pin;
if (startPin->Kind == PinKind::Input)
std::swap(startPin, endPin);
// Check for duplicate link before creating
if (!IsLinkDuplicate(startPin->ID, endPin->ID))
{
// Create link in container's map indexed by ID
// Use container's ID generator for consistency
auto *container = GetActiveRootContainer();
if (!container)
continue;
auto* rootContainer = dynamic_cast<RootContainer*>(container);
if (!rootContainer)
continue;
ed::LinkId newLinkId = ed::LinkId(container->GetNextId());
Link newLink(newLinkId, startPin->ID, endPin->ID);
// Generate UUID for link
newLink.UUID = m_UuidIdManager.GenerateUuid();
// Use AddLink method instead of direct map access for safety
Link *createdLink = rootContainer->AddLink(newLink);
if (!createdLink)
{
LOG_WARN("[LINK_DRAG] WARNING - link with ID {} already exists or insertion failed",
static_cast<long long>(newLinkId.Get()));
continue;
}
// Register link UUID mapping
m_UuidIdManager.RegisterLink(createdLink->UUID, ToRuntimeId(createdLink->ID));
createdLink->Color = GetIconColor(startPin->Type);
// Note: AddLink already adds to m_LinkIds, no need to add again
// Mark as parameter link if connecting parameter to block
if (startPin->Node && startPin->Node->Type == NodeType::Parameter &&
endPin->Node && endPin->Node->IsBlockBased())
{
createdLink->IsParameterLink = true;
}
else if (endPin->Node && endPin->Node->Type == NodeType::Parameter &&
startPin->Node && startPin->Node->IsBlockBased())
{
createdLink->IsParameterLink = true;
}
// Auto-create guided waypoints for new links
ed::SetLinkGuided(createdLink->ID, true);
createdLink->UserManipulatedWaypoints = false; // Auto-generated waypoints
// Generate initial waypoints based on current node positions
if (startPin->Node && endPin->Node)
{
auto startNodePos = ed::GetNodePosition(startPin->Node->ID);
auto startNodeSize = ed::GetNodeSize(startPin->Node->ID);
auto endNodePos = ed::GetNodePosition(endPin->Node->ID);
auto endNodeSize = ed::GetNodeSize(endPin->Node->ID);
ImVec2 startPos, endPos, startDir, endDir;
// Determine pin positions and directions using helper functions
startPos = GetPinPosition(startPin, startNodePos, startNodeSize);
startDir = GetPinDirection(startPin);
endPos = GetPinPosition(endPin, endNodePos, endNodeSize);
endDir = GetPinDirection(endPin);
ImVec2 startNodeMin = startNodePos;
ImVec2 startNodeMax = startNodePos + startNodeSize;
ImVec2 endNodeMin = endNodePos;
ImVec2 endNodeMax = endNodePos + endNodeSize;
// Validate node bounds before calling GenerateWaypoints
const float MAX_REASONABLE_COORD_POPUP = 100000.0f;
bool startNodeValidPopup = (std::abs(startNodePos.x) < MAX_REASONABLE_COORD_POPUP &&
std::abs(startNodePos.y) < MAX_REASONABLE_COORD_POPUP &&
startNodeSize.x > 0 && startNodeSize.y > 0);
bool endNodeValidPopup = (std::abs(endNodePos.x) < MAX_REASONABLE_COORD_POPUP &&
std::abs(endNodePos.y) < MAX_REASONABLE_COORD_POPUP &&
endNodeSize.x > 0 && endNodeSize.y > 0);
if (!startNodeValidPopup || !endNodeValidPopup)
{
LOG_WARN("[GenerateWaypoints VALIDATION] RenderContextMenus (Create New Node popup): Skipping - invalid node bounds");
LOG_WARN(" startNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
static_cast<long long>(startPin->Node->ID.Get()),
startNodePos.x, startNodePos.y, startNodeSize.x, startNodeSize.y,
startNodeValidPopup);
LOG_WARN(" endNode: ID={} pos=({:.1f}, {:.1f}) size=({:.1f}, {:.1f}) valid={}",
static_cast<long long>(endPin->Node->ID.Get()),
endNodePos.x, endNodePos.y, endNodeSize.x, endNodeSize.y,
endNodeValidPopup);
}
else
{
// Build RoutingContext
PathFinding::RoutingContext ctx;
ctx.StartPin.Position = startPos;
ctx.StartPin.Direction = startDir;
ctx.StartPin.Type = startPin->Type;
ctx.StartPin.Kind = startPin->Kind;
ctx.StartPin.IsFlowPin = (startPin->Type == PinType::Flow);
ctx.StartPin.IsParameterPin = !ctx.StartPin.IsFlowPin;
ctx.EndPin.Position = endPos;
ctx.EndPin.Direction = endDir;
ctx.EndPin.Type = endPin->Type;
ctx.EndPin.Kind = endPin->Kind;
ctx.EndPin.IsFlowPin = (endPin->Type == PinType::Flow);
ctx.EndPin.IsParameterPin = !ctx.EndPin.IsFlowPin;
ctx.StartNode.Min = startNodeMin;
ctx.StartNode.Max = startNodeMax;
ctx.StartNode.Type = startPin->Node ? startPin->Node->Type : NodeType::Blueprint;
ctx.StartNode.BlockType = startPin->Node && startPin->Node->IsBlockBased() ? startPin->Node->BlockType : std::string();
ctx.StartNode.IsGroup = false;
ctx.StartNode.ZPosition = 0.0f;
ctx.EndNode.Min = endNodeMin;
ctx.EndNode.Max = endNodeMax;
ctx.EndNode.Type = endPin->Node ? endPin->Node->Type : NodeType::Blueprint;
ctx.EndNode.BlockType = endPin->Node && endPin->Node->IsBlockBased() ? endPin->Node->BlockType : std::string();
ctx.EndNode.IsGroup = false;
ctx.EndNode.ZPosition = 0.0f;
ctx.Margin = 40.0f;
ctx.Obstacles = {};
ctx.EditorContext = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(m_Editor);
ctx.Container = GetActiveRootContainer();
// Generate waypoints using pathfinding
auto waypoints = PathFinding::GenerateWaypoints(ctx);
// Add waypoints to the new link
for (const auto &wp : waypoints)
{
ed::AddLinkControlPoint(newLink.ID, wp);
}
// Reset flag after adding waypoints (AddLinkControlPoint triggers callback)
createdLink->UserManipulatedWaypoints = false;
}
}
}
break;
}
}
}
}
ImGui::EndPopup();
}
else
m_CreateNewNode = false;
ImGui::PopStyleVar();
ed::Resume();
#endif
}
void App::OnFrame(float deltaTime)
{
static int frameCount = 0;
frameCount++;
UpdateTouch();
RenderDebugInfo();
auto &io = ImGui::GetIO();
ed::SetCurrentEditor(m_Editor);
Splitter(true, 4.0f, &m_LeftPaneWidth, &m_RightPaneWidth, 50.0f, 50.0f);
ShowLeftPane(m_LeftPaneWidth - 4.0f);
ImGui::SameLine(0.0f, 12.0f);
ed::Begin("Node editor");
{
auto cursorTopLeft = ImGui::GetCursorScreenPos();
// Initial zoom to content (after first frame when nodes are rendered)
// Skip if we have saved view settings (m_NeedsInitialZoom is set to false in LoadViewSettings)
// Check if active root container has nodes
bool hasNodes = GetActiveRootContainer() && !GetActiveRootContainer()->m_Nodes.empty();
if (m_NeedsInitialZoom && hasNodes)
{
LOG_TRACE("[OnFrame] Performing initial zoom to content (no saved view state)");
ed::NavigateToContent(0.0f);
// Access internal editor to get content bounds and expand them more
// EditorContext* is an opaque pointer, cast to internal type
auto *editor = reinterpret_cast<ed::Detail::EditorContext *>(m_Editor);
if (editor)
{
// Get content bounds
ImRect contentBounds = editor->GetContentBounds();
if (contentBounds.Min.x < contentBounds.Max.x && contentBounds.Min.y < contentBounds.Max.y)
{
// Expand by extra margin (25% more than default) to zoom out a bit
float extraMargin = 0.25f; // 25% extra margin = zoom out more
float extend = ImMax(contentBounds.GetWidth(), contentBounds.GetHeight());
contentBounds.Expand(extend * extraMargin * 0.5f);
// Navigate to expanded bounds with margin (zoomIn=true adds margin handling)
editor->NavigateTo(contentBounds, true, 0.0f); // instant, no animation
}
}
else
{
// Fallback: just use public API
ed::NavigateToContent(0.0f);
}
m_NeedsInitialZoom = false;
}
else if (!m_NeedsInitialZoom && hasNodes)
{
// We have saved view state - it's being restored by the editor
static bool loggedOnce = false;
if (!loggedOnce)
{
LOG_TRACE("[OnFrame] Skipping initial zoom - restoring saved canvas view state");
loggedOnce = true;
}
}
// Keyboard shortcuts (only when shortcuts enabled and not editing text)
if (ed::AreShortcutsEnabled() && !io.WantTextInput)
{
std::vector<ed::NodeId> selectedNodes;
std::vector<ed::LinkId> selectedLinks;
HandleKeyboardShortcuts(selectedNodes, selectedLinks);
}
RenderNodes(m_NewLinkPin);
RenderLinks();
UpdateGuidedLinks();
// Process edge dragging system
// Set callback to mark links as user-manipulated when edges are dragged
m_EdgeEditor.MarkLinkCallback = [](ed::LinkId linkId, void* userData) -> void
{
auto* app = static_cast<App*>(userData);
app->MarkLinkUserManipulated(linkId);
};
m_EdgeEditor.MarkLinkUserData = this;
m_EdgeEditor.Process();
m_EdgeEditor.DrawFeedback();
HandleLinkCreationAndDeletion();
ImGui::SetCursorScreenPos(cursorTopLeft);
}
RenderDeferredTooltips();
RenderContextMenus();
RenderBlockEditDialog(); // Render block edit dialog if open
RenderParameterEditDialog(); // Render parameter edit dialog if open
ed::End();
RenderOrdinals();
}