Add unit test for hydraulic component type assignability

- Created a new test class TestItemsSource in test_osBaseItemsSource.cs.
- Implemented TestTypeAssignability method to verify that osHydPipe, osHydDischargeTank, and osHydPump correctly implement the IHydraulicComponent interface.
- Added console output to display results of type checks and comparisons.
This commit is contained in:
Miguel 2025-09-04 18:25:31 +02:00
parent 42c9ab9449
commit de4193dee3
9 changed files with 233 additions and 254 deletions

View File

@ -74,6 +74,9 @@
<None Remove="imagenes\gear.png" />
<None Remove="imagenes\motorNegro.png" />
<None Remove="imagenes\motorVerde.png" />
<None Remove="imagenes\pump.png" />
<None Remove="imagenes\pump_run.png" />
<None Remove="imagenes\pump_stop.png" />
<None Remove="Images\base.png" />
<None Remove="motor2.png" />
<None Remove="tank.png" />
@ -150,6 +153,8 @@
<Resource Include="imagenes\gear.png" />
<Resource Include="imagenes\motorNegro.png" />
<Resource Include="imagenes\motorVerde.png" />
<Resource Include="imagenes\pump_run.png" />
<Resource Include="imagenes\pump_stop.png" />
<Resource Include="imagenes\tank.png" />
<EmbeddedResource Include="Images\base.png" />
</ItemGroup>

View File

@ -169,21 +169,80 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
// Solo crear elemento si ambos componentes están conectados
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
// Crear el elemento Pipe según la documentación
var pipeElement = new Pipe(Length, Diameter, Roughness);
// Obtener los nombres de nodos correctos para cada componente
string fromNodeName = GetNodeNameForComponent(Id_ComponenteA, true); // true = es el nodo origen
string toNodeName = GetNodeNameForComponent(Id_ComponenteB, false); // false = es el nodo destino
elements.Add(new HydraulicElementDefinition(
name: $"{Nombre}_Pipe",
fromNode: Id_ComponenteA, // Usar el nombre del componente A como nodo origen
toNode: Id_ComponenteB, // Usar el nombre del componente B como nodo destino
element: pipeElement,
description: $"Tubería {Nombre} - L:{Length:F2}m, D:{Diameter*1000:F0}mm ({Id_ComponenteA} → {Id_ComponenteB})"
));
if (!string.IsNullOrEmpty(fromNodeName) && !string.IsNullOrEmpty(toNodeName))
{
// Crear el elemento Pipe según la documentación
var pipeElement = new Pipe(Length, Diameter, Roughness);
elements.Add(new HydraulicElementDefinition(
name: $"{Nombre}_Pipe",
fromNode: fromNodeName,
toNode: toNodeName,
element: pipeElement,
description: $"Tubería {Nombre} - L:{Length:F2}m, D:{Diameter*1000:F0}mm ({fromNodeName} → {toNodeName})"
));
}
}
return elements;
}
/// <summary>
/// Obtiene el nombre del nodo correcto para un componente dado
/// </summary>
/// <param name="componentName">Nombre del componente</param>
/// <param name="isSource">True si es el nodo origen de la tubería, False si es destino</param>
/// <returns>Nombre del nodo o string vacío si no se encuentra</returns>
private string GetNodeNameForComponent(string componentName, bool isSource)
{
if (string.IsNullOrEmpty(componentName) || _mainViewModel == null)
return "";
// Buscar el componente hidráulico
var component = _mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IHydraulicComponent comp &&
((osBase)comp).Nombre == componentName) as IHydraulicComponent;
if (component == null)
return "";
// Obtener los nodos que crea este componente
var nodes = component.GetHydraulicNodes();
if (nodes == null || !nodes.Any())
return "";
// Determinar qué nodo usar según el tipo de componente
var componentType = component.GetType();
// Para tanques: siempre usar su único nodo
if (componentType.Name.Contains("Tank"))
{
return nodes.FirstOrDefault()?.Name ?? "";
}
// Para bombas: usar nodo de entrada o salida según corresponda
if (componentType.Name.Contains("Pump"))
{
if (isSource)
{
// Si la tubería sale desde este componente, usar el nodo de salida de la bomba
return nodes.FirstOrDefault(n => n.Name.EndsWith("_Out"))?.Name ?? "";
}
else
{
// Si la tubería llega a este componente, usar el nodo de entrada de la bomba
return nodes.FirstOrDefault(n => n.Name.EndsWith("_In"))?.Name ?? "";
}
}
// Para otros tipos de componentes, usar el primer nodo disponible
return nodes.FirstOrDefault()?.Name ?? "";
}
public void UpdateHydraulicProperties()
{
// Actualizar propiedades antes de la simulación si es necesario
@ -195,18 +254,25 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
// Solo procesar si ambos componentes están conectados
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
string branchKey = $"{Id_ComponenteA}->{Id_ComponenteB}";
// Obtener los nombres de nodos correctos
string fromNodeName = GetNodeNameForComponent(Id_ComponenteA, true);
string toNodeName = GetNodeNameForComponent(Id_ComponenteB, false);
if (flows.TryGetValue(branchKey, out double flow))
if (!string.IsNullOrEmpty(fromNodeName) && !string.IsNullOrEmpty(toNodeName))
{
CurrentFlow = flow;
}
string branchKey = $"{fromNodeName}->{toNodeName}";
// Calcular pérdida de presión entre nodos
if (pressures.TryGetValue(Id_ComponenteA, out double pressureA) &&
pressures.TryGetValue(Id_ComponenteB, out double pressureB))
{
PressureDrop = pressureA - pressureB;
if (flows.TryGetValue(branchKey, out double flow))
{
CurrentFlow = flow;
}
// Calcular pérdida de presión entre nodos
if (pressures.TryGetValue(fromNodeName, out double pressureA) &&
pressures.TryGetValue(toNodeName, out double pressureB))
{
PressureDrop = pressureA - pressureB;
}
}
}
}

