diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs index 808b882..1c532e2 100644 --- a/Examples/Nodify.Calculator/CalculatorViewModel.cs +++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs @@ -31,6 +31,9 @@ namespace Nodify.Calculator // Dynamic Split node: populate outputs when a model connects to the Square input if (!IsLoading) HandleSplitNodeConnected(c); + // Dynamic New Object node: populate inputs when a model connects + if (!IsLoading) HandleNewObjectNodeConnected(c); + // Dynamic Copy node: adapt connectors when something connects if (!IsLoading) HandleCopyNodeConnected(c); @@ -57,6 +60,9 @@ namespace Nodify.Calculator // Dynamic Split node: clear outputs when model disconnects HandleSplitNodeDisconnected(c); + // Dynamic New Object node: clear inputs when model disconnects + HandleNewObjectNodeDisconnected(c); + // Dynamic Copy node: reset on disconnect HandleCopyNodeDisconnected(c); @@ -336,6 +342,118 @@ namespace Nodify.Calculator sysVm.Title = "Split"; } + private void HandleNewObjectNodeConnected(ConnectionViewModel c) + { + // Find the New Object node's Square input connector + var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null; + if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null; + if (squareInput == null) return; + + var newObjOp = squareInput.Operation; + if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT) + return; + + // The other end is the source — find which model class it comes from + var sourceConnector = (c.Input == squareInput) ? c.Output : c.Input; + var sourceOp = sourceConnector.Operation; + + string className = null; + if (sourceOp is SystemOperationViewModel srcSys) + { + var title = srcSys.Title ?? ""; + if (title.StartsWith("GET ")) className = title.Substring(4).Trim(); + else if (title.StartsWith("SET ")) className = title.Split(' ').ElementAtOrDefault(1); + } + else if (sourceOp is APIOperationViewModel apiVm) + { + className = apiVm.ResponseModelClassName; + if (!string.IsNullOrEmpty(className) && className.StartsWith("List<") && className.EndsWith(">")) + className = className.Substring(5, className.Length - 6); + } + + if (string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(sourceConnector.DataType)) + { + var dt = sourceConnector.DataType; + if (dt.StartsWith("List<") && dt.EndsWith(">")) + dt = dt.Substring(5, dt.Length - 6); + if (!new[] { "string", "int", "double", "bool", "object" }.Contains(dt.ToLower())) + className = dt; + } + + if (string.IsNullOrEmpty(className)) return; + + var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels"); + var filePath = Path.Combine(customModelDir, $"{className}.cs"); + if (!File.Exists(filePath)) return; + + var fileContent = File.ReadAllText(filePath); + var properties = OperationFactory.GetPropertiesFromClassPublic(fileContent); + if (properties == null || properties.Count == 0) return; + + // Remove existing dynamic inputs (non-triangle, non-square) + var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList(); + toRemove.ForEach(i => + { + DisconnectConnector(i); + newObjOp.Input.Remove(i); + }); + + // Add property inputs + foreach (var prop in properties) + { + var propColor = ConnectorViewModel.GetColorForType(prop.Type); + newObjOp.Input.Add(new ConnectorViewModel + { + Title = $"{prop.Name} ({prop.Type})", + Shape = ConnectorShape.Circle, + ConnectorColor = propColor, + DataType = prop.Type.ToLower() + }); + } + + // Update the output connector to reflect the model type + var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square); + if (modelOutput != null) + { + modelOutput.Title = className; + modelOutput.DataType = className; + } + + sysVm.Title = $"New {className}"; + } + + private void HandleNewObjectNodeDisconnected(ConnectionViewModel c) + { + var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null; + if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null; + if (squareInput == null) return; + + var newObjOp = squareInput.Operation; + if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT) + return; + + var stillConnected = Connections.Any(con => con.Input == squareInput || con.Output == squareInput); + if (stillConnected) return; + + // Remove dynamic property inputs (keep triangle and square connectors) + var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList(); + toRemove.ForEach(i => + { + DisconnectConnector(i); + newObjOp.Input.Remove(i); + }); + + // Reset output + var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square); + if (modelOutput != null) + { + modelOutput.Title = "Object"; + modelOutput.DataType = "object"; + } + + sysVm.Title = "New Object"; + } + private void HandleCopyNodeConnected(ConnectionViewModel c) { // Find the COPY node's input connector diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs index 115efa3..a979824 100644 --- a/Examples/Nodify.Calculator/Executor.cs +++ b/Examples/Nodify.Calculator/Executor.cs @@ -456,6 +456,52 @@ namespace Nodify.Calculator return; } + // Handle New Object nodes — construct JSON from property inputs + if (op is SystemOperationViewModel newObjSysOp && newObjSysOp.SystemOperationType == SystemOperations.NEW_OBJECT) + { + OnLogMe?.Invoke($"Constructing new object: {newObjSysOp.Title}"); + var jObj = new JObject(); + // Iterate property inputs (skip triangle flow + square model connectors) + foreach (var inp in op.Input) + { + if (inp.Shape == ConnectorShape.Triangle || inp.Shape == ConnectorShape.Square) continue; + + // Extract property name from title like "Name (string)" + var propName = inp.Title ?? ""; + var parenIdx = propName.IndexOf(" ("); + if (parenIdx > 0) propName = propName.Substring(0, parenIdx); + + // Find connection feeding this input + var inputCon = connections.FirstOrDefault(cn => cn.Input == inp); + string val = null; + if (inputCon != null) + { + var srcNodeId = inputCon.Output.Operation.NodeId; + if (outputs.TryGetValue(srcNodeId, out var srcVal)) + val = srcVal; + else if (inputCon.Output.Value != null) + val = inputCon.Output.Value.ToString(); + } + + if (val != null) + { + // Try to parse as JSON token, fallback to string + try { jObj[propName] = JToken.Parse(val); } + catch { jObj[propName] = val; } + } + else + { + jObj[propName] = null; + } + } + + var json = jObj.ToString(Formatting.None); + outputs[op.NodeId] = json; + variables[op.NodeId] = json; + OnLogMe?.Invoke($"Object constructed: {json}"); + return; + } + // Handle Function nodes — execute the inner flow if (op is FunctionOperationViewModel funcOp) { diff --git a/Examples/Nodify.Calculator/GraphSerializer.cs b/Examples/Nodify.Calculator/GraphSerializer.cs index 459d4d5..ccdf34a 100644 --- a/Examples/Nodify.Calculator/GraphSerializer.cs +++ b/Examples/Nodify.Calculator/GraphSerializer.cs @@ -247,6 +247,10 @@ namespace Nodify.Calculator case SystemOperations.SPLIT: info.Input.Add(""); break; + case SystemOperations.NEW_OBJECT: + info.Input.Add(""); + info.IsFlowNode = true; + break; case SystemOperations.IF: info.Output.Add(""); info.IsFlowNode = true; @@ -340,6 +344,30 @@ namespace Nodify.Calculator // Update title from saved sysVm.Title = nd.Title; } + else if (sysVm.SystemOperationType == SystemOperations.NEW_OBJECT) + { + // Restore dynamic property inputs (keep triangle + square, replace the rest) + var dynamicInputs = op.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList(); + foreach (var d in dynamicInputs) op.Input.Remove(d); + + foreach (var ic in nd.InputConnectors) + { + if (ic.Shape == "Triangle" || ic.Shape == "Square") continue; + op.Input.Add(DeserializeConnector(ic, true)); + } + + // Restore output connector metadata (Square model output) + var savedModelOutput = nd.OutputConnectors.FirstOrDefault(o => o.Shape == "Square"); + var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square); + if (savedModelOutput != null && modelOutput != null) + { + modelOutput.Title = savedModelOutput.Title; + modelOutput.DataType = savedModelOutput.DataType; + modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb); + } + + sysVm.Title = nd.Title; + } else if (sysVm.SystemOperationType == SystemOperations.COPY) { // Restore adapted connectors diff --git a/Examples/Nodify.Calculator/Operations/OperationFactory.cs b/Examples/Nodify.Calculator/Operations/OperationFactory.cs index 5e472cf..a0725bf 100644 --- a/Examples/Nodify.Calculator/Operations/OperationFactory.cs +++ b/Examples/Nodify.Calculator/Operations/OperationFactory.cs @@ -108,9 +108,19 @@ namespace Nodify.Calculator }; debugNode.Input.Add("Value"); + var newObjectNode = new OperationInfoViewModel() + { + Title = "New Object", + Type = OperationType.System, + sysOp = SystemOperations.NEW_OBJECT, + IsFlowNode = true + }; + newObjectNode.Input.Add(""); + systemNodes.Add(authNode); systemNodes.Add(copynode); systemNodes.Add(debugNode); + systemNodes.Add(newObjectNode); systemNodes.Add(begin); systemNodes.Add(ending); systemNodes.Add(debugAndCreateModels); @@ -331,6 +341,35 @@ namespace Nodify.Calculator return funcOp; } + if (info.sysOp == SystemOperations.NEW_OBJECT) + { + var newObjOp = new SystemOperationViewModel + { + Title = info.Title, + SystemOperationType = SystemOperations.NEW_OBJECT + }; + // Flow connectors + newObjOp.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle }); + newObjOp.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false }); + // Model class input (Square — user connects a model source) + foreach (var inp in input) + { + inp.Shape = ConnectorShape.Square; + inp.ConnectorColor = Color.MediumPurple; + newObjOp.Input.Add(inp); + } + // Model output (Square — returns the constructed object) + newObjOp.Output.Add(new ConnectorViewModel + { + Title = "Object", + IsInput = false, + Shape = ConnectorShape.Square, + ConnectorColor = Color.MediumPurple, + DataType = "object" + }); + return newObjOp; + } + if (info.sysOp == SystemOperations.DEBUG) { var debugOp = new SystemOperationViewModel diff --git a/Examples/Nodify.Calculator/SystemOperationViewModel.cs b/Examples/Nodify.Calculator/SystemOperationViewModel.cs index 55deb5a..3c8bee7 100644 --- a/Examples/Nodify.Calculator/SystemOperationViewModel.cs +++ b/Examples/Nodify.Calculator/SystemOperationViewModel.cs @@ -19,7 +19,8 @@ namespace Nodify.Calculator SPLIT, AUTH, FUNCTION, - DEBUG + DEBUG, + NEW_OBJECT } public class SystemOperationViewModel : OperationViewModel