#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 #include #include #include 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(m_PinIconSize), static_cast(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 selectedNodes; std::vector selectedLinks; selectedNodes.resize(ed::GetSelectedObjectCount()); selectedLinks.resize(ed::GetSelectedObjectCount()); int nodeCount = ed::GetSelectedNodes(selectedNodes.data(), static_cast(selectedNodes.size())); int linkCount = ed::GetSelectedLinks(selectedLinks.data(), static_cast(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 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(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(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 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 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(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(nodePtr)); LOG_WARN("[DELETE] RenderNodes: Node details - Type={}, IsBlockBased={}, BlockType='{}'", static_cast(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(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(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(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 &executedNodes, const std::vector &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 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 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(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 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(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 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 orderedNodeIds; orderedNodeIds.resize(static_cast(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(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(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 &selectedNodes, std::vector &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(selectedNodes.size())); int linkCount = ed::GetSelectedLinks(selectedLinks.data(), static_cast(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(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(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 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(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(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(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(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(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(startPin), startPin ? (startPin->Kind == PinKind::Output ? "Output" : "Input") : "null", static_cast(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(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(startPin->Type), static_cast(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(newLinkId.Get()), container->GetCurrentId()); // Create link in container's map indexed by ID auto* rootContainer = dynamic_cast(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(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(createdLink), static_cast(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(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(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(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(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(linkId.Get())); GetActiveRootContainer()->RemoveLink(linkId); LOG_INFO("[DELETE] OnFrame: Link {} fully deleted", static_cast(linkId.Get())); } else { LOG_WARN("[DELETE] OnFrame: Link {} not found in m_Links!", static_cast(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", ""); } 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(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(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(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(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(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(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(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(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(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(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 selectedNodes; std::vector 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(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(); }