View File

@ -18,8 +18,33 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
/// </summary>
public partial class osHydPump : osBase, IHydraulicPump, IosBase
{
public static string NombreCategoria() => "Hidraulico";
public static string NombreClase()
{
return "Bomba Hidráulica";
}
#region Properties
[ObservableProperty]
[property: JsonIgnore]
[property: Category("Apariencia")]
[property: Description("Imagen visual")]
[property: Name("Imagen")]
public ImageSource imageSource_oculta;
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Tamaño visual de la bomba")]
[property: Name("Tamaño")]
public float tamano;
public override void OnResize(float Delta_Width, float Delta_Height)
{
Tamano += Delta_Width + Delta_Height;
}
private double _pumpHead = 50.0; // metros
private double _maxFlow = 0.01; // m³/s (36 m³/h)
private double _speedRatio = 1.0;
@ -103,10 +128,21 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
get => _isRunning;
set
{
SetProperty(ref _isRunning, value);
if (SetProperty(ref _isRunning, value))
{
UpdatePumpImage();
}
}
}
private void UpdatePumpImage()
{
if (IsRunning)
ImageSource_oculta = ImageFromPath("/imagenes/pump_run.png");
else
ImageSource_oculta = ImageFromPath("/imagenes/pump_stop.png");
}
[Category("🔧 Bomba Hidráulica")]
[DisplayName("Dirección")]
[Description("Dirección de la bomba (+1 o -1)")]
@ -175,15 +211,11 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
// Los objetos hidráulicos no necesitan actualizar geometría física
}
public override void OnResize(float Delta_Width, float Delta_Height)
{
Ancho += Delta_Width;
Alto += Delta_Height;
}
public osHydPump()
{
Nombre = "Bomba Hidráulica";
Tamano = 0.30f;
ImageSource_oculta = ImageFromPath("/imagenes/pump_stop.png");
}
public override void UpdateGeometryStart()
@ -211,6 +243,7 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
{
// Los objetos hidráulicos no necesitan geometría física
base.ucLoaded();
UpdatePumpImage();
}
public override void ucUnLoaded()
@ -242,11 +275,17 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
if (HasHydraulicComponents)
{
// Calcular velocidad efectiva basada en el estado de la bomba
double effectiveSpeed = IsRunning ? SpeedRatio : 0.0;
// Asegurar que la velocidad esté en rango válido
effectiveSpeed = Math.Max(0.0, Math.Min(1.0, effectiveSpeed));
// Crear bomba con parámetros actuales
var pump = new PumpHQ(
h0: PumpHead,
q0: MaxFlow,
speedRel: IsRunning ? SpeedRatio : 0.0, // Si no está funcionando, velocidad = 0
speedRel: effectiveSpeed,
direction: PumpDirection
);
@ -259,6 +298,8 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
);
elements.Add(pumpElement);
Debug.WriteLine($"Bomba {Nombre}: Creando elemento hidráulico - H0={PumpHead}m, Q0={MaxFlow}m³/s, Velocidad={effectiveSpeed:F2}, Dirección={PumpDirection}");
}
return elements;
@ -288,16 +329,25 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
if (flows.ContainsKey(pumpBranchName))
{
CurrentFlow = flows[pumpBranchName];
Debug.WriteLine($"Bomba {Nombre}: Aplicando caudal={CurrentFlow:F6} m³/s ({CurrentFlowLMin:F2} L/min)");
}
if (pressures.ContainsKey(inletNodeName))
{
CurrentPressure = pressures[inletNodeName];
Debug.WriteLine($"Bomba {Nombre}: Presión entrada={CurrentPressure:F2} Pa ({CurrentPressureBar:F2} bar)");
}
else if (pressures.ContainsKey(outletNodeName))
{
CurrentPressure = pressures[outletNodeName];
Debug.WriteLine($"Bomba {Nombre}: Presión salida={CurrentPressure:F2} Pa ({CurrentPressureBar:F2} bar)");
}
// Disparar PropertyChanged para actualizar el UI
OnPropertyChanged(nameof(CurrentFlow));
OnPropertyChanged(nameof(CurrentFlowLMin));
OnPropertyChanged(nameof(CurrentPressure));
OnPropertyChanged(nameof(CurrentPressureBar));
}
#endregion
@ -345,13 +395,6 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
}
#endregion
#region Static Interface Implementation
public static string NombreClase() => "Bomba Hidráulica";
public static string NombreCategoria() => "Componentes Hidráulicos";
#endregion
}
/// <summary>

