Completed the split node functionality

This commit is contained in:
Ankitkumar Satapara
2026-04-18 19:30:00 +05:30
parent 2f532a8536
commit 381e187b6d
7 changed files with 386 additions and 15 deletions

View File

@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
@@ -24,6 +25,9 @@ namespace Nodify.Calculator
c.Input.Value = c.Output.Value; c.Input.Value = c.Output.Value;
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
HandleSplitNodeConnected(c);
}) })
.WhenRemoved(c => .WhenRemoved(c =>
{ {
@@ -41,6 +45,9 @@ namespace Nodify.Calculator
} }
c.Output.ValueObservers.Remove(c.Input); c.Output.ValueObservers.Remove(c.Input);
// Dynamic Split node: clear outputs when model disconnects
HandleSplitNodeDisconnected(c);
}); });
Operations.WhenAdded(x => Operations.WhenAdded(x =>
@@ -112,7 +119,16 @@ namespace Nodify.Calculator
source.Shape == target.Shape && source.Shape == target.Shape &&
!source.IsConnected && !source.IsConnected &&
!target.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) internal void OpenGetSetVariable(Point TargetLocation, string className)
{ {
@@ -179,6 +195,91 @@ namespace Nodify.Calculator
GroupSize = new Size(bounding.Width, bounding.Height) 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";
}
} }
} }

View File

@@ -96,5 +96,37 @@ namespace Nodify.Calculator
[Newtonsoft.Json.JsonIgnore] [Newtonsoft.Json.JsonIgnore]
[BsonIgnore] [BsonIgnore]
public List<ConnectorViewModel> ValueObservers { get; } = new List<ConnectorViewModel>(); 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,
};
}
} }
} }

View 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&lt;string&gt;</sys:String>
<sys:String>List&lt;int&gt;</sys:String>
<sys:String>List&lt;object&gt;</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>

View 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;
}
}
}

View File

@@ -678,8 +678,12 @@
</ItemsControl> </ItemsControl>
<!-- Separator --> <!-- Separator -->
<TextBlock Text="Models" FontWeight="Bold" Margin="0 8 0 4" <StackPanel Orientation="Horizontal" Margin="0 8 0 4">
Visibility="{Binding Calculator.OperationsMenu.AvailableModels.Count, Converter={shared:BooleanToVisibilityConverter}}" /> <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 --> <!-- Class Model Variables -->
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableModels}" <ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableModels}"

View File

@@ -76,7 +76,7 @@ namespace Nodify.Calculator
IsFlowNode = false IsFlowNode = false
}; };
splitNode.Input.Add(""); splitNode.Input.Add("");
splitNode.Output.Add(""); // No default output - outputs are added dynamically on connection
var takeNode = new OperationInfoViewModel() var takeNode = new OperationInfoViewModel()
{ {
@@ -330,6 +330,19 @@ namespace Nodify.Calculator
SystemOperationType = info.sysOp 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.sysOp == SystemOperations.GET_SET && info.IsModelNode && !info.IsSimpleVariable)
{ {
if (info.Title == "GET") if (info.Title == "GET")
@@ -345,44 +358,100 @@ namespace Nodify.Calculator
var flpath = System.IO.Path.Combine(customModelDir, info.ClassName + ".cs"); var flpath = System.IO.Path.Combine(customModelDir, info.ClassName + ".cs");
if (File.Exists(flpath)) if (File.Exists(flpath))
{ {
//Read all the properties from the file
var fileContent = File.ReadAllText(flpath); var fileContent = File.ReadAllText(flpath);
// Parse and analyze properties
var properties = GetPropertiesFromClass(fileContent); var properties = GetPropertiesFromClass(fileContent);
// Print out the extracted properties and their types
Console.WriteLine("Properties found:"); Console.WriteLine("Properties found:");
foreach (var property in properties) foreach (var property in properties)
{ {
Console.WriteLine($"Property Name: {property.Name}, Type: {property.Type}"); 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; info.IsFlowNode = true;
} }
var flTitle = $"{info.Title} {info.ClassName}"; var flTitle2 = $"{info.Title} {info.ClassName}";
sysOp.Title = flTitle; sysOp.Title = flTitle2;
} }
if (info.sysOp == SystemOperations.GET_SET && info.IsSimpleVariable) if (info.sysOp == SystemOperations.GET_SET && info.IsSimpleVariable)
{ {
var varLabel = $"{info.Title} {info.ClassName} ({info.VariableType})"; var varLabel = $"{info.Title} {info.ClassName} ({info.VariableType})";
sysOp.Title = varLabel; sysOp.Title = varLabel;
var varType = info.VariableType.ToLower();
var varColor = ConnectorViewModel.GetColorForType(varType);
if (info.Title == "GET") if (info.Title == "GET")
{ {
// GET variable: output the value, no flow needed // GET variable: output the value, no flow needed
info.Output.Add("Value");
info.IsFlowNode = false; 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") else if (info.Title == "SET")
{ {
// SET variable: input connector for the value, flow node // SET variable: input connector for the value, flow node
info.Input.Add("Value");
info.IsFlowNode = true; 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) static List<CustomProperty> GetPropertiesFromClass(string classContent)
{ {
// Parse the C# class content using Roslyn // 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 Name { get; set; }
public string Type { get; set; } public string Type { get; set; }

View File

@@ -161,6 +161,7 @@ namespace Nodify.Calculator
ImportSwaggerCommand = new DelegateCommand(ImportSwagger); ImportSwaggerCommand = new DelegateCommand(ImportSwagger);
AddVariableCommand = new DelegateCommand(AddVariable); AddVariableCommand = new DelegateCommand(AddVariable);
CreateFunctionCommand = new DelegateCommand(CreateFunction); CreateFunctionCommand = new DelegateCommand(CreateFunction);
CreateModelCommand = new DelegateCommand(CreateModel);
} }
@@ -184,6 +185,46 @@ namespace Nodify.Calculator
public INodifyCommand ImportSwaggerCommand { get; } public INodifyCommand ImportSwaggerCommand { get; }
public INodifyCommand AddVariableCommand { get; } public INodifyCommand AddVariableCommand { get; }
public INodifyCommand CreateFunctionCommand { 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() private void CreateFunction()
{ {