3509 lines
302 KiB
C++
3509 lines
302 KiB
C++
#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();
|
||
}
|