Compare commits

..

No commits in common. "Bepuphysics" and "Aether" have entirely different histories.

66 changed files with 6522 additions and 8577 deletions

View File

@ -1,4 +0,0 @@
---
alwaysApply: true
---
Quisiera que con los conocimientos importantes y desiciones importantes que hemos adquirido y utilizado los agregues a Documentation\MemoriadeEvolucion.md manteniendo el estilo que ya tenemos de texto simple sin demasiado codigo y una semantica resumida.

View File

@ -1,33 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace CtrEditor.Converters
{
public class BooleanToLocalizedTextConverter : IValueConverter
{
public string TrueText { get; set; } = "True";
public string FalseText { get; set; } = "False";
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? TrueText : FalseText;
}
return FalseText;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string text)
{
if (text == TrueText)
return true;
if (text == FalseText)
return false;
}
return false;
}
}
}

View File

@ -5,9 +5,6 @@
},
{
"path": "../Libraries/LibS7Adv"
},
{
"path": "../Librerias/bepuphysics2-master"
}
],
"settings": {}

View File

@ -28,16 +28,11 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="ObjetosSim\Fluids\**" />
<EmbeddedResource Remove="ObjetosSim\Fluids\**" />
<None Remove="ObjetosSim\Fluids\**" />
<Page Remove="ObjetosSim\Fluids\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Documentation\BEPU Forces.cs" />
<Compile Remove="Documentation\PlantillaEstandarizacion.cs" />
<Compile Remove="ObjetosSim\ucBasicExample.xaml.cs" />
<Compile Remove="ObjetosSim\ucTransporteCurva.xaml.cs" />
<Compile Remove="Simulacion\FPhysics.cs" />
<Compile Remove="Simulacion\GeometrySimulator.cs" />
</ItemGroup>
<ItemGroup>
@ -81,24 +76,26 @@
</ItemGroup>
<ItemGroup>
<Page Remove="ObjetosSim\ucBasicExample.xaml" />
<Page Remove="ObjetosSim\ucTransporteCurva.xaml" />
</ItemGroup>
<ItemGroup>
<None Include="Documentation\BEPU Forces.cs" />
<None Include="Documentation\PlantillaEstandarizacion.cs" />
<None Include="ObjetosSim\ucBasicExample.xaml" />
<None Include="ObjetosSim\ucBasicExample.xaml.cs" />
<None Include="Simulacion\FPhysics.cs" />
<None Include="Simulacion\GeometrySimulator.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BepuPhysics" Version="2.5.0-beta.26" />
<PackageReference Include="BepuUtilities" Version="2.5.0-beta.26" />
<PackageReference Include="Aether.Physics2D" Version="2.2.0" />
<PackageReference Include="ClosedXML" Version="0.105.0-rc" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Emgu.CV" Version="4.9.0.5494" />
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.9.0.5494" />
<PackageReference Include="Emgu.CV.UI" Version="4.9.0.5494" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.7.25104.5739" />
<PackageReference Include="HelixToolkit.Wpf" Version="2.27.0" />
<PackageReference Include="LanguageDetection" Version="1.2.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc3.3" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
@ -106,6 +103,8 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="PaddleOCRSharp" Version="4.5.0.1" />
<PackageReference Include="Tesseract" Version="5.2.0" />
<PackageReference Include="Tesseract.Drawing" Version="5.2.0" />
</ItemGroup>
<ItemGroup>
@ -188,6 +187,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="ObjetosSim\Fluids\" />
<Folder Include="paddleocr\cls\inference\" />
<Folder Include="paddleocr\det\inference\" />
<Folder Include="paddleocr\keys\" />

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
### Aqui se mantiene la memoria de evolucion de las distintas decisiones que fueron tomadas y porque
BEPU.cs : SimulationManagerBEPU gestor de la simulacion con el motor BEPUphysics y punto de creacion y modificacion de los objetos dentro del mundo que se derivan des simBase
simBase : Clase base que da un marco a el resto de los objetos
simBarrera : Simula una fotocelula con un espejo usando RayCast
simBotella : Simula una botella que puede transitar por los transportes simTransporte o simCurve
simCurve : Simula una curva o arco de curva de transporte
simDescarte : Permite la eliminacion localizada de botellas en un punto del mundo
simGuia : Es un box con el que las botellas pueden colisionar y cambiar de direccion.
simTransporte : Es un box por donde las botellas pueden desplazarse usando un truco de aplicar la Velocity.Linear pero sin integrar esta velocidad para que no se mueva el objeto transporte.
* Se usaron esferas en vez de cilindros para mejorar la eficiencia. En el Debug3D si se usan cilindros. **Revertido**: Se ha vuelto a usar `Cylinder` para las `simBotella` ya que el nuevo sistema de fricción debería prevenir la rotación indeseada que ocurría con los `LinearAxisMotor`.
* Se reemplazó el sistema de `LinearAxisMotor` que actuaba sobre las `simBotella` por un sistema basado en fricción. Los transportes (`simTransporte` y `simCurve`) ahora son cuerpos cinemáticos que no se mueven de su sitio. Para lograr que arrastren a las botellas, se les asigna una velocidad (`Velocity.Linear` o `Velocity.Angular`) justo antes de que el solver se ejecute (en `OnSubstepStarted`) y se les quita justo después (en `OnSubstepEnded`). Esto permite que los cuerpos cinemáticos transmitan su velocidad a través de la fricción durante la simulación, pero evita que el `PoseIntegrator` los desplace de su posición original, ya que su velocidad es cero cuando se integra la pose.
* Para aumentar la estabilidad de las `simBotella` y evitar que roten descontroladamente sobre su eje Z al ser arrastradas, se implementó un callback `IntegrateVelocity` personalizado. Este callback identifica las botellas durante la integración y les aplica un amortiguamiento angular (`AngularDamping`) adicional solo en el eje Z. Se descartó la idea inicial de modificar dinámicamente la masa de las botellas dentro de este callback, ya que la arquitectura de BEPUphysics no permite cambiar la masa o la inercia de un cuerpo durante la fase de integración de velocidad.
* Originalmente se habia puesto todos los objetos en awaken para poder usar las colisiones constantemente incluso con objetos en modo sleep para que los simBarrera puedan detectar las colisiones. Ahora que se usar RayCast podemos dejar que las simBotellas se duerman
* La unica clase que se ha terminado de refactorizar respecto a el cambio de coordenadas es simBarrera y ucPhotocell. El concepto es poder separar usando metodos de SimulationManagerBEPU y estructuras como BarreraData la conversion de coordenadas de WPF a coordenadas BEPU. Para esto se usa CoordinateConverter que permite bidireccionalmente convertir las coordenadas pero esto solo se debe usar en SimulationManagerBEPU las clases derivadas de osBase solo deben manejar coordenadas WPF, mientras que las clases dervadas de simBase solo deben almacenar y usar coordenadas BEPU. La z tambien es algo que se debe transferir a SimulationManagerBEPU ya que los objetos simBase deberian recibir tambien sus coordenadas de Z desde SimulationManagerBEPU y ser SimulationManagerBEPU el que gestione las Z.
* Se ha implementado un sistema para evitar que las botellas (`simBotella`) "floten" o se eleven de manera irreal por la acumulación de presión en la simulación. Cada botella ahora registra si está en contacto con un transporte y almacena la última coordenada Z válida durante dicho contacto. Si la botella deja de tener contacto con transportes por varios frames consecutivos, se incrementa un contador de "presión". Al superar un umbral, el sistema reestablece la posición Z de la botella a su última altura conocida, previniendo la flotación. Este contador de presión se decrementa rápidamente al volver a hacer contacto con un transporte.
* Cuando una botella (`simBotella`) entra en contacto con un transporte de frenado (`simTransporte` con `isBrake = true`), su posición se ajusta automáticamente para centrarla en el eje longitudinal del transporte. Esto se realiza una única vez, en el primer contacto, para asegurar un acoplamiento suave y predecible. La posición de la botella se proyecta sobre la línea central del transporte y su velocidad lateral se anula, evitando que la botella se desvíe mientras frena y se alinea con el flujo de salida.
* Se ha implementado un indicador visual simple para mostrar cuando los transportes (`simTransporte`) están en movimiento. El sistema detecta automáticamente si un transporte tiene velocidad (`Speed > 0.001`) y cambia su color a verde brillante. Los transportes detenidos se muestran en verde normal. Esta funcionalidad se integra en el sistema de materiales del `BEPUVisualization3DManager` y se actualiza automáticamente cuando cambia el estado del transporte, proporcionando una retroalimentación visual inmediata del estado de operación.
* Se ha implementado un sistema de animaciones automáticas usando StoryBoard de WPF para los transportes en movimiento. Los transportes activos muestran una animación continua que combina: (1) rotación sutil muy lenta alrededor del eje Z (20 segundos por vuelta completa) y (2) pulsación cíclica del color del material (1.5 segundos por ciclo). Las animaciones se crean y destruyen automáticamente según el estado del transporte, sin necesidad de actualización manual en cada frame. El sistema gestiona las animaciones activas en un diccionario y las limpia correctamente cuando se eliminan objetos. Se resolvió el problema de `InvalidOperationException` al animar brushes inmutables creando una función `CreateAnimatableMaterial` que genera materiales específicamente diseñados para ser animados sin estar "frozen", proporcionando una experiencia visual fluida y eficiente.
* Se ha mejorado el sistema de guías curvas (`ucTransporteCurvaGuias`) para incluir apertura en cono en los extremos de entrada y salida. Se agregó el parámetro `AnguloAperturaGuias` (por defecto 5 grados) que permite configurar la apertura modificando los radios de las guías en los puntos extremos. En lugar de cambiar ángulos, se reduce el radio de la guía superior (externa) y se aumenta el radio de la guía inferior (interna) en los segmentos inicial y final, creando naturalmente la apertura en cono. La modificación del radio se calcula usando `Math.Sin(anguloApertura)` para obtener el desplazamiento apropiado. Esta apertura facilita la entrada y salida de botellas del transporte curvo, reduciendo atascos y mejorando el flujo de materiales manteniendo la continuidad geométrica de las guías.

View File

@ -6,7 +6,7 @@ using Ookii.Dialogs.Wpf;
using System.Collections.ObjectModel;
using System.Windows.Threading;
using CtrEditor.ObjetosSim;
using LibS7Adv; // Using stub implementation
using LibS7Adv;
using System.IO;
using Newtonsoft.Json;
using System.Windows;
@ -26,7 +26,6 @@ using CtrEditor.Serialization; // Add this line
using CtrEditor.Controls; // Add this using directive
using CtrEditor.PopUps; // Add this using directive
namespace CtrEditor
{
@ -45,18 +44,14 @@ namespace CtrEditor
private float TiempoDesdeStartSimulacion;
private bool Debug_SimulacionCreado = false;
public SimulationManagerBEPU simulationManager = new SimulationManagerBEPU();
public SimulationManagerFP simulationManager = new SimulationManagerFP();
private readonly DispatcherTimer _timerSimulacion;
private readonly DispatcherTimer _timerPLCUpdate;
private readonly DispatcherTimer _timerDisplayUpdate;
private readonly DispatcherTimer _timer3DUpdate; // Nuevo timer para actualización 3D cuando simulación está detenida
public Canvas MainCanvas;
// Manager para la visualización 3D
public BEPUVisualization3DManager Visualization3DManager { get; set; }
[ObservableProperty]
private bool isConnected;
@ -96,6 +91,7 @@ namespace CtrEditor
public ICommand StopSimulationCommand { get; }
public ICommand ItemDoubleClickCommand { get; private set; }
public SimulationFluidsViewModel FluidSimulation { get; private set; }
public ICommand TBStartSimulationCommand { get; }
public ICommand TBStopSimulationCommand { get; }
@ -118,12 +114,6 @@ namespace CtrEditor
public ICommand TBMultiPageMatrixCommand { get; }
public ICommand TBLibraryManagerCommand { get; }
// Comandos para vista 3D
public ICommand TB3DViewTopCommand { get; }
public ICommand TB3DViewSideCommand { get; }
public ICommand TB3DViewFrontCommand { get; }
public ICommand TB3DViewIsometricCommand { get; }
public ICommand TBTogglePLCConnectionCommand => new RelayCommand(() =>
{
@ -181,18 +171,6 @@ namespace CtrEditor
partial void OnIsSimulationRunningChanged(bool value)
{
CommandManager.InvalidateRequerySuggested(); // Notificar que el estado de los comandos ha cambiado
// Controlar el timer de actualización 3D
if (value)
{
// Simulación iniciada - detener timer 3D (la sincronización se hace en Step())
_timer3DUpdate.Stop();
}
else
{
// Simulación detenida - iniciar timer 3D para mantener sincronización
_timer3DUpdate.Start();
}
}
partial void OnHasUnsavedChangesChanged(bool value)
@ -389,18 +367,16 @@ namespace CtrEditor
_timerDisplayUpdate.Tick += OnDisplayUpdate;
_timerDisplayUpdate.Start();
_timer3DUpdate = new DispatcherTimer();
_timer3DUpdate.Interval = TimeSpan.FromMilliseconds(20);
_timer3DUpdate.Tick += OnTick3DUpdate;
_timer3DUpdate.Start(); // Iniciar porque la simulación empieza detenida
StartSimulationCommand = new RelayCommand(StartSimulation);
StopSimulationCommand = new RelayCommand(StopSimulation);
TBStartSimulationCommand = new RelayCommand(StartSimulation, () => !IsSimulationRunning);
TBStopSimulationCommand = new RelayCommand(StopSimulation, () => IsSimulationRunning);
// Inicializar simulación de fluidos
FluidSimulation = new SimulationFluidsViewModel(this);
TBStartFluidSimulationCommand = new RelayCommand(StartFluidSimulation, () => !FluidSimulation.IsFluidSimulationRunning);
TBStopFluidSimulationCommand = new RelayCommand(StopFluidSimulation, () => FluidSimulation.IsFluidSimulationRunning);
TBSaveCommand = new RelayCommand(Save);
TBEliminarUserControlCommand = new RelayCommand(EliminarUserControl, () => habilitarEliminarUserControl);
@ -413,13 +389,6 @@ namespace CtrEditor
TBMultiPageAnalizeCommand = new RelayCommand(MultiPageAnalizeCommand);
TBMultiPageMatrixCommand = new RelayCommand(MultiPageMatrixCommand);
TBLibraryManagerCommand = new RelayCommand(ShowLibraryManager);
// Comandos para vista 3D
TB3DViewTopCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Top));
TB3DViewSideCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Side));
TB3DViewFrontCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Front));
TB3DViewIsometricCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Isometric));
TBToggle3DUpdateCommand = new RelayCommand(() => Is3DUpdateEnabled = !Is3DUpdateEnabled);
RenameImageCommand = new RelayCommand<string>(RenameImage);
stopwatch_Sim = new Stopwatch();
@ -439,9 +408,6 @@ namespace CtrEditor
// Conectar DatosDeTrabajo con este ViewModel para el escaneo de imágenes
datosDeTrabajo.SetMainViewModel(this);
// NOTA: La conexión del manager 3D se hace en MainWindow_Loaded
// después de que se cree la instancia de BEPUVisualization3DManager
}
// Métodos para manejo de datos de imágenes
@ -907,6 +873,8 @@ namespace CtrEditor
private void StartSimulation()
{
// Detener simulación de fluidos si está ejecutándose
StopFluidSimulation();
IsSimulationRunning = true;
@ -916,7 +884,7 @@ namespace CtrEditor
foreach (var objetoSimulable in ObjetosSimulables)
objetoSimulable.UpdateGeometryStart();
simulationManager.Debug_DrawInitialBodies();
TiempoDesdeStartSimulacion = 0;
Debug_SimulacionCreado = true;
@ -933,6 +901,7 @@ namespace CtrEditor
if (Debug_SimulacionCreado)
{
simulationManager.Debug_ClearSimulationShapes();
Debug_SimulacionCreado = false;
}
_timerSimulacion.Stop();
@ -944,7 +913,26 @@ namespace CtrEditor
MainWindow?.ClearUndoHistory();
}
/// <summary>
/// Inicia la simulación de fluidos independiente
/// </summary>
public void StartFluidSimulation()
{
// Detener simulación física si está ejecutándose
StopSimulation();
FluidSimulation.StartFluidSimulation();
CommandManager.InvalidateRequerySuggested();
}
/// <summary>
/// Detiene la simulación de fluidos independiente
/// </summary>
public void StopFluidSimulation()
{
FluidSimulation.StopFluidSimulation();
CommandManager.InvalidateRequerySuggested();
}
private void OnTickSimulacion(object sender, EventArgs e)
{
@ -958,8 +946,10 @@ namespace CtrEditor
accumulatedSimTime += elapsedMilliseconds;
simSampleCount++;
// Contador de tiempo desde el inicio de la simulación
if (TiempoDesdeStartSimulacion <= 1200)
// Eliminar el diseño de Debug luego de 2 segundos
if (TiempoDesdeStartSimulacion > 1200)
simulationManager.Debug_ClearSimulationShapes();
else
TiempoDesdeStartSimulacion += (float)elapsedMilliseconds;
foreach (var objetoSimulable in ObjetosSimulables)
@ -1196,20 +1186,6 @@ namespace CtrEditor
[ObservableProperty]
private double plcUpdateSpeed;
[ObservableProperty]
private bool is3DUpdateEnabled = true;
public ICommand TBToggle3DUpdateCommand { get; }
partial void OnIs3DUpdateEnabledChanged(bool value)
{
// Sincronizar la propiedad con el simulationManager
if (simulationManager != null)
{
simulationManager.Is3DUpdateEnabled = value;
}
}
private void OnDisplayUpdate(object? sender, EventArgs e)
{
if (simSampleCount > 0)
@ -1289,6 +1265,7 @@ namespace CtrEditor
{
// Detener simulaciones antes de cambiar la escala
StopSimulation();
StopFluidSimulation();
DisconnectPLC();
// Actualizar la escala en el UnitConverter
@ -1361,15 +1338,6 @@ namespace CtrEditor
libraryWindow.Show();
}
private void OnTick3DUpdate(object? sender, EventArgs e)
{
// Solo actualizar Helix3D si la simulación está detenida y la actualización 3D está habilitada
// Cuando la simulación está corriendo, la sincronización se hace en simulationManager.Step()
if (!IsSimulationRunning && Is3DUpdateEnabled && Visualization3DManager != null)
{
Visualization3DManager.SynchronizeWorld();
}
}
}

View File

@ -10,7 +10,6 @@
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ObjetosSim="clr-namespace:CtrEditor.ObjetosSim"
xmlns:helix="http://helix-toolkit.org/wpf"
xmlns:ObjetosExtraccion="clr-namespace:CtrEditor.ObjetosSim.Extraccion_Datos"
x:Class="CtrEditor.MainWindow"
Height="900" Width="1600" WindowState="Maximized" ResizeMode="CanResize" Title="{Binding directorioTrabajo, Converter={StaticResource UnsavedChangesConverter}}"
@ -123,9 +122,7 @@
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="3*" />
<RowDefinition Height="5" />
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ToolBarTray Grid.Row="0">
@ -202,36 +199,6 @@
</StackPanel>
</Button>
<Button Command="{Binding TBToggle3DUpdateCommand}" ToolTip="Activar/Desactivar actualización Debug 3D para mejorar rendimiento">
<StackPanel>
<Image Source="Icons/app.png" Width="24" Height="24" />
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="Debug3D: {0}">
<Binding Path="Is3DUpdateEnabled">
<Binding.Converter>
<converters:BooleanToLocalizedTextConverter TrueText="Activado" FalseText="Desactivado" />
</Binding.Converter>
</Binding>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Style.Triggers>
<DataTrigger Binding="{Binding Is3DUpdateEnabled}" Value="True">
<Setter Property="Background" Value="LightGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding Is3DUpdateEnabled}" Value="False">
<Setter Property="Background" Value="LightCoral" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</ToolBar>
</ToolBarTray>
@ -256,35 +223,6 @@
</Canvas.RenderTransform>
</Canvas>
</ScrollViewer>
<!-- Separador entre vistas -->
<GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch" Background="DarkGray"
ResizeDirection="Rows" VerticalAlignment="Center" />
<!-- Vista 3D (Inferior) -->
<Border Grid.Row="3" BorderBrush="Gray" BorderThickness="1" Margin="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header del panel 3D -->
<TextBlock Grid.Row="0" Text="Debug 3D - Mundo BEPU" Background="DarkGray" Foreground="White"
Padding="5" FontWeight="Bold" />
<!-- HelixViewport3D -->
<helix:HelixViewport3D x:Name="Debug3DViewport" Grid.Row="1" ShowCoordinateSystem="True"
ShowFrameRate="True" CoordinateSystemLabelZ="Z - Altura" EnableCurrentPosition="True"
ShowFieldOfView="True" CalculateCursorPosition="True" IsManipulationEnabled="True"
Orthographic="True">
<helix:DefaultLights />
<helix:GridLinesVisual3D Width="100" Length="100" MinorDistance="1" Thickness="0.01"
MajorDistance="7" />
<!-- Contenido del viewport se configurará desde code-behind -->
</helix:HelixViewport3D>
</Grid>
</Border>
</Grid>
<!-- GridSplitter -->

View File

@ -1,5 +1,4 @@
using CtrEditor.ObjetosSim;
using CtrEditor.Simulacion;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
@ -46,9 +45,6 @@ namespace CtrEditor
private dataDebug dataDebug = new dataDebug();
// Manager para la visualización 3D
private BEPUVisualization3DManager _visualization3DManager;
public MainWindow()
{
InitializeComponent();
@ -95,18 +91,11 @@ namespace CtrEditor
viewModel.MainWindow = this;
viewModel.ImageSelected += ViewModel_ImageSelected;
viewModel?.LoadInitialData();
viewModel.simulationManager.DebugCanvas = ImagenEnTrabajoCanvas;
viewModel.MainCanvas = ImagenEnTrabajoCanvas;
// Inicializar ObjectHierarchyView
ObjectHierarchy.Initialize(viewModel);
// Inicializar el manager de visualización 3D
_visualization3DManager = new BEPUVisualization3DManager(Debug3DViewport, viewModel.simulationManager);
viewModel.Visualization3DManager = _visualization3DManager;
// Conectar el manager 3D con el simulation manager (orden correcto de inicialización)
viewModel.simulationManager.Visualization3DManager = _visualization3DManager;
}
}

View File

@ -1,5 +1,8 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucBotella" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:CtrEditor.ObjetosSim">
<UserControl x:Class="CtrEditor.ObjetosSim.ucBotella"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
>
<UserControl.DataContext>
<vm:osBotella/>

View File

@ -1,9 +1,11 @@
using System.Windows;
using System.Windows.Controls;
//using using Microsoft.Xna.Framework;
using LibS7Adv;
using CtrEditor.Simulacion;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Numerics;
using nkast.Aether.Physics2D.Common;
using System.Windows.Media;
using CtrEditor.FuncionesBase;
using System.ComponentModel;
@ -81,18 +83,18 @@ namespace CtrEditor.ObjetosSim
[property: Name("Inercia de Simulación")]
private float inercia_desde_simulacion;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Conservar objeto cuando sale de transporte")]
[property: Name("Conservar Fuera de Transporte")]
private bool preserve_Outside_Transport;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Porcentaje de tracción con transporte")]
[property: Name("Porcentaje de Tracción")]
private float porcentaje_Traccion;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Indica si la botella está en un transporte con freno")]
[property: Name("En Transporte con Freno")]
private bool enTransporteConFreno;
[ObservableProperty]
[property: Category("Simulación")]
[property: Description("Masa del objeto en kg")]
@ -109,7 +111,7 @@ namespace CtrEditor.ObjetosSim
}
public void SetCentro(float x, float y)
{ Left = x - Diametro / 2; Top = y - Diametro / 2; }
{ Left = x; Top = y; }
public void SetCentro(Vector2 centro)
{
@ -130,6 +132,7 @@ namespace CtrEditor.ObjetosSim
Diametro = 0.10f;
Mass = 1;
ColorButton_oculto = Brushes.Gray;
Preserve_Outside_Transport = true;
}
public void UpdateAfterMove()
@ -143,52 +146,39 @@ namespace CtrEditor.ObjetosSim
public override void UpdateGeometryStart()
{
// Se llama cuando inicia la simulación - crear geometría si no existe
if (SimGeometria == null)
{
SimGeometria = simulationManager.AddBotella(Diametro, GetCentro(), Mass);
SimGeometria.SimObjectType = "Botella";
SimGeometria.WpfObject = this;
}
else
{
// Se llama antes de la simulacion
ActualizarGeometrias();
SimGeometria?.SetDiameter(Diametro);
SimGeometria?.SetMass(Mass);
}
}
public override void UpdateGeometryStep()
{
// Se llama durante cada paso de la simulación
if (SimGeometria != null)
{
// Actualizar posición desde la simulación hacia WPF
SetCentro(SimGeometria.Center);
}
// Se llama antes de la simulacion
ActualizarGeometrias();
}
public override void UpdateControl(int elapsedMilliseconds)
{
SetCentro(SimGeometria.Center);
// Sistema de colores jerarquizado para diferentes estados
if (SimGeometria.isOnBrakeTransport)
ColorButton_oculto = Brushes.Blue; // En transporte con freno (prioridad sobre presión)
else if (!SimGeometria.isOnBrakeTransport && SimGeometria.PressureBuildup>1)
ColorButton_oculto = Brushes.Red; // La botella tiene mucha presion
if (SimGeometria.isRestricted)
ColorButton_oculto = Brushes.Yellow;
else
ColorButton_oculto = Brushes.Gray; // 5. Estado libre
{
if (SimGeometria.IsOnAnyTransport())
ColorButton_oculto = Brushes.Red;
else
ColorButton_oculto = Brushes.Gray;
}
// Ha sido marcada para remover
if (SimGeometria.Descartar)
RemoverDesdeSimulacion = true;
// ✅ ELIMINADO: Lógica redundante - BEPU.cs ya elimina botellas que caen en Z automáticamente
// Eliminar la botella si esta fuera de un transporte
if (!Preserve_Outside_Transport && !SimGeometria.IsOnAnyTransport())
RemoverDesdeSimulacion = true;
Velocidad_desde_simulacion = SimGeometria.GetLinearVelocity().ToString();
Inercia_desde_simulacion = SimGeometria.Mass; // En BEPU usamos masa en lugar de inercia directa
Velocidad_desde_simulacion = SimGeometria.Body.LinearVelocity.ToString();
Inercia_desde_simulacion = SimGeometria.Body.Inertia;
Porcentaje_Traccion = SimGeometria.OverlapPercentage;
EnTransporteConFreno = SimGeometria.isOnBrakeTransport; // Actualizar estado de freno
}
public override void ucLoaded()
@ -196,7 +186,7 @@ namespace CtrEditor.ObjetosSim
// El UserControl ya se ha cargado y podemos obtener las coordenadas para
// crear el objeto de simulacion
base.ucLoaded();
SimGeometria = simulationManager.AddBotella(Diametro, GetCentro(), Mass);
SimGeometria = simulationManager.AddCircle(Diametro, GetCentro(), Mass);
}
public override void ucUnLoaded()
{

View File

@ -5,7 +5,7 @@ using System.Windows.Controls;
using LibS7Adv;
using CtrEditor.Simulacion;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Numerics;
using nkast.Aether.Physics2D.Common;
using System.Windows.Media;
using CtrEditor.FuncionesBase;
using System.ComponentModel;
@ -80,18 +80,6 @@ namespace CtrEditor.ObjetosSim
[property: Name("Inercia Simulación")]
private float inercia_desde_simulacion;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Porcentaje de tracción con transporte")]
[property: Name("Porcentaje de Tracción")]
private float porcentaje_Traccion;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Indica si la botella está en un transporte con freno")]
[property: Name("En Transporte con Freno")]
private bool enTransporteConFreno;
[ObservableProperty]
[property: Category("Simulación")]
[property: Description("Masa de la botella")]
@ -152,27 +140,19 @@ namespace CtrEditor.ObjetosSim
public override void UpdateControl(int elapsedMilliseconds)
{
SetCentro(SimGeometria.Center);
// Sistema de colores jerarquizado para diferentes estados
if (SimGeometria.isRestricted)
ColorButton_oculto = Brushes.Yellow; // Estado restringido (prioridad alta)
else if (SimGeometria.isOnBrakeTransport)
ColorButton_oculto = Brushes.Blue; // En transporte con freno - NUEVO ESTADO
else if (SimGeometria.IsOnAnyTransport())
ColorButton_oculto = Brushes.Red; // En transporte normal
ColorButton_oculto = Brushes.Yellow;
else
ColorButton_oculto = Brushes.Gray; // Estado libre
// Ha sido marcada para remover
if (SimGeometria.Descartar)
{
if (SimGeometria.isOnTransports > 0)
ColorButton_oculto = Brushes.Red;
else
ColorButton_oculto = Brushes.Gray;
}
if (SimGeometria.Descartar) // Ha sido marcada para remover
RemoverDesdeSimulacion = true;
// ✅ ELIMINADO: Lógica redundante - BEPU.cs ya elimina botellas que caen en Z automáticamente
Velocidad_desde_simulacion = SimGeometria.GetLinearVelocity().ToString();
Inercia_desde_simulacion = SimGeometria.Mass; // En BEPU usamos masa en lugar de inercia directa
Porcentaje_Traccion = SimGeometria.OverlapPercentage;
EnTransporteConFreno = SimGeometria.isOnBrakeTransport; // Actualizar estado de freno
Velocidad_desde_simulacion = SimGeometria.Body.LinearVelocity.ToString();
Inercia_desde_simulacion = SimGeometria.Body.Inertia;
}
public override void ucLoaded()
@ -180,7 +160,7 @@ namespace CtrEditor.ObjetosSim
// El UserControl ya se ha cargado y podemos obtener las coordenadas para
// crear el objeto de simulacion
base.ucLoaded();
SimGeometria = simulationManager.AddBotella(Diametro, GetCentro(), Mass);
SimGeometria = simulationManager.AddCircle(Diametro, GetCentro(), Mass);
}
public override void ucUnLoaded()
{

View File

@ -2,16 +2,20 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim">
<UserControl.DataContext>
<vm:osBottGenerator />
<vm:osFiller/>
</UserControl.DataContext>
<Grid>
<Image Source="/imagenes/gear.png" Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}" Stretch="Uniform">
<Image Source="/imagenes/gear.png"
Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Stretch="Uniform">
<Image.RenderTransform>
<RotateTransform Angle="{Binding Angulo}" />
</Image.RenderTransform>

View File