View File

@ -3,77 +3,39 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:CtrEditor.ObjetosSim"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim.HydraulicComponents"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:osHydPump Ancho="0.15" Alto="0.10"/>
<vm:osHydPump ImageSource_oculta="/imagenes/pump_run.png" />
</UserControl.DataContext>
<Canvas RenderTransformOrigin="0,0">
<Canvas.RenderTransform>
<Grid RenderTransformOrigin="0,0">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform />
<SkewTransform />
<RotateTransform Angle="{Binding Angulo}" />
<TranslateTransform />
<RotateTransform Angle="{Binding Angulo}"/>
</TransformGroup>
</Canvas.RenderTransform>
</Grid.RenderTransform>
<!-- Fondo de la bomba -->
<Ellipse x:Name="PumpBackground"
Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Fill="{Binding ColorButton_oculto}"
Stroke="DarkSlateGray"
StrokeThickness="2"/>
<!-- Indicador de dirección con tamaño proporcional -->
<Polygon x:Name="DirectionArrow"
Fill="DarkBlue"
Points="0.5,0.3 0.8,0.5 0.5,0.7"
RenderTransformOrigin="0.5,0.5">
<Polygon.RenderTransform>
<ScaleTransform ScaleX="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
ScaleY="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"/>
</Polygon.RenderTransform>
</Polygon>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Texto de identificación con Viewbox para escalado -->
<Viewbox Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Stretch="Uniform">
<Label Content="{Binding Nombre}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontWeight="Bold"
FontSize="18"
Opacity="0.9"
Foreground="White"/>
</Viewbox>
<!-- Imagen de la bomba -->
<Image Grid.Row="0" Source="{Binding ImageSource_oculta}"
Width="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Tamano, Converter={StaticResource MeterToPixelConverter}}"
Stretch="Uniform"/>
<!-- Indicador de estado (LED) con posición proporcional -->
<Ellipse x:Name="StatusLED"
Width="8"
Height="8"
Fill="{Binding IsRunning, Converter={StaticResource BoolToColorConverter}}"
Canvas.Right="5"
Canvas.Top="5"/>
<!-- Panel de información con presión y caudal -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="{Binding CurrentPressureBar, StringFormat='{}{0:F1} bar'}"
Foreground="White" Background="Blue" Padding="2" Margin="1" FontSize="8"/>
<TextBlock Text="{Binding CurrentFlowLMin, StringFormat='{}{0:F1} L/min'}"
Foreground="White" Background="Green" Padding="2" Margin="1" FontSize="8"/>
</StackPanel>
</Grid>
<!-- Conexiones de entrada y salida con posiciones proporcionales -->
<Rectangle x:Name="InletConnection"
Width="10"
Height="4"
Fill="Green"
Canvas.Left="-5"
Canvas.Top="{Binding Alto, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=0.5}"/>
<Rectangle x:Name="OutletConnection"
Width="10"
Height="4"
Fill="Red"
Canvas.Right="-5"
Canvas.Top="{Binding Alto, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=0.5}"/>
</Canvas>
</UserControl>

