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; using LibS7Adv; namespace CtrEditor.ObjetosSim { /// /// Tubería hidráulica que conecta componentes del sistema hidráulico /// public partial class osHydPipe : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase { // TSNet Integration [JsonIgnore] public TSNetPipeAdapter TSNetAdapter { get; private set; } // 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); } // Propiedades adicionales para información de fluido private FluidProperties _currentFluid = new FluidProperties(FluidType.Air); [Category("🧪 Fluido Actual")] [DisplayName("Tipo de fluido")] [Description("Tipo de fluido que atraviesa la tubería")] [ReadOnly(true)] public FluidType CurrentFluidType { get => _currentFluid.Type; private set { if (_currentFluid.Type != value) { _currentFluid.Type = value; OnPropertyChanged(); OnPropertyChanged(nameof(CurrentFluidDescription)); } } } [Category("🧪 Fluido Actual")] [DisplayName("Descripción")] [Description("Descripción completa del fluido actual")] [ReadOnly(true)] public string CurrentFluidDescription => _currentFluid.Description; [Category("🧪 Fluido Actual")] [DisplayName("Temperatura (°C)")] [Description("Temperatura del fluido en grados Celsius")] [ReadOnly(true)] [JsonProperty] public double CurrentFluidTemperature => _currentFluid.Temperature; [Category("🧪 Fluido Actual")] [DisplayName("Brix (%)")] [Description("Concentración en grados Brix (para jarabes)")] [ReadOnly(true)] [JsonProperty] public double CurrentFluidBrix => _currentFluid.Brix; [Category("🧪 Fluido Actual")] [DisplayName("Concentración (%)")] [Description("Concentración en porcentaje (para químicos)")] [ReadOnly(true)] [JsonProperty] public double CurrentFluidConcentration => _currentFluid.Concentration; [Category("🧪 Fluido Actual")] [DisplayName("Densidad (kg/L)")] [Description("Densidad del fluido en kg/L")] [ReadOnly(true)] [JsonProperty] public double CurrentFluidDensity => _currentFluid.Density; [Category("🧪 Fluido Actual")] [DisplayName("Viscosidad (cP)")] [Description("Viscosidad dinámica en centipoise")] [ReadOnly(true)] [JsonProperty] public double CurrentFluidViscosity => _currentFluid.Viscosity; [Category("🧪 Fluido Actual")] [DisplayName("Velocidad (m/s)")] [Description("Velocidad del fluido en la tubería")] [ReadOnly(true)] [JsonProperty] public double FluidVelocity { get { if (Diameter <= 0) return 0; var area = Math.PI * Math.Pow(Diameter / 2, 2); // m² return Math.Abs(CurrentFlow) / area; // m/s } } [Category("🧪 Fluido Actual")] [DisplayName("Número de Reynolds")] [Description("Número de Reynolds (Re = ρ × v × D / μ) - Caracteriza el régimen de flujo")] [ReadOnly(true)] [JsonProperty] public double ReynoldsNumber { get { if (CurrentFluidViscosity <= 0 || Diameter <= 0) return 0; // Re = (ρ × v × D) / μ // ρ: densidad (kg/m³) - convertir de kg/L // v: velocidad (m/s) // D: diámetro (m) // μ: viscosidad dinámica (Pa·s) - convertir de cP var density_kg_m3 = CurrentFluidDensity * 1000; // kg/L → kg/m³ var viscosity_pa_s = CurrentFluidViscosity * 0.001; // cP → Pa·s return (density_kg_m3 * FluidVelocity * Diameter) / viscosity_pa_s; } } [Category("🧪 Fluido Actual")] [DisplayName("Régimen de Flujo")] [Description("Tipo de régimen basado en Reynolds (Laminar < 2300 < Turbulento)")] [ReadOnly(true)] [JsonProperty] public string FlowRegime { get { var re = ReynoldsNumber; return re switch { < 2300 => $"Laminar (Re = {re:F0})", > 4000 => $"Turbulento (Re = {re:F0})", _ => $"Transición (Re = {re:F0})" }; } } // Método para exponer propiedades detalladas en serialización JSON [JsonProperty("DetailedFluidProperties")] [Browsable(false)] public Dictionary DetailedFluidProperties => new Dictionary { ["CurrentFluidTemperature"] = CurrentFluidTemperature, ["CurrentFluidBrix"] = CurrentFluidBrix, ["CurrentFluidConcentration"] = CurrentFluidConcentration, ["CurrentFluidDensity"] = CurrentFluidDensity, ["CurrentFluidViscosity"] = CurrentFluidViscosity, ["FluidVelocity"] = FluidVelocity, ["ReynoldsNumber"] = ReynoldsNumber, ["FlowRegime"] = FlowRegime }; [Category("📊 Estado")] [DisplayName("Flujo (L/min)")] [Description("Flujo actual en litros por minuto")] [ReadOnly(true)] public double CurrentFlowLMin => Math.Abs(CurrentFlow) * 60000.0; // Conversión de m³/s a L/min [Category("📊 Estado")] [DisplayName("Dirección")] [Description("Dirección del flujo")] [ReadOnly(true)] public string FlowDirection => CurrentFlow > 0 ? $"{Id_ComponenteA} → {Id_ComponenteB}" : CurrentFlow < 0 ? $"{Id_ComponenteB} → {Id_ComponenteA}" : "Sin flujo"; [JsonIgnore] public SolidColorBrush FluidColor { get { try { var colorHex = _currentFluid.Color; return new SolidColorBrush((Color)ColorConverter.ConvertFromString(colorHex)); } catch { return Brushes.Gray; } } } private string pipeId = ""; public string PipeId { get => pipeId; set => SetProperty(ref pipeId, value); } // 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; } // IHydraulicPipe Implementation // Ya implementadas las propiedades Length, Diameter, Roughness arriba // 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)) { // 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 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; } /// /// Obtiene el nombre del nodo correcto para un componente dado /// /// Nombre del componente /// True si es el nodo origen de la tubería, False si es destino /// Nombre del nodo o string vacío si no se encuentra 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 // Por ejemplo, cambios dinámicos en diámetro o rugosidad } public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) { try { // Aplicar resultados a través del TSNet Adapter if (TSNetAdapter?.Results != null) { // Buscar flujo usando el nombre del elemento de TSNet string pipeElementName = $"{Nombre}_Pipe"; if (flows.ContainsKey(pipeElementName)) { var flowM3s = flows[pipeElementName]; CurrentFlow = flowM3s; TSNetAdapter.Results.CalculatedFlowM3s = flowM3s; Debug.WriteLine($"Pipe {Nombre}: TSNet Flow={flowM3s:F6} m³/s ({CurrentFlowLMin:F2} L/min)"); } else { // Intentar claves alternativas basadas en componentes conectados var alternativeKeys = new[] { $"{Id_ComponenteA}_{Id_ComponenteB}_Pipe", $"Pipe_{Id_ComponenteA}_{Id_ComponenteB}", TSNetAdapter.PipeId, $"Branch_{Nombre}" }; bool flowFound = false; foreach (string altKey in alternativeKeys) { if (flows.ContainsKey(altKey)) { var flowM3s = flows[altKey]; CurrentFlow = flowM3s; TSNetAdapter.Results.CalculatedFlowM3s = flowM3s; flowFound = true; break; } } if (!flowFound) { CurrentFlow = 0.0; TSNetAdapter.Results.CalculatedFlowM3s = 0.0; } } // Calcular pérdida de presión entre nodos conectados if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB)) { var fromNodeName = GetNodeNameForComponent(Id_ComponenteA, true); var toNodeName = GetNodeNameForComponent(Id_ComponenteB, false); if (pressures.ContainsKey(fromNodeName) && pressures.ContainsKey(toNodeName)) { var pressureA = pressures[fromNodeName]; var pressureB = pressures[toNodeName]; PressureDrop = pressureA - pressureB; TSNetAdapter.Results.PressureDropBar = PressureDrop; Debug.WriteLine($"Pipe {Nombre}: Pressure drop={PressureDrop:F0} Pa ({fromNodeName} → {toNodeName})"); } } // Actualizar resultados del adapter TSNetAdapter.Results.Timestamp = DateTime.Now; TSNetAdapter.Results.FlowStatus = $"Flow: {CurrentFlowLMin:F1}L/min, Drop: {PressureDrop:F1}Pa"; // Actualizar fluido desde componente fuente UpdateFluidFromSource(); } // Notificar cambios para UI OnPropertyChanged(nameof(CurrentFlow)); OnPropertyChanged(nameof(CurrentFlowLMin)); OnPropertyChanged(nameof(PressureDrop)); OnPropertyChanged(nameof(FlowDirection)); OnPropertyChanged(nameof(FluidVelocity)); OnPropertyChanged(nameof(ReynoldsNumber)); OnPropertyChanged(nameof(FlowRegime)); // Debug periódico if (Environment.TickCount % 2000 < 50) // Cada ~2 segundos { Debug.WriteLine($"Pipe {Nombre}: Flow={CurrentFlowLMin:F1}L/min, Drop={PressureDrop:F1}Pa, {FlowDirection}"); } } catch (Exception ex) { Debug.WriteLine($"Error en Pipe {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) { // Para tuberías, la presión se maneja a través de los nodos } public double GetPressure() { return PressureDrop; } // 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; } } } // 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() { // En el nuevo sistema unificado, el HydraulicSimulationManager se encarga // de registrar automáticamente los objetos que implementan IHydraulicComponent // No necesitamos crear objetos simHydraulic* separados Debug.WriteLine($"[DEBUG] {Nombre}: UpdateGeometryStart() - ComponenteA: '{Id_ComponenteA}', ComponenteB: '{Id_ComponenteB}'"); ActualizarGeometrias(); } public override void UpdateGeometryStep() { // Los objetos hidráulicos actualizan sus resultados // a través de ApplyHydraulicResults() desde HydraulicSimulationManager } // Método para actualizar fluido desde componente fuente public void UpdateFluidFromSource() { if (_mainViewModel?.ObjetosSimulables == null) return; // Determinar componente fuente basado en dirección del flujo string sourceComponentName = CurrentFlow >= 0 ? Id_ComponenteA : Id_ComponenteB; var sourceComponent = _mainViewModel.ObjetosSimulables .OfType() .FirstOrDefault(c => c is osBase osb && osb.Nombre == sourceComponentName); if (sourceComponent is osHydTank tank) { var sourceFluid = tank.CurrentOutputFluid; if (sourceFluid != null) { _currentFluid = sourceFluid.Clone(); OnPropertyChanged(nameof(CurrentFluidType)); OnPropertyChanged(nameof(CurrentFluidDescription)); OnPropertyChanged(nameof(FluidColor)); } } // Actualizar color visual if (Math.Abs(CurrentFlow) > 1e-6) // Hay flujo { ColorButton_oculto = FluidColor; } else { ColorButton_oculto = Brushes.Gray; // Sin flujo } } 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 cada ciclo UpdateFluidFromSource(); // Actualizar propiedades de UI calculadas OnPropertyChanged(nameof(CurrentFlowLMin)); OnPropertyChanged(nameof(FlowDirection)); OnPropertyChanged(nameof(FluidVelocity)); OnPropertyChanged(nameof(ReynoldsNumber)); OnPropertyChanged(nameof(FlowRegime)); // Actualizar color visual basado en fluido UpdatePipeColorFromFluid(); // Debug periódico cada 5 segundos if (Environment.TickCount % 5000 < elapsedMilliseconds) { Debug.WriteLine($"Pipe {Nombre}: Flow={CurrentFlowLMin:F1}L/min, Drop={PressureDrop:F1}Pa, {FlowDirection}"); } } catch (Exception ex) { Debug.WriteLine($"Error in Pipe {Nombre} UpdateControl: {ex.Message}"); } } public override void ucLoaded() { // En el nuevo sistema unificado, no necesitamos crear objetos separados base.ucLoaded(); } public override void ucUnLoaded() { // En el nuevo sistema unificado, el HydraulicSimulationManager // maneja automáticamente el registro/desregistro de objetos base.ucUnLoaded(); } 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; } /// /// Actualiza el color de la tubería basado en el fluido que transporta /// private void UpdatePipeColorFromFluid() { try { if (Math.Abs(CurrentFlow) < 1e-6) { ColorButton_oculto = Brushes.Gray; // Sin flujo return; } // Color basado en el fluido actual var fluidColor = FluidColor; if (fluidColor != null) { ColorButton_oculto = fluidColor; } else { // Color por defecto según dirección de flujo ColorButton_oculto = CurrentFlow > 0 ? Brushes.Blue : Brushes.Red; } } catch { ColorButton_oculto = Brushes.Gray; // Color por defecto en caso de error } } // Constructor public osHydPipe() { PipeId = Guid.NewGuid().ToString(); IsVisFilter = true; // Asegurar que el componente hidráulico sea visible en filtros // No inicializar TSNet Adapter aquí - se creará cuando se registre con TSNetSimulationManager // TSNetAdapter = new TSNetPipeAdapter(this); } } }