@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using System.Diagnostics;
using CtrEditor.FuncionesBase;
using System.ComponentModel;
using DocumentFormat.OpenXml.Spreadsheet;
namespace CtrEditor.ObjetosSim
{
@ -40,8 +39,11 @@ namespace CtrEditor.ObjetosSim
[property: Name("Offset Vertical")]
private float offsetTopSalida;
// ✅ ELIMINADO: preserve_Outside_Transport - Redundante con ProcessCleanupSystem de BEPU
// Las botellas que caen en Z son eliminadas automáticamente por BEPU.cs
[ObservableProperty]
[property: Description("Las botellas se destruirán si caen fuera del transporte")]
[property: Category("Configuración")]
[property: Name("Conservar Fuera de Transporte")]
private bool preserve_Outside_Transport;
[ObservableProperty]
[property: Description("Tag PLC para habilitar funcionamiento. 1 => siempre activo")]
[property: Category("Enlace PLC")]
@ -87,7 +89,6 @@ namespace CtrEditor.ObjetosSim
[property: Category("Información")]
[property: Name("Salida Filtro")]
bool filter_Output;
[ObservableProperty]
[property: Description("Cantidad de botellas generadas por hora")]
[property: Category("Configuración")]
@ -114,32 +115,11 @@ namespace CtrEditor.ObjetosSim
[property: Name("Velocidad Actual (%)")]
private float velocidad_actual_percentual;
[ObservableProperty]
[property: Description("Distancia libre minima sin botellas sobre el transporte para colocar otra botella")]
[property: Category("Simulación")]
[property: Name("Diametro libre minimo")]
private float distancia_libre;
[ObservableProperty]
[property: Description("Diámetro de las botellas generadas")]
[property: Category("Configuración")]
[property: Name("Diámetro Botella")]
private float diametro_botella;
public osBottGenerator()
{
Ancho = 0.40f;
Alto = 0.40f;
Angulo = 0;
Velocidad_actual_percentual = 100;
Diametro_botella = 0.1f;
Botellas_hora = 10000;
Consenso = true;
Distancia_libre = 0.1f;
}
public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds)
{
if (Consenso_NC)
@ -150,7 +130,7 @@ namespace CtrEditor.ObjetosSim
private bool HayEspacioParaNuevaBotella(float X, float Y)
{
float radioMinimo = Distancia_libre; // Distancia mínima entre centros
float radioMinimo = Diametro_botella / 4; // Distancia mínima entre centros
float radioMinimoCuadrado = radioMinimo * radioMinimo;
// Buscar todas las botellas cercanas
@ -204,7 +184,7 @@ namespace CtrEditor.ObjetosSim
{
var nuevaBotella = _mainViewModel.CrearObjetoSimulable(typeof(osBotella), X, Y);
((osBotella)nuevaBotella).Diametro = Diametro_botella;
// ✅ ELIMINADO: Preserve_Outside_Transport - Ya no es necesario
((osBotella)nuevaBotella).Preserve_Outside_Transport = Preserve_Outside_Transport;
nuevaBotella.AutoCreated = true;
// Recalcular el tiempo entre botellas por si cambió la velocidad

View File

@ -1,15 +1,20 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucDescarte" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<UserControl x:Class="CtrEditor.ObjetosSim.ucDescarte"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:CtrEditor.ObjetosSim"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim" mc:Ignorable="d">
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CtrEditor.ObjetosSim"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl.Resources>
<Storyboard x:Key="PulsingStoryboard" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="AnimatedEllipse"
<DoubleAnimation
Storyboard.TargetName="AnimatedEllipse"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"
From="1" To="0.5" Duration="0:0:1" AutoReverse="True" />
<DoubleAnimation Storyboard.TargetName="AnimatedEllipse"
<DoubleAnimation
Storyboard.TargetName="AnimatedEllipse"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
From="1" To="0.5" Duration="0:0:1" AutoReverse="True" />
</Storyboard>
@ -20,12 +25,18 @@
</UserControl.DataContext>
<Grid>
<Ellipse Height="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}"
Width="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}" Stroke="Yellow"
Fill="Black" Opacity="0.5" />
<Ellipse x:Name="AnimatedEllipse" Height="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}"
Width="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}" Stroke="Blue"
Fill="Transparent" RenderTransformOrigin="0.5,0.5">
<Ellipse
Height="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}"
Width="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}"
Stroke="Yellow"
Fill="Black"
Opacity="0.5"/>
<Ellipse x:Name="AnimatedEllipse"
Height="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}"
Width="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}"
Stroke="Blue"
Fill="Transparent"
RenderTransformOrigin="0.5,0.5">
<Ellipse.RenderTransform>
<TransformGroup>
<ScaleTransform />

View File

@ -3,11 +3,11 @@ using LibS7Adv;
using CtrEditor.Simulacion;
using System.Windows;
using System.Windows.Controls;
using nkast.Aether.Physics2D.Common;
using System.Windows.Media.Animation;
using CommunityToolkit.Mvvm.ComponentModel;
using CtrEditor.FuncionesBase;
using System.ComponentModel;
using System.Numerics;
namespace CtrEditor.ObjetosSim
{
/// <summary>
@ -41,7 +41,7 @@ namespace CtrEditor.ObjetosSim
partial void OnDiametroChanged(float value)
{
ActualizarGeometrias();
SimGeometria?.SetDiameter(Diametro);
}
public Vector2 GetCentro()
@ -73,15 +73,8 @@ namespace CtrEditor.ObjetosSim
{
if (SimGeometria != null)
{
// ✅ SISTEMA INTELIGENTE: Solo recrear si el diámetro cambió
if (HasDiscardDimensionsChanged(SimGeometria, Diametro))
{
System.Diagnostics.Debug.WriteLine($"[osDescarte] Recreando descarte por cambio de diámetro");
SimGeometria.SetDiameter(Diametro);
}
// ✅ USAR MÉTODO COORDINATECONVERTER
SimGeometria.UpdateFromWpfCenter(GetCentro());
SimGeometria.SetPosition(GetCentro());
}
}

View File

@ -5,7 +5,6 @@ using LibS7Adv;
using CtrEditor.Simulacion;
using CtrEditor.FuncionesBase;
using System.ComponentModel;
using System.Numerics;
namespace CtrEditor.ObjetosSim
{
@ -33,35 +32,14 @@ namespace CtrEditor.ObjetosSim
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Grosor de la guía en metros")]
[property: Name("Grosor de la Guía")]
[property: Description("Alto de la guía en metros")]
[property: Name("Alto de la Guía")]
public float altoGuia;
private void ActualizarGeometrias()
{
if (_visualRepresentation is ucGuia uc && SimGeometria != null)
{
var topLeft = new Vector2(Left, Top);
// ✅ SISTEMA INTELIGENTE: Solo recrear si las dimensiones han cambiado
if (HasGuideDimensionsChanged(SimGeometria, Ancho, AltoGuia))
{
//System.Diagnostics.Debug.WriteLine($"[osGuia] Recreando guía por cambio de dimensiones: {Ancho}x{AltoGuia}");
// ✅ RECREAR COMPLETAMENTE: Las dimensiones cambiaron
SimGeometria.Create(Ancho, AltoGuia, topLeft, Angulo);
SimGeometria.SetDimensions(Ancho, AltoGuia);
}
else
{
// System.Diagnostics.Debug.WriteLine($"[osGuia] Solo actualizando posición/rotación: Left={Left}, Top={Top}, Angulo={Angulo}");
// ✅ SOLO ACTUALIZAR POSICIÓN/ROTACIÓN: Usar dimensiones reales para conversión correcta
SimGeometria.UpdateFromWpfParameters(topLeft, Angulo, Ancho, AltoGuia);
}
}
if (_visualRepresentation is ucGuia uc)
UpdateOrCreateLine(SimGeometria, uc.Guia);
}
public override void OnMoveResizeRotate()
@ -103,21 +81,7 @@ namespace CtrEditor.ObjetosSim
simulationManager?.Remove(SimGeometria);
}
public override void AnchoChanged(float value)
{
ActualizarGeometrias();
}
public override void AnguloChanged(float value)
{
ActualizarGeometrias();
}
// Método llamado cuando cambia AltoGuia
partial void OnAltoGuiaChanged(float value)
{
ActualizarGeometrias();
}
}
public partial class ucGuia : UserControl, IDataContainer

View File

@ -3,7 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:localuc="clr-namespace:CtrEditor.ObjetosSim.UserControls" xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
xmlns:localuc="clr-namespace:CtrEditor.ObjetosSim.UserControls"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl.DataContext>
@ -12,9 +13,7 @@
<Canvas x:Name="MainCanvas">
<localuc:CircularSegment x:Name="Transporte" Angle="0"
OuterRadius="{Binding RadioExterno, Converter={StaticResource MeterToPixelConverter}}"
InnerRadius="{Binding RadioInterno, Converter={StaticResource MeterToPixelConverter}}"
<localuc:CircularSegment x:Name="Transporte" Angle="0" OuterRadius="{Binding RadioExterno, Converter={StaticResource MeterToPixelConverter}}" InnerRadius="{Binding RadioInterno, Converter={StaticResource MeterToPixelConverter}}"
StartAngle="{Binding Angulo}" EndAngle="{Binding AnguloFinal}" />
</Canvas>
</UserControl>

View File

@ -7,7 +7,6 @@ using CtrEditor.Simulacion;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using CtrEditor.FuncionesBase;
using System.Text.Json.Serialization;
using System.Numerics;
namespace CtrEditor.ObjetosSim
{
@ -94,50 +93,6 @@ namespace CtrEditor.ObjetosSim
ActualizarGeometrias();
}
partial void OnRadioInternoChanged(float value)
{
// Ensure radioInterno is always less than radioExterno
if (value >= RadioExterno)
{
RadioInterno = RadioExterno * 0.75f; // Maintain proportion
return;
}
ActualizarGeometrias();
}
partial void OnArco_en_gradosChanged(float value)
{
OnPropertyChanged(nameof(AnguloFinal));
ActualizarGeometrias();
}
// Manejar cambios de posición usando métodos virtuales de osBase
public override void LeftChanging(float oldValue, float newValue)
{
base.LeftChanging(oldValue, newValue);
ActualizarPosicionBEPU();
}
public override void TopChanging(float oldValue, float newValue)
{
base.TopChanging(oldValue, newValue);
ActualizarPosicionBEPU();
}
private void ActualizarPosicionBEPU()
{
if (Simulation_TransporteCurva != null)
{
// ✅ CORRIGIDO: startAngle = Angulo, endAngle = Angulo + Arco_en_grados (como Aether)
var topLeft = new Vector2(Left, Top);
Simulation_TransporteCurva.Create(RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados, topLeft, 0);
// Sincronizar con la visualización 3D tras la actualización
simulationManager?.Visualization3DManager?.SynchronizeWorld();
}
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Radio interior de la curva en metros")]
@ -185,7 +140,6 @@ namespace CtrEditor.ObjetosSim
public override void AnguloChanged(float value)
{
OnPropertyChanged(nameof(AnguloFinal));
ActualizarGeometrias();
}
[ObservableProperty]
@ -202,15 +156,7 @@ namespace CtrEditor.ObjetosSim
{
if (_visualRepresentation is ucTransporteCurva uc)
{
if (Simulation_TransporteCurva != null)
{
// ✅ CORRIGIDO: startAngle = Angulo, endAngle = Angulo + Arco_en_grados (como Aether)
var topLeft = new Vector2(Left, Top);
Simulation_TransporteCurva.Create(RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados, topLeft, 0);
// Sincronizar con la visualización 3D tras la actualización
simulationManager?.Visualization3DManager?.SynchronizeWorld();
}
UpdateCurve(Simulation_TransporteCurva, RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados);
SetSpeed();
}
}
@ -263,9 +209,6 @@ namespace CtrEditor.ObjetosSim
// Ensure radioInterno is always less than radioExterno
if (RadioInterno >= RadioExterno)
RadioInterno = RadioExterno * 0.75f;
// Actualizar geometrías en BEPU después del redimensionamiento
ActualizarGeometrias();
}
@ -313,9 +256,7 @@ namespace CtrEditor.ObjetosSim
if (_visualRepresentation is ucTransporteCurva uc)
{
// ✅ CORRIGIDO: startAngle = Angulo, endAngle = Angulo + Arco_en_grados (como Aether)
var topLeft = new Vector2(Left, Top);
Simulation_TransporteCurva = simulationManager?.AddCurve(RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados, topLeft, 0);
Simulation_TransporteCurva = AddCurve(RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados);
CrearAnimacionStoryBoardTrasnporteCircular(uc.Transporte.TransportePath, InvertirDireccion, Angulo);
}
}

View File

@ -8,7 +8,6 @@ using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using CtrEditor.FuncionesBase;
using System.Text.Json.Serialization;
using System.Windows.Media;
using System.Numerics;
namespace CtrEditor.ObjetosSim
{
@ -134,17 +133,6 @@ namespace CtrEditor.ObjetosSim
ActualizarGeometrias();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Ángulo de apertura en grados para los extremos de las guías")]
[property: Name("Ángulo Apertura Guías")]
private float anguloAperturaGuias = 5f;
partial void OnAnguloAperturaGuiasChanged(float value)
{
ActualizarGeometrias();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Distancia de separación de las guías desde el borde")]
@ -251,42 +239,9 @@ namespace CtrEditor.ObjetosSim
partial void OnArco_en_gradosChanged(float value)
{
OnPropertyChanged(nameof(AnguloFinal));
ActualizarGeometrias();
}
// ✅ NUEVO: Manejar cambios de posición usando métodos virtuales de osBase
public override void LeftChanging(float oldValue, float newValue)
{
base.LeftChanging(oldValue, newValue);
ActualizarPosicionBEPU();
}
public override void TopChanging(float oldValue, float newValue)
{
base.TopChanging(oldValue, newValue);
ActualizarPosicionBEPU();
}
/// <summary>
/// ✅ CORREGIDO: Actualizar posición en BEPU usando métodos de conversión apropiados
/// </summary>
private void ActualizarPosicionBEPU()
{
if (Simulation_TransporteCurvaGuias != null)
{
// ✅ USAR MÉTODOS DE COORDINATECONVERTER
var topLeft = new Vector2(Left, Top);
Simulation_TransporteCurvaGuias.UpdateFromWpfParameters(topLeft, 0f, RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados);
// Recrear las guías en la nueva posición
ActualizarGuiasCurvas();
// Sincronizar con la visualización 3D tras la actualización
simulationManager?.Visualization3DManager?.SynchronizeWorld();
}
}
[Hidden]
public float AnguloFinal
{
@ -297,12 +252,7 @@ namespace CtrEditor.ObjetosSim
{
if (_visualRepresentation is ucTransporteCurvaGuias uc)
{
// ✅ USAR MÉTODO DE COORDINATECONVERTER PARA ACTUALIZACIÓN COMPLETA
if (Simulation_TransporteCurvaGuias != null)
{
var topLeft = new Vector2(Left, Top);
Simulation_TransporteCurvaGuias.UpdateFromWpfParameters(topLeft, 0f, RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados);
}
UpdateCurve(Simulation_TransporteCurvaGuias, RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados);
ActualizarGuiasCurvas();
SetSpeed();
}
@ -343,17 +293,16 @@ namespace CtrEditor.ObjetosSim
if (radioGuiaInferior < 0.01f)
radioGuiaInferior = 0.01f;
// ✅ CORREGIDO: Convertir ángulos a radianes usando método estándar
// Convertir ángulos a radianes
float anguloInicioRad = simBase.GradosARadianes(Angulo);
float anguloFinalRad = simBase.GradosARadianes(AnguloFinal);
float rangoAngular = anguloFinalRad - anguloInicioRad;
float anguloAperturaRad = simBase.GradosARadianes(AnguloAperturaGuias);
// Calcular el paso angular entre segmentos
float pasoAngular = rangoAngular / NumeroSegmentosGuias;
// Obtener el centro una vez para todo el método
Vector2 centro = GetCurveCenterInMeter(RadioExterno);
nkast.Aether.Physics2D.Common.Vector2 centro = GetCurveCenterInMeter(RadioExterno);
// Crear segmentos para guía superior (externa)
for (int i = 0; i < NumeroSegmentosGuias; i++)
@ -361,35 +310,21 @@ namespace CtrEditor.ObjetosSim
float angulo1 = anguloInicioRad + i * pasoAngular;
float angulo2 = anguloInicioRad + (i + 1) * pasoAngular;
// Calcular radios ajustados para crear apertura en cono
// Para guía superior: reducir radio en los extremos para crear apertura
float radio1 = radioGuiaSuperior;
float radio2 = radioGuiaSuperior;
// Calcular reducción del radio basada en el ángulo de apertura
float reduccionRadio = radioGuiaSuperior * (float)Math.Sin(anguloAperturaRad/10);
if (InvertirDireccion && i == 0) // Primer segmento
radio1 += reduccionRadio;
if (!InvertirDireccion && i == NumeroSegmentosGuias - 1) // Último segmento
radio2 += reduccionRadio;
Vector2 punto1 = new Vector2(
radio1 * (float)Math.Cos(angulo1),
radio1 * (float)Math.Sin(angulo1)
nkast.Aether.Physics2D.Common.Vector2 punto1 = new nkast.Aether.Physics2D.Common.Vector2(
radioGuiaSuperior * (float)Math.Cos(angulo1),
radioGuiaSuperior * (float)Math.Sin(angulo1)
);
Vector2 punto2 = new Vector2(
radio2 * (float)Math.Cos(angulo2),
radio2 * (float)Math.Sin(angulo2)
nkast.Aether.Physics2D.Common.Vector2 punto2 = new nkast.Aether.Physics2D.Common.Vector2(
radioGuiaSuperior * (float)Math.Cos(angulo2),
radioGuiaSuperior * (float)Math.Sin(angulo2)
);
// Ajustar por la posición del objeto
punto1 += centro;
punto2 += centro;
simGuia guiaSegmento = CrearGuiaDesdeDosPuntos(punto1, punto2);
if (guiaSegmento != null)
simGuia guiaSegmento = simulationManager.AddLine(punto1, punto2);
GuiasSuperiores.Add(guiaSegmento);
}
@ -399,67 +334,25 @@ namespace CtrEditor.ObjetosSim
float angulo1 = anguloInicioRad + i * pasoAngular;
float angulo2 = anguloInicioRad + (i + 1) * pasoAngular;
// Calcular radios ajustados para crear apertura en cono
// Para guía inferior: aumentar radio en los extremos para crear apertura
float radio1 = radioGuiaInferior;
float radio2 = radioGuiaInferior;
// Calcular aumento del radio basada en el ángulo de apertura
float aumentoRadio = radioGuiaInferior * (float)Math.Sin(anguloAperturaRad/10);
if (InvertirDireccion && i == 0) // Primer segmento
radio1 -= aumentoRadio;
if (InvertirDireccion && i == NumeroSegmentosGuias - 1) // Último segmento
radio2 -= aumentoRadio;
Vector2 punto1 = new Vector2(
radio1 * (float)Math.Cos(angulo1),
radio1 * (float)Math.Sin(angulo1)
nkast.Aether.Physics2D.Common.Vector2 punto1 = new nkast.Aether.Physics2D.Common.Vector2(
radioGuiaInferior * (float)Math.Cos(angulo1),
radioGuiaInferior * (float)Math.Sin(angulo1)
);
Vector2 punto2 = new Vector2(
radio2 * (float)Math.Cos(angulo2),
radio2 * (float)Math.Sin(angulo2)
nkast.Aether.Physics2D.Common.Vector2 punto2 = new nkast.Aether.Physics2D.Common.Vector2(
radioGuiaInferior * (float)Math.Cos(angulo2),
radioGuiaInferior * (float)Math.Sin(angulo2)
);
// Ajustar por la posición del objeto
punto1 += centro;
punto2 += centro;
simGuia guiaSegmento = CrearGuiaDesdeDosPuntos(punto1, punto2);
if (guiaSegmento != null)
simGuia guiaSegmento = simulationManager.AddLine(punto1, punto2);
GuiasInferiores.Add(guiaSegmento);
}
}
/// <summary>
/// ✅ CORREGIDO: Método helper para crear una guía desde dos puntos usando conversiones apropiadas
/// Convierte dos puntos Vector2 a los parámetros requeridos por AddLine
/// </summary>
private simGuia CrearGuiaDesdeDosPuntos(Vector2 punto1, Vector2 punto2)
{
try
{
// Calcular la longitud entre los dos puntos
var direccion = punto2 - punto1;
float longitud = direccion.Length();
if (longitud < 0.001f) // Evitar líneas de longitud cero
return null;
// ✅ CORREGIDO: Calcular el ángulo de la línea correctamente para WPF
float angulo = (float)Math.Atan2(direccion.Y, direccion.X) * 180f / (float)Math.PI;
// ✅ USAR punto1 como topLeft - simulationManager.AddLine ya maneja las conversiones WPF->BEPU
return simulationManager.AddLine(longitud, GrosorGuias, punto1, angulo);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error creando guía desde dos puntos: {ex.Message}");
return null;
}
}
public override void OnMoveResizeRotate()
{
ActualizarGeometrias();
@ -523,9 +416,6 @@ namespace CtrEditor.ObjetosSim
// Ensure radioInterno is always less than radioExterno
if (RadioInterno >= RadioExterno)
RadioInterno = RadioExterno * 0.75f;
// ✅ NUEVO: Actualizar geometrías en BEPU después del redimensionamiento
ActualizarGeometrias();
}
public osTransporteCurvaGuias()
@ -575,9 +465,7 @@ namespace CtrEditor.ObjetosSim
if (_visualRepresentation is ucTransporteCurvaGuias uc)
{
// ✅ CORREGIDO: Usar simulationManager?.AddCurve con conversión WPF correcta
var topLeft = new Vector2(Left, Top);
Simulation_TransporteCurvaGuias = simulationManager?.AddCurve(RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados, topLeft, 0);
Simulation_TransporteCurvaGuias = AddCurve(RadioInterno, RadioExterno, Angulo, Angulo + Arco_en_grados);
CrearGuiasCurvas(); // Crear las guías curvas
CrearAnimacionStoryBoardTrasnporteCircular(uc.Transporte.TransportePath, InvertirDireccion, Angulo);
}

View File

@ -2,13 +2,13 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl.Resources>
<!-- Define the VisualBrush for the conveyor belt pattern -->
<VisualBrush x:Key="BeltBrush" TileMode="Tile" Viewport="0,0,20,10" ViewportUnits="Absolute" Viewbox="0,0,20,10"
ViewboxUnits="Absolute">
<VisualBrush x:Key="BeltBrush" TileMode="Tile" Viewport="0,0,20,10" ViewportUnits="Absolute" Viewbox="0,0,20,10" ViewboxUnits="Absolute">
<VisualBrush.Transform>
<TransformGroup>
<TranslateTransform/>
@ -38,8 +38,7 @@
</TransformGroup>
</Canvas.RenderTransform>
<StackPanel x:Name="RectanglesContainer">
<Rectangle x:Name="GuiaSuperior"
Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
<Rectangle x:Name="GuiaSuperior" Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding AltoGuia, Converter={StaticResource MeterToPixelConverter}}"
Fill="{Binding Color, Converter={StaticResource ColorToBrushConverter}}"
Margin="{Binding Distance, Converter={StaticResource DistanceToMarginConverter}}"/>
@ -47,19 +46,19 @@
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Margin="{Binding Distance, Converter={StaticResource DistanceToMarginConverter}}"
Fill="{StaticResource BeltBrush}"/>
<Rectangle x:Name="GuiaInferior"
Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
<Rectangle x:Name="GuiaInferior" Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding AltoGuia, Converter={StaticResource MeterToPixelConverter}}"
Fill="{Binding Color, Converter={StaticResource ColorToBrushConverter}}"/>
</StackPanel>
<Viewbox Canvas.Top="{Binding AltoGuia, Converter={StaticResource MeterToPixelConverter}}"
Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}" Stretch="Uniform">
<Label Content="{Binding Nombre}" VerticalAlignment="Center" HorizontalAlignment="Center"
FontWeight="Bold" FontSize="18" Opacity="0.9" />
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Stretch="Uniform">
<Label Content="{Binding Nombre}" VerticalAlignment="Center" HorizontalAlignment="Center" FontWeight="Bold" FontSize="18" Opacity="0.9"/>
</Viewbox>
</Canvas>
</Grid>
</UserControl>

View File

@ -1,5 +1,4 @@
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@ -9,7 +8,6 @@ using CtrEditor.Simulacion;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using CtrEditor.FuncionesBase;
using System.Text.Json.Serialization;
using Siemens.Simatic.Simulation.Runtime;
namespace CtrEditor.ObjetosSim
{
@ -126,16 +124,6 @@ namespace CtrEditor.ObjetosSim
ActualizarGeometrias();
}
public override void AnchoChanged(float value)
{
ActualizarGeometrias();
}
public override void AnguloChanged(float value)
{
ActualizarGeometrias();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Actuar como freno")]
@ -144,14 +132,8 @@ namespace CtrEditor.ObjetosSim
partial void OnEsFrenoChanged(bool value)
{
System.Diagnostics.Debug.WriteLine($"[🔧 FRENO CAMBIO] Transporte '{Nombre}' - EsFreno cambiado a: {value}");
ActualizarGeometrias();
// Verificar que la asignación se realizó correctamente
if (SimGeometria != null)
{
System.Diagnostics.Debug.WriteLine($"[✅ FRENO SYNC] SimGeometria.isBrake = {SimGeometria.isBrake}");
}
SimGeometria.isBrake = value;
}
[ObservableProperty]
@ -160,11 +142,6 @@ namespace CtrEditor.ObjetosSim
[property: Name("Coeficiente Fricción")]
public float frictionCoefficient;
partial void OnFrictionCoefficientChanged(float value)
{
ActualizarGeometrias();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Velocidad máxima a 50Hz")]
@ -183,132 +160,32 @@ namespace CtrEditor.ObjetosSim
[property: Name("En Marcha")]
public bool esMarcha;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Información sobre botellas en el transporte con freno")]
[property: Name("Info Botellas Freno")]
public string infoBotellasFreno = "Sin información";
/// <summary>
/// Actualiza la información de botellas en el transporte con freno
/// </summary>
public void ActualizarInfoBotellasFreno()
{
if (SimGeometria?.isBrake == true && simulationManager != null)
{
// Contar botellas marcadas como en transporte con freno
var botellasEnFreno = simulationManager.Cuerpos
.OfType<simBotella>()
.Where(b => b.isOnBrakeTransport)
.Count();
InfoBotellasFreno = $"Botellas con freno: {botellasEnFreno}";
}
else if (SimGeometria?.isBrake == false)
{
InfoBotellasFreno = "Transporte SIN freno";
}
else
{
InfoBotellasFreno = "Sin información";
}
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Distancia entre guías")]
[property: Name("Distancia")]
private float distance;
partial void OnDistanceChanged(float value)
{
ActualizarGeometrias();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Alto de las guías")]
[property: Name("Alto Guía")]
private float altoGuia;
partial void OnAltoGuiaChanged(float value)
{
ActualizarGeometrias();
}
private void ActualizarGeometrias()
{
if (_visualRepresentation is ucTransporteGuias uc)
{
UpdateRectangle(SimGeometria, uc.Transporte, Alto, Ancho, Angulo);
UpdateOrCreateLine(Guia_Superior, uc.GuiaSuperior);
UpdateOrCreateLine(Guia_Inferior, uc.GuiaInferior);
// Actualizar guías con método específico para transporte
UpdateTransportGuide(Guia_Superior, uc.GuiaSuperior, isTopGuide: true);
UpdateTransportGuide(Guia_Inferior, uc.GuiaInferior, isTopGuide: false);
if (SimGeometria != null)
{
SimGeometria.TransportWithGuides = true;
SimGeometria.DistanceGuide2Guide = Distance;
SimGeometria.isBrake = EsFreno; // Usar propiedad generada
SimGeometria.Friction = FrictionCoefficient; // Usar propiedad generada
System.Diagnostics.Debug.WriteLine($"[🔄 ACTUALIZAR GEOMETRIAS] Transporte '{Nombre}' - TransportWithGuides={SimGeometria.TransportWithGuides}, DistanceGuide2Guide={SimGeometria.DistanceGuide2Guide:F3}, isBrake={SimGeometria.isBrake}, Friction={SimGeometria.Friction:F3}, Speed={SimGeometria.Speed:F1}");
// Actualizar información de botellas si es un transporte con freno
ActualizarInfoBotellasFreno();
}
SimGeometria.DistanceGuide2Guide = Alto;
SimGeometria.isBrake = esFreno;
SetSpeed();
}
}
/// <summary>
/// Actualiza una guía específica del transporte (superior o inferior)
/// </summary>
/// <param name="simGuia">Objeto de simulación de la guía</param>
/// <param name="wpfRect">Rectángulo WPF de la guía</param>
/// <param name="isTopGuide">true para guía superior, false para inferior</param>
private void UpdateTransportGuide(simGuia simGuia, System.Windows.Shapes.Rectangle wpfRect, bool isTopGuide)
{
if (simGuia != null && wpfRect != null)
{
// Actualizar propiedades específicas de la guía
simGuia.UpdateProperties(Ancho, AltoGuia, Angulo);
// Calcular posición usando el rectángulo WPF actual
var topLeft2D = GetRectangleTopLeft(wpfRect);
// Crear/actualizar la guía con las dimensiones correctas
simGuia.Create(Ancho, AltoGuia, topLeft2D, Angulo);
System.Diagnostics.Debug.WriteLine($"[UpdateTransportGuide] {(isTopGuide ? "Superior" : "Inferior")} - Pos: ({topLeft2D.X:F3}, {topLeft2D.Y:F3}), Ancho: {Ancho:F3}, Alto: {AltoGuia:F3}, Ángulo: {Angulo:F1}°");
}
}
/// <summary>
/// Crea una guía específica del transporte (superior o inferior)
/// </summary>
/// <param name="wpfRect">Rectángulo WPF de la guía</param>
/// <param name="isTopGuide">true para guía superior, false para inferior</param>
/// <returns>Objeto simGuia creado</returns>
private simGuia CreateTransportGuide(System.Windows.Shapes.Rectangle wpfRect, bool isTopGuide)
{
if (wpfRect != null)
{
// Calcular posición usando el rectángulo WPF actual
var topLeft2D = GetRectangleTopLeft(wpfRect);
// Crear la guía con las dimensiones del transporte
var simGuia = simulationManager.AddLine(Ancho, AltoGuia, topLeft2D, Angulo);
System.Diagnostics.Debug.WriteLine($"[CreateTransportGuide] {(isTopGuide ? "Superior" : "Inferior")} creada - Pos: ({topLeft2D.X:F3}, {topLeft2D.Y:F3}), Ancho: {Ancho:F3}, Alto: {AltoGuia:F3}, Ángulo: {Angulo:F1}°");
return simGuia;
}
return null;
}
public override void OnMoveResizeRotate()
{
ActualizarGeometrias();
@ -319,7 +196,7 @@ namespace CtrEditor.ObjetosSim
Ancho = 1;
Alto = 0.10f;
AltoGuia = 0.03f;
Distance = 0.5f;
Distance = 0.01f;
Tag_ReleActivatedMotor = "1";
}
@ -343,12 +220,6 @@ namespace CtrEditor.ObjetosSim
VelocidadActual = id_motor.Velocidad;
else
VelocidadActual = 0;
// Actualizar información de botellas en tiempo real si es un transporte con freno
if (EsFreno)
{
ActualizarInfoBotellasFreno();
}
}
public override void ucLoaded()
@ -362,22 +233,12 @@ namespace CtrEditor.ObjetosSim
if (_visualRepresentation is ucTransporteGuias uc)
{
SimGeometria = AddRectangle(simulationManager, uc.Transporte, Alto, Ancho, Angulo);
// CORREGIR: Asegurar que todas las propiedades se inicialicen correctamente
if (SimGeometria != null)
{
SimGeometria.TransportWithGuides = true; // Siempre true para TransporteGuias
SimGeometria.DistanceGuide2Guide = Distance; // Usar Distance en lugar de Alto
SimGeometria.isBrake = EsFreno; // Usar propiedad generada
SimGeometria.Friction = FrictionCoefficient; // Usar propiedad generada
}
// Crear guías usando método específico para transporte
Guia_Superior = CreateTransportGuide(uc.GuiaSuperior, isTopGuide: true);
Guia_Inferior = CreateTransportGuide(uc.GuiaInferior, isTopGuide: false);
SimGeometria.TransportWithGuides = true;
SimGeometria.DistanceGuide2Guide = Alto;
Guia_Superior = AddLine(simulationManager, uc.GuiaSuperior);
Guia_Inferior = AddLine(simulationManager, uc.GuiaInferior);
CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion);
SetSpeed(); // Aplicar velocidad inicial
}
OnId_MotorChanged(Id_Motor); // Link Id_Motor = Motor
}

View File

@ -7,7 +7,6 @@ using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using System.ComponentModel;
using CtrEditor.FuncionesBase;
using System.Text.Json.Serialization;
using System.Numerics;
namespace CtrEditor.ObjetosSim
{

View File

@ -1,5 +1,5 @@
using CtrEditor.Simulacion;
using LibS7Adv; // Using stub implementation
using LibS7Adv;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@ -9,7 +9,6 @@ using CtrEditor.FuncionesBase;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
using System.ComponentModel;
using System.Numerics;
namespace CtrEditor.ObjetosSim
{
@ -35,14 +34,7 @@ namespace CtrEditor.ObjetosSim
public override string Nombre
{
get => nombre;
set
{
if (SetProperty(ref nombre, value))
{
// Asegurar que siempre se notifique el cambio, incluso en inicialización
OnPropertyChanged();
}
}
set => SetProperty(ref nombre, value);
}
[ObservableProperty]
@ -94,6 +86,12 @@ namespace CtrEditor.ObjetosSim
[property: Name("Ancho del Haz")]
float ancho_Haz_De_Luz;
[ObservableProperty]
[property: Description("Distancia al cuello de la botella")]
[property: Category("Información")]
[property: Name("Distancia al Cuello")]
float distancia_cuello;
[ObservableProperty]
[property: Description("Tipo de detección: cuello de botella o botella completa")]
[property: Category("Configuración")]
@ -104,7 +102,7 @@ namespace CtrEditor.ObjetosSim
{
if (Simulation_Photocell == null) return;
simulationManager.SetBarreraDetectNeck(Simulation_Photocell, value);
Simulation_Photocell.DetectNeck = value;
}
@ -202,10 +200,6 @@ namespace CtrEditor.ObjetosSim
Frecuency = 0;
timer = new Stopwatch();
timer.Start();
Color = Brushes.Black;
// Forzar notificación de la propiedad Nombre para asegurar binding inicial
OnPropertyChanged(nameof(Nombre));
}
public override void UpdateGeometryStart()
@ -215,14 +209,11 @@ namespace CtrEditor.ObjetosSim
}
public override void UpdateControl(int elapsedMilliseconds)
{
if (Simulation_Photocell == null) return;
var barreraData = simulationManager.GetBarreraData(Simulation_Photocell);
Distancia_cuello = Simulation_Photocell.Distancia;
if (DetectarCuello)
LuzCortada = barreraData.LuzCortadaNeck;
LuzCortada = Simulation_Photocell.LuzCortadaNeck;
else
LuzCortada = barreraData.LuzCortada;
LuzCortada = Simulation_Photocell.LuzCortada > 0;
}
public override void UpdatePLCPrimerCiclo()
{
@ -241,17 +232,13 @@ namespace CtrEditor.ObjetosSim
base.ucLoaded();
if (_visualRepresentation is ucPhotocell uc)
Simulation_Photocell = AddBarrera(simulationManager, uc.Photocell, Ancho_Haz_De_Luz, Ancho, Angulo, DetectarCuello);
Simulation_Photocell = AddBarrera(simulationManager, uc.Photocell, Alto, Ancho, Angulo, DetectarCuello);
}
public override void ucUnLoaded()
{
// El UserControl se esta eliminando
// eliminar el objeto de simulacion
if (Simulation_Photocell != null)
{
simulationManager.RemoveBarrera(Simulation_Photocell);
Simulation_Photocell = null;
}
simulationManager.Remove(Simulation_Photocell);
}

View File

@ -76,7 +76,7 @@ namespace CtrEditor.ObjetosSim
return instance;
}
public static void AssignDatos(UserControl userControl, osBase datos, SimulationManagerBEPU simulationManager)
public static void AssignDatos(UserControl userControl, osBase datos, SimulationManagerFP simulationManager)
{
if (userControl is IDataContainer dataContainer)
{

View File

@ -3,7 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CtrEditor.ObjetosSim.UserControls" mc:Ignorable="d" Name="circularSegmentControl">
xmlns:local="clr-namespace:CtrEditor.ObjetosSim.UserControls"
mc:Ignorable="d" Name="circularSegmentControl">
<Canvas Name="mainCanvas">
<Path Name="path" Stroke="Black" StrokeThickness="1">
<Path.Fill>

View File

@ -4,7 +4,7 @@ using CtrEditor.Serialization;
using CtrEditor.Services;
using CtrEditor.Simulacion;
using LibS7Adv;
using System.Numerics;
using nkast.Aether.Physics2D.Common;
using PaddleOCRSharp;
using Siemens.Simatic.Simulation.Runtime;
using System.ComponentModel; // Para poder usar [property: Category ...
@ -16,6 +16,7 @@ using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Tesseract;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using Application = System.Windows.Application;
using ItemCollection = Xceed.Wpf.Toolkit.PropertyGrid.Attributes.ItemCollection;
@ -43,16 +44,16 @@ namespace CtrEditor.ObjetosSim
{
private MainViewModel? _mainViewModel;
private UserControl? VisualRepresentation;
private SimulationManagerBEPU? simulationManager;
private SimulationManagerFP? simulationManager;
public DataSaveToSerialize(MainViewModel a, UserControl b, SimulationManagerBEPU c)
public DataSaveToSerialize(MainViewModel a, UserControl b, SimulationManagerFP c)
{
_mainViewModel = a;
VisualRepresentation = b;
simulationManager = c;
}
public void DataRestoreAfterSerialize(out MainViewModel a, out UserControl b, out SimulationManagerBEPU c)
public void DataRestoreAfterSerialize(out MainViewModel a, out UserControl b, out SimulationManagerFP c)
{
a = _mainViewModel;
b = VisualRepresentation;
@ -61,25 +62,6 @@ namespace CtrEditor.ObjetosSim
}
/// <summary>
/// Estructura para almacenar las dimensiones de los objetos de simulación
/// </summary>
public struct SimObjectDimensions
{
public float Width { get; set; }
public float Height { get; set; }
public float Radius { get; set; }
public int ObjectType { get; set; } // Tipo de objeto: 1=Transport, 2=Barrier, 3=Guide, etc.
public bool Equals(SimObjectDimensions other)
{
return Math.Abs(Width - other.Width) < 0.001f &&
Math.Abs(Height - other.Height) < 0.001f &&
Math.Abs(Radius - other.Radius) < 0.001f &&
ObjectType == other.ObjectType;
}
}
public abstract partial class osBase : ObservableObject
{
public virtual string Nombre { get; set; } = "osBase";
@ -89,11 +71,6 @@ namespace CtrEditor.ObjetosSim
[JsonIgnore]
private System.Threading.Timer timer = null;
// ✅ SISTEMA DE DIMENSIONES: Para evitar recreación innecesaria de objetos físicos
[JsonIgnore]
private static Dictionary<simBase, SimObjectDimensions> _lastKnownDimensions = new Dictionary<simBase, SimObjectDimensions>();
[ObservableProperty]
[property: JsonIgnore]
[property: Hidden]
@ -136,7 +113,6 @@ namespace CtrEditor.ObjetosSim
// Actualizar posición relativa si el movimiento no viene del FramePlate
UpdateFramePlateRelativePosition();
}
public virtual void LeftChanging(float oldValue, float newValue) { }
[ObservableProperty]
@ -590,6 +566,89 @@ namespace CtrEditor.ObjetosSim
}
public string CaptureImageAreaAndDoOCR(float Left, float Top, float Ancho, float Alto, float Angulo = 0, bool ShowPreview = false)
{
if (_mainViewModel?.MainCanvas.Children[0] is Image imagenDeFondo)
{
if (imagenDeFondo.Source is BitmapSource bitmapSource)
{
float originalDpiX = (float)bitmapSource.DpiX;
float originalDpiY = (float)bitmapSource.DpiY;
float canvasDpiX = 96;
float canvasDpiY = 96;
float scaleFactorX = originalDpiX / canvasDpiX;
float scaleFactorY = originalDpiY / canvasDpiY;
int x = (int)MeterToPixels(Left * scaleFactorX);
int y = (int)MeterToPixels(Top * scaleFactorY);
int width = (int)MeterToPixels(Ancho * scaleFactorX);
int height = (int)MeterToPixels(Alto * scaleFactorY);
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x + width > bitmapSource.PixelWidth) width = bitmapSource.PixelWidth - x;
if (y + height > bitmapSource.PixelHeight) height = bitmapSource.PixelHeight - y;
CroppedBitmap croppedBitmap = new CroppedBitmap(bitmapSource, new Int32Rect(x, y, width, height));
TransformedBitmap transformedBitmap = new TransformedBitmap();
transformedBitmap.BeginInit();
transformedBitmap.Source = croppedBitmap;
if (Angulo != 0)
{
// TransformedBitmap only accepts rotations in 90-degree increments
// Round to nearest 90 degrees: 0, 90, 180, or 270
double normalizedAngle = Math.Round(Angulo / 90.0) * 90.0;
RotateTransform rotateTransform = new RotateTransform(-normalizedAngle);
transformedBitmap.Transform = rotateTransform;
}
transformedBitmap.EndInit();
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(transformedBitmap));
using (MemoryStream memoryStream = new MemoryStream())
{
encoder.Save(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
if (ShowPreview) ShowPreviewWindow(memoryStream);
using (var bmp = new System.Drawing.Bitmap(memoryStream))
{
int targetDpi = 400;
var resizedBmp = new System.Drawing.Bitmap(bmp, new System.Drawing.Size(bmp.Width * targetDpi / (int)originalDpiX, bmp.Height * targetDpi / (int)originalDpiY));
using (var msResized = new MemoryStream())
{
resizedBmp.Save(msResized, System.Drawing.Imaging.ImageFormat.Png);
msResized.Seek(0, SeekOrigin.Begin);
using (var img = Pix.LoadFromMemory(msResized.ToArray()))
{
// Use AppDomain.CurrentDomain.BaseDirectory to ensure we find Tesseract in the application directory
string tesseractPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Tesseract");
using (var engine = new TesseractEngine(tesseractPath, "eng", EngineMode.Default))
{
// Configuraciones para mejorar el OCR de una sola letra
engine.SetVariable("tessedit_char_whitelist", " ABCDEFGHIJKLMNÑOPQRSTUVWXYZabcdefghijklmnñopqrstuvwxyz0123456789-./"); // Lista blanca de caracteres
var result = engine.Process(img);
return result.GetText();
}
}
}
}
}
}
}
return "";
}
// Reemplaza el método existente CaptureImageAreaAndDoOCR con esta implementación
public string CaptureImageAreaAndDoOCRPPaddle(float Left, float Top, float Ancho, float Alto, float Angulo = 0, bool ShowPreview = false)
{
@ -709,6 +768,7 @@ namespace CtrEditor.ObjetosSim
[property: Name("Habilitado en Todas Páginas")]
private bool enable_On_All_Pages;
partial void OnEnable_On_All_PagesChanged(bool value)
{
// Si se está desactivando el modo global
@ -725,6 +785,7 @@ namespace CtrEditor.ObjetosSim
}
}
// Local Data for Global Objects
[NotifyPropertyChangedFor(nameof(Show_On_This_Page))]
[ObservableProperty]
@ -989,7 +1050,6 @@ namespace CtrEditor.ObjetosSim
/// <param name="elapsedMilliseconds"></param>
public virtual void UpdateControl(int elapsedMilliseconds) { }
/// <summary>
/// Se llama antes de comenzar la simulacion con el boton de Iniciar simulacion.
/// La idea es actualizar los objetos en el motor fisico antes de comenzar la simulacion fisica.
@ -1051,7 +1111,7 @@ namespace CtrEditor.ObjetosSim
/// Link al Simualdor fisico.
/// </summary>
[JsonIgnore]
public SimulationManagerBEPU simulationManager;
public SimulationManagerFP simulationManager;
/// <summary>
/// Prepara la clase para ser serializable poniendo a null los objetos que tienen referencias circulares
@ -1542,112 +1602,56 @@ namespace CtrEditor.ObjetosSim
return new Vector2((topLeft.X + bottomRight.X) / 2, (topLeft.Y + bottomRight.Y) / 2);
}
public Vector2 GetRectangleTopLeft(Rectangle wpfRect)
{
var coords = GetRectangleCoordinatesInMeter(wpfRect);
return coords.TopLeft;
}
public void UpdateRectangle(simTransporte simRect, Rectangle wpfRect, float Alto, float Ancho, float Angulo)
{
if (simRect != null)
{
var topLeft2D = GetRectangleTopLeft(wpfRect);
// ✅ SISTEMA INTELIGENTE: Solo recrear si las dimensiones han cambiado
bool dimensionsChanged = HasTransportDimensionsChanged(simRect, Ancho, Alto);
if (dimensionsChanged)
{
// Las dimensiones cambiaron, recrear el objeto físico
simRect.Create(Ancho, Alto, topLeft2D, Angulo);
simRect.SetDimensions(Ancho, Alto); // Actualizar dimensiones internas
}
else
{
// ✅ CORREGIDO: Usar UpdateFromWpfParameters para manejar correctamente Top-Left + Ángulo
simRect.UpdateFromWpfParameters(topLeft2D, Angulo);
simRect.Create(Ancho, Alto, GetRectangleCenter(wpfRect), Angulo);
}
// Actualizar propiedades cacheadas
simRect.UpdateCachedProperties();
}
public void UpdateRectangle(simBarrera simRect, Rectangle wpfRect, float Alto, float Ancho, float Angulo)
{
if (simRect != null)
simRect.Create(Ancho, Alto, GetRectangleCenter(wpfRect), Angulo);
}
public void UpdateRectangle(simBarrera barrera, Rectangle wpfRect, float Alto, float Ancho, float Angulo)
public void UpdateCurve(simCurve curva, float RadioInterno, float RadioExterno, float startAngle, float endAngle)
{
if (barrera != null)
{
var topLeft2D = GetRectangleTopLeft(wpfRect);
simulationManager.UpdateBarrera(barrera, Ancho, Alto, topLeft2D, Angulo);
}
curva.Create(RadioInterno, RadioExterno, startAngle, endAngle, GetCurveCenterInMeter(RadioExterno));
}
public simTransporte AddRectangle(SimulationManagerBEPU simulationManager, Rectangle wpfRect, float Alto, float Ancho, float Angulo)
public simCurve AddCurve(float RadioInterno, float RadioExterno, float startAngle, float endAngle)
{
var topLeft2D = GetRectangleTopLeft(wpfRect);
return simulationManager.AddRectangle(Ancho, Alto, topLeft2D, Angulo);
return simulationManager.AddCurve(RadioInterno, RadioExterno, startAngle, endAngle, GetCurveCenterInMeter(RadioExterno));
}
public simBarrera AddBarrera(SimulationManagerBEPU simulationManager, Rectangle wpfRect, float Alto, float Ancho, float Angulo, bool detectarCuello)
public simTransporte AddRectangle(SimulationManagerFP simulationManager, Rectangle wpfRect, float Alto, float Ancho, float Angulo)
{
var topLeft2D = GetRectangleTopLeft(wpfRect);
// Convertir Vector2 a Vector3 para BEPU (Z = 0 para objetos 2D)
return simulationManager.CreateBarrera(Ancho, Alto, topLeft2D, Angulo, detectarCuello);
return simulationManager.AddRectangle(Ancho, Alto, GetRectangleCenter(wpfRect), Angulo);
}
public simBarrera AddBarrera(SimulationManagerFP simulationManager, Rectangle wpfRect, float Alto, float Ancho, float Angulo, bool detectarCuello)
{
return simulationManager.AddBarrera(Ancho, Alto, GetRectangleCenter(wpfRect), Angulo, detectarCuello);
}
public void UpdateOrCreateLine(simGuia simGuia, Rectangle wpfRect)
{
if (simGuia != null)
{
// Usar el mismo sistema que transportes: Top-Left + Ángulo
var topLeft2D = GetRectangleTopLeft(wpfRect);
// Actualizar las propiedades desde el objeto osGuia
if (this is osGuia guiaObj)
var coords = GetCenterLineVectors(wpfRect);
// Crear o actualizar simRectangle
simGuia.Create(coords.Start, coords.End); // asumiendo que el ángulo inicial es 0
}
}
public simGuia AddLine(SimulationManagerFP simulationManager, Rectangle wpfRect)
{
// ✅ SISTEMA INTELIGENTE: Solo recrear si las dimensiones han cambiado
bool dimensionsChanged = HasGuideDimensionsChanged(simGuia, guiaObj.Ancho, guiaObj.AltoGuia);
if (dimensionsChanged)
{
// Las dimensiones cambiaron, recrear el objeto físico
simGuia.Create(guiaObj.Ancho, guiaObj.AltoGuia, topLeft2D, guiaObj.Angulo);
simGuia.SetDimensions(guiaObj.Ancho, guiaObj.AltoGuia); // Actualizar dimensiones internas
}
else
{
// Solo actualizar posición y rotación
simGuia.SetPosition(topLeft2D, guiaObj.Angulo);
}
// Actualizar propiedades internas
simGuia.UpdateProperties(guiaObj.Ancho, guiaObj.AltoGuia, guiaObj.Angulo);
}
}
}
public simGuia AddLine(SimulationManagerBEPU simulationManager, Rectangle wpfRect)
{
// Usar el mismo sistema que transportes: Top-Left + Ángulo
var topLeft2D = GetRectangleTopLeft(wpfRect);
// Obtener propiedades desde el objeto osGuia
float ancho = 1.0f;
float altoGuia = 0.05f;
float angulo = 0f;
if (this is osGuia guiaObj)
{
ancho = guiaObj.Ancho;
altoGuia = guiaObj.AltoGuia;
angulo = guiaObj.Angulo;
}
var simGuia = simulationManager.AddLine(ancho, altoGuia, topLeft2D, angulo);
return simGuia;
var coords = GetCenterLineVectors(wpfRect);
return simulationManager.AddLine(coords.Start, coords.End);
}
public ImageSource ImageFromPath(string value)
@ -1712,131 +1716,8 @@ namespace CtrEditor.ObjetosSim
}
}
// ✅ MÉTODOS PARA GESTIÓN INTELIGENTE DE DIMENSIONES
/// <summary>
/// Verifica si las dimensiones de un transporte han cambiado
/// </summary>
protected bool HasTransportDimensionsChanged(simTransporte transport, float newWidth, float newHeight)
{
if (transport == null) return true;
var newDimensions = new SimObjectDimensions
{
Width = newWidth,
Height = newHeight,
ObjectType = 1 // Transport
};
if (_lastKnownDimensions.TryGetValue(transport, out var lastDimensions))
{
bool changed = !newDimensions.Equals(lastDimensions);
if (changed)
{
_lastKnownDimensions[transport] = newDimensions;
System.Diagnostics.Debug.WriteLine($"[Dimensions] Transport dimensions CHANGED: {newWidth}x{newHeight}");
}
return changed;
}
else
{
// Primera vez - consideramos como cambio
_lastKnownDimensions[transport] = newDimensions;
System.Diagnostics.Debug.WriteLine($"[Dimensions] Transport first time: {newWidth}x{newHeight}");
return true;
}
}
/// <summary>
/// Verifica si las dimensiones de una guía han cambiado
/// </summary>
protected bool HasGuideDimensionsChanged(simGuia guide, float newWidth, float newHeight)
{
if (guide == null) return true;
var newDimensions = new SimObjectDimensions
{
Width = newWidth,
Height = newHeight,
ObjectType = 3 // Guide
};
if (_lastKnownDimensions.TryGetValue(guide, out var lastDimensions))
{
bool changed = !newDimensions.Equals(lastDimensions);
if (changed)
{
_lastKnownDimensions[guide] = newDimensions;
System.Diagnostics.Debug.WriteLine($"[Dimensions] Guide dimensions CHANGED: {newWidth}x{newHeight}");
}
return changed;
}
else
{
// Primera vez - consideramos como cambio
_lastKnownDimensions[guide] = newDimensions;
System.Diagnostics.Debug.WriteLine($"[Dimensions] Guide first time: {newWidth}x{newHeight}");
return true;
}
}
/// <summary>
/// ✅ NUEVO - Sistema inteligente de verificación de dimensiones de descartes
/// Verifica si el diámetro de un descarte ha cambiado
/// </summary>
protected bool HasDiscardDimensionsChanged(simDescarte discard, float newDiameter)
{
if (discard == null) return true;
var newDimensions = new SimObjectDimensions
{
Width = newDiameter,
Height = newDiameter,
Radius = newDiameter / 2f,
ObjectType = 4 // Tipo 4 = Descarte
};
if (_lastKnownDimensions.TryGetValue(discard, out var lastDimensions))
{
bool changed = !newDimensions.Equals(lastDimensions);
if (changed)
{
_lastKnownDimensions[discard] = newDimensions;
System.Diagnostics.Debug.WriteLine($"[Dimensions] Discard dimensions CHANGED: Ø{newDiameter}");
}
return changed;
}
else
{
// Primera vez - consideramos como cambio
_lastKnownDimensions[discard] = newDimensions;
System.Diagnostics.Debug.WriteLine($"[Dimensions] Discard first time: Ø{newDiameter}");
return true;
}
}
/// <summary>
/// Limpia las dimensiones almacenadas para un objeto específico
/// </summary>
public static void ClearStoredDimensions(simBase simObj)
{
if (simObj != null && _lastKnownDimensions.ContainsKey(simObj))
{
_lastKnownDimensions.Remove(simObj);
System.Diagnostics.Debug.WriteLine($"[Dimensions] Cleared stored dimensions for {simObj.GetType().Name}");
}
}
/// <summary>
/// Limpia todas las dimensiones almacenadas (útil al cerrar simulación)
/// </summary>
public static void ClearAllStoredDimensions()
{
_lastKnownDimensions.Clear();
System.Diagnostics.Debug.WriteLine($"[Dimensions] Cleared ALL stored dimensions");
}
}
public class UniqueId
{
public int Value { get; set; }

View File

@ -0,0 +1,325 @@
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
{
/// <summary>
/// ViewModel para el sistema de fluidos
/// </summary>
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: Category("Configuración")]
[property: Description("Ancho del área de simulación en metros")]
[property: Name("Ancho Simulación")]
private float anchoSimulacion = 10.0f;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Alto del área de simulación en metros")]
[property: Name("Alto Simulación")]
private float altoSimulacion = 10.0f;
// Propiedades del fluido
[ObservableProperty]
[property: Category("Apariencia")]
[property: Description("Tamaño visual de las partículas")]
[property: Name("Tamaño Partícula")]
private float tamañoParticula = 0.01f;
[ObservableProperty]
[property: Category("Apariencia")]
[property: Description("Color del fluido")]
[property: Name("Color Fluido")]
private Color colorFluido = Colors.CornflowerBlue;
[ObservableProperty]
[property: Category("Apariencia")]
[property: Description("Opacidad de las partículas")]
[property: Name("Opacidad Partículas")]
private double opacidadParticulas = 0.7;
// Propiedades de gravedad
[ObservableProperty]
[property: Category("Simulación")]
[property: Description("Gravedad en X (m/s²)")]
[property: Name("Gravedad X")]
private float gravedadX = 0.0f;
[ObservableProperty]
[property: Category("Simulación")]
[property: Description("Gravedad en Y (m/s²)")]
[property: Name("Gravedad Y")]
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: Category("Información")]
[property: Description("Número de partículas")]
[property: Name("Número Partículas")]
private int numeroParticulas;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Rendimiento en FPS")]
[property: Name("FPS")]
private double fps;
// Referencia a componentes (solo para la función Add)
private List<IContenedorFluido> _contenedores = new List<IContenedorFluido>();
// Nombre de la clase para identificación
public static string NombreClase()
{
return "Sistema de Fluidos";
}
private string nombre = NombreClase();
[property: Category("Identificación")]
[property: Description("Nombre identificativo del objeto")]
[property: Name("Nombre")]
public override string Nombre
{
get => nombre;
set => SetProperty(ref nombre, value);
}
// Métodos para interactuar con la simulación
/// <summary>
/// Agrega partículas en un punto específico
/// </summary>
public void AgregarParticula(Vector2 posicion)
{
_simFluidos?.AgregarParticula(posicion);
}
/// <summary>
/// Agrega múltiples partículas en un área
/// </summary>
public void AgregarParticulasEnArea(Vector2 centro, float ancho, float alto, int cantidad)
{
_simFluidos?.AgregarParticulasEnArea(centro, ancho, alto, cantidad);
}
/// <summary>
/// Crea un nuevo tanque y lo agrega a la simulación
/// </summary>
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;
}
/// <summary>
/// Crea una nueva tubería
/// </summary>
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;
}
/// <summary>
/// Crea una nueva válvula
/// </summary>
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;
}
/// <summary>
/// Elimina un componente de la simulación
/// </summary>
public void EliminarComponente(IContenedorFluido componente)
{
if (_simFluidos == null || componente == null) return;
_simFluidos.RemoverContenedor(componente);
_contenedores.Remove(componente);
}
/// <summary>
/// Limpia todas las partículas de la simulación
/// </summary>
public void LimpiarParticulas()
{
_simFluidos?.LimpiarParticulas();
}
/// <summary>
/// Actualiza el vector de gravedad según las propiedades
/// </summary>
private void ActualizarGravedad()
{
if (_simFluidos != null)
{
_simFluidos.AjustarGravedad(new Vector2(GravedadX, GravedadY));
}
}
/// <summary>
/// Constructor de la clase
/// </summary>
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;
}
}
/// <summary>
/// Llamado cuando se inicia la simulación de fluidos
/// </summary>
public void OnFluidSimulationStart()
{
// Crear la simulación de fluidos si es necesario
UpdateGeometryStart();
}
/// <summary>
/// Llamado cuando se detiene la simulación de fluidos
/// </summary>
public void OnFluidSimulationStop()
{
// Detener recursos si es necesario
SimulationStop();
}
/// <summary>
/// Actualiza la simulación de fluidos
/// </summary>
public void UpdateFluidSimulation(float deltaTime)
{
// Actualizar la simulación con el delta time
if (_simFluidos != null)
{
_simFluidos.Actualizar(deltaTime);
// Actualizar el control visual
UpdateControl((int)(deltaTime * 1000));
}
}
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);
}
}
}
}