View File

@ -3,7 +3,6 @@ using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using CtrEditor.ObjetosSim;
using CtrEditor.FuncionesBase;
@ -17,7 +16,6 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
public osBase? Datos { get; set; }
public int zIndex_fromFrames { get; set; } = 0;
private Storyboard? _rotationAnimation;
private bool _isHighlighted = false;
public ucHydPump()
@ -25,13 +23,11 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
InitializeComponent();
this.Loaded += OnLoaded;
this.Unloaded += OnUnloaded;
DataContextChanged += OnDataContextChanged;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Datos?.ucLoaded();
UpdateVisualState();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
@ -39,155 +35,12 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
Datos?.ucUnLoaded();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext is osHydPump pump)
{
Datos = pump;
pump.PropertyChanged += OnPumpPropertyChanged;
UpdateVisualState();
}
}
private void OnPumpPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(osHydPump.IsRunning) ||
e.PropertyName == nameof(osHydPump.SpeedRatio) ||
e.PropertyName == nameof(osHydPump.PumpDirection) ||
e.PropertyName == nameof(osHydPump.CurrentFlow) ||
e.PropertyName == nameof(osHydPump.CurrentPressure))
{
UpdateVisualState();
}
}
/// <summary>
/// Actualiza el estado visual de la bomba
/// </summary>
private void UpdateVisualState()
{
if (Datos is not osHydPump pump) return;
try
{
Dispatcher.BeginInvoke(() =>
{
UpdateStatusLED(pump);
UpdateRotationAnimation(pump);
UpdateDirectionArrow(pump);
UpdateStatusInfo(pump);
});
}
catch (Exception ex)
{
Debug.WriteLine($"Error updating pump visual state: {ex.Message}");
}
}
/// <summary>
/// Actualiza el LED de estado
/// </summary>
private void UpdateStatusLED(osHydPump pump)
{
if (pump.IsRunning && pump.SpeedRatio > 0)
{
StatusLED.Fill = new SolidColorBrush(Colors.LimeGreen);
}
else
{
StatusLED.Fill = new SolidColorBrush(Colors.Red);
}
}
/// <summary>
/// Actualiza la animación de rotación
/// </summary>
private void UpdateRotationAnimation(osHydPump pump)
{
_rotationAnimation?.Stop();
if (pump.IsRunning && pump.SpeedRatio > 0)
{
// Crear animación de rotación
var rotateTransform = new RotateTransform();
PumpBackground.RenderTransform = rotateTransform;
PumpBackground.RenderTransformOrigin = new Point(0.5, 0.5);
var animation = new DoubleAnimation
{
From = 0,
To = 360,
Duration = TimeSpan.FromSeconds(2.0 / pump.SpeedRatio), // Más rápido con mayor velocidad
RepeatBehavior = RepeatBehavior.Forever
};
_rotationAnimation = new Storyboard();
Storyboard.SetTarget(animation, rotateTransform);
Storyboard.SetTargetProperty(animation, new PropertyPath(RotateTransform.AngleProperty));
_rotationAnimation.Children.Add(animation);
_rotationAnimation.Begin();
}
else
{
PumpBackground.RenderTransform = null;
}
}
/// <summary>
/// Actualiza la flecha de dirección
/// </summary>
private void UpdateDirectionArrow(osHydPump pump)
{
if (pump.PumpDirection == -1)
{
// Cambiar el color de la flecha para indicar dirección inversa
DirectionArrow.Fill = new SolidColorBrush(Colors.Orange);
}
else
{
DirectionArrow.Fill = new SolidColorBrush(Colors.DarkBlue);
}
}
/// <summary>
/// Actualiza la información de estado
/// </summary>
private void UpdateStatusInfo(osHydPump pump)
{
// La información de estado se mostrará a través de la etiqueta principal
// El StatusLED ya está configurado para mostrar el estado IsRunning
}
#region IDataContainer Implementation
public void Highlight(bool state)
{
_isHighlighted = state;
if (state)
{
PumpBackground.Stroke = new SolidColorBrush(Colors.Yellow);
PumpBackground.StrokeThickness = 3;
}
else
{
PumpBackground.Stroke = new SolidColorBrush(Colors.DarkSlateGray);
PumpBackground.StrokeThickness = 2;
}
}
#endregion
protected override void OnMouseEnter(System.Windows.Input.MouseEventArgs e)
{
base.OnMouseEnter(e);
// No hay elementos adicionales para mostrar en la implementación actual
}
protected override void OnMouseLeave(System.Windows.Input.MouseEventArgs e)
{
base.OnMouseLeave(e);
// No hay elementos adicionales para ocultar en la implementación actual
// Aquí se podría agregar lógica de resaltado si fuera necesario
}
public ZIndexEnum ZIndex_Base()
@ -195,9 +48,6 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents
return ZIndexEnum.Estaticos;
}
~ucHydPump()
{
_rotationAnimation?.Stop();
}
#endregion
}
}

