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);
+ }
+}