View File

@ -0,0 +1,24 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucBasicExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:osBasicExample Ancho="2"/>
</UserControl.DataContext>
<Canvas>
<Rectangle x:Name="Transporte"
Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Fill="{StaticResource BeltBrush}">
<Rectangle.RenderTransform>
<RotateTransform Angle="{Binding Angulo}"/>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
</UserControl>

View File

@ -0,0 +1,245 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using LibS7Adv;
using CtrEditor.Simulacion;
using System.Windows.Input;
using System.ComponentModel;
namespace CtrEditor.ObjetosSim
{
/// <summary>
/// Interaction logic for ucBasicExample.xaml
/// </summary>
///
public partial class osBasicExample : osBase, IosBase
{
private osBase _osMotor = null;
private simTransporte SimGeometria;
public static string NombreClase()
{
return "Ejemplo Básico";
}
private string nombre = NombreClase();
[property: Category("Identificación")]
[property: Description("Nombre identificativo del objeto")]
[property: Name("Nombre")]
public override string Nombre
{
get => nombre;
set => SetProperty(ref nombre, value);
}
[ObservableProperty]
[property: Category("Simulación")]
[property: Description("Velocidad actual del transporte")]
[property: Name("Velocidad Actual")]
public float velocidadActual;
partial void OnVelocidadActualChanged(float value)
{
SetSpeed();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Invierte el sentido de movimiento")]
[property: Name("Invertir Dirección")]
bool invertirDireccion;
partial void OnInvertirDireccionChanged(bool value)
{
SetSpeed();
if (_visualRepresentation is ucBasicExample uc)
{
CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion);
ActualizarAnimacionStoryBoardTransporte(VelocidadActual);
}
}
void SetSpeed()
{
if (InvertirDireccion)
SimGeometria?.SetSpeed(-VelocidadActual);
else
SimGeometria?.SetSpeed(VelocidadActual);
ActualizarAnimacionStoryBoardTransporte(VelocidadActual);
}
[ObservableProperty]
[property: Category("Enlace PLC")]
[property: Description("Motor enlazado al transporte")]
[property: Name("Motor")]
public string motor;
partial void OnMotorChanged(string value)
{
_osMotor = ObtenerLink(Motor, typeof(osVMmotorSim));
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Ancho del transporte")]
[property: Name("Ancho")]
public float ancho;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Alto del transporte")]
[property: Name("Alto")]
public float alto;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Ángulo de rotación")]
[property: Name("Ángulo")]
public float angulo;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Coeficiente de fricción")]
[property: Name("Coeficiente Fricción")]
public float frictionCoefficient;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Velocidad máxima a 50Hz")]
[property: Name("Velocidad Max 50Hz")]
public float velMax50hz;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Tiempo de rampa")]
[property: Name("Tiempo Rampa")]
public float tiempoRampa;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Estado de marcha")]
[property: Name("En Marcha")]
public bool esMarcha;
private void ActualizarGeometrias()
{
if (_visualRepresentation is ucBasicExample uc)
{
UpdateRectangle(SimGeometria, uc.Transporte, Alto, Ancho, Angulo);
SetSpeed();
}
ActualizarAnimacionStoryBoardTransporte(VelocidadActual);
}
public osBasicExample()
{
Ancho = 1;
Alto = 0.10f;
}
public override void SimulationStop()
{
// Se llama al detener la simulacion
ActualizarAnimacionStoryBoardTransporte(VelocidadActual);
}
public override void UpdateGeometryStart()
{
// Se llama antes de la simulacion
ActualizarGeometrias();
}
public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds)
{
if (_osMotor != null)
{
if (_osMotor is osVMmotorSim motor)
VelocidadActual = motor.Velocidad;
}
else if (Motor.Length > 0)
_osMotor = ObtenerLink(Motor, typeof(osVMmotorSim));
}
public override void ucLoaded()
{
// El UserControl ya se ha cargado y podemos obtener las coordenadas para
// crear el objeto de simulacion
base.ucLoaded();
if (_visualRepresentation is ucBasicExample uc)
{
SimGeometria = AddRectangle(simulationManager, uc.Transporte, Alto, Ancho, Angulo);
CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion);
}
}
public override void ucUnLoaded()
{
// El UserControl se esta eliminando
// eliminar el objeto de simulacion
simulationManager.Remove(SimGeometria);
}
}
public partial class ucBasicExample : UserControl, IDataContainer
{
public osBase? Datos { get; set; }
public ucBasicExample()
{
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 Resize(float width, float height)
{
if (Datos is osBasicExample datos)
datos.Ancho += PixelToMeter.Instance.calc.PixelsToMeters(width);
}
public void Move(float LeftPixels, float TopPixels)
{
if (Datos != null)
{
Datos.Left = PixelToMeter.Instance.calc.PixelsToMeters(LeftPixels);
Datos.Top = PixelToMeter.Instance.calc.PixelsToMeters(TopPixels);
}
}
public float Angle()
{
if (Datos != null)
if (Datos is osBasicExample datos)
return datos.Angulo;
return 0f;
}
public void Rotate(float Angle)
{
if (Datos != null)
if (Datos is osBasicExample datos)
datos.Angulo += Angle;
}
public void Highlight(bool State) { }
public int ZIndex()
{
return 1;
}
}
}

View File

@ -0,0 +1,33 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucSistemaFluidos"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim">
<UserControl.DataContext>
<vm:osSistemaFluidos/>
</UserControl.DataContext>
<Grid>
<Grid x:Name="ContenedorVisual" Background="Transparent">
<!-- El DrawingVisual renderizará las partículas aquí -->
</Grid>
<!-- Panel informativo opcional que se puede ocultar -->
<Border Padding="5"
Background="#60000000"
CornerRadius="5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="5">
<StackPanel>
<TextBlock Text="{Binding Nombre}"
Foreground="White"
FontWeight="Bold"/>
<TextBlock Text="{Binding NumeroParticulas, StringFormat='Partículas: {0}'}"
Foreground="White"/>
<TextBlock Text="{Binding Fps, StringFormat='FPS: {0:F1}'}"
Foreground="White"/>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@ -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
{
/// <summary>
/// Lógica para ucSistemaFluidos.xaml
/// </summary>
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;
}
/// <summary>
/// Actualiza la visualización de partículas
/// </summary>
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);
}
}
}
}
/// <summary>
/// Clase que maneja el rendering optimizado de partículas usando DrawingVisual
/// </summary>
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;
}
/// <summary>
/// Actualiza la visualización de las partículas de fluido
/// </summary>
/// <param name="particulas">Lista de partículas a visualizar</param>
/// <param name="tamañoParticula">Tamaño de las partículas en metros</param>
/// <param name="colorFluido">Color base del fluido</param>
/// <param name="opacidadBase">Opacidad base de las partículas</param>
public void ActualizarParticulasFluido(IEnumerable<Particle> 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();
}
/// <summary>
/// Mezcla dos colores según un factor (0-1)
/// </summary>
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);
}
}
}

View File

@ -0,0 +1,31 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucTuberiaFluido"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CtrEditor.ObjetosSim"
xmlns:ctr="clr-namespace:CtrEditor">
<UserControl.DataContext>
<local:osTuberiaFluido/>
</UserControl.DataContext>
<Grid>
<Path x:Name="TuberiaPath"
Data="{Binding PathData}"
StrokeEndLineCap="Round"
StrokeStartLineCap="Round"
StrokeLineJoin="Round">
<Path.Effect>
<BlurEffect Radius="0.5"/>
</Path.Effect>
</Path>
<!-- Visualización opcional de la densidad del fluido dentro de la tubería -->
<Path x:Name="FluidoPath"
Data="{Binding PathData}"
Opacity="{Binding DensidadFluido}"
StrokeEndLineCap="Round"
StrokeStartLineCap="Round"
StrokeLineJoin="Round">
</Path>
</Grid>
</UserControl>

