diff --git a/Documentation/SistemaFluidos.md b/Documentation/SistemaFluidos.md new file mode 100644 index 0000000..9d1fb7b --- /dev/null +++ b/Documentation/SistemaFluidos.md @@ -0,0 +1,99 @@ +# Guía de Uso: Sistema de Fluidos + +## Introducción + +El Sistema de Fluidos es una extensión para CtrEditor que permite simular el comportamiento de líquidos en elementos como tuberías, válvulas y tanques. Utiliza una implementación optimizada basada en el método SPH (Smoothed Particle Hydrodynamics) para la simulación de fluidos. + +## Componentes del Sistema + +El sistema de fluidos incluye los siguientes componentes: + +1. **SistemaFluidos**: El componente principal que gestiona la simulación de partículas. +2. **TuberíaFluido**: Componente para crear tuberías por las que circula el fluido. +3. **ValvulaFluido**: Componente que permite regular el flujo del fluido. + +## Añadir el Sistema de Fluidos + +Para comenzar a utilizar el sistema de fluidos: + +1. Inserte un componente "SistemaFluidos" en su diseño. +2. Configure el tamaño del área de simulación y la gravedad según sus necesidades. +3. Añada los componentes de tuberías y válvulas que necesite. + +## Propiedades Principales + +### SistemaFluidos + +- **AnchoSimulacion/AltoSimulacion**: Dimensiones del área de simulación. +- **GravedadX/GravedadY**: Vector de gravedad para la simulación. +- **TamañoParticula**: Tamaño visual de las partículas de fluido. +- **ColorFluido**: Color base para las partículas de fluido. +- **OpacidadParticulas**: Transparencia de las partículas (0-1). + +### TuberíaFluido + +- **Diametro**: Diámetro de la tubería. +- **Color**: Color de la tubería. +- **ColorFluido**: Color del fluido dentro de la tubería. + +### ValvulaFluido + +- **Apertura**: Grado de apertura de la válvula (0=cerrada, 1=abierta). +- **DiametroTuberia**: Diámetro de las tuberías conectadas. +- **TagApertura**: Tag PLC para controlar la apertura. + +## Integración con PLC + +El sistema de fluidos permite integración con PLC mediante tags: + +- **TagNivelTanque1**: Lectura/escritura del nivel de llenado del tanque. +- **TagAperturaValvula1**: Lectura/escritura de la apertura de la válvula. + +## Añadir Fluido a la Simulación + +Para añadir partículas de fluido: + +1. Seleccione el componente SistemaFluidos. +2. Utilice el método `AgregarParticulasEnArea` para añadir partículas en una región. + +Ejemplo: + +```csharp +// Añadir 100 partículas en un área centrada en (x,y) con ancho y alto especificados +_sistemaFluidos.AgregarParticulasEnArea(new Vector2(x, y), ancho, alto, 100); +``` + +## Optimización + +El Sistema de Fluidos utiliza `DrawingVisual` para una renderización eficiente, lo que permite simular miles de partículas con un impacto mínimo en el rendimiento. + +## Limitaciones + +- La simulación está optimizada para cantidades moderadas de partículas (hasta 10,000). +- Los contenedores (tuberías, válvulas) son aproximaciones y pueden presentar fugas o comportamientos inesperados con flujos muy rápidos. + +## Recomendaciones + +- Comience con pocas partículas y aumente gradualmente según sea necesario. +- Mantenga la configuración de gravedad en valores realistas. +- Para simular líquidos específicos, ajuste la visualización con el color y opacidad adecuados. + +## Ejemplos de Uso + +### Sistema Básico de Flujo + +``` +SistemaFluidos → TuberíaFluido → ValvulaFluido → TuberíaFluido → Tanque +``` + +### Circuito Cerrado + +``` +SistemaFluidos → TuberíaFluido → ValvulaFluido → TuberíaFluido → TuberíaFluido (retorno) +``` + +## Solución de Problemas + +- **Partículas escapando de contenedores**: Verifique que los componentes estén correctamente posicionados y conectados. +- **Bajo rendimiento**: Reduzca el número de partículas o el tamaño visual. +- **Cambios de apertura de válvula no afectan el flujo**: Verifique la conexión PLC y la configuración de tags. diff --git a/ObjetosSim/osSistemaFluidos.cs b/ObjetosSim/osSistemaFluidos.cs new file mode 100644 index 0000000..5456730 --- /dev/null +++ b/ObjetosSim/osSistemaFluidos.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CtrEditor.FuncionesBase; +using CtrEditor.Simulacion.Fluids; +using CtrEditor.Simulacion.Fluids.Components; +using nkast.Aether.Physics2D.Common; +using Color = System.Windows.Media.Color; +using Siemens.Simatic.Simulation.Runtime; +using LibS7Adv; + +namespace CtrEditor.ObjetosSim +{ + /// + /// ViewModel para el sistema de fluidos + /// + public partial class osSistemaFluidos : osBase, IosBase + { + // Referencia a la simulación de fluidos + public SimulacionFluidos _simFluidos; + + // Tamaño del área de simulación + [ObservableProperty] + [property: Description("Ancho del área de simulación en metros")] + [property: Category("Simulación:")] + private float anchoSimulacion = 10.0f; + + [ObservableProperty] + [property: Description("Alto del área de simulación en metros")] + [property: Category("Simulación:")] + private float altoSimulacion = 10.0f; + + // Propiedades del fluido + [ObservableProperty] + [property: Description("Tamaño visual de las partículas")] + [property: Category("Visual:")] + private float tamañoParticula = 0.01f; + + [ObservableProperty] + [property: Description("Color del fluido")] + [property: Category("Visual:")] + private Color colorFluido = Colors.CornflowerBlue; + + [ObservableProperty] + [property: Description("Opacidad de las partículas")] + [property: Category("Visual:")] + private double opacidadParticulas = 0.7; + + // Propiedades de gravedad + [ObservableProperty] + [property: Description("Gravedad en X (m/s²)")] + [property: Category("Física:")] + private float gravedadX = 0.0f; + + [ObservableProperty] + [property: Description("Gravedad en Y (m/s²)")] + [property: Category("Física:")] + private float gravedadY = 9.8f; + + partial void OnGravedadXChanged(float value) + { + ActualizarGravedad(); + } + + partial void OnGravedadYChanged(float value) + { + ActualizarGravedad(); + } + + // Estadísticas de la simulación + [ObservableProperty] + [property: Description("Número de partículas")] + [property: Category("Estadísticas:")] + private int numeroParticulas; + + [ObservableProperty] + [property: Description("Rendimiento en FPS")] + [property: Category("Estadísticas:")] + private double fps; + + // Referencia a componentes (solo para la función Add) + private List _contenedores = new List(); + + // Nombre de la clase para identificación + public static string NombreClase() + { + return "SistemaFluidos"; + } + + private string nombre = NombreClase(); + public override string Nombre + { + get => nombre; + set => SetProperty(ref nombre, value); + } + + // Métodos para interactuar con la simulación + + /// + /// Agrega partículas en un punto específico + /// + public void AgregarParticula(Vector2 posicion) + { + _simFluidos?.AgregarParticula(posicion); + } + + /// + /// Agrega múltiples partículas en un área + /// + public void AgregarParticulasEnArea(Vector2 centro, float ancho, float alto, int cantidad) + { + _simFluidos?.AgregarParticulasEnArea(centro, ancho, alto, cantidad); + } + + /// + /// Crea un nuevo tanque y lo agrega a la simulación + /// + public Tanque CrearTanque(Vector2 posicion, float ancho, float alto) + { + if (_simFluidos == null) return null; + + Tanque tanque = new Tanque(posicion, ancho, alto, (int)(AnchoSimulacion * 100)); + _simFluidos.AgregarContenedor(tanque); + _contenedores.Add(tanque); + return tanque; + } + + /// + /// Crea una nueva tubería + /// + public Tuberia CrearTuberia(float diametro) + { + if (_simFluidos == null) return null; + + Tuberia tuberia = new Tuberia(diametro, (int)(AnchoSimulacion * 100)); + _simFluidos.AgregarContenedor(tuberia); + _contenedores.Add(tuberia); + return tuberia; + } + + /// + /// Crea una nueva válvula + /// + public Valvula CrearValvula(Vector2 posicion, float diametro, float apertura = 1.0f) + { + if (_simFluidos == null) return null; + + Valvula valvula = new Valvula(posicion, diametro, apertura, (int)(AnchoSimulacion * 100)); + _simFluidos.AgregarContenedor(valvula); + _contenedores.Add(valvula); + return valvula; + } + + /// + /// Elimina un componente de la simulación + /// + public void EliminarComponente(IContenedorFluido componente) + { + if (_simFluidos == null || componente == null) return; + + _simFluidos.RemoverContenedor(componente); + _contenedores.Remove(componente); + } + + /// + /// Limpia todas las partículas de la simulación + /// + public void LimpiarParticulas() + { + _simFluidos?.LimpiarParticulas(); + } + + /// + /// Actualiza el vector de gravedad según las propiedades + /// + private void ActualizarGravedad() + { + if (_simFluidos != null) + { + _simFluidos.AjustarGravedad(new Vector2(GravedadX, GravedadY)); + } + } + + /// + /// Constructor de la clase + /// + public osSistemaFluidos() + { + // Inicializar propiedades básicas + Ancho = 1.0f; + Alto = 1.0f; + } + + // Métodos sobrescritos de osBase + + public override void UpdateGeometryStart() + { + // Crear la simulación de fluidos si es necesario + if (_simFluidos == null) + { + _simFluidos = new SimulacionFluidos( + AnchoSimulacion, + AltoSimulacion, + 10000, // Máximo de partículas + new Vector2(GravedadX, GravedadY) + ); + } + } + + public override void UpdateGeometryStep() + { + // No es necesario actualizar en cada paso + } + + public override void UpdateControl(int elapsedMilliseconds) + { + // Actualizar estadísticas + if (_simFluidos != null) + { + NumeroParticulas = _simFluidos.ParticlesCount; + } + } + + public override void SimulationStop() + { + // Limpiar recursos si es necesario cuando se detiene la simulación + } + + public override void ucLoaded() + { + base.ucLoaded(); + + // Inicializar la simulación de fluidos si es necesario + UpdateGeometryStart(); + } + + public override void ucUnLoaded() + { + // Limpiar recursos + _simFluidos = null; + _contenedores.Clear(); + } + + // Implementación para las conexiones con PLC + + [ObservableProperty] + [property: Description("Tag de lectura/escritura del nivel del Tanque 1")] + [property: Category("PLC:")] + private string tagNivelTanque1; + + [ObservableProperty] + [property: Description("Tag de lectura/escritura de la apertura de la Válvula 1")] + [property: Category("PLC:")] + private string tagAperturaValvula1; + + // Referencia a componentes típicos para integración con PLC + private Tanque _tanque1; + private Valvula _valvula1; + + public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds) + { + // Ejemplo de integración con PLC para la válvula + if (_valvula1 != null && !string.IsNullOrEmpty(TagAperturaValvula1)) + { + float aperturaValvula = LeerWordTagScaled(TagAperturaValvula1) / 100.0f; + _valvula1.Apertura = Math.Clamp(aperturaValvula, 0, 1); + } + + // Ejemplo de escritura del nivel del tanque al PLC + if (_tanque1 != null && !string.IsNullOrEmpty(TagNivelTanque1)) + { + float nivelTanque = 0; // Implementar cálculo real del nivel + EscribirWordTagScaled(TagNivelTanque1, nivelTanque * 100, 0, 100, 0, 27648); + } + } + } +} diff --git a/ObjetosSim/ucSistemaFluidos.xaml b/ObjetosSim/ucSistemaFluidos.xaml new file mode 100644 index 0000000..feb4e42 --- /dev/null +++ b/ObjetosSim/ucSistemaFluidos.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ObjetosSim/ucSistemaFluidos.xaml.cs b/ObjetosSim/ucSistemaFluidos.xaml.cs new file mode 100644 index 0000000..7a2c27f --- /dev/null +++ b/ObjetosSim/ucSistemaFluidos.xaml.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using CtrEditor.FuncionesBase; +using CtrEditor.Simulacion.Fluids; +using tainicom.Aether.Physics2D.Fluids; + +namespace CtrEditor.ObjetosSim +{ + /// + /// Lógica para ucSistemaFluidos.xaml + /// + public partial class ucSistemaFluidos : UserControl, IDataContainer + { + public osBase? Datos { get; set; } + public int zIndex_fromFrames { get; set; } + + // Host para el sistema visual de partículas + private ParticulasFluidoHost _hostVisual; + + // Timer para medir FPS + private DateTime _ultimaActualizacion = DateTime.Now; + private int _contadorFrames = 0; + private double _fps = 0; + + public ucSistemaFluidos() + { + InitializeComponent(); + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + + // Inicializar el host visual y agregarlo al contenedor + _hostVisual = new ParticulasFluidoHost(); + ContenedorVisual.Children.Add(_hostVisual); + + // Configurar actualizaciones periódicas para FPS + CompositionTarget.Rendering += OnRendering; + } + + private void OnRendering(object sender, EventArgs e) + { + _contadorFrames++; + + // Calcular FPS cada segundo + TimeSpan elapsed = DateTime.Now - _ultimaActualizacion; + if (elapsed.TotalSeconds >= 1) + { + _fps = _contadorFrames / elapsed.TotalSeconds; + _contadorFrames = 0; + _ultimaActualizacion = DateTime.Now; + + if (Datos is osSistemaFluidos vm) + { + vm.Fps = _fps; + } + } + + // Actualizar la visualización de partículas + ActualizarVisualizacion(); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Datos?.ucLoaded(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Datos?.ucUnLoaded(); + CompositionTarget.Rendering -= OnRendering; + } + + public void Highlight(bool State) { } + + public ZIndexEnum ZIndex_Base() + { + return ZIndexEnum.Dinamicos; + } + + /// + /// Actualiza la visualización de partículas + /// + private void ActualizarVisualizacion() + { + if (Datos is osSistemaFluidos viewModel) + { + // Obtener la simulación activa desde el ViewModel + var simulacion = viewModel._simFluidos?.SistemaFluido; + if (simulacion != null) + { + // Configurar propiedades visuales + float tamañoParticula = viewModel.TamañoParticula; + Color colorFluido = viewModel.ColorFluido; + + // Actualizar la visualización + _hostVisual.ActualizarParticulasFluido(simulacion.Particles, tamañoParticula, colorFluido, viewModel.OpacidadParticulas); + } + } + } + } + + /// + /// Clase que maneja el rendering optimizado de partículas usando DrawingVisual + /// + public class ParticulasFluidoHost : FrameworkElement + { + private readonly DrawingVisual _visual = new DrawingVisual(); + private readonly MeterToPixelConverter _converter = new MeterToPixelConverter(); + + public ParticulasFluidoHost() + { + AddVisualChild(_visual); + } + + protected override int VisualChildrenCount => 1; + + protected override Visual GetVisualChild(int index) + { + if (index != 0) + throw new ArgumentOutOfRangeException(); + + return _visual; + } + + /// + /// Actualiza la visualización de las partículas de fluido + /// + /// Lista de partículas a visualizar + /// Tamaño de las partículas en metros + /// Color base del fluido + /// Opacidad base de las partículas + public void ActualizarParticulasFluido(IEnumerable particulas, + float tamañoParticula, + Color colorFluido, + double opacidadBase) + { + using (DrawingContext dc = _visual.RenderOpen()) + { + foreach (var particula in particulas) + { + // Ignorar partículas inactivas u ocultas + if (particula == null) + continue; + + // Convertir posición a píxeles + float x = (float)_converter.Convert((particula.Position.X / 100) + 5, null, null, null); + float y = (float)_converter.Convert(particula.Position.Y / 100, null, null, null); + float radio = (float)_converter.Convert(tamañoParticula / 2, null, null, null); + + // Calcular opacidad basada en densidad + double opacidad = Math.Clamp(particula.Density / 15.0, 0.4, 0.9) * opacidadBase; + + // Calcular color basado en velocidad (opcional) + Color colorFinal = colorFluido; + double velocidad = Math.Sqrt(particula.Velocity.X * particula.Velocity.X + + particula.Velocity.Y * particula.Velocity.Y); + + if (velocidad > 50) + { + // Partículas más rápidas tienden a ser más claras + float factor = Math.Min(1.0f, (float)velocidad / 400); + colorFinal = MezclarColores(colorFluido, Colors.White, factor); + } + + SolidColorBrush brush = new SolidColorBrush(colorFinal) { Opacity = opacidad }; + + // Dibujar partícula + dc.DrawEllipse(brush, null, new Point(x, y), radio, radio); + } + } + + InvalidateVisual(); + } + + /// + /// Mezcla dos colores según un factor (0-1) + /// + private Color MezclarColores(Color color1, Color color2, float factor) + { + byte r = (byte)(color1.R + (color2.R - color1.R) * factor); + byte g = (byte)(color1.G + (color2.G - color1.G) * factor); + byte b = (byte)(color1.B + (color2.B - color1.B) * factor); + return Color.FromRgb(r, g, b); + } + } +} diff --git a/ObjetosSim/ucTuberiaFluido.xaml b/ObjetosSim/ucTuberiaFluido.xaml new file mode 100644 index 0000000..cf5e2d7 --- /dev/null +++ b/ObjetosSim/ucTuberiaFluido.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/ObjetosSim/ucTuberiaFluido.xaml.cs b/ObjetosSim/ucTuberiaFluido.xaml.cs new file mode 100644 index 0000000..0502a90 --- /dev/null +++ b/ObjetosSim/ucTuberiaFluido.xaml.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using CtrEditor.FuncionesBase; +using CtrEditor.Simulacion.Fluids.Components; +using nkast.Aether.Physics2D.Common; +using CommunityToolkit.Mvvm.ComponentModel; +using System.ComponentModel; + +namespace CtrEditor.ObjetosSim +{ + /// + /// Lógica para ucTuberiaFluido.xaml + /// + public partial class ucTuberiaFluido : UserControl, IDataContainer + { + public osBase? Datos { get; set; } + public int zIndex_fromFrames { get; set; } + + public ucTuberiaFluido() + { + InitializeComponent(); + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Datos?.ucLoaded(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Datos?.ucUnLoaded(); + } + + public void Highlight(bool State) { } + + public ZIndexEnum ZIndex_Base() + { + return ZIndexEnum.Estaticos; + } + } + + /// + /// ViewModel para la tubería de fluidos + /// + public partial class osTuberiaFluido : osBase, IosBase + { + // Tubería en la simulación de fluidos + private Tuberia _tuberia; + + // Referencia al sistema de fluidos + private osSistemaFluidos _sistemaFluidos; + + // Propiedades visuales + [ObservableProperty] + [property: Description("Diámetro de la tubería en metros")] + [property: Category("Dimensiones:")] + private float diametro = 0.05f; + + partial void OnDiametroChanged(float value) + { + // Actualizar geometría si la tubería ya existe + if (_tuberia != null) + { + // No es posible cambiar el diámetro directamente, recrear la tubería + ReconstruirTuberia(); + } + } + + [ObservableProperty] + [property: Description("Diámetro interno para visualización del fluido")] + [property: Category("Visual:")] + private float diametroInterno; + + [ObservableProperty] + [property: Description("Color de la tubería")] + [property: Category("Visual:")] + private System.Windows.Media.Color color = System.Windows.Media.Colors.Gray; + + [ObservableProperty] + [property: Description("Color del fluido")] + [property: Category("Visual:")] + private System.Windows.Media.Color colorFluido = System.Windows.Media.Colors.CornflowerBlue; + + [ObservableProperty] + [property: Description("Datos del path para dibujar la tubería")] + [property: Category("Interno:")] + private string pathData; + + [ObservableProperty] + [property: Description("Densidad del fluido (0-1)")] + [property: Category("Simulación:")] + private double densidadFluido = 0.7; + + // Lista de puntos que forman la tubería + private List _puntos = new List(); + + // Nombre de la clase para identificación + public static string NombreClase() + { + return "TuberiaFluido"; + } + + private string nombre = NombreClase(); + public override string Nombre + { + get => nombre; + set => SetProperty(ref nombre, value); + } + + /// + /// Agrega un punto a la tubería + /// + public void AgregarPunto(float x, float y) + { + _puntos.Add(new Vector2(x, y)); + ActualizarPathData(); + + // Si la tubería ya está creada, agregar el punto a la simulación + if (_tuberia != null) + { + _tuberia.AgregarPunto(new Vector2(x, y)); + } + } + + /// + /// Actualiza la representación visual de la tubería + /// + private void ActualizarPathData() + { + if (_puntos.Count < 2) return; + + var converter = new MeterToPixelConverter(); + StringBuilder sb = new StringBuilder(); + + // Iniciar el path + sb.Append($"M {converter.Convert(_puntos[0].X, null, null, null)} {converter.Convert(_puntos[0].Y, null, null, null)} "); + + // Añadir los demás puntos + for (int i = 1; i < _puntos.Count; i++) + { + sb.Append($"L {converter.Convert(_puntos[i].X, null, null, null)} {converter.Convert(_puntos[i].Y, null, null, null)} "); + } + + PathData = sb.ToString(); + } + + /// + /// Reconstruye la tubería en la simulación + /// + private void ReconstruirTuberia() + { + if (_sistemaFluidos != null) + { + // Eliminar la tubería existente + if (_tuberia != null) + { + _sistemaFluidos.EliminarComponente(_tuberia); + } + + // Crear nueva tubería + _tuberia = _sistemaFluidos.CrearTuberia(Diametro); + + // Agregar todos los puntos existentes + foreach (var punto in _puntos) + { + _tuberia.AgregarPunto(punto); + } + } + } + + /// + /// Constructor + /// + public osTuberiaFluido() + { + DiametroInterno = Diametro * 0.8f; + } + + public override void OnMove(float LeftPixels, float TopPixels) + { + // Mover todos los puntos de la tubería + if (_puntos.Count > 0) + { + Vector2 delta = new Vector2(Left - CanvasGetLeftinMeter(), Top - CanvasGetTopinMeter()); + + for (int i = 0; i < _puntos.Count; i++) + { + _puntos[i] += delta; + } + + ActualizarPathData(); + ReconstruirTuberia(); + } + } + + public override void ucLoaded() + { + base.ucLoaded(); + + // Buscar el sistema de fluidos en la simulación + if (_mainViewModel?.ObjetosSimulables != null) + { + foreach (var obj in _mainViewModel.ObjetosSimulables) + { + if (obj is osSistemaFluidos sistemaFluidos) + { + _sistemaFluidos = sistemaFluidos; + break; + } + } + } + + // Si no hay puntos, agregar dos puntos iniciales + if (_puntos.Count == 0) + { + _puntos.Add(new Vector2(Left, Top)); + _puntos.Add(new Vector2(Left + Ancho, Top)); + ActualizarPathData(); + } + + // Crear la tubería en la simulación + ReconstruirTuberia(); + } + + public override void ucUnLoaded() + { + // Eliminar la tubería de la simulación + if (_sistemaFluidos != null && _tuberia != null) + { + _sistemaFluidos.EliminarComponente(_tuberia); + _tuberia = null; + } + } + + public override void UpdateControl(int elapsedMilliseconds) + { + // Actualizar la densidad del fluido basada en la simulación + if (_sistemaFluidos != null && _sistemaFluidos._simFluidos != null && _puntos.Count > 0) + { + // Calcular el centro aproximado de la tubería + Vector2 centro = Vector2.Zero; + foreach (var punto in _puntos) + { + centro += punto; + } + centro /= _puntos.Count; + + // Obtener densidad en esa posición + DensidadFluido = _sistemaFluidos._simFluidos.ObtenerDensidadEnPosicion(centro); + } + } + } +} diff --git a/ObjetosSim/ucValvulaFluido.xaml b/ObjetosSim/ucValvulaFluido.xaml new file mode 100644 index 0000000..3a4c768 --- /dev/null +++ b/ObjetosSim/ucValvulaFluido.xaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ObjetosSim/ucValvulaFluido.xaml.cs b/ObjetosSim/ucValvulaFluido.xaml.cs new file mode 100644 index 0000000..1c4829f --- /dev/null +++ b/ObjetosSim/ucValvulaFluido.xaml.cs @@ -0,0 +1,206 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using CtrEditor.FuncionesBase; +using CtrEditor.Simulacion.Fluids.Components; +using nkast.Aether.Physics2D.Common; +using CommunityToolkit.Mvvm.ComponentModel; +using Color = System.Windows.Media.Color; +using System.ComponentModel; +using LibS7Adv; + +namespace CtrEditor.ObjetosSim +{ + /// + /// Lógica para ucValvulaFluido.xaml + /// + public partial class ucValvulaFluido : UserControl, IDataContainer + { + public osBase? Datos { get; set; } + public int zIndex_fromFrames { get; set; } + + public ucValvulaFluido() + { + InitializeComponent(); + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Datos?.ucLoaded(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Datos?.ucUnLoaded(); + } + + public void Highlight(bool State) { } + + public ZIndexEnum ZIndex_Base() + { + return ZIndexEnum.Estaticos; + } + } + + /// + /// ViewModel para la válvula de fluidos + /// + public partial class osValvulaFluido : osBase, IosBase + { + // Válvula en la simulación de fluidos + private Valvula _valvula; + + // Referencia al sistema de fluidos + private osSistemaFluidos _sistemaFluidos; + + // Propiedades dimensionales + [ObservableProperty] + [property: Category("Dimensiones:")] + [property: Description("Ancho de la válvula en metros")] + private float ancho = 0.1f; + + [ObservableProperty] + [property: Description("Alto de la válvula en metros")] + [property: Category("Dimensiones:")] + private float alto = 0.06f; + + [ObservableProperty] + [property: Description("Diámetro de la tubería conectada")] + [property: Category("Dimensiones:")] + private float diametroTuberia = 0.05f; + + // Propiedades visuales + [ObservableProperty] + [property: Description("Color de la válvula")] + [property: Category("Visual:")] + private Color color = Colors.Silver; + + [ObservableProperty] + [property: Description("Color del indicador de apertura")] + [property: Category("Visual:")] + private Color colorIndicador = Colors.CornflowerBlue; + + // Propiedades de funcionamiento + [ObservableProperty] + [property: Description("Apertura de la válvula (0-1)")] + [property: Category("Operación:")] + private float apertura = 1.0f; + + partial void OnAperturaChanged(float value) + { + Apertura = Math.Clamp(value, 0, 1); + + // Actualizar la válvula en la simulación + if (_valvula != null) + { + _valvula.Apertura = Apertura; + } + } + + // Tag PLC para la válvula + [ObservableProperty] + [property: Description("Tag de lectura/escritura de la apertura (0-100%)")] + [property: Category("PLC:")] + private string tagApertura; + + // Propiedades calculadas para visualización + public float AperturaVisual => Ancho * 0.8f * Apertura; + public float GrosorIndicador => Alto * 0.2f; + public float ValorApertura => Apertura; + + // Propiedades para posicionamiento de elementos + public float OffsetXRectangulo => -Ancho / 2; + public float OffsetYRectangulo => -Alto / 2; + public float OffsetXIndicador => -Ancho * 0.4f; + public float OffsetYIndicador => -GrosorIndicador / 2; + public float OffsetXTexto => -Ancho * 0.15f; + public float OffsetYTexto => Alto * 0.1f; + + // Nombre de la clase para identificación + public static string NombreClase() + { + return "ValvulaFluido"; + } + + private string nombre = NombreClase(); + public override string Nombre + { + get => nombre; + set => SetProperty(ref nombre, value); + } + + /// + /// Constructor + /// + public osValvulaFluido() + { + // Constructor + } + + public override void OnMove(float LeftPixels, float TopPixels) + { + if (_valvula != null) + { + // Actualizar posición de la válvula en la simulación + _valvula.ActualizarPosicion(new Vector2(Left, Top)); + } + } + + public override void OnRotate(float Delta_Angle) + { + // La rotación visual ya está manejada por el XAML + // No necesita actualizaciones adicionales en la simulación + } + + public override void ucLoaded() + { + base.ucLoaded(); + + // Buscar el sistema de fluidos en la simulación + if (_mainViewModel?.ObjetosSimulables != null) + { + foreach (var obj in _mainViewModel.ObjetosSimulables) + { + if (obj is osSistemaFluidos sistemaFluidos) + { + _sistemaFluidos = sistemaFluidos; + break; + } + } + } + + // Crear la válvula en la simulación + if (_sistemaFluidos != null) + { + _valvula = _sistemaFluidos.CrearValvula( + new Vector2(Left, Top), + DiametroTuberia, + Apertura + ); + } + } + + public override void ucUnLoaded() + { + // Eliminar la válvula de la simulación + if (_sistemaFluidos != null && _valvula != null) + { + _sistemaFluidos.EliminarComponente(_valvula); + _valvula = null; + } + } + + public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds) + { + // Manejar la comunicación con PLC + if (!string.IsNullOrEmpty(TagApertura)) + { + float aperturaPlc = LeerWordTagScaled(TagApertura) / 100.0f; + Apertura = Math.Clamp(aperturaPlc, 0, 1); + } + } + } +} diff --git a/Simulacion/Fluids/Components/ComponentesFluidos.cs b/Simulacion/Fluids/Components/ComponentesFluidos.cs new file mode 100644 index 0000000..5712119 --- /dev/null +++ b/Simulacion/Fluids/Components/ComponentesFluidos.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Generic; +using nkast.Aether.Physics2D.Common; +using tainicom.Aether.Physics2D.Fluids; + +namespace CtrEditor.Simulacion.Fluids.Components +{ + /// + /// Clase base para todos los componentes de fluidos + /// + public abstract class ComponenteFluido : IContenedorFluido + { + public Vector2 Posicion { get; set; } + public bool EsActivo { get; set; } = true; + + /// + /// Convierte una posición de metros/WPF a sistema de coordenadas de FluidSystem2 + /// + protected Vector2 ConvertirPosicion(Vector2 posicion, int worldWidth) + { + return new Vector2( + posicion.X * 100 - worldWidth/2, + posicion.Y * 100 + ); + } + + /// + /// Implementación en clases derivadas para restricciones específicas + /// + public abstract void RestringirParticula(Particle particula); + } + + /// + /// Tanque para contener fluidos + /// + public class Tanque : ComponenteFluido + { + public float Ancho { get; set; } + public float Alto { get; set; } + private int _worldWidth; + + // Coordenadas ajustadas al sistema de FluidSystem2 + private float _xMin, _xMax, _yMin, _yMax; + private float _coefRebote = 0.3f; // Factor de rebote en colisiones + + /// + /// Constructor para un tanque + /// + /// Posición del centro del tanque + /// Ancho del tanque + /// Alto del tanque + /// Ancho del mundo en cm (para conversión de coordenadas) + public Tanque(Vector2 posicion, float ancho, float alto, int worldWidth) + { + Posicion = posicion; + Ancho = ancho; + Alto = alto; + _worldWidth = worldWidth; + + ActualizarLimites(); + } + + /// + /// Actualiza los límites internos del tanque después de cambiar la posición o tamaño + /// + public void ActualizarLimites() + { + // Convertir a coordenadas internas + _xMin = (Posicion.X - Ancho/2) * 100 - _worldWidth/2; + _xMax = (Posicion.X + Ancho/2) * 100 - _worldWidth/2; + _yMin = Posicion.Y * 100; + _yMax = (Posicion.Y + Alto) * 100; + } + + /// + /// Restringe las partículas para que permanezcan dentro del tanque + /// + public override void RestringirParticula(Particle particula) + { + if (!EsActivo) return; + + // Comprobar si la partícula está dentro del área del tanque + bool dentroX = particula.Position.X >= _xMin && particula.Position.X <= _xMax; + bool dentroY = particula.Position.Y >= _yMin && particula.Position.Y <= _yMax; + + if (dentroX && dentroY) + { + // La partícula está dentro del tanque, comprobar si está cerca de los bordes + if (particula.Position.X < _xMin + 5) + { + particula.Position.X = _xMin + 5; + particula.Velocity.X *= -_coefRebote; + } + else if (particula.Position.X > _xMax - 5) + { + particula.Position.X = _xMax - 5; + particula.Velocity.X *= -_coefRebote; + } + + if (particula.Position.Y < _yMin + 5) + { + particula.Position.Y = _yMin + 5; + particula.Velocity.Y *= -_coefRebote; + } + else if (particula.Position.Y > _yMax - 5) + { + particula.Position.Y = _yMax - 5; + particula.Velocity.Y *= -_coefRebote; + } + } + } + + /// + /// Obtiene el nivel de llenado del tanque en porcentaje + /// + /// Instancia activa de la simulación + /// Porcentaje de llenado (0-1) + public float ObtenerNivelLlenado() + { + // Sería necesario acceder a las partículas para calcular esto + // Implementación en clases derivadas específicas + return 0.0f; + } + } + + /// + /// Representa un segmento de tubo para fluidos + /// + public class SegmentoTubo + { + public Vector2 Inicio { get; } + public Vector2 Fin { get; } + public float Radio { get; } + + public SegmentoTubo(Vector2 inicio, Vector2 fin, float radio) + { + Inicio = inicio; + Fin = fin; + Radio = radio; + } + + /// + /// Contiene y restringe el movimiento de una partícula dentro del tubo + /// + /// Verdadero si se aplicó alguna restricción + public bool ContenerParticula(Particle particula) + { + // Calcular distancia de la partícula al segmento de línea + Vector2 pos = new Vector2(particula.Position.X, particula.Position.Y); + float distancia = DistanciaPuntoALinea(pos, Inicio, Fin); + + // Si la distancia es mayor que el radio, aplicar restricción + if (distancia > Radio) + { + // Calcular punto más cercano en el segmento + Vector2 puntoMasCercano = PuntoMasCercanoEnLinea(pos, Inicio, Fin); + + // Dirección desde partícula hacia punto más cercano en el centro del tubo + Vector2 direccion = Vector2.Normalize(puntoMasCercano - pos); + + // Mover la partícula al interior del tubo + particula.Position.X = puntoMasCercano.X - direccion.X * Radio * 0.9f; + particula.Position.Y = puntoMasCercano.Y - direccion.Y * Radio * 0.9f; + + // Ajustar velocidad para simular rebote + float velocidadNormal = Vector2.Dot( + new Vector2(particula.Velocity.X, particula.Velocity.Y), + direccion); + + particula.Velocity.X -= direccion.X * velocidadNormal * 1.8f; + particula.Velocity.Y -= direccion.Y * velocidadNormal * 1.8f; + + return true; + } + + return false; // La partícula está dentro del tubo + } + + /// + /// Calcula la distancia de un punto a una línea + /// + private float DistanciaPuntoALinea(Vector2 punto, Vector2 lineaInicio, Vector2 lineaFin) + { + // Vector que representa la dirección de la línea + Vector2 linea = lineaFin - lineaInicio; + float longitud = linea.Length(); + + if (longitud < 0.0001f) + return Vector2.Distance(punto, lineaInicio); // Es un punto, no una línea + + // Normalizar el vector de la línea + Vector2 lineaNormalizada = linea / longitud; + + // Vector desde el inicio de la línea hasta el punto + Vector2 puntoDesdeInicio = punto - lineaInicio; + + // Proyectar puntoDesdeInicio sobre la línea + float proyeccion = Vector2.Dot(puntoDesdeInicio, lineaNormalizada); + + // Limitar la proyección al segmento de línea + proyeccion = Math.Max(0, Math.Min(longitud, proyeccion)); + + // Punto más cercano en la línea + Vector2 puntoMasCercano = lineaInicio + lineaNormalizada * proyeccion; + + // Distancia desde el punto hasta el punto más cercano + return Vector2.Distance(punto, puntoMasCercano); + } + + /// + /// Calcula el punto más cercano en una línea desde un punto dado + /// + private Vector2 PuntoMasCercanoEnLinea(Vector2 punto, Vector2 lineaInicio, Vector2 lineaFin) + { + // Vector que representa la dirección de la línea + Vector2 linea = lineaFin - lineaInicio; + float longitud = linea.Length(); + + if (longitud < 0.0001f) + return lineaInicio; // Es un punto, no una línea + + // Normalizar el vector de la línea + Vector2 lineaNormalizada = linea / longitud; + + // Vector desde el inicio de la línea hasta el punto + Vector2 puntoDesdeInicio = punto - lineaInicio; + + // Proyectar puntoDesdeInicio sobre la línea + float proyeccion = Vector2.Dot(puntoDesdeInicio, lineaNormalizada); + + // Limitar la proyección al segmento de línea + proyeccion = Math.Max(0, Math.Min(longitud, proyeccion)); + + // Punto más cercano en la línea + return lineaInicio + lineaNormalizada * proyeccion; + } + } + + /// + /// Tubería para conducir fluidos entre puntos + /// + public class Tuberia : ComponenteFluido + { + private List _segmentos = new List(); + private List _puntos = new List(); + public float Diametro { get; private set; } + private int _worldWidth; + + /// + /// Constructor para una tubería + /// + /// Diámetro de la tubería + /// Ancho del mundo en cm (para conversión de coordenadas) + public Tuberia(float diametro, int worldWidth) + { + Diametro = diametro; + _worldWidth = worldWidth; + } + + /// + /// Agrega un punto a la tubería, creando segmentos automáticamente + /// + public void AgregarPunto(Vector2 punto) + { + // Convertir punto a coordenadas internas + Vector2 puntoAjustado = ConvertirPosicion(punto, _worldWidth); + + // Si ya hay puntos, crear un segmento con el punto anterior + if (_puntos.Count > 0) + { + _segmentos.Add(new SegmentoTubo( + _puntos[_puntos.Count - 1], + puntoAjustado, + Diametro * 100 / 2 // Radio en unidades internas + )); + } + + _puntos.Add(puntoAjustado); + } + + /// + /// Restringe las partículas para que permanezcan dentro de la tubería + /// + public override void RestringirParticula(Particle particula) + { + if (!EsActivo) return; + + // Comprobar si la partícula está cerca de la tubería + Vector2 posParticula = new Vector2(particula.Position.X, particula.Position.Y); + float distanciaMinima = float.MaxValue; + SegmentoTubo segmentoMasCercano = null; + + // Encontrar el segmento más cercano + foreach (var segmento in _segmentos) + { + float distancia = DistanciaPuntoASegmento(posParticula, segmento.Inicio, segmento.Fin); + if (distancia < distanciaMinima) + { + distanciaMinima = distancia; + segmentoMasCercano = segmento; + } + } + + // Aplicar restricciones si hay un segmento cercano + if (segmentoMasCercano != null) + { + segmentoMasCercano.ContenerParticula(particula); + } + } + + /// + /// Calcula la distancia desde un punto a un segmento de línea + /// + private float DistanciaPuntoASegmento(Vector2 punto, Vector2 segmentoInicio, Vector2 segmentoFin) + { + // Vector dirección del segmento + Vector2 segmento = segmentoFin - segmentoInicio; + float longitudCuadrada = segmento.LengthSquared(); + + if (longitudCuadrada < 0.0001f) + return Vector2.Distance(punto, segmentoInicio); // Es un punto, no un segmento + + // Calcular proyección del punto sobre el segmento + float t = Vector2.Dot(punto - segmentoInicio, segmento) / longitudCuadrada; + t = Math.Max(0, Math.Min(1, t)); + + // Punto más cercano en el segmento + Vector2 proyeccion = segmentoInicio + t * segmento; + + // Distancia desde el punto hasta la proyección + return Vector2.Distance(punto, proyeccion); + } + } + + /// + /// Válvula para controlar el flujo de fluidos + /// + public class Valvula : ComponenteFluido + { + public float Apertura { get; set; } // 0.0 = cerrada, 1.0 = abierta + public float Diametro { get; set; } + private int _worldWidth; + private Vector2 _posicionAjustada; + + /// + /// Constructor para una válvula + /// + /// Posición de la válvula + /// Diámetro del conducto de la válvula + /// Apertura inicial (0-1) + /// Ancho del mundo en cm (para conversión de coordenadas) + public Valvula(Vector2 posicion, float diametro, float apertura, int worldWidth) + { + Posicion = posicion; + Diametro = diametro; + Apertura = Math.Clamp(apertura, 0, 1); + _worldWidth = worldWidth; + + _posicionAjustada = ConvertirPosicion(posicion, worldWidth); + } + + /// + /// Actualiza la posición ajustada después de cambiar la posición + /// + public void ActualizarPosicion(Vector2 nuevaPosicion) + { + Posicion = nuevaPosicion; + _posicionAjustada = ConvertirPosicion(nuevaPosicion, _worldWidth); + } + + /// + /// Restringe y modula el flujo de partículas a través de la válvula + /// + public override void RestringirParticula(Particle particula) + { + if (!EsActivo) return; + + // Calcular distancia al centro de la válvula + Vector2 posParticula = new Vector2(particula.Position.X, particula.Position.Y); + float distancia = Vector2.Distance(_posicionAjustada, posParticula); + float radioValvula = Diametro * 100 / 2; // Radio en unidades internas + + // Verificar si la partícula está dentro del radio de acción de la válvula + if (distancia < radioValvula * 1.2) + { + // Si la válvula está cerrada o casi cerrada, bloquear paso + if (Apertura < 0.05f) + { + // Rechazar partícula + Vector2 direccion = Vector2.Normalize(posParticula - _posicionAjustada); + particula.Position.X = _posicionAjustada.X + direccion.X * (radioValvula * 1.2f); + particula.Position.Y = _posicionAjustada.Y + direccion.Y * (radioValvula * 1.2f); + + // Reducir velocidad significativamente + particula.Velocity.X *= 0.1f; + particula.Velocity.Y *= 0.1f; + } + // Si está parcialmente abierta, reducir velocidad proporcionalmente + else if (Apertura < 0.9f) + { + // Calcular factor de reducción basado en la apertura + float factorReduccion = Apertura * Apertura; // Relación no lineal + + // Aplicar resistencia proporcional a la apertura + particula.Velocity.X *= factorReduccion; + particula.Velocity.Y *= factorReduccion; + } + } + } + } +} diff --git a/Simulacion/Fluids/SimulacionFluidos.cs b/Simulacion/Fluids/SimulacionFluidos.cs new file mode 100644 index 0000000..a642319 --- /dev/null +++ b/Simulacion/Fluids/SimulacionFluidos.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using nkast.Aether.Physics2D.Common; +using tainicom.Aether.Physics2D.Fluids; + +namespace CtrEditor.Simulacion.Fluids +{ + /// + /// Clase principal que gestiona la simulación de fluidos basada en FluidSystem2 + /// + public class SimulacionFluidos + { + public FluidSystem2 SistemaFluido { get; private set; } + private List _contenedores = new List(); + private int _worldWidth; + private int _worldHeight; + + public int ParticlesCount => SistemaFluido?.ParticlesCount ?? 0; + + /// + /// Constructor para el sistema de simulación de fluidos + /// + /// Ancho del área de simulación en metros + /// Alto del área de simulación en metros + /// Número máximo de partículas + /// Vector de gravedad + public SimulacionFluidos(float ancho, float alto, int maxParticulas, Vector2 gravedad) + { + // Convertir parámetros a unidades internas de FluidSystem2 + _worldWidth = (int)(ancho * 100); // Convertir a cm + _worldHeight = (int)(alto * 100); // Convertir a cm + + // Inicializar el sistema + SistemaFluido = new FluidSystem2( + gravedad, + maxParticulas, + _worldWidth, + _worldHeight + ); + + // Configurar comportamiento del fluido + SistemaFluido.ElasticityEnabled = true; + SistemaFluido.PlasticityEnabled = true; + } + + /// + /// Método para agregar una partícula al sistema + /// + /// Posición en metros + public void AgregarParticula(Vector2 posicion) + { + // Convertir a sistema de coordenadas de FluidSystem2 + float xAjustado = posicion.X * 100 - _worldWidth/2; + float yAjustado = posicion.Y * 100; + + SistemaFluido.AddParticle(new Vector2(xAjustado, yAjustado)); + } + + /// + /// Agrega múltiples partículas en un área rectangular + /// + /// Centro del área + /// Ancho del área + /// Alto del área + /// Cantidad de partículas a agregar + public void AgregarParticulasEnArea(Vector2 centro, float ancho, float alto, int cantidad) + { + Random rnd = new Random(); + for (int i = 0; i < cantidad; i++) + { + float x = centro.X - ancho/2 + (float)rnd.NextDouble() * ancho; + float y = centro.Y - alto/2 + (float)rnd.NextDouble() * alto; + AgregarParticula(new Vector2(x, y)); + } + } + + /// + /// Agrega un contenedor al sistema de fluidos + /// + /// Implementación de IContenedorFluido + public void AgregarContenedor(IContenedorFluido contenedor) + { + _contenedores.Add(contenedor); + } + + /// + /// Elimina un contenedor del sistema de fluidos + /// + /// Contenedor a eliminar + public void RemoverContenedor(IContenedorFluido contenedor) + { + _contenedores.Remove(contenedor); + } + + /// + /// Actualiza la simulación avanzando un paso de tiempo + /// + /// Tiempo transcurrido en segundos + public void Actualizar(float deltaTime) + { + // Aplicar restricciones de contenedores personalizados + foreach (var contenedor in _contenedores) + { + for (int i = 0; i < SistemaFluido.ParticlesCount; i++) + { + var particula = SistemaFluido.Particles[i]; + contenedor.RestringirParticula(particula); + } + } + + // Actualizar la física del sistema de fluidos + SistemaFluido.Update(deltaTime); + } + + /// + /// Obtiene la densidad del fluido en una posición específica + /// + /// Posición en metros + /// Densidad relativa de fluido (0-1) + public float ObtenerDensidadEnPosicion(Vector2 posicion) + { + // Convertir a sistema de coordenadas de FluidSystem2 + float xAjustado = posicion.X * 100 - _worldWidth/2; + float yAjustado = posicion.Y * 100; + Vector2 posAjustada = new Vector2(xAjustado, yAjustado); + + float densidadTotal = 0; + int particulasCercanas = 0; + + // Buscar partículas cercanas y sumar sus densidades + foreach (var p in SistemaFluido.Particles) + { + float distancia = Vector2.Distance(posAjustada, p.Position); + if (distancia < FluidSystem2.InfluenceRadius) + { + densidadTotal += p.Density; + particulasCercanas++; + } + } + + // Calcular densidad promedio, normalizada + if (particulasCercanas > 0) + return Math.Min(1.0f, densidadTotal / (particulasCercanas * FluidSystem2.DensityRest * 1.5f)); + + return 0; + } + + /// + /// Ajusta la gravedad del sistema + /// + /// Nuevo vector de gravedad + public void AjustarGravedad(Vector2 gravedad) + { + SistemaFluido.Gravity = gravedad; + } + + /// + /// Limpia todas las partículas del sistema + /// + public void LimpiarParticulas() + { + // No hay método clear() directo en FluidSystem2, recreamos el sistema + SistemaFluido = new FluidSystem2( + SistemaFluido.Gravity, + SistemaFluido.MaxParticleLimit, + _worldWidth, + _worldHeight + ); + + // Restaurar configuración + SistemaFluido.ElasticityEnabled = true; + SistemaFluido.PlasticityEnabled = true; + } + } + + /// + /// Interfaz para contenedores de fluido personalizados como tubos, tanques, etc. + /// + public interface IContenedorFluido + { + /// + /// Aplica restricciones a una partícula para mantenerla dentro o fuera del contenedor + /// + void RestringirParticula(Particle particula); + } +}