Implemented save load functionality to save and load the editor
This commit is contained in:
@@ -39,45 +39,26 @@ namespace Nodify.Calculator
|
|||||||
});
|
});
|
||||||
SaveFileCommand = new DelegateCommand(() =>
|
SaveFileCommand = new DelegateCommand(() =>
|
||||||
{
|
{
|
||||||
var firstEditor = Editors.First();
|
try
|
||||||
var allNodes = firstEditor.Calculator.Operations;
|
|
||||||
|
|
||||||
SaveGraphModel svm = new SaveGraphModel();
|
|
||||||
foreach (var item in allNodes)
|
|
||||||
{
|
{
|
||||||
SaveNodes svn = new SaveNodes()
|
var firstEditor = Editors.First();
|
||||||
{
|
GraphSerializer.Save(firstEditor.Calculator);
|
||||||
Location = item.Location,
|
|
||||||
};
|
|
||||||
svm.Nodes.Add(svn);
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
|
||||||
var jsonEditors = JsonConvert.SerializeObject(svm, new JsonSerializerSettings
|
|
||||||
{
|
{
|
||||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
MessageBox.Show($"Failed to save: {ex.Message}", "Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
});
|
}
|
||||||
//var jsonEditors = JsonConvert.SerializeObject(Editors);
|
|
||||||
File.WriteAllText("SaveFile.AEXN", jsonEditors);
|
|
||||||
});
|
});
|
||||||
OpenFileCommand = new DelegateCommand(() =>
|
OpenFileCommand = new DelegateCommand(() =>
|
||||||
{
|
{
|
||||||
if (File.Exists("SaveFile.AEXN"))
|
try
|
||||||
{
|
{
|
||||||
var allText = File.ReadAllText("SaveFile.AEXN");
|
|
||||||
var edts = JsonConvert.DeserializeObject<SaveGraphModel>(allText);
|
|
||||||
var firstEditor = Editors.First();
|
var firstEditor = Editors.First();
|
||||||
var nodesToAdd = edts.Nodes;
|
GraphSerializer.Load(firstEditor.Calculator, firstEditor.Calculator.OperationsMenu);
|
||||||
foreach (var item in nodesToAdd)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
firstEditor.Calculator.Operations.Add(new()
|
{
|
||||||
{
|
MessageBox.Show($"Failed to load: {ex.Message}", "Load Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
Location = item.Location,
|
|
||||||
NodeId = "H",
|
|
||||||
Title = "HHA"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Editors.WhenAdded((editor) =>
|
Editors.WhenAdded((editor) =>
|
||||||
@@ -98,6 +79,14 @@ namespace Nodify.Calculator
|
|||||||
{
|
{
|
||||||
Name = $"Editor {Editors.Count + 1}"
|
Name = $"Editor {Editors.Count + 1}"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-load saved graph from project DB
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var firstEditor = Editors.First();
|
||||||
|
GraphSerializer.Load(firstEditor.Calculator, firstEditor.Calculator.OperationsMenu);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator)
|
private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace Nodify.Calculator
|
|||||||
{
|
{
|
||||||
public class CalculatorViewModel : ObservableObject
|
public class CalculatorViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
public bool IsLoading { get; set; }
|
||||||
|
|
||||||
public CalculatorViewModel()
|
public CalculatorViewModel()
|
||||||
{
|
{
|
||||||
CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>(
|
CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>(
|
||||||
@@ -27,13 +29,13 @@ namespace Nodify.Calculator
|
|||||||
c.Output.ValueObservers.Add(c.Input);
|
c.Output.ValueObservers.Add(c.Input);
|
||||||
|
|
||||||
// Dynamic Split node: populate outputs when a model connects to the Square input
|
// Dynamic Split node: populate outputs when a model connects to the Square input
|
||||||
HandleSplitNodeConnected(c);
|
if (!IsLoading) HandleSplitNodeConnected(c);
|
||||||
|
|
||||||
// Dynamic Copy node: adapt connectors when something connects
|
// Dynamic Copy node: adapt connectors when something connects
|
||||||
HandleCopyNodeConnected(c);
|
if (!IsLoading) HandleCopyNodeConnected(c);
|
||||||
|
|
||||||
// Dynamic Take node: adapt connectors when a list connects
|
// Dynamic Take node: adapt connectors when a list connects
|
||||||
HandleTakeNodeConnected(c);
|
if (!IsLoading) HandleTakeNodeConnected(c);
|
||||||
})
|
})
|
||||||
.WhenRemoved(c =>
|
.WhenRemoved(c =>
|
||||||
{
|
{
|
||||||
@@ -65,7 +67,7 @@ namespace Nodify.Calculator
|
|||||||
Operations.WhenAdded(x =>
|
Operations.WhenAdded(x =>
|
||||||
{
|
{
|
||||||
x.Input.WhenRemoved(RemoveConnection);
|
x.Input.WhenRemoved(RemoveConnection);
|
||||||
x.NodeId = (Operations.Count + 1).ToString();
|
if (!IsLoading) x.NodeId = (Operations.Count + 1).ToString();
|
||||||
Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}");
|
Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}");
|
||||||
if (x is CalculatorInputOperationViewModel ci)
|
if (x is CalculatorInputOperationViewModel ci)
|
||||||
{
|
{
|
||||||
|
|||||||
420
Examples/Nodify.Calculator/GraphSerializer.cs
Normal file
420
Examples/Nodify.Calculator/GraphSerializer.cs
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
using Nodify.Calculator.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Nodify.Calculator
|
||||||
|
{
|
||||||
|
public static class GraphSerializer
|
||||||
|
{
|
||||||
|
public static void Save(CalculatorViewModel calculator)
|
||||||
|
{
|
||||||
|
var graph = new SaveGraphModel();
|
||||||
|
var operations = calculator.Operations;
|
||||||
|
var connections = calculator.Connections;
|
||||||
|
|
||||||
|
// Ensure all nodes have IDs
|
||||||
|
int idCounter = 1;
|
||||||
|
foreach (var op in operations)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(op.NodeId))
|
||||||
|
op.NodeId = $"node_{idCounter++}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize nodes
|
||||||
|
foreach (var op in operations)
|
||||||
|
{
|
||||||
|
var nodeData = new NodeData
|
||||||
|
{
|
||||||
|
NodeId = op.NodeId,
|
||||||
|
Title = op.Title ?? string.Empty,
|
||||||
|
LocationX = op.Location.X,
|
||||||
|
LocationY = op.Location.Y
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (op)
|
||||||
|
{
|
||||||
|
case TakeOperationViewModel take:
|
||||||
|
nodeData.NodeType = "System";
|
||||||
|
nodeData.SystemOp = take.SystemOperationType.ToString();
|
||||||
|
nodeData.NthIndex = take.NthIndex;
|
||||||
|
nodeData.IsRandom = take.IsRandom;
|
||||||
|
break;
|
||||||
|
case FunctionOperationViewModel func:
|
||||||
|
nodeData.NodeType = "Function";
|
||||||
|
nodeData.FunctionName = func.FunctionName;
|
||||||
|
foreach (var p in func.InputParameters)
|
||||||
|
nodeData.FunctionInputs.Add(new FunctionParamData { Name = p.Name, Type = p.Type });
|
||||||
|
foreach (var p in func.OutputParameters)
|
||||||
|
nodeData.FunctionOutputs.Add(new FunctionParamData { Name = p.Name, Type = p.Type });
|
||||||
|
break;
|
||||||
|
case SystemOperationViewModel sys:
|
||||||
|
nodeData.NodeType = "System";
|
||||||
|
nodeData.SystemOp = sys.SystemOperationType.ToString();
|
||||||
|
// For GET_SET nodes, derive ClassName from title
|
||||||
|
if (sys.SystemOperationType == SystemOperations.GET_SET)
|
||||||
|
{
|
||||||
|
var title = sys.Title ?? "";
|
||||||
|
if (title.StartsWith("GET ")) nodeData.ClassName = title.Substring(4).Trim();
|
||||||
|
else if (title.StartsWith("SET ")) nodeData.ClassName = title.Substring(4).Trim();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case APIOperationViewModel api:
|
||||||
|
nodeData.NodeType = "API";
|
||||||
|
nodeData.OPType = api.OperationType;
|
||||||
|
nodeData.ResponseModelClassName = api.ResponseModelClassName;
|
||||||
|
break;
|
||||||
|
case ExpandoOperationViewModel:
|
||||||
|
nodeData.NodeType = "Expando";
|
||||||
|
break;
|
||||||
|
case CalculatorOperationViewModel:
|
||||||
|
nodeData.NodeType = "Calculator";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
nodeData.NodeType = "Normal";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save connector metadata for all nodes
|
||||||
|
foreach (var c in op.Input)
|
||||||
|
nodeData.InputConnectors.Add(SerializeConnector(c));
|
||||||
|
foreach (var c in op.Output)
|
||||||
|
nodeData.OutputConnectors.Add(SerializeConnector(c));
|
||||||
|
|
||||||
|
graph.Nodes.Add(nodeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize connections
|
||||||
|
foreach (var conn in connections)
|
||||||
|
{
|
||||||
|
var inputOp = conn.Input?.Operation;
|
||||||
|
var outputOp = conn.Output?.Operation;
|
||||||
|
if (inputOp == null || outputOp == null) continue;
|
||||||
|
|
||||||
|
var inputIndex = inputOp.Input.IndexOf(conn.Input);
|
||||||
|
var outputIndex = outputOp.Output.IndexOf(conn.Output);
|
||||||
|
|
||||||
|
if (inputIndex < 0 || outputIndex < 0) continue;
|
||||||
|
|
||||||
|
graph.Connections.Add(new ConnectionData
|
||||||
|
{
|
||||||
|
TargetNodeId = inputOp.NodeId,
|
||||||
|
TargetConnectorIndex = inputIndex,
|
||||||
|
SourceNodeId = outputOp.NodeId,
|
||||||
|
SourceConnectorIndex = outputIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to LiteDB
|
||||||
|
using var db = new LiteDbHelper<SaveGraphModel>("Graph");
|
||||||
|
db.DeleteMany(_ => true);
|
||||||
|
db.Insert(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Load(CalculatorViewModel calculator, OperationsMenuViewModel opsMenu)
|
||||||
|
{
|
||||||
|
SaveGraphModel graph;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var db = new LiteDbHelper<SaveGraphModel>("Graph");
|
||||||
|
graph = db.FindAll().FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graph == null || graph.Nodes.Count == 0) return;
|
||||||
|
|
||||||
|
// Clear existing
|
||||||
|
calculator.Connections.Clear();
|
||||||
|
calculator.Operations.Clear();
|
||||||
|
|
||||||
|
calculator.IsLoading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var nodeMap = new Dictionary<string, OperationViewModel>();
|
||||||
|
|
||||||
|
// Recreate nodes
|
||||||
|
foreach (var nd in graph.Nodes)
|
||||||
|
{
|
||||||
|
OperationViewModel op = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
op = RecreateNode(nd, opsMenu);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op == null) continue;
|
||||||
|
|
||||||
|
op.NodeId = nd.NodeId;
|
||||||
|
op.Location = new System.Windows.Point(nd.LocationX, nd.LocationY);
|
||||||
|
nodeMap[nd.NodeId] = op;
|
||||||
|
calculator.Operations.Add(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate connections
|
||||||
|
foreach (var cd in graph.Connections)
|
||||||
|
{
|
||||||
|
if (!nodeMap.TryGetValue(cd.SourceNodeId, out var sourceOp)) continue;
|
||||||
|
if (!nodeMap.TryGetValue(cd.TargetNodeId, out var targetOp)) continue;
|
||||||
|
if (cd.SourceConnectorIndex >= sourceOp.Output.Count) continue;
|
||||||
|
if (cd.TargetConnectorIndex >= targetOp.Input.Count) continue;
|
||||||
|
|
||||||
|
var sourceConn = sourceOp.Output[cd.SourceConnectorIndex];
|
||||||
|
var targetConn = targetOp.Input[cd.TargetConnectorIndex];
|
||||||
|
|
||||||
|
calculator.Connections.Add(new ConnectionViewModel
|
||||||
|
{
|
||||||
|
Input = targetConn,
|
||||||
|
Output = sourceConn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
calculator.IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OperationViewModel RecreateNode(NodeData nd, OperationsMenuViewModel opsMenu)
|
||||||
|
{
|
||||||
|
switch (nd.NodeType)
|
||||||
|
{
|
||||||
|
case "API":
|
||||||
|
{
|
||||||
|
// Find matching swagger operation or create from saved data
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
OPType = nd.OPType?.ToLower() ?? "get",
|
||||||
|
Type = OperationType.API,
|
||||||
|
ResponseModelClassName = nd.ResponseModelClassName ?? string.Empty
|
||||||
|
};
|
||||||
|
// Restore inputs from saved connector data (skip flow triangle at index 0)
|
||||||
|
foreach (var ic in nd.InputConnectors)
|
||||||
|
{
|
||||||
|
if (ic.Shape != "Triangle")
|
||||||
|
info.Input.Add(ic.Title);
|
||||||
|
}
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "System":
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<SystemOperations>(nd.SystemOp, out var sysOp))
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
Type = OperationType.System,
|
||||||
|
sysOp = sysOp
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sysOp == SystemOperations.GET_SET && !string.IsNullOrEmpty(nd.ClassName))
|
||||||
|
{
|
||||||
|
info.IsModelNode = true;
|
||||||
|
info.ClassName = nd.ClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up inputs/outputs based on system operation type
|
||||||
|
switch (sysOp)
|
||||||
|
{
|
||||||
|
case SystemOperations.BEGIN:
|
||||||
|
info.Output.Add(""); // flow output only
|
||||||
|
break;
|
||||||
|
case SystemOperations.END:
|
||||||
|
info.Input.Add(""); // flow input only
|
||||||
|
break;
|
||||||
|
case SystemOperations.TAKE:
|
||||||
|
info.Input.Add("List");
|
||||||
|
break;
|
||||||
|
case SystemOperations.COPY:
|
||||||
|
info.Input.Add("");
|
||||||
|
break;
|
||||||
|
case SystemOperations.SPLIT:
|
||||||
|
info.Input.Add("");
|
||||||
|
break;
|
||||||
|
case SystemOperations.IF:
|
||||||
|
info.Output.Add("");
|
||||||
|
info.IsFlowNode = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
info.Output.Add("");
|
||||||
|
info.IsFlowNode = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var op = OperationFactory.GetOperation(info);
|
||||||
|
|
||||||
|
// Restore Take properties
|
||||||
|
if (op is TakeOperationViewModel takeOp)
|
||||||
|
{
|
||||||
|
takeOp.NthIndex = nd.NthIndex;
|
||||||
|
takeOp.IsRandom = nd.IsRandom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore dynamic connectors for SPLIT/COPY/TAKE from saved data
|
||||||
|
RestoreDynamicConnectors(op, nd);
|
||||||
|
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Function":
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.FunctionName,
|
||||||
|
Type = OperationType.System,
|
||||||
|
sysOp = SystemOperations.FUNCTION,
|
||||||
|
IsFunction = true
|
||||||
|
};
|
||||||
|
foreach (var inp in nd.FunctionInputs)
|
||||||
|
info.FunctionInputs.Add(new FunctionParameterInfo { Name = inp.Name, Type = inp.Type });
|
||||||
|
foreach (var outp in nd.FunctionOutputs)
|
||||||
|
info.FunctionOutputs.Add(new FunctionParameterInfo { Name = outp.Name, Type = outp.Type });
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Expando":
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
Type = OperationType.Expando
|
||||||
|
};
|
||||||
|
// Restore input count from saved connectors
|
||||||
|
foreach (var ic in nd.InputConnectors)
|
||||||
|
info.Input.Add(ic.Title);
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
Type = OperationType.Normal
|
||||||
|
};
|
||||||
|
foreach (var ic in nd.InputConnectors)
|
||||||
|
info.Input.Add(ic.Title);
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RestoreDynamicConnectors(OperationViewModel op, NodeData nd)
|
||||||
|
{
|
||||||
|
// For SPLIT nodes: if saved outputs have more than just the flow triangle,
|
||||||
|
// restore the dynamic property outputs
|
||||||
|
if (op is SystemOperationViewModel sysVm)
|
||||||
|
{
|
||||||
|
if (sysVm.SystemOperationType == SystemOperations.SPLIT)
|
||||||
|
{
|
||||||
|
// Remove default non-triangle outputs, then add saved ones
|
||||||
|
var defaultNonTriangle = op.Output.Where(o => o.Shape != ConnectorShape.Triangle).ToList();
|
||||||
|
foreach (var d in defaultNonTriangle) op.Output.Remove(d);
|
||||||
|
|
||||||
|
foreach (var sc in nd.OutputConnectors)
|
||||||
|
{
|
||||||
|
if (sc.Shape == "Triangle") continue;
|
||||||
|
op.Output.Add(DeserializeConnector(sc, false));
|
||||||
|
}
|
||||||
|
// Update title from saved
|
||||||
|
sysVm.Title = nd.Title;
|
||||||
|
}
|
||||||
|
else if (sysVm.SystemOperationType == SystemOperations.COPY)
|
||||||
|
{
|
||||||
|
// Restore adapted connectors
|
||||||
|
if (nd.InputConnectors.Count > 0 && nd.InputConnectors[0].Shape != "Circle")
|
||||||
|
{
|
||||||
|
// Input was adapted — restore
|
||||||
|
var savedInput = nd.InputConnectors[0];
|
||||||
|
if (op.Input.Count > 0)
|
||||||
|
{
|
||||||
|
var inp = op.Input[0];
|
||||||
|
inp.Shape = ParseShape(savedInput.Shape);
|
||||||
|
inp.ConnectorColor = Color.FromArgb(savedInput.ColorArgb);
|
||||||
|
inp.DataType = savedInput.DataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore output connectors
|
||||||
|
var existingOutputs = op.Output.ToList();
|
||||||
|
foreach (var eo in existingOutputs) op.Output.Remove(eo);
|
||||||
|
foreach (var sc in nd.OutputConnectors)
|
||||||
|
{
|
||||||
|
op.Output.Add(DeserializeConnector(sc, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sysVm.SystemOperationType == SystemOperations.TAKE)
|
||||||
|
{
|
||||||
|
// Restore adapted list input
|
||||||
|
foreach (var inp in op.Input)
|
||||||
|
{
|
||||||
|
if (!inp.IsTakeListConnector) continue;
|
||||||
|
var savedInp = nd.InputConnectors.FirstOrDefault(c => c.IsTakeListConnector);
|
||||||
|
if (savedInp != null)
|
||||||
|
{
|
||||||
|
inp.Shape = ParseShape(savedInp.Shape);
|
||||||
|
inp.ConnectorColor = Color.FromArgb(savedInp.ColorArgb);
|
||||||
|
inp.DataType = savedInp.DataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Restore output
|
||||||
|
var existingOutputs = op.Output.ToList();
|
||||||
|
foreach (var eo in existingOutputs) op.Output.Remove(eo);
|
||||||
|
foreach (var sc in nd.OutputConnectors)
|
||||||
|
{
|
||||||
|
op.Output.Add(DeserializeConnector(sc, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConnectorData SerializeConnector(ConnectorViewModel c)
|
||||||
|
{
|
||||||
|
return new ConnectorData
|
||||||
|
{
|
||||||
|
Title = c.Title ?? string.Empty,
|
||||||
|
Shape = c.Shape.ToString(),
|
||||||
|
ColorArgb = c.RawColor.ToArgb(),
|
||||||
|
DataType = c.DataType ?? string.Empty,
|
||||||
|
IsCopyConnector = c.IsCopyConnector,
|
||||||
|
IsTakeListConnector = c.IsTakeListConnector
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConnectorViewModel DeserializeConnector(ConnectorData cd, bool isInput)
|
||||||
|
{
|
||||||
|
return new ConnectorViewModel
|
||||||
|
{
|
||||||
|
Title = cd.Title,
|
||||||
|
Shape = ParseShape(cd.Shape),
|
||||||
|
ConnectorColor = Color.FromArgb(cd.ColorArgb),
|
||||||
|
DataType = cd.DataType,
|
||||||
|
IsCopyConnector = cd.IsCopyConnector,
|
||||||
|
IsTakeListConnector = cd.IsTakeListConnector,
|
||||||
|
IsInput = isInput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConnectorShape ParseShape(string shape)
|
||||||
|
{
|
||||||
|
return Enum.TryParse<ConnectorShape>(shape, out var s) ? s : ConnectorShape.Circle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Nodify.Interactivity;
|
using Nodify.Interactivity;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
@@ -16,6 +18,39 @@ namespace Nodify.Calculator
|
|||||||
typeof(UIElement),
|
typeof(UIElement),
|
||||||
Keyboard.PreviewGotKeyboardFocusEvent,
|
Keyboard.PreviewGotKeyboardFocusEvent,
|
||||||
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
|
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
|
||||||
|
|
||||||
|
Closing += MainWindow_Closing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainWindow_Closing(object sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
var result = MessageBox.Show(
|
||||||
|
"Do you want to save the project before closing?",
|
||||||
|
"Save Project",
|
||||||
|
MessageBoxButton.YesNoCancel,
|
||||||
|
MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (result == MessageBoxResult.Cancel)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (DataContext is ApplicationViewModel appVm && appVm.Editors.Count > 0)
|
||||||
|
{
|
||||||
|
var firstEditor = appVm.Editors.First();
|
||||||
|
GraphSerializer.Save(firstEditor.Calculator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Failed to save: {ex.Message}", "Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||||
|
|||||||
@@ -1,20 +1,75 @@
|
|||||||
using Microsoft.CodeAnalysis;
|
using System.Collections.Generic;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace Nodify.Calculator.Models
|
namespace Nodify.Calculator.Models
|
||||||
{
|
{
|
||||||
public class SaveGraphModel
|
public class SaveGraphModel
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public int Id { get; set; } = 1;
|
||||||
|
public List<NodeData> Nodes { get; set; } = new List<NodeData>();
|
||||||
public List<SaveNodes> Nodes { get; set; } = new List<SaveNodes>();
|
public List<ConnectionData> Connections { get; set; } = new List<ConnectionData>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class NodeData
|
||||||
|
{
|
||||||
|
/// <summary>Unique ID matching OperationViewModel.NodeId</summary>
|
||||||
|
public string NodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Discriminator: "API", "System", "Expando", "Calculator", "Function"</summary>
|
||||||
|
public string NodeType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public double LocationX { get; set; }
|
||||||
|
public double LocationY { get; set; }
|
||||||
|
|
||||||
|
// API node properties
|
||||||
|
public string OPType { get; set; } = string.Empty;
|
||||||
|
public string ResponseModelClassName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// System node properties
|
||||||
|
public string SystemOp { get; set; } = string.Empty;
|
||||||
|
public string ClassName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Take node properties
|
||||||
|
public int NthIndex { get; set; }
|
||||||
|
public bool IsRandom { get; set; }
|
||||||
|
|
||||||
|
// Function node properties
|
||||||
|
public string FunctionName { get; set; } = string.Empty;
|
||||||
|
public List<FunctionParamData> FunctionInputs { get; set; } = new List<FunctionParamData>();
|
||||||
|
public List<FunctionParamData> FunctionOutputs { get; set; } = new List<FunctionParamData>();
|
||||||
|
|
||||||
|
/// <summary>Saved input connector metadata (for dynamic nodes like SPLIT/COPY/TAKE)</summary>
|
||||||
|
public List<ConnectorData> InputConnectors { get; set; } = new List<ConnectorData>();
|
||||||
|
/// <summary>Saved output connector metadata</summary>
|
||||||
|
public List<ConnectorData> OutputConnectors { get; set; } = new List<ConnectorData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConnectorData
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Shape { get; set; } = "Circle";
|
||||||
|
public int ColorArgb { get; set; }
|
||||||
|
public string DataType { get; set; } = string.Empty;
|
||||||
|
public bool IsCopyConnector { get; set; }
|
||||||
|
public bool IsTakeListConnector { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConnectionData
|
||||||
|
{
|
||||||
|
public string SourceNodeId { get; set; } = string.Empty;
|
||||||
|
public int SourceConnectorIndex { get; set; }
|
||||||
|
public string TargetNodeId { get; set; } = string.Empty;
|
||||||
|
public int TargetConnectorIndex { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FunctionParamData
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep legacy for compatibility
|
||||||
public class SaveNodes
|
public class SaveNodes
|
||||||
{
|
{
|
||||||
public Point Location { get; set; }
|
public Point Location { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user