View File

@ -0,0 +1,268 @@
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
{
/// <summary>
/// Lógica para ucTuberiaFluido.xaml
/// </summary>
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;
}
}
/// <summary>
/// ViewModel para la tubería de fluidos
/// </summary>
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: Category("Configuración")]
[property: Description("Diámetro de la tubería en metros")]
[property: Name("Diámetro")]
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: Category("Apariencia")]
[property: Description("Diámetro interno para visualización del fluido")]
[property: Name("Diámetro Interno")]
private float diametroInterno;
[ObservableProperty]
[property: Category("Apariencia")]
[property: Description("Color de la tubería")]
[property: Name("Color Tubería")]
private System.Windows.Media.Color color = System.Windows.Media.Colors.Gray;
[ObservableProperty]
[property: Category("Apariencia")]
[property: Description("Color del fluido")]
[property: Name("Color Fluido")]
private System.Windows.Media.Color colorFluido = System.Windows.Media.Colors.CornflowerBlue;
[ObservableProperty]
[property: Category("Información")]
[property: Description("Datos del path para dibujar la tubería")]
[property: Name("Path Data")]
private string pathData;
[ObservableProperty]
[property: Category("Simulación")]
[property: Description("Densidad del fluido (0-1)")]
[property: Name("Densidad Fluido")]
private double densidadFluido = 0.7;
// Lista de puntos que forman la tubería
private List<Vector2> _puntos = new List<Vector2>();
// Nombre de la clase para identificación
public static string NombreClase()
{
return "Tubería de Fluido";
}
private string nombre = NombreClase();
[property: Category("Identificación")]
[property: Description("Nombre identificativo del objeto")]
[property: Name("Nombre")]
public override string Nombre
{
get => nombre;
set => SetProperty(ref nombre, value);
}
/// <summary>
/// Agrega un punto a la tubería
/// </summary>
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));
}
}
/// <summary>
/// Actualiza la representación visual de la tubería
/// </summary>
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();
}
/// <summary>
/// Reconstruye la tubería en la simulación
/// </summary>
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);
}
}
}
/// <summary>
/// Constructor
/// </summary>
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);
}
}
}
}

View File

@ -0,0 +1,42 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucValvulaFluido"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CtrEditor.ObjetosSim">
<UserControl.DataContext>
<local:osValvulaFluido/>
</UserControl.DataContext>
<Grid>
<Canvas RenderTransformOrigin="0.5,0.5">
<Canvas.RenderTransform>
<TransformGroup>
<RotateTransform Angle="{Binding Angulo}"/>
</TransformGroup>
</Canvas.RenderTransform>
<!-- Cuerpo de la válvula -->
<Rectangle Width="{Binding Ancho, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding Alto, Converter={StaticResource MeterToPixelConverter}}"
Fill="{Binding Color, Converter={StaticResource ColorToBrushConverter}}"
RadiusX="5" RadiusY="5"
Canvas.Left="{Binding OffsetXRectangulo, Converter={StaticResource MeterToPixelConverter}}"
Canvas.Top="{Binding OffsetYRectangulo, Converter={StaticResource MeterToPixelConverter}}"/>
<!-- Indicador de apertura -->
<Rectangle Width="{Binding AperturaVisual, Converter={StaticResource MeterToPixelConverter}}"
Height="{Binding GrosorIndicador, Converter={StaticResource MeterToPixelConverter}}"
Fill="{Binding ColorIndicador, Converter={StaticResource ColorToBrushConverter}}"
Canvas.Left="{Binding OffsetXIndicador, Converter={StaticResource MeterToPixelConverter}}"
Canvas.Top="{Binding OffsetYIndicador, Converter={StaticResource MeterToPixelConverter}}"/>
<!-- Texto con el valor numérico de apertura -->
<TextBlock Text="{Binding ValorApertura, StringFormat='{}{0:P0}'}"
Canvas.Left="{Binding OffsetXTexto, Converter={StaticResource MeterToPixelConverter}}"
Canvas.Top="{Binding OffsetYTexto, Converter={StaticResource MeterToPixelConverter}}"
FontWeight="Bold"
Foreground="White"
FontSize="12"/>
</Canvas>
</Grid>
</UserControl>

View File

@ -0,0 +1,217 @@
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
{
/// <summary>
/// Lógica para ucValvulaFluido.xaml
/// </summary>
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;
}
}
/// <summary>
/// ViewModel para la válvula de fluidos
/// </summary>
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("Configuración")]
[property: Description("Ancho de la válvula en metros")]
[property: Name("Ancho")]
private float ancho = 0.1f;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Alto de la válvula en metros")]
[property: Name("Alto")]
private float alto = 0.06f;
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Diámetro de la tubería conectada")]
[property: Name("Diámetro Tubería")]
private float diametroTuberia = 0.05f;
// Propiedades visuales
[ObservableProperty]
[property: Category("Apariencia")]
[property: Description("Color de la válvula")]
[property: Name("Color")]
private Color color = Colors.Silver;
[ObservableProperty]
[property: Category("Apariencia")]
[property: Description("Color del indicador de apertura")]
[property: Name("Color Indicador")]
private Color colorIndicador = Colors.CornflowerBlue;
// Propiedades de funcionamiento
[ObservableProperty]
[property: Category("Simulación")]
[property: Description("Apertura de la válvula (0-1)")]
[property: Name("Apertura")]
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: Category("Enlace PLC")]
[property: Description("Tag PLC para lectura/escritura de apertura (0-100%)")]
[property: Name("Tag Apertura")]
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 "Válvula de Fluido";
}
private string nombre = NombreClase();
[property: Category("Identificación")]
[property: Description("Nombre identificativo del objeto")]
[property: Name("Nombre")]
public override string Nombre
{
get => nombre;
set => SetProperty(ref nombre, value);
}
/// <summary>
/// Constructor
/// </summary>
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);
}
}
}
}

View File

@ -14,9 +14,9 @@ namespace CtrEditor.Serialization
{
private readonly DatosDeTrabajo _datosDeTrabajo;
private readonly MainViewModel _mainViewModel;
private readonly SimulationManagerBEPU _simulationManager;
private readonly SimulationManagerFP _simulationManager;
public StateSerializer(MainViewModel mainViewModel, DatosDeTrabajo datosDeTrabajo, SimulationManagerBEPU simulationManager)
public StateSerializer(MainViewModel mainViewModel, DatosDeTrabajo datosDeTrabajo, SimulationManagerFP simulationManager)
{
_mainViewModel = mainViewModel;
_datosDeTrabajo = datosDeTrabajo;

1054
Simulacion/Aether.cs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,240 +0,0 @@
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.Simulacion
{
/// <summary>
/// Clase centralizada para manejar todas las conversiones entre coordenadas WPF y BEPU
/// WPF: Y hacia abajo, ángulos en sentido horario, Top-Left como referencia
/// BEPU: Y hacia arriba, ángulos en sentido antihorario, Center como referencia
/// </summary>
public static class CoordinateConverter
{
/// <summary>
/// Convierte ángulo de WPF a BEPU (invierte signo) - USO INTERNO
/// </summary>
private static float WpfAngleToBepuAngle(float wpfAngle)
{
return -wpfAngle;
}
/// <summary>
/// Convierte ángulo de BEPU a WPF (invierte signo) - USO INTERNO
/// </summary>
private static float BepuAngleToWpfAngle(float bepuAngle)
{
return -bepuAngle;
}
/// <summary>
/// Convierte posición Y de WPF a BEPU (invierte signo) - USO INTERNO
/// </summary>
internal static float WpfYToBepuY(float wpfY)
{
return -wpfY;
}
/// <summary>
/// Convierte posición Y de BEPU a WPF (invierte signo) - RETORNA VALOR WPF
/// </summary>
public static float BepuYToWpfY(float bepuY)
{
return -bepuY;
}
/// <summary>
/// Convierte Vector2 de WPF a BEPU (solo invierte Y) - USO INTERNO
/// </summary>
internal static Vector2 WpfToBepuVector2(Vector2 wpfVector)
{
return new Vector2(wpfVector.X, -wpfVector.Y);
}
/// <summary>
/// Convierte Vector2 de BEPU a WPF (solo invierte Y) - RETORNA VALOR WPF
/// </summary>
public static Vector2 BepuToWpfVector2(Vector2 bepuVector)
{
return new Vector2(bepuVector.X, -bepuVector.Y);
}
/// <summary>
/// Convierte Vector3 de BEPU a Vector2 WPF (proyección XY con Y invertida) - RETORNA VALOR WPF
/// </summary>
public static Vector2 BepuVector3ToWpfVector2(Vector3 bepuVector)
{
return new Vector2(bepuVector.X, -bepuVector.Y);
}
/// <summary>
/// Calcula la posición del centro en coordenadas BEPU desde Top-Left WPF - USO INTERNO
/// Maneja correctamente la rotación del objeto
/// </summary>
internal static Vector3 CalculateBepuCenterFromWpfTopLeft(Vector2 wpfTopLeft, float width, float height, float wpfAngle, float zPosition)
{
// Calcular el offset del centro desde Top-Left (sin rotación)
var offsetX = width / 2f;
var offsetY = height / 2f;
// Convertir ángulo WPF a radianes para cálculos trigonométricos
var angleRadians = simBase.GradosARadianes(wpfAngle);
var cos = (float)Math.Cos(angleRadians);
var sin = (float)Math.Sin(angleRadians);
// Rotar el offset alrededor del Top-Left usando el ángulo WPF
var rotatedOffsetX = offsetX * cos - offsetY * sin;
var rotatedOffsetY = offsetX * sin + offsetY * cos;
// Calcular nueva posición del centro manteniendo Top-Left fijo
var centerX = wpfTopLeft.X + rotatedOffsetX;
var centerY = wpfTopLeft.Y + rotatedOffsetY;
// Convertir a 3D con Y invertida para BEPU
var bepuY = WpfYToBepuY(centerY);
var result = new Vector3(centerX, bepuY, zPosition);
return result;
}
/// <summary>
/// Calcula la posición Top-Left WPF desde el centro BEPU - RETORNA VALOR WPF
/// Maneja correctamente la rotación del objeto
/// </summary>
public static Vector2 CalculateWpfTopLeftFromBepuCenter(Vector3 bepuCenter, float width, float height, float wpfAngle)
{
// Convertir centro BEPU a WPF
var wpfCenterX = bepuCenter.X;
var wpfCenterY = BepuYToWpfY(bepuCenter.Y);
// Calcular el offset del centro al Top-Left (sin rotación)
var offsetX = -width / 2f;
var offsetY = -height / 2f;
// Convertir ángulo WPF a radianes para cálculos trigonométricos
var angleRadians = simBase.GradosARadianes(wpfAngle);
var cos = (float)Math.Cos(angleRadians);
var sin = (float)Math.Sin(angleRadians);
// Rotar el offset usando el ángulo WPF
var rotatedOffsetX = offsetX * cos - offsetY * sin;
var rotatedOffsetY = offsetX * sin + offsetY * cos;
// Calcular Top-Left desde el centro
var topLeftX = wpfCenterX + rotatedOffsetX;
var topLeftY = wpfCenterY + rotatedOffsetY;
return new Vector2(topLeftX, topLeftY);
}
/// <summary>
/// Crea un Quaternion para BEPU desde un ángulo WPF - USO INTERNO
/// </summary>
internal static Quaternion CreateBepuQuaternionFromWpfAngle(float wpfAngle)
{
var bepuAngle = WpfAngleToBepuAngle(wpfAngle);
return Quaternion.CreateFromAxisAngle(Vector3.UnitZ, simBase.GradosARadianes(bepuAngle));
}
/// <summary>
/// Extrae el ángulo WPF desde un Quaternion BEPU - RETORNA VALOR WPF
/// </summary>
public static float ExtractWpfAngleFromBepuQuaternion(Quaternion bepuQuaternion)
{
// Extraer ángulo Z del quaternion
var bepuAngleRadians = (float)Math.Atan2(
2.0 * (bepuQuaternion.W * bepuQuaternion.Z + bepuQuaternion.X * bepuQuaternion.Y),
1.0 - 2.0 * (bepuQuaternion.Y * bepuQuaternion.Y + bepuQuaternion.Z * bepuQuaternion.Z)
);
var bepuAngleDegrees = simBase.RadianesAGrados(bepuAngleRadians);
return BepuAngleToWpfAngle(bepuAngleDegrees);
}
/// <summary>
/// Actualiza la posición de un body BEPU manteniendo su rotación actual - USO INTERNO
/// </summary>
internal static void UpdateBepuBodyPosition(Simulation simulation, BodyHandle bodyHandle, Vector3 newBepuPosition)
{
if (simulation != null && simulation.Bodies.BodyExists(bodyHandle))
{
var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle);
bodyReference.Pose.Position = newBepuPosition;
}
}
/// <summary>
/// Actualiza la rotación de un body BEPU manteniendo su posición actual - USO INTERNO
/// </summary>
internal static void UpdateBepuBodyRotation(Simulation simulation, BodyHandle bodyHandle, float wpfAngle)
{
if (simulation != null && simulation.Bodies.BodyExists(bodyHandle))
{
var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle);
bodyReference.Pose.Orientation = CreateBepuQuaternionFromWpfAngle(wpfAngle);
}
}
/// <summary>
/// Actualiza posición y rotación de un body BEPU simultáneamente - USO INTERNO
/// </summary>
internal static void UpdateBepuBodyPose(Simulation simulation, BodyHandle bodyHandle, Vector3 newBepuPosition, float wpfAngle)
{
if (simulation != null && simulation.Bodies.BodyExists(bodyHandle))
{
var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle);
bodyReference.Pose.Position = newBepuPosition;
bodyReference.Pose.Orientation = CreateBepuQuaternionFromWpfAngle(wpfAngle);
}
}
/// <summary>
/// Obtiene la posición del centro en coordenadas BEPU - USO INTERNO
/// </summary>
internal static Vector3 GetBepuBodyPosition(Simulation simulation, BodyHandle bodyHandle)
{
if (simulation != null && simulation.Bodies.BodyExists(bodyHandle))
{
var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle);
return bodyReference.Pose.Position;
}
return Vector3.Zero;
}
/// <summary>
/// Obtiene el ángulo WPF desde un body BEPU - RETORNA VALOR WPF
/// </summary>
public static float GetWpfAngleFromBepuBody(Simulation simulation, BodyHandle bodyHandle)
{
if (simulation != null && simulation.Bodies.BodyExists(bodyHandle))
{
var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle);
return ExtractWpfAngleFromBepuQuaternion(bodyReference.Pose.Orientation);
}
return 0f;
}
/// <summary>
/// ✅ NUEVO: Convierte directamente de grados WPF a radianes BEPU - USO INTERNO
/// Maneja tanto la inversión de signo como la conversión a radianes en una sola operación
/// </summary>
internal static float WpfDegreesToBepuRadians(float wpfDegrees)
{
return simBase.GradosARadianes(WpfAngleToBepuAngle(wpfDegrees));
}
/// <summary>
/// ✅ NUEVO: Convierte directamente de radianes BEPU a grados WPF - RETORNA VALOR WPF
/// Maneja tanto la conversión a grados como la inversión de signo en una sola operación
/// </summary>
public static float BepuRadiansToWpfDegrees(float bepuRadians)
{
return BepuAngleToWpfAngle(simBase.RadianesAGrados(bepuRadians));
}
}
}

641
Simulacion/FPhysics.cs Normal file
View File

@ -0,0 +1,641 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Factories;
using FarseerPhysics.Collision.Shapes;
using nkast.Aether.Physics2D.Common;
using FarseerPhysics.Common;
using System.Windows;
using System.Diagnostics;
using FarseerPhysics.Dynamics.Joints;
using CtrEditor.ObjetosSim;
using System.Windows.Documents;
namespace CtrEditor.Simulacion
{
public class simBase
{
public Body Body { get; protected set; }
public World _world;
public void RemoverBody()
{
if (Body != null)
{
_world.RemoveBody(Body);
}
}
public void SetPosition(float x, float y)
{
Body.SetTransform(new Vector2(x, y), Body.Rotation);
}
public void SetPosition(Vector2 centro)
{
Body.SetTransform(centro, Body.Rotation);
}
}
public class simCurve : simBase
{
private float _innerRadius;
private float _outerRadius;
private float _startAngle;
private float _endAngle;
public float Speed { get; set; } // Velocidad para efectos de cinta transportadora
public simCurve(World world, float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 position)
{
_world = world;
_innerRadius = innerRadius;
_outerRadius = outerRadius;
_startAngle = MathHelper.ToRadians(startAngle);
_endAngle = MathHelper.ToRadians(endAngle);
Create(position);
}
public void Create(float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 position)
{
if (_world == null) return;
_innerRadius = innerRadius;
_outerRadius = outerRadius;
_startAngle = MathHelper.ToRadians(startAngle);
_endAngle = MathHelper.ToRadians(endAngle);
Create(position);
}
public void Create(Vector2 position)
{
RemoverBody();
// Crear la geometría del sensor de curva
List<Vertices> segments = CreateCurveVertices(_innerRadius, _outerRadius, _startAngle, _endAngle);
Body = new Body(_world);
foreach (var segment in segments)
{
var shape = new PolygonShape(segment, 1f);
var fixture = Body.CreateFixture(shape);
fixture.IsSensor = true;
}
Body.Position = position;
Body.BodyType = BodyType.Static;
Body.UserData = this;
}
public void SetSpeed(float speed)
{
Speed = speed;
}
private List<Vertices> CreateCurveVertices(float innerRadius, float outerRadius, float startAngle, float endAngle)
{
List<Vertices> verticesList = new List<Vertices>();
int segments = 32;
float angleStep = (endAngle - startAngle) / segments;
Vertices innerVertices = new Vertices();
Vertices outerVertices = new Vertices();
for (int i = 0; i <= segments; i++)
{
float angle = startAngle + i * angleStep;
innerVertices.Add(new Vector2(innerRadius * (float)Math.Cos(angle), innerRadius * (float)Math.Sin(angle)));
outerVertices.Add(new Vector2(outerRadius * (float)Math.Cos(angle), outerRadius * (float)Math.Sin(angle)));
}
outerVertices.Reverse();
innerVertices.AddRange(outerVertices);
verticesList.Add(innerVertices);
return verticesList;
}
public void ApplyCurveEffect(Fixture bottle)
{
Vector2 centerToBottle = bottle.Body.Position - Body.Position;
float distanceToCenter = centerToBottle.Length();
if (distanceToCenter >= _innerRadius && distanceToCenter <= _outerRadius)
{
// Calcular la velocidad tangencial
float speedMetersPerSecond = Speed / 60.0f;
float angularVelocity = speedMetersPerSecond / distanceToCenter;
// Vector tangente (perpendicular al radio)
Vector2 tangent = new Vector2(-centerToBottle.Y, centerToBottle.X);
tangent.Normalize();
// Velocidad deseada
Vector2 desiredVelocity = tangent * angularVelocity * distanceToCenter;
bottle.Body.LinearVelocity = desiredVelocity;
}
}
}
public class simDescarte : simBase
{
private float _radius;
public simDescarte(World world, float diameter, Vector2 position)
{
_world = world;
_radius = diameter / 2;
Create(position);
}
public void SetDiameter(float diameter)
{
_radius = diameter / 2;
Create(Body.Position); // Recrear el círculo con el nuevo tamaño
}
public void Create(Vector2 position)
{
RemoverBody();
Body = BodyFactory.CreateCircle(_world, _radius, 1f, position);
Body.FixtureList[0].IsSensor = true;
Body.BodyType = BodyType.Static;
Body.UserData = this; // Importante para la identificación durante la colisión
}
}
public class simTransporte : simBase
{
public float Speed { get; set; } // Velocidad para efectos de cinta transportadora
public float DistanceGuide2Guide { get; set; }
public bool TransportWithGuides = false;
public simTransporte(World world, float width, float height, Vector2 position, float angle = 0)
{
_world = world;
Create(width, height, position, angle);
}
public float Angle
{
get { return MathHelper.ToDegrees(Body.Rotation); }
set { Body.Rotation = MathHelper.ToRadians(value); }
}
public new void SetPosition(float x, float y)
{
Body.Position = new Vector2(x, y);
}
public void SetSpeed(float speed)
{
Speed = speed;
}
public void SetDimensions(float width, float height)
{
Body.DestroyFixture(Body.FixtureList[0]);
var newShape = new PolygonShape(PolygonTools.CreateRectangle(width / 2, height / 2), 1f);
Body.CreateFixture(newShape);
}
public void Create(float width, float height, Vector2 position, float angle = 0)
{
RemoverBody();
Body = BodyFactory.CreateRectangle(_world, width, height, 1f, position);
Body.FixtureList[0].IsSensor = true;
Body.BodyType = BodyType.Static;
Body.Rotation = MathHelper.ToRadians(angle);
Body.UserData = this; // Importante para la identificación durante la colisión
}
}
public class simBarrera : simBase
{
public bool LuzCortada = false;
public simBarrera(World world, float width, float height, Vector2 position, float angle = 0)
{
_world = world;
Create(width, height, position, angle);
}
public float Angle
{
get { return MathHelper.ToDegrees(Body.Rotation); }
set { Body.Rotation = MathHelper.ToRadians(value); }
}
public new void SetPosition(float x, float y)
{
Body.Position = new Vector2(x, y);
}
public void SetDimensions(float width, float height)
{
Body.DestroyFixture(Body.FixtureList[0]);
var newShape = new PolygonShape(PolygonTools.CreateRectangle(width / 2, height / 2), 1f);
Body.CreateFixture(newShape);
}
public void Create(float width, float height, Vector2 position, float angle = 0)
{
RemoverBody();
Body = BodyFactory.CreateRectangle(_world, width, height, 1f, position);
Body.FixtureList[0].IsSensor = true;
Body.BodyType = BodyType.Static;
Body.Rotation = MathHelper.ToRadians(angle);
Body.UserData = this; // Importante para la identificación durante la colisión
LuzCortada = false;
}
}
public class simGuia : simBase
{
public simGuia(World world, Vector2 start, Vector2 end)
{
_world = world;
Create(start, end);
}
public void Create(Vector2 start, Vector2 end)
{
RemoverBody();
Body = BodyFactory.CreateEdge(_world, start, end);
Body.BodyType = BodyType.Static;
Body.UserData = this; // Importante para la identificación durante la colisión
}
public void UpdateVertices(Vector2 newStart, Vector2 newEnd)
{
Create(newStart, newEnd); // Recrear la línea con nuevos vértices
}
}
public class simBotella : simBase
{
private float _radius;
private float _mass;
public bool Descartar = false;
public simBotella(World world, float diameter, Vector2 position, float mass)
{
_world = world;
_radius = diameter / 2;
_mass = mass;
Create(position);
}
public float CenterX
{
get { return Body.Position.X; }
set { }
}
public float CenterY
{
get { return Body.Position.Y; }
set { }
}
public Vector2 Center
{
get { return Body.Position; }
}
public float Mass
{
get
{
if (_mass <= 0)
_mass = 1;
return _mass;
}
set { _mass = value; }
}
private void Create(Vector2 position)
{
RemoverBody();
Body = BodyFactory.CreateCircle(_world, _radius, 0.2f, position);
Body.BodyType = BodyType.Dynamic;
// Restablecer manejador de eventos de colisión
Body.OnCollision += HandleCollision;
//Body.OnSeparation += HandleOnSeparation;
Body.UserData = this; // Importante para la identificación durante la colisión
// Configurar la fricción
Body.Friction = 0.3f; // Ajustar según sea necesario para tu simulación
// Configurar amortiguamiento
Body.LinearDamping = 0.4f; // Ajustar para controlar la reducción de la velocidad lineal
Body.AngularDamping = 0.4f; // Ajustar para controlar la reducción de la velocidad angular
Body.Restitution = 0.2f; // Baja restitución para menos rebote
// Body.IsBullet = true;
}
public void SetDiameter(float diameter)
{
_radius = diameter / 2;
Create(Body.Position); // Recrear el círculo con el nuevo tamaño
}
public void SetMass(float mass)
{
Mass = mass;
}
private bool HandleCollision(Fixture fixtureA, Fixture fixtureB, FarseerPhysics.Dynamics.Contacts.Contact contact)
{
if (fixtureB.Body.UserData is simBarrera Sensor)
{
Sensor.LuzCortada = true;
return true;
}
else if (fixtureB.Body.UserData is simCurve curve)
{
curve.ApplyCurveEffect(fixtureA);
return true; // No aplicar respuestas físicas
}
else if (fixtureB.Body.UserData is simDescarte)
{
Descartar = true;
return true;
} else if (fixtureB.Body.UserData is simTransporte)
{
simTransporte conveyor = fixtureB.Body.UserData as simTransporte;
if ( conveyor.Speed != 0 ) {
CircleShape circleShape = fixtureA.Shape as CircleShape;
PolygonShape polygonShape = fixtureB.Shape as PolygonShape;
// Obtener centro y radio del círculo
Vector2 centroCirculo = fixtureA.Body.Position;
float radio = circleShape.Radius;
// Obtener los vértices del polígono (rectángulo)
Vector2[] vertices = new Vector2[polygonShape.Vertices.Count];
float cos = (float)Math.Cos(fixtureB.Body.Rotation);
float sin = (float)Math.Sin(fixtureB.Body.Rotation);
for (int i = 0; i < polygonShape.Vertices.Count; i++)
{
Vector2 vertex = polygonShape.Vertices[i];
float rotatedX = vertex.X * cos - vertex.Y * sin + fixtureB.Body.Position.X;
float rotatedY = vertex.X * sin + vertex.Y * cos + fixtureB.Body.Position.Y;
vertices[i] = new Vector2(rotatedX, rotatedY);
}
// Calcular el porcentaje de la superficie compartida
float porcentajeCompartido = InterseccionCirculoRectangulo.CalcularSuperficieCompartida(vertices, centroCirculo, radio);
// Aplicar el efecto del transportador usando el porcentaje calculado
if (conveyor.TransportWithGuides)
if (conveyor.DistanceGuide2Guide <= radio * 2)
CenterFixtureOnConveyor(fixtureA, conveyor);
ApplyConveyorEffect(conveyor, fixtureA, porcentajeCompartido);
}
return true; // No aplicar respuestas físicas
}
return true; // No aplicar respuestas físicas
}
private void ApplyConveyorEffect(simTransporte conveyor, Fixture circleFixture, float porcentajeCompartido)
{
float speedMetersPerSecond = conveyor.Speed / 60.0f;
Vector2 desiredVelocity = new Vector2((float)Math.Cos(conveyor.Body.Rotation), (float)Math.Sin(conveyor.Body.Rotation)) * speedMetersPerSecond;
circleFixture.Body.LinearVelocity += desiredVelocity * porcentajeCompartido;
}
private void CenterFixtureOnConveyor(Fixture fixtureA, simTransporte conveyor)
{
// Obtener el centro del conveyor
Vector2 conveyorCenter = conveyor.Body.Position;
// Calcular el vector de la línea horizontal centrada de conveyor
float halfDistance = conveyor.DistanceGuide2Guide / 2;
float cos = (float)Math.Cos(conveyor.Body.Rotation);
float sin = (float)Math.Sin(conveyor.Body.Rotation);
Vector2 offset = new Vector2(halfDistance * cos, halfDistance * sin);
// Línea horizontal centrada de conveyor en el espacio del mundo
Vector2 lineStart = conveyorCenter - offset;
Vector2 lineEnd = conveyorCenter + offset;
// Proyectar el centro de fixtureA sobre la línea horizontal
Vector2 fixtureCenter = fixtureA.Body.Position;
Vector2 closestPoint = ProjectPointOntoLine(fixtureCenter, lineStart, lineEnd);
// Mover fixtureA al punto más cercano en la línea horizontal
fixtureA.Body.Position = closestPoint;
}
private Vector2 ProjectPointOntoLine(Vector2 point, Vector2 lineStart, Vector2 lineEnd)
{
Vector2 lineDirection = lineEnd - lineStart;
lineDirection.Normalize();
Vector2 pointToLineStart = point - lineStart;
float projectionLength = Vector2.Dot(pointToLineStart, lineDirection);
return lineStart + projectionLength * lineDirection;
}
}
public class SimulationManagerFP
{
private World world;
private Canvas simulationCanvas;
public List<simBase> Cuerpos;
private Stopwatch stopwatch;
private double stopwatch_last;
public Canvas DebugCanvas { get => simulationCanvas; set => simulationCanvas = value; }
public SimulationManagerFP()
{
world = new World(new Vector2(0, 0)); // Vector2.Zero
Cuerpos = new List<simBase>();
stopwatch = new Stopwatch();
stopwatch.Start();
}
public void Clear()
{
if (world.BodyList.Count > 0)
world.Clear();
if (Cuerpos.Count > 0)
Cuerpos.Clear();
}
public void Step()
{
// Detener el cronómetro y obtener el tiempo transcurrido en milisegundos
float elapsedMilliseconds = (float) (stopwatch.Elapsed.TotalMilliseconds - stopwatch_last);
stopwatch_last = stopwatch.Elapsed.TotalMilliseconds;
// Pasar el tiempo transcurrido al método Step
world.Step(elapsedMilliseconds / 1000.0f);
}
public void Remove(simBase Objeto)
{
if (Objeto != null)
{
Objeto.RemoverBody();
Cuerpos.Remove(Objeto);
}
}
public simCurve AddCurve(float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 position)
{
simCurve curva = new simCurve( world, innerRadius, outerRadius, startAngle, endAngle, position);
Cuerpos.Add(curva);
return curva;
}
public simBotella AddCircle(float diameter, Vector2 position, float mass)
{
simBotella circle = new simBotella(world, diameter, position, mass);
Cuerpos.Add(circle);
return circle;
}
public simTransporte AddRectangle(float width, float height, Vector2 position, float angle)
{
simTransporte rectangle = new simTransporte(world, width, height, position, angle);
Cuerpos.Add(rectangle);
return rectangle;
}
public simBarrera AddBarrera(float width, float height, Vector2 position, float angle)
{
simBarrera rectangle = new simBarrera(world, width, height, position, angle);
Cuerpos.Add(rectangle);
return rectangle;
}
public simGuia AddLine(Vector2 start, Vector2 end)
{
simGuia line = new simGuia(world, start, end);
Cuerpos.Add(line);
return line;
}
public simDescarte AddDescarte(float diameter, Vector2 position)
{
simDescarte descarte = new simDescarte(world, diameter, position);
Cuerpos.Add(descarte);
return descarte;
}
public void Debug_DrawInitialBodies()
{
Debug_ClearSimulationShapes();
world.Step(0.01f); // Para actualizar la BodyList
foreach (Body body in world.BodyList)
{
foreach (Fixture fixture in body.FixtureList)
{
DrawShape(fixture);
}
}
}
public void Debug_ClearSimulationShapes()
{
var simulationShapes = simulationCanvas.Children.OfType<System.Windows.Shapes.Shape>().Where(s => s.Tag as string == "Simulation").ToList();
foreach (var shape in simulationShapes)
{
simulationCanvas.Children.Remove(shape);
}
}
private void DrawShape(Fixture fixture)
{
System.Windows.Shapes.Shape shape;
switch (fixture.ShapeType)
{
case ShapeType.Circle:
shape = DrawCircle(fixture);
break;
case ShapeType.Polygon:
shape = DrawPolygon(fixture);
break;
case ShapeType.Edge:
shape = DrawEdge(fixture);
break;
default:
return;
}
shape.Tag = "Simulation"; // Marcar para simulación
Canvas.SetZIndex(shape, 20);
simulationCanvas.Children.Add(shape);
}
private float p(float x)
{
float c = PixelToMeter.Instance.calc.MetersToPixels(x);
return c;
}
private System.Windows.Shapes.Shape DrawEdge(Fixture fixture)
{
EdgeShape edge = fixture.Shape as EdgeShape;
Line line = new Line
{
X1 = p(edge.Vertex1.X + fixture.Body.Position.X), // Aplicar escala y posición
Y1 = p(edge.Vertex1.Y + fixture.Body.Position.Y),
X2 = p(edge.Vertex2.X + fixture.Body.Position.X),
Y2 = p(edge.Vertex2.Y + fixture.Body.Position.Y),
Stroke = Brushes.Black,
StrokeThickness = 2
};
return line;
}
private System.Windows.Shapes.Shape DrawCircle(Fixture fixture)
{
CircleShape circle = fixture.Shape as CircleShape;
Ellipse ellipse = new Ellipse
{
Width = p(circle.Radius * 2), // Escalado para visualización
Height = p(circle.Radius * 2), // Escalado para visualización
Stroke = Brushes.Black,
StrokeThickness = 2
};
Canvas.SetLeft(ellipse, p(fixture.Body.Position.X - circle.Radius));
Canvas.SetTop(ellipse, p(fixture.Body.Position.Y - circle.Radius));
return ellipse;
}
private System.Windows.Shapes.Shape DrawPolygon(Fixture fixture)
{
Polygon polygon = new Polygon { Stroke = Brushes.Black, StrokeThickness = 2 };
PolygonShape polyShape = fixture.Shape as PolygonShape;
float cos = (float)Math.Cos(fixture.Body.Rotation);
float sin = (float)Math.Sin(fixture.Body.Rotation);
foreach (Vector2 vertex in polyShape.Vertices)
{
float rotatedX = vertex.X * cos - vertex.Y * sin + fixture.Body.Position.X;
float rotatedY = vertex.X * sin + vertex.Y * cos + fixture.Body.Position.Y;
polygon.Points.Add(new Point(p(rotatedX), p(rotatedY)));
}
return polygon;
}
}
}

