diff --git a/src/Managers/ofxOceanodeCanvas.cpp b/src/Managers/ofxOceanodeCanvas.cpp index 13cccaa..5989a00 100644 --- a/src/Managers/ofxOceanodeCanvas.cpp +++ b/src/Managers/ofxOceanodeCanvas.cpp @@ -1000,7 +1000,9 @@ void ofxOceanodeCanvas::draw(bool *open, ofColor color, string title){ container->pasteModulesAndConnectionsInPosition(ImGui::GetMousePos() - offset, ImGui::GetIO().KeyShift); }else if(ImGui::IsKeyPressed((ImGuiKey)'A')){ selectAllNodes(); - } + }else if(ImGui::IsKeyPressed((ImGuiKey)'E')){ + container->encapsulateSelectedNodes(); + } } else if(ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace)) && !ImGui::IsAnyItemActive()){ container->deleteSelectedModules(); diff --git a/src/Managers/ofxOceanodeContainer.cpp b/src/Managers/ofxOceanodeContainer.cpp index 85371c6..63fb29d 100644 --- a/src/Managers/ofxOceanodeContainer.cpp +++ b/src/Managers/ofxOceanodeContainer.cpp @@ -12,6 +12,8 @@ #include "ofxOceanodeTypesRegistry.h" #include "ofxOceanodeNodeModel.h" #include "ofxOceanodeShared.h" +#include "ofxOceanodeNodeMacro.h" + #ifdef OFXOCEANODE_USE_MIDI #include "ofxOceanodeMidiBinding.h" @@ -1646,3 +1648,819 @@ ofxOceanodeAbstractConnection* ofxOceanodeContainer::createConnection(ofxOceanod } return connection; } + + +///////////////////////ENCAPSULATE +/// +/// + + +void ofxOceanodeContainer::encapsulateSelectedNodes(const string& macroName) { + // 1. Validate selection + auto selectedNodes = getSelectedModules(); + if(selectedNodes.empty()) { + ofLogWarning("Encapsulation") << "No nodes selected for encapsulation"; + return; + } + + ofLogNotice("Encapsulation") << "Encapsulating " << selectedNodes.size() << " nodes"; + + // 2. Calculate center position for macro placement + glm::vec2 centerPos(0, 0); + for(auto node : selectedNodes) { +#ifndef OFXOCEANODE_HEADLESS + centerPos += node->getNodeGui().getPosition(); +#endif + } + if(!selectedNodes.empty()) { + centerPos /= selectedNodes.size(); + } + + // 3. CRITICAL FIX: Store node information BEFORE cutting them + // We need to preserve the node info before the nodes are deleted + struct NodeInfo { + string originalName; + string nodeType; + glm::vec2 position; + }; + + vector originalNodeInfo; + for(auto node : selectedNodes) { + NodeInfo info; + info.originalName = node->getParameters().getName(); + info.nodeType = node->getNodeModel().nodeName(); +#ifndef OFXOCEANODE_HEADLESS + info.position = node->getNodeGui().getPosition(); +#endif + originalNodeInfo.push_back(info); + } + + // 4. Analyze external connections BEFORE cutting (nodes are still valid) + auto externalConnections = analyzeExternalConnections(selectedNodes); + ofLogNotice("Encapsulation") << "Found " << externalConnections.size() << " external connections"; + + // 5. Cut selected nodes (this deletes them from memory) + if(!cutSelectedModulesWithConnections()) { + ofLogError("Encapsulation") << "Failed to cut selected nodes"; + return; + } + + // 6. Create macro node + auto macroNode = createNodeFromName("Macro"); + if(!macroNode) { + ofLogError("Encapsulation") << "Failed to create macro node"; + return; + } + +#ifndef OFXOCEANODE_HEADLESS + macroNode->getNodeGui().setPosition(centerPos); + macroNode->getNodeGui().setSelected(true); +#endif + + // 7. Get macro internals + auto macroModel = dynamic_cast(¯oNode->getNodeModel()); + if(!macroModel) { + ofLogError("Encapsulation") << "Failed to cast to macro model"; + macroNode->deleteSelf(); + return; + } + + auto macroContainer = macroModel->getContainer(); + if(!macroContainer) { + ofLogError("Encapsulation") << "Failed to get macro container"; + macroNode->deleteSelf(); + return; + } + + // 8. Paste nodes inside macro + if(!macroContainer->pasteModulesAndConnectionsInPosition(glm::vec2(0, 0), false)) { + ofLogWarning("Encapsulation") << "Failed to paste nodes inside macro"; + } + + // CRITICAL FIX: Replace the node mapping section in encapsulateSelectedNodes() + // The issue is that the mapping logic doesn't work correctly with macros + + // 9. FIXED: Update external connections using stored node info + if(!externalConnections.empty()) { + auto macroNodes = macroContainer->getAllModules(); + + // MACRO-AWARE FIX: Ensure any macro nodes have their parameters properly exposed + ensureMacroParametersExposed(macroNodes); + + // Create a mapping from original node names to new node names + // CRITICAL FIX: Use a smarter mapping strategy for macros + map nodeNameMapping; + + ofLogNotice("Encapsulation") << "Creating node name mapping from stored info..."; + + // For macros, we need to match based on exposed parameters, not just type and order + // because the order might change and macros have unique parameter sets + + // Collect the pasted nodes by type + map> pastedNodesByType; + for(auto node : macroNodes) { + string nodeType = node->getNodeModel().nodeName(); + pastedNodesByType[nodeType].push_back(node); + } + + // For each original node, find its best match in the pasted nodes + for(size_t i = 0; i < originalNodeInfo.size(); i++) { + const auto& origInfo = originalNodeInfo[i]; + string originalName = origInfo.originalName; + string nodeType = origInfo.nodeType; + + if(nodeType == "Macro") { + // SPECIAL HANDLING FOR MACROS: Match by parameter similarity + ofLogNotice("Encapsulation") << "Finding macro match for: " << originalName; + + // Find which parameters this original macro was involved in + set originalMacroParams; + for(const auto& extConn : externalConnections) { + if(extConn.isIncoming) { + for(const auto& internalConn : extConn.internalConnections) { + if(internalConn.nodeName == originalName) { + originalMacroParams.insert(internalConn.paramName); + } + } + } else { + if(extConn.internalConnection.nodeName == originalName) { + originalMacroParams.insert(extConn.internalConnection.paramName); + } + } + } + + ofLogNotice("Encapsulation") << "Original macro " << originalName << " used parameters:"; + for(const auto& param : originalMacroParams) { + ofLogNotice("Encapsulation") << " - " << param; + } + + // Find the best matching macro among the pasted ones + ofxOceanodeNode* bestMatch = nullptr; + int bestScore = -1; + + for(auto pastedMacro : pastedNodesByType[nodeType]) { + if(nodeNameMapping.find(pastedMacro->getParameters().getName()) != nodeNameMapping.end()) { + continue; // Already mapped + } + + auto& pastedParams = pastedMacro->getParameters(); + int score = 0; + + ofLogNotice("Encapsulation") << "Checking pasted macro: " << pastedMacro->getParameters().getName(); + ofLogNotice("Encapsulation") << "Available parameters:"; + for(auto& param : pastedParams) { + ofLogNotice("Encapsulation") << " - " << param->getName(); + if(originalMacroParams.count(param->getName()) > 0) { + score++; + ofLogNotice("Encapsulation") << " MATCH!"; + } + } + + ofLogNotice("Encapsulation") << "Score: " << score << "/" << originalMacroParams.size(); + + if(score > bestScore) { + bestScore = score; + bestMatch = pastedMacro; + } + } + + if(bestMatch) { + string newName = bestMatch->getParameters().getName(); + nodeNameMapping[originalName] = newName; + // Mark this pasted node as used by adding reverse mapping + nodeNameMapping[newName] = originalName; + + ofLogNotice("Encapsulation") << "MACRO MATCH: " << originalName << " → " << newName + << " (score: " << bestScore << "/" << originalMacroParams.size() << ")"; + } else { + ofLogError("Encapsulation") << "No good match found for macro: " << originalName; + } + } else { + // Regular node logic (unchanged) + if(pastedNodesByType[nodeType].size() > 0) { + // Count how many nodes of this type we've seen before this one + int typeIndex = 0; + for(size_t j = 0; j < i; j++) { + if(originalNodeInfo[j].nodeType == nodeType) { + typeIndex++; + } + } + + if(typeIndex < pastedNodesByType[nodeType].size()) { + string newName = pastedNodesByType[nodeType][typeIndex]->getParameters().getName(); + nodeNameMapping[originalName] = newName; + + ofLogNotice("Encapsulation") << "REGULAR MATCH: " << originalName << " → " << newName; + } + } + } + } + + // Update external connections using the mapping + for(auto& extConn : externalConnections) { + if(extConn.isIncoming) { + // Update input router internal connections + for(auto& internalConn : extConn.internalConnections) { + string originalName = internalConn.nodeName; + + if(nodeNameMapping.find(originalName) != nodeNameMapping.end()) { + string newName = nodeNameMapping[originalName]; + if(newName != originalName) { + ofLogNotice("Encapsulation") << "Updated input: " << originalName << " → " << newName; + internalConn.nodeName = newName; + } + } + } + } else { + // Update output router internal connection + string originalName = extConn.internalConnection.nodeName; + + if(nodeNameMapping.find(originalName) != nodeNameMapping.end()) { + string newName = nodeNameMapping[originalName]; + if(newName != originalName) { + ofLogNotice("Encapsulation") << "Updated output: " << originalName << " → " << newName; + extConn.internalConnection.nodeName = newName; + } + } + } + } + + // 10. Create routers and reconnect + createRoutersAndReconnect(macroNode, externalConnections); + } + + ofLogNotice("Encapsulation") << "Encapsulation completed successfully"; +} + +vector ofxOceanodeContainer::analyzeExternalConnections(vector selectedNodes) { + // For input routers: group by external parameter (one external → multiple internals) + map inputRouters; + + // For output routers: group by internal parameter (one internal → multiple externals) + map, ExternalConnection> outputRouters; // Key: {nodeName, paramName} + + // Track router names to avoid conflicts within this encapsulation + map nameCounters; + + ofLogNotice("Encapsulation") << "=== DEBUGGING CONNECTION ANALYSIS ==="; + ofLogNotice("Encapsulation") << "Total connections in container: " << connections.size(); + ofLogNotice("Encapsulation") << "Selected nodes for encapsulation: " << selectedNodes.size(); + + // First, let's see what nodes exist in the container + ofLogNotice("Encapsulation") << "ALL NODES in container:"; + for(auto& nodeTypeMap : dynamicNodes) { + for(auto& node : nodeTypeMap.second) { + ofLogNotice("Encapsulation") << " - " << node.second->getParameters().getName() + << " (type: " << node.second->getNodeModel().nodeName() << ")"; + } + } + + // Check if any of our selected nodes are macros or routers + ofLogNotice("Encapsulation") << "SELECTED NODES details:"; + for(auto node : selectedNodes) { + ofLogNotice("Encapsulation") << " - " << node->getParameters().getName() + << " (type: " << node->getNodeModel().nodeName() << ")"; + + // Check if this is a router or macro + if(node->getNodeModel().nodeName().find("Router") != string::npos) { + ofLogWarning("Encapsulation") << " WARNING: Selected node is a router!"; + } + if(node->getNodeModel().nodeName().find("Macro") != string::npos) { + ofLogWarning("Encapsulation") << " WARNING: Selected node is a macro!"; + } + } + + ofLogNotice("Encapsulation") << "ANALYZING ALL CONNECTIONS:"; + + int connectionIndex = 0; + for(auto& connection : connections) { + if(!connection) continue; + + auto sourceNode = getNodeFromParameter(connection->getSourceParameter()); + auto sinkNode = getNodeFromParameter(connection->getSinkParameter()); + + if(!sourceNode || !sinkNode) { + ofLogWarning("Encapsulation") << "Connection " << connectionIndex << ": Could not find nodes"; + connectionIndex++; + continue; + } + + bool sourceSelected = isNodeInList(sourceNode, selectedNodes); + bool sinkSelected = isNodeInList(sinkNode, selectedNodes); + + string sourceNodeName = sourceNode->getParameters().getName(); + string sinkNodeName = sinkNode->getParameters().getName(); + string sourceParamName = connection->getSourceParameter().getName(); + string sinkParamName = connection->getSinkParameter().getName(); + + ofLogNotice("Encapsulation") << "Connection " << connectionIndex << ": " + << sourceNodeName << "." << sourceParamName << " -> " + << sinkNodeName << "." << sinkParamName; + ofLogNotice("Encapsulation") << " Source selected: " << (sourceSelected ? "YES" : "NO") + << ", Sink selected: " << (sinkSelected ? "YES" : "NO"); + + // Check if this connection involves existing routers + bool sourceIsRouter = sourceNodeName.find("Router") != string::npos; + bool sinkIsRouter = sinkNodeName.find("Router") != string::npos; + bool sourceIsMacro = sourceNodeName.find("Macro") != string::npos; + bool sinkIsMacro = sinkNodeName.find("Macro") != string::npos; + + if(sourceIsRouter || sinkIsRouter || sourceIsMacro || sinkIsMacro) { + ofLogWarning("Encapsulation") << " -> INVOLVES EXISTING ROUTER/MACRO: " + << "src_router=" << sourceIsRouter << ", sink_router=" << sinkIsRouter + << ", src_macro=" << sourceIsMacro << ", sink_macro=" << sinkIsMacro; + } + + // Skip internal connections (both nodes selected) + if(sourceSelected && sinkSelected) { + ofLogNotice("Encapsulation") << " -> INTERNAL CONNECTION (preserved)"; + connectionIndex++; + continue; + } + + // Skip unrelated connections (neither node selected) + if(!sourceSelected && !sinkSelected) { + ofLogNotice("Encapsulation") << " -> UNRELATED CONNECTION (ignored)"; + connectionIndex++; + continue; + } + + // This is an external connection we need to handle + if(sourceSelected && !sinkSelected) { + ofLogNotice("Encapsulation") << " -> OUTGOING CONNECTION (needs output router)"; + + // Check if sink is a router from previous encapsulation + if(sinkIsRouter) { + ofLogError("Encapsulation") << " ERROR: Trying to connect to existing router! This might cause issues."; + ofLogError("Encapsulation") << " Sink: " << sinkNodeName << "." << sinkParamName; + } + + string fullInternalNodeName = sourceNodeName; + string originalInternalParamName = sourceParamName; + auto internalKey = make_pair(fullInternalNodeName, originalInternalParamName); + + auto it = outputRouters.find(internalKey); + if(it != outputRouters.end()) { + it->second.externalConnections.push_back(&connection->getSinkParameter()); + } else { + ExternalConnection extConn; + extConn.routerType = connection->getSourceParameter().valueType(); + extConn.isIncoming = false; + extConn.routerName = generateRouterName(originalInternalParamName, nameCounters); + extConn.internalConnection.nodeName = fullInternalNodeName; + extConn.internalConnection.paramName = originalInternalParamName; + extConn.externalConnections.push_back(&connection->getSinkParameter()); + + outputRouters[internalKey] = extConn; + ofLogNotice("Encapsulation") << " Created output router: " << extConn.routerName; + } + } + else if(!sourceSelected && sinkSelected) { + ofLogNotice("Encapsulation") << " -> INCOMING CONNECTION (needs input router)"; + + // Check if source is a router from previous encapsulation + if(sourceIsRouter) { + ofLogError("Encapsulation") << " ERROR: Source is existing router! This might cause issues."; + ofLogError("Encapsulation") << " Source: " << sourceNodeName << "." << sourceParamName; + } + + auto externalParam = &connection->getSourceParameter(); + string fullInternalNodeName = sinkNodeName; + string originalInternalParamName = sinkParamName; + + auto it = inputRouters.find(externalParam); + if(it != inputRouters.end()) { + it->second.internalConnections.push_back({fullInternalNodeName, originalInternalParamName}); + } else { + ExternalConnection extConn; + extConn.routerType = connection->getSourceParameter().valueType(); + extConn.externalParam = externalParam; + extConn.isIncoming = true; + extConn.routerName = generateRouterName(originalInternalParamName, nameCounters); + extConn.internalConnections.push_back({fullInternalNodeName, originalInternalParamName}); + + inputRouters[externalParam] = extConn; + ofLogNotice("Encapsulation") << " Created input router: " << extConn.routerName; + } + } + + connectionIndex++; + } + + // Convert maps to vector + vector externals; + for(auto& pair : inputRouters) { + externals.push_back(pair.second); + } + for(auto& pair : outputRouters) { + externals.push_back(pair.second); + } + + ofLogNotice("Encapsulation") << "=== ANALYSIS COMPLETE ==="; + ofLogNotice("Encapsulation") << "Will create " << externals.size() << " routers"; + + return externals; +} + +void ofxOceanodeContainer::createRoutersAndReconnect(ofxOceanodeNode* macroNode, vector& connections) { + auto macroModel = dynamic_cast(¯oNode->getNodeModel()); + if(!macroModel) { + ofLogError("Encapsulation") << "Invalid macro model for router creation"; + return; + } + + auto macroContainer = macroModel->getContainer(); + if(!macroContainer) { + ofLogError("Encapsulation") << "Invalid macro container for router creation"; + return; + } + + ofLogNotice("Encapsulation") << "=== CREATING ROUTERS INSIDE MACRO ==="; + ofLogNotice("Encapsulation") << "Creating " << connections.size() << " routers inside macro..."; + + // Get all nodes inside the macro + auto macroNodes = macroContainer->getAllModules(); + ofLogNotice("Encapsulation") << "Found " << macroNodes.size() << " nodes inside macro:"; + for(auto node : macroNodes) { + ofLogNotice("Encapsulation") << " - " << node->getParameters().getName() + << " (type: " << node->getNodeModel().nodeName() << ")"; + } + + int routerIndex = 0; + vector createdRouters; + + // Create routers and connect them appropriately + for(auto& extConn : connections) { + try { + string routerTypeName = "Router " + macroContainer->getTypesRegistry()->getTypeNameFromTypeDescription(extConn.routerType); + + ofLogNotice("Encapsulation") << "Creating router " << (routerIndex + 1) << "/" << connections.size() + << ": " << routerTypeName << " named '" << extConn.routerName << "'"; + + auto routerNode = macroContainer->createNodeFromName(routerTypeName); + if(!routerNode) { + ofLogError("Encapsulation") << "Failed to create router: " << routerTypeName; + continue; + } + + createdRouters.push_back(routerNode); + + // Set router name + auto routerModel = dynamic_cast(&routerNode->getNodeModel()); + if(routerModel) { + routerModel->getNameParam() = extConn.routerName; + ofLogNotice("Encapsulation") << "Set router name to: " << extConn.routerName; + } + + #ifndef OFXOCEANODE_HEADLESS + // SMART POSITIONING: Position router near its connected nodes + glm::vec2 routerPos(0, 0); + int connectedNodeCount = 0; + + if(extConn.isIncoming) { + // Input router: position near the average of all internal nodes it connects to + for(const auto& internalConn : extConn.internalConnections) { + // Find the internal node this router will connect to + for(auto node : macroNodes) { + if(node->getParameters().getName() == internalConn.nodeName) { + routerPos += node->getNodeGui().getPosition(); + connectedNodeCount++; + break; + } + } + } + + if(connectedNodeCount > 0) { + routerPos /= connectedNodeCount; + // Offset input routers to the left of the connected nodes + routerPos.x -= 200; + } else { + // Fallback position for input routers + routerPos = glm::vec2(-200, routerIndex * 80); + } + } else { + // Output router: position near the single internal node it connects from + for(auto node : macroNodes) { + if(node->getParameters().getName() == extConn.internalConnection.nodeName) { + routerPos = node->getNodeGui().getPosition(); + // Offset output routers to the right of the connected node + routerPos.x += 300; + connectedNodeCount = 1; + break; + } + } + + if(connectedNodeCount == 0) { + // Fallback position for output routers + routerPos = glm::vec2(400, routerIndex * 80); + } + } + + // Add some vertical spacing if multiple routers would overlap + routerPos.y += (routerIndex % 3) * 60; // Slight vertical offset for multiple routers + + routerNode->getNodeGui().setPosition(routerPos); + + ofLogNotice("Encapsulation") << "Positioned " << (extConn.isIncoming ? "input" : "output") + << " router '" << extConn.routerName << "' at (" << routerPos.x << ", " << routerPos.y << ")"; + #endif + + // Get router's Value parameter + auto& routerParams = routerNode->getParameters(); + if(!routerParams.contains("Value")) { + ofLogError("Encapsulation") << "Router does not have Value parameter!"; + ofLogError("Encapsulation") << "Available parameters:"; + for(auto& param : routerParams) { + ofLogError("Encapsulation") << " - " << param->getName(); + } + continue; + } + + auto routerParam = dynamic_cast(&routerParams.get("Value")); + if(!routerParam) { + ofLogError("Encapsulation") << "Could not cast Value parameter to ofxOceanodeAbstractParameter"; + continue; + } + + if(extConn.isIncoming) { + // Input router: connect to multiple internal parameters + ofLogNotice("Encapsulation") << "Connecting input router to " << extConn.internalConnections.size() << " internal parameters:"; + for(const auto& internalConn : extConn.internalConnections) { + ofxOceanodeAbstractParameter* internalParam = findInternalParameter(macroNodes, internalConn); + if(internalParam) { + // Check if target parameter already has a connection + if(internalParam->hasInConnection()) { + ofLogError("Encapsulation") << "ERROR: Target parameter " << internalConn.nodeName + << "." << internalConn.paramName << " already has an input connection!"; + ofLogError("Encapsulation") << "This should not happen - investigating connection analysis..."; + continue; + } + + auto conn = macroContainer->createConnection(*routerParam, *internalParam); + if(conn) { + ofLogNotice("Encapsulation") << " ✓ Connected input router '" << extConn.routerName + << "' → " << internalConn.nodeName << "." << internalConn.paramName; + } else { + ofLogError("Encapsulation") << " ✗ Failed to connect input router '" << extConn.routerName + << "' → " << internalConn.nodeName << "." << internalConn.paramName; + } + } else { + ofLogError("Encapsulation") << " ✗ Could not find internal parameter: " + << internalConn.nodeName << "." << internalConn.paramName; + } + } + } else { + // Output router: connect from single internal parameter + ofLogNotice("Encapsulation") << "Connecting output router from internal parameter:"; + ofxOceanodeAbstractParameter* internalParam = findInternalParameter(macroNodes, extConn.internalConnection); + if(internalParam) { + auto conn = macroContainer->createConnection(*internalParam, *routerParam); + if(conn) { + ofLogNotice("Encapsulation") << " ✓ Connected " << extConn.internalConnection.nodeName + << "." << extConn.internalConnection.paramName << " → output router '" << extConn.routerName << "'"; + } else { + ofLogError("Encapsulation") << " ✗ Failed to connect " << extConn.internalConnection.nodeName + << "." << extConn.internalConnection.paramName << " → output router '" << extConn.routerName << "'"; + } + } else { + ofLogError("Encapsulation") << " ✗ Could not find internal parameter: " + << extConn.internalConnection.nodeName << "." << extConn.internalConnection.paramName; + } + } + + routerIndex++; + + } catch(const std::exception& e) { + ofLogError("Encapsulation") << "Exception creating/connecting router: " << e.what(); + } + } + + // Update everything to process the new routers + ofEventArgs args; + for(auto router : createdRouters) { + router->update(args); + } + macroNode->update(args); + + // Give time for macro to process routers and expose them as parameters + ofSleepMillis(100); + macroNode->update(args); + + // Now reconnect external connections to macro parameters + ofLogNotice("Encapsulation") << "=== RECONNECTING EXTERNAL CONNECTIONS ==="; + + auto& macroParams = macroNode->getParameters(); + ofLogNotice("Encapsulation") << "Available macro parameters:"; + for(auto& param : macroParams) { + ofLogNotice("Encapsulation") << " - " << param->getName() << " (" << param->valueType() << ")"; + } + + int reconnectedCount = 0; + for(auto& extConn : connections) { + try { + if(macroParams.contains(extConn.routerName)) { + auto macroParam = dynamic_cast(¯oParams.get(extConn.routerName)); + if(!macroParam) { + ofLogError("Encapsulation") << "Could not cast macro parameter: " << extConn.routerName; + continue; + } + + if(extConn.isIncoming) { + // Incoming: external → macro input (single connection) + if(extConn.externalParam) { + auto conn = createConnection(*extConn.externalParam, *macroParam); + if(conn) { + ofLogNotice("Encapsulation") << " ✓ Reconnected incoming: external → " << extConn.routerName; + reconnectedCount++; + } else { + ofLogError("Encapsulation") << " ✗ Failed to reconnect incoming to " << extConn.routerName; + } + } + } else { + // Outgoing: macro output → multiple externals + for(auto externalParam : extConn.externalConnections) { + if(externalParam) { + auto conn = createConnection(*macroParam, *externalParam); + if(conn) { + ofLogNotice("Encapsulation") << " ✓ Reconnected outgoing: " << extConn.routerName << " → external"; + reconnectedCount++; + } else { + ofLogError("Encapsulation") << " ✗ Failed to reconnect outgoing from " << extConn.routerName; + } + } + } + } + } else { + ofLogError("Encapsulation") << "Macro parameter not found: " << extConn.routerName; + } + } catch(const std::exception& e) { + ofLogError("Encapsulation") << "Exception reconnecting: " << e.what(); + } + } + + ofLogNotice("Encapsulation") << "=== ENCAPSULATION COMPLETE ==="; + ofLogNotice("Encapsulation") << "Successfully reconnected: " << reconnectedCount << " connections"; +} + +bool ofxOceanodeContainer::isNodeInList(ofxOceanodeNode* node, vector& nodeList) { + return std::find(nodeList.begin(), nodeList.end(), node) != nodeList.end(); +} + +string ofxOceanodeContainer::generateRouterName(const string& paramName, map& nameCounters) { + // Clean parameter name + string cleanParamName = paramName; + ofStringReplace(cleanParamName, ".", "_"); + ofStringReplace(cleanParamName, " ", "_"); + + // Remove consecutive underscores and trim + while(cleanParamName.find("__") != string::npos) { + ofStringReplace(cleanParamName, "__", "_"); + } + if(!cleanParamName.empty() && cleanParamName[0] == '_') { + cleanParamName = cleanParamName.substr(1); + } + if(!cleanParamName.empty() && cleanParamName.back() == '_') { + cleanParamName = cleanParamName.substr(0, cleanParamName.length() - 1); + } + if(cleanParamName.empty()) { + cleanParamName = "Param"; + } + + // Check if this name already exists in this encapsulation + if(nameCounters.find(cleanParamName) == nameCounters.end()) { + nameCounters[cleanParamName] = 1; + return cleanParamName; + } else { + nameCounters[cleanParamName]++; + return cleanParamName + "_" + ofToString(nameCounters[cleanParamName]); + } +} + +ofxOceanodeNode* ofxOceanodeContainer::getNodeFromParameter(ofxOceanodeAbstractParameter& param) { + // Use the nodeModel to get the parameter group name + auto nodeModel = param.getNodeModel(); + if(!nodeModel) return nullptr; + + string paramGroupName = nodeModel->getParameterGroup().getEscapedName(); + + auto it = parameterGroupNodesMap.find(paramGroupName); + if(it != parameterGroupNodesMap.end()) { + return it->second; + } + + return nullptr; +} + +// Fix the getParameterTypeName method to properly handle nodePort: + +ofxOceanodeAbstractParameter* ofxOceanodeContainer::findInternalParameter( + const vector& macroNodes, + const InternalConnection& internalConn) { + + ofLogVerbose("Encapsulation") << "Looking for internal parameter: " << internalConn.nodeName << "." << internalConn.paramName; + + for(auto node : macroNodes) { + if(node->getParameters().getName() == internalConn.nodeName) { + auto& nodeParams = node->getParameters(); + + ofLogVerbose("Encapsulation") << "Found matching node: " << internalConn.nodeName; + + // MACRO-AWARE FIX: Check if this is a macro node + bool isMacroNode = (node->getNodeModel().nodeName() == "Macro"); + + if(isMacroNode) { + ofLogNotice("Encapsulation") << "Node is a macro, looking for exposed parameter: " << internalConn.paramName; + + // For macro nodes, the exposed router parameters appear directly in the parameter group + // after the macro has processed its internal routers + + // Force an update to ensure all router parameters are exposed + ofEventArgs args; + node->update(args); + + // Small delay to allow parameter exposure to complete + ofSleepMillis(50); + node->update(args); + + // Now check for the parameter + if(nodeParams.contains(internalConn.paramName)) { + auto param = dynamic_cast(&nodeParams.get(internalConn.paramName)); + if(param) { + ofLogNotice("Encapsulation") << "✓ Found macro exposed parameter: " << internalConn.paramName; + return param; + } else { + ofLogError("Encapsulation") << "✗ Found parameter but failed to cast: " << internalConn.paramName; + } + } else { + ofLogError("Encapsulation") << "✗ Macro parameter not found: " << internalConn.paramName; + ofLogError("Encapsulation") << "Available macro parameters:"; + for(auto& param : nodeParams) { + ofLogError("Encapsulation") << " - '" << param->getName() << "'"; + } + } + } else { + // Regular node logic (unchanged) + ofLogVerbose("Encapsulation") << "Regular node, available parameters:"; + for(auto& param : nodeParams) { + ofLogVerbose("Encapsulation") << " - '" << param->getName() << "'"; + } + + if(nodeParams.contains(internalConn.paramName)) { + auto param = dynamic_cast(&nodeParams.get(internalConn.paramName)); + if(param) { + ofLogVerbose("Encapsulation") << "✓ Found and cast parameter: " << internalConn.paramName; + return param; + } else { + ofLogError("Encapsulation") << "✗ Found parameter but failed to cast: " << internalConn.paramName; + } + } else { + ofLogError("Encapsulation") << "✗ Parameter not found in node: " << internalConn.paramName; + } + } + break; + } + } + + ofLogError("Encapsulation") << "Could not find internal parameter: " + << internalConn.nodeName << "." << internalConn.paramName; + return nullptr; +} + +string ofxOceanodeContainer::extractNodeType(const string& nodeName) { + // Extract base type from node name (e.g., "Vector Item Operations 2" → "Vector Item Operations") + string nodeType = nodeName; + auto lastSpacePos = nodeType.find_last_of(' '); + if(lastSpacePos != string::npos) { + string possibleId = nodeType.substr(lastSpacePos + 1); + bool isNumeric = !possibleId.empty() && std::all_of(possibleId.begin(), possibleId.end(), ::isdigit); + if(isNumeric) { + nodeType = nodeType.substr(0, lastSpacePos); + } + } + return nodeType; +} + +void ofxOceanodeContainer::ensureMacroParametersExposed(const vector& macroNodes) { + ofLogNotice("Encapsulation") << "Ensuring macro parameters are exposed..."; + + // Force multiple updates to ensure all macro router parameters are exposed + for(int i = 0; i < 3; i++) { + ofEventArgs args; + for(auto node : macroNodes) { + if(node->getNodeModel().nodeName() == "Macro") { + node->update(args); + } + } + ofSleepMillis(25); // Small delay between updates + } + + // Log final parameter state + for(auto node : macroNodes) { + if(node->getNodeModel().nodeName() == "Macro") { + ofLogNotice("Encapsulation") << "Macro " << node->getParameters().getName() << " exposed parameters:"; + for(auto& param : node->getParameters()) { + ofLogNotice("Encapsulation") << " - " << param->getName() << " (" << param->valueType() << ")"; + } + } + } +} diff --git a/src/Managers/ofxOceanodeContainer.h b/src/Managers/ofxOceanodeContainer.h index 288a046..398990b 100644 --- a/src/Managers/ofxOceanodeContainer.h +++ b/src/Managers/ofxOceanodeContainer.h @@ -18,6 +18,8 @@ class ofxOceanodeNodeModel; class ofxOceanodeNodeRegistry; class ofxOceanodeTypesRegistry; +class ofxOceanodeNodeMacro; + #ifdef OFXOCEANODE_USE_OSC #include "ofxOsc.h" @@ -63,6 +65,7 @@ class ofxOceanodeContainer { void activate(); void deactivate(); + ofxOceanodeNode* createNodeFromName(string name, int identifier = -1, bool isPersistent = false); ofxOceanodeNode& createNode(unique_ptr && nodeModel, int identifier = -1, bool isPersistent = false, string additionalInfo = ""); @@ -130,6 +133,10 @@ class ofxOceanodeContainer { void setBpm(float _bpm); void resetPhase(); + + // Node encapsulation functionality + void encapsulateSelectedNodes(const string& macroName = "Encapsulated"); + ofEvent> loadPresetEvent; ofEvent> loadPresetNumEvent; @@ -210,7 +217,40 @@ class ofxOceanodeContainer { #endif string canvasID; - vector comments; -}; + // Encapsulation support structures and methods + struct InternalConnection { + string nodeName; // Name of the internal node + string paramName; // Name of the internal parameter + }; + + struct ExternalConnection { + // For input routers (external → multiple internals) + ofxOceanodeAbstractParameter* externalParam; // Single external parameter (for inputs) + vector internalConnections; // Multiple internal connections (for inputs) + + // For output routers (single internal → multiple externals) + InternalConnection internalConnection; // Single internal connection (for outputs) + vector externalConnections; // Multiple external parameters (for outputs) + + bool isIncoming; // true: external->internal, false: internal->external + string routerName; // Generated name for router + string routerType; // Parameter type for router creation + }; + + vector analyzeExternalConnections(vector selectedNodes); + void createRoutersAndReconnect(ofxOceanodeNode* macroNode, vector& connections); + string generateRouterName(const string& paramName, map& nameCounters); + string extractNodeType(const string& nodeName); + void ensureMacroParametersExposed(const vector& macroNodes); + + string mapParameterTypeToRouterName(const string& paramType); + ofxOceanodeNode* getNodeFromParameter(ofxOceanodeAbstractParameter& param); + bool isNodeInList(ofxOceanodeNode* node, vector& nodeList); + string getParameterTypeName(ofxOceanodeAbstractParameter& param); + ofxOceanodeAbstractParameter* findInternalParameter(const vector& macroNodes, const InternalConnection& internalConn); + + + vector comments; + }; #endif /* ofxOceanodeContainer_h */ diff --git a/src/Managers/ofxOceanodeTypesRegistry.cpp b/src/Managers/ofxOceanodeTypesRegistry.cpp index 1859297..4aae16c 100644 --- a/src/Managers/ofxOceanodeTypesRegistry.cpp +++ b/src/Managers/ofxOceanodeTypesRegistry.cpp @@ -34,3 +34,11 @@ shared_ptr ofxOceanodeTypesRegistry::createOceanod } return nullptr; } + +std::string ofxOceanodeTypesRegistry::getTypeNameFromTypeDescription(std::string description){ + for(auto functionForType : paramTypeToNameColector){ + string name = functionForType(description); + if(name != "") return name; + } + return ""; +} diff --git a/src/Managers/ofxOceanodeTypesRegistry.h b/src/Managers/ofxOceanodeTypesRegistry.h index a746e27..f5a316a 100644 --- a/src/Managers/ofxOceanodeTypesRegistry.h +++ b/src/Managers/ofxOceanodeTypesRegistry.h @@ -17,6 +17,7 @@ class ofxOceanodeTypesRegistry{ using registryCreator = std::function; using routerCreator = std::function(ofxOceanodeNode* routerNode)>; using absParamCreator = std::function (ofAbstractParameter &p)>; + using paramTypeToName = std::function; ofxOceanodeTypesRegistry(); ~ofxOceanodeTypesRegistry(){}; @@ -27,7 +28,7 @@ class ofxOceanodeTypesRegistry{ } template - void registerType(){ + void registerType(std::string name = typeid(T).name()){ registryCreator creator = [](ofxOceanodeContainer &container, ofxOceanodeAbstractParameter &source, ofxOceanodeAbstractParameter &sink, bool active) -> ofxOceanodeAbstractConnection* { if(source.valueType() == typeid(T).name()){ @@ -101,22 +102,33 @@ class ofxOceanodeTypesRegistry{ }; absParamColector.push_back(std::move(aPCreator)); + + paramTypeToName pTtoName = [name](std::string description) -> std::string{ + if(description == typeid(T).name()){ + return name; + } + return ""; + }; + + paramTypeToNameColector.push_back(std::move(pTtoName)); } ofxOceanodeAbstractConnection* createCustomTypeConnection(ofxOceanodeContainer &container, ofxOceanodeAbstractParameter &source, ofxOceanodeAbstractParameter &sink, bool active = true); shared_ptr createRouterFromType(ofxOceanodeNode* routerNode); shared_ptr createOceanodeAbstractFromAbstract(ofAbstractParameter &p); + std::string getTypeNameFromTypeDescription(std::string description); private: vector registryColector; vector routerColector; vector absParamColector; + vector paramTypeToNameColector; ofEventListeners listeners; }; template<> -inline void ofxOceanodeTypesRegistry::registerType(){ +inline void ofxOceanodeTypesRegistry::registerType(std::string name){ registryCreator creator = [](ofxOceanodeContainer &container, ofxOceanodeAbstractParameter &source, ofxOceanodeAbstractParameter &sink, bool active) -> ofxOceanodeAbstractConnection* { if(source.valueType() == typeid(void).name() && sink.valueType() == typeid(void).name()){ @@ -160,6 +172,15 @@ inline void ofxOceanodeTypesRegistry::registerType(){ }; absParamColector.push_back(std::move(aPCreator)); + + paramTypeToName pTtoName = [name](std::string description) -> std::string{ + if(description == typeid(void).name()){ + return name; + } + return ""; + }; + + paramTypeToNameColector.push_back(std::move(pTtoName)); } #endif /* ofxOceanodeTypesRegistry_h */ diff --git a/src/ofxOceanode.cpp b/src/ofxOceanode.cpp index af1f786..7d557d0 100644 --- a/src/ofxOceanode.cpp +++ b/src/ofxOceanode.cpp @@ -27,17 +27,17 @@ ofxOceanode::ofxOceanode(){ #endif //Register default types - typesRegistry->registerType(); - typesRegistry->registerType(); - typesRegistry->registerType(); - typesRegistry->registerType(); - typesRegistry->registerType(); - typesRegistry->registerType(); - typesRegistry->registerType>(); - typesRegistry->registerType>(); - typesRegistry->registerType(); - typesRegistry->registerType(); - typesRegistry->registerType(); + typesRegistry->registerType("f"); + typesRegistry->registerType("i"); + typesRegistry->registerType("b"); + typesRegistry->registerType("v"); + typesRegistry->registerType("s"); + typesRegistry->registerType("c"); + typesRegistry->registerType>("v_f"); + typesRegistry->registerType>("v_i"); + typesRegistry->registerType("color"); + typesRegistry->registerType("color_f"); + typesRegistry->registerType("timestamp"); //Register default nodes nodeRegistry->registerModel("Generators"); diff --git a/src/ofxOceanode.h b/src/ofxOceanode.h index e2f944b..dd72849 100644 --- a/src/ofxOceanode.h +++ b/src/ofxOceanode.h @@ -54,7 +54,7 @@ class ofxOceanode { template void registerType(string name = typeid(T).name(), T defaultValue = T()){ - typesRegistry->registerType(); + typesRegistry->registerType(name); nodeRegistry->registerModel>("Router", name, defaultValue); nodeRegistry->registerModel>("Portal", name, defaultValue); }; @@ -64,15 +64,15 @@ class ofxOceanode { std::function bufferAssignFunction = [](T1 &data, T2 &container){container = data;}, std::function bufferReturnFunction = [](T2 &container)->T1{return container;}, std::function bufferCheckFunction = [](T1 &data)->bool{return true;}){ - typesRegistry->registerType(); + typesRegistry->registerType(name); nodeRegistry->registerModel>("Router", name, defaultValue); nodeRegistry->registerModel>("Portal", name, defaultValue); nodeRegistry->registerModel>("Buffer", name, defaultValue, false, bufferAssignFunction, bufferReturnFunction, bufferCheckFunction); - typesRegistry->registerType*>(); + typesRegistry->registerType*>("buffer_" + name); nodeRegistry->registerModel*>>("Router", "buffer_" + name, nullptr); nodeRegistry->registerModel*>>("Portal", "buffer_" + name, nullptr); nodeRegistry->registerModel>("Header", name, defaultValue); -// registerType>("v_" + name, std::vector(1, defaultValue)); + registerType>("v_" + name, std::vector(1, defaultValue)); }; template diff --git a/src/ofxOceanodeShared.h b/src/ofxOceanodeShared.h index a73fadd..c097ea5 100644 --- a/src/ofxOceanodeShared.h +++ b/src/ofxOceanodeShared.h @@ -183,7 +183,7 @@ class ofxOceanodeShared{ return typedPortals; } - + private: ofxOceanodeShared(){};