View File

@ -1919,9 +1919,30 @@ namespace CtrEditor.ObjetosSim
items.Add("");
foreach (var obj in objetosSimulables)
{
if (obj.GetType() == typeof(T))
try
{
items.Add(obj.Nombre);
// Verificar si el tipo T es una interfaz
if (typeof(T).IsInterface)
{
// Para interfaces, verificar si el objeto implementa la interfaz
if (typeof(T).IsAssignableFrom(obj.GetType()))
{
items.Add(obj.Nombre);
}
}
else
{
// Para clases concretas, usar comparación directa de tipos
if (obj.GetType() == typeof(T))
{
items.Add(obj.Nombre);
}
}
}
catch (Exception ex)
{
// Log the exception but continue processing other objects
System.Diagnostics.Debug.WriteLine($"Error checking type compatibility for {obj?.GetType()?.Name}: {ex.Message}");
}
}

BIN
imagenes/pump_run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
imagenes/pump_stop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

32
test_osBaseItemsSource.cs Normal file
View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using CtrEditor.ObjetosSim;
using CtrEditor.ObjetosSim.HydraulicComponents;
using CtrEditor.HydraulicSimulator;
// Este archivo es solo para verificar que la corrección funciona
// Se puede eliminar después de confirmar que todo funciona bien
namespace CtrEditor.Test
{
class TestItemsSource
{
static void TestTypeAssignability()
{
// Crear instancias de objetos hidráulicos
var pipe = new osHydPipe();
var tank = new osHydDischargeTank();
var pump = new osHydPump();
// Probar la corrección - debería ser true para todos
Console.WriteLine($"osHydPipe implements IHydraulicComponent: {typeof(IHydraulicComponent).IsAssignableFrom(pipe.GetType())}");
Console.WriteLine($"osHydDischargeTank implements IHydraulicComponent: {typeof(IHydraulicComponent).IsAssignableFrom(tank.GetType())}");
Console.WriteLine($"osHydPump implements IHydraulicComponent: {typeof(IHydraulicComponent).IsAssignableFrom(pump.GetType())}");
// La comparación anterior que fallaba
Console.WriteLine($"osHydPipe.GetType() == typeof(IHydraulicComponent): {pipe.GetType() == typeof(IHydraulicComponent)}");
Console.WriteLine($"osHydDischargeTank.GetType() == typeof(IHydraulicComponent): {tank.GetType() == typeof(IHydraulicComponent)}");
Console.WriteLine($"osHydPump.GetType() == typeof(IHydraulicComponent): {pump.GetType() == typeof(IHydraulicComponent)}");
}
}
}