View File

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using nkast.Aether.Physics2D.Common;
using CtrEditor.Simulacion.Fluids;
using CtrEditor.ObjetosSim;
namespace CtrEditor.Simulacion
{
/// <summary>
/// Gestor para la simulación de fluidos independiente de la simulación física principal
/// </summary>
public class FluidSimulationManager
{
private SimulacionFluidos _simulacion;
private readonly List<Action> _deferredActions = new List<Action>();
private readonly List<osSistemaFluidos> _sistemasRegistrados = new List<osSistemaFluidos>();
private float _defaultWidth = 10.0f;
private float _defaultHeight = 10.0f;
private bool _isRunning = false;
private Stopwatch _stopwatch;
private double _lastUpdateTime;
public bool IsRunning => _isRunning;
/// <summary>
/// Constructor
/// </summary>
public FluidSimulationManager()
{
_stopwatch = new Stopwatch();
}
/// <summary>
/// Inicializa la simulación de fluidos
/// </summary>
public void Initialize()
{
if (_simulacion == null)
{
_simulacion = new SimulacionFluidos(
_defaultWidth,
_defaultHeight,
10000, // max partículas
new Vector2(0, 9.8f) // gravedad por defecto
);
}
}
/// <summary>
/// Inicia la simulación de fluidos
/// </summary>
public void Start()
{
if (!_isRunning)
{
_isRunning = true;
_stopwatch.Start();
_lastUpdateTime = _stopwatch.Elapsed.TotalMilliseconds;
// Notificar a los sistemas de fluidos
foreach (var sistema in _sistemasRegistrados)
{
sistema.OnFluidSimulationStart();
}
}
}
/// <summary>
/// Detiene la simulación de fluidos
/// </summary>
public void Stop()
{
if (_isRunning)
{
_isRunning = false;
_stopwatch.Stop();
_stopwatch.Reset();
// Notificar a los sistemas de fluidos
foreach (var sistema in _sistemasRegistrados)
{
sistema.OnFluidSimulationStop();
}
}
}
/// <summary>
/// Actualiza un paso de la simulación de fluidos
/// </summary>
public void Step()
{
if (!_isRunning) return;
// Calcular delta time
double currentTime = _stopwatch.Elapsed.TotalMilliseconds;
float deltaTime = (float)(currentTime - _lastUpdateTime) / 1000.0f;
_lastUpdateTime = currentTime;
// Asegurar que deltaTime no es demasiado grande (evita inestabilidades)
if (deltaTime > 0.05f) deltaTime = 0.05f;
// Procesar acciones diferidas
foreach (var action in _deferredActions)
{
action();
}
_deferredActions.Clear();
// Actualizar la simulación
_simulacion?.Actualizar(deltaTime);
// Notificar a los sistemas de fluidos
foreach (var sistema in _sistemasRegistrados)
{
sistema.UpdateFluidSimulation(deltaTime);
}
}
/// <summary>
/// Registra un sistema de fluidos para ser actualizado
/// </summary>
public void RegisterFluidSystem(osSistemaFluidos sistema)
{
if (!_sistemasRegistrados.Contains(sistema))
{
_sistemasRegistrados.Add(sistema);
// Si el sistema ya está corriendo, notificar al nuevo sistema
if (_isRunning)
{
sistema.OnFluidSimulationStart();
}
}
}
/// <summary>
/// Elimina un sistema de fluidos del registro
/// </summary>
public void UnregisterFluidSystem(osSistemaFluidos sistema)
{
_sistemasRegistrados.Remove(sistema);
}
/// <summary>
/// Añade una acción para ser ejecutada en el próximo paso de simulación
/// </summary>
public void AddDeferredAction(Action action)
{
_deferredActions.Add(action);
}
/// <summary>
/// Obtiene la instancia de simulación de fluidos
/// </summary>
public SimulacionFluidos GetSimulacion()
{
if (_simulacion == null)
{
Initialize();
}
return _simulacion;
}
/// <summary>
/// Limpia los recursos de la simulación
/// </summary>
public void Clear()
{
Stop();
_simulacion = null;
_sistemasRegistrados.Clear();
_deferredActions.Clear();
}
}
}

View File

@ -0,0 +1,121 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using nkast.Aether.Physics2D.Common;
namespace tainicom.Aether.Physics2D.Fluids
{
/// <summary>
/// Fluid parameters, see pvfs.pdf for a detailed explanation
/// </summary>
public struct FluidDefinition
{
/// <summary>
/// Distance of influence between the particles
/// </summary>
public float InfluenceRadius;
/// <summary>
/// Density of the fluid
/// </summary>
public float DensityRest;
/// <summary>
/// Stiffness of the fluid (when particles are far)
/// </summary>
public float Stiffness;
/// <summary>
/// Stiffness of the fluid (when particles are near)
/// Set by Check()
/// </summary>
public float StiffnessNear;
/// <summary>
/// Toggles viscosity forces
/// </summary>
public bool UseViscosity;
/// <summary>
/// See pvfs.pdf for more information
/// </summary>
public float ViscositySigma;
/// <summary>
/// See pvfs.pdf for more information
/// </summary>
public float ViscosityBeta;
/// <summary>
/// Toggles plasticity computation (springs etc.)
/// </summary>
public bool UsePlasticity;
/// <summary>
/// Plasticity, amount of memory of the shape
/// See pvfs.pdf for more information
/// </summary>
public float Plasticity;
/// <summary>
/// K of the springs used for plasticity
/// </summary>
public float KSpring;
/// <summary>
/// Amount of change of the rest length of the springs (when compressed)
/// </summary>
public float YieldRatioCompress;
/// <summary>
/// Amount of change of the rest length of the springs (when stretched)
/// </summary>
public float YieldRatioStretch;
public static FluidDefinition Default
{
get
{
FluidDefinition def = new FluidDefinition
{
InfluenceRadius = 1.0f,
DensityRest = 10.0f,
Stiffness = 10.0f,
StiffnessNear = 0.0f, // Set by Check()
UseViscosity = false,
ViscositySigma = 10.0f,
ViscosityBeta = 0.0f,
UsePlasticity = false,
Plasticity = 0.3f,
KSpring = 2.0f,
YieldRatioCompress = 0.1f,
YieldRatioStretch = 0.1f
};
def.Check();
return def;
}
}
public void Check()
{
InfluenceRadius = MathUtils.Clamp(InfluenceRadius, 0.1f, 10.0f);
DensityRest = MathUtils.Clamp(DensityRest, 1.0f, 100.0f);
Stiffness = MathUtils.Clamp(Stiffness, 0.1f, 10.0f);
StiffnessNear = Stiffness * 100.0f; // See pvfs.pdf
ViscositySigma = Math.Max(ViscositySigma, 0.0f);
ViscosityBeta = Math.Max(ViscosityBeta, 0.0f);
Plasticity = Math.Max(Plasticity, 0.0f);
KSpring = Math.Max(KSpring, 0.0f);
YieldRatioCompress = Math.Max(YieldRatioCompress, 0.0f);
YieldRatioStretch = Math.Max(YieldRatioStretch, 0.0f);
}
}
}

View File

@ -0,0 +1,80 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using System.Collections.Generic;
using nkast.Aether.Physics2D.Common;
namespace tainicom.Aether.Physics2D.Fluids
{
public class FluidParticle
{
public Vector2 Position;
public Vector2 PreviousPosition;
public Vector2 Velocity;
public Vector2 Acceleration;
internal FluidParticle(Vector2 position)
{
Neighbours = new List<FluidParticle>();
IsActive = true;
MoveTo(position);
Damping = 0.0f;
Mass = 1.0f;
}
public bool IsActive { get; set; }
public List<FluidParticle> Neighbours { get; private set; }
// For gameplay purposes
public float Density { get; internal set; }
public float Pressure { get; internal set; }
// Other properties
public int Index { get; internal set; }
// Physics properties
public float Damping { get; set; }
public float Mass { get; set; }
public void MoveTo(Vector2 p)
{
Position = p;
PreviousPosition = p;
Velocity = Vector2.Zero;
Acceleration = Vector2.Zero;
}
public void ApplyForce(ref Vector2 force)
{
Acceleration += force * Mass;
}
public void ApplyImpulse(ref Vector2 impulse)
{
Velocity += impulse;
}
public void Update(float deltaTime)
{
Velocity += Acceleration * deltaTime;
Vector2 delta = (1.0f - Damping) * Velocity * deltaTime;
PreviousPosition = Position;
Position += delta;
Acceleration = Vector2.Zero;
}
public void UpdateVelocity(float deltaTime)
{
Velocity = (Position - PreviousPosition) / deltaTime;
}
}
}

View File

@ -0,0 +1,413 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using System;
using System.Collections.Generic;
using nkast.Aether.Physics2D.Common;
namespace tainicom.Aether.Physics2D.Fluids
{
public class FluidSystem1
{
private float _influenceRadiusSquared;
private HashGrid _hashGrid = new HashGrid();
private Dictionary<SpringHash, Spring> _springs = new Dictionary<SpringHash, Spring>();
private List<SpringHash> _springsToRemove = new List<SpringHash>();
private Vector2 _totalForce;
public FluidSystem1(Vector2 gravity)
{
Gravity = gravity;
Particles = new List<FluidParticle>();
DefaultDefinition();
}
public FluidDefinition Definition { get; private set; }
public List<FluidParticle> Particles { get; private set; }
public int ParticlesCount { get { return Particles.Count; } }
public Vector2 Gravity { get; set; }
public void DefaultDefinition()
{
SetDefinition(FluidDefinition.Default);
}
public void SetDefinition(FluidDefinition def)
{
Definition = def;
Definition.Check();
_influenceRadiusSquared = Definition.InfluenceRadius * Definition.InfluenceRadius;
}
public FluidParticle AddParticle(Vector2 position)
{
FluidParticle particle = new FluidParticle(position) { Index = Particles.Count };
Particles.Add(particle);
return particle;
}
public void Clear()
{
//TODO
}
public void ApplyForce(Vector2 f)
{
_totalForce += f;
}
private void ApplyForces()
{
Vector2 f = Gravity + _totalForce;
for (int i = 0; i < Particles.Count; ++i)
{
Particles[i].ApplyForce(ref f);
}
_totalForce = Vector2.Zero;
}
private void ApplyViscosity(FluidParticle p, float timeStep)
{
for (int i = 0; i < p.Neighbours.Count; ++i)
{
FluidParticle neighbour = p.Neighbours[i];
if (p.Index >= neighbour.Index)
{
continue;
}
float q;
Vector2.DistanceSquared(ref p.Position, ref neighbour.Position, out q);
if (q > _influenceRadiusSquared)
{
continue;
}
Vector2 direction;
Vector2.Subtract(ref neighbour.Position, ref p.Position, out direction);
if (direction.LengthSquared() < float.Epsilon)
{
continue;
}
direction.Normalize();
Vector2 deltaVelocity;
Vector2.Subtract(ref p.Velocity, ref neighbour.Velocity, out deltaVelocity);
float u;
Vector2.Dot(ref deltaVelocity, ref direction, out u);
if (u > 0.0f)
{
q = 1.0f - (float)Math.Sqrt(q) / Definition.InfluenceRadius;
float impulseFactor = 0.5f * timeStep * q * (u * (Definition.ViscositySigma + Definition.ViscosityBeta * u));
Vector2 impulse;
Vector2.Multiply(ref direction, -impulseFactor, out impulse);
p.ApplyImpulse(ref impulse);
Vector2.Multiply(ref direction, impulseFactor, out impulse);
neighbour.ApplyImpulse(ref impulse);
}
}
}
private const int MaxNeighbors = 25;
//private int _len2;
//private int _j;
//private float _q;
//private float _qq;
//private Vector2 _rij;
//private float _d;
//private Vector2 _dx;
private float _density;
private float _densityNear;
private float _pressure;
private float _pressureNear;
private float[] _distanceCache = new float[MaxNeighbors];
//private void DoubleDensityRelaxation1(FluidParticle p, float timeStep)
//{
// _density = 0;
// _densityNear = 0;
// _len2 = p.Neighbours.Count;
// if (_len2 > MaxNeighbors)
// _len2 = MaxNeighbors;
// for (_j = 0; _j < _len2; _j++)
// {
// _q = Vector2.DistanceSquared(p.Position, p.Neighbours[_j].Position);
// _distanceCache[_j] = _q;
// if (_q < _influenceRadiusSquared && _q != 0)
// {
// _q = (float)Math.Sqrt(_q);
// _q /= Definition.InfluenceRadius;
// _qq = ((1 - _q) * (1 - _q));
// _density += _qq;
// _densityNear += _qq * (1 - _q);
// }
// }
// _pressure = Definition.Stiffness * (_density - Definition.DensityRest);
// _pressureNear = Definition.StiffnessNear * _densityNear;
// _dx = Vector2.Zero;
// for (_j = 0; _j < _len2; _j++)
// {
// _q = _distanceCache[_j];
// if (_q < _influenceRadiusSquared && _q != 0)
// {
// _q = (float)Math.Sqrt(_q);
// _rij = p.Neighbours[_j].Position;
// _rij -= p.Position;
// _rij *= 1 / _q;
// _q /= _influenceRadiusSquared;
// _d = ((timeStep * timeStep) * (_pressure * (1 - _q) + _pressureNear * (1 - _q) * (1 - _q)));
// _rij *= _d * 0.5f;
// p.Neighbours[_j].Position += _rij;
// _dx -= _rij;
// }
// }
// p.Position += _dx;
//}
private void DoubleDensityRelaxation(FluidParticle particle, float deltaTime2)
{
_density = 0.0f;
_densityNear = 0.0f;
int neightborCount = particle.Neighbours.Count;
if (neightborCount > MaxNeighbors)
neightborCount = MaxNeighbors;
for (int i = 0; i < neightborCount; ++i)
{
FluidParticle neighbour = particle.Neighbours[i];
if (particle.Index == neighbour.Index)
continue;
float q;
Vector2.DistanceSquared(ref particle.Position, ref neighbour.Position, out q);
_distanceCache[i] = q;
if (q > _influenceRadiusSquared)
continue;
q = 1.0f - (float)Math.Sqrt(q) / Definition.InfluenceRadius;
float densityDelta = q * q;
_density += densityDelta;
_densityNear += densityDelta * q;
}
_pressure = Definition.Stiffness * (_density - Definition.DensityRest);
_pressureNear = Definition.StiffnessNear * _densityNear;
// For gameplay purposes
particle.Density = _density + _densityNear;
particle.Pressure = _pressure + _pressureNear;
Vector2 delta = Vector2.Zero;
for (int i = 0; i < neightborCount; ++i)
{
FluidParticle neighbour = particle.Neighbours[i];
if (particle.Index == neighbour.Index)
continue;
float q = _distanceCache[i];
if (q > _influenceRadiusSquared)
continue;
q = 1.0f - (float)Math.Sqrt(q) / Definition.InfluenceRadius;
float dispFactor = deltaTime2 * (q * (_pressure + _pressureNear * q));
Vector2 direction;
Vector2.Subtract(ref neighbour.Position, ref particle.Position, out direction);
if (direction.LengthSquared() < float.Epsilon)
continue;
direction.Normalize();
Vector2 disp;
Vector2.Multiply(ref direction, dispFactor, out disp);
Vector2.Add(ref neighbour.Position, ref disp, out neighbour.Position);
Vector2.Multiply(ref direction, -dispFactor, out disp);
Vector2.Add(ref delta, ref disp, out delta);
}
Vector2.Add(ref particle.Position, ref delta, out particle.Position);
}
private void CreateSprings(FluidParticle p)
{
for (int i = 0; i < p.Neighbours.Count; ++i)
{
FluidParticle neighbour = p.Neighbours[i];
if (p.Index >= neighbour.Index)
continue;
float q;
Vector2.DistanceSquared(ref p.Position, ref neighbour.Position, out q);
if (q > _influenceRadiusSquared)
continue;
SpringHash hash = new SpringHash { P0 = p, P1 = neighbour };
if (!_springs.ContainsKey(hash))
{
//TODO: Use pool?
Spring spring = new Spring(p, neighbour) { RestLength = (float)Math.Sqrt(q) };
_springs.Add(hash, spring);
}
}
}
private void AdjustSprings(float timeStep)
{
foreach (var pair in _springs)
{
Spring spring = pair.Value;
spring.Update(timeStep, Definition.KSpring, Definition.InfluenceRadius);
if (spring.Active)
{
float L = spring.RestLength;
float distance;
Vector2.Distance(ref spring.P0.Position, ref spring.P1.Position, out distance);
if (distance > (L + (Definition.YieldRatioStretch * L)))
{
spring.RestLength += timeStep * Definition.Plasticity * (distance - L - (Definition.YieldRatioStretch * L));
}
else if (distance < (L - (Definition.YieldRatioCompress * L)))
{
spring.RestLength -= timeStep * Definition.Plasticity * (L - (Definition.YieldRatioCompress * L) - distance);
}
}
else
{
_springsToRemove.Add(pair.Key);
}
}
for (int i = 0; i < _springsToRemove.Count; ++i)
{
_springs.Remove(_springsToRemove[i]);
}
}
private void ComputeNeighbours()
{
_hashGrid.GridSize = Definition.InfluenceRadius;
_hashGrid.Clear();
for (int i = 0; i < Particles.Count; ++i)
{
FluidParticle p = Particles[i];
if (p.IsActive)
{
_hashGrid.Add(p);
}
}
for (int i = 0; i < Particles.Count; ++i)
{
FluidParticle p = Particles[i];
p.Neighbours.Clear();
_hashGrid.Find(ref p.Position, p.Neighbours);
}
}
public void Update(float deltaTime)
{
if (deltaTime == 0)
return;
float deltaTime2 = 0.5f * deltaTime * deltaTime;
ComputeNeighbours();
ApplyForces();
if (Definition.UseViscosity)
{
for (int i = 0; i < Particles.Count; ++i)
{
FluidParticle p = Particles[i];
if (p.IsActive)
{
ApplyViscosity(p, deltaTime);
}
}
}
for (int i = 0; i < Particles.Count; ++i)
{
FluidParticle p = Particles[i];
if (p.IsActive)
{
p.Update(deltaTime);
}
}
for (int i = 0; i < Particles.Count; ++i)
{
FluidParticle p = Particles[i];
if (p.IsActive)
{
DoubleDensityRelaxation(p, deltaTime2);
}
}
if (Definition.UsePlasticity)
{
for (int i = 0; i < Particles.Count; ++i)
{
FluidParticle p = Particles[i];
if (p.IsActive)
{
CreateSprings(p);
}
}
}
AdjustSprings(deltaTime);
UpdateVelocities(deltaTime);
}
internal void UpdateVelocities(float timeStep)
{
for (int i = 0; i < Particles.Count; ++i)
{
Particles[i].UpdateVelocity(timeStep);
}
}
}
}

View File

