#include "app.h" #include "blocks/block.h" #include "blocks/parameter_node.h" #include "containers/root_container.h" #include "crude_json.h" #include "types.h" #include "Logging.h" #include #include namespace { enum class SerializedPinType { InputParameter, OutputParameter, InputFlow, OutputFlow }; inline SerializedPinType DetermineSerializedPinType(const Pin& pin) { const bool isFlow = (pin.Type == PinType::Flow); const bool isInput = (pin.Kind == PinKind::Input); if (isFlow) return isInput ? SerializedPinType::InputFlow : SerializedPinType::OutputFlow; return isInput ? SerializedPinType::InputParameter : SerializedPinType::OutputParameter; } inline const char* SerializedPinTypeToString(SerializedPinType type) { switch (type) { case SerializedPinType::InputParameter: return "InputParameter"; case SerializedPinType::OutputParameter: return "OutputParameter"; case SerializedPinType::InputFlow: return "InputFlow"; case SerializedPinType::OutputFlow: return "OutputFlow"; default: return "InputParameter"; } } inline bool TryParseSerializedPinType(const std::string& value, SerializedPinType& outType) { if (value == "InputParameter") { outType = SerializedPinType::InputParameter; return true; } if (value == "OutputParameter") { outType = SerializedPinType::OutputParameter; return true; } if (value == "InputFlow") { outType = SerializedPinType::InputFlow; return true; } if (value == "OutputFlow") { outType = SerializedPinType::OutputFlow; return true; } return false; } inline bool SerializedPinTypeIsFlow(SerializedPinType type) { return type == SerializedPinType::InputFlow || type == SerializedPinType::OutputFlow; } inline bool SerializedPinTypeIsInput(SerializedPinType type) { return type == SerializedPinType::InputFlow || type == SerializedPinType::InputParameter; } } int App::GetNextId() { return m_GraphState.GetNextId(); } ed::LinkId App::GetNextLinkId() { return m_GraphState.GetNextLinkId(); } void App::TouchNode(ed::NodeId id) { m_NodeTouchTime[id] = m_TouchTime; } float App::GetTouchProgress(ed::NodeId id) { auto it = m_NodeTouchTime.find(id); if (it != m_NodeTouchTime.end() && it->second > 0.0f) return (m_TouchTime - it->second) / m_TouchTime; else return 0.0f; } void App::UpdateTouch() { const auto deltaTime = ImGui::GetIO().DeltaTime; for (auto& entry : m_NodeTouchTime) { if (entry.second > 0.0f) entry.second -= deltaTime; } } Node* App::FindNode(ed::NodeId id) { // Delegate to GraphState which searches all root containers return m_GraphState.FindNode(id); } Link* App::FindLink(ed::LinkId id) { // Delegate to GraphState which searches all root containers return m_GraphState.FindLink(id); } Pin* App::FindPin(ed::PinId id) { if (!id) return nullptr; auto* activeRoot = m_GraphState.GetActiveRootContainer(); if (!activeRoot) return nullptr; // Search in active root container (and recursively in child containers) // Use the App* overload to properly resolve IDs to node pointers return activeRoot->FindPin(id, this); } bool App::IsPinLinked(ed::PinId id) { return m_GraphState.IsPinLinked(id); } bool App::CanCreateLink(Pin* a, Pin* b) { if (!a || !b || a == b || a->Kind == b->Kind || a->Type != b->Type || a->Node == b->Node) return false; // Prevent connecting source nodes with their shortcuts Node* nodeA = a->Node; Node* nodeB = b->Node; if (nodeA && nodeB && nodeA->Type == NodeType::Parameter && nodeB->Type == NodeType::Parameter) { if (nodeA->ParameterInstance && nodeB->ParameterInstance) { // Check if nodeA is a source and nodeB is its shortcut if (nodeA->ParameterInstance->IsSource() && nodeB->ParameterInstance->GetSourceID() == nodeA->ParameterInstance->GetID()) { return false; // Cannot connect source to its shortcut } // Check if nodeB is a source and nodeA is its shortcut if (nodeB->ParameterInstance->IsSource() && nodeA->ParameterInstance->GetSourceID() == nodeB->ParameterInstance->GetID()) { return false; // Cannot connect shortcut to its source } } } return true; } bool App::IsLinkDuplicate(ed::PinId startPinId, ed::PinId endPinId) { if (!startPinId || !endPinId) return false; // Check links from active root container if available, otherwise use m_Links auto* container = GetActiveRootContainer(); std::vector linksToCheck; if (container) { // Use GetLinks() to resolve IDs to pointers (safe from reallocation) linksToCheck = container->GetLinks(this); } else { // No container - get links from active root container auto* activeRoot = GetActiveRootContainer(); if (activeRoot) { linksToCheck = activeRoot->GetAllLinks(); } } for (auto* linkPtr : linksToCheck) { if (!linkPtr) continue; auto& link = *linkPtr; if ((link.StartPinID == startPinId && link.EndPinID == endPinId) || (link.StartPinID == endPinId && link.EndPinID == startPinId)) { return true; } } return false; } Link* App::FindLinkConnectedToPin(ed::PinId pinId) { if (!pinId) return nullptr; // Check links from active root container if available auto* container = GetActiveRootContainer(); std::vector linksToCheck; if (container) { // Use GetLinks() to resolve IDs to pointers (safe from reallocation) linksToCheck = container->GetLinks(this); } else { // No container - get links from active root container auto* activeRoot = GetActiveRootContainer(); if (activeRoot) { linksToCheck = activeRoot->GetAllLinks(); } } for (auto* linkPtr : linksToCheck) { if (!linkPtr) continue; auto& link = *linkPtr; if (link.StartPinID == pinId || link.EndPinID == pinId) return &link; } return nullptr; } void App::BuildNode(Node* node) { if (!node) { return; } for (auto& input : node->Inputs) { input.Node = node; input.Kind = PinKind::Input; } for (auto& output : node->Outputs) { output.Node = node; output.Kind = PinKind::Output; } } void App::BuildNodes() { LOG_TRACE("[CHECKPOINT] BuildNodes: Beginning"); // Build nodes from active root container auto* container = GetActiveRootContainer(); if (container) { // Use GetNodes() to resolve IDs to pointers auto nodes = container->GetNodes(this); // Build all container nodes int nodeIndex = 0; for (auto* node : nodes) { if (node) { BuildNode(node); nodeIndex++; } } } else { // No container - try to use active root container auto* rootContainer = GetActiveRootContainer(); if (rootContainer) { auto nodes = rootContainer->GetAllNodes(); for (auto* node : nodes) { if (node) BuildNode(node); } } } LOG_TRACE("[CHECKPOINT] BuildNodes: Complete"); } ImColor App::GetIconColor(PinType type) { switch (type) { default: case PinType::Flow: return ImColor(255, 255, 255); case PinType::Bool: return ImColor(220, 48, 48); case PinType::Int: return ImColor( 68, 201, 156); case PinType::Float: return ImColor(147, 226, 74); case PinType::String: return ImColor(124, 21, 153); case PinType::Object: return ImColor( 51, 150, 215); case PinType::Function: return ImColor(218, 0, 183); case PinType::Delegate: return ImColor(255, 48, 48); } } Node* App::SpawnBlockNode(const char* blockType, int nodeId) { auto* activeRoot = GetActiveRootContainer(); if (!activeRoot) { LOG_ERROR("[CREATE] SpawnBlockNode: ERROR - no active root container"); return nullptr; } // Use container's ID generator to ensure no ID reuse if (nodeId < 0) { nodeId = activeRoot->GetNextId(); } auto block = BlockRegistry::Instance().CreateBlock(blockType, nodeId); if (!block) { return nullptr; } // Create node - will be added to container after construction Node newNode(nodeId, block->GetName(), ImColor(128, 195, 248)); newNode.Type = NodeType::Blueprint; newNode.BlockType = blockType; // Mark as block-based newNode.BlockInstance = block; // Store block instance for rendering // Generate UUID for node (persistent ID for save/load) newNode.UUID = m_UuidIdManager.GenerateUuid(); // Build block structure (adds pins to node) block->Build(newNode, this); // Generate UUIDs for all pins for (auto& pin : newNode.Inputs) { pin.UUID = m_UuidIdManager.GenerateUuid(); } for (auto& pin : newNode.Outputs) { pin.UUID = m_UuidIdManager.GenerateUuid(); } // Add node to active root container FIRST (creates node copy in map) Node* addedNode = activeRoot->AddNode(newNode); if (!addedNode) { return nullptr; } // Register UUID mapping (after node is added to container) m_UuidIdManager.RegisterNode(addedNode->UUID, nodeId); // Register pin UUID mappings for (auto& pin : addedNode->Inputs) { m_UuidIdManager.RegisterPin(pin.UUID, pin.ID.Get()); } for (auto& pin : addedNode->Outputs) { m_UuidIdManager.RegisterPin(pin.UUID, pin.ID.Get()); } // CRITICAL: Build node structure AFTER AddNode // This ensures pins' Node pointers point to the actual node in the container, not the temporary copy BuildNode(addedNode); return addedNode; } Node* App::SpawnParameterNode(PinType paramType, int nodeId, ParameterDisplayMode displayMode) { auto* activeRoot = GetActiveRootContainer(); if (!activeRoot) { return nullptr; } // Use container's ID generator to ensure no ID reuse if (nodeId < 0) { nodeId = activeRoot->GetNextId(); } // Create parameter instance auto param = ParameterRegistry::Instance().CreateParameter(paramType, nodeId); if (!param) { return nullptr; } param->SetDisplayMode(displayMode); // Create node - will be added to container after construction Node newNode(nodeId, param->GetName(), GetIconColor(paramType)); newNode.Type = NodeType::Parameter; newNode.ParameterType = paramType; newNode.ParameterInstance = param; // CRITICAL: Parameter nodes should NEVER have BlockInstance set newNode.BlockInstance = nullptr; // Explicitly ensure it's null newNode.BlockType = ""; // Ensure BlockType is empty so IsBlockBased() returns false // Generate UUID for node (persistent ID for save/load) newNode.UUID = m_UuidIdManager.GenerateUuid(); // Build node structure (adds output pin) param->Build(newNode, this); // Generate UUIDs for all pins for (auto& pin : newNode.Inputs) { pin.UUID = m_UuidIdManager.GenerateUuid(); } for (auto& pin : newNode.Outputs) { pin.UUID = m_UuidIdManager.GenerateUuid(); } Node* addedNode = activeRoot->AddNode(newNode); if (!addedNode) { LOG_ERROR("[CREATE] SpawnParameterNode: ERROR - AddNode returned nullptr"); return nullptr; } // Register UUID mapping (after node is added to container) m_UuidIdManager.RegisterNode(addedNode->UUID, nodeId); // Register pin UUID mappings for (auto& pin : addedNode->Inputs) { m_UuidIdManager.RegisterPin(pin.UUID, pin.ID.Get()); } for (auto& pin : addedNode->Outputs) { m_UuidIdManager.RegisterPin(pin.UUID, pin.ID.Get()); } BuildNode(addedNode); return addedNode; } void App::SaveGraph(const std::string& filename, RootContainer* container) { if (!container) { LOG_WARN("[SAVE] SaveGraph: No container provided, skipping save"); return; } crude_json::value root; // Save all nodes with their types AND positions auto& nodesArray = root["app_nodes"]; // Get nodes to save from the provided container std::vector nodesToSave; auto nodes = container->GetNodes(this); for (auto* node : nodes) if (node) nodesToSave.push_back(node); // Track orphaned nodes to remove after saving std::vector orphanedNodes; for (const auto* nodePtr : nodesToSave) { if (!nodePtr) { LOG_WARN("[SAVE] SaveGraph: Skipping null node pointer"); continue; } // Safe access to node type NodeType nodeType; try { nodeType = nodePtr->Type; } catch (...) { LOG_WARN("[SAVE] SaveGraph: Cannot access node Type field - skipping corrupted node (ptr={:p})", static_cast(nodePtr)); orphanedNodes.push_back(const_cast(nodePtr)); continue; } // Skip parameter nodes with validation if (nodeType == NodeType::Parameter) { // Validate parameter node has ParameterInstance using safe getter if (!nodePtr->GetParameterInstance()) { LOG_WARN("[SAVE] SaveGraph: Parameter node {} has null ParameterInstance - marking for removal", nodePtr->ID.Get()); orphanedNodes.push_back(const_cast(nodePtr)); continue; } } else { // Block-based node: validate BlockInstance if (nodePtr->IsBlockBased() && !nodePtr->BlockInstance) { int nodeId = -1; try { nodeId = nodePtr->ID.Get(); } catch (...) { nodeId = -1; } LOG_WARN("[SAVE] SaveGraph: Block node {} has null BlockInstance - marking for removal", nodeId); orphanedNodes.push_back(const_cast(nodePtr)); continue; } // Validate BlockInstance pointer is not corrupted if (nodePtr->BlockInstance) { uintptr_t blockPtrValue = reinterpret_cast(nodePtr->BlockInstance); if (blockPtrValue < 0x1000 || blockPtrValue > 0x7FFFFFFFFFFFFFFFULL) { int nodeId = -1; try { nodeId = nodePtr->ID.Get(); } catch (...) { nodeId = -1; } LOG_WARN("[SAVE] SaveGraph: Block node {} has corrupted BlockInstance pointer: 0x{:016X} - marking for removal", nodeId, static_cast(blockPtrValue)); orphanedNodes.push_back(const_cast(nodePtr)); continue; } // Validate BlockInstance ID matches node ID try { int blockId = nodePtr->BlockInstance->GetID(); int nodeId = nodePtr->ID.Get(); if (blockId != nodeId) { LOG_WARN("[SAVE] SaveGraph: Block node {} has mismatched BlockInstance ID (block={}, node={}) - marking for removal", nodeId, blockId, nodeId); orphanedNodes.push_back(const_cast(nodePtr)); continue; } } catch (...) { int nodeId = -1; try { nodeId = nodePtr->ID.Get(); } catch (...) { nodeId = -1; } LOG_WARN("[SAVE] SaveGraph: Cannot access BlockInstance ID for node {} - marking for removal", nodeId); orphanedNodes.push_back(const_cast(nodePtr)); continue; } } } const auto& node = *nodePtr; // Safe access to node ID int nodeId = -1; try { nodeId = node.ID.Get(); } catch (...) { LOG_WARN("[SAVE] SaveGraph: Cannot access node ID - skipping"); orphanedNodes.push_back(const_cast(nodePtr)); continue; } crude_json::value nodeData; // Save UUID (persistent) - runtime IDs are never saved nodeData["uuid_high"] = (double)node.UUID.high; nodeData["uuid_low"] = (double)node.UUID.low; nodeData["name"] = node.Name; // Save node position (may fail for corrupted nodes) try { auto pos = ed::GetNodePosition(node.ID); nodeData["position"]["x"] = pos.x; nodeData["position"]["y"] = pos.y; // Save group size if it's a group node auto size = ed::GetNodeSize(node.ID); if (size.x > 0 || size.y > 0) { nodeData["size"]["x"] = size.x; nodeData["size"]["y"] = size.y; } } catch (...) { LOG_WARN("[SAVE] SaveGraph: Cannot access node position for node {} - using defaults", nodeId); nodeData["position"]["x"] = 0.0; nodeData["position"]["y"] = 0.0; } // Save based on node type if (nodeType == NodeType::Parameter) { // Parameter node - let it save its own state if (node.ParameterInstance) { node.ParameterInstance->SaveState(const_cast(node), nodeData, container, this); } } else if (!node.BlockType.empty()) { // Block-based node nodeData["node_type"] = "block"; nodeData["block_type"] = node.BlockType; // Save block display mode nodeData["block_display_mode"] = (double)static_cast(node.BlockDisplay); // Call block's SaveState callback (passes container info) if (node.BlockInstance) { node.BlockInstance->SaveState(const_cast(node), nodeData, container, this); } } else { // Hardcoded blueprint node nodeData["node_type"] = "hardcoded"; } nodesArray.push_back(nodeData); } // Remove orphaned nodes from container after saving if (!orphanedNodes.empty()) { LOG_INFO("[SAVE] SaveGraph: Removing {} orphaned node(s) from container", orphanedNodes.size()); for (auto* orphanedNode : orphanedNodes) { LOG_DEBUG("[DELETE] SaveGraph: Removing orphaned node from container"); // RemoveNode uses ID-based lookup, safe even if pointer is invalidated if (orphanedNode) container->RemoveNode(orphanedNode->ID); } } // Save all links with control points and pin type info auto& linksArray = root["app_links"]; // Get links to save from the provided container std::vector linksToSave; auto links = container->GetLinks(this); for (auto* link : links) if (link) linksToSave.push_back(link); for (const auto* linkPtr : linksToSave) { if (!linkPtr) continue; const auto& link = *linkPtr; crude_json::value linkData; // Save link UUID (persistent) - runtime IDs are never saved auto& linkUuid = linkData["uuid"]; linkUuid["high"] = (double)link.UUID.high; linkUuid["low"] = (double)link.UUID.low; // Find pins and their nodes auto startPin = FindPin(link.StartPinID); auto endPin = FindPin(link.EndPinID); if (!startPin || !endPin || !startPin->Node || !endPin->Node) { LOG_WARN("[SAVE] SaveGraph: Skipping link {} - missing pin or node (startPin={:p}, endPin={:p})", link.ID.Get(), static_cast(startPin), static_cast(endPin)); continue; // Skip invalid links } // Helper lambda to find relative pin index within its category auto getPinRelativeIndex = [](const Pin* pin, Node* node) -> int { if (!pin || !node) return -1; bool isFlow = (pin->Type == PinType::Flow); bool isInput = (pin->Kind == PinKind::Input); // Get the appropriate pin list const auto& pinList = isInput ? node->Inputs : node->Outputs; // Count pins of the same category int index = 0; for (const auto& p : pinList) { bool pIsFlow = (p.Type == PinType::Flow); // Same category? if (pIsFlow == isFlow) { if (p.ID == pin->ID) return index; index++; } } return -1; // Not found }; auto fillEndpoint = [&](const char* key, Pin* pin, int relativeIndex) { auto& endpoint = linkData[key]; endpoint["node_id_h"] = (double)pin->Node->UUID.high; endpoint["node_id_l"] = (double)pin->Node->UUID.low; endpoint["pin_index"] = (double)relativeIndex; endpoint["pin_type"] = SerializedPinTypeToString(DetermineSerializedPinType(*pin)); }; // Calculate relative indices int startPinIndex = getPinRelativeIndex(startPin, startPin->Node); int endPinIndex = getPinRelativeIndex(endPin, endPin->Node); if (startPinIndex < 0 || endPinIndex < 0) { LOG_WARN("[SAVE] SaveGraph: Skipping link {} - couldn't calculate relative pin index (start={}, end={})", link.ID.Get(), startPinIndex, endPinIndex); continue; } fillEndpoint("start", startPin, startPinIndex); fillEndpoint("end", endPin, endPinIndex); // Save if this is a parameter connection for styling bool isParameterLink = (startPin->Node && startPin->Node->Type == NodeType::Parameter && endPin->Node && endPin->Node->IsBlockBased()); if (isParameterLink) linkData["is_parameter_link"] = true; // Save user-manipulated flag (preserve user's link path) if (link.UserManipulatedWaypoints) linkData["user_manipulated"] = true; // Save delay setting if (link.Delay != 0.0f) linkData["delay"] = link.Delay; // Save link mode (Auto/Bezier, Straight, or Guided) auto linkMode = ed::GetLinkMode(link.ID); if (linkMode == ed::LinkMode::Auto) { linkData["link_mode"] = "bezier"; } else if (linkMode == ed::LinkMode::Straight) { linkData["link_mode"] = "straight"; } else if (linkMode == ed::LinkMode::Guided) { linkData["link_mode"] = "guided"; // Save guided link control points if any int cpCount = ed::GetLinkControlPointCount(link.ID); if (cpCount > 0) { auto& controlPoints = linkData["control_points"]; std::vector points(cpCount); ed::GetLinkControlPoints(link.ID, points.data(), cpCount); for (const auto& pos : points) { crude_json::value pt; pt["x"] = pos.x; pt["y"] = pos.y; controlPoints.push_back(pt); } } } linksArray.push_back(linkData); } // Save to file with pretty formatting (4 space indent) // Supports both relative and absolute paths std::ofstream file(filename); if (file) { file << root.dump(4); LOG_INFO("Graph saved to: {}", filename); } else { LOG_ERROR("Error: Failed to save graph to: {}", filename); } } size_t App::LoadViewSettings(char* data) { // First call: query size if (!data) { std::ifstream file("Blueprints.json"); if (!file) return 0; // No view settings saved file.seekg(0, std::ios::end); size_t size = static_cast(file.tellg()); // If we have saved view settings, skip initial zoom to content if (size > 0) { m_NeedsInitialZoom = false; LOG_INFO("[LoadViewSettings] Found saved view state, skipping initial zoom to content"); } return size; } // Second call: read data std::ifstream file("Blueprints.json"); if (!file) return 0; std::string jsonData((std::istreambuf_iterator(file)), std::istreambuf_iterator()); memcpy(data, jsonData.c_str(), jsonData.size()); return jsonData.size(); } bool App::SaveViewSettings(const char* data, size_t size) { // Node editor gives us full settings JSON with nodes/links we don't want // We only want to save view state (scroll, zoom, visible_rect, selection) std::string jsonStr(data, size); auto settings = crude_json::value::parse(jsonStr); if (settings.is_discarded() || !settings.is_object()) return false; // Create new JSON with only view state crude_json::value viewOnly; if (settings.contains("view")) viewOnly["view"] = settings["view"]; if (settings.contains("selection")) viewOnly["selection"] = settings["selection"]; // Save only view state to Blueprints.json std::ofstream file("Blueprints.json"); if (!file) return false; file << viewOnly.dump(4); return true; } void App::LoadGraph(const std::string& filename, RootContainer* container) { if (!container) { LOG_TRACE("[CHECKPOINT] LoadGraph: No container provided, skipping load"); return; } LOG_TRACE("[CHECKPOINT] LoadGraph: Beginning, filename={}", filename); // Load app-level graph data // Supports both relative and absolute paths std::ifstream file(filename); if (!file) { LOG_WARN("No existing graph file found at: {} (starting with empty graph)", filename); LOG_TRACE("[CHECKPOINT] LoadGraph: File not found, returning"); return; } LOG_INFO("Loading graph from: {}", filename); LOG_TRACE("[CHECKPOINT] LoadGraph: Reading file data"); std::string data((std::istreambuf_iterator(file)), std::istreambuf_iterator()); LOG_TRACE("[CHECKPOINT] LoadGraph: Parsing JSON, size={}", data.size()); auto root = crude_json::value::parse(data); if (root.is_discarded() || !root.is_object()) { LOG_TRACE("[CHECKPOINT] LoadGraph: JSON parse failed or not object, returning"); return; } LOG_TRACE("[CHECKPOINT] LoadGraph: JSON parsed successfully"); // Clear UUID mappings before loading new graph m_UuidIdManager.Clear(); // Load nodes with positions if (root.contains("app_nodes") && root["app_nodes"].is_array()) { const auto& nodesArray = root["app_nodes"].get(); LOG_TRACE("[CHECKPOINT] LoadGraph: Found {} nodes to load", nodesArray.size()); // CRITICAL: Track highest pin ID from saved data to avoid ID reuse // Old format saves pin_id values in inputs/outputs arrays // We must skip past these IDs when generating new ones int highestPinId = container->GetCurrentId(); for (const auto& nodeData : nodesArray) { if (!nodeData.is_object()) continue; // Scan input pin IDs if (nodeData.contains("inputs") && nodeData["inputs"].is_array()) { for (const auto& pinData : nodeData["inputs"].get()) { if (pinData.is_object() && pinData.contains("pin_id")) { int pinId = (int)pinData["pin_id"].get(); if (pinId >= highestPinId) highestPinId = pinId + 1; } } } // Scan output pin IDs if (nodeData.contains("outputs") && nodeData["outputs"].is_array()) { for (const auto& pinData : nodeData["outputs"].get()) { if (pinData.is_object() && pinData.contains("pin_id")) { int pinId = (int)pinData["pin_id"].get(); if (pinId >= highestPinId) highestPinId = pinId + 1; } } } // Scan group pin definitions (groups save pin_id separately) if (nodeData.contains("group_pin_definitions") && nodeData["group_pin_definitions"].is_array()) { for (const auto& pinDef : nodeData["group_pin_definitions"].get()) { if (pinDef.is_object() && pinDef.contains("pin_id")) { int pinId = (int)pinDef["pin_id"].get(); if (pinId >= highestPinId) highestPinId = pinId + 1; } } } } // Update container's ID counter to avoid reusing pin IDs while (container->GetCurrentId() < highestPinId) { container->GetNextId(); } LOG_TRACE("[CHECKPOINT] LoadGraph: Container ID counter updated to {} (highest pin ID was {})", container->GetCurrentId(), highestPinId - 1); // Load and spawn nodes for (const auto& nodeData : nodesArray) { if (!nodeData.is_object()) continue; // Load UUID from file (persistent identifier) Uuid64 loadedUuid(0, 0); if (nodeData.contains("uuid_high") && nodeData.contains("uuid_low")) { loadedUuid.high = (uint32_t)nodeData["uuid_high"].get(); loadedUuid.low = (uint32_t)nodeData["uuid_low"].get(); LOG_TRACE("[CHECKPOINT] LoadGraph: Loading node with UUID: 0x{:08X}{:08X}", loadedUuid.high, loadedUuid.low); } else if (nodeData.contains("id")) { // Old format - generate UUID from runtime ID for migration int oldId = (int)nodeData["id"].get(); loadedUuid = Uuid64(0, (uint32_t)oldId); LOG_TRACE("[CHECKPOINT] LoadGraph: Old format node ID {} - generating UUID: 0x{:08X}{:08X}", oldId, loadedUuid.high, loadedUuid.low); } std::string nodeType = nodeData.contains("node_type") ? nodeData["node_type"].get() : "hardcoded"; Node* spawnedNode = nullptr; // Spawn based on node type if (nodeType == "parameter") { // Parameter node LOG_TRACE("[CHECKPOINT] LoadGraph: Processing parameter node with UUID: 0x{:08X}{:08X}", loadedUuid.high, loadedUuid.low); if (!nodeData.contains("param_type") && !nodeData.contains("parameter_type")) { LOG_TRACE("[CHECKPOINT] LoadGraph: ERROR - parameter node missing type field!"); continue; } PinType paramType; if (nodeData.contains("param_type")) { paramType = static_cast((int)nodeData["param_type"].get()); } else if (nodeData.contains("parameter_type")) { paramType = static_cast((int)nodeData["parameter_type"].get()); } LOG_TRACE("[CHECKPOINT] LoadGraph: Spawning parameter node, type={}", static_cast(paramType)); // Load display mode ParameterDisplayMode displayMode = ParameterDisplayMode::NameAndValue; if (nodeData.contains("display_mode")) displayMode = static_cast((int)nodeData["display_mode"].get()); // Spawn with auto-generated runtime ID (pass -1) spawnedNode = SpawnParameterNode(paramType, -1, displayMode); // Replace auto-generated UUID with loaded UUID if (spawnedNode && loadedUuid.IsValid()) { // Unregister the auto-generated UUID m_UuidIdManager.UnregisterNode(spawnedNode->UUID); for (auto& pin : spawnedNode->Inputs) m_UuidIdManager.UnregisterPin(pin.UUID); for (auto& pin : spawnedNode->Outputs) m_UuidIdManager.UnregisterPin(pin.UUID); // Assign loaded UUID spawnedNode->UUID = loadedUuid; // Re-register with loaded UUID m_UuidIdManager.RegisterNode(loadedUuid, spawnedNode->ID.Get()); LOG_TRACE("[CHECKPOINT] LoadGraph: Assigned loaded UUID to node (runtime ID={})", spawnedNode->ID.Get()); } if (spawnedNode && nodeData.contains("value")) { switch (paramType) { case PinType::Bool: spawnedNode->BoolValue = nodeData["value"].get(); break; case PinType::Int: spawnedNode->IntValue = (int)nodeData["value"].get(); break; case PinType::Float: spawnedNode->FloatValue = (float)nodeData["value"].get(); break; case PinType::String: spawnedNode->StringValue = nodeData["value"].get(); break; default: break; } // Sync to parameter instance if (spawnedNode->ParameterInstance) { switch (paramType) { case PinType::Bool: spawnedNode->ParameterInstance->SetBool(spawnedNode->BoolValue); break; case PinType::Int: spawnedNode->ParameterInstance->SetInt(spawnedNode->IntValue); break; case PinType::Float: spawnedNode->ParameterInstance->SetFloat(spawnedNode->FloatValue); break; case PinType::String: spawnedNode->ParameterInstance->SetString(spawnedNode->StringValue); break; default: break; } } } // Restore custom name if set if (spawnedNode && nodeData.contains("name")) { spawnedNode->Name = nodeData["name"].get(); if (spawnedNode->ParameterInstance) spawnedNode->ParameterInstance->SetName(nodeData["name"].get().c_str()); } // Call parameter node's LoadState callback (passes container info) if (spawnedNode && spawnedNode->ParameterInstance) { spawnedNode->ParameterInstance->LoadState(*spawnedNode, nodeData, container, this); } } else if (nodeType == "block" && nodeData.contains("block_type")) { // Block-based node std::string blockType = nodeData["block_type"].get(); LOG_TRACE("[CHECKPOINT] LoadGraph: Spawning block node, type={}, UUID=0x{:08X}{:08X}", blockType, loadedUuid.high, loadedUuid.low); // Spawn with auto-generated runtime ID (pass -1) spawnedNode = SpawnBlockNode(blockType.c_str(), -1); LOG_TRACE("[CHECKPOINT] LoadGraph: Block node spawned at {:p}", static_cast(spawnedNode)); // Replace auto-generated UUID with loaded UUID if (spawnedNode && loadedUuid.IsValid()) { // Unregister the auto-generated UUID m_UuidIdManager.UnregisterNode(spawnedNode->UUID); for (auto& pin : spawnedNode->Inputs) m_UuidIdManager.UnregisterPin(pin.UUID); for (auto& pin : spawnedNode->Outputs) m_UuidIdManager.UnregisterPin(pin.UUID); // Assign loaded UUID spawnedNode->UUID = loadedUuid; // Re-register with loaded UUID m_UuidIdManager.RegisterNode(loadedUuid, spawnedNode->ID.Get()); LOG_TRACE("[CHECKPOINT] LoadGraph: Assigned loaded UUID to node (runtime ID={})", spawnedNode->ID.Get()); } // Restore block display mode if (spawnedNode && nodeData.contains("block_display_mode")) { spawnedNode->BlockDisplay = static_cast<::BlockDisplayMode>((int)nodeData["block_display_mode"].get()); } // Call block's LoadState callback (passes container info) if (spawnedNode && spawnedNode->BlockInstance) { spawnedNode->BlockInstance->LoadState(*spawnedNode, nodeData, container, this); } } // Hardcoded nodes would be created here if we had them // Restore node position (use spawned node's runtime ID, not loaded ID) if (spawnedNode && nodeData.contains("position") && nodeData["position"].is_object()) { float x = (float)nodeData["position"]["x"].get(); float y = (float)nodeData["position"]["y"].get(); ed::SetNodePosition(spawnedNode->ID, ImVec2(x, y)); } // Note: Node size is now calculated automatically by the node editor based on content // (Groups are rendered like regular blocks, so explicit size setting is not needed) } LOG_TRACE("[CHECKPOINT] LoadGraph: All nodes loaded, container has {} nodes", container->m_Nodes.size()); } // Load links with control points - clear links from the container // Clear all links from the container auto allLinkIds = container->m_LinkIds; // Copy IDs before clearing for (auto linkId : allLinkIds) { container->RemoveLink(linkId); } container->m_LinkIds.clear(); LOG_TRACE("[CHECKPOINT] LoadGraph: Clearing container link IDs (had {} links)", container->m_LinkIds.size()); if (root.contains("app_links") && root["app_links"].is_array()) { const auto& linksArray = root["app_links"].get(); LOG_TRACE("[CHECKPOINT] LoadGraph: Loading {} links", linksArray.size()); for (const auto& linkData : linksArray) { if (!linkData.is_object()) continue; // Load link UUID Uuid64 linkUuid(0, 0); if (linkData.contains("uuid") && linkData["uuid"].is_object()) { const auto& uuidObj = linkData["uuid"]; if (uuidObj.contains("high") && uuidObj.contains("low")) { linkUuid.high = (uint32_t)uuidObj["high"].get(); linkUuid.low = (uint32_t)uuidObj["low"].get(); LOG_DEBUG("[LoadGraph] Loading link UUID: 0x{:08X}{:08X}", linkUuid.high, linkUuid.low); } } // Generate NEW runtime link ID (don't use ID from file) int linkRuntimeId = container->GetNextId(); // Helper lambda to resolve pin by relative index (using node UUID) auto resolvePinByRelativeIndex = [this](const Uuid64& nodeUuid, int pinIndex, bool isFlow, bool isInput) -> Pin* { // Resolve node UUID to runtime ID int nodeRuntimeId = m_UuidIdManager.GetNodeRuntimeId(nodeUuid); if (nodeRuntimeId < 0) return nullptr; Node* node = FindNode(ed::NodeId(nodeRuntimeId)); if (!node) return nullptr; // Get the appropriate pin list (non-const access since we return Pin*) auto& pinList = isInput ? node->Inputs : node->Outputs; // Find the Nth pin of the matching category int index = 0; for (auto& pin : pinList) { bool pIsFlow = (pin.Type == PinType::Flow); // Same category? if (pIsFlow == isFlow) { if (index == pinIndex) return &pin; index++; } } return nullptr; // Index out of range }; Pin* startPin = nullptr; Pin* endPin = nullptr; int startPinId = -1; int endPinId = -1; auto parseEndpoint = [&](const char* key, Pin*& outPin, int& outPinId) -> bool { if (!linkData.contains(key) || !linkData[key].is_object()) return false; const auto& endpoint = linkData[key]; if (!endpoint.contains("node_id_h") || !endpoint.contains("node_id_l") || !endpoint.contains("pin_index") || !endpoint.contains("pin_type")) return false; Uuid64 nodeUuid( (uint32_t)endpoint["node_id_h"].get(), (uint32_t)endpoint["node_id_l"].get() ); int pinIndex = (int)endpoint["pin_index"].get(); std::string pinTypeStr = endpoint["pin_type"].get(); SerializedPinType parsedType; if (!TryParseSerializedPinType(pinTypeStr, parsedType)) { LOG_WARN("[LoadGraph] Unknown pin_type '{}' for link endpoint '{}'", pinTypeStr, key); return false; } bool isFlow = SerializedPinTypeIsFlow(parsedType); bool isInput = SerializedPinTypeIsInput(parsedType); outPin = resolvePinByRelativeIndex(nodeUuid, pinIndex, isFlow, isInput); if (!outPin) return false; outPinId = outPin->ID.Get(); return true; }; bool startParsed = parseEndpoint("start", startPin, startPinId); bool endParsed = parseEndpoint("end", endPin, endPinId); // Orphan checks - validate pins and nodes exist bool linkRemoved = false; if (!startParsed) { LOG_WARN("[LoadGraph] REMOVING link (runtime {}) - start endpoint could not be resolved", linkRuntimeId); linkRemoved = true; } else if (!endParsed) { LOG_WARN("[LoadGraph] REMOVING link (runtime {}) - end endpoint could not be resolved", linkRuntimeId); linkRemoved = true; } else if (!startPin->Node) { LOG_WARN("[LoadGraph] REMOVING link (runtime {}) - start pin {} has null node", linkRuntimeId, startPinId); linkRemoved = true; } else if (!endPin->Node) { LOG_WARN("[LoadGraph] REMOVING link (runtime {}) - end pin {} has null node", linkRuntimeId, endPinId); linkRemoved = true; } else { // Validate: Check that nodes are registered with editor (have valid positions) const float FLT_MAX_THRESHOLD = 3.40282e+38f; ImVec2 startNodePos = ed::GetNodePosition(startPin->Node->ID); ImVec2 endNodePos = ed::GetNodePosition(endPin->Node->ID); if (startNodePos.x >= FLT_MAX_THRESHOLD) { LOG_WARN("[LoadGraph] REMOVING link (runtime {}) - start node {} not registered with editor", linkRuntimeId, startPin->Node->ID.Get()); linkRemoved = true; } else if (endNodePos.x >= FLT_MAX_THRESHOLD) { LOG_WARN("[LoadGraph] REMOVING link (runtime {}) - end node {} not registered with editor", linkRuntimeId, endPin->Node->ID.Get()); linkRemoved = true; } } // Remove link entirely - don't add it to container if validation failed if (linkRemoved) { continue; // Don't create link } // Create link with NEW runtime ID and resolved pin runtime IDs auto linkToAdd = ::Link(ed::LinkId(linkRuntimeId), ed::PinId(startPinId), ed::PinId(endPinId)); linkToAdd.UUID = linkUuid; // Assign loaded UUID Link* linkPtr = container->AddLink(linkToAdd); if (!linkPtr) { LOG_TRACE("[CHECKPOINT] LoadGraph: WARNING - link with runtime ID {} already exists, skipping", linkRuntimeId); continue; } // Register link UUID mapping if (linkUuid.IsValid()) { m_UuidIdManager.RegisterLink(linkUuid, linkRuntimeId); LOG_DEBUG("[LoadGraph] Registered link UUID 0x{:08X}{:08X} -> runtime ID {}", linkUuid.high, linkUuid.low, linkRuntimeId); } // Restore parameter link flag if (linkData.contains("is_parameter_link") && linkData["is_parameter_link"].get()) { linkPtr->IsParameterLink = true; } // Restore user-manipulated flag (preserve user's link path) if (linkData.contains("user_manipulated") && linkData["user_manipulated"].get()) { linkPtr->UserManipulatedWaypoints = true; } // Restore delay setting if (linkData.contains("delay") && linkData["delay"].is_number()) { linkPtr->Delay = static_cast(linkData["delay"].get()); } // Note: auto_adjust field is ignored (redundant with link_mode=guided) // Restore link mode (Auto/Bezier, Straight, or Guided) // Store in pending map to apply after link is registered with editor (use NEW runtime ID) if (linkData.contains("link_mode") && linkData["link_mode"].is_string()) { std::string modeStr = linkData["link_mode"].get(); if (modeStr == "bezier") { m_PendingLinkModes[ed::LinkId(linkRuntimeId)] = ax::NodeEditor::LinkMode::Auto; } else if (modeStr == "straight") { m_PendingLinkModes[ed::LinkId(linkRuntimeId)] = ax::NodeEditor::LinkMode::Straight; } else if (modeStr == "guided") { m_PendingLinkModes[ed::LinkId(linkRuntimeId)] = ax::NodeEditor::LinkMode::Guided; // Store control points for guided links if (linkData.contains("control_points") && linkData["control_points"].is_array()) { const auto& controlPoints = linkData["control_points"].get(); for (const auto& pt : controlPoints) { if (pt.is_object() && pt.contains("x") && pt.contains("y")) { float x = (float)pt["x"].get(); float y = (float)pt["y"].get(); // Store in a pending list to apply after links are drawn (use NEW runtime ID) m_PendingGuidedLinks[ed::LinkId(linkRuntimeId)].push_back(ImVec2(x, y)); } } } } // If link_mode is missing, default to Auto (bezier) mode } } LOG_TRACE("[CHECKPOINT] LoadGraph: All links loaded"); } } // Container management (delegates to GraphState) RootContainer* App::GetActiveRootContainer() { return m_GraphState.GetActiveRootContainer(); } RootContainer* App::AddRootContainer(const std::string& filename) { auto* container = m_GraphState.AddRootContainer(filename); return container; } void App::RemoveRootContainer(RootContainer* container) { m_GraphState.RemoveRootContainer(container); } void App::SetActiveRootContainer(RootContainer* container) { m_GraphState.SetActiveRootContainer(container); } Container* App::FindContainerForNode(ed::NodeId nodeId) { return m_GraphState.FindContainerForNode(nodeId); } void App::MarkLinkUserManipulated(ed::LinkId linkId) { // Mark link as user-manipulated to preserve waypoints and disable auto-adjustment auto* container = GetActiveRootContainer(); if (!container) return; auto* link = container->FindLink(linkId); if (link) { link->UserManipulatedWaypoints = true; } }