/// <summary> /// Collapse a set of nodes in a given workspace. /// </summary> /// <param name="selectedNodes"> The function definition for the user-defined node </param> /// <param name="currentWorkspace"> The workspace where</param> /// <param name="isTestMode"></param> /// <param name="args"></param> public CustomNodeWorkspaceModel Collapse( IEnumerable<NodeModel> selectedNodes, WorkspaceModel currentWorkspace, bool isTestMode, FunctionNamePromptEventArgs args) { var selectedNodeSet = new HashSet<NodeModel>(selectedNodes); // Note that undoable actions are only recorded for the "currentWorkspace", // the nodes which get moved into "newNodeWorkspace" are not recorded for undo, // even in the new workspace. Their creations will simply be treated as part of // the opening of that new workspace (i.e. when a user opens a file, she will // not expect the nodes that show up to be undoable). // // After local nodes are moved into "newNodeWorkspace" as the result of // conversion, if user performs an undo, new set of nodes will be created in // "currentWorkspace" (not moving those nodes in the "newNodeWorkspace" back // into "currentWorkspace"). In another word, undo recording is on a per- // workspace basis, it does not work across different workspaces. // UndoRedoRecorder undoRecorder = currentWorkspace.UndoRecorder; CustomNodeWorkspaceModel newWorkspace; using (undoRecorder.BeginActionGroup()) { #region Determine Inputs and Outputs //Step 1: determine which nodes will be inputs to the new node var inputs = new HashSet<Tuple<NodeModel, int, Tuple<int, NodeModel>>>( selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.InPortData.Count) .Where(node.HasConnectedInput) .Select(data => Tuple.Create(node, data, node.InputNodes[data])) .Where(input => !selectedNodeSet.Contains(input.Item3.Item2)))); var outputs = new HashSet<Tuple<NodeModel, int, Tuple<int, NodeModel>>>( selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.OutPortData.Count) .Where(node.HasOutput) .SelectMany( data => node.OutputNodes[data].Where( output => !selectedNodeSet.Contains(output.Item2)) .Select(output => Tuple.Create(node, data, output))))); #endregion #region Detect 1-node holes (higher-order function extraction) Log(Properties.Resources.CouldNotRepairOneNodeHoles, WarningLevel.Mild); // http://adsk-oss.myjetbrains.com/youtrack/issue/MAGN-5603 //var curriedNodeArgs = // new HashSet<NodeModel>( // inputs.Select(x => x.Item3.Item2) // .Intersect(outputs.Select(x => x.Item3.Item2))).Select( // outerNode => // { // //var node = new Apply1(); // var node = newNodeWorkspace.AddNode<Apply1>(); // node.SetNickNameFromAttribute(); // node.DisableReporting(); // node.X = outerNode.X; // node.Y = outerNode.Y; // //Fetch all input ports // // in order // // that have inputs // // and whose input comes from an inner node // List<int> inPortsConnected = // Enumerable.Range(0, outerNode.InPortData.Count) // .Where( // x => // outerNode.HasInput(x) // && selectedNodeSet.Contains( // outerNode.Inputs[x].Item2)) // .ToList(); // var nodeInputs = // outputs.Where(output => output.Item3.Item2 == outerNode) // .Select( // output => // new // { // InnerNodeInputSender = output.Item1, // OuterNodeInPortData = output.Item3.Item1 // }) // .ToList(); // nodeInputs.ForEach(_ => node.AddInput()); // node.RegisterAllPorts(); // return // new // { // OuterNode = outerNode, // InnerNode = node, // Outputs = // inputs.Where( // input => input.Item3.Item2 == outerNode) // .Select(input => input.Item3.Item1), // Inputs = nodeInputs, // OuterNodePortDataList = inPortsConnected // }; // }).ToList(); #endregion #region UI Positioning Calculations double avgX = selectedNodeSet.Average(node => node.X); double avgY = selectedNodeSet.Average(node => node.Y); double leftMost = selectedNodeSet.Min(node => node.X); double topMost = selectedNodeSet.Min(node => node.Y); double rightMost = selectedNodeSet.Max(node => node.X + node.Width); double leftShift = leftMost - 250; #endregion #region Handle full selected connectors // Step 2: Determine all the connectors whose start/end owners are // both in the selection set, and then move them from the current // workspace into the new workspace. var fullySelectedConns = new HashSet<ConnectorModel>( currentWorkspace.Connectors.Where( conn => { bool startSelected = selectedNodeSet.Contains(conn.Start.Owner); bool endSelected = selectedNodeSet.Contains(conn.End.Owner); return startSelected && endSelected; })); foreach (var connector in fullySelectedConns) { undoRecorder.RecordDeletionForUndo(connector); connector.Delete(); } #endregion #region Handle partially selected connectors // Step 3: Partially selected connectors (either one of its start // and end owners is in the selection) are to be destroyed. var partiallySelectedConns = currentWorkspace.Connectors.Where( conn => selectedNodeSet.Contains(conn.Start.Owner) || selectedNodeSet.Contains(conn.End.Owner)).ToList(); foreach (var connector in partiallySelectedConns) { undoRecorder.RecordDeletionForUndo(connector); connector.Delete(); } #endregion #region Transfer nodes and connectors to new workspace var newNodes = new List<NodeModel>(); var newAnnotations = new List<AnnotationModel>(); // Step 4: move all nodes to new workspace remove from old // PB: This could be more efficiently handled by a copy paste, but we // are preservering the node foreach (var node in selectedNodeSet) { undoRecorder.RecordDeletionForUndo(node); currentWorkspace.RemoveNode(node); // Assign a new guid to this node, otherwise when node is // compiled to AST, literally it is still in global scope // instead of in function scope. node.GUID = Guid.NewGuid(); node.RenderPackages.Clear(); // shift nodes node.X = node.X - leftShift; node.Y = node.Y - topMost; newNodes.Add(node); } //Copy the group from newNodes foreach (var group in DynamoSelection.Instance.Selection.OfType<AnnotationModel>()) { undoRecorder.RecordDeletionForUndo(group); currentWorkspace.RemoveGroup(group); group.GUID = Guid.NewGuid(); group.SelectedModels = group.DeletedModelBases; newAnnotations.Add(group); } foreach (var conn in fullySelectedConns) { ConnectorModel.Make(conn.Start.Owner, conn.End.Owner, conn.Start.Index, conn.End.Index); } #endregion #region Process inputs var inConnectors = new List<Tuple<NodeModel, int>>(); var uniqueInputSenders = new Dictionary<Tuple<NodeModel, int>, Symbol>(); //Step 3: insert variables (reference step 1) foreach (var input in Enumerable.Range(0, inputs.Count).Zip(inputs, Tuple.Create)) { int inputIndex = input.Item1; NodeModel inputReceiverNode = input.Item2.Item1; int inputReceiverData = input.Item2.Item2; NodeModel inputNode = input.Item2.Item3.Item2; int inputData = input.Item2.Item3.Item1; Symbol node; var key = Tuple.Create(inputNode, inputData); if (uniqueInputSenders.ContainsKey(key)) { node = uniqueInputSenders[key]; } else { inConnectors.Add(Tuple.Create(inputNode, inputData)); node = new Symbol { InputSymbol = inputReceiverNode.InPortData[inputReceiverData].NickName, X = 0 }; // Try to figure out the type of input of custom node // from the type of input of selected node. There are // two kinds of nodes whose input type are available: // function node and custom node. List<Library.TypedParameter> parameters = null; if (inputReceiverNode is Function) { var func = inputReceiverNode as Function; parameters = func.Controller.Definition.Parameters.ToList(); } else if (inputReceiverNode is DSFunctionBase) { var dsFunc = inputReceiverNode as DSFunctionBase; var funcDesc = dsFunc.Controller.Definition; parameters = funcDesc.Parameters.ToList(); if (funcDesc.Type == DSEngine.FunctionType.InstanceMethod || funcDesc.Type == DSEngine.FunctionType.InstanceProperty) { var dummyType = new ProtoCore.Type() { Name = funcDesc.ClassName }; var instanceParam = new TypedParameter(funcDesc.ClassName, dummyType); parameters.Insert(0, instanceParam); } } // so the input of custom node has format // input_var_name : type if (parameters != null && parameters.Count() > inputReceiverData) { var typeName = parameters[inputReceiverData].DisplayTypeName; if (!string.IsNullOrEmpty(typeName)) { node.InputSymbol += " : " + typeName; } } node.SetNickNameFromAttribute(); node.Y = inputIndex*(50 + node.Height); uniqueInputSenders[key] = node; newNodes.Add(node); } //var curriedNode = curriedNodeArgs.FirstOrDefault(x => x.OuterNode == inputNode); //if (curriedNode == null) //{ ConnectorModel.Make(node, inputReceiverNode, 0, inputReceiverData); //} //else //{ // //Connect it to the applier // newNodeWorkspace.AddConnection(node, curriedNode.InnerNode, 0, 0); // //Connect applier to the inner input receive // newNodeWorkspace.AddConnection( // curriedNode.InnerNode, // inputReceiverNode, // 0, // inputReceiverData); //} } #endregion #region Process outputs //List of all inner nodes to connect an output. Unique. var outportList = new List<Tuple<NodeModel, int>>(); var outConnectors = new List<Tuple<NodeModel, int, int>>(); int i = 0; if (outputs.Any()) { foreach (var output in outputs) { if (outportList.All(x => !(x.Item1 == output.Item1 && x.Item2 == output.Item2))) { NodeModel outputSenderNode = output.Item1; int outputSenderData = output.Item2; //NodeModel outputReceiverNode = output.Item3.Item2; //if (curriedNodeArgs.Any(x => x.OuterNode == outputReceiverNode)) // continue; outportList.Add(Tuple.Create(outputSenderNode, outputSenderData)); //Create Symbol Node var node = new Output { Symbol = outputSenderNode.OutPortData[outputSenderData].NickName, X = rightMost + 75 - leftShift }; node.Y = i*(50 + node.Height); node.SetNickNameFromAttribute(); newNodes.Add(node); ConnectorModel.Make(outputSenderNode, node, outputSenderData, 0); i++; } } //Connect outputs to new node outConnectors.AddRange( from output in outputs let outputSenderNode = output.Item1 let outputSenderData = output.Item2 let outputReceiverData = output.Item3.Item1 let outputReceiverNode = output.Item3.Item2 select Tuple.Create( outputReceiverNode, outportList.FindIndex( x => x.Item1 == outputSenderNode && x.Item2 == outputSenderData), outputReceiverData)); } else { foreach (var hanging in selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.OutPortData.Count) .Where(port => !node.HasOutput(port)) .Select(port => new { node, port })).Distinct()) { //Create Symbol Node var node = new Output { Symbol = hanging.node.OutPortData[hanging.port].NickName, X = rightMost + 75 - leftShift }; node.Y = i*(50 + node.Height); node.SetNickNameFromAttribute(); newNodes.Add(node); ConnectorModel.Make(hanging.node, node, hanging.port, 0); i++; } } #endregion var newId = Guid.NewGuid(); newWorkspace = new CustomNodeWorkspaceModel( nodeFactory, newNodes, Enumerable.Empty<NoteModel>(), newAnnotations, Enumerable.Empty<PresetModel>(), new WorkspaceInfo() { X = 0, Y = 0, Name = args.Name, Category = args.Category, Description = args.Description, ID = newId.ToString(), FileName = string.Empty }, currentWorkspace.ElementResolver); newWorkspace.HasUnsavedChanges = true; RegisterCustomNodeWorkspace(newWorkspace); var collapsedNode = CreateCustomNodeInstance(newId, isTestMode: isTestMode); collapsedNode.X = avgX; collapsedNode.Y = avgY; currentWorkspace.AddNode(collapsedNode, centered: false); undoRecorder.RecordCreationForUndo(collapsedNode); foreach (var connector in inConnectors.Select((x, idx) => new { node = x.Item1, from = x.Item2, to = idx }) .Select( nodeTuple => ConnectorModel.Make( nodeTuple.node, collapsedNode, nodeTuple.@from, nodeTuple.to)) .Where(connector => connector != null)) { undoRecorder.RecordCreationForUndo(connector); } foreach (var connector in outConnectors.Select( nodeTuple => ConnectorModel.Make( collapsedNode, nodeTuple.Item1, nodeTuple.Item2, nodeTuple.Item3)).Where(connector => connector != null)) { undoRecorder.RecordCreationForUndo(connector); } } return newWorkspace; }
/// <summary> /// Deserialize a function definition from a given path. A side effect of this function is that /// the node is added to the dictionary of loadedNodes. /// </summary> /// <param name="functionId">The function guid we're currently loading</param> /// <param name="isTestMode"></param> /// <param name="workspace">The resultant function definition</param> /// <returns></returns> private bool InitializeCustomNode(Guid functionId, bool isTestMode, out CustomNodeWorkspaceModel workspace) { try { var customNodeInfo = NodeInfos[functionId]; var xmlPath = customNodeInfo.Path; Log(String.Format(Properties.Resources.LoadingNodeDefinition, customNodeInfo, xmlPath)); var xmlDoc = new XmlDocument(); xmlDoc.Load(xmlPath); WorkspaceInfo info; if (WorkspaceInfo.FromXmlDocument( xmlDoc, xmlPath, isTestMode, false, AsLogger(), out info) && info.IsCustomNodeWorkspace) { info.ID = functionId.ToString(); if (migrationManager.ProcessWorkspace(info, xmlDoc, isTestMode, nodeFactory)) { return InitializeCustomNode(info, xmlDoc, out workspace); } } Log(string.Format(Properties.Resources.CustomNodeCouldNotBeInitialized, customNodeInfo.Name)); workspace = null; return false; } catch (Exception ex) { Log(Properties.Resources.OpenWorkspaceError); Log(ex); if (isTestMode) throw; // Rethrow for NUnit. workspace = null; return false; } }
/// <summary> /// Creates a new Custom Node in the manager. /// </summary> /// <param name="name">Name of the custom node.</param> /// <param name="category">Category for the custom node.</param> /// <param name="description">Description of the custom node.</param> /// <param name="functionId"> /// Optional identifier to be used for the custom node. By default, will make a new unique one. /// </param> /// <returns>Newly created Custom Node Workspace.</returns> public WorkspaceModel CreateCustomNode(string name, string category, string description, Guid? functionId = null) { var newId = functionId ?? Guid.NewGuid(); var info = new WorkspaceInfo() { Name = name, Category = category, Description = description, X = 0, Y = 0, ID = newId.ToString(), FileName = string.Empty }; var workspace = new CustomNodeWorkspaceModel(info, nodeFactory); RegisterCustomNodeWorkspace(workspace); return workspace; }
private void RegisterCustomNodeWorkspace(CustomNodeWorkspaceModel newWorkspace) { RegisterCustomNodeWorkspace( newWorkspace, newWorkspace.CustomNodeInfo, newWorkspace.CustomNodeDefinition); }
private void RegisterCustomNodeWorkspace( CustomNodeWorkspaceModel newWorkspace, CustomNodeInfo info, CustomNodeDefinition definition) { loadedWorkspaceModels[newWorkspace.CustomNodeId] = newWorkspace; SetFunctionDefinition(definition); OnDefinitionUpdated(definition); newWorkspace.DefinitionUpdated += () => { var newDef = newWorkspace.CustomNodeDefinition; SetFunctionDefinition(newDef); OnDefinitionUpdated(newDef); }; SetNodeInfo(info); newWorkspace.InfoChanged += () => { var newInfo = newWorkspace.CustomNodeInfo; SetNodeInfo(newInfo); OnInfoUpdated(newInfo); }; newWorkspace.FunctionIdChanged += oldGuid => { loadedWorkspaceModels.Remove(oldGuid); loadedCustomNodes.Remove(oldGuid); loadOrder.Remove(oldGuid); loadedWorkspaceModels[newWorkspace.CustomNodeId] = newWorkspace; }; }
/// <summary> /// Get the function workspace from a guid /// </summary> /// <param name="id">The unique id for the node.</param> /// <param name="isTestMode"> /// Flag specifying whether or not this should operate in "test mode". /// </param> /// <param name="ws"></param> /// <returns>The path to the node or null if it wasn't found.</returns> public bool TryGetFunctionWorkspace(Guid id, bool isTestMode, out CustomNodeWorkspaceModel ws) { if (Contains(id)) { if (!loadedWorkspaceModels.TryGetValue(id, out ws)) { if (InitializeCustomNode(id, isTestMode, out ws)) return true; } else return true; } ws = null; return false; }
private bool InitializeCustomNode( WorkspaceInfo workspaceInfo, XmlDocument xmlDoc, out CustomNodeWorkspaceModel workspace) { // Add custom node definition firstly so that a recursive // custom node won't recursively load itself. SetPreloadFunctionDefinition(Guid.Parse(workspaceInfo.ID)); var nodeGraph = NodeGraph.LoadGraphFromXml(xmlDoc, nodeFactory); var newWorkspace = new CustomNodeWorkspaceModel( nodeFactory, nodeGraph.Nodes, nodeGraph.Notes, nodeGraph.Annotations, nodeGraph.Presets, workspaceInfo); RegisterCustomNodeWorkspace(newWorkspace); workspace = newWorkspace; return true; }
/// <summary> /// Deserialize a function definition from a given path. A side effect of this function is that /// the node is added to the dictionary of loadedNodes. /// </summary> /// <param name="funcDefGuid">The function guid we're currently loading</param> /// <param name="def">The resultant function definition</param> /// <returns></returns> private bool GetDefinitionFromPath(Guid funcDefGuid, out CustomNodeDefinition def) { try { var xmlPath = GetNodePath(funcDefGuid); #region read xml file var xmlDoc = new XmlDocument(); xmlDoc.Load(xmlPath); string funName = null; string category = ""; double cx = 0; double cy = 0; string description = ""; string version = ""; double zoom = 1.0; string id = ""; // load the header // handle legacy workspace nodes called dynWorkspace // and new workspaces without the dyn prefix XmlNodeList workspaceNodes = xmlDoc.GetElementsByTagName("Workspace"); if(workspaceNodes.Count == 0) workspaceNodes = xmlDoc.GetElementsByTagName("dynWorkspace"); foreach (XmlNode node in workspaceNodes) { foreach (XmlAttribute att in node.Attributes) { if (att.Name.Equals("X")) cx = double.Parse(att.Value, CultureInfo.InvariantCulture); else if (att.Name.Equals("Y")) cy = double.Parse(att.Value, CultureInfo.InvariantCulture); else if (att.Name.Equals("zoom")) zoom = double.Parse(att.Value, CultureInfo.InvariantCulture); else if (att.Name.Equals("Name")) funName = att.Value; else if (att.Name.Equals("Category")) category = att.Value; else if (att.Name.Equals("Description")) description = att.Value; else if (att.Name.Equals("ID")) id = att.Value; else if (att.Name.Equals("Version")) version = att.Value; } } Version fileVersion = MigrationManager.VersionFromString(version); var currentVersion = MigrationManager.VersionFromWorkspace(dynamoModel.HomeSpace); if (fileVersion > currentVersion) { bool resume = Utils.DisplayFutureFileMessage(this.dynamoModel, xmlPath, fileVersion, currentVersion); if (!resume) { def = null; return false; } } var decision = MigrationManager.ShouldMigrateFile(fileVersion, currentVersion); if (decision == MigrationManager.Decision.Abort) { Utils.DisplayObsoleteFileMessage(this.dynamoModel, xmlPath, fileVersion, currentVersion); def = null; return false; } else if (decision == MigrationManager.Decision.Migrate) { string backupPath = string.Empty; bool isTesting = DynamoModel.IsTestMode; // No backup during test. if (!isTesting && MigrationManager.BackupOriginalFile(xmlPath, ref backupPath)) { string message = string.Format( "Original file '{0}' gets backed up at '{1}'", Path.GetFileName(xmlPath), backupPath); dynamoModel.Logger.Log(message); } MigrationManager.Instance.ProcessWorkspaceMigrations(this.dynamoModel, xmlDoc, fileVersion); MigrationManager.Instance.ProcessNodesInWorkspace(this.dynamoModel, xmlDoc, fileVersion); } // we have a dyf and it lacks an ID field, we need to assign it // a deterministic guid based on its name. By doing it deterministically, // files remain compatible if (string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(funName)) { id = GuidUtility.Create(GuidUtility.UrlNamespace, funName).ToString(); } #endregion //DynamoCommands.WriteToLogCmd.Execute("Loading node definition for \"" + funName + "\" from: " + xmlPath); this.dynamoModel.Logger.Log("Loading node definition for \"" + funName + "\" from: " + xmlPath); var ws = new CustomNodeWorkspaceModel(dynamoModel, funName, category.Length > 0 ? category : "Custom Nodes", description, cx, cy) { WatchChanges = false, FileName = xmlPath, Zoom = zoom }; def = new CustomNodeDefinition(Guid.Parse(id)) { WorkspaceModel = ws, IsBeingLoaded = true }; // set the node as loaded LoadedCustomNodes.Remove(def.FunctionId); LoadedCustomNodes.Add(def.FunctionId, def); XmlNodeList elNodes = xmlDoc.GetElementsByTagName("Elements"); XmlNodeList cNodes = xmlDoc.GetElementsByTagName("Connectors"); XmlNodeList nNodes = xmlDoc.GetElementsByTagName("Notes"); if (elNodes.Count == 0) elNodes = xmlDoc.GetElementsByTagName("dynElements"); if (cNodes.Count == 0) cNodes = xmlDoc.GetElementsByTagName("dynConnectors"); if (nNodes.Count == 0) nNodes = xmlDoc.GetElementsByTagName("dynNotes"); XmlNode elNodesList = elNodes[0]; XmlNode cNodesList = cNodes[0]; XmlNode nNodesList = nNodes[0]; #region instantiate nodes foreach (XmlNode elNode in elNodesList.ChildNodes) { XmlAttribute typeAttrib = elNode.Attributes["type"]; XmlAttribute guidAttrib = elNode.Attributes["guid"]; XmlAttribute nicknameAttrib = elNode.Attributes["nickname"]; XmlAttribute xAttrib = elNode.Attributes["x"]; XmlAttribute yAttrib = elNode.Attributes["y"]; XmlAttribute lacingAttrib = elNode.Attributes["lacing"]; XmlAttribute isVisAttrib = elNode.Attributes["isVisible"]; XmlAttribute isUpstreamVisAttrib = elNode.Attributes["isUpstreamVisible"]; string typeName = typeAttrib.Value; //test the GUID to confirm that it is non-zero //if it is zero, then we have to fix it //this will break the connectors, but it won't keep //propagating bad GUIDs var guid = new Guid(guidAttrib.Value); if (guid == Guid.Empty) { guid = Guid.NewGuid(); } string nickname = nicknameAttrib.Value; double x = double.Parse(xAttrib.Value, CultureInfo.InvariantCulture); double y = double.Parse(yAttrib.Value, CultureInfo.InvariantCulture); bool isVisible = true; if (isVisAttrib != null) isVisible = isVisAttrib.Value == "true" ? true : false; bool isUpstreamVisible = true; if (isUpstreamVisAttrib != null) isUpstreamVisible = isUpstreamVisAttrib.Value == "true" ? true : false; // Retrieve optional 'function' attribute (only for DSFunction). XmlAttribute signatureAttrib = elNode.Attributes["function"]; var signature = signatureAttrib == null ? null : signatureAttrib.Value; NodeModel el = null; XmlElement dummyElement = null; try { // The attempt to create node instance may fail due to "type" being // something else other than "NodeModel" derived object type. This // is possible since some legacy nodes have been made to derive from // "MigrationNode" object type that is not derived from "NodeModel". // typeName = Nodes.Utilities.PreprocessTypeName(typeName); System.Type type = Nodes.Utilities.ResolveType(this.dynamoModel, typeName); if (type != null) el = ws.NodeFactory.CreateNodeInstance(type, nickname, signature, guid); if (el != null) { el.Load(elNode); } else { var e = elNode as XmlElement; dummyElement = MigrationManager.CreateMissingNode(e, 1, 1); } } catch (UnresolvedFunctionException) { // If a given function is not found during file load, then convert the // function node into a dummy node (instead of crashing the workflow). // var e = elNode as XmlElement; dummyElement = MigrationManager.CreateUnresolvedFunctionNode(e); } if (dummyElement != null) // If a dummy node placement is desired. { // The new type representing the dummy node. typeName = dummyElement.GetAttribute("type"); System.Type type = Dynamo.Nodes.Utilities.ResolveType(this.dynamoModel, typeName); el = ws.NodeFactory.CreateNodeInstance(type, nickname, string.Empty, guid); el.Load(dummyElement); } ws.Nodes.Add(el); el.X = x; el.Y = y; if (lacingAttrib != null) { LacingStrategy lacing = LacingStrategy.First; Enum.TryParse(lacingAttrib.Value, out lacing); el.ArgumentLacing = lacing; } el.DisableReporting(); // This is to fix MAGN-3648. Method reference in CBN that gets // loaded before method definition causes a CBN to be left in // a warning state. This is to clear such warnings and set the // node to "Dead" state (correct value of which will be set // later on with a call to "EnableReporting" below). Please // refer to the defect for details and other possible fixes. // if (el.State == ElementState.Warning && (el is CodeBlockNodeModel)) el.State = ElementState.Dead; el.IsVisible = isVisible; el.IsUpstreamVisible = isUpstreamVisible; } #endregion #region instantiate connectors foreach (XmlNode connector in cNodesList.ChildNodes) { XmlAttribute guidStartAttrib = connector.Attributes[0]; XmlAttribute intStartAttrib = connector.Attributes[1]; XmlAttribute guidEndAttrib = connector.Attributes[2]; XmlAttribute intEndAttrib = connector.Attributes[3]; XmlAttribute portTypeAttrib = connector.Attributes[4]; var guidStart = new Guid(guidStartAttrib.Value); var guidEnd = new Guid(guidEndAttrib.Value); int startIndex = Convert.ToInt16(intStartAttrib.Value); int endIndex = Convert.ToInt16(intEndAttrib.Value); var portType = ((PortType)Convert.ToInt16(portTypeAttrib.Value)); //find the elements to connect NodeModel start = null; NodeModel end = null; foreach (NodeModel e in ws.Nodes) { if (e.GUID == guidStart) { start = e; } else if (e.GUID == guidEnd) { end = e; } if (start != null && end != null) { break; } } try { var newConnector = ws.AddConnection( start, end, startIndex, endIndex, portType); } catch { dynamoModel.WriteToLog(string.Format("ERROR : Could not create connector between {0} and {1}.", start.NickName, end.NickName)); } } #endregion #region instantiate notes if (nNodesList != null) { foreach (XmlNode note in nNodesList.ChildNodes) { XmlAttribute textAttrib = note.Attributes[0]; XmlAttribute xAttrib = note.Attributes[1]; XmlAttribute yAttrib = note.Attributes[2]; string text = textAttrib.Value; double x = Convert.ToDouble(xAttrib.Value, CultureInfo.InvariantCulture); double y = Convert.ToDouble(yAttrib.Value, CultureInfo.InvariantCulture); ws.AddNote(false, x, y, text, Guid.NewGuid()); } } #endregion foreach (var e in ws.Nodes) e.EnableReporting(); def.IsBeingLoaded = false; def.Compile(this.dynamoModel, this.dynamoModel.EngineController); ws.WatchChanges = true; OnGetDefinitionFromPath(def); } catch (Exception ex) { dynamoModel.WriteToLog("There was an error opening the workbench."); dynamoModel.WriteToLog(ex); if (DynamoModel.IsTestMode) throw ex; // Rethrow for NUnit. def = null; return false; } return true; }
private static void RegisterCustomNodeInstanceForUpdates(Function node, CustomNodeWorkspaceModel workspace) { Action defUpdatedHandler = () => { node.ResyncWithDefinition(workspace.CustomNodeDefinition); }; workspace.DefinitionUpdated += defUpdatedHandler; Action infoChangedHandler = () => { var info = workspace.CustomNodeInfo; node.NickName = info.Name; node.Description = info.Description; node.Category = info.Category; }; workspace.InfoChanged += infoChangedHandler; node.Disposed += (args) => { workspace.DefinitionUpdated -= defUpdatedHandler; workspace.InfoChanged -= infoChangedHandler; }; }
/// <summary> /// Add a new, visible Custom Node workspace to Dynamo /// </summary> /// <param name="workspace"></param> public void AddCustomNodeWorkspace(CustomNodeWorkspaceModel workspace) { AddWorkspace(workspace); }
/// <summary> /// Deserialize a function definition from a given path. A side effect of this function is that /// the node is added to the dictionary of loadedNodes. /// </summary> /// <param name="funcDefGuid">The function guid we're currently loading</param> /// <param name="controller">Reference to the calling controller</param> /// <param name="def">The resultant function definition</param> /// <returns></returns> private bool GetDefinitionFromPath(Guid funcDefGuid, out FunctionDefinition def) { var controller = dynSettings.Controller; try { var xmlPath = GetNodePath(funcDefGuid); #region read xml file var xmlDoc = new XmlDocument(); xmlDoc.Load(xmlPath); string funName = null; string category = ""; double cx = 0; double cy = 0; string description = ""; string version = ""; double zoom = 1.0; string id = ""; // load the header // handle legacy workspace nodes called dynWorkspace // and new workspaces without the dyn prefix XmlNodeList workspaceNodes = xmlDoc.GetElementsByTagName("Workspace"); if(workspaceNodes.Count == 0) workspaceNodes = xmlDoc.GetElementsByTagName("dynWorkspace"); foreach (XmlNode node in workspaceNodes) { foreach (XmlAttribute att in node.Attributes) { if (att.Name.Equals("X")) cx = double.Parse(att.Value, CultureInfo.InvariantCulture); else if (att.Name.Equals("Y")) cy = double.Parse(att.Value, CultureInfo.InvariantCulture); else if (att.Name.Equals("zoom")) zoom = double.Parse(att.Value, CultureInfo.InvariantCulture); else if (att.Name.Equals("Name")) funName = att.Value; else if (att.Name.Equals("Category")) category = att.Value; else if (att.Name.Equals("Description")) description = att.Value; else if (att.Name.Equals("ID")) id = att.Value; else if (att.Name.Equals("Version")) version = att.Value; } } // we have a dyf and it lacks an ID field, we need to assign it // a deterministic guid based on its name. By doing it deterministically, // files remain compatible if (string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(funName)) { id = GuidUtility.Create(GuidUtility.UrlNamespace, funName).ToString(); } #endregion //DynamoCommands.WriteToLogCmd.Execute("Loading node definition for \"" + funName + "\" from: " + xmlPath); dynSettings.Controller.DynamoModel.WriteToLog("Loading node definition for \"" + funName + "\" from: " + xmlPath); var ws = new CustomNodeWorkspaceModel( funName, category.Length > 0 ? category : "Custom Nodes", description, cx, cy) { WatchChanges = false, FileName = xmlPath, Zoom = zoom }; def = new FunctionDefinition(Guid.Parse(id)) { WorkspaceModel = ws }; // load a dummy version, so any nodes depending on this node // will find an (empty) identifier on compilation FScheme.Expression dummyExpression = FScheme.Expression.NewNumber_E(0); controller.FSchemeEnvironment.DefineSymbol(def.FunctionId.ToString(), dummyExpression); // set the node as loaded this.LoadedCustomNodes.Add(def.FunctionId, def); XmlNodeList elNodes = xmlDoc.GetElementsByTagName("Elements"); XmlNodeList cNodes = xmlDoc.GetElementsByTagName("Connectors"); XmlNodeList nNodes = xmlDoc.GetElementsByTagName("Notes"); if (elNodes.Count == 0) elNodes = xmlDoc.GetElementsByTagName("dynElements"); if (cNodes.Count == 0) cNodes = xmlDoc.GetElementsByTagName("dynConnectors"); if (nNodes.Count == 0) nNodes = xmlDoc.GetElementsByTagName("dynNotes"); XmlNode elNodesList = elNodes[0]; XmlNode cNodesList = cNodes[0]; XmlNode nNodesList = nNodes[0]; #region instantiate nodes var badNodes = new List<Guid>(); foreach (XmlNode elNode in elNodesList.ChildNodes) { XmlAttribute typeAttrib = elNode.Attributes["type"]; XmlAttribute guidAttrib = elNode.Attributes["guid"]; XmlAttribute nicknameAttrib = elNode.Attributes["nickname"]; XmlAttribute xAttrib = elNode.Attributes["x"]; XmlAttribute yAttrib = elNode.Attributes["y"]; XmlAttribute lacingAttrib = elNode.Attributes["lacing"]; XmlAttribute isVisAttrib = elNode.Attributes["isVisible"]; XmlAttribute isUpstreamVisAttrib = elNode.Attributes["isUpstreamVisible"]; string typeName = typeAttrib.Value; //test the GUID to confirm that it is non-zero //if it is zero, then we have to fix it //this will break the connectors, but it won't keep //propagating bad GUIDs var guid = new Guid(guidAttrib.Value); if (guid == Guid.Empty) { guid = Guid.NewGuid(); } string nickname = nicknameAttrib.Value; double x = double.Parse(xAttrib.Value, CultureInfo.InvariantCulture); double y = double.Parse(yAttrib.Value, CultureInfo.InvariantCulture); bool isVisible = true; if (isVisAttrib != null) isVisible = isVisAttrib.Value == "true" ? true : false; bool isUpstreamVisible = true; if (isUpstreamVisAttrib != null) isUpstreamVisible = isUpstreamVisAttrib.Value == "true" ? true : false; typeName = Dynamo.Nodes.Utilities.PreprocessTypeName(typeName); System.Type type = Dynamo.Nodes.Utilities.ResolveType(typeName); if (null == type) { badNodes.Add(guid); continue; } NodeModel el = dynSettings.Controller.DynamoModel.CreateNodeInstance(type, nickname, guid); if (lacingAttrib != null) { LacingStrategy lacing = LacingStrategy.First; Enum.TryParse(lacingAttrib.Value, out lacing); el.ArgumentLacing = lacing; } el.IsVisible = isVisible; el.IsUpstreamVisible = isUpstreamVisible; ws.Nodes.Add(el); el.WorkSpace = ws; var node = el; node.X = x; node.Y = y; if (el == null) return false; el.DisableReporting(); el.Load( elNode, string.IsNullOrEmpty(version) ? new Version(0, 0, 0, 0) : new Version(version)); } #endregion #region instantiate connectors foreach (XmlNode connector in cNodesList.ChildNodes) { XmlAttribute guidStartAttrib = connector.Attributes[0]; XmlAttribute intStartAttrib = connector.Attributes[1]; XmlAttribute guidEndAttrib = connector.Attributes[2]; XmlAttribute intEndAttrib = connector.Attributes[3]; XmlAttribute portTypeAttrib = connector.Attributes[4]; var guidStart = new Guid(guidStartAttrib.Value); var guidEnd = new Guid(guidEndAttrib.Value); int startIndex = Convert.ToInt16(intStartAttrib.Value); int endIndex = Convert.ToInt16(intEndAttrib.Value); PortType portType = ((PortType)Convert.ToInt16(portTypeAttrib.Value)); //find the elements to connect NodeModel start = null; NodeModel end = null; if (badNodes.Contains(guidStart) || badNodes.Contains(guidEnd)) continue; foreach (NodeModel e in ws.Nodes) { if (e.GUID == guidStart) { start = e; } else if (e.GUID == guidEnd) { end = e; } if (start != null && end != null) { break; } } try { var newConnector = ConnectorModel.Make( start, end, startIndex, endIndex, portType); if (newConnector != null) ws.Connectors.Add(newConnector); } catch { //DynamoCommands.WriteToLogCmd.Execute(string.Format("ERROR : Could not create connector between {0} and {1}.", start.NickName, end.NickName)); dynSettings.Controller.DynamoModel.WriteToLog(string.Format("ERROR : Could not create connector between {0} and {1}.", start.NickName, end.NickName)); } } #endregion #region instantiate notes if (nNodesList != null) { foreach (XmlNode note in nNodesList.ChildNodes) { XmlAttribute textAttrib = note.Attributes[0]; XmlAttribute xAttrib = note.Attributes[1]; XmlAttribute yAttrib = note.Attributes[2]; string text = textAttrib.Value; double x = Convert.ToDouble(xAttrib.Value, CultureInfo.InvariantCulture); double y = Convert.ToDouble(yAttrib.Value, CultureInfo.InvariantCulture); Guid guid = Guid.NewGuid(); var command = new DynCmd.CreateNoteCommand(guid, text, x, y, false); dynSettings.Controller.DynamoModel.AddNoteInternal(command, ws); } } #endregion foreach (var e in ws.Nodes) e.EnableReporting(); def.CompileAndAddToEnvironment(controller.FSchemeEnvironment); ws.WatchChanges = true; this.OnGetDefinitionFromPath(def); } catch (Exception ex) { dynSettings.Controller.DynamoModel.WriteToLog("There was an error opening the workbench."); dynSettings.Controller.DynamoModel.WriteToLog(ex); if (DynamoController.IsTestMode) Assert.Fail(ex.Message); def = null; return false; } return true; }
public CustomNodeDefinition NewCustomNodeWorkspace( Guid id, string name, string category, string description, bool makeCurrentWorkspace, double workspaceOffsetX = 0, double workspaceOffsetY = 0) { var workSpace = new CustomNodeWorkspaceModel(this, name, category, description, workspaceOffsetX, workspaceOffsetY) { WatchChanges = true }; Workspaces.Add(workSpace); var functionDefinition = new CustomNodeDefinition(id) { WorkspaceModel = workSpace }; functionDefinition.SyncWithWorkspace(this, true, true); if (makeCurrentWorkspace) { CurrentWorkspace = workSpace; } return functionDefinition; }
/// <summary> /// Collapse a set of nodes in a given workspace. /// </summary> /// <param name="dynamoModel">The current DynamoModel</param> /// <param name="selectedNodes"> The function definition for the user-defined node </param> /// <param name="currentWorkspace"> The workspace where</param> /// <param name="args"></param> public static void Collapse(DynamoModel dynamoModel, IEnumerable<NodeModel> selectedNodes, WorkspaceModel currentWorkspace, FunctionNamePromptEventArgs args = null) { var selectedNodeSet = new HashSet<NodeModel>(selectedNodes); if (args == null || !args.Success) { args = new FunctionNamePromptEventArgs(); dynamoModel.OnRequestsFunctionNamePrompt(null, args); if (!args.Success) { return; } } // Note that undoable actions are only recorded for the "currentWorkspace", // the nodes which get moved into "newNodeWorkspace" are not recorded for undo, // even in the new workspace. Their creations will simply be treated as part of // the opening of that new workspace (i.e. when a user opens a file, she will // not expect the nodes that show up to be undoable). // // After local nodes are moved into "newNodeWorkspace" as the result of // conversion, if user performs an undo, new set of nodes will be created in // "currentWorkspace" (not moving those nodes in the "newNodeWorkspace" back // into "currentWorkspace"). In another word, undo recording is on a per- // workspace basis, it does not work across different workspaces. // UndoRedoRecorder undoRecorder = currentWorkspace.UndoRecorder; using (undoRecorder.BeginActionGroup()) { var newNodeWorkspace = new CustomNodeWorkspaceModel( dynamoModel, args.Name, args.Category, args.Description, 0, 0) { WatchChanges = false, HasUnsavedChanges = true }; var newNodeDefinition = new CustomNodeDefinition(Guid.NewGuid()) { WorkspaceModel = newNodeWorkspace }; currentWorkspace.DisableReporting(); #region Determine Inputs and Outputs //Step 1: determine which nodes will be inputs to the new node var inputs = new HashSet<Tuple<NodeModel, int, Tuple<int, NodeModel>>>( selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.InPortData.Count) .Where(node.HasConnectedInput) .Select(data => Tuple.Create(node, data, node.Inputs[data])) .Where(input => !selectedNodeSet.Contains(input.Item3.Item2)))); var outputs = new HashSet<Tuple<NodeModel, int, Tuple<int, NodeModel>>>( selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.OutPortData.Count) .Where(node.HasOutput) .SelectMany( data => node.Outputs[data].Where( output => !selectedNodeSet.Contains(output.Item2)) .Select(output => Tuple.Create(node, data, output))))); #endregion #region Detect 1-node holes (higher-order function extraction) var curriedNodeArgs = new HashSet<NodeModel>( inputs.Select(x => x.Item3.Item2) .Intersect(outputs.Select(x => x.Item3.Item2))).Select( outerNode => { //var node = new Apply1(); var node = newNodeWorkspace.AddNode<Apply1>(); node.SetNickNameFromAttribute(); node.DisableReporting(); node.X = outerNode.X; node.Y = outerNode.Y; //Fetch all input ports // in order // that have inputs // and whose input comes from an inner node List<int> inPortsConnected = Enumerable.Range(0, outerNode.InPortData.Count) .Where( x => outerNode.HasInput(x) && selectedNodeSet.Contains( outerNode.Inputs[x].Item2)) .ToList(); var nodeInputs = outputs.Where(output => output.Item3.Item2 == outerNode) .Select( output => new { InnerNodeInputSender = output.Item1, OuterNodeInPortData = output.Item3.Item1 }) .ToList(); nodeInputs.ForEach(_ => node.AddInput()); node.RegisterAllPorts(); return new { OuterNode = outerNode, InnerNode = node, Outputs = inputs.Where( input => input.Item3.Item2 == outerNode) .Select(input => input.Item3.Item1), Inputs = nodeInputs, OuterNodePortDataList = inPortsConnected }; }).ToList(); #endregion #region UI Positioning Calculations double avgX = selectedNodeSet.Average(node => node.X); double avgY = selectedNodeSet.Average(node => node.Y); double leftMost = selectedNodeSet.Min(node => node.X); double topMost = selectedNodeSet.Min(node => node.Y); double rightMost = selectedNodeSet.Max(node => node.X + node.Width); #endregion #region Handle full selected connectors // Step 2: Determine all the connectors whose start/end owners are // both in the selection set, and then move them from the current // workspace into the new workspace. var fullySelectedConns = new HashSet<ConnectorModel>( currentWorkspace.Connectors.Where( conn => { bool startSelected = selectedNodeSet.Contains(conn.Start.Owner); bool endSelected = selectedNodeSet.Contains(conn.End.Owner); return startSelected && endSelected; })); foreach (var ele in fullySelectedConns) { undoRecorder.RecordDeletionForUndo(ele); currentWorkspace.Connectors.Remove(ele); } #endregion #region Handle partially selected connectors // Step 3: Partially selected connectors (either one of its start // and end owners is in the selection) are to be destroyed. var partiallySelectedConns = currentWorkspace.Connectors.Where( conn => selectedNodeSet.Contains(conn.Start.Owner) || selectedNodeSet.Contains(conn.End.Owner)).ToList(); foreach (ConnectorModel connector in partiallySelectedConns) { undoRecorder.RecordDeletionForUndo(connector); connector.NotifyConnectedPortsOfDeletion(); currentWorkspace.Connectors.Remove(connector); } #endregion #region Transfer nodes and connectors to new workspace // Step 4: move all nodes to new workspace remove from old // PB: This could be more efficiently handled by a copy paste, but we // are preservering the node foreach (var ele in selectedNodeSet) { undoRecorder.RecordDeletionForUndo(ele); ele.SaveResult = false; currentWorkspace.Nodes.Remove(ele); ele.Workspace = newNodeWorkspace; } // add to new newNodeWorkspace.Nodes.AddRange(selectedNodeSet); newNodeWorkspace.Connectors.AddRange(fullySelectedConns); foreach (var node in newNodeWorkspace.Nodes) node.DisableReporting(); double leftShift = leftMost - 250; foreach (NodeModel node in newNodeWorkspace.Nodes) { node.X = node.X - leftShift; node.Y = node.Y - topMost; } #endregion #region Process inputs var inConnectors = new List<Tuple<NodeModel, int>>(); var uniqueInputSenders = new Dictionary<Tuple<NodeModel, int>, Symbol>(); //Step 3: insert variables (reference step 1) foreach (var input in Enumerable.Range(0, inputs.Count).Zip(inputs, Tuple.Create)) { int inputIndex = input.Item1; NodeModel inputReceiverNode = input.Item2.Item1; int inputReceiverData = input.Item2.Item2; NodeModel inputNode = input.Item2.Item3.Item2; int inputData = input.Item2.Item3.Item1; Symbol node; var key = Tuple.Create(inputNode, inputData); if (uniqueInputSenders.ContainsKey(key)) { node = uniqueInputSenders[key]; } else { inConnectors.Add(Tuple.Create(inputNode, inputData)); node = newNodeWorkspace.AddNode<Symbol>(); node.InputSymbol = inputReceiverNode.InPortData[inputReceiverData].NickName; node.SetNickNameFromAttribute(); node.DisableReporting(); node.X = 0; node.Y = inputIndex*(50 + node.Height); uniqueInputSenders[key] = node; } var curriedNode = curriedNodeArgs.FirstOrDefault(x => x.OuterNode == inputNode); if (curriedNode == null) { newNodeWorkspace.AddConnection( node, inputReceiverNode, 0, inputReceiverData); } else { //Connect it to the applier newNodeWorkspace.AddConnection(node, curriedNode.InnerNode, 0, 0); //Connect applier to the inner input receive newNodeWorkspace.AddConnection( curriedNode.InnerNode, inputReceiverNode, 0, inputReceiverData); } } #endregion #region Process outputs //List of all inner nodes to connect an output. Unique. var outportList = new List<Tuple<NodeModel, int>>(); var outConnectors = new List<Tuple<NodeModel, int, int>>(); int i = 0; if (outputs.Any()) { foreach (var output in outputs) { if ( outportList.All( x => !(x.Item1 == output.Item1 && x.Item2 == output.Item2))) { NodeModel outputSenderNode = output.Item1; int outputSenderData = output.Item2; NodeModel outputReceiverNode = output.Item3.Item2; if (curriedNodeArgs.Any(x => x.OuterNode == outputReceiverNode)) continue; outportList.Add(Tuple.Create(outputSenderNode, outputSenderData)); //Create Symbol Node var node = newNodeWorkspace.AddNode<Output>(); node.Symbol = outputSenderNode.OutPortData[outputSenderData].NickName; node.SetNickNameFromAttribute(); node.DisableReporting(); node.X = rightMost + 75 - leftShift; node.Y = i*(50 + node.Height); newNodeWorkspace.AddConnection( outputSenderNode, node, outputSenderData, 0); i++; } } //Connect outputs to new node foreach (var output in outputs) { //Node to be connected to in CurrentWorkspace NodeModel outputSenderNode = output.Item1; //Port to be connected to on outPutNode_outer int outputSenderData = output.Item2; int outputReceiverData = output.Item3.Item1; NodeModel outputReceiverNode = output.Item3.Item2; var curriedNode = curriedNodeArgs.FirstOrDefault(x => x.OuterNode == outputReceiverNode); if (curriedNode == null) { // we create the connectors in the current space later outConnectors.Add( Tuple.Create( outputReceiverNode, outportList.FindIndex( x => x.Item1 == outputSenderNode && x.Item2 == outputSenderData), outputReceiverData)); } else { int targetPort = curriedNode.Inputs.First( x => x.InnerNodeInputSender == outputSenderNode) .OuterNodeInPortData; int targetPortIndex = curriedNode.OuterNodePortDataList.IndexOf(targetPort); //Connect it (new dynConnector) newNodeWorkspace.AddConnection( outputSenderNode, curriedNode.InnerNode, outputSenderData, targetPortIndex + 1); } } } else { foreach (var hanging in selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.OutPortData.Count) .Where(port => !node.HasOutput(port)) .Select(port => new { node, port })).Distinct()) { //Create Symbol Node var node = newNodeWorkspace.AddNode<Output>(); node.Symbol = hanging.node.OutPortData[hanging.port].NickName; node.SetNickNameFromAttribute(); //store the element in the elements list node.DisableReporting(); node.X = rightMost + 75 - leftShift; node.Y = i*(50 + node.Height); newNodeWorkspace.AddConnection(hanging.node, node, hanging.port, 0); i++; } } #endregion // save and load the definition from file newNodeDefinition.SyncWithWorkspace(dynamoModel, true, true); dynamoModel.Workspaces.Add(newNodeWorkspace); string name = newNodeDefinition.FunctionId.ToString(); var collapsedNode = currentWorkspace.AddNode(avgX, avgY, name); undoRecorder.RecordCreationForUndo(collapsedNode); // place the node as intended, not centered collapsedNode.X = avgX; collapsedNode.Y = avgY; collapsedNode.DisableReporting(); foreach ( var nodeTuple in inConnectors.Select( (x, idx) => new { node = x.Item1, from = x.Item2, to = idx })) { var conn = currentWorkspace.AddConnection( nodeTuple.node, collapsedNode, nodeTuple.from, nodeTuple.to); if (conn != null) { undoRecorder.RecordCreationForUndo(conn); } } foreach (var nodeTuple in outConnectors) { var conn = currentWorkspace.AddConnection( collapsedNode, nodeTuple.Item1, nodeTuple.Item2, nodeTuple.Item3); if (conn != null) { undoRecorder.RecordCreationForUndo(conn); } } collapsedNode.EnableReporting(); currentWorkspace.EnableReporting(); foreach (var node in newNodeWorkspace.Nodes) node.EnableReporting(); newNodeWorkspace.WatchChanges = true; } }
/// <summary> /// Collapse a set of nodes in a given workspace. Has the side effects of prompting the user /// first in order to obtain the name and category for the new node, /// writes the function to a dyf file, adds it to the FunctionDict, adds it to search, and compiles and /// places the newly created symbol (defining a lambda) in the Controller's FScheme Environment. /// </summary> /// <param name="selectedNodes"> The function definition for the user-defined node </param> /// <param name="currentWorkspace"> The workspace where</param> public static void Collapse(IEnumerable<NodeModel> selectedNodes, WorkspaceModel currentWorkspace, FunctionNamePromptEventArgs args=null) { var selectedNodeSet = new HashSet<NodeModel>(selectedNodes); if (args == null || !args.Success) { args = new FunctionNamePromptEventArgs(); dynSettings.Controller.DynamoModel.OnRequestsFunctionNamePrompt(null, args); //if (!dynSettings.Controller.DynamoViewModel.ShowNewFunctionDialog(ref newNodeName, ref newNodeCategory)) if (!args.Success) { return; } } var newNodeWorkspace = new CustomNodeWorkspaceModel(args.Name, args.Category, args.Description, 0, 0) { WatchChanges = false, HasUnsavedChanges = true }; var newNodeDefinition = new FunctionDefinition(Guid.NewGuid()) { WorkspaceModel = newNodeWorkspace }; currentWorkspace.DisableReporting(); #region Determine Inputs and Outputs //Step 1: determine which nodes will be inputs to the new node var inputs = new HashSet<Tuple<NodeModel, int, Tuple<int, NodeModel>>>( selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.InPortData.Count).Where(node.HasConnectedInput) .Select(data => Tuple.Create(node, data, node.Inputs[data])) .Where(input => !selectedNodeSet.Contains(input.Item3.Item2)))); var outputs = new HashSet<Tuple<NodeModel, int, Tuple<int, NodeModel>>>( selectedNodeSet.SelectMany( node => Enumerable.Range(0, node.OutPortData.Count).Where(node.HasOutput).SelectMany( data => node.Outputs[data] .Where(output => !selectedNodeSet.Contains(output.Item2)) .Select(output => Tuple.Create(node, data, output))))); #endregion #region Detect 1-node holes (higher-order function extraction) var curriedNodeArgs = new HashSet<NodeModel>( inputs .Select(x => x.Item3.Item2) .Intersect(outputs.Select(x => x.Item3.Item2))) .Select( outerNode => { var node = new Apply1(); //MVVM : Don't make direct reference to view here //MVVM: no reference to view here //dynNodeView nodeUI = node.NodeUI; var elNameAttrib = node.GetType().GetCustomAttributes(typeof(NodeNameAttribute), true)[0] as NodeNameAttribute; if (elNameAttrib != null) { node.NickName = elNameAttrib.Name; } node.GUID = Guid.NewGuid(); //store the element in the elements list newNodeWorkspace.Nodes.Add(node); node.WorkSpace = newNodeWorkspace; node.DisableReporting(); //MVVM : Can't set view location here //dynSettings.Bench.WorkBench.Children.Add(nodeUI); //Place it in an appropriate spot //Canvas.SetLeft(nodeUI, Canvas.GetLeft(outerNode.NodeUI)); //Canvas.SetTop(nodeUI, Canvas.GetTop(outerNode.NodeUI)); node.X = outerNode.X; node.Y = outerNode.Y; //Fetch all input ports // in order // that have inputs // and whose input comes from an inner node List<int> inPortsConnected = Enumerable.Range(0, outerNode.InPortData.Count) .Where( x => outerNode.HasInput(x) && selectedNodeSet.Contains( outerNode.Inputs[x].Item2)) .ToList(); var nodeInputs = outputs .Where(output => output.Item3.Item2 == outerNode) .Select( output => new { InnerNodeInputSender = output.Item1, OuterNodeInPortData = output.Item3.Item1 }).ToList(); nodeInputs.ForEach(_ => node.AddInput()); node.RegisterAllPorts(); return new { OuterNode = outerNode, InnerNode = node, Outputs = inputs.Where(input => input.Item3.Item2 == outerNode) .Select(input => input.Item3.Item1), Inputs = nodeInputs, OuterNodePortDataList = inPortsConnected }; }).ToList(); #endregion #region UI Positioning Calculations double avgX = selectedNodeSet.Average(node => node.X); double avgY = selectedNodeSet.Average(node => node.Y); double leftMost = selectedNodeSet.Min(node => node.X); double topMost = selectedNodeSet.Min(node => node.Y); double rightMost = selectedNodeSet.Max(node => node.X + node.Width); #endregion #region Move selection to new workspace var connectors = new HashSet<ConnectorModel>(currentWorkspace.Connectors.Where( conn => selectedNodeSet.Contains(conn.Start.Owner) && selectedNodeSet.Contains(conn.End.Owner))); //Step 2: move all nodes to new workspace // remove from old foreach (var ele in selectedNodeSet) { ele.SaveResult = false; currentWorkspace.Nodes.Remove(ele); ele.WorkSpace = newNodeWorkspace; } foreach (var ele in connectors) { currentWorkspace.Connectors.Remove(ele); } // add to new newNodeWorkspace.Nodes.AddRange(selectedNodeSet); newNodeWorkspace.Connectors.AddRange(connectors); double leftShift = leftMost - 250; foreach (NodeModel node in newNodeWorkspace.Nodes) { node.X = node.X - leftShift; node.Y = node.Y - topMost; } #endregion #region Insert new node into the current workspace //Step 5: insert new node into original workspace //var collapsedNode = dynSettings.Controller.DynamoViewModel.CreateFunction( // inputs.Select(x => x.Item1.InPortData[x.Item2].NickName), // outputs // .Where(x => !curriedNodeArgs.Any(y => y.OuterNode == x.Item3.Item2)) // .Select(x => x.Item1.OutPortData[x.Item2].NickName), // newNodeDefinition); //collapsedNode.GUID = Guid.NewGuid(); //currentWorkspace.Nodes.Add(collapsedNode); //collapsedNode.WorkSpace = currentWorkspace; //collapsedNode.X = avgX; //collapsedNode.Y = avgY; #endregion #region Destroy all hanging connectors //Step 6: connect inputs and outputs var removeConnectors = currentWorkspace.Connectors.Where(c => selectedNodeSet.Contains(c.Start.Owner) || selectedNodeSet.Contains(c.End.Owner)) .ToList(); foreach (ConnectorModel connector in removeConnectors) { connector.NotifyConnectedPortsOfDeletion(); currentWorkspace.Connectors.Remove(connector); } #endregion newNodeWorkspace.Nodes.ToList().ForEach(x => x.DisableReporting()); var inConnectors = new List<Tuple<NodeModel, int, int>>(); #region Process inputs var uniqueInputSenders = new Dictionary<Tuple<NodeModel, int>, Symbol>(); //Step 3: insert variables (reference step 1) foreach (var input in Enumerable.Range(0, inputs.Count).Zip(inputs, Tuple.Create)) { int inputIndex = input.Item1; NodeModel inputReceiverNode = input.Item2.Item1; int inputReceiverData = input.Item2.Item2; NodeModel inputNode = input.Item2.Item3.Item2; int inputData = input.Item2.Item3.Item1; Symbol node; var key = Tuple.Create(inputNode, inputData); if (uniqueInputSenders.ContainsKey(key)) { node = uniqueInputSenders[key]; } else { //MVVM : replace NodeUI reference with node inConnectors.Add(Tuple.Create(inputNode, inputData, inputIndex)); //Create Symbol Node node = new Symbol { InputSymbol = inputReceiverNode.InPortData[inputReceiverData].NickName }; //MVVM : Don't make direct reference to view here //dynNodeView nodeUI = node.NodeUI; var elNameAttrib = node.GetType().GetCustomAttributes(typeof(NodeNameAttribute), true)[0] as NodeNameAttribute; if (elNameAttrib != null) { node.NickName = elNameAttrib.Name; } node.GUID = Guid.NewGuid(); //store the element in the elements list newNodeWorkspace.Nodes.Add(node); node.WorkSpace = newNodeWorkspace; node.DisableReporting(); node.X = 0; node.Y = inputIndex * (50 + node.Height); uniqueInputSenders[key] = node; } var curriedNode = curriedNodeArgs.FirstOrDefault(x => x.OuterNode == inputNode); if (curriedNode == null) { var conn1 = ConnectorModel.Make(node, inputReceiverNode, 0, inputReceiverData, PortType.INPUT); if (conn1 != null) newNodeWorkspace.Connectors.Add(conn1); } else { //Connect it to the applier var conn = ConnectorModel.Make(node, curriedNode.InnerNode, 0, 0, PortType.INPUT); if (conn != null) newNodeWorkspace.Connectors.Add(conn); //Connect applier to the inner input receive var conn2 = ConnectorModel.Make( curriedNode.InnerNode, inputReceiverNode, 0, inputReceiverData, PortType.INPUT); if (conn2 != null) newNodeWorkspace.Connectors.Add(conn2); } } #endregion #region Process outputs //List of all inner nodes to connect an output. Unique. var outportList = new List<Tuple<NodeModel, int>>(); var outConnectors = new List<Tuple<NodeModel, int, int>>(); int i = 0; foreach (var output in outputs) { if (outportList.All(x => !(x.Item1 == output.Item1 && x.Item2 == output.Item2))) { NodeModel outputSenderNode = output.Item1; int outputSenderData = output.Item2; NodeModel outputReceiverNode = output.Item3.Item2; if (curriedNodeArgs.Any(x => x.OuterNode == outputReceiverNode)) continue; outportList.Add(Tuple.Create(outputSenderNode, outputSenderData)); //Create Symbol Node var node = new Output { Symbol = outputSenderNode.OutPortData[outputSenderData].NickName }; //dynNodeView nodeUI = node.NodeUI; var elNameAttrib = node.GetType().GetCustomAttributes(typeof(NodeNameAttribute), false)[0] as NodeNameAttribute; if (elNameAttrib != null) { node.NickName = elNameAttrib.Name; } node.GUID = Guid.NewGuid(); //store the element in the elements list newNodeWorkspace.Nodes.Add(node); node.WorkSpace = newNodeWorkspace; node.DisableReporting(); node.X = rightMost + 75 - leftShift; node.Y = i*(50 + node.Height); var conn = ConnectorModel.Make( outputSenderNode, node, outputSenderData, 0, PortType.INPUT); if (conn != null) newNodeWorkspace.Connectors.Add(conn); i++; } } //Connect outputs to new node foreach (var output in outputs) { //Node to be connected to in CurrentWorkspace NodeModel outputSenderNode = output.Item1; //Port to be connected to on outPutNode_outer int outputSenderData = output.Item2; int outputReceiverData = output.Item3.Item1; NodeModel outputReceiverNode = output.Item3.Item2; var curriedNode = curriedNodeArgs.FirstOrDefault( x => x.OuterNode == outputReceiverNode); if (curriedNode == null) { // we create the connectors in the current space later //MVVM : replaced multiple dynNodeView refrences with dynNode outConnectors.Add( Tuple.Create( outputReceiverNode, outportList.FindIndex( x => x.Item1 == outputSenderNode && x.Item2 == outputSenderData), outputReceiverData)); } else { int targetPort = curriedNode.Inputs .First( x => x.InnerNodeInputSender == outputSenderNode) .OuterNodeInPortData; int targetPortIndex = curriedNode.OuterNodePortDataList.IndexOf(targetPort); //Connect it (new dynConnector) var conn = ConnectorModel.Make( outputSenderNode, curriedNode.InnerNode, outputSenderData, targetPortIndex + 1, PortType.INPUT); if (conn != null) newNodeWorkspace.Connectors.Add(conn); } } #endregion // save and load the definition from file newNodeDefinition.SyncWithWorkspace(true, true); dynSettings.Controller.DynamoModel.Workspaces.Add(newNodeWorkspace); string name = newNodeDefinition.FunctionId.ToString(); var collapsedNode = dynSettings.Controller.DynamoModel.CreateNode(avgX, avgY, name); // place the node as intended, not centered collapsedNode.X = avgX; collapsedNode.Y = avgY; collapsedNode.DisableReporting(); foreach (var nodeTuple in inConnectors) { var conn = ConnectorModel.Make( nodeTuple.Item1, collapsedNode, nodeTuple.Item2, nodeTuple.Item3, PortType.INPUT); if (conn != null) currentWorkspace.Connectors.Add(conn); } foreach (var nodeTuple in outConnectors) { var conn = ConnectorModel.Make( collapsedNode, nodeTuple.Item1, nodeTuple.Item2, nodeTuple.Item3, PortType.INPUT); if (conn != null) currentWorkspace.Connectors.Add(conn); } collapsedNode.EnableReporting(); currentWorkspace.EnableReporting(); newNodeWorkspace.WatchChanges = true; }