@ -0,0 +1,95 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using System;
using System.Collections.Generic;
using nkast.Aether.Physics2D.Common;
namespace tainicom.Aether.Physics2D.Fluids
{
/// <summary>
/// Grid used by particle system to keep track of neightbor particles.
/// </summary>
public class HashGrid
{
private Dictionary<ulong, List<FluidParticle>> _hash = new Dictionary<ulong, List<FluidParticle>>();
private Stack<List<FluidParticle>> _bucketPool = new Stack<List<FluidParticle>>();
public HashGrid()
{
GridSize = 1.0f;
}
public float GridSize { get; set; }
private static ulong HashKey(int x, int y)
{
return ((ulong)x * 2185031351ul) ^ ((ulong)y * 4232417593ul);
}
private ulong HashKey(Vector2 position)
{
return HashKey(
(int)Math.Floor(position.X / GridSize),
(int)Math.Floor(position.Y / GridSize)
);
}
public void Clear()
{
foreach (KeyValuePair<ulong, List<FluidParticle>> pair in _hash)
{
pair.Value.Clear();
_bucketPool.Push(pair.Value);
}
_hash.Clear();
}
public void Add(FluidParticle particle)
{
ulong key = HashKey(particle.Position);
List<FluidParticle> bucket;
if (!_hash.TryGetValue(key, out bucket))
{
if (_bucketPool.Count > 0)
{
bucket = _bucketPool.Pop();
}
else
{
bucket = new List<FluidParticle>();
}
_hash.Add(key, bucket);
}
bucket.Add(particle);
}
public void Find(ref Vector2 position, List<FluidParticle> neighbours)
{
int ix = (int)Math.Floor(position.X / GridSize);
int iy = (int)Math.Floor(position.Y / GridSize);
// Check all 9 neighbouring cells
for (int x = ix - 1; x <= ix + 1; ++x)
{
for (int y = iy - 1; y <= iy + 1; ++y)
{
ulong key = HashKey(x, y);
List<FluidParticle> bucket;
if (_hash.TryGetValue(key, out bucket))
{
for (int i = 0; i < bucket.Count; ++i)
{
if (bucket[i] != null)
{
neighbours.Add(bucket[i]);
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,57 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using nkast.Aether.Physics2D.Common;
namespace tainicom.Aether.Physics2D.Fluids
{
//TODO: Could be struct?
public class Spring
{
public FluidParticle P0;
public FluidParticle P1;
public Spring(FluidParticle p0, FluidParticle p1)
{
Active = true;
P0 = p0;
P1 = p1;
}
public bool Active { get; set; }
public float RestLength { get; set; }
public void Update(float timeStep, float kSpring, float influenceRadius)
{
if (!Active)
return;
Vector2 dir = P1.Position - P0.Position;
float distance = dir.Length();
dir.Normalize();
// This is to avoid imploding simulation with really springy fluids
if (distance < 0.5f * influenceRadius)
{
Active = false;
return;
}
if (RestLength > influenceRadius)
{
Active = false;
return;
}
//Algorithm 3
float displacement = timeStep * timeStep * kSpring * (1.0f - RestLength / influenceRadius) * (RestLength - distance) * 0.5f;
dir *= displacement;
P0.Position -= dir;
P1.Position += dir;
}
}
}

View File

@ -0,0 +1,26 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using System.Collections.Generic;
namespace tainicom.Aether.Physics2D.Fluids
{
public class SpringHash : IEqualityComparer<SpringHash>
{
public FluidParticle P0;
public FluidParticle P1;
public bool Equals(SpringHash lhs, SpringHash rhs)
{
return (lhs.P0.Index == rhs.P0.Index && lhs.P1.Index == rhs.P1.Index)
|| (lhs.P0.Index == rhs.P1.Index && lhs.P1.Index == rhs.P0.Index);
}
public int GetHashCode(SpringHash s)
{
return (s.P0.Index * 73856093) ^ (s.P1.Index * 19349663) ^ (s.P0.Index * 19349663) ^ (s.P1.Index * 73856093);
}
}
}

View File

@ -0,0 +1,445 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using System;
using System.Collections.Generic;
using nkast.Aether.Physics2D.Common;
namespace tainicom.Aether.Physics2D.Fluids
{
public class FluidSystem2
{
public const int MaxNeighbors = 25;
public const int CellSize = 1;
// Most of these can be tuned at runtime with F1-F9 and keys 1-9 (no numpad)
public const float InfluenceRadius = 20.0f;
public const float InfluenceRadiusSquared = InfluenceRadius * InfluenceRadius;
public const float Stiffness = 0.504f;
public const float StiffnessFarNearRatio = 10.0f;
public const float StiffnessNear = Stiffness * StiffnessFarNearRatio;
public const float ViscositySigma = 0.0f;
public const float ViscosityBeta = 0.3f;
public const float DensityRest = 10.0f;
public const float KSpring = 0.3f;
public const float RestLength = 5.0f;
public const float RestLengthSquared = RestLength * RestLength;
public const float YieldRatioStretch = 0.5f;
public const float YieldRatioCompress = 0.5f;
public const float Plasticity = 0.5f;
public const int VelocityCap = 150;
public const float DeformationFactor = 0f;
public const float CollisionForce = 0.3f;
private bool _isElasticityInitialized;
private bool _elasticityEnabled;
private bool _isPlasticityInitialized;
private bool _plasticityEnabled;
private float _deltaTime2;
private Vector2 _dx = new Vector2(0.0f, 0.0f);
private const int Wpadding = 20;
private const int Hpadding = 20;
public SpatialTable Particles;
// Temp variables
private Vector2 _rij = new Vector2(0.0f, 0.0f);
private Vector2 _tempVect = new Vector2(0.0f, 0.0f);
private Dictionary<int, List<int>> _springPresenceTable;
private List<Spring2> _springs;
private List<Particle> _tempParticles;
private int _worldWidth;
private int _worldHeight;
public int ParticlesCount { get { return Particles.Count; } }
public FluidSystem2(Vector2 gravity, int maxParticleLimit, int worldWidth, int worldHeight)
{
_worldHeight = worldHeight;
_worldWidth = worldWidth;
Particles = new SpatialTable(worldWidth, worldHeight, CellSize);
MaxParticleLimit = maxParticleLimit;
Gravity = gravity;
}
public Vector2 Gravity { get; set; }
public int MaxParticleLimit { get; private set; }
public bool ElasticityEnabled
{
get { return _elasticityEnabled; }
set
{
if (!_isElasticityInitialized)
InitializeElasticity();
_elasticityEnabled = value;
}
}
public bool PlasticityEnabled
{
get { return _plasticityEnabled; }
set
{
if (!_isPlasticityInitialized)
InitializePlasticity();
_plasticityEnabled = value;
}
}
private void UpdateParticleVelocity(float deltaTime)
{
for(int i = 0; i < Particles.Count; i++)
{
Particle particle = Particles[i];
particle.PreviousPosition = particle.Position;
particle.Position = new Vector2(particle.Position.X + (deltaTime * particle.Velocity.X), particle.Position.Y + (deltaTime * particle.Velocity.Y));
}
}
private void WallCollision(Particle pi)
{
float x = 0;
float y = 0;
if (pi.Position.X > (_worldWidth / 2 - Wpadding))
x -= (pi.Position.X - (_worldWidth / 2 - Wpadding)) / CollisionForce;
else if (pi.Position.X < (-_worldWidth / 2 + Wpadding))
x += ((-_worldWidth / 2 + Wpadding) - pi.Position.X) / CollisionForce;
if (pi.Position.Y > (_worldHeight - Hpadding))
y -= (pi.Position.Y - (_worldHeight - Hpadding)) / CollisionForce;
else if (pi.Position.Y < Hpadding)
y += (Hpadding - pi.Position.Y) / CollisionForce;
pi.Velocity.X += x;
pi.Velocity.Y += y;
}
private void CapVelocity(Vector2 v)
{
if (v.X > VelocityCap)
v.X = VelocityCap;
else if (v.X < -VelocityCap)
v.X = -VelocityCap;
if (v.Y > VelocityCap)
v.Y = VelocityCap;
else if (v.Y < -VelocityCap)
v.Y = -VelocityCap;
}
private void InitializePlasticity()
{
_isPlasticityInitialized = true;
_springs.Clear();
float q;
foreach (Particle pa in Particles)
{
foreach (Particle pb in Particles)
{
if (pa.GetHashCode() == pb.GetHashCode())
continue;
Vector2.Distance(ref pa.Position, ref pb.Position, out q);
Vector2.Subtract(ref pb.Position, ref pa.Position, out _rij);
_rij /= q;
if (q < RestLength)
{
_springs.Add(new Spring2(pa, pb, q));
}
}
pa.Velocity = Vector2.Zero;
}
}
private void CalculatePlasticity(float deltaTime)
{
foreach (Spring2 spring in _springs)
{
spring.Update();
if (spring.CurrentDistance == 0)
continue;
Vector2.Subtract(ref spring.PB.Position, ref spring.PA.Position, out _rij);
_rij /= spring.CurrentDistance;
float D = deltaTime * KSpring * (spring.RestLength - spring.CurrentDistance);
_rij *= (D * 0.5f);
spring.PA.Position = new Vector2(spring.PA.Position.X - _rij.X, spring.PA.Position.Y - _rij.Y);
spring.PB.Position = new Vector2(spring.PB.Position.X + _rij.X, spring.PB.Position.Y + _rij.Y);
}
}
private void InitializeElasticity()
{
_isElasticityInitialized = true;
foreach (Particle particle in Particles)
{
_springPresenceTable.Add(particle.GetHashCode(), new List<int>(MaxParticleLimit));
particle.Velocity = Vector2.Zero;
}
}
private void CalculateElasticity(float deltaTime)
{
float sqDist;
for (int i = 0; i < Particles.Count; i++)
{
Particle pa = Particles[i];
if (Particles.CountNearBy(pa) <= 1)
continue;
_tempParticles = Particles.GetNearby(pa);
int len2 = _tempParticles.Count;
if (len2 > MaxNeighbors)
len2 = MaxNeighbors;
for (int j = 0; j < len2; j++)
{
Particle pb = Particles[j];
Vector2.DistanceSquared(ref pa.Position, ref pb.Position, out sqDist);
if (sqDist > RestLengthSquared)
continue;
if (pa.GetHashCode() == pb.GetHashCode())
continue;
if (!_springPresenceTable[pa.GetHashCode()].Contains(pb.GetHashCode()))
{
_springs.Add(new Spring2(pa, pb, RestLength));
_springPresenceTable[pa.GetHashCode()].Add(pb.GetHashCode());
}
}
}
for (int i = _springs.Count - 1; i >= 0; i--)
{
Spring2 spring = _springs[i];
spring.Update();
// Stretch
if (spring.CurrentDistance > (spring.RestLength + DeformationFactor))
{
spring.RestLength += deltaTime * Plasticity * (spring.CurrentDistance - spring.RestLength - (YieldRatioStretch * spring.RestLength));
}
// Compress
else if (spring.CurrentDistance < (spring.RestLength - DeformationFactor))
{
spring.RestLength -= deltaTime * Plasticity * (spring.RestLength - (YieldRatioCompress * spring.RestLength) - spring.CurrentDistance);
}
// Remove springs with restLength longer than REST_LENGTH
if (spring.RestLength > RestLength)
{
_springs.RemoveAt(i);
_springPresenceTable[spring.PA.GetHashCode()].Remove(spring.PB.GetHashCode());
}
else
{
if (spring.CurrentDistance == 0)
continue;
Vector2.Subtract(ref spring.PB.Position, ref spring.PA.Position, out _rij);
_rij /= spring.CurrentDistance;
float D = deltaTime * KSpring * (spring.RestLength - spring.CurrentDistance);
_rij *= (D * 0.5f);
spring.PA.Position = new Vector2(spring.PA.Position.X - _rij.X, spring.PA.Position.Y - _rij.Y);
spring.PB.Position = new Vector2(spring.PB.Position.X + _rij.X, spring.PB.Position.Y + _rij.Y);
}
}
}
private void ApplyGravity(Particle particle)
{
particle.Velocity = new Vector2(particle.Velocity.X + Gravity.X, particle.Velocity.Y + Gravity.Y);
}
private void ApplyViscosity(float deltaTime)
{
float u, q;
for (int i = 0; i < Particles.Count; i++)
{
Particle particle = Particles[i];
_tempParticles = Particles.GetNearby(particle);
int len2 = _tempParticles.Count;
if (len2 > MaxNeighbors)
len2 = MaxNeighbors;
for (int j = 0; j < len2; j++)
{
Particle tempParticle = _tempParticles[j];
Vector2.DistanceSquared(ref particle.Position, ref tempParticle.Position, out q);
if ((q < InfluenceRadiusSquared) && (q != 0))
{
q = (float)Math.Sqrt(q);
Vector2.Subtract(ref tempParticle.Position, ref particle.Position, out _rij);
Vector2.Divide(ref _rij, q, out _rij);
Vector2.Subtract(ref particle.Velocity, ref tempParticle.Velocity, out _tempVect);
Vector2.Dot(ref _tempVect, ref _rij, out u);
if (u <= 0.0f)
continue;
q /= InfluenceRadius;
float I = (deltaTime * (1 - q) * (ViscositySigma * u + ViscosityBeta * u * u));
Vector2.Multiply(ref _rij, (I * 0.5f), out _rij);
Vector2.Subtract(ref particle.Velocity, ref _rij, out _tempVect);
particle.Velocity = _tempVect;
_tempVect = tempParticle.Velocity;
_tempVect += _rij;
tempParticle.Velocity = _tempVect;
}
}
}
}
private void DoubleDensityRelaxation()
{
float q;
for (int i = 0; i < Particles.Count; i++)
{
Particle particle = Particles[i];
particle.Density = 0;
particle.NearDensity = 0;
_tempParticles = Particles.GetNearby(particle);
int len2 = _tempParticles.Count;
if (len2 > MaxNeighbors)
len2 = MaxNeighbors;
for (int j = 0; j < len2; j++)
{
Particle tempParticle = _tempParticles[j];
Vector2.DistanceSquared(ref particle.Position, ref tempParticle.Position, out q);
if (q < InfluenceRadiusSquared && q != 0)
{
q = (float)Math.Sqrt(q);
q /= InfluenceRadius;
float qq = ((1 - q) * (1 - q));
particle.Density += qq;
particle.NearDensity += qq * (1 - q);
}
}
particle.Pressure = (Stiffness * (particle.Density - DensityRest));
particle.NearPressure = (StiffnessNear * particle.NearDensity);
_dx = Vector2.Zero;
for (int j = 0; j < len2; j++)
{
Particle tempParticle = _tempParticles[j];
Vector2.DistanceSquared(ref particle.Position, ref tempParticle.Position, out q);
if ((q < InfluenceRadiusSquared) && (q != 0))
{
q = (float)Math.Sqrt(q);
Vector2.Subtract(ref tempParticle.Position, ref particle.Position, out _rij);
Vector2.Divide(ref _rij, q, out _rij);
q /= InfluenceRadius;
float D = (_deltaTime2 * (particle.Pressure * (1 - q) + particle.NearPressure * (1 - q) * (1 - q)));
Vector2.Multiply(ref _rij, (D * 0.5f), out _rij);
tempParticle.Position = new Vector2(tempParticle.Position.X + _rij.X, tempParticle.Position.Y + _rij.Y);
Vector2.Subtract(ref _dx, ref _rij, out _dx);
}
}
particle.Position = particle.Position + _dx;
}
}
public void Update(float deltaTime)
{
if (deltaTime == 0)
return;
_deltaTime2 = deltaTime * deltaTime;
ApplyViscosity(deltaTime);
//Update velocity
UpdateParticleVelocity(deltaTime);
Particles.Rehash();
if (_elasticityEnabled)
CalculateElasticity(deltaTime);
if (_plasticityEnabled)
CalculatePlasticity(deltaTime);
DoubleDensityRelaxation();
for(int i = 0; i < Particles.Count; i++)
{
Particle particle = Particles[i];
particle.Velocity = new Vector2((particle.Position.X - particle.PreviousPosition.X) / deltaTime, (particle.Position.Y - particle.PreviousPosition.Y) / deltaTime);
ApplyGravity(particle);
WallCollision(particle);
CapVelocity(particle.Velocity);
}
}
public void AddParticle(Vector2 position)
{
Particles.Add(new Particle(position.X, position.Y));
}
}
public class Particle
{
public float Density;
public float NearDensity;
public float NearPressure;
public Vector2 Position = new Vector2(0, 0);
public float Pressure;
public Vector2 PreviousPosition = new Vector2(0, 0);
public Vector2 Velocity = new Vector2(0, 0);
public Particle(float posX, float posY)
{
Position = new Vector2(posX, posY);
}
}
public class Spring2
{
public float CurrentDistance;
public Particle PA;
public Particle PB;
public float RestLength;
public Spring2(Particle pa, Particle pb, float restLength)
{
PA = pa;
PB = pb;
RestLength = restLength;
}
public void Update()
{
Vector2.Distance(ref PA.Position, ref PB.Position, out CurrentDistance);
}
public bool Contains(Particle p)
{
return (PA.GetHashCode() == p.GetHashCode() || PB.GetHashCode() == p.GetHashCode());
}
}
}

View File

@ -0,0 +1,179 @@
/* Original source Farseer Physics Engine:
* Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com
* Microsoft Permissive License (Ms-PL) v1.1
*/
using System.Collections;
using System.Collections.Generic;
namespace tainicom.Aether.Physics2D.Fluids
{
public class SpatialTable : IEnumerable<Particle>
{
// default nearby table size
private const int DefaultNearbySize = 50;
private List<Particle> _table;
private List<Particle> _voidList = new List<Particle>(1);
private List<Particle>[][] _nearby;
bool _initialized;
private int _row;
private int _column;
private int _cellSize;
public SpatialTable(int column, int row, int cellSize)
{
_row = row;
_cellSize = cellSize;
_column = column;
}
public void Initialize()
{
_table = new List<Particle>((_row * _column) / 2);
_nearby = new List<Particle>[_column][];
for (int i = 0; i < _column; ++i)
{
_nearby[i] = new List<Particle>[_row];
for (int j = 0; j < _row; ++j)
{
_nearby[i][j] = new List<Particle>(DefaultNearbySize);
}
}
_initialized = true;
}
/// <summary>
/// Append value to the table and identify its position in the space.
/// Don't need to rehash table after append operation.</summary>
/// <param name="value"></param>
public void Add(Particle value)
{
if (!_initialized)
Initialize();
AddInterRadius(value);
_table.Add(value);
}
public Particle this[int i]
{
get { return _table[i]; }
set { _table[i] = value; }
}
public void Remove(Particle value)
{
_table.Remove(value);
}
public void Clear()
{
for (int i = 0; i < _column; ++i)
{
for (int j = 0; j < _row; ++j)
{
_nearby[i][j].Clear();
_nearby[i][j] = null;
}
}
_table.Clear();
}
public int Count
{
get { return (_table == null)? 0 : _table.Count; }
}
public List<Particle> GetNearby(Particle value)
{
int x = posX(value);
int y = posY(value);
if (!InRange(x, y))
return _voidList;
return _nearby[x][y];
}
private int posX(Particle value)
{
return (int)((value.Position.X + (_column / 2) + 0.3f) / _cellSize);
}
private int posY(Particle value)
{
return (int)((value.Position.Y + 0.3f) / _cellSize);
}
public int CountNearBy(Particle value)
{
return GetNearby(value).Count;
}
/// <summary>
/// Updates the spatial relationships of objects. Rehash function
/// needed if elements change their position in the space.
/// </summary>
public void Rehash()
{
if (_table == null || _table.Count == 0)
return;
for (int i = 0; i < _column; i++)
{
for (int j = 0; j < _row; j++)
{
if (_nearby[i][j] != null)
_nearby[i][j].Clear();
}
}
foreach (Particle particle in _table)
{
AddInterRadius(particle);
}
}
/// <summary>
/// Add element to its position and neighbor cells.
/// </summary>
/// <param name="value"></param>
private void AddInterRadius(Particle value)
{
for (int i = -1; i < 2; ++i)
{
for (int j = -1; j < 2; ++j)
{
int x = posX(value) + i;
int y = posY(value) + j;
if (InRange(x, y))
_nearby[x][y].Add(value);
}
}
}
/// <summary>
/// Check if a position is out of the spatial range
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns>true if position is in range.</returns>
private bool InRange(float x, float y)
{
return (x > 0 && x < _column && y > 0 && y < _row);
}
public IEnumerator<Particle> GetEnumerator()
{
return _table.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,412 @@
using System;
using System.Collections.Generic;
using nkast.Aether.Physics2D.Common;
using tainicom.Aether.Physics2D.Fluids;
using CtrEditor.Simulacion.Fluids.Components;
namespace CtrEditor.Simulacion.Fluids.Components
{
/// <summary>
/// Clase base para todos los componentes de fluidos
/// </summary>
public abstract class ComponenteFluido : IContenedorFluido
{
public Vector2 Posicion { get; set; }
public bool EsActivo { get; set; } = true;
/// <summary>
/// Convierte una posición de metros/WPF a sistema de coordenadas de FluidSystem2
/// </summary>
protected Vector2 ConvertirPosicion(Vector2 posicion, int worldWidth)
{
return new Vector2(
posicion.X * 100 - worldWidth/2,
posicion.Y * 100
);
}
/// <summary>
/// Implementación en clases derivadas para restricciones específicas
/// </summary>
public abstract void RestringirParticula(Particle particula);
}
/// <summary>
/// Tanque para contener fluidos
/// </summary>
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
/// <summary>
/// Constructor para un tanque
/// </summary>
/// <param name="posicion">Posición del centro del tanque</param>
/// <param name="ancho">Ancho del tanque</param>
/// <param name="alto">Alto del tanque</param>
/// <param name="worldWidth">Ancho del mundo en cm (para conversión de coordenadas)</param>
public Tanque(Vector2 posicion, float ancho, float alto, int worldWidth)
{
Posicion = posicion;
Ancho = ancho;
Alto = alto;
_worldWidth = worldWidth;
ActualizarLimites();
}
/// <summary>
/// Actualiza los límites internos del tanque después de cambiar la posición o tamaño
/// </summary>
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;
}
/// <summary>
/// Restringe las partículas para que permanezcan dentro del tanque
/// </summary>
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;
}
}
}
/// <summary>
/// Obtiene el nivel de llenado del tanque en porcentaje
/// </summary>
/// <param name="simulacion">Instancia activa de la simulación</param>
/// <returns>Porcentaje de llenado (0-1)</returns>
public float ObtenerNivelLlenado()
{
// Sería necesario acceder a las partículas para calcular esto
// Implementación en clases derivadas específicas
return 0.0f;
}
}
/// <summary>
/// Representa un segmento de tubo para fluidos
/// </summary>
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;
}
/// <summary>
/// Contiene y restringe el movimiento de una partícula dentro del tubo
/// </summary>
/// <returns>Verdadero si se aplicó alguna restricción</returns>
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 = VectorUtils.NormalizeVector(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 = VectorUtils.DotProduct(
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
}
/// <summary>
/// Calcula la distancia de un punto a una línea
/// </summary>
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 VectorUtils.DistanceBetween(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 = VectorUtils.DotProduct(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 VectorUtils.DistanceBetween(punto, puntoMasCercano);
}
/// Calcula el punto más cercano en una línea desde un punto dado
/// </summary>
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(ref puntoDesdeInicio, ref lineaNormalizada, out proyeccion);
// 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;
}
}
/// <summary>
/// Tubería para conducir fluidos entre puntos
/// </summary>
public class Tuberia : ComponenteFluido
{
private List<SegmentoTubo> _segmentos = new List<SegmentoTubo>();
private List<Vector2> _puntos = new List<Vector2>();
public float Diametro { get; private set; }
private int _worldWidth;
/// <summary>
/// Constructor para una tubería
/// </summary>
/// <param name="diametro">Diámetro de la tubería</param>
/// <param name="worldWidth">Ancho del mundo en cm (para conversión de coordenadas)</param>
public Tuberia(float diametro, int worldWidth)
{
Diametro = diametro;
_worldWidth = worldWidth;
}
/// <summary>
/// Agrega un punto a la tubería, creando segmentos automáticamente
/// </summary>
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);
}
/// <summary>
/// Restringe las partículas para que permanezcan dentro de la tubería
/// </summary>
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);
}
}
/// <summary>
/// Calcula la distancia desde un punto a un segmento de línea
/// </summary>
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 VectorUtils.DistanceBetween(punto, segmentoInicio); // Es un punto, no un segmento
// Calcular proyección del punto sobre el segmento
float t = VectorUtils.DotProduct(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 VectorUtils.DistanceBetween(punto, proyeccion);
}
}
/// <summary>
/// Válvula para controlar el flujo de fluidos
/// </summary>
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;
/// <summary>
/// Constructor para una válvula
/// </summary>
/// <param name="posicion">Posición de la válvula</param>
/// <param name="diametro">Diámetro del conducto de la válvula</param>
/// <param name="apertura">Apertura inicial (0-1)</param>
/// <param name="worldWidth">Ancho del mundo en cm (para conversión de coordenadas)</param>
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);
}
/// <summary>
/// Actualiza la posición ajustada después de cambiar la posición
/// </summary>
public void ActualizarPosicion(Vector2 nuevaPosicion)
{
Posicion = nuevaPosicion;
_posicionAjustada = ConvertirPosicion(nuevaPosicion, _worldWidth);
}
/// <summary>
/// Restringe y modula el flujo de partículas a través de la válvula
/// </summary>
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 = VectorUtils.DistanceBetween(_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 = VectorUtils.NormalizeVector(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;
}
}
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using nkast.Aether.Physics2D.Common;
namespace CtrEditor.Simulacion.Fluids.Components
{
/// <summary>
/// Utilidades para operaciones con Vector2 de nkast.Aether.Physics2D.Common
/// </summary>
public static class VectorUtils
{
/// <summary>
/// Calcula la distancia entre dos vectores
/// </summary>
public static float DistanceBetween(Vector2 v1, Vector2 v2)
{
float result;
Vector2 v1Ref = v1;
Vector2 v2Ref = v2;
Vector2.Distance(ref v1Ref, ref v2Ref, out result);
return result;
}
/// <summary>
/// Calcula el producto punto entre dos vectores
/// </summary>
public static float DotProduct(Vector2 v1, Vector2 v2)
{
float result;
Vector2 v1Ref = v1;
Vector2 v2Ref = v2;
Vector2.Dot(ref v1Ref, ref v2Ref, out result);
return result;
}
/// <summary>
/// Normaliza un vector y retorna el resultado
/// </summary>
public static Vector2 NormalizeVector(Vector2 v)
{
Vector2 result;
float length = v.Length();
if (length < 1e-6f)
return new Vector2(0, 0);
Vector2.Divide(ref v, length, out result);
return result;
}
/// <summary>
/// Calcula la reflexión de un vector respecto a una normal
/// </summary>
public static Vector2 Reflect(Vector2 vector, Vector2 normal)
{
Vector2 normalized = NormalizeVector(normal);
float dot;
Vector2 vectorRef = vector;
Vector2 normalizedRef = normalized;
Vector2.Dot(ref vectorRef, ref normalizedRef, out dot);
Vector2 result;
Vector2.Multiply(ref normalizedRef, 2f * dot, out result);
Vector2 vectorRef2 = vector;
Vector2 resultRef = result;
Vector2.Subtract(ref vectorRef2, ref resultRef, out result);
return result;
}
}
}

View File

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using nkast.Aether.Physics2D.Common;
using tainicom.Aether.Physics2D.Fluids;
using CtrEditor.Simulacion.Fluids.Components;
namespace CtrEditor.Simulacion.Fluids
{
/// <summary>
/// Clase principal que gestiona la simulación de fluidos basada en FluidSystem2
/// </summary>
public class SimulacionFluidos
{
public FluidSystem2 SistemaFluido { get; private set; }
private List<IContenedorFluido> _contenedores = new List<IContenedorFluido>();
private int _worldWidth;
private int _worldHeight;
public int ParticlesCount => SistemaFluido?.ParticlesCount ?? 0;
/// <summary>
/// Constructor para el sistema de simulación de fluidos
/// </summary>
/// <param name="ancho">Ancho del área de simulación en metros</param>
/// <param name="alto">Alto del área de simulación en metros</param>
/// <param name="maxParticulas">Número máximo de partículas</param>
/// <param name="gravedad">Vector de gravedad</param>
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;
}
/// <summary>
/// Método para agregar una partícula al sistema
/// </summary>
/// <param name="posicion">Posición en metros</param>
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));
}
/// <summary>
/// Agrega múltiples partículas en un área rectangular
/// </summary>
/// <param name="centro">Centro del área</param>
/// <param name="ancho">Ancho del área</param>
/// <param name="alto">Alto del área</param>
/// <param name="cantidad">Cantidad de partículas a agregar</param>
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));
}
}
/// <summary>
/// Agrega un contenedor al sistema de fluidos
/// </summary>
/// <param name="contenedor">Implementación de IContenedorFluido</param>
public void AgregarContenedor(IContenedorFluido contenedor)
{
_contenedores.Add(contenedor);
}
/// <summary>
/// Elimina un contenedor del sistema de fluidos
/// </summary>
/// <param name="contenedor">Contenedor a eliminar</param>
public void RemoverContenedor(IContenedorFluido contenedor)
{
_contenedores.Remove(contenedor);
}
/// <summary>
/// Actualiza la simulación avanzando un paso de tiempo
/// </summary>
/// <param name="deltaTime">Tiempo transcurrido en segundos</param>
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);
}
/// <summary>
/// Obtiene la densidad del fluido en una posición específica
/// </summary>
/// <param name="posicion">Posición en metros</param>
/// <returns>Densidad relativa de fluido (0-1)</returns>
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 posAjustadaRef = posAjustada;
Vector2 positionRef = p.Position;
Vector2.Distance(ref posAjustadaRef, ref positionRef, out distancia);
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;
}
/// <summary>
/// Ajusta la gravedad del sistema
/// </summary>
/// <param name="gravedad">Nuevo vector de gravedad</param>
public void AjustarGravedad(Vector2 gravedad)
{
SistemaFluido.Gravity = gravedad;
}
/// <summary>
/// Limpia todas las partículas del sistema
/// </summary>
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;
}
}
/// <summary>
/// Interfaz para contenedores de fluido personalizados como tubos, tanques, etc.
/// </summary>
public interface IContenedorFluido
{
/// <summary>
/// Aplica restricciones a una partícula para mantenerla dentro o fuera del contenedor
/// </summary>
void RestringirParticula(Particle particula);
}
}

View File

