Completed the split node functionality
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
@@ -24,6 +25,9 @@ namespace Nodify.Calculator
|
||||
c.Input.Value = c.Output.Value;
|
||||
|
||||
c.Output.ValueObservers.Add(c.Input);
|
||||
|
||||
// Dynamic Split node: populate outputs when a model connects to the Square input
|
||||
HandleSplitNodeConnected(c);
|
||||
})
|
||||
.WhenRemoved(c =>
|
||||
{
|
||||
@@ -41,6 +45,9 @@ namespace Nodify.Calculator
|
||||
}
|
||||
|
||||
c.Output.ValueObservers.Remove(c.Input);
|
||||
|
||||
// Dynamic Split node: clear outputs when model disconnects
|
||||
HandleSplitNodeDisconnected(c);
|
||||
});
|
||||
|
||||
Operations.WhenAdded(x =>
|
||||
@@ -112,7 +119,16 @@ namespace Nodify.Calculator
|
||||
source.Shape == target.Shape &&
|
||||
!source.IsConnected &&
|
||||
!target.IsConnected &&
|
||||
source.IsInput != target.IsInput);
|
||||
source.IsInput != target.IsInput &&
|
||||
AreDataTypesCompatible(source.DataType, target.DataType));
|
||||
|
||||
private static bool AreDataTypesCompatible(string sourceType, string targetType)
|
||||
{
|
||||
// If either side has no DataType set, allow connection (generic/untyped connector)
|
||||
if (string.IsNullOrEmpty(sourceType) || string.IsNullOrEmpty(targetType))
|
||||
return true;
|
||||
return string.Equals(sourceType, targetType, System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal void OpenGetSetVariable(Point TargetLocation, string className)
|
||||
{
|
||||
@@ -179,6 +195,91 @@ namespace Nodify.Calculator
|
||||
GroupSize = new Size(bounding.Width, bounding.Height)
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleSplitNodeConnected(ConnectionViewModel c)
|
||||
{
|
||||
// Determine which side is the Split node's input (Square connector)
|
||||
var splitInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
|
||||
if (splitInput == null) splitInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
|
||||
if (splitInput == null) return;
|
||||
|
||||
var splitOp = splitInput.Operation;
|
||||
if (splitOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.SPLIT)
|
||||
return;
|
||||
|
||||
// The other end is the source — find which model class it comes from
|
||||
var sourceConnector = (c.Input == splitInput) ? c.Output : c.Input;
|
||||
var sourceOp = sourceConnector.Operation;
|
||||
|
||||
// Determine class name from the source operation title (e.g. "GET ClassName" or "SET ClassName")
|
||||
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);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(className)) return;
|
||||
|
||||
// Read model .cs file and parse properties
|
||||
var customModelDir = "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 any existing dynamic outputs (except flow connectors)
|
||||
var toRemove = splitOp.Output.Where(o => o.Shape != ConnectorShape.Triangle).ToList();
|
||||
toRemove.ForEach(o =>
|
||||
{
|
||||
DisconnectConnector(o);
|
||||
splitOp.Output.Remove(o);
|
||||
});
|
||||
|
||||
// Add property outputs
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var propColor = ConnectorViewModel.GetColorForType(prop.Type);
|
||||
splitOp.Output.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = $"{prop.Name} ({prop.Type})",
|
||||
IsInput = false,
|
||||
Shape = ConnectorShape.Circle,
|
||||
ConnectorColor = propColor,
|
||||
DataType = prop.Type.ToLower()
|
||||
});
|
||||
}
|
||||
|
||||
sysVm.Title = $"Split {className}";
|
||||
}
|
||||
|
||||
private void HandleSplitNodeDisconnected(ConnectionViewModel c)
|
||||
{
|
||||
var splitInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
|
||||
if (splitInput == null) splitInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
|
||||
if (splitInput == null) return;
|
||||
|
||||
var splitOp = splitInput.Operation;
|
||||
if (splitOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.SPLIT)
|
||||
return;
|
||||
|
||||
// Check if the Square input still has any connection
|
||||
var stillConnected = Connections.Any(con => con.Input == splitInput || con.Output == splitInput);
|
||||
if (stillConnected) return;
|
||||
|
||||
// Remove all dynamic output connectors and their connections
|
||||
var toRemove = splitOp.Output.Where(o => o.Shape != ConnectorShape.Triangle).ToList();
|
||||
toRemove.ForEach(o =>
|
||||
{
|
||||
DisconnectConnector(o);
|
||||
splitOp.Output.Remove(o);
|
||||
});
|
||||
|
||||
sysVm.Title = "Split";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,5 +96,37 @@ namespace Nodify.Calculator
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[BsonIgnore]
|
||||
public List<ConnectorViewModel> ValueObservers { get; } = new List<ConnectorViewModel>();
|
||||
|
||||
private string _dataType = string.Empty;
|
||||
/// <summary>
|
||||
/// The underlying C# type of this connector (e.g. "string", "int", "double").
|
||||
/// Used to enforce type-safe connections.
|
||||
/// </summary>
|
||||
public string DataType
|
||||
{
|
||||
get => _dataType;
|
||||
set => SetProperty(ref _dataType, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a color based on the C# data type.
|
||||
/// </summary>
|
||||
public static System.Drawing.Color GetColorForType(string typeName)
|
||||
{
|
||||
return (typeName ?? "").ToLower() switch
|
||||
{
|
||||
"string" => System.Drawing.Color.CornflowerBlue,
|
||||
"int" => System.Drawing.Color.LightGreen,
|
||||
"double" => System.Drawing.Color.Orange,
|
||||
"float" => System.Drawing.Color.Gold,
|
||||
"bool" => System.Drawing.Color.Tomato,
|
||||
"decimal" => System.Drawing.Color.MediumOrchid,
|
||||
"long" => System.Drawing.Color.YellowGreen,
|
||||
"datetime" => System.Drawing.Color.DeepSkyBlue,
|
||||
"object" => System.Drawing.Color.Silver,
|
||||
"list<object>" => System.Drawing.Color.Plum,
|
||||
_ => System.Drawing.Color.LightGray,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
Examples/Nodify.Calculator/CreateModelDialog.xaml
Normal file
67
Examples/Nodify.Calculator/CreateModelDialog.xaml
Normal file
@@ -0,0 +1,67 @@
|
||||
<Window x:Class="Nodify.Calculator.CreateModelDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Create New Model"
|
||||
Width="500"
|
||||
Height="420"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize"
|
||||
Background="#2D2D30"
|
||||
Foreground="White">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Model/Class Name -->
|
||||
<TextBlock Text="Model / Class Name:" Grid.Row="0" Margin="0 0 0 4" />
|
||||
<TextBox x:Name="ModelNameBox" Grid.Row="1" Margin="0 0 0 10"
|
||||
Background="#3E3E42" Foreground="White" Padding="4" />
|
||||
|
||||
<!-- Properties -->
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="2" Margin="0 0 0 4">
|
||||
<TextBlock Text="Properties:" FontWeight="Bold" />
|
||||
<Button Content="➕ Add" Margin="10 0 0 0" Padding="8 2" Click="AddProperty_Click"
|
||||
Background="#3E3E42" Foreground="White" Cursor="Hand" />
|
||||
</StackPanel>
|
||||
<DataGrid x:Name="PropertiesGrid" Grid.Row="3" Margin="0 0 0 10"
|
||||
AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="True"
|
||||
Background="#3E3E42" Foreground="White" RowBackground="#3E3E42"
|
||||
AlternatingRowBackground="#333337" GridLinesVisibility="None"
|
||||
HeadersVisibility="Column" BorderBrush="#555">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
|
||||
<DataGridComboBoxColumn Header="Type" SelectedItemBinding="{Binding Type}" Width="120">
|
||||
<DataGridComboBoxColumn.ItemsSource>
|
||||
<x:Array Type="sys:String" xmlns:sys="clr-namespace:System;assembly=System.Runtime">
|
||||
<sys:String>string</sys:String>
|
||||
<sys:String>int</sys:String>
|
||||
<sys:String>double</sys:String>
|
||||
<sys:String>bool</sys:String>
|
||||
<sys:String>float</sys:String>
|
||||
<sys:String>decimal</sys:String>
|
||||
<sys:String>long</sys:String>
|
||||
<sys:String>DateTime</sys:String>
|
||||
<sys:String>object</sys:String>
|
||||
<sys:String>List<string></sys:String>
|
||||
<sys:String>List<int></sys:String>
|
||||
<sys:String>List<object></sys:String>
|
||||
</x:Array>
|
||||
</DataGridComboBoxColumn.ItemsSource>
|
||||
</DataGridComboBoxColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="4">
|
||||
<Button Content="Create" Width="80" Margin="0 0 10 0" Padding="4"
|
||||
Click="OnCreateClick" IsDefault="True" />
|
||||
<Button Content="Cancel" Width="80" Padding="4"
|
||||
Click="OnCancelClick" IsCancel="True" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
55
Examples/Nodify.Calculator/CreateModelDialog.xaml.cs
Normal file
55
Examples/Nodify.Calculator/CreateModelDialog.xaml.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ModelPropertyInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = "string";
|
||||
}
|
||||
|
||||
public partial class CreateModelDialog : Window
|
||||
{
|
||||
public string ModelName { get; private set; } = string.Empty;
|
||||
public List<ModelPropertyInfo> Properties { get; } = new List<ModelPropertyInfo>();
|
||||
|
||||
private readonly ObservableCollection<ModelPropertyInfo> _properties = new ObservableCollection<ModelPropertyInfo>();
|
||||
|
||||
public CreateModelDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
PropertiesGrid.ItemsSource = _properties;
|
||||
}
|
||||
|
||||
private void AddProperty_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_properties.Add(new ModelPropertyInfo { Name = $"Property{_properties.Count + 1}", Type = "string" });
|
||||
}
|
||||
|
||||
private void OnCreateClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ModelNameBox.Text))
|
||||
{
|
||||
MessageBox.Show("Please enter a model name.", "Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_properties.Count == 0)
|
||||
{
|
||||
MessageBox.Show("Please add at least one property.", "Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
ModelName = ModelNameBox.Text.Trim();
|
||||
Properties.AddRange(_properties);
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void OnCancelClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,8 +678,12 @@
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Separator -->
|
||||
<TextBlock Text="Models" FontWeight="Bold" Margin="0 8 0 4"
|
||||
Visibility="{Binding Calculator.OperationsMenu.AvailableModels.Count, Converter={shared:BooleanToVisibilityConverter}}" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0 8 0 4">
|
||||
<TextBlock Text="Models" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<Button Content="➕" Margin="8 0 0 0" Padding="6 1" FontSize="10" Cursor="Hand"
|
||||
Command="{Binding Calculator.OperationsMenu.CreateModelCommand}"
|
||||
Background="#3E3E42" Foreground="White" ToolTip="Create New Model" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Class Model Variables -->
|
||||
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableModels}"
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Nodify.Calculator
|
||||
IsFlowNode = false
|
||||
};
|
||||
splitNode.Input.Add("");
|
||||
splitNode.Output.Add("");
|
||||
// No default output - outputs are added dynamically on connection
|
||||
|
||||
var takeNode = new OperationInfoViewModel()
|
||||
{
|
||||
@@ -330,6 +330,19 @@ namespace Nodify.Calculator
|
||||
SystemOperationType = info.sysOp
|
||||
};
|
||||
|
||||
if (info.sysOp == SystemOperations.SPLIT)
|
||||
{
|
||||
sysOp.Title = info.Title;
|
||||
// Square input to accept model/class objects
|
||||
foreach (var item in input)
|
||||
{
|
||||
item.Shape = ConnectorShape.Square;
|
||||
item.ConnectorColor = Color.MediumPurple;
|
||||
sysOp.Input.Add(item);
|
||||
}
|
||||
return sysOp;
|
||||
}
|
||||
|
||||
if (info.sysOp == SystemOperations.GET_SET && info.IsModelNode && !info.IsSimpleVariable)
|
||||
{
|
||||
if (info.Title == "GET")
|
||||
@@ -345,44 +358,100 @@ namespace Nodify.Calculator
|
||||
var flpath = System.IO.Path.Combine(customModelDir, info.ClassName + ".cs");
|
||||
if (File.Exists(flpath))
|
||||
{
|
||||
//Read all the properties from the file
|
||||
var fileContent = File.ReadAllText(flpath);
|
||||
|
||||
// Parse and analyze properties
|
||||
var properties = GetPropertiesFromClass(fileContent);
|
||||
|
||||
// Print out the extracted properties and their types
|
||||
Console.WriteLine("Properties found:");
|
||||
foreach (var property in properties)
|
||||
{
|
||||
Console.WriteLine($"Property Name: {property.Name}, Type: {property.Type}");
|
||||
info.Output.Add(property.Name);
|
||||
}
|
||||
}
|
||||
|
||||
info.IsFlowNode = true;
|
||||
|
||||
// Build the node with flow connectors first
|
||||
var flTitle = $"{info.Title} {info.ClassName}";
|
||||
sysOp.Title = flTitle;
|
||||
|
||||
// Flow connectors
|
||||
sysOp.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
|
||||
sysOp.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
|
||||
|
||||
// Input: Square connector for the class object
|
||||
foreach (var item in input)
|
||||
{
|
||||
item.Shape = ConnectorShape.Square;
|
||||
item.ConnectorColor = Color.MediumPurple;
|
||||
sysOp.Input.Add(item);
|
||||
}
|
||||
|
||||
// Outputs: Circle connectors per property, colored by type
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propColor = ConnectorViewModel.GetColorForType(property.Type);
|
||||
var propType = property.Type.ToLower();
|
||||
sysOp.Output.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = $"{property.Name} ({property.Type})",
|
||||
IsInput = false,
|
||||
Shape = ConnectorShape.Circle,
|
||||
ConnectorColor = propColor,
|
||||
DataType = propType
|
||||
});
|
||||
}
|
||||
|
||||
return sysOp;
|
||||
}
|
||||
|
||||
info.IsFlowNode = true;
|
||||
}
|
||||
var flTitle = $"{info.Title} {info.ClassName}";
|
||||
sysOp.Title = flTitle;
|
||||
var flTitle2 = $"{info.Title} {info.ClassName}";
|
||||
sysOp.Title = flTitle2;
|
||||
}
|
||||
|
||||
if (info.sysOp == SystemOperations.GET_SET && info.IsSimpleVariable)
|
||||
{
|
||||
var varLabel = $"{info.Title} {info.ClassName} ({info.VariableType})";
|
||||
sysOp.Title = varLabel;
|
||||
var varType = info.VariableType.ToLower();
|
||||
var varColor = ConnectorViewModel.GetColorForType(varType);
|
||||
|
||||
if (info.Title == "GET")
|
||||
{
|
||||
// GET variable: output the value, no flow needed
|
||||
info.Output.Add("Value");
|
||||
info.IsFlowNode = false;
|
||||
|
||||
// Flow connectors not needed, output connector with type color
|
||||
sysOp.Output.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = "Value",
|
||||
IsInput = false,
|
||||
Shape = ConnectorShape.Circle,
|
||||
ConnectorColor = varColor,
|
||||
DataType = varType
|
||||
});
|
||||
|
||||
return sysOp;
|
||||
}
|
||||
else if (info.Title == "SET")
|
||||
{
|
||||
// SET variable: input connector for the value, flow node
|
||||
info.Input.Add("Value");
|
||||
info.IsFlowNode = true;
|
||||
|
||||
// Flow connectors
|
||||
sysOp.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
|
||||
sysOp.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
|
||||
|
||||
// Data input with type color
|
||||
sysOp.Input.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = "Value",
|
||||
Shape = ConnectorShape.Circle,
|
||||
ConnectorColor = varColor,
|
||||
DataType = varType
|
||||
});
|
||||
|
||||
return sysOp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +534,8 @@ namespace Nodify.Calculator
|
||||
}
|
||||
}
|
||||
|
||||
public static List<CustomProperty> GetPropertiesFromClassPublic(string classContent) => GetPropertiesFromClass(classContent);
|
||||
|
||||
static List<CustomProperty> GetPropertiesFromClass(string classContent)
|
||||
{
|
||||
// Parse the C# class content using Roslyn
|
||||
@@ -500,7 +571,7 @@ namespace Nodify.Calculator
|
||||
}
|
||||
}
|
||||
|
||||
class CustomProperty // <- Renamed to avoid conflicts with System.Reflection.PropertyInfo
|
||||
public class CustomProperty // <- Renamed to avoid conflicts with System.Reflection.PropertyInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
|
||||
@@ -161,6 +161,7 @@ namespace Nodify.Calculator
|
||||
ImportSwaggerCommand = new DelegateCommand(ImportSwagger);
|
||||
AddVariableCommand = new DelegateCommand(AddVariable);
|
||||
CreateFunctionCommand = new DelegateCommand(CreateFunction);
|
||||
CreateModelCommand = new DelegateCommand(CreateModel);
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +185,46 @@ namespace Nodify.Calculator
|
||||
public INodifyCommand ImportSwaggerCommand { get; }
|
||||
public INodifyCommand AddVariableCommand { get; }
|
||||
public INodifyCommand CreateFunctionCommand { get; }
|
||||
public INodifyCommand CreateModelCommand { get; }
|
||||
|
||||
private void CreateModel()
|
||||
{
|
||||
var dialog = new CreateModelDialog();
|
||||
dialog.Owner = System.Windows.Application.Current.MainWindow;
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
var className = dialog.ModelName;
|
||||
|
||||
if (AvailableModels.Any(m => m.Title == className))
|
||||
{
|
||||
MessageBox.Show($"A model named '{className}' already exists.", "Duplicate Model", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate .cs file
|
||||
var customModelDir = "CustomModels";
|
||||
Directory.CreateDirectory(customModelDir);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"public class {className}");
|
||||
sb.AppendLine("{");
|
||||
foreach (var prop in dialog.Properties)
|
||||
{
|
||||
sb.AppendLine($"\tpublic {prop.Type} {prop.Name} {{ get; set; }}");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), sb.ToString());
|
||||
|
||||
var modelInfo = new OperationInfoViewModel
|
||||
{
|
||||
Title = className,
|
||||
IsModelNode = true,
|
||||
Type = OperationType.System,
|
||||
sysOp = SystemOperations.GET_SET,
|
||||
ClassName = className
|
||||
};
|
||||
AvailableModels.Add(modelInfo);
|
||||
}
|
||||
|
||||
private void CreateFunction()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user