using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Windows.Media; using CtrEditor.HydraulicSimulator; using CtrEditor.HydraulicSimulator.TSNet.Components; 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 { /// /// Bomba hidráulica que implementa las interfaces del simulador hidráulico /// public partial class osHydPump : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase { public static string NombreCategoria() => "Componentes Hidráulicos"; public static string NombreClase() { return "Bomba Hidráulica"; } // TSNet Integration [JsonIgnore] public TSNetPumpAdapter TSNetAdapter { get; private set; } // 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; private bool _isRunning = true; private int _pumpDirection = 1; private double _currentFlow = 0.0; private double _currentPressure = 0.0; // Propiedades visuales específicas de la bomba [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 { // Si el valor es mayor a 1, asumir que está en m³/h y convertir a m³/s double valueInM3s = value > 1.0 ? value / 3600.0 : value; if (SetProperty(ref _maxFlow, Math.Max(0.0001, valueInM3s))) { InvalidateHydraulicNetwork(); Debug.WriteLine($"Bomba {Nombre}: MaxFlow establecido a {_maxFlow:F6} m³/s ({_maxFlow * 3600:F2} m³/h)"); } } } [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 { 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)")] [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 (bar)")] [JsonIgnore] public double CurrentPressure { get => _currentPressure; private set => SetProperty(ref _currentPressure, value); } [Category("📊 Estado Actual")] [DisplayName("Presión (Pa)")] [Description("Presión actual en Pascal")] [JsonIgnore] public double CurrentPressurePa => CurrentPressure * 100000.0; [Category("📊 Estado Actual")] [DisplayName("Presión (bar)")] [Description("Presión actual en la bomba (bar)")] [JsonIgnore] public double CurrentPressureBar => CurrentPressure; // Ya está en bar internamente [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 [Category("📊 Estado Actual")] [DisplayName("Tiene Flujo")] [Description("Indica si hay flujo en la bomba")] [JsonIgnore] public bool HasFlow => Math.Abs(CurrentFlow) > 1e-6; [Category("📊 Estado Actual")] [DisplayName("Estado")] [Description("Estado actual de la bomba")] [JsonIgnore] public string PumpStatus { get { if (!IsRunning) return "Bomba detenida"; if (HasFlow) return "✅ Bombeando"; return "⚠️ Sin flujo"; } } [Category("📊 Estado Actual")] [DisplayName("Estado Detallado")] [Description("Información detallada del estado operacional")] [JsonIgnore] public string DetailedStatus { get { var flow = CurrentFlowLMin; var pressure = CurrentPressureBar; return $"Flujo: {flow:F1} L/min | Presión: {pressure:F1} bar | {PumpStatus}"; } } // Propiedades de fluido actual private FluidProperties _currentFluid = new FluidProperties(FluidType.Air); [Category("🧪 Fluido Actual")] [DisplayName("Tipo de fluido")] [Description("Tipo de fluido que está bombeando")] [ReadOnly(true)] public FluidType CurrentFluidType => _currentFluid.Type; [Category("🧪 Fluido Actual")] [DisplayName("Descripción")] [Description("Descripción del fluido que se está bombeando")] [ReadOnly(true)] public string CurrentFluidDescription => _currentFluid.Description; [Category("📊 Estado Actual")] [DisplayName("Factor Viscosidad")] [Description("Factor de eficiencia debido a la viscosidad del fluido")] [ReadOnly(true)] public double ViscosityEffect { get { // Factor basado en viscosidad del fluido vs agua var waterViscosity = 0.001; // Pa·s var currentViscosity = _currentFluid.Viscosity; return Math.Max(0.1, Math.Min(1.0, waterViscosity / currentViscosity)); } } [Category("📊 Estado Actual")] [DisplayName("Flujo Efectivo (L/min)")] [Description("Flujo real ajustado por viscosidad")] [ReadOnly(true)] public double EffectiveFlowLMin => CurrentFlowLMin * ViscosityEffect; [Category("📊 Estado Actual")] [DisplayName("Temperatura (°C)")] [Description("Temperatura del fluido bombeado")] [ReadOnly(true)] public double FluidTemperature => _currentFluid.Temperature; [Category("📊 Estado Actual")] [DisplayName("Densidad (kg/L)")] [Description("Densidad del fluido bombeado")] [ReadOnly(true)] public double FluidDensity => _currentFluid.Density; [Category("📊 Estado Actual")] [DisplayName("Brix (%)")] [Description("Concentración en grados Brix (para jarabes)")] [ReadOnly(true)] public double FluidBrix => _currentFluid.Brix; [Category("📊 Estado Actual")] [DisplayName("Concentración (%)")] [Description("Concentración en porcentaje (para químicos)")] [ReadOnly(true)] public double FluidConcentration => _currentFluid.Concentration; [Category("📊 Estado Actual")] [DisplayName("Viscosidad (cP)")] [Description("Viscosidad dinámica en centipoise")] [ReadOnly(true)] public double FluidViscosity => _currentFluid.Viscosity; [Category("📊 Estado Actual")] [DisplayName("Descripción del Fluido")] [Description("Descripción completa del fluido")] [ReadOnly(true)] public string FluidDescription => _currentFluid.Description; // 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 osHydPump() { Nombre = "Bomba Hidráulica"; Tamano = 0.30f; // Inicializar dimensiones usando las propiedades de osBase Ancho = 0.15f; Alto = 0.10f; Angulo = 0f; // Asegurar que el movimiento esté habilitado Lock_movement = false; ImageSource_oculta = ImageFromPath("/imagenes/pump_stop.png"); // No inicializar TSNet Adapter aquí - se creará cuando se registre con TSNetSimulationManager // TSNetAdapter = new TSNetPumpAdapter(this); } public override void UpdateGeometryStart() { // En el nuevo sistema unificado, el HydraulicSimulationManager se encarga // de registrar automáticamente los objetos que implementan IHydraulicComponent // No necesitamos crear objetos simHydraulic* separados } public override void UpdateGeometryStep() { // Los objetos hidráulicos actualizan sus resultados // a través de ApplyHydraulicResults() desde HydraulicSimulationManager } // Método para actualizar fluido desde tanque de succión public void UpdateFluidFromSuction() { if (_mainViewModel?.ObjetosSimulables == null) return; try { // Solo actualizar fluido si hay flujo if (HasFlow) { // Buscar el componente conectado en la succión var suctionComponent = FindSuctionComponent(); if (suctionComponent is osHydTank tank) { var sourceFluid = tank.CurrentOutputFluid; if (sourceFluid != null) { _currentFluid = sourceFluid.Clone(); OnPropertyChanged(nameof(CurrentFluidType)); OnPropertyChanged(nameof(CurrentFluidDescription)); OnPropertyChanged(nameof(ViscosityEffect)); OnPropertyChanged(nameof(EffectiveFlowLMin)); OnPropertyChanged(nameof(FluidTemperature)); OnPropertyChanged(nameof(FluidDensity)); // Actualizar color de la bomba según el fluido UpdatePumpColorFromFluid(); } } } else { // Si no hay flujo, mantener aire como fluido if (_currentFluid.Type != FluidType.Air) { _currentFluid = new FluidProperties(FluidType.Air); OnPropertyChanged(nameof(CurrentFluidType)); OnPropertyChanged(nameof(CurrentFluidDescription)); UpdatePumpColorFromFluid(); } } } catch (Exception ex) { // Mantener fluido por defecto en caso de error System.Diagnostics.Debug.WriteLine($"Error updating pump fluid: {ex.Message}"); } } private void UpdatePumpColorFromFluid() { if (!IsRunning) { ColorButton_oculto = Brushes.Gray; return; } // Color basado solo en si hay flujo o no if (HasFlow) { // Si hay flujo, usar color del fluido o verde por defecto try { var fluidColorHex = _currentFluid.Color; var fluidColor = (Color)System.Windows.Media.ColorConverter.ConvertFromString(fluidColorHex); ColorButton_oculto = new SolidColorBrush(fluidColor); } catch { ColorButton_oculto = Brushes.Green; // Color por defecto si hay flujo } } else { ColorButton_oculto = Brushes.Yellow; // Sin flujo (standby o sin succión) } } private IHydraulicComponent FindSuctionComponent() { // Buscar tuberías conectadas a esta bomba var connectedPipes = _mainViewModel.ObjetosSimulables .OfType() .Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre) .ToList(); foreach (var pipe in connectedPipes) { // La succión es donde el flujo viene HACIA la bomba string suctionComponentName = null; if (pipe.Id_ComponenteB == Nombre && pipe.CurrentFlow >= 0) { // Flujo de A hacia B (hacia esta bomba) suctionComponentName = pipe.Id_ComponenteA; } else if (pipe.Id_ComponenteA == Nombre && pipe.CurrentFlow < 0) { // Flujo de B hacia A (hacia esta bomba) suctionComponentName = pipe.Id_ComponenteB; } if (!string.IsNullOrEmpty(suctionComponentName)) { return _mainViewModel.ObjetosSimulables .OfType() .FirstOrDefault(c => c is osBase osb && osb.Nombre == suctionComponentName); } } return null; } public override void UpdateControl(int elapsedMilliseconds) { // En el nuevo sistema TSNet, los valores provienen exclusivamente de TSNet // No realizamos cálculos internos - solo actualizamos propiedades de UI try { // Los resultados de TSNet ya están aplicados en ApplyHydraulicResults() // Solo actualizamos propiedades derivadas y visuales // Actualizar propiedades del fluido basado en si hay flujo UpdateFluidFromSuction(); // Actualizar propiedades de UI calculadas OnPropertyChanged(nameof(CurrentFlowLMin)); OnPropertyChanged(nameof(EffectiveFlowLMin)); OnPropertyChanged(nameof(HasFlow)); OnPropertyChanged(nameof(PumpStatus)); OnPropertyChanged(nameof(DetailedStatus)); OnPropertyChanged(nameof(ViscosityEffect)); // Actualizar color visual basado en estado UpdatePumpColorFromFluid(); // Debug periódico cada 5 segundos if (Environment.TickCount % 5000 < elapsedMilliseconds) { Debug.WriteLine($"Pump {Nombre}: Flow={CurrentFlowLMin:F1}L/min, Pressure={CurrentPressureBar:F2}bar, Status={PumpStatus}"); } } catch (Exception ex) { Debug.WriteLine($"Error in Pump {Nombre} UpdateControl: {ex.Message}"); } } public override void ucLoaded() { // En el nuevo sistema unificado, no necesitamos crear objetos separados base.ucLoaded(); // No reinicializar TSNet Adapter aquí - se creará cuando se registre con TSNetSimulationManager // if (TSNetAdapter == null) // { // TSNetAdapter = new TSNetPumpAdapter(this); // Debug.WriteLine($"Pump {Nombre}: TSNetAdapter inicializado en ucLoaded"); // } UpdatePumpImage(); } public override void ucUnLoaded() { // En el nuevo sistema unificado, el HydraulicSimulationManager // maneja automáticamente el registro/desregistro de objetos base.ucUnLoaded(); } // IHydraulicComponent Implementation [JsonIgnore] public bool HasHydraulicComponents => true; public List GetHydraulicNodes() { var nodes = new List(); // Las bombas no crean nodos propios - deben estar conectadas a otros componentes para funcionar if (!HasConnectedComponents()) { Debug.WriteLine($"Bomba {Nombre}: Sin componentes conectados - no puede funcionar"); } else { //Debug.WriteLine($"Bomba {Nombre}: Conectada a otros componentes - lista para operar"); } return nodes; } public List GetHydraulicElements() { var elements = new List(); // Solo crear elementos si la bomba está conectada a otros componentes if (!HasConnectedComponents()) { Debug.WriteLine($"Bomba {Nombre}: Sin conexiones - no se puede crear elemento hidráulico"); return elements; } 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)); // Obtener los nombres de nodos correctos basados en las conexiones var (inletNode, outletNode) = GetConnectedNodeNames(); // Verificar que tenemos nodos válidos if (string.IsNullOrEmpty(inletNode) || string.IsNullOrEmpty(outletNode)) { Debug.WriteLine($"Bomba {Nombre}: Nodos de conexión inválidos - inlet: '{inletNode}', outlet: '{outletNode}'"); return elements; } // Crear bomba estándar - el solver manejará las limitaciones de flujo var pump = new PumpHQ( h0: PumpHead, q0: MaxFlow, speedRel: effectiveSpeed, direction: PumpDirection ); // Asignar nombres de nodos pump.InletNodeName = inletNode; pump.OutletNodeName = outletNode; var pumpElement = new HydraulicElementDefinition( $"{Nombre}_Pump", inletNode, outletNode, pump, $"Bomba {Nombre}" ); elements.Add(pumpElement); Debug.WriteLine($"Bomba {Nombre}: Creando elemento hidráulico - H0={PumpHead}m, Q0={MaxFlow:F6}m³/s ({MaxFlow*3600:F2}m³/h), Velocidad={effectiveSpeed:F2}, Dirección={PumpDirection}"); Debug.WriteLine($"Bomba {Nombre}: Conectando {inletNode} -> {outletNode}"); } return elements; } public void UpdateHydraulicProperties() { // Validaciones simples antes de la simulación if (!IsRunning) { SpeedRatio = 0.0; } if (SpeedRatio < 0.0) SpeedRatio = 0.0; if (SpeedRatio > 1.0) SpeedRatio = 1.0; // El solver se encarga del resto - NPSH, limitaciones de flujo, etc. } public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) { try { // Aplicar resultados a través del TSNet Adapter if (TSNetAdapter?.Results != null) { // Buscar resultados para esta bomba usando el NodeId del adapter string pumpElementName = $"{Nombre}_Pump"; // Actualizar flujo desde TSNet if (flows.ContainsKey(pumpElementName)) { var flowM3s = flows[pumpElementName]; CurrentFlow = flowM3s; TSNetAdapter.Results.CalculatedFlowM3s = flowM3s; Debug.WriteLine($"Pump {Nombre}: TSNet Flow={flowM3s:F6} m³/s ({CurrentFlowLMin:F2} L/min)"); } else { // Intentar con NodeId del adapter if (flows.ContainsKey(TSNetAdapter.NodeId)) { var flowM3s = flows[TSNetAdapter.NodeId]; CurrentFlow = flowM3s; TSNetAdapter.Results.CalculatedFlowM3s = flowM3s; } else { CurrentFlow = 0.0; TSNetAdapter.Results.CalculatedFlowM3s = 0.0; } } // Actualizar presión desde TSNet var (inletNode, outletNode) = GetConnectedNodeNames(); if (!string.IsNullOrEmpty(outletNode) && pressures.ContainsKey(outletNode)) { var pressurePa = pressures[outletNode]; CurrentPressure = pressurePa / 100000.0; // Convertir Pa a bar TSNetAdapter.Results.OutletPressureBar = CurrentPressure; Debug.WriteLine($"Pump {Nombre}: TSNet Pressure={pressurePa:F0} Pa = {CurrentPressure:F3} bar"); } else if (!string.IsNullOrEmpty(inletNode) && pressures.ContainsKey(inletNode)) { var pressurePa = pressures[inletNode]; CurrentPressure = pressurePa / 100000.0; TSNetAdapter.Results.InletPressureBar = CurrentPressure; } // Actualizar estado del adapter TSNetAdapter.Results.IsOperating = IsRunning; TSNetAdapter.Results.CalculatedEfficiency = CurrentFlow * SpeedRatio; TSNetAdapter.Results.Timestamp = DateTime.Now; TSNetAdapter.Results.OperationalStatus = $"Flow: {CurrentFlowLMin:F1}L/min, Pressure: {CurrentPressureBar:F2}bar"; // Actualizar el fluido desde el tanque de succión si hay flujo if (HasFlow) { UpdateFluidFromSuction(); } } // Notificar cambios para UI OnPropertyChanged(nameof(CurrentFlow)); OnPropertyChanged(nameof(CurrentFlowLMin)); OnPropertyChanged(nameof(CurrentPressure)); OnPropertyChanged(nameof(HasFlow)); OnPropertyChanged(nameof(PumpStatus)); OnPropertyChanged(nameof(DetailedStatus)); // Debug periódico if (Environment.TickCount % 2000 < 50) // Cada ~2 segundos { Debug.WriteLine($"Pump {Nombre}: Flow={CurrentFlowLMin:F1}L/min, Pressure={CurrentPressureBar:F2}bar, Running={IsRunning}"); } } catch (Exception ex) { Debug.WriteLine($"Error en Pump {Nombre} ApplyHydraulicResults: {ex.Message}"); } } // IHydraulicFlowReceiver Implementation public void SetFlow(double flow) { CurrentFlow = flow; } public double GetFlow() { return CurrentFlow; } // IHydraulicPressureReceiver Implementation public void SetPressure(double pressure) { CurrentPressure = pressure; } public double GetPressure() { return CurrentPressure; } // Helper Methods /// /// Verifica si la bomba tiene componentes conectados a través de tuberías /// private bool HasConnectedComponents() { if (_mainViewModel == null) return false; // Buscar tuberías que conecten esta bomba con otros componentes // CORREGIDO: Usar Nombre en lugar de ID para buscar conexiones var connectedPipes = _mainViewModel.ObjetosSimulables .OfType() .Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre) .ToList(); return connectedPipes.Any(); } /// /// Obtiene los nombres de nodos correctos para la bomba basándose en las conexiones /// private (string inletNode, string outletNode) GetConnectedNodeNames() { if (_mainViewModel == null) return (string.Empty, string.Empty); string inletNode = string.Empty; string outletNode = string.Empty; // Buscar tuberías conectadas a esta bomba // CORREGIDO: Usar Nombre en lugar de ID para buscar conexiones var connectedPipes = _mainViewModel.ObjetosSimulables .OfType() .Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre) .ToList(); foreach (var pipe in connectedPipes) { if (pipe.Id_ComponenteB == Nombre && !string.IsNullOrEmpty(pipe.Id_ComponenteA)) { // Esta bomba es el destino, el componente A es la fuente (inlet) inletNode = ResolveComponentIdToNodeName(pipe.Id_ComponenteA); //Debug.WriteLine($"Bomba {Nombre}: Nodo inlet identificado como '{inletNode}'"); } else if (pipe.Id_ComponenteA == Nombre && !string.IsNullOrEmpty(pipe.Id_ComponenteB)) { // Esta bomba es la fuente, el componente B es el destino (outlet) outletNode = ResolveComponentIdToNodeName(pipe.Id_ComponenteB); //Debug.WriteLine($"Bomba {Nombre}: Nodo outlet identificado como '{outletNode}'"); } } return (inletNode, outletNode); } /// /// Resuelve un ID de componente al nombre de nodo hidráulico correspondiente /// private string ResolveComponentIdToNodeName(string componentId) { if (string.IsNullOrEmpty(componentId) || _mainViewModel == null) return string.Empty; // CORREGIDO: Buscar el componente por NOMBRE (no por ID numérico) // Los pipes usan nombres de componentes como "Tanque Origen", "Bomba Principal" var component = _mainViewModel.ObjetosSimulables .FirstOrDefault(obj => obj.Nombre == componentId); if (component == null) return string.Empty; // Para tanques, el nombre del nodo es el nombre del tanque if (component is osHydTank tank) { return tank.Nombre; } // Para tuberías, necesitamos seguir la conexión hasta encontrar un tanque terminal if (component is osHydPipe pipe) { // Las tuberías NO son nodos - debemos encontrar el tanque terminal return FindTerminalNodeFromPipe(pipe, componentId); } // Para otros tipos de componentes hidráulicos if (component is IHydraulicComponent hydComponent) { return component.Nombre; } return string.Empty; } /// /// Encuentra el nodo terminal (tanque) siguiendo la cadena desde una tubería /// private string FindTerminalNodeFromPipe(osHydPipe startPipe, string originComponentId) { var visited = new HashSet(); var queue = new Queue(); queue.Enqueue(startPipe); visited.Add(startPipe.Id.Value.ToString()); while (queue.Count > 0) { var currentPipe = queue.Dequeue(); // Verificar ambos extremos de la tubería var connectionIds = new[] { currentPipe.Id_ComponenteA, currentPipe.Id_ComponenteB }; foreach (var connectionId in connectionIds) { if (string.IsNullOrEmpty(connectionId) || connectionId == originComponentId) continue; var connectedComponent = _mainViewModel.ObjetosSimulables .FirstOrDefault(obj => obj.Id.Value.ToString() == connectionId); // Si encontramos un tanque, ese es nuestro nodo terminal if (connectedComponent is osHydTank tank) { Debug.WriteLine($"Bomba {Nombre}: Nodo terminal encontrado: {tank.Nombre}"); return tank.Nombre; } // Si encontramos otra tubería, continuar la búsqueda if (connectedComponent is osHydPipe nextPipe && !visited.Contains(nextPipe.Id.Value.ToString())) { queue.Enqueue(nextPipe); visited.Add(nextPipe.Id.Value.ToString()); } } } Debug.WriteLine($"Bomba {Nombre}: No se pudo encontrar nodo terminal desde tubería {startPipe.Nombre}"); return string.Empty; } private void InvalidateHydraulicNetwork() { _mainViewModel?.hydraulicSimulationManager?.InvalidateNetwork(); } } /// /// 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; } } }