@ -0,0 +1,375 @@
using CtrEditor.ObjetosSim;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Numerics;
using System.Windows.Shapes;
public class Circle
{
private Vector2 position;
public float Left
{
get { return position.X; }
set { position.X = value; }
}
public float Top
{
get { return position.Y; }
set { position.Y = value; }
}
public float Diameter { get; set; }
public float Mass { get; set; }
public float AngleofMovement { get; set; } // En grados
public float Speed { get; set; }
public float Overlap { get; set; }
public Circle(float left = 0, float top = 0, float diameter = 10, float mass = 1, float angle = 0, float speed = 0)
{
position = new Vector2(left, top);
Diameter = diameter;
Mass = mass;
AngleofMovement = angle;
Speed = speed;
}
public void Move(float timeStep_ms, List<Circle> circles, List<Rectangle> rectangles, List<Line> lines)
{
// Convertir timeStep de milisegundos a segundos para la simulación
float timeStepInSeconds = timeStep_ms / 1000.0f;
bool isTracted = false; // Indicador para verificar si el círculo está siendo traccionado
Overlap = 0;
// Aplicar fuerza desde el rectángulo si está sobre uno
foreach (var rectangle in rectangles)
{
float overlap = CalculateOverlapPercentage(this, rectangle);
if (overlap > 10)
{
Overlap += overlap;
isTracted = true; // El círculo está siendo traccionado por un rectángulo
// Convertir la velocidad del rectángulo de metros por minuto a metros por segundo
float rectangleSpeedInMetersPerSecond = rectangle.Speed / 60.0f;
if (rectangleSpeedInMetersPerSecond < Speed)
{
// Aplicar una fuerza de frenado si la velocidad del rectángulo es menor que la velocidad del círculo
float brakingForce = (Speed - rectangleSpeedInMetersPerSecond) * (overlap / 100.0f);
Speed -= brakingForce * timeStepInSeconds;
}
else
{
// Alinear gradualmente la velocidad del círculo con la del rectángulo si es mayor
Speed += (rectangleSpeedInMetersPerSecond - Speed) * (overlap / 100.0f) * timeStepInSeconds;
}
AngleofMovement = rectangle.Angle;
}
}
// Si el círculo no está siendo traccionado, aplicar desaceleración
if (!isTracted)
{
float deceleration = (1.0f / Mass) * 10.0f; // Coeficiente de desaceleración inversamente proporcional a la masa
Speed -= deceleration * timeStepInSeconds;
if (Speed < 0) Speed = 0; // Evitar que la velocidad sea negativa
}
// Calcular nueva posición
Vector2 direction = new Vector2((float)Math.Cos(AngleofMovement * Math.PI / 180), (float)Math.Sin(AngleofMovement * Math.PI / 180));
Vector2 velocity = direction * Speed * timeStepInSeconds;
position += velocity;
// Ajustar por colisiones con líneas
foreach (var line in lines)
{
Vector2 movementVector = Vector2FromPolar(Speed, AngleofMovement);
Vector2 circleCenter = GetCircleCenter(this);
// Calcular la nueva posición tentativa del centro del círculo
Vector2 newPosition = circleCenter + movementVector * timeStepInSeconds;
if (LineCircleCollision(newPosition, Diameter / 2, line, out Vector2 collisionPoint))
{
// Ajustar la posición del centro del círculo y el vector de movimiento
AdjustCircleAfterCollision(ref newPosition, ref movementVector, line, collisionPoint, Diameter / 2);
}
// Actualizar la posición del círculo basada en el nuevo centro
Left = newPosition.X - Diameter / 2;
Top = newPosition.Y - Diameter / 2;
Speed = movementVector.Length();
AngleofMovement = PolarAngleFromVector(movementVector);
}
// Ajustar por superposición con otros círculos
foreach (var other in circles)
{
if (this != other && IsColliding(this, other))
{
AdjustForOverlap(other);
}
}
}
Vector2 GetCircleCenter(Circle circle)
{
return new Vector2(circle.Left + circle.Diameter / 2, circle.Top + circle.Diameter / 2);
}
private bool LineCircleCollision(Vector2 circleCenter, float radius, Line line, out Vector2 collisionPoint)
{
// Transformar la línea a un vector con el ángulo rotado
float angleRadians = line.Angle * (float)Math.PI / 180;
Vector2 lineDirection = new Vector2(
(float)Math.Cos(angleRadians),
(float)Math.Sin(angleRadians)
);
// Calcular los puntos de inicio y fin de la línea
Vector2 lineStart = new Vector2(line.Left, line.Top);
Vector2 lineEnd = lineStart + lineDirection * line.Length;
// Encontrar el punto más cercano del centro del círculo a la línea
Vector2 lineToCircle = circleCenter - lineStart;
float projection = Vector2.Dot(lineToCircle, lineDirection);
projection = Math.Clamp(projection, 0, line.Length);
Vector2 closestPointOnLine = lineStart + projection * lineDirection;
// Calcular la distancia entre el círculo y el punto más cercano
float distance = Vector2.Distance(circleCenter, closestPointOnLine);
collisionPoint = closestPointOnLine;
// Verificar si hay colisión
return distance <= radius;
}
private void AdjustCircleAfterCollision(ref Vector2 circlePosition, ref Vector2 movementVector, Line line, Vector2 collisionPoint, float radius)
{
// Calcular el vector normal de la línea para reflejar la dirección de movimiento del círculo
float angleRadians = line.Angle * (float)Math.PI / 180;
Vector2 lineNormal = new Vector2(-(float)Math.Sin(angleRadians), (float)Math.Cos(angleRadians));
// Asegurar que la normal está correctamente orientada hacia fuera de la línea
if (Vector2.Dot(lineNormal, circlePosition - collisionPoint) < 0)
lineNormal = -lineNormal;
// Calcular el desplazamiento necesario para separar el círculo de la línea
Vector2 displacement = lineNormal * (radius - Vector2.Distance(circlePosition, collisionPoint));
circlePosition += displacement;
// Opcionalmente, ajustar la velocidad si el círculo debe "rebotar" de la línea
movementVector = Vector2.Reflect(movementVector, lineNormal);
}
private Vector2 Vector2FromPolar(float speed, float angleDegrees)
{
float angleRadians = angleDegrees * (float)Math.PI / 180;
return new Vector2(
speed * (float)Math.Cos(angleRadians),
speed * (float)Math.Sin(angleRadians)
);
}
private float PolarAngleFromVector(Vector2 vector)
{
return (float)Math.Atan2(vector.Y, vector.X) * 180 / (float)Math.PI;
}
private void AdjustForOverlap(Circle other)
{
if (this == other) return; // No auto-interacción
float distance = Vector2.Distance(this.position, other.position);
float radiusSum = (this.Diameter / 2) + (other.Diameter / 2);
if (distance < radiusSum) // Los círculos están solapando
{
Vector2 directionToOther = Vector2.Normalize(other.position - this.position);
float overlapDistance = radiusSum - distance;
// Decidir qué círculo mover basado en sus velocidades
if (this.Speed == 0 && other.Speed > 0)
{
// Mover este círculo si su velocidad es cero y el otro se está moviendo
this.position -= directionToOther * overlapDistance;
}
else if (other.Speed == 0 && this.Speed > 0)
{
// Mover el otro círculo si su velocidad es cero y este se está moviendo
other.position += directionToOther * overlapDistance;
}
else if (this.Speed == 0 && other.Speed == 0)
{
// Si ambos tienen velocidad cero, mover ambos a la mitad del solapamiento
this.position -= directionToOther * (overlapDistance / 2);
other.position += directionToOther * (overlapDistance / 2);
}
}
}
private bool IsColliding(Circle circle1, Circle circle2)
{
float distance = Vector2.Distance(circle1.position, circle2.position);
float radiusSum = (circle1.Diameter / 2) + (circle2.Diameter / 2);
return distance <= radiusSum;
}
// Circulos sobre Rectangulos
public static float CalculateOverlapPercentage(Circle circle, Rectangle rectangle)
{
// Convertir el círculo en un cuadrado aproximado.
float squareSide = circle.Diameter / (float)Math.Sqrt(Math.PI);
RotatedRect square = new RotatedRect(
new Point2f(circle.Left + circle.Diameter / 2, circle.Top + circle.Diameter / 2),
new Size2f(squareSide, squareSide),
0 // Sin rotación
);
// Ajustamos el rectángulo para que se considere rotado desde el centro, pero calculado desde Top-Left
RotatedRect rotatedRectangle = CreateRotatedRectFromTopLeft(rectangle);
// Usar OpenCV para encontrar la intersección.
using (var mat = new Mat())
{
var result = Cv2.RotatedRectangleIntersection(square, rotatedRectangle, mat);
if (result != RectanglesIntersectTypes.None)
{
// Calcular el área de la intersección
float intersectionArea = (float) Cv2.ContourArea(mat);
float circleArea = (float)(Math.PI * Math.Pow(circle.Diameter / 2, 2));
return (intersectionArea / circleArea) * 100;
}
}
return 0; // No hay intersección
}
public static RotatedRect CreateRotatedRectFromTopLeft(Rectangle rectangle)
{
// El punto de pivote es Top-Left, calculamos el centro sin rotar
float originalCenterX = rectangle.Left + rectangle.Length / 2.0f;
float originalCenterY = rectangle.Top + rectangle.Width / 2.0f;
// Convertimos el ángulo a radianes para la rotación
float angleRadians = rectangle.Angle * (float)Math.PI / 180;
// Calcular las nuevas coordenadas del centro después de la rotación
float rotatedCenterX = rectangle.Left + (originalCenterX - rectangle.Left) * (float)Math.Cos(angleRadians) - (originalCenterY - rectangle.Top) * (float)Math.Sin(angleRadians);
float rotatedCenterY = rectangle.Top + (originalCenterX - rectangle.Left) * (float)Math.Sin(angleRadians) + (originalCenterY - rectangle.Top) * (float)Math.Cos(angleRadians);
// Crear el RotatedRect con el nuevo centro y el tamaño original
RotatedRect rotatedRect = new RotatedRect(
new Point2f(rotatedCenterX, rotatedCenterY),
new Size2f(rectangle.Length, rectangle.Width),
rectangle.Angle
);
return rotatedRect;
}
}
public class Rectangle
{
private Vector2 position;
public float Left
{
get { return position.X; }
set { position = new Vector2(value, position.Y); }
}
public float Top
{
get { return position.Y; }
set { position = new Vector2(position.X, value); }
}
public float Length { get; set; }
public float Width { get; set; }
public float Angle { get; set; } // En grados
public float Speed { get; set; } // Velocidad del rectángulo
public Rectangle(float left = 0, float top = 0, float length = 10, float width = 10, float angle = 0, float speed = 0)
{
position = new Vector2(left, top);
Length = length;
Width = width;
Angle = angle;
Speed = speed;
}
}
public class Line
{
private Vector2 position;
public float Left
{
get { return position.X; }
set { position = new Vector2(value, position.Y); }
}
public float Top
{
get { return position.Y; }
set { position = new Vector2(position.X, value); }
}
public float Length { get; set; }
public float Width { get; set; }
public float Angle { get; set; } // En grados
public float Grip { get; set; } // Friccion por contacto
public Line(float left = 0, float top = 0, float length = 10, float angle = 0, float grip = 0)
{
position = new Vector2(left, top);
Length = length;
Angle = angle;
Grip = grip;
}
}
public class Square
{
public float Left { get; set; }
public float Top { get; set; }
public float Size { get; set; } // 'Size' es la longitud de un lado del cuadrado
public Square(float left, float top, float size)
{
Left = left;
Top = top;
Size = size;
}
}
// Clase principal que gestiona la simulación
public class SimulationManager
{
public List<Circle> circles;
public List<Rectangle> rectangles;
public List<Line> lines;
public SimulationManager()
{
circles = new List<Circle>();
rectangles = new List<Rectangle>();
lines = new List<Line>();
}
public void Step(float timeStep)
{
foreach (var circle in circles)
{
circle.Move(timeStep, circles, rectangles, lines);
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualBasic.Devices;
using nkast.Aether.Physics2D.Common;
namespace CtrEditor.Simulacion
{
internal class InterseccionCirculoRectangulo
{
// Definición de la función CalcularSuperficieCompartida
public static float CalcularSuperficieCompartida(Vector2[] vertices, Vector2 center, float radio)
{
float totalCircleArea = (float) (Math.PI * radio * radio);
// Distancia a líneas ajustado
float[] distances = new float[4];
for (int i = 0; i < 4; i++)
{
distances[i] = DistanceFromLine(center, vertices[i], vertices[(i + 1) % 4]);
}
float minDistance = float.MaxValue;
foreach (var dist in distances)
{
if (Math.Abs(dist) < Math.Abs(minDistance))
minDistance = dist;
}
float d = Math.Abs(minDistance);
float sharedArea = 0;
if (Array.TrueForAll(distances, dist => Math.Abs(dist) > radio))
{
sharedArea = totalCircleArea;
}
else if (d < radio)
{
float cosTheta = Math.Min(1, d / radio);
float sinTheta = (float)Math.Sqrt(Math.Max(0, radio * radio - d * d));
if (minDistance < 0) // El centro está dentro del rectángulo
{
float areaOutside = radio * radio * (float)Math.Acos(cosTheta) - d * sinTheta;
sharedArea = totalCircleArea - areaOutside;
}
else // El centro está fuera del rectángulo
{
sharedArea = radio * radio * (float)Math.Acos(cosTheta) - d * sinTheta;
}
}
else
{
sharedArea = 0;
}
var area = (sharedArea / totalCircleArea) * 1.1f;
return area > 1 ? 1 : area;
}
public static float DistanceFromLine(Vector2 point, Vector2 start, Vector2 end)
{
float A = end.Y - start.Y;
float B = start.X - end.X;
float C = end.X * start.Y - start.X * end.Y;
float distance = (A * point.X + B * point.Y + C) / (float)Math.Sqrt(A * A + B * B);
return distance;
}
}
}

244
Simulacion/OverlapedArea.cs Normal file
View File

@ -0,0 +1,244 @@
using nkast.Aether.Physics2D.Collision.Shapes;
using nkast.Aether.Physics2D.Common;
using nkast.Aether.Physics2D.Dynamics;
using System.Diagnostics;
using CtrEditor.Simulacion;
namespace CtrEditor.Simulacion
{
class OverlapedArea
{
public static float CalculateOverlapedArea(Body bodyA, Body bodyB)
{
List<Vector2> aVertices = GetRotatedVertices(bodyA);
List<Vector2> bVertices = GetRotatedVertices(bodyB);
List<Vector2> intersectionPolygon = SutherlandHodgmanClip(aVertices, bVertices);
//Debug.WriteLine("");
//Debug.WriteLine("");
//Debug.WriteLine("subjectPolygon :");
//foreach (var vertex in aVertices)
//{
// Debug.WriteLine(vertex);
//}
//Debug.WriteLine("clipPolygon :");
//foreach (var vertex in bVertices)
//{
// Debug.WriteLine(vertex);
//}
//Debug.WriteLine("intersectionPolygon:");
//foreach (var vertex in intersectionPolygon)
//{
// Debug.WriteLine(vertex);
//}
return ComputePolygonArea(intersectionPolygon);
}
public static float ComputePolygonArea(List<Vector2> polygon)
{
float area = 0;
int count = polygon.Count;
for (int i = 0; i < count; i++)
{
Vector2 current = polygon[i];
Vector2 next = polygon[(i + 1) % count];
area += current.X * next.Y - next.X * current.Y;
}
return Math.Abs(area) / 2.0f;
}
public static List<Vector2> SutherlandHodgmanClip(List<Vector2> subjectPolygon, List<Vector2> clipPolygon)
{
List<Vector2> outputList = new List<Vector2>(subjectPolygon);
for (int i = 0; i < clipPolygon.Count; i++)
{
Vector2 clipEdgeStart = clipPolygon[i];
Vector2 clipEdgeEnd = clipPolygon[(i + 1) % clipPolygon.Count];
List<Vector2> inputList = outputList;
outputList = new List<Vector2>();
for (int j = 0; j < inputList.Count; j++)
{
Vector2 currentVertex = inputList[j];
Vector2 previousVertex = inputList[(j + inputList.Count - 1) % inputList.Count];
if (IsInside(clipEdgeStart, clipEdgeEnd, currentVertex))
{
if (!IsInside(clipEdgeStart, clipEdgeEnd, previousVertex))
{
outputList.Add(ComputeIntersection(clipEdgeStart, clipEdgeEnd, previousVertex, currentVertex));
}
outputList.Add(currentVertex);
}
else if (IsInside(clipEdgeStart, clipEdgeEnd, previousVertex))
{
outputList.Add(ComputeIntersection(clipEdgeStart, clipEdgeEnd, previousVertex, currentVertex));
}
}
}
return outputList;
}
private static bool IsInside(Vector2 edgeStart, Vector2 edgeEnd, Vector2 point)
{
return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y) > (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X);
}
private static Vector2 ComputeIntersection(Vector2 edgeStart, Vector2 edgeEnd, Vector2 point1, Vector2 point2)
{
Vector2 edge = edgeEnd - edgeStart;
Vector2 segment = point2 - point1;
float edgeCrossSegment = Cross(edge, segment);
if (Math.Abs(edgeCrossSegment) < float.Epsilon)
{
return point1; // Return any point on the segment since they are nearly parallel.
}
float t = Cross(point1 - edgeStart, edge) / edgeCrossSegment;
return point1 + t * segment;
}
public static float Cross(Vector2 v1, Vector2 v2)
{
return v1.X * v2.Y - v1.Y * v2.X;
}
public static List<Vector2> GetRotatedRectangleVertices(Vector2 center, float width, float height, float rotation)
{
float halfWidth = width / 2.0f;
float halfHeight = height / 2.0f;
float cos = (float)Math.Cos(rotation);
float sin = (float)Math.Sin(rotation);
return new List<Vector2>
{
new Vector2(center.X + cos * (-halfWidth) - sin * (-halfHeight), center.Y + sin * (-halfWidth) + cos * (-halfHeight)),
new Vector2(center.X + cos * (halfWidth) - sin * (-halfHeight), center.Y + sin * (halfWidth) + cos * (-halfHeight)),
new Vector2(center.X + cos * (halfWidth) - sin * (halfHeight), center.Y + sin * (halfWidth) + cos * (halfHeight)),
new Vector2(center.X + cos * (-halfWidth) - sin * (halfHeight), center.Y + sin * (-halfWidth) + cos * (halfHeight))
};
}
public static List<Vector2> GetRotatedVertices(Body body)
{
List<Vector2> vertices = new List<Vector2>();
// Verificar el tipo de Shape del Body
foreach (var fixture in body.FixtureList)
{
if (fixture.Shape is PolygonShape polygonShape)
{
// Es un rectángulo o un polígono
foreach (var vertex in polygonShape.Vertices)
{
vertices.Add(RotatePoint(vertex, body.Position, body.Rotation));
}
}
else if (fixture.Shape is CircleShape circleShape)
{
// Es un círculo
float radius = circleShape.Radius;
float halfSide = radius; // El lado del cuadrado inscrito es igual al radio
Vector2[] squareVertices = new Vector2[]
{
new Vector2(-halfSide, -halfSide),
new Vector2(halfSide, -halfSide),
new Vector2(halfSide, halfSide),
new Vector2(-halfSide, halfSide)
};
foreach (var vertex in squareVertices)
{
vertices.Add(vertex + body.Position); // Trasladar el cuadrado a la posición del cuerpo
}
}
}
return vertices;
}
private static Vector2 RotatePoint(Vector2 point, Vector2 origin, float rotation)
{
float cos = (float)Math.Cos(rotation);
float sin = (float)Math.Sin(rotation);
float x = point.X * cos - point.Y * sin;
float y = point.X * sin + point.Y * cos;
return new Vector2(x + origin.X, y + origin.Y);
}
}
public class OverlapedAreaFast
{
public static float CalculateOverlapedArea(simBotella botella, simTransporte conveyor)
{
var porcentajeSuperpuesto = (float) (1.1 * CalculateOverlapPercentage(botella.Body.Position, botella.Radius, conveyor.Body.Position, conveyor.Width, conveyor.Height, conveyor.Body.Rotation));
return porcentajeSuperpuesto > 1 ? 1 : porcentajeSuperpuesto; // 1.1 Porque las botellas apollan sobre un circulo inscripto 10 % menor.
}
public static double[] RotatePoint(double px, double py, double ox, double oy, double angle)
{
double cosAngle = Math.Cos(angle);
double sinAngle = Math.Sin(angle);
double qx = ox + cosAngle * (px - ox) - sinAngle * (py - oy);
double qy = oy + sinAngle * (px - ox) + cosAngle * (py - oy);
return new double[] { qx, qy };
}
public static double CalculateOverlapPercentage(
Vector2 circleCenter, double circleRadius, Vector2 rectCenter, double rectWidth, double rectHeight, double rectAngle)
{
// Transform the center of the circle to the coordinate system of the rotated rectangle
double[] newCircleCenter = RotatePoint(circleCenter.X, circleCenter.Y, rectCenter.X, rectCenter.Y, -rectAngle);
// Create a square with the same rotation as the rectangle
double squareSide = 2 * circleRadius;
double[] squareCenter = newCircleCenter;
// Coordinates of the square (non-rotated)
double x3 = squareCenter[0] - circleRadius;
double y3 = squareCenter[1] - circleRadius;
double x4 = squareCenter[0] + circleRadius;
double y4 = squareCenter[1] + circleRadius;
// Coordinates of the rectangle (non-rotated)
double x1 = rectCenter.X - rectWidth / 2;
double y1 = rectCenter.Y - rectHeight / 2;
double x2 = rectCenter.X + rectWidth / 2;
double y2 = rectCenter.Y + rectHeight / 2;
// Limits of the intersection
double xLeft = Math.Max(x1, x3);
double xRight = Math.Min(x2, x4);
double yBottom = Math.Max(y1, y3);
double yTop = Math.Min(y2, y4);
// Width and height of the intersection
double intersectWidth = Math.Max(0, xRight - xLeft);
double intersectHeight = Math.Max(0, yTop - yBottom);
// Area of the intersection
double intersectionArea = intersectWidth * intersectHeight;
// Area of the square
double squareArea = squareSide * squareSide;
// Overlap percentage relative to the total area of the square
double squareOverlapPercentage = (squareArea > 0) ? (intersectionArea / squareArea) : 0;
return squareOverlapPercentage;
}
}
}

View File

@ -1,344 +0,0 @@
using BepuPhysics;
using BepuPhysics.Collidables;
using BepuPhysics.Trees;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.Simulacion
{
/// <summary>
/// ✅ CORREGIDO: RayHitHandler personalizado para simBarrera
/// Calcula la distancia mínima del rayo al centro de cada botella
/// </summary>
public struct BarreraRayHitHandler : IRayHitHandler
{
private simBarrera _barrera;
private List<simBotella> _detectedBottles;
private SimulationManagerBEPU _simulationManager;
private float _minDistance;
private bool _neckDetected;
private bool _fullDetected;
private Vector3 _rayOrigin;
private Vector3 _rayDirection;
internal BarreraRayHitHandler(simBarrera barrera, SimulationManagerBEPU simulationManager, Vector3 rayOrigin, Vector3 rayDirection)
{
_barrera = barrera;
_simulationManager = simulationManager;
_detectedBottles = new List<simBotella>();
_minDistance = float.MaxValue;
_neckDetected = false;
_fullDetected = false;
_rayOrigin = rayOrigin;
_rayDirection = rayDirection;
}
public bool AllowTest(CollidableReference collidable)
{
// Solo testear botellas dinámicas
if (collidable.Mobility == CollidableMobility.Dynamic)
{
var bottle = _simulationManager?.GetSimBaseFromBodyHandle(collidable.BodyHandle) as simBotella;
return bottle != null;
}
return false;
}
public bool AllowTest(CollidableReference collidable, int childIndex)
{
// Para botellas (esferas), childIndex siempre es 0
return AllowTest(collidable);
}
public void OnRayHit(in RayData ray, ref float maximumT, float t, Vector3 normal, CollidableReference collidable, int childIndex)
{
try
{
// Obtener la botella que fue hit
var bottle = _simulationManager?.GetSimBaseFromBodyHandle(collidable.BodyHandle) as simBotella;
if (bottle == null) return;
// Obtener posición del centro de la botella
var bottleCenter = bottle.GetPosition();
// ✅ CORREGIDO: Calcular la distancia mínima del rayo (línea) al centro de la botella
var minDistanceToCenter = CalculateMinimumDistanceFromRayToPoint(_rayOrigin, _rayDirection, bottleCenter);
// Actualizar distancia mínima global
if (minDistanceToCenter < _minDistance)
{
_minDistance = minDistanceToCenter;
}
// ✅ CORREGIDO: Verificar detección completa usando distancia mínima del rayo al centro
if (minDistanceToCenter <= bottle.Radius)
{
_fullDetected = true;
if (!_detectedBottles.Contains(bottle))
{
_detectedBottles.Add(bottle);
}
}
// ✅ CORREGIDO: Verificar detección de cuello usando circunferencia imaginaria con radio/2
if (_barrera.DetectNeck && minDistanceToCenter <= (bottle.Radius / 2f))
{
_neckDetected = true;
}
// System.Diagnostics.Debug.WriteLine($"[BarreraRayHitHandler] Hit: Botella={bottle.BodyHandle}, distanciaMinima={minDistanceToCenter:F3}, radio={bottle.Radius:F3}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[BarreraRayHitHandler] Error en OnRayHit: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Calcula la distancia mínima de un rayo (línea) a un punto
/// </summary>
private float CalculateMinimumDistanceFromRayToPoint(Vector3 rayOrigin, Vector3 rayDirection, Vector3 point)
{
// Vector desde el origen del rayo al punto
var rayToPoint = point - rayOrigin;
// Proyección del vector rayToPoint sobre la dirección del rayo
var projection = Vector3.Dot(rayToPoint, rayDirection);
// Punto más cercano en el rayo al punto dado
var closestPointOnRay = rayOrigin + rayDirection * projection;
// Distancia mínima del rayo al punto
return Vector3.Distance(closestPointOnRay, point);
}
// ✅ NUEVO: Método para obtener los resultados del raycast
public void GetResults(out bool luzCortada, out bool luzCortadaNeck, out float distancia, out List<simBotella> botellas)
{
luzCortada = _fullDetected;
luzCortadaNeck = _neckDetected;
distancia = _minDistance == float.MaxValue ? 0f : _minDistance;
botellas = new List<simBotella>(_detectedBottles);
}
}
/// <summary>
/// Estructura para datos de barrera que se exponen a WPF
/// </summary>
public struct BarreraData
{
public bool LuzCortada;
public bool LuzCortadaNeck;
public BarreraData(bool luzCortada, bool luzCortadaNeck)
{
LuzCortada = luzCortada;
LuzCortadaNeck = luzCortadaNeck;
}
}
public class simBarrera : simBase
{
internal bool LuzCortada;
internal bool LuzCortadaNeck;
public List<simBotella> ListSimBotellaContact;
public float Width { get; set; }
public float Height { get; set; }
public bool DetectNeck { get; set; }
// ✅ NUEVO: Propiedades para raycast
private SimulationManagerBEPU _simulationManager;
// ✅ CORREGIDO: Almacenar ángulo internamente como radianes BEPU (como simCurve)
private float _angleRadians; // ✅ SIEMPRE en radianes BEPU (ya convertido desde WPF)
internal simBarrera(Simulation simulation, float width, float height, Vector3 bepuPosition, float bepuRadians = 0, bool detectectNeck = false, SimulationManagerBEPU simulationManager = null)
{
_simulation = simulation;
_simulationManager = simulationManager;
Width = width;
Height = height;
DetectNeck = detectectNeck;
ListSimBotellaContact = new List<simBotella>();
// ✅ CREAR POSICIÓN SIN BODY FÍSICO usando parámetros BEPU internos
CreatePosition(width, height, bepuPosition, bepuRadians);
}
internal void Update(float width, float height, Vector3 bepuPosition, float bepuRadians = 0)
{
Width = width;
Height = height;
ListSimBotellaContact = new List<simBotella>();
// ✅ CREAR POSICIÓN SIN BODY FÍSICO usando parámetros BEPU internos
CreatePosition(width, height, bepuPosition, bepuRadians);
}
// ✅ NUEVO: Variables para almacenar pose sin crear body físico
private Vector3 _position;
private Quaternion _orientation;
private bool _poseInitialized = false;
// ✅ NUEVO: Propiedad pública para acceder a la posición
public Vector3 Position
{
get => _poseInitialized ? _position : Vector3.Zero;
set
{
_position = value;
_poseInitialized = true;
}
}
// ✅ NUEVO: Propiedad pública para acceder al ángulo en radianes BEPU
public float AngleRadians
{
get => _poseInitialized ? _angleRadians : 0f;
set
{
_angleRadians = value;
UpdateOrientationFromInternalAngle();
_poseInitialized = true;
}
}
// ✅ NUEVO: Método interno para actualizar orientación desde ángulo interno
private void UpdateOrientationFromInternalAngle()
{
// ✅ CREAR QUATERNION DESDE RADIANES BEPU INTERNOS
_orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, _angleRadians);
}
/// <summary>
/// ✅ INTERNO: Actualiza posición y rotación desde parámetros BEPU internos
/// </summary>
internal void Update(Vector3 bepuPosition, float bepuRadians)
{
Position = bepuPosition;
AngleRadians = bepuRadians;
}
public void SetDimensions(float width, float height)
{
Width = width;
Height = height;
}
/// <summary>
/// ✅ INTERNO: Crea la posición usando parámetros BEPU internos
/// </summary>
internal void CreatePosition(float width, float height, Vector3 bepuPosition, float bepuRadians)
{
Width = width;
Height = height;
// ✅ ALMACENAR DIRECTAMENTE parámetros BEPU internos
Position = bepuPosition;
AngleRadians = bepuRadians;
// ✅ NO CREAR BODY FÍSICO - solo almacenar datos
_bodyCreated = false;
}
/// <summary>
/// ✅ CORREGIDO: Realiza raycast para detectar botellas usando coordenadas BEPU internas
/// </summary>
public void PerformRaycast()
{
try
{
// Resetear flags
LuzCortada = false;
LuzCortadaNeck = false;
ListSimBotellaContact?.Clear();
// Validar que tenemos simulación y manager
if (_simulation == null || _simulationManager == null || !_poseInitialized)
{
System.Diagnostics.Debug.WriteLine("[PerformRaycast] Simulación, manager o pose no inicializados");
return;
}
// ✅ CORREGIDO: Usar coordenadas BEPU internas para cálculos
var bepuPosition = Position;
var Orientation = _orientation;
// ✅ CORREGIDO: Crear puntos del ray a la altura correcta
var halfWidth = Width / 2f;
var rayStartLocal = new Vector3(-halfWidth, 0, 0);
var rayEndLocal = new Vector3(halfWidth, 0, 0);
// ✅ CORREGIDO: Ajustar la posición de la barrera para que el ray esté a la altura correcta
var barreraPosition = new Vector3(bepuPosition.X, bepuPosition.Y, bepuPosition.Z);
// ✅ TRANSFORMAR PUNTOS LOCALES A COORDENADAS MUNDIALES A LA ALTURA CORRECTA
var worldStart = barreraPosition + Vector3.Transform(rayStartLocal, Orientation);
var worldEnd = barreraPosition + Vector3.Transform(rayEndLocal, Orientation);
// ✅ CALCULAR DIRECCIÓN Y DISTANCIA DEL RAY
var rayDirection = worldEnd - worldStart;
var rayDistance = rayDirection.Length();
if (rayDistance < 0.001f)
{
System.Diagnostics.Debug.WriteLine("[PerformRaycast] Ray demasiado corto");
return;
}
rayDirection = Vector3.Normalize(rayDirection);
// ✅ CORREGIDO: CREAR HANDLER PARA PROCESAR HITS con parámetros correctos
var rayHandler = new BarreraRayHitHandler(this, _simulationManager, worldStart, rayDirection);
// ✅ REALIZAR RAYCAST
_simulation.RayCast(worldStart, rayDirection, rayDistance, ref rayHandler);
// ✅ OBTENER RESULTADOS
rayHandler.GetResults(out bool luzCortada, out bool luzCortadaNeck, out float distancia, out List<simBotella> botellas);
// ✅ ASIGNAR RESULTADOS
LuzCortada = luzCortada;
LuzCortadaNeck = luzCortadaNeck;
if (ListSimBotellaContact != null)
{
ListSimBotellaContact.Clear();
ListSimBotellaContact.AddRange(botellas);
}
//System.Diagnostics.Debug.WriteLine($"[PerformRaycast] {worldStart} - {worldEnd} - {rayDistance} - {rayDirection} ");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PerformRaycast] Error: {ex.Message}");
}
}
/// <summary>
/// ✅ INTERNO: Métodos Create que trabajan solo con parámetros BEPU internos
/// </summary>
internal void Create(float width, float height, Vector3 bepuPosition, float bepuRadians = 0, bool detectectNeck = false)
{
CreatePosition(width, height, bepuPosition, bepuRadians);
}
/// <summary>
/// ✅ OVERRIDE: RemoverBody no hace nada porque no hay body físico
/// </summary>
public new void RemoverBody()
{
// ✅ NO HAY BODY FÍSICO QUE REMOVER
_bodyCreated = false;
_poseInitialized = false;
}
}
}

View File

@ -1,144 +0,0 @@
using BepuPhysics.Collidables;
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.Simulacion
{
public class simBase
{
public object WpfObject { get; set; }
public string SimObjectType { get; set; }
public BodyHandle BodyHandle { get; protected set; }
public Simulation _simulation;
protected bool _bodyCreated = false; // Bandera para saber si hemos creado un cuerpo
protected SimulationManagerBEPU _simulationManager; // ✅ NUEVO: Referencia al manager
// ✅ CORREGIDO: Restaurar factor de conversión correcto
static public float SpeedConversionFactor
{
get => 60f; // Factor de conversión de velocidad interna a m/s
}
// Constantes para las posiciones Z de los objetos 3D
public const float zPos_Transporte = 0f; // Z de la parte baja
public const float zAltura_Transporte = 0.1f; // Altura del transporte sobre zPos
public const float zPos_Guia = 0.05f; // Z de la parte baja
public const float zAltura_Guia = 0.20f; // Altura de la guía sobre zPos
public const float zPos_Barrera = zPos_Transporte + zAltura_Transporte + 0.05f; // Z de la parte baja - 0.1 Altura Botella
public const float zPos_Descarte = 0.1f; // Z de la parte baja
// Constantes para configuración
public const float zPos_Curve = zPos_Transporte+ zAltura_Transporte; // Z de la parte alta de la curva
public void RemoverBody()
{
try
{
// Solo intentar remover si realmente hemos creado un cuerpo antes
if (_bodyCreated && _simulation != null && _simulation.Bodies != null && _simulation.Bodies.BodyExists(BodyHandle))
{
_simulation.Bodies.Remove(BodyHandle);
_bodyCreated = false; // Marcar como no creado después de remover
//System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ✅ Body eliminado: {BodyHandle}");
}
//else
//{
// System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ⚠️ Body no existe o no creado: {BodyHandle}");
//}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ❌ ERROR: {ex.Message}");
_bodyCreated = false; // Marcar como no creado en caso de error
}
}
/// <summary>
/// ✅ NUEVO: Cambia la forma de un body existente, limpiando la forma anterior para evitar memory leaks
/// </summary>
protected void ChangeBodyShape(TypedIndex newShapeIndex)
{
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
// ✅ CRÍTICO: Obtener la forma anterior para limpiarla del pool de shapes
var oldShapeIndex = bodyReference.Collidable.Shape;
// Cambiar a la nueva forma
_simulation.Bodies.SetShape(BodyHandle, newShapeIndex);
// ✅ CRÍTICO: Limpiar la forma anterior del pool para evitar memory leaks
// Nota: Solo limpiar si es diferente (para evitar limpiar la forma que acabamos de asignar)
if (oldShapeIndex.Packed != newShapeIndex.Packed)
{
try
{
_simulation.Shapes.RemoveAndDispose(oldShapeIndex, _simulation.BufferPool);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[simBase] Warning: Could not dispose old shape: {ex.Message}");
// Continuar - esto no es crítico para la funcionalidad
}
}
}
}
public static float Min(float Value, float Min = 0.01f)
{
return Math.Max(Value, Min);
}
public static float GradosARadianes(float grados)
{
return grados * (float)Math.PI / 180f;
}
public static float RadianesAGrados(float radianes)
{
return radianes * 180f / (float)Math.PI;
}
public void SetPosition(float x, float y, float z = 0)
{
CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, new Vector3(x, y, z));
}
public void SetPosition(Vector2 wpfPosition)
{
// Mantener la coordenada Z actual para preservar la altura del objeto
var currentBepuPosition = CoordinateConverter.GetBepuBodyPosition(_simulation, BodyHandle);
var newBepuPosition = new Vector3(wpfPosition.X, CoordinateConverter.WpfYToBepuY(wpfPosition.Y), currentBepuPosition.Z);
CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, newBepuPosition);
}
public void SetPosition(Vector3 bepuPosition)
{
CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, bepuPosition);
}
public Vector3 GetPosition()
{
return CoordinateConverter.GetBepuBodyPosition(_simulation, BodyHandle);
}
public void SetRotation(float wpfAngle)
{
CoordinateConverter.UpdateBepuBodyRotation(_simulation, BodyHandle, wpfAngle);
}
public float GetRotationZ()
{
return CoordinateConverter.GetWpfAngleFromBepuBody(_simulation, BodyHandle);
}
}
}

View File

@ -1,274 +0,0 @@
using BepuPhysics.Collidables;
using BepuPhysics.Constraints;
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using DocumentFormat.OpenXml.Spreadsheet;
using System.Windows.Media.Media3D;
using System.Windows.Shapes;
namespace CtrEditor.Simulacion
{
public class simBotella : simBase
{
public float Radius;
public float Height; // Altura para la visualización del cilindro en Helix
private float _mass;
public bool Descartar = false;
public int isOnTransports;
public List<simBase> ListOnTransports;
public bool isRestricted;
public bool isNoMoreRestricted;
public simTransporte ConveyorRestrictedTo;
public float OverlapPercentage;
public float _neckRadius;
public bool isOnBrakeTransport; // Nueva propiedad para marcar si está en un transporte con freno
public simTransporte CurrentBrakeTransport { get; set; } // ✅ NUEVA: Referencia al transporte de freno actual
// ✅ NUEVO: Propiedades para control de flotación/presión
public float? LastTransportCollisionZ { get; set; }
public float PressureBuildup { get; set; } = 0;
public bool IsTouchingTransport { get; set; } = false;
// ✅ NUEVO: Propiedades para la física personalizada
public float BaseInverseMass { get; private set; }
public Vector3 InitialPosition3D { get; private set; }
private List<Action> _deferredActions;
public simBotella(Simulation simulation, List<Action> deferredActions, float diameter, Vector2 position, float mass, float neckRadius = 0)
{
_simulation = simulation;
_deferredActions = deferredActions;
Radius = diameter / 2f;
Height = diameter; // Altura igual al diámetro para mantener proporciones similares
_mass = mass;
_neckRadius = neckRadius;
ListOnTransports = new List<simBase>();
// ✅ USAR COORDINATECONVERTER para conversión centralizada
var position3D = new Vector3(position.X, CoordinateConverter.WpfYToBepuY(position.Y), Radius + zPos_Transporte + zAltura_Transporte);
Create(position3D);
}
public float CenterX
{
get
{
return GetPosition().X;
}
set
{
var pos = GetPosition();
SetPosition(value, pos.Y, pos.Z);
}
}
public float CenterY
{
get
{
// ✅ USAR COORDINATECONVERTER para conversión centralizada
return CoordinateConverter.BepuYToWpfY(GetPosition().Y);
}
set
{
var pos = GetPosition();
// ✅ USAR COORDINATECONVERTER para conversión centralizada
SetPosition(pos.X, CoordinateConverter.WpfYToBepuY(value), pos.Z);
}
}
public Vector2 Center
{
get
{
var pos3D = GetPosition();
// ✅ USAR COORDINATECONVERTER para conversión centralizada
return CoordinateConverter.BepuVector3ToWpfVector2(pos3D);
}
set
{
// Mantener la Z actual, solo cambiar X, Y
var currentPos = GetPosition();
// ✅ USAR COORDINATECONVERTER para conversión centralizada
SetPosition(value.X, CoordinateConverter.WpfYToBepuY(value.Y), currentPos.Z);
}
}
public float Mass
{
get
{
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
return 1f / bodyReference.LocalInertia.InverseMass;
}
return _mass;
}
set
{
_mass = value;
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var cylinder = new Cylinder(Radius, Height);
var inertia = cylinder.ComputeInertia(_mass);
// Modificamos el tensor de inercia para evitar que la botella vuelque.
var inverseInertia = inertia.InverseInertiaTensor;
inverseInertia.XX = 0f; // Resistencia infinita a la rotación en el eje X (vuelco).
inverseInertia.YY = 0f; // Resistencia infinita a la rotación en el eje Y (vuelco).
// La resistencia en ZZ (eje Z) se mantiene, permitiendo que la botella gire sobre su base.
inertia.InverseInertiaTensor = inverseInertia;
_simulation.Bodies.SetLocalInertia(BodyHandle, inertia);
}
}
}
private void Create(Vector3 position)
{
RemoverBody();
var cylinder = new Cylinder(Radius, Height);
var shapeIndex = _simulation.Shapes.Add(cylinder);
// 1. Creamos el cuerpo con una inercia por defecto.
var defaultInertia = cylinder.ComputeInertia(_mass);
var activityDescription = new BodyActivityDescription(-1f);
var bodyDescription = BodyDescription.CreateDynamic(
new RigidPose(position),
new BodyVelocity(),
defaultInertia,
new CollidableDescription(shapeIndex, 0),
activityDescription
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true;
// 2. Inmediatamente después de crearlo, aplicamos la inercia personalizada.
// Esto asegura que SetLocalInertia se llame para todos los objetos nuevos.
var inertia = cylinder.ComputeInertia(_mass);
var inverseInertia = inertia.InverseInertiaTensor;
inverseInertia.XX = 0f;
inverseInertia.YY = 0f;
inertia.InverseInertiaTensor = inverseInertia;
_simulation.Bodies.SetLocalInertia(BodyHandle, inertia);
// ✅ NUEVO: Almacenar la masa inversa base después de crear el cuerpo
// Esto nos sirve como referencia para la lógica de masa dinámica.
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
this.BaseInverseMass = bodyReference.LocalInertia.InverseMass;
// ✅ CORREGIDO: Usar el diccionario del SimulationManager y la clave .Value del handle.
if (_simulationManager != null)
_simulationManager.CollidableData[BodyHandle.Value] = this;
}
public void SetDiameter(float diameter)
{
Radius = diameter / 2f;
Height = diameter; // Mantener altura igual al diámetro para proporciones consistentes
// ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var cylinder = new Cylinder(Radius, Height);
var shapeIndex = _simulation.Shapes.Add(cylinder);
ChangeBodyShape(shapeIndex);
}
}
public void SetHeight(float height)
{
Height = height;
// ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var cylinder = new Cylinder(Radius, Height);
var shapeIndex = _simulation.Shapes.Add(cylinder);
ChangeBodyShape(shapeIndex);
}
}
public void SetMass(float mass)
{
Mass = mass;
}
/// <summary>
/// Limita la rotación de la botella solo al plano XY (siempre "de pie")
/// Esto simplifica enormemente la simulación y es más realista para botellas
/// </summary>
public void LimitRotationToXYPlane()
{
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
// Extraer solo la rotación en Z (plano XY) y eliminar las rotaciones en X e Y
var currentOrientation = bodyReference.Pose.Orientation;
// Convertir a ángulo en Z solamente
var rotationZ = GetRotationZ();
// ✅ CORREGIDO: Calificar explícitamente para evitar ambigüedad de tipos.
var correctedOrientation = System.Numerics.Quaternion.CreateFromAxisAngle(Vector3.UnitZ, rotationZ);
// Aplicar la orientación corregida
bodyReference.Pose.Orientation = correctedOrientation;
// También limitar la velocidad angular a solo rotación en Z
var angularVelocity = bodyReference.Velocity.Angular;
bodyReference.Velocity.Angular = new Vector3(0, 0, angularVelocity.Z);
}
}
public void ApplyLinearVelocity(Vector3 velocity)
{
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
bodyReference.Velocity.Linear = velocity;
}
}
public Vector3 GetLinearVelocity()
{
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
return bodyReference.Velocity.Linear;
}
return Vector3.Zero;
}
public void CenterFixtureOnConveyor()
{
// Implementar lógica de centrado si es necesario
}
public bool IsOnAnyTransport()
{
return isOnTransports > 0;
}
/// <summary>
/// ✅ SIMPLIFICADO: RemoverBody que confía en BEPU para limpiar constraints
/// </summary>
public new void RemoverBody()
{
base.RemoverBody();
}
}
}

View File

