diff --git a/App.xaml b/App.xaml index 7625f43..2da8429 100644 --- a/App.xaml +++ b/App.xaml @@ -6,6 +6,7 @@ + @@ -17,6 +18,7 @@ + diff --git a/Documentation/MemoriadeEvolucion.md b/Documentation/MemoriadeEvolucion.md index a47d820..b446de8 100644 --- a/Documentation/MemoriadeEvolucion.md +++ b/Documentation/MemoriadeEvolucion.md @@ -118,3 +118,158 @@ if (velocity.Linear.Z > maxUpwardVelocity) { - **Solución integral**: Resuelve tanto el problema de elevación como el de contactos múltiples simultáneos - **Código más simple**: Elimina la complejidad del sistema de tracking de densidad de contactos +# Sistema Hidráulico Integrado con HydraulicSimulationManager + +## Arquitectura del Sistema Hidráulico + +Se ha implementado un sistema de simulación hidráulica completo que funciona en paralelo con la simulación física BEPU. El sistema está basado en el `HydraulicSimulationManager` que actúa como puente entre los objetos gráficos derivados de `osBase` y el motor de simulación hidráulica. + +### Componentes Principales + +#### HydraulicSimulationManager +- **Función**: Gestiona el ciclo de vida y la comunicación entre objetos hidráulicos y el solver +- **Integración**: Se ejecuta en paralelo con BEPU en el timer loop principal de MainViewModel +- **Responsabilidades**: + - Registro automático de objetos que implementan `IHydraulicComponent` + - Construcción dinámica de la red hidráulica + - Aplicación de resultados a los objetos tras cada iteración del solver + +#### Interfaces Hidráulicas +- **IHydraulicComponent**: Interfaz base para todos los objetos hidráulicos +- **IHydraulicPump**: Para bombas con curvas H-Q +- **IHydraulicPipe**: Para tuberías con pérdidas por fricción +- **IHydraulicTank**: Para tanques con cálculo de nivel dinámico +- **IHydraulicValve**: Para válvulas con coeficiente Kv + +## Mejoras Implementadas en Objetos Hidráulicos + +### 1. Sistema de Conexiones Mejorado en osHydPipe + +**Problema Original**: El sistema de conexiones era limitado y no seguía el patrón establecido en otros objetos del sistema. + +**Solución Implementada**: +```csharp +// Antes - Sistema limitado +[ObservableProperty] string id_InletComponent = ""; +[ObservableProperty] string id_OutletComponent = ""; + +// Después - Patrón consistente +[ItemsSource(typeof(osBaseItemsSource))] +[ObservableProperty] string id_ComponenteA = ""; + +[ItemsSource(typeof(osBaseItemsSource))] +[ObservableProperty] string id_ComponenteB = ""; +``` + +**Beneficios**: +- **Filtrado automático**: Solo muestra objetos que implementan `IHydraulicComponent` en el PropertyGrid +- **Consistencia**: Sigue el mismo patrón usado en `ucTransporteGuias` con `Motor` +- **Mantenimiento automático**: PropertyChangedHandlers mantienen sincronizados los nombres +- **Validación**: Solo crea elementos hidráulicos cuando ambos componentes están conectados + +### 2. Lógica de Nodos Corregida + +**Problema Original**: Las tuberías intentaban crear sus propios nodos, generando duplicación y conflictos. + +**Solución Implementada**: +```csharp +// Las tuberías no crean nodos propios +public List GetHydraulicNodes() +{ + // Los nodos son creados por los componentes conectados (tanques, bombas, etc.) + return new List(); +} + +// Solo conectan nodos existentes +public List GetHydraulicElements() +{ + if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB)) + { + var pipeElement = new Pipe(Length, Diameter, Roughness); + // Conecta directamente usando nombres de componentes como nodos + return new List { + new HydraulicElementDefinition($"{Nombre}_Pipe", Id_ComponenteA, Id_ComponenteB, pipeElement) + }; + } + return new List(); +} +``` + +### 3. Integración Completa con ItemsSource + +**En osHydDischargeTank**: +```csharp +[Category("🔗 Conexiones")] +[Name("Componente Entrada")] +[ItemsSource(typeof(osBaseItemsSource))] +[ObservableProperty] string id_InletComponent = ""; +``` + +## Flujo de Simulación Hidráulica + +### 1. Registro Automático +- Los objetos que implementan `IHydraulicComponent` se registran automáticamente en `HydraulicSimulationManager` +- El sistema detecta cuando hay cambios y marca la red para reconstrucción (`_networkNeedsRebuild = true`) + +### 2. Construcción de Red +- **BuildNodesFromObjects()**: Crea nodos basándose en las definiciones de cada objeto +- **BuildBranchesFromObjects()**: Crea elementos (tuberías, bombas, válvulas) que conectan los nodos +- La red resultante es compatible con el solver `HydraulicNetwork.Solve()` + +### 3. Resolución y Aplicación +- **UpdateObjectProperties()**: Actualiza propiedades antes del solver (apertura de válvulas, velocidad de bombas, etc.) +- **Network.Solve()**: Resuelve el sistema de ecuaciones usando el algoritmo iterativo +- **ApplyResultsToObjects()**: Aplica caudales y presiones calculados a cada objeto + +## Patrón de Conexión Establecido + +El sistema establecido para conexiones sigue este patrón consistente: + +```csharp +// 1. Propiedad string con ItemsSource filtrado +[ItemsSource(typeof(osBaseItemsSource))] +[ObservableProperty] string id_ComponenteConectado = ""; + +// 2. Referencia privada al objeto real +[JsonIgnore] private IInterfaceEspecifica ComponenteConectado = null; + +// 3. PropertyChangedHandler para mantener sincronización +[JsonIgnore] private PropertyChangedEventHandler componentePropertyChangedHandler; + +// 4. Método OnChanged que maneja la conexión +partial void OnId_ComponenteConectadoChanged(string value) +{ + // Desconectar anterior si existe + if (ComponenteConectado != null && componentePropertyChangedHandler != null) + ((INotifyPropertyChanged)ComponenteConectado).PropertyChanged -= componentePropertyChangedHandler; + + // Buscar y conectar nuevo componente + if (_mainViewModel != null && !string.IsNullOrEmpty(value)) + { + ComponenteConectado = (IInterfaceEspecifica)_mainViewModel.ObjetosSimulables + .FirstOrDefault(s => s is IInterfaceEspecifica comp && + ((osBase)comp).Nombre == value); + + // Establecer handler para sincronización de nombres + if (ComponenteConectado != null) + { + componentePropertyChangedHandler = (sender, e) => + { + if (e.PropertyName == nameof(osBase.Nombre)) + Id_ComponenteConectado = ((osBase)sender).Nombre; + }; + ((INotifyPropertyChanged)ComponenteConectado).PropertyChanged += componentePropertyChangedHandler; + } + } +} +``` + +## Beneficios del Sistema Integrado + +- **Doble simulación**: Simulación física (BEPU) y hidráulica funcionan en paralelo sin interferencia +- **Interfaces unificadas**: Todos los objetos hidráulicos implementan interfaces consistentes +- **Red dinámica**: La red hidráulica se reconstruye automáticamente cuando cambian las conexiones +- **Resultados bidireccionales**: Los resultados hidráulicos pueden influir en la simulación física y viceversa +- **Patrón consistente**: Todas las conexiones siguen el mismo patrón establecido +- **Mantenimiento automático**: Los nombres y referencias se mantienen sincronizados automáticamente + diff --git a/HydraulicSimulator/Examples/SimpleHydraulicSystemExample.cs b/HydraulicSimulator/Examples/SimpleHydraulicSystemExample.cs new file mode 100644 index 0000000..ca17bb1 --- /dev/null +++ b/HydraulicSimulator/Examples/SimpleHydraulicSystemExample.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using HydraulicSimulator.Models; +using CtrEditor.HydraulicSimulator; +using CtrEditor.ObjetosSim.HydraulicComponents; + +namespace CtrEditor.HydraulicSimulator.Examples +{ + /// + /// Ejemplo de uso de los componentes hidráulicos según la documentación + /// Muestra cómo crear una red simple: Bomba -> Tubería -> Tanque de Descarga + /// + public class SimpleHydraulicSystemExample + { + /// + /// Crea un sistema hidráulico simple con bomba, tubería y tanque + /// + public static void RunExample() + { + Console.WriteLine("🚀 Iniciando ejemplo de sistema hidráulico simple"); + Console.WriteLine("Sistema: Bomba -> Tubería -> Tanque de Descarga"); + Console.WriteLine(); + + // 1. Crear la red hidráulica + var network = new HydraulicNetwork(); + + // 2. Agregar nodos según la documentación + network.AddNode("SUMINISTRO", 0); // Tanque de suministro a presión atmosférica + network.AddNode("BOMBA_OUT"); // Salida de bomba (presión calculada) + network.AddNode("TANQUE_DESCARGA", 0); // Tanque de descarga libre + + // 3. Crear elementos hidráulicos + var pump = new PumpHQ( + h0: 80, // Cabeza a caudal cero: 80 metros + q0: 0.01, // Caudal a cabeza cero: 0.01 m³/s (36 m³/h) + speedRel: 1.0, // Velocidad nominal + direction: 1 // Dirección normal + ); + + var pipe = new Pipe( + length: 50, // 50 metros de longitud + diameter: 0.08, // 80mm de diámetro + roughness: 0.0015 // Rugosidad del acero comercial + ); + + // 4. Conectar elementos en ramas según la documentación + network.AddBranch("SUMINISTRO", "BOMBA_OUT", + new List { pump }, "Bomba_Principal"); + + network.AddBranch("BOMBA_OUT", "TANQUE_DESCARGA", + new List { pipe }, "Tuberia_Descarga"); + + // 5. Resolver la red + Console.WriteLine("⚙️ Resolviendo red hidráulica..."); + var result = network.Solve( + maxIterations: 200, + tolerance: 1e-4, + verbose: true + ); + + // 6. Mostrar resultados + if (result.Converged) + { + Console.WriteLine("✅ Simulación exitosa!"); + Console.WriteLine($"Convergió en {result.Iterations} iteraciones"); + Console.WriteLine(); + + ShowResults(result); + ShowComponentAnalysis(result, pump, pipe); + } + else + { + Console.WriteLine($"❌ No convergió después de {result.Iterations} iteraciones"); + Console.WriteLine($"Error final: {result.Residual:E6}"); + } + } + + private static void ShowResults(SolutionResult result) + { + Console.WriteLine("📊 RESULTADOS DE LA SIMULACIÓN:"); + Console.WriteLine("═══════════════════════════════"); + + // Flujos + Console.WriteLine("🌊 Flujos:"); + foreach (var flow in result.Flows) + { + double flowM3h = flow.Value * 3600; // Convertir a m³/h + double flowLmin = flow.Value * 60000; // Convertir a L/min + Console.WriteLine($" {flow.Key}:"); + Console.WriteLine($" {flow.Value:F6} m³/s = {flowM3h:F2} m³/h = {flowLmin:F1} L/min"); + } + + Console.WriteLine(); + + // Presiones + Console.WriteLine("📈 Presiones:"); + foreach (var pressure in result.Pressures) + { + double pressureBar = pressure.Value / 100000.0; // Convertir a bar + double pressureKPa = pressure.Value / 1000.0; // Convertir a kPa + Console.WriteLine($" {pressure.Key}:"); + Console.WriteLine($" {pressure.Value:F0} Pa = {pressureKPa:F1} kPa = {pressureBar:F3} bar"); + } + } + + private static void ShowComponentAnalysis(SolutionResult result, PumpHQ pump, Pipe pipe) + { + Console.WriteLine(); + Console.WriteLine("🔧 ANÁLISIS DE COMPONENTES:"); + Console.WriteLine("═══════════════════════════"); + + // Análisis de la bomba + if (result.Flows.TryGetValue("Bomba_Principal", out double pumpFlow)) + { + double pumpFlowM3h = pumpFlow * 3600; + double pumpHead = pump.Dp(pumpFlow, Fluid.Water20C) / (1000 * 9.81); // Convertir Pa a metros + double pumpPower = pumpFlow * pump.Dp(pumpFlow, Fluid.Water20C) / 1000; // Potencia hidráulica en kW + + Console.WriteLine("💨 Bomba:"); + Console.WriteLine($" Caudal: {pumpFlowM3h:F1} m³/h"); + Console.WriteLine($" Cabeza: {pumpHead:F1} m"); + Console.WriteLine($" Potencia hidráulica: {pumpPower:F2} kW"); + } + + // Análisis de la tubería + if (result.Flows.TryGetValue("Tuberia_Descarga", out double pipeFlow)) + { + double pipePressureDrop = pipe.Dp(pipeFlow, Fluid.Water20C); + double pipeHeadLoss = pipePressureDrop / (1000 * 9.81); // Convertir Pa a metros + double velocity = Math.Abs(pipeFlow) / (Math.PI * (pipe.D * pipe.D) / 4.0); + + Console.WriteLine(); + Console.WriteLine("🚰 Tubería:"); + Console.WriteLine($" Caudal: {pipeFlow * 3600:F1} m³/h"); + Console.WriteLine($" Velocidad: {velocity:F2} m/s"); + Console.WriteLine($" Pérdida de carga: {pipeHeadLoss:F2} m"); + Console.WriteLine($" Pérdida de presión: {pipePressureDrop / 1000:F1} kPa"); + } + } + + /// + /// Ejemplo de cómo los objetos gráficos osHyd* se integrarían en la simulación + /// + public static void ShowGraphicObjectsIntegration() + { + Console.WriteLine(); + Console.WriteLine("🎨 INTEGRACIÓN CON OBJETOS GRÁFICOS:"); + Console.WriteLine("═══════════════════════════════════"); + Console.WriteLine(); + Console.WriteLine("Los objetos creados (osHydPump, osHydPipe, osHydDischargeTank)"); + Console.WriteLine("se integran automáticamente en el sistema de la siguiente manera:"); + Console.WriteLine(); + Console.WriteLine("1. 📋 Registro automático:"); + Console.WriteLine(" - Se registran por reflexión usando NombreCategoria() = \"Hidráulicos\""); + Console.WriteLine(" - Aparecen en la categoría \"Hidráulicos\" del editor"); + Console.WriteLine(); + Console.WriteLine("2. 🔗 Sistema de conexiones:"); + Console.WriteLine(" - Usan ItemsSource para filtrar componentes"); + Console.WriteLine(" - Similar al sistema Motor/Transporte existente"); + Console.WriteLine(" - Conexiones: Id_InletComponent, Id_OutletComponent"); + Console.WriteLine(); + Console.WriteLine("3. 🚫 No participan en Bepu:"); + Console.WriteLine(" - Override Start() sin crear geometrías Bepu"); + Console.WriteLine(" - Solo participan en el simulador hidráulico"); + Console.WriteLine(); + Console.WriteLine("4. 📊 Integración con HydraulicSimulationManager:"); + Console.WriteLine(" - Implementan IHydraulicComponent para GetHydraulicNodes()"); + Console.WriteLine(" - Implementan GetHydraulicElements() para el solver"); + Console.WriteLine(" - Reciben resultados via ApplyHydraulicResults()"); + Console.WriteLine(); + Console.WriteLine("5. 🎯 Ejemplo de configuración:"); + Console.WriteLine(" osHydPump (Nombre=\"Bomba_01\", H0=80m, Q0=36m³/h)"); + Console.WriteLine(" ↓ (Id_OutletComponent=\"Tuberia_01\")"); + Console.WriteLine(" osHydPipe (Nombre=\"Tuberia_01\", L=50m, D=80mm)"); + Console.WriteLine(" ↓ (Id_OutletComponent=\"Tanque_01\")"); + Console.WriteLine(" osHydDischargeTank (Nombre=\"Tanque_01\", Area=1m²)"); + } + } +} diff --git a/HydraulicSimulator/Tests/QuickHydraulicTest.cs b/HydraulicSimulator/Tests/QuickHydraulicTest.cs new file mode 100644 index 0000000..9e58603 --- /dev/null +++ b/HydraulicSimulator/Tests/QuickHydraulicTest.cs @@ -0,0 +1,46 @@ +using System; +using CtrEditor.HydraulicSimulator.Examples; + +namespace CtrEditor.HydraulicSimulator.Tests +{ + /// + /// Clase para probar rápidamente los componentes hidráulicos + /// Ejecutar desde el método Main o desde tests unitarios + /// + public static class QuickHydraulicTest + { + /// + /// Ejecuta una prueba rápida del sistema hidráulico + /// + public static void RunQuickTest() + { + try + { + Console.WriteLine("🧪 PRUEBA RÁPIDA DEL SISTEMA HIDRÁULICO"); + Console.WriteLine("═══════════════════════════════════════"); + Console.WriteLine(); + + // Ejecutar ejemplo simple + SimpleHydraulicSystemExample.RunExample(); + + // Mostrar información de integración + SimpleHydraulicSystemExample.ShowGraphicObjectsIntegration(); + + Console.WriteLine(); + Console.WriteLine("✅ Prueba completada exitosamente!"); + Console.WriteLine(); + Console.WriteLine("📝 PRÓXIMOS PASOS:"); + Console.WriteLine("1. Agregar los componentes al editor visual"); + Console.WriteLine("2. Configurar conexiones entre componentes"); + Console.WriteLine("3. Integrar con HydraulicSimulationManager"); + Console.WriteLine("4. Probar simulación en tiempo real"); + + } + catch (Exception ex) + { + Console.WriteLine($"❌ Error en la prueba: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + } + } +} diff --git a/MainWindow.xaml b/MainWindow.xaml index c0313ba..88ceda8 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -134,7 +134,7 @@ - + diff --git a/ObjetosSim/HydraulicComponents/osHydDischargeTank.cs b/ObjetosSim/HydraulicComponents/osHydDischargeTank.cs new file mode 100644 index 0000000..75b7e4f --- /dev/null +++ b/ObjetosSim/HydraulicComponents/osHydDischargeTank.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Windows.Media; +using CtrEditor.HydraulicSimulator; +using CtrEditor.ObjetosSim; +using CtrEditor.FuncionesBase; +using HydraulicSimulator.Models; +using Newtonsoft.Json; +using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; +using CommunityToolkit.Mvvm.ComponentModel; +using LibS7Adv; + +namespace CtrEditor.ObjetosSim.HydraulicComponents +{ + /// + /// Tanque de descarga hidráulico con cálculo de nivel dinámico + /// + public partial class osHydDischargeTank : osBase, IHydraulicTank, IosBase + { + #region Properties + + private double _area = 1.0; // m² + private double _currentVolume = 0.5; // m³ + private double _maxVolume = 2.0; // m³ + private double _minVolume = 0.0; // m³ + private double _currentHeight = 0.0; + private double _currentPressure = 0.0; + + // Propiedades visuales + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Ancho visual del tanque en metros")] + [property: Name("Ancho")] + private float ancho = 0.5f; + + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Alto visual del tanque en metros")] + [property: Name("Alto")] + private float alto = 0.8f; + + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Color visual del tanque")] + [property: Name("Color")] + private Brush colorButton_oculto = Brushes.LightBlue; + + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Ángulo de rotación del tanque en grados")] + [property: Name("Ángulo")] + private float angulo = 0f; + + [Category("🔧 Tanque Hidráulico")] + [Description("Área de la base del tanque en m² (alias para compatibilidad)")] + [Name("Área Base (m²)")] + public double Area + { + get => _area; + set => SetProperty(ref _area, Math.Max(0.1, value)); + } + + [Category("🔧 Tanque Hidráulico")] + [Description("Presión del tanque en Pa")] + [Name("Presión Tanque (Pa)")] + public double TankPressure + { + get => _currentPressure; + set => SetProperty(ref _currentPressure, value); + } + + [Category("🔧 Tanque Hidráulico")] + [Description("Nivel actual del tanque en metros")] + [Name("Nivel (m)")] + public double Level + { + get => _currentHeight; + set + { + SetProperty(ref _currentHeight, value); + CurrentVolume = value * Area; + } + } + + [Category("🔧 Tanque Hidráulico")] + [Description("Área de la sección transversal del tanque en m²")] + [Name("Área Sección (m²)")] + public double CrossSectionalArea + { + get => _area; + set => SetProperty(ref _area, Math.Max(0.1, value)); + } + + [Category("🔧 Tanque Hidráulico")] + [Description("Indica si el tanque tiene presión fija")] + [Name("Presión Fija")] + [ReadOnly(true)] + public bool IsFixedPressure => false; // Los tanques de descarga tienen presión calculada + + [Category("🔧 Tanque Hidráulico")] + [Description("Volumen actual del tanque en m³")] + [Name("Volumen Actual (m³)")] + public double CurrentVolume + { + get => _currentVolume; + set => SetProperty(ref _currentVolume, Math.Max(MinVolume, Math.Min(MaxVolume, value))); + } + + [Category("🔧 Tanque Hidráulico")] + [Description("Volumen máximo del tanque en m³")] + [Name("Volumen Máximo (m³)")] + public double MaxVolume + { + get => _maxVolume; + set => SetProperty(ref _maxVolume, Math.Max(CurrentVolume, value)); + } + + [Category("🔧 Tanque Hidráulico")] + [Description("Volumen mínimo del tanque en m³")] + [Name("Volumen Mínimo (m³)")] + public double MinVolume + { + get => _minVolume; + set => SetProperty(ref _minVolume, Math.Max(0.0, Math.Min(CurrentVolume, value))); + } + + [ObservableProperty] + [property: Category("🔗 Conexiones")] + [property: Description("Componente conectado en la entrada")] + [property: Name("Componente Entrada")] + [property: ItemsSource(typeof(osBaseItemsSource))] + private string id_InletComponent = ""; + + [Category("📊 Estado")] + [Description("Altura actual del líquido en metros")] + [Name("Altura Actual (m)")] + [ReadOnly(true)] + public double CurrentHeight + { + get => _currentHeight; + set => SetProperty(ref _currentHeight, value); + } + + [Category("📊 Estado")] + [Description("Presión hidrostática en el fondo del tanque en Pa")] + [Name("Presión Fondo (Pa)")] + [ReadOnly(true)] + public double CurrentPressure + { + get => _currentPressure; + set => SetProperty(ref _currentPressure, value); + } + + [Category("📊 Estado")] + [Description("Porcentaje de llenado del tanque")] + [Name("Nivel (%)")] + [ReadOnly(true)] + public double FillPercentage => MaxVolume > 0 ? (CurrentVolume / MaxVolume) * 100.0 : 0.0; + + #endregion + + #region Component References + + [JsonIgnore] + private IHydraulicComponent InletComponent = null; + + [JsonIgnore] + private PropertyChangedEventHandler inletPropertyChangedHandler; + + [JsonIgnore] + public Action ActualizarTamaño { get; set; } + + #endregion + + #region IHydraulicComponent Implementation + + public bool HasHydraulicComponents => true; + + public List GetHydraulicNodes() + { + var nodes = new List(); + + // El tanque de descarga es un nodo con presión libre (calculada) + nodes.Add(new HydraulicNodeDefinition(Nombre, false, null, "Tanque de descarga")); + + return nodes; + } + + public List GetHydraulicElements() + { + // Los tanques no son elementos, son nodos + return new List(); + } + + public void UpdateHydraulicProperties() + { + // Actualizar presión basada en el nivel actual + UpdateHeightAndPressure(); + } + + public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) + { + // Actualizar presión del tanque + if (pressures.TryGetValue(Nombre, out double pressure)) + { + TankPressure = pressure; + // Calcular nivel basado en la presión hidrostática + Level = pressure / (1000.0 * 9.81); // Asumiendo densidad del agua + } + + // Calcular flujo neto hacia el tanque + double netFlow = 0.0; + foreach (var flow in flows) + { + if (flow.Key.EndsWith($"->{Nombre}")) + netFlow += flow.Value; // Flujo entrante + else if (flow.Key.StartsWith($"{Nombre}->")) + netFlow -= flow.Value; // Flujo saliente + } + + // Aquí podrías actualizar el volumen en función del tiempo + // Esto se haría en el update loop principal de la simulación + } + + #endregion + + #region IHydraulicPressureReceiver Implementation + + public void SetPressure(double pressure) + { + TankPressure = pressure; + // Actualizar nivel basado en presión hidrostática + Level = pressure / (1000.0 * 9.81); + } + + public double GetPressure() + { + return TankPressure; + } + + #endregion + + #region Connection Management + + partial void OnId_InletComponentChanged(string value) + { + if (InletComponent != null && inletPropertyChangedHandler != null) + ((INotifyPropertyChanged)InletComponent).PropertyChanged -= inletPropertyChangedHandler; + + if (_mainViewModel != null && !string.IsNullOrEmpty(value)) + { + InletComponent = (IHydraulicComponent)_mainViewModel.ObjetosSimulables + .FirstOrDefault(s => s is IHydraulicComponent comp && + ((osBase)comp).Nombre == value); + + if (InletComponent != null) + { + inletPropertyChangedHandler = (sender, e) => + { + if (e.PropertyName == nameof(osBase.Nombre)) + { + Id_InletComponent = ((osBase)sender).Nombre; + } + }; + ((INotifyPropertyChanged)InletComponent).PropertyChanged += inletPropertyChangedHandler; + } + } + } + + #endregion + + #region Volume and Level Calculations + + private void UpdateHeightAndPressure() + { + Level = Area > 0 ? CurrentVolume / Area : 0.0; + // Presión hidrostática en el fondo del tanque (densidad del agua = 1000 kg/m³) + TankPressure = 1000.0 * 9.81 * Level; + } + + /// + /// Actualiza el volumen del tanque basado en flujos netos según la documentación + /// + /// Diccionario de flujos de la simulación + /// Intervalo de tiempo en segundos + public void UpdateVolume(Dictionary flows, double deltaTime) + { + double netFlow = 0.0; + + // Sumar flujos entrantes, restar salientes + foreach (var flow in flows) + { + if (flow.Key.EndsWith($"->{Nombre}")) + netFlow += flow.Value; // Entrante + else if (flow.Key.StartsWith($"{Nombre}->")) + netFlow -= flow.Value; // Saliente + } + + // Actualizar volumen + CurrentVolume += netFlow * deltaTime; + CurrentVolume = Math.Max(MinVolume, Math.Min(MaxVolume, CurrentVolume)); + + // Actualizar altura y presión + UpdateHeightAndPressure(); + } + + /// + /// Calcula la presión en el fondo del tanque según la documentación + /// + /// Densidad del fluido (kg/m³) + /// Presión hidrostática en Pa + public double BottomPressure(double density = 1000.0) + { + return density * 9.81 * Level; // ρgh + } + + #endregion + + #region osBase Implementation + + public static string NombreCategoria() => "Componentes Hidráulicos"; + + public static string NombreClase() => "Tanque de Descarga"; + + private string nombre = NombreClase(); + + [Category("Identificación")] + [Description("Nombre identificativo del objeto")] + [Name("Nombre")] + public override string Nombre + { + get => nombre; + set => SetProperty(ref nombre, value); + } + + public override void AltoChanged(float value) + { + ActualizarGeometrias(); + } + + public override void AnchoChanged(float value) + { + ActualizarGeometrias(); + } + + public void Start() + { + // Los tanques no participan en la simulación física Bepu + // Solo en el sistema hidráulico + UpdateHeightAndPressure(); + } + + public override void UpdateGeometryStart() + { + ActualizarGeometrias(); + } + + public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds) + { + // Los tanques pueden tener sensores de nivel + // Implementar aquí si es necesario + } + + private void ActualizarGeometrias() + { + try + { + // Actualizar geometría visual + if (this.ActualizarTamaño != null) + this.ActualizarTamaño(Ancho, Alto); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error actualizando geometría: {ex.Message}"); + } + } + + public void Inicializar(int valorInicial) + { + UpdateHeightAndPressure(); + OnId_InletComponentChanged(Id_InletComponent); + } + + public void Disposing() + { + if (InletComponent != null && inletPropertyChangedHandler != null) + ((INotifyPropertyChanged)InletComponent).PropertyChanged -= inletPropertyChangedHandler; + } + + #endregion + + #region Constructor + + public osHydDischargeTank() + { + UpdateHeightAndPressure(); + } + + #endregion + } +} diff --git a/ObjetosSim/HydraulicComponents/osHydPipe.cs b/ObjetosSim/HydraulicComponents/osHydPipe.cs index e69de29..053339a 100644 --- a/ObjetosSim/HydraulicComponents/osHydPipe.cs +++ b/ObjetosSim/HydraulicComponents/osHydPipe.cs @@ -0,0 +1,382 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Windows.Media; +using CtrEditor.HydraulicSimulator; +using CtrEditor.ObjetosSim; +using CtrEditor.FuncionesBase; +using HydraulicSimulator.Models; +using Newtonsoft.Json; +using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; +using CommunityToolkit.Mvvm.ComponentModel; +using LibS7Adv; + +namespace CtrEditor.ObjetosSim.HydraulicComponents +{ + /// + /// Tubería hidráulica que conecta componentes del sistema hidráulico + /// + public partial class osHydPipe : osBase, IHydraulicPipe, IosBase + { + #region Properties + + private double _length = 1.0; // metros + private double _diameter = 0.05; // metros (50mm) + private double _roughness = 4.5e-5; // metros - rugosidad del acero comercial + private double _currentFlow = 0.0; + private double _pressureDrop = 0.0; + + // Propiedades visuales + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Ancho visual de la tubería en metros")] + [property: Name("Ancho")] + private float ancho = 1.0f; + + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Alto visual de la tubería en metros")] + [property: Name("Alto")] + private float alto = 0.05f; + + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Color visual de la tubería")] + [property: Name("Color")] + private Brush colorButton_oculto = Brushes.Gray; + + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Ángulo de rotación de la tubería en grados")] + [property: Name("Ángulo")] + private float angulo = 0f; + + [Category("🔧 Tubería Hidráulica")] + [Description("Longitud de la tubería en metros")] + [Name("Longitud (m)")] + public double Length + { + get => _length; + set => SetProperty(ref _length, Math.Max(0.1, value)); + } + + [Category("🔧 Tubería Hidráulica")] + [Description("Diámetro interno de la tubería en metros")] + [Name("Diámetro (m)")] + public double Diameter + { + get => _diameter; + set => SetProperty(ref _diameter, Math.Max(0.001, value)); + } + + [Category("🔧 Tubería Hidráulica")] + [Description("Rugosidad del material en metros")] + [Name("Rugosidad (m)")] + public double Roughness + { + get => _roughness; + set => SetProperty(ref _roughness, Math.Max(1e-6, value)); + } + + [ObservableProperty] + [property: Category("🔗 Conexiones")] + [property: Description("Primer componente hidráulico conectado")] + [property: Name("Componente A")] + [property: ItemsSource(typeof(osBaseItemsSource))] + private string id_ComponenteA = ""; + + [ObservableProperty] + [property: Category("🔗 Conexiones")] + [property: Description("Segundo componente hidráulico conectado")] + [property: Name("Componente B")] + [property: ItemsSource(typeof(osBaseItemsSource))] + private string id_ComponenteB = ""; + + [Category("📊 Estado")] + [Description("Flujo actual a través de la tubería en m³/s")] + [Name("Flujo Actual (m³/s)")] + [ReadOnly(true)] + public double CurrentFlow + { + get => _currentFlow; + set => SetProperty(ref _currentFlow, value); + } + + [Category("📊 Estado")] + [Description("Pérdida de presión en la tubería en Pa")] + [Name("Pérdida Presión (Pa)")] + [ReadOnly(true)] + public double PressureDrop + { + get => _pressureDrop; + set => SetProperty(ref _pressureDrop, value); + } + + private string pipeId = ""; + public string PipeId + { + get => pipeId; + set => SetProperty(ref pipeId, value); + } + + #endregion + + #region Component References + + [JsonIgnore] + private IHydraulicComponent ComponenteA = null; + + [JsonIgnore] + private IHydraulicComponent ComponenteB = null; + + [JsonIgnore] + private PropertyChangedEventHandler componenteAPropertyChangedHandler; + + [JsonIgnore] + private PropertyChangedEventHandler componenteBPropertyChangedHandler; + + [JsonIgnore] + public Action ActualizarTamaño { get; set; } + + #endregion + + #region IHydraulicPipe Implementation + + // Ya implementadas las propiedades Length, Diameter, Roughness arriba + + #endregion + + #region IHydraulicComponent Implementation + + public bool HasHydraulicComponents => true; + + public List GetHydraulicNodes() + { + var nodes = new List(); + + // Las tuberías conectan componentes existentes, no crean nodos propios + // Los nodos son creados por los componentes conectados (tanques, bombas, etc.) + // La tubería solo actúa como elemento que conecta nodos existentes + + return nodes; + } + + public List GetHydraulicElements() + { + var elements = new List(); + + // 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); + + 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})" + )); + } + + return elements; + } + + public void UpdateHydraulicProperties() + { + // Actualizar propiedades antes de la simulación si es necesario + // Por ejemplo, cambios dinámicos en diámetro o rugosidad + } + + public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) + { + // Solo procesar si ambos componentes están conectados + if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB)) + { + string branchKey = $"{Id_ComponenteA}->{Id_ComponenteB}"; + + if (flows.TryGetValue(branchKey, out double flow)) + { + CurrentFlow = flow; + } + + // 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; + } + } + } + + #endregion + + #region IHydraulicFlowReceiver Implementation + + public void SetFlow(double flow) + { + CurrentFlow = flow; + } + + public double GetFlow() + { + return CurrentFlow; + } + + #endregion + + #region IHydraulicPressureReceiver Implementation + + public void SetPressure(double pressure) + { + // Para tuberías, la presión se maneja a través de los nodos + } + + public double GetPressure() + { + return PressureDrop; + } + + #endregion + + #region Connection Management + + partial void OnId_ComponenteAChanged(string value) + { + if (ComponenteA != null && componenteAPropertyChangedHandler != null) + ((INotifyPropertyChanged)ComponenteA).PropertyChanged -= componenteAPropertyChangedHandler; + + if (_mainViewModel != null && !string.IsNullOrEmpty(value)) + { + ComponenteA = (IHydraulicComponent)_mainViewModel.ObjetosSimulables + .FirstOrDefault(s => s is IHydraulicComponent comp && + ((osBase)comp).Nombre == value); + + if (ComponenteA != null) + { + componenteAPropertyChangedHandler = (sender, e) => + { + if (e.PropertyName == nameof(osBase.Nombre)) + { + Id_ComponenteA = ((osBase)sender).Nombre; + } + }; + ((INotifyPropertyChanged)ComponenteA).PropertyChanged += componenteAPropertyChangedHandler; + } + } + } + + partial void OnId_ComponenteBChanged(string value) + { + if (ComponenteB != null && componenteBPropertyChangedHandler != null) + ((INotifyPropertyChanged)ComponenteB).PropertyChanged -= componenteBPropertyChangedHandler; + + if (_mainViewModel != null && !string.IsNullOrEmpty(value)) + { + ComponenteB = (IHydraulicComponent)_mainViewModel.ObjetosSimulables + .FirstOrDefault(s => s is IHydraulicComponent comp && + ((osBase)comp).Nombre == value); + + if (ComponenteB != null) + { + componenteBPropertyChangedHandler = (sender, e) => + { + if (e.PropertyName == nameof(osBase.Nombre)) + { + Id_ComponenteB = ((osBase)sender).Nombre; + } + }; + ((INotifyPropertyChanged)ComponenteB).PropertyChanged += componenteBPropertyChangedHandler; + } + } + } + + #endregion + + #region osBase Implementation + + public static string NombreCategoria() => "Componentes Hidráulicos"; + + public static string NombreClase() => "Tubería Hidráulica"; + + private string nombre = NombreClase(); + + [Category("Identificación")] + [Description("Nombre identificativo del objeto")] + [Name("Nombre")] + public override string Nombre + { + get => nombre; + set => SetProperty(ref nombre, value); + } + + public override void AltoChanged(float value) + { + ActualizarGeometrias(); + } + + public override void AnchoChanged(float value) + { + ActualizarGeometrias(); + } + + public void Start() + { + // Las tuberías no participan en la simulación física Bepu + // Solo en el sistema hidráulico + } + + public override void UpdateGeometryStart() + { + ActualizarGeometrias(); + } + + public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds) + { + // Las tuberías no tienen control PLC directo + } + + private void ActualizarGeometrias() + { + try + { + // Actualizar geometría visual + if (this.ActualizarTamaño != null) + this.ActualizarTamaño(Ancho, Alto); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error actualizando geometría: {ex.Message}"); + } + } + + public void Inicializar(int valorInicial) + { + PipeId = $"Pipe_{Nombre}_{valorInicial}"; + OnId_ComponenteAChanged(Id_ComponenteA); + OnId_ComponenteBChanged(Id_ComponenteB); + } + + public void Disposing() + { + if (ComponenteA != null && componenteAPropertyChangedHandler != null) + ((INotifyPropertyChanged)ComponenteA).PropertyChanged -= componenteAPropertyChangedHandler; + + if (ComponenteB != null && componenteBPropertyChangedHandler != null) + ((INotifyPropertyChanged)ComponenteB).PropertyChanged -= componenteBPropertyChangedHandler; + } + + #endregion + + #region Constructor + + public osHydPipe() + { + PipeId = Guid.NewGuid().ToString(); + } + + #endregion + } +} diff --git a/ObjetosSim/HydraulicComponents/osHydPump.cs b/ObjetosSim/HydraulicComponents/osHydPump.cs index c1a8271..ca836c0 100644 --- a/ObjetosSim/HydraulicComponents/osHydPump.cs +++ b/ObjetosSim/HydraulicComponents/osHydPump.cs @@ -46,6 +46,12 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents [property: Description("Color visual de la bomba")] [property: Name("Color")] private Brush colorButton_oculto = Brushes.Blue; + + [ObservableProperty] + [property: Category("🎨 Apariencia")] + [property: Description("Ángulo de rotación de la bomba en grados")] + [property: Name("Ángulo")] + private float angulo = 0f; [Category("🔧 Bomba Hidráulica")] [DisplayName("Cabeza de bomba")] diff --git a/ObjetosSim/HydraulicComponents/osPumpExample.cs b/ObjetosSim/HydraulicComponents/osPumpExample.cs deleted file mode 100644 index c1a8271..0000000 --- a/ObjetosSim/HydraulicComponents/osPumpExample.cs +++ /dev/null @@ -1,364 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Windows.Media; -using CtrEditor.HydraulicSimulator; -using CtrEditor.ObjetosSim; -using CtrEditor.FuncionesBase; -using HydraulicSimulator.Models; -using Newtonsoft.Json; -using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace CtrEditor.ObjetosSim.HydraulicComponents -{ - /// - /// Bomba hidráulica que implementa las interfaces del simulador hidráulico - /// - public partial class osHydPump : osBase, IHydraulicPump, IosBase - { - #region Properties - - private double _pumpHead = 50.0; // metros - private double _maxFlow = 0.01; // m³/s (36 m³/h) - private double _speedRatio = 1.0; - private bool _isRunning = true; - private int _pumpDirection = 1; - private double _currentFlow = 0.0; - private double _currentPressure = 0.0; - - // Propiedades visuales - [ObservableProperty] - [property: Category("🎨 Apariencia")] - [property: Description("Ancho de la bomba en metros")] - [property: Name("Ancho")] - private float ancho = 0.15f; - - [ObservableProperty] - [property: Category("🎨 Apariencia")] - [property: Description("Alto de la bomba en metros")] - [property: Name("Alto")] - private float alto = 0.10f; - - [ObservableProperty] - [property: Category("🎨 Apariencia")] - [property: Description("Color visual de la bomba")] - [property: Name("Color")] - private Brush colorButton_oculto = Brushes.Blue; - - [Category("🔧 Bomba Hidráulica")] - [DisplayName("Cabeza de bomba")] - [Description("Cabeza de la bomba a caudal cero (m)")] - public double PumpHead - { - get => _pumpHead; - set - { - if (SetProperty(ref _pumpHead, Math.Max(0, value))) - { - InvalidateHydraulicNetwork(); - } - } - } - - [Category("🔧 Bomba Hidráulica")] - [DisplayName("Caudal máximo")] - [Description("Caudal máximo de la bomba (m³/s)")] - public double MaxFlow - { - get => _maxFlow; - set - { - if (SetProperty(ref _maxFlow, Math.Max(0.001, value))) - { - InvalidateHydraulicNetwork(); - } - } - } - - [Category("🔧 Bomba Hidráulica")] - [DisplayName("Velocidad relativa")] - [Description("Velocidad relativa de la bomba (0.0 a 1.0)")] - public double SpeedRatio - { - get => _speedRatio; - set - { - SetProperty(ref _speedRatio, Math.Max(0.0, Math.Min(1.0, value))); - } - } - - [Category("🔧 Bomba Hidráulica")] - [DisplayName("Funcionando")] - [Description("Indica si la bomba está encendida")] - public bool IsRunning - { - get => _isRunning; - set - { - SetProperty(ref _isRunning, value); - } - } - - [Category("🔧 Bomba Hidráulica")] - [DisplayName("Dirección")] - [Description("Dirección de la bomba (+1 o -1)")] - [ItemsSource(typeof(PumpDirectionItemsSource))] - public int PumpDirection - { - get => _pumpDirection; - set - { - if (SetProperty(ref _pumpDirection, value == -1 ? -1 : 1)) - { - InvalidateHydraulicNetwork(); - } - } - } - - [Category("📊 Estado Actual")] - [DisplayName("Caudal actual")] - [Description("Caudal actual de la bomba (m³/s)")] - [JsonIgnore] - public double CurrentFlow - { - get => _currentFlow; - private set => SetProperty(ref _currentFlow, value); - } - - [Category("📊 Estado Actual")] - [DisplayName("Presión actual")] - [Description("Presión actual en la bomba (Pa)")] - [JsonIgnore] - public double CurrentPressure - { - get => _currentPressure; - private set => SetProperty(ref _currentPressure, value); - } - - [Category("📊 Estado Actual")] - [DisplayName("Presión (bar)")] - [Description("Presión actual en la bomba (bar)")] - [JsonIgnore] - public double CurrentPressureBar => CurrentPressure / 100000.0; - - [Category("📊 Estado Actual")] - [DisplayName("Caudal (L/min)")] - [Description("Caudal actual en L/min")] - [JsonIgnore] - public double CurrentFlowLMin => CurrentFlow * 60000.0; // m³/s a L/min - - #endregion - - #region Constructor y Métodos Base - - private string nombre = NombreClase(); - - [Category("🏷️ Identificación")] - [Description("Nombre identificativo del objeto")] - [Name("Nombre")] - public override string Nombre - { - get => nombre; - set => SetProperty(ref nombre, value); - } - - public override void OnMove(float LeftPixels, float TopPixels) - { - // 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"; - } - - public override void UpdateGeometryStart() - { - // Los objetos hidráulicos se registran automáticamente - // cuando inicia la simulación a través de las interfaces - } - - public override void UpdateGeometryStep() - { - // Los objetos hidráulicos actualizan sus resultados - // a través de ApplyHydraulicResults() - } - - public override void UpdateControl(int elapsedMilliseconds) - { - // Actualizar el color según el estado - if (IsRunning) - ColorButton_oculto = Brushes.Green; - else - ColorButton_oculto = Brushes.Gray; - } - - public override void ucLoaded() - { - // Los objetos hidráulicos no necesitan geometría física - base.ucLoaded(); - } - - public override void ucUnLoaded() - { - // Los objetos hidráulicos no tienen geometría que limpiar - } - - #endregion - - #region IHydraulicComponent Implementation - - [JsonIgnore] - public bool HasHydraulicComponents => true; - - public List GetHydraulicNodes() - { - var nodes = new List - { - new HydraulicNodeDefinition($"{Nombre}_In", false, null, "Entrada de la bomba"), - new HydraulicNodeDefinition($"{Nombre}_Out", false, null, "Salida de la bomba") - }; - - return nodes; - } - - public List GetHydraulicElements() - { - var elements = new List(); - - if (HasHydraulicComponents) - { - // 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 - direction: PumpDirection - ); - - var pumpElement = new HydraulicElementDefinition( - $"{Nombre}_Pump", - $"{Nombre}_In", - $"{Nombre}_Out", - pump, - $"Bomba {Nombre}" - ); - - elements.Add(pumpElement); - } - - return elements; - } - - public void UpdateHydraulicProperties() - { - // Aquí se pueden hacer validaciones o ajustes antes de la simulación - if (!IsRunning) - { - SpeedRatio = 0.0; - } - - if (SpeedRatio < 0.0) SpeedRatio = 0.0; - if (SpeedRatio > 1.0) SpeedRatio = 1.0; - - Debug.WriteLine($"Bomba {Nombre}: Velocidad={SpeedRatio:F2}, Funcionando={IsRunning}"); - } - - public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) - { - // Buscar resultados para esta bomba - string pumpBranchName = $"{Nombre}_Pump"; - string inletNodeName = $"{Nombre}_In"; - string outletNodeName = $"{Nombre}_Out"; - - if (flows.ContainsKey(pumpBranchName)) - { - CurrentFlow = flows[pumpBranchName]; - } - - if (pressures.ContainsKey(inletNodeName)) - { - CurrentPressure = pressures[inletNodeName]; - } - else if (pressures.ContainsKey(outletNodeName)) - { - CurrentPressure = pressures[outletNodeName]; - } - } - - #endregion - - #region IHydraulicFlowReceiver Implementation - - public void SetFlow(double flow) - { - CurrentFlow = flow; - } - - public double GetFlow() - { - return CurrentFlow; - } - - #endregion - - #region IHydraulicPressureReceiver Implementation - - public void SetPressure(double pressure) - { - CurrentPressure = pressure; - } - - public double GetPressure() - { - return CurrentPressure; - } - - #endregion - - #region Helper Methods - - /// - /// Invalida la red hidráulica para forzar reconstrucción - /// - private void InvalidateHydraulicNetwork() - { - // Si tenemos acceso al MainViewModel, invalidar la red - if (_mainViewModel?.hydraulicSimulationManager != null) - { - _mainViewModel.hydraulicSimulationManager.InvalidateNetwork(); - } - } - - #endregion - - #region Static Interface Implementation - - public static string NombreClase() => "Bomba Hidráulica"; - public static string NombreCategoria() => "Componentes Hidráulicos"; - - #endregion - } - - /// - /// Proveedor de items para la dirección de la bomba - /// - public class PumpDirectionItemsSource : IItemsSource - { - public Xceed.Wpf.Toolkit.PropertyGrid.Attributes.ItemCollection GetValues() - { - var items = new Xceed.Wpf.Toolkit.PropertyGrid.Attributes.ItemCollection(); - items.Add(1, "Adelante (+1)"); - items.Add(-1, "Atrás (-1)"); - return items; - } - } -} diff --git a/ObjetosSim/HydraulicComponents/ucHydDischargeTank.xaml b/ObjetosSim/HydraulicComponents/ucHydDischargeTank.xaml new file mode 100644 index 0000000..444e208 --- /dev/null +++ b/ObjetosSim/HydraulicComponents/ucHydDischargeTank.xaml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Blue + 0.6 + + + + + + + + + + + + + + + + + + + + + diff --git a/ObjetosSim/HydraulicComponents/ucHydDischargeTank.xaml.cs b/ObjetosSim/HydraulicComponents/ucHydDischargeTank.xaml.cs new file mode 100644 index 0000000..da5d993 --- /dev/null +++ b/ObjetosSim/HydraulicComponents/ucHydDischargeTank.xaml.cs @@ -0,0 +1,70 @@ +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows; +using CtrEditor.ObjetosSim; +using CtrEditor.FuncionesBase; + +namespace CtrEditor.ObjetosSim.HydraulicComponents +{ + /// + /// Interaction logic for ucHydDischargeTank.xaml + /// + public partial class ucHydDischargeTank : UserControl, IDataContainer + { + public osBase? Datos { get; set; } + public int zIndex_fromFrames { get; set; } = 0; + + private bool _isHighlighted = false; + + public ucHydDischargeTank() + { + InitializeComponent(); + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + DataContextChanged += OnDataContextChanged; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Datos?.ucLoaded(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Datos?.ucUnLoaded(); + } + + private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (DataContext is osHydDischargeTank tank) + { + Datos = tank; + } + } + + #region IDataContainer Implementation + + public void Highlight(bool state) + { + _isHighlighted = state; + + if (state) + { + rectTank.Stroke = new SolidColorBrush(Colors.Yellow); + rectTank.StrokeThickness = 3; + } + else + { + rectTank.Stroke = new SolidColorBrush(Colors.DarkSlateGray); + rectTank.StrokeThickness = 2; + } + } + + #endregion + + public ZIndexEnum ZIndex_Base() + { + return ZIndexEnum.Estaticos; + } + } +} diff --git a/ObjetosSim/HydraulicComponents/ucHydPipe.xaml b/ObjetosSim/HydraulicComponents/ucHydPipe.xaml new file mode 100644 index 0000000..728d2d8 --- /dev/null +++ b/ObjetosSim/HydraulicComponents/ucHydPipe.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ObjetosSim/HydraulicComponents/ucHydPipe.xaml.cs b/ObjetosSim/HydraulicComponents/ucHydPipe.xaml.cs new file mode 100644 index 0000000..8e60b80 --- /dev/null +++ b/ObjetosSim/HydraulicComponents/ucHydPipe.xaml.cs @@ -0,0 +1,70 @@ +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows; +using CtrEditor.ObjetosSim; +using CtrEditor.FuncionesBase; + +namespace CtrEditor.ObjetosSim.HydraulicComponents +{ + /// + /// Interaction logic for ucHydPipe.xaml + /// + public partial class ucHydPipe : UserControl, IDataContainer + { + public osBase? Datos { get; set; } + public int zIndex_fromFrames { get; set; } = 0; + + private bool _isHighlighted = false; + + public ucHydPipe() + { + InitializeComponent(); + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + DataContextChanged += OnDataContextChanged; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Datos?.ucLoaded(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Datos?.ucUnLoaded(); + } + + private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (DataContext is osHydPipe pipe) + { + Datos = pipe; + } + } + + #region IDataContainer Implementation + + public void Highlight(bool state) + { + _isHighlighted = state; + + if (state) + { + rectPipe.Stroke = new SolidColorBrush(Colors.Yellow); + rectPipe.StrokeThickness = 3; + } + else + { + rectPipe.Stroke = new SolidColorBrush(Colors.DarkSlateGray); + rectPipe.StrokeThickness = 2; + } + } + + #endregion + + public ZIndexEnum ZIndex_Base() + { + return ZIndexEnum.Estaticos; + } + } +} diff --git a/ObjetosSim/HydraulicComponents/ucHydPump.xaml b/ObjetosSim/HydraulicComponents/ucHydPump.xaml index 0166eab..2ae0cbd 100644 --- a/ObjetosSim/HydraulicComponents/ucHydPump.xaml +++ b/ObjetosSim/HydraulicComponents/ucHydPump.xaml @@ -3,82 +3,77 @@ 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" - mc:Ignorable="d" - d:DesignHeight="60" d:DesignWidth="80"> - + xmlns:i="http://schemas.microsoft.com/xaml/behaviors" + xmlns:vm="clr-namespace:CtrEditor.ObjetosSim.HydraulicComponents" + mc:Ignorable="d"> + + + + + + + + + + + + + + + - - + - + - - + + - + + Fill="{Binding IsRunning, Converter={StaticResource BoolToColorConverter}}" + Canvas.Right="5" + Canvas.Top="5"/> - + + Fill="Green" + Canvas.Left="-5" + Canvas.Top="{Binding Alto, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=0.5}"/> + Fill="Red" + Canvas.Right="-5" + Canvas.Top="{Binding Alto, Converter={StaticResource MeterToPixelConverter}, ConverterParameter=0.5}"/> - - - - - - - - + diff --git a/ObjetosSim/HydraulicComponents/ucHydPump.xaml.cs b/ObjetosSim/HydraulicComponents/ucHydPump.xaml.cs index 38027e1..6ba1a86 100644 --- a/ObjetosSim/HydraulicComponents/ucHydPump.xaml.cs +++ b/ObjetosSim/HydraulicComponents/ucHydPump.xaml.cs @@ -140,12 +140,11 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents { if (pump.PumpDirection == -1) { - ArrowRotation.Angle = 180; // Invertir flecha + // Cambiar el color de la flecha para indicar dirección inversa DirectionArrow.Fill = new SolidColorBrush(Colors.Orange); } else { - ArrowRotation.Angle = 0; DirectionArrow.Fill = new SolidColorBrush(Colors.DarkBlue); } } @@ -155,18 +154,8 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents /// private void UpdateStatusInfo(osHydPump pump) { - FlowText.Text = $"{pump.CurrentFlowLMin:F1} L/min"; - PressureText.Text = $"{pump.CurrentPressureBar:F2} bar"; - - // Mostrar información si hay datos relevantes - if (Math.Abs(pump.CurrentFlow) > 0.001 || Math.Abs(pump.CurrentPressure) > 1000) - { - StatusInfo.Visibility = Visibility.Visible; - } - else - { - StatusInfo.Visibility = Visibility.Collapsed; - } + // 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 @@ -192,24 +181,13 @@ namespace CtrEditor.ObjetosSim.HydraulicComponents protected override void OnMouseEnter(System.Windows.Input.MouseEventArgs e) { base.OnMouseEnter(e); - if (!_isHighlighted) - { - // Mostrar información al pasar el mouse - StatusInfo.Visibility = Visibility.Visible; - } + // No hay elementos adicionales para mostrar en la implementación actual } protected override void OnMouseLeave(System.Windows.Input.MouseEventArgs e) { base.OnMouseLeave(e); - if (!_isHighlighted && Datos is osHydPump pump) - { - // Ocultar información si no hay datos relevantes - if (Math.Abs(pump.CurrentFlow) < 0.001 && Math.Abs(pump.CurrentPressure) < 1000) - { - StatusInfo.Visibility = Visibility.Collapsed; - } - } + // No hay elementos adicionales para ocultar en la implementación actual } public ZIndexEnum ZIndex_Base() diff --git a/ObjetosSim/HydraulicComponents/ucPumpExample.xaml b/ObjetosSim/HydraulicComponents/ucPumpExample.xaml deleted file mode 100644 index 0166eab..0000000 --- a/ObjetosSim/HydraulicComponents/ucPumpExample.xaml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ObjetosSim/HydraulicComponents/ucPumpExample.xaml.cs b/ObjetosSim/HydraulicComponents/ucPumpExample.xaml.cs deleted file mode 100644 index 2795d92..0000000 --- a/ObjetosSim/HydraulicComponents/ucPumpExample.xaml.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -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; - -namespace CtrEditor.ObjetosSim.HydraulicComponents -{ - /// - /// UserControl para la bomba hidráulica de ejemplo - /// - public partial class ucHydPump : UserControl, IDataContainer - { - public osBase? Datos { get; set; } - public int zIndex_fromFrames { get; set; } = 0; - - private Storyboard? _rotationAnimation; - private bool _isHighlighted = false; - - public ucHydPump() - { - 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) - { - 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(); - } - } - - /// - /// Actualiza el estado visual de la bomba - /// - 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}"); - } - } - - /// - /// Actualiza el LED de estado - /// - private void UpdateStatusLED(osHydPump pump) - { - if (pump.IsRunning && pump.SpeedRatio > 0) - { - StatusLED.Fill = new SolidColorBrush(Colors.LimeGreen); - } - else - { - StatusLED.Fill = new SolidColorBrush(Colors.Red); - } - } - - /// - /// Actualiza la animación de rotación - /// - 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; - } - } - - /// - /// Actualiza la flecha de dirección - /// - private void UpdateDirectionArrow(osHydPump pump) - { - if (pump.PumpDirection == -1) - { - ArrowRotation.Angle = 180; // Invertir flecha - DirectionArrow.Fill = new SolidColorBrush(Colors.Orange); - } - else - { - ArrowRotation.Angle = 0; - DirectionArrow.Fill = new SolidColorBrush(Colors.DarkBlue); - } - } - - /// - /// Actualiza la información de estado - /// - private void UpdateStatusInfo(osHydPump pump) - { - FlowText.Text = $"{pump.CurrentFlowLMin:F1} L/min"; - PressureText.Text = $"{pump.CurrentPressureBar:F2} bar"; - - // Mostrar información si hay datos relevantes - if (Math.Abs(pump.CurrentFlow) > 0.001 || Math.Abs(pump.CurrentPressure) > 1000) - { - StatusInfo.Visibility = Visibility.Visible; - } - else - { - StatusInfo.Visibility = Visibility.Collapsed; - } - } - - #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); - if (!_isHighlighted) - { - // Mostrar información al pasar el mouse - StatusInfo.Visibility = Visibility.Visible; - } - } - - protected override void OnMouseLeave(System.Windows.Input.MouseEventArgs e) - { - base.OnMouseLeave(e); - if (!_isHighlighted && Datos is osHydPump pump) - { - // Ocultar información si no hay datos relevantes - if (Math.Abs(pump.CurrentFlow) < 0.001 && Math.Abs(pump.CurrentPressure) < 1000) - { - StatusInfo.Visibility = Visibility.Collapsed; - } - } - } - - public ZIndexEnum ZIndex_Base() - { - return ZIndexEnum.Estaticos; - } - - ~ucHydPump() - { - _rotationAnimation?.Stop(); - } - } -} diff --git a/XAMLhelpers.cs b/XAMLhelpers.cs index b77bc77..ea29a9f 100644 --- a/XAMLhelpers.cs +++ b/XAMLhelpers.cs @@ -393,6 +393,47 @@ namespace CtrEditor } } + public class PercentageToHeightConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is float percentage) + { + // Assume the tank height is 100 pixels by default, or use parameter if provided + double tankHeight = 100; + if (parameter is double paramHeight) + tankHeight = paramHeight; + else if (parameter is string paramStr && double.TryParse(paramStr, out double parsed)) + tankHeight = parsed; + + return tankHeight * (percentage / 100.0); + } + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class BoolToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isTrue) + { + return isTrue ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red); + } + return new SolidColorBrush(Colors.Gray); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + public class BrushToColorNameConverter : IValueConverter {