@ -1,348 +0,0 @@
using BepuPhysics.Collidables;
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.Simulacion
{
/// <summary>
/// Representa una curva o arco en la simulación física.
/// ✅ IMPORTANTE: Los ángulos _startAngle y _endAngle se almacenan INTERNAMENTE en radianes BEPU
/// (ya convertidos desde grados WPF usando CoordinateConverter.WpfDegreesToBepuRadians)
/// Las propiedades públicas StartAngle/EndAngle devuelven grados WPF usando CoordinateConverter.BepuRadiansToWpfDegrees
/// ✅ NUEVO: Cada triángulo actúa como un mini-transporte con su propia dirección tangencial fija
/// </summary>
public class simCurve : simBase
{
private float _innerRadius;
private float _outerRadius;
private float _startAngle; // ✅ SIEMPRE en radianes BEPU
private float _endAngle; // ✅ SIEMPRE en radianes BEPU
public float Speed { get; set; } // Velocidad para efectos de cinta transportadora (m/s)
private List<Action> _deferredActions;
// ✅ NUEVO: Almacenar el centro real de la curva
private Vector3 _curveCenter;
// ✅ SIMPLIFICADO: Propiedades esenciales únicamente
public List<simBotella> BottlesOnCurve { get; private set; } = new List<simBotella>();
// ✅ NUEVO: Almacenar triángulos creados para acceso directo
private Triangle[] _storedTriangles;
public float InnerRadius => _innerRadius;
public float OuterRadius => _outerRadius;
public float StartAngle => CoordinateConverter.BepuRadiansToWpfDegrees(_startAngle); // Convertir de radianes BEPU internos a grados WPF
public float EndAngle => CoordinateConverter.BepuRadiansToWpfDegrees(_endAngle); // Convertir de radianes BEPU internos a grados WPF
// ✅ NUEVO: Propiedad para acceder al centro real de la curva
public Vector3 CurveCenter => _curveCenter;
public simCurve(Simulation simulation, List<Action> deferredActions, float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 topLeft, float unused = 0, SimulationManagerBEPU simulationManager = null)
{
_simulation = simulation;
_deferredActions = deferredActions;
_simulationManager = simulationManager; // ✅ NUEVO: Almacenar referencia
_innerRadius = innerRadius;
_outerRadius = outerRadius;
// ✅ SIMPLIFICADO: Usar conversión directa WPF grados → BEPU radianes
_startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle);
_endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle);
// ✅ NUEVO: Calcular y almacenar el centro real de la curva
var curveSize = outerRadius * 2f;
var zPosition = zPos_Curve;
_curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(topLeft, curveSize, curveSize, 0f, zPosition);
// ✅ SIMPLIFICADO: Crear la curva directamente
Create(innerRadius, outerRadius, startAngle, endAngle, topLeft, 0);
}
// ✅ MODIFICADO: Actualizar la velocidad y luego la del cuerpo cinemático
public void SetSpeed(float speed)
{
Speed = speed;
ActualizarVelocidadCinematica();
}
/// <summary>
/// ✅ NUEVO: Aplica la velocidad angular al cuerpo cinemático de BEPU.
/// </summary>
public void ActualizarVelocidadCinematica()
{
if (_simulation != null && this.BodyHandle.Value >= 0 && _simulation.Bodies.BodyExists(this.BodyHandle))
{
var body = _simulation.Bodies.GetBodyReference(BodyHandle);
float effectiveRadius = (_innerRadius + _outerRadius) / 2.0f;
if (effectiveRadius > 0.001f)
{
// La velocidad tangencial (Speed) se convierte a velocidad angular (ω = v / r)
// ✅ CORREGIDO: Convertir velocidad de m/min a m/s para la simulación
float angularSpeed = (Speed / SpeedConversionFactor) / effectiveRadius;
// La rotación es alrededor del eje Z en el sistema de coordenadas de BEPU
body.Velocity.Angular = new Vector3(0, 0, angularSpeed);
}
else
{
body.Velocity.Angular = Vector3.Zero;
}
}
}
/// <summary>
/// ✅ NUEVO: Detiene completamente la curva
/// </summary>
public void StopCurve()
{
SetSpeed(0f);
}
/// <summary>
/// ✅ NUEVO: Invierte la dirección de la curva manteniendo la misma velocidad
/// </summary>
public void ReverseCurve()
{
SetSpeed(-Speed);
}
/// <summary>
/// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF
/// </summary>
internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle, float innerRadius, float outerRadius, float startAngle, float endAngle)
{
// Actualizar parámetros de la curva
_innerRadius = innerRadius;
_outerRadius = outerRadius;
// ✅ CORREGIDO: Usar conversión directa WPF grados → BEPU radianes
_startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle);
_endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle);
// ✅ NUEVO: Actualizar el centro real de la curva
var curveSize = outerRadius * 2f;
var zPosition = zPos_Curve;
_curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, curveSize, curveSize, 0f, zPosition);
Create(_curveCenter);
}
/// <summary>
/// ✅ NUEVO: Actualiza posición desde Top-Left WPF manteniendo parámetros actuales
/// </summary>
internal void SetPositionFromWpfTopLeft(Vector2 wpfTopLeft)
{
var curveSize = _outerRadius * 2f;
var zPosition = GetPosition().Z; // Mantener Z actual
_curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, curveSize, curveSize, 0f, zPosition);
Create(_curveCenter);
}
/// <summary>
/// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual
/// </summary>
internal Vector2 GetWpfTopLeft()
{
var curveSize = _outerRadius * 2f;
return CoordinateConverter.CalculateWpfTopLeftFromBepuCenter(_curveCenter, curveSize, curveSize, 0f);
}
/// <summary>
/// ✅ CORREGIDO: Obtiene los triángulos reales creados para la curva
/// Devuelve los triángulos en coordenadas locales para evitar transformación duplicada
/// </summary>
public Triangle[] GetRealBEPUTriangles()
{
try
{
if (_storedTriangles == null || _storedTriangles.Length == 0)
{
System.Diagnostics.Debug.WriteLine($"[GetRealBEPUTriangles] No hay triángulos almacenados");
return new Triangle[0];
}
// ✅ CORREGIDO: Devolver triángulos en coordenadas locales
// La visualización 3D aplicará la transformación una sola vez
System.Diagnostics.Debug.WriteLine($"[GetRealBEPUTriangles] Devolviendo {_storedTriangles.Length} triángulos en coordenadas locales");
return _storedTriangles;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[GetRealBEPUTriangles] Error: {ex.Message}");
return new Triangle[0];
}
}
/// <summary>
/// ✅ NUEVO: Obtiene los triángulos transformados a coordenadas mundiales (solo para debugging)
/// </summary>
public Triangle[] GetWorldBEPUTriangles()
{
try
{
if (_storedTriangles == null || _storedTriangles.Length == 0)
{
return new Triangle[0];
}
var worldTriangles = TransformTrianglesToWorldCoordinates(_storedTriangles);
System.Diagnostics.Debug.WriteLine($"[GetWorldBEPUTriangles] Devolviendo {worldTriangles.Length} triángulos en coordenadas mundiales");
return worldTriangles;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[GetWorldBEPUTriangles] Error: {ex.Message}");
return new Triangle[0];
}
}
/// <summary>
/// ✅ NUEVO: Transforma triángulos de coordenadas locales a mundiales
/// </summary>
private Triangle[] TransformTrianglesToWorldCoordinates(Triangle[] localTriangles)
{
try
{
if (_simulation == null || !_simulation.Bodies.BodyExists(BodyHandle))
{
System.Diagnostics.Debug.WriteLine($"[TransformTriangles] Cuerpo no existe, devolviendo triángulos locales");
return localTriangles; // Fallback: devolver triángulos sin transformar
}
var body = _simulation.Bodies[BodyHandle];
var bodyPosition = body.Pose.Position;
var bodyOrientation = body.Pose.Orientation;
var transformedTriangles = new Triangle[localTriangles.Length];
for (int i = 0; i < localTriangles.Length; i++)
{
var localTriangle = localTriangles[i];
// Transformar cada vértice del triángulo a coordenadas mundiales
var worldA = bodyPosition + Vector3.Transform(localTriangle.A, bodyOrientation);
var worldB = bodyPosition + Vector3.Transform(localTriangle.B, bodyOrientation);
var worldC = bodyPosition + Vector3.Transform(localTriangle.C, bodyOrientation);
transformedTriangles[i] = new Triangle(worldA, worldB, worldC);
}
System.Diagnostics.Debug.WriteLine($"[TransformTriangles] Transformados {transformedTriangles.Length} triángulos. Body pos: {bodyPosition}");
return transformedTriangles;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[TransformTriangles] Error: {ex.Message}, devolviendo triángulos locales");
return localTriangles; // Fallback en caso de error
}
}
public new void RemoverBody()
{
// ✅ NUEVO: Limpiar triángulos almacenados
_storedTriangles = null;
// ✅ SIMPLIFICADO: Solo remover el cuerpo principal (Mesh único)
base.RemoverBody();
}
public void Create(float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 wpfTopLeft, float unused = 0)
{
// ✅ CORREGIDO: Como Aether - startAngle y endAngle definen directamente el sector
// No hay rotación separada del objeto
// Actualizar parámetros internos
_innerRadius = innerRadius;
_outerRadius = outerRadius;
// ✅ CORREGIDO: Usar conversión directa WPF grados → BEPU radianes
_startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle);
_endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle);
// ✅ NUEVO: Actualizar el centro real de la curva
var curveSize = outerRadius * 2f;
var zPosition = zPos_Curve;
_curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, curveSize, curveSize, 0f, zPosition);
Create(_curveCenter); // Llamar a la sobrecarga privada
}
private void Create(Vector3 position)
{
RemoverBody();
var triangles = CreateSimpleArcTriangles(_innerRadius, _outerRadius, _startAngle, _endAngle);
if (triangles.Length == 0)
{
System.Diagnostics.Debug.WriteLine($"[simCurve] No se crearon triángulos, no se creará el cuerpo.");
return;
}
_storedTriangles = triangles;
// ✅ CORREGIDO: Convertir el array de triángulos a un Buffer<Triangle> de BEPU.
_simulation.BufferPool.Take(triangles.Length, out BepuUtilities.Memory.Buffer<Triangle> triangleBuffer);
for (int i = 0; i < triangles.Length; i++)
{
triangleBuffer[i] = triangles[i];
}
var mesh = new Mesh(triangleBuffer, Vector3.One, _simulation.BufferPool);
var shapeIndex = _simulation.Shapes.Add(mesh);
var bodyDescription = BodyDescription.CreateKinematic(
new RigidPose(position, Quaternion.Identity), // La rotación se maneja con la velocidad angular, no con la pose inicial
new CollidableDescription(shapeIndex, 0.1f),
new BodyActivityDescription(-1f) // ✅ CORREGIDO: -1f para que el cuerpo cinemático NUNCA duerma y no se mueva por su cuenta.
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true;
// ✅ NUEVO: Establecer la velocidad cinemática inicial al crear el cuerpo.
ActualizarVelocidadCinematica();
}
/// <summary>
/// ✅ SIMPLIFICADO: Crea triángulos usando Triangle de BEPU directamente
/// Solo superficie superior, eliminando complejidad innecesaria
/// Los triángulos se crean en coordenadas locales (Z=0) y la posición del cuerpo los ubica correctamente
/// </summary>
/// <param name="innerRadius">Radio interno del arco</param>
/// <param name="outerRadius">Radio externo del arco</param>
/// <param name="startAngle">Ángulo inicial en radianes BEPU</param>
/// <param name="endAngle">Ángulo final en radianes BEPU</param>
/// <returns>Array de triángulos nativos de BEPU en coordenadas locales</returns>
private Triangle[] CreateSimpleArcTriangles(float innerRadius, float outerRadius, float startAngle, float endAngle)
{
var triangles = new List<Triangle>();
// ✅ SIMPLIFICADO: Menos segmentos, menos complejidad
float arcLength = Math.Abs(endAngle - startAngle) * ((innerRadius + outerRadius) / 2f);
int segments = Math.Max(8, Math.Min((int)(arcLength * 8), 32)); // Menos segmentos
float angleStep = (endAngle - startAngle) / segments;
// ✅ SIMPLIFICADO: Sin inversión compleja de ángulos
for (int i = 0; i < segments; i++)
{
float angle1 = startAngle + i * angleStep;
float angle2 = startAngle + (i + 1) * angleStep;
// ✅ COORDENADAS LOCALES: Triángulos en Z=0, el cuerpo los posiciona correctamente
var inner1 = new Vector3(innerRadius * (float)Math.Cos(angle1), innerRadius * (float)Math.Sin(angle1), 0);
var outer1 = new Vector3(outerRadius * (float)Math.Cos(angle1), outerRadius * (float)Math.Sin(angle1), 0);
var inner2 = new Vector3(innerRadius * (float)Math.Cos(angle2), innerRadius * (float)Math.Sin(angle2), 0);
var outer2 = new Vector3(outerRadius * (float)Math.Cos(angle2), outerRadius * (float)Math.Sin(angle2), 0);
// ✅ USAR Triangle NATIVO DE BEPU (solo superficie superior)
triangles.Add(new Triangle(inner1, outer1, outer2));
triangles.Add(new Triangle(inner1, outer2, inner2));
}
System.Diagnostics.Debug.WriteLine($"[CreateSimpleArcTriangles] Creados {triangles.Count} triángulos en coordenadas locales");
return triangles.ToArray();
}
}
}

View File

@ -1,94 +0,0 @@
using BepuPhysics.Collidables;
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.Simulacion
{
public class simDescarte : simBase
{
private float _radius;
private List<Action> _deferredActions;
public List<simBotella> ListSimBotellaContact;
public simDescarte(Simulation simulation, List<Action> deferredActions, float diameter, Vector2 position)
{
_simulation = simulation;
_deferredActions = deferredActions;
_radius = diameter / 2f;
ListSimBotellaContact = new List<simBotella>();
// ✅ USAR COORDINATECONVERTER para conversión centralizada
var position3D = new Vector3(position.X, CoordinateConverter.WpfYToBepuY(position.Y), zPos_Descarte + _radius);
Create(position3D);
}
public float Radius
{
get { return _radius; }
set
{
_radius = Math.Max(value, 0.01f); // Mínimo 1cm
// Recrear el cuerpo con el nuevo radio
var currentPos = GetPosition();
Create(currentPos);
}
}
public void SetDiameter(float diameter)
{
Radius = diameter / 2f;
}
public float GetDiameter()
{
return _radius * 2f;
}
public void Create(Vector2 position)
{
// ✅ USAR COORDINATECONVERTER para conversión centralizada
var position3D = new Vector3(position.X, CoordinateConverter.WpfYToBepuY(position.Y), zPos_Descarte + _radius);
Create(position3D);
}
/// <summary>
/// ✅ NUEVO: Actualiza posición usando coordenadas WPF apropiadas
/// </summary>
internal void UpdateFromWpfCenter(Vector2 wpfCenter)
{
var position3D = new Vector3(wpfCenter.X, CoordinateConverter.WpfYToBepuY(wpfCenter.Y), zPos_Descarte + _radius);
// Actualizar solo posición manteniendo orientación
CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, position3D);
}
private void Create(Vector3 position)
{
RemoverBody();
// Crear esfera sensor para detección
var sphere = new BepuPhysics.Collidables.Sphere(_radius);
var shapeIndex = _simulation.Shapes.Add(sphere);
// Crear como SENSOR (Kinematic con speculative margin 0 para detección pura)
// Configurar actividad para NUNCA dormir - los sensores deben estar siempre activos
var activityDescription = new BodyActivityDescription(-1f); // -1 = nunca dormir
var bodyDescription = BodyDescription.CreateKinematic(
new RigidPose(position),
new CollidableDescription(shapeIndex, 0f), // Speculative margin 0 para sensores
activityDescription
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true; // Marcar que hemos creado un cuerpo
}
}
}

View File

@ -1,155 +0,0 @@
using BepuPhysics.Collidables;
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.Simulacion
{
public class simGuia : simBase
{
private List<Action> _deferredActions;
// Propiedades para acceder a las dimensiones del objeto WPF
public float GuideThickness { get; set; } // Espesor de la guía
// ✅ NUEVO: Agregar propiedades Width y Height para almacenar dimensiones reales
public float Width { get; set; }
public float Height { get; set; }
public simGuia(Simulation simulation, List<Action> deferredActions, float width, float height, Vector2 topLeft, float angle)
{
_simulation = simulation;
_deferredActions = deferredActions;
// ✅ NUEVO: Almacenar dimensiones reales
Width = width;
Height = height;
GuideThickness = height; // Mantener compatibilidad
Create(width, height, topLeft, angle);
}
public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle)
{
RemoverBody();
// ✅ NUEVO: Actualizar dimensiones almacenadas
Width = width;
Height = height;
GuideThickness = height; // Mantener compatibilidad
// ✅ USAR COORDINATECONVERTER para conversión centralizada
var zPosition = zAltura_Guia / 2 + zPos_Guia;
var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition);
// ✅ CRÍTICO: Crear el Box con las dimensiones correctas almacenadas
var box = new Box(Width, GuideThickness, zAltura_Guia);
var shapeIndex = _simulation.Shapes.Add(box);
// ✅ USAR COORDINATECONVERTER para conversión centralizada
var bodyDescription = BodyDescription.CreateKinematic(
new RigidPose(bepuCenter, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)),
new CollidableDescription(shapeIndex, 0.1f),
new BodyActivityDescription(0.01f)
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true;
}
// Método para actualizar las propiedades desde el objeto WPF
public void UpdateProperties(float ancho, float altoGuia, float angulo)
{
// ✅ CORREGIDO: Actualizar todas las dimensiones
Width = ancho;
Height = altoGuia;
GuideThickness = altoGuia; // Mantener compatibilidad
}
/// <summary>
/// ✅ CORREGIDO - Actualiza las dimensiones y recrea el cuerpo físico si es necesario
/// </summary>
public void SetDimensions(float width, float height)
{
// ✅ NUEVO: Actualizar dimensiones almacenadas
Width = width;
Height = height;
GuideThickness = height; // Mantener compatibilidad
// ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var box = new Box(width, GuideThickness, zAltura_Guia);
var shapeIndex = _simulation.Shapes.Add(box);
ChangeBodyShape(shapeIndex);
}
}
/// <summary>
/// ✅ CORREGIDO - Actualiza posición usando las dimensiones reales almacenadas
/// </summary>
public void SetPosition(Vector2 wpfTopLeft, float wpfAngle, float actualWidth, float actualHeight)
{
// ✅ NUEVO: Actualizar dimensiones almacenadas
Width = actualWidth;
Height = actualHeight;
GuideThickness = actualHeight; // Mantener compatibilidad
// ✅ CRÍTICO: Recrear el box colisionador con las nuevas dimensiones
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var box = new Box(actualWidth, GuideThickness, zAltura_Guia);
var shapeIndex = _simulation.Shapes.Add(box);
ChangeBodyShape(shapeIndex);
}
// ✅ USAR COORDINATECONVERTER para conversión centralizada con dimensiones reales
var zPosition = zAltura_Guia / 2 + zPos_Guia;
var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, actualWidth, actualHeight, wpfAngle, zPosition);
// Actualizar posición y rotación simultáneamente
CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle);
}
/// <summary>
/// ✅ CORREGIDO: Actualiza tanto posición como rotación usando dimensiones reales
/// </summary>
internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle, float width, float height)
{
// ✅ NUEVO: Actualizar dimensiones almacenadas
Width = width;
Height = height;
GuideThickness = height; // Mantener compatibilidad
// ✅ CRÍTICO: Recrear el box colisionador con las nuevas dimensiones
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var box = new Box(width, GuideThickness, zAltura_Guia);
var shapeIndex = _simulation.Shapes.Add(box);
ChangeBodyShape(shapeIndex);
}
// ✅ USAR COORDINATECONVERTER para conversión centralizada con dimensiones reales
var zPosition = zAltura_Guia / 2 + zPos_Guia;
var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition);
// Actualizar posición y rotación simultáneamente
CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle);
}
/// <summary>
/// ✅ CORREGIDO - Usar dimensiones reales almacenadas en lugar de aproximaciones
/// </summary>
public void SetPosition(Vector2 wpfTopLeft, float wpfAngle = 0)
{
// ✅ CORREGIDO: Usar dimensiones reales almacenadas en lugar de aproximaciones
SetPosition(wpfTopLeft, wpfAngle, Width, Height);
}
}
}

View File

@ -1,237 +0,0 @@
using BepuPhysics.Collidables;
using BepuPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CtrEditor.Simulacion
{
public class simTransporte : simBase
{
public float Speed { get; set; } // Velocidad para efectos de cinta transportadora (m/s)
public float Friction { get; set; } // Friccion para efectos de cinta transportadora
public float DistanceGuide2Guide { get; set; }
public bool isBrake { get; set; }
public bool TransportWithGuides = false;
private List<Action> _deferredActions;
public float Width { get; set; }
public float Height { get; set; }
// ✅ NUEVAS PROPIEDADES - cachear cálculos costosos
public Vector3 DirectionVector { get; private set; }
public List<simBotella> BottlesOnTransport { get; private set; } = new List<simBotella>();
public simTransporte(Simulation simulation, List<Action> deferredActions, float width, float height, Vector2 topLeft, float angle = 0, SimulationManagerBEPU simulationManager = null)
{
_simulation = simulation;
_deferredActions = deferredActions;
_simulationManager = simulationManager; // ✅ NUEVO: Almacenar referencia
Width = width;
Height = height;
// Usar el nuevo método Create que maneja Top-Left correctamente
Create(width, height, topLeft, angle);
// ✅ INICIALIZAR PROPIEDADES CRÍTICAS
UpdateCachedProperties();
}
public float Angle
{
get
{
return GetRotationZ();
}
set
{
SetRotation(value);
}
}
/// <summary>
/// ✅ SOBRESCRITO: SetRotation que actualiza automáticamente las propiedades cacheadas
/// </summary>
public new void SetRotation(float wpfAngle)
{
base.SetRotation(wpfAngle);
// ✅ CRÍTICO: Actualizar propiedades cacheadas y velocidad después del cambio de rotación
UpdateCachedProperties();
}
public new void SetPosition(float x, float y, float z = 0)
{
base.SetPosition(x, y, z);
}
/// <summary>
/// ✅ NUEVO: Actualiza posición desde Top-Left WPF con dimensiones y ángulo actuales
/// </summary>
internal void SetPositionFromWpfTopLeft(Vector2 wpfTopLeft)
{
var currentWpfAngle = GetRotationZ(); // Ya usa CoordinateConverter
var zPosition = GetPosition().Z; // Mantener Z actual
var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, currentWpfAngle, zPosition);
SetPosition(bepuCenter);
}
/// <summary>
/// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual
/// </summary>
internal Vector2 GetWpfTopLeft()
{
var bepuCenter = GetPosition();
var wpfAngle = GetRotationZ(); // Ya usa CoordinateConverter
return CoordinateConverter.CalculateWpfTopLeftFromBepuCenter(bepuCenter, Width, Height, wpfAngle);
}
/// <summary>
/// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF
/// </summary>
internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle)
{
var zPosition = GetPosition().Z; // Mantener Z actual
var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, wpfAngle, zPosition);
CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle);
// ✅ CRÍTICO: Actualizar propiedades cacheadas y velocidad después del cambio de orientación
UpdateCachedProperties();
}
// ✅ NUEVO MÉTODO - actualizar propiedades cacheadas y velocidad cinemática
internal void UpdateCachedProperties()
{
// ✅ CORREGIDO: La dirección siempre es UnitX rotado por el ángulo del transporte
// NO depende de las dimensiones (Width >= Height) sino solo de la rotación
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
var bepuQuaternion = bodyReference.Pose.Orientation;
// ✅ SIEMPRE usar UnitX y aplicar la rotación
DirectionVector = Vector3.Transform(Vector3.UnitX, bepuQuaternion);
// 🔍 DEBUG: Agregar información detallada
var wpfAngle = GetRotationZ();
//System.Diagnostics.Debug.WriteLine($"[UpdateCached] WPF Angle: {wpfAngle}°, DirectionVector: {DirectionVector}, Length: {DirectionVector.Length()}");
}
else
{
// ✅ CORREGIDO: Aplicar conversión de coordenadas WPF→BEPU en el vector
var wpfAngle = GetRotationZ(); // Ángulo WPF en grados
var wpfAngleRadians = simBase.GradosARadianes(wpfAngle);
// Calcular el vector en coordenadas WPF
var wpfX = (float)Math.Cos(wpfAngleRadians);
var wpfY = (float)Math.Sin(wpfAngleRadians);
// ✅ APLICAR CONVERSIÓN Y: En WPF Y+ es abajo, en BEPU Y+ es arriba
DirectionVector = new Vector3(wpfX, -wpfY, 0); // Invertir Y para conversión WPF→BEPU
// 🔍 DEBUG: Agregar información detallada
System.Diagnostics.Debug.WriteLine($"[UpdateCached-Fallback] WPF Angle: {wpfAngle}°, WPF Vector: ({wpfX:F3}, {wpfY:F3}), BEPU DirectionVector: {DirectionVector}, Length: {DirectionVector.Length()}");
}
// ✅ NUEVO: Actualizar la velocidad del cuerpo cinemático siempre que cambien las propiedades
ActualizarVelocidadCinematica();
}
// ✅ MODIFICADO: Actualizar la velocidad y luego la del cuerpo cinemático
public void SetSpeed(float speed)
{
Speed = speed;
ActualizarVelocidadCinematica();
}
/// <summary>
/// ✅ NUEVO: Aplica la velocidad al cuerpo cinemático de BEPU.
/// </summary>
public void ActualizarVelocidadCinematica()
{
if (_simulation != null && this.BodyHandle.Value >= 0 && _simulation.Bodies.BodyExists(this.BodyHandle))
{
var body = _simulation.Bodies.GetBodyReference(BodyHandle);
// ✅ CORREGIDO: Convertir velocidad de m/min a m/s para la simulación
body.Velocity.Linear = this.DirectionVector * (this.Speed / SpeedConversionFactor);
}
}
/// <summary>
/// Detiene completamente el transporte
/// </summary>
public void StopTransport()
{
SetSpeed(0f);
}
/// <summary>
/// Invierte la dirección del transporte manteniendo la misma velocidad
/// </summary>
public void ReverseTransport()
{
SetSpeed(-Speed);
}
public void SetDimensions(float width, float height)
{
Width = width;
Height = height;
// ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior
if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle))
{
var box = new Box(width, height, zAltura_Transporte);
var shapeIndex = _simulation.Shapes.Add(box);
ChangeBodyShape(shapeIndex);
}
// ✅ CRÍTICO: Actualizar propiedades cacheadas después del cambio de dimensiones
UpdateCachedProperties();
}
public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle = 0)
{
// ✅ USAR COORDINATECONVERTER para conversión centralizada
var zPosition = zAltura_Transporte / 2f + zPos_Transporte;
var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition);
Create(width, height, bepuCenter, wpfAngle);
}
/// <summary>
/// ✅ SIMPLIFICADO: RemoverBody ya no necesita limpiar restricciones de botellas.
/// </summary>
public new void RemoverBody()
{
base.RemoverBody();
}
public void Create(float width, float height, Vector3 bepuPosition, float wpfAngle = 0)
{
RemoverBody();
var box = new Box(width, height, zAltura_Transporte);
var shapeIndex = _simulation.Shapes.Add(box);
// ✅ USAR COORDINATECONVERTER para conversión centralizada
var bodyDescription = BodyDescription.CreateKinematic(
new RigidPose(bepuPosition, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)),
new CollidableDescription(shapeIndex, 0.1f),
new BodyActivityDescription(-1f)
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true; // Marcar que hemos creado un cuerpo
// ✅ CRÍTICO: Actualizar propiedades cacheadas después de crear el body
UpdateCachedProperties();
}
}
}

View File

@ -0,0 +1,126 @@
using System;
using System.Diagnostics;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CtrEditor.Simulacion.Fluids;
using nkast.Aether.Physics2D.Common;
using CtrEditor.ObjetosSim;
namespace CtrEditor
{
/// <summary>
/// ViewModel para controlar la simulación de fluidos de forma independiente
/// </summary>
public partial class SimulationFluidsViewModel : ObservableObject
{
private MainViewModel _mainViewModel;
private readonly DispatcherTimer _timerSimulacionFluidos;
private Stopwatch _stopwatch;
private double _lastUpdateTime;
// Propiedades observables
[ObservableProperty]
private bool isFluidSimulationRunning;
[ObservableProperty]
private float fps;
[ObservableProperty]
private int particulasCount;
/// <summary>
/// Constructor
/// </summary>
/// <param name="mainViewModel">Referencia al ViewModel principal</param>
public SimulationFluidsViewModel(MainViewModel mainViewModel)
{
_mainViewModel = mainViewModel;
// Inicializar timer para simulación de fluidos
_timerSimulacionFluidos = new DispatcherTimer();
_timerSimulacionFluidos.Interval = TimeSpan.FromMilliseconds(16); // ~60fps
_timerSimulacionFluidos.Tick += OnTickSimulacionFluidos;
_stopwatch = new Stopwatch();
_stopwatch.Start();
}
/// <summary>
/// Inicia la simulación de fluidos
/// </summary>
public void StartFluidSimulation()
{
if (IsFluidSimulationRunning)
return;
IsFluidSimulationRunning = true;
// Notificar a todos los objetos que usan fluidos que inicie su simulación
foreach (var objetoSimulable in _mainViewModel.ObjetosSimulables)
{
if (objetoSimulable is osSistemaFluidos sistemaFluidos)
{
sistemaFluidos.UpdateGeometryStart();
}
}
_lastUpdateTime = _stopwatch.Elapsed.TotalMilliseconds;
_timerSimulacionFluidos.Start();
}
/// <summary>
/// Detiene la simulación de fluidos
/// </summary>
public void StopFluidSimulation()
{
if (!IsFluidSimulationRunning)
return;
IsFluidSimulationRunning = false;
_timerSimulacionFluidos.Stop();
// Notificar a todos los objetos que usan fluidos que detenga su simulación
foreach (var objetoSimulable in _mainViewModel.ObjetosSimulables)
{
if (objetoSimulable is osSistemaFluidos sistemaFluidos)
{
sistemaFluidos.SimulationStop();
}
}
}
/// <summary>
/// Evento que se dispara cada tick de la simulación de fluidos
/// </summary>
private void OnTickSimulacionFluidos(object sender, EventArgs e)
{
// Calcular delta time
double currentTime = _stopwatch.Elapsed.TotalMilliseconds;
float deltaTime = (float)(currentTime - _lastUpdateTime) / 1000.0f; // convertir a segundos
_lastUpdateTime = currentTime;
int totalParticleCount = 0;
// Actualizar todos los sistemas de fluidos
foreach (var objetoSimulable in _mainViewModel.ObjetosSimulables)
{
if (objetoSimulable is osSistemaFluidos sistemaFluidos && sistemaFluidos.Show_On_This_Page)
{
// Actualizar la simulación con el deltaTime calculado
if (sistemaFluidos._simFluidos != null)
{
sistemaFluidos._simFluidos.Actualizar(deltaTime);
totalParticleCount += sistemaFluidos._simFluidos.ParticlesCount;
}
// Actualizar controles visuales
sistemaFluidos.UpdateControl((int)(deltaTime * 1000));
}
}
// Actualizar estadísticas
Fps = 1.0f / Math.Max(deltaTime, 0.001f);
ParticulasCount = totalParticleCount;
}
}
}

View File

@ -34,20 +34,6 @@ namespace CtrEditor
public bool IsMaximized { get; set; } = false;
}
public class CameraSettings
{
public double PositionX { get; set; } = 3.86;
public double PositionY { get; set; } = -18.13;
public double PositionZ { get; set; } = 10;
public double LookDirectionX { get; set; } = -3.86;
public double LookDirectionY { get; set; } = 18.1;
public double LookDirectionZ { get; set; } = -10;
public double UpDirectionX { get; set; } = -0.1;
public double UpDirectionY { get; set; } = 0.48;
public double UpDirectionZ { get; set; } = 0.87;
public double FieldOfView { get; set; } = 60;
}
internal class EstadoPersistente
{
// Ruta donde se guardará el estado
@ -72,8 +58,6 @@ namespace CtrEditor
public LibraryWindowSettings LibraryWindow { get; set; } = new LibraryWindowSettings();
public CameraSettings Camera { get; set; } = new CameraSettings();
// Propiedad pública con get y set para controlar el acceso a _strDirectorioTrabajo
public string directorio
{
@ -107,7 +91,6 @@ namespace CtrEditor
_strDirectorioTrabajo = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
RecentDirectories = new List<string>();
LibraryWindow = new LibraryWindowSettings();
Camera = new CameraSettings();
return this;
}
@ -150,9 +133,6 @@ namespace CtrEditor
// Asegurar que LibraryWindow esté inicializado
if (estado.LibraryWindow == null)
estado.LibraryWindow = new LibraryWindowSettings();
// Asegurar que Camera esté inicializado
if (estado.Camera == null)
estado.Camera = new CameraSettings();
return estado;
}
return new EstadoPersistente().Inizializar(); // Devuelve una nueva instancia si no existe el archivo