Compare commits

...

8 Commits

Author SHA1 Message Date
Miguel 4f2a109332 Implementar sistema de persistencia de configuración del entorno de trabajo, incluyendo captura y restauración de estado para el GridSplitter, canvas y actualizaciones 3D. Se añade la clase WorkspaceConfiguration y se actualizan varios archivos para integrar esta funcionalidad. 2025-09-03 20:51:51 +02:00
Miguel cad650b3d0 Mejorar la gestión del foco en la interfaz, evitando interferencias con el PLCControl. Se implementaron verificaciones para forzar actualizaciones de bindings sin robar el foco y se añadió un método para buscar controles padres en el árbol visual. 2025-09-02 18:55:48 +02:00
Miguel 3b953b7998 Ajustar parámetros de fricción estática y dinámica en la simulación de botellas, mejorando el comportamiento en situaciones de contacto. Implementar límites de velocidad en el plano XY y corregir la lógica de actualización de posición para evitar elevaciones excesivas. 2025-09-02 12:30:30 +02:00
Miguel 18017db56a Se eliminó el sistema de limitación de fuerzas para contactos múltiples, reemplazándolo por un enfoque de intervención post-solver que mejora el control de velocidades Z y simplifica la lógica de simulación. Se ajustaron los parámetros de fricción y recuperación, optimizando el comportamiento de las botellas en situaciones de contacto. Además, se eliminaron funciones relacionadas con el seguimiento de densidad de contactos, simplificando el código y mejorando la eficiencia del sistema. 2025-07-09 12:05:49 +02:00
Miguel b44bdc5ece Se implementó un nuevo sistema de limitación de fuerzas para contactos múltiples simultáneos en la simulación de botellas, mejorando la estabilidad y realismo al evitar fuerzas armónicas irreales. Se introdujo un enfoque post-solver para controlar velocidades Z excesivas y se simplificó la lógica de integración de velocidad. Además, se ajustaron los parámetros de fricción y recuperación para optimizar el comportamiento en situaciones de contacto. Se añadieron nuevas funciones para el seguimiento de densidad de contactos y la calibración visual del deslizamiento. 2025-07-09 09:47:39 +02:00
Miguel 6887ede5e0 Se realizaron ajustes significativos en la configuración de fricción y recuperación en la simulación de botellas, mejorando la estabilidad y el comportamiento en contactos. Se implementaron nuevas funciones para limitar fuerzas Z y calcular la profundidad de penetración, optimizando la respuesta de las botellas en situaciones de contacto. Además, se ajustaron los parámetros de amortiguamiento y velocidad de separación para lograr un comportamiento más realista en la simulación. 2025-07-08 17:10:44 +02:00
Miguel 62b45ebf1c Se implementaron mejoras en la gestión de imágenes de fondo en el viewport 3D, permitiendo agregar, remover y sincronizar imágenes de fondo con la visualización 3D. Se corrigieron las convenciones de posicionamiento y escalado para asegurar coherencia visual entre el canvas 2D y la representación 3D. Además, se ajustaron los coeficientes de fricción en la simulación de botellas, optimizando el comportamiento en contactos y curvas. 2025-07-06 21:29:53 +02:00
Miguel f7f49d5df0 Se implementó un sistema de fricción dinámica que simula el comportamiento de fricción estática y dinámica en contactos botella-transporte y botella-curva, mejorando el flujo de materiales y reduciendo atascos. Se reemplazó el sistema de presión por un nuevo sistema de calibración visual que permite ajustar las fricciones de cada componente, mostrando el nivel de deslizamiento en las botellas. Además, se realizaron ajustes en la lógica de colisiones y se optimizó la gestión de fricción en la simulación. 2025-07-06 21:07:11 +02:00
17 changed files with 1080 additions and 189 deletions

View File

@ -30,3 +30,91 @@ simTransporte : Es un box por donde las botellas pueden desplazarse usando un tr
* 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. * 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.
* Se ha implementado un sistema de fricción dinámica que simula el comportamiento de fricción estática vs dinámica para contactos botella-transporte y botella-curva. El sistema calcula el deslizamiento relativo entre la botella y la superficie del transporte: cuando el deslizamiento es bajo (< 0.05 m/s), se aplica fricción estática alta para un arrastre efectivo; cuando el deslizamiento es alto, se aplica fricción dinámica menor para permitir un deslizamiento suave. Para curvas, se calcula la velocidad de superficie usando el producto vectorial (v = ω × r). Para contactos botella-botella, se usa detección de profundidad de penetración para aplicar materiales ultra-suaves cuando hay alta presión, previniendo acumulaciones explosivas. Este enfoque proactivo reemplaza el sistema reactivo de `PressureBuildup` y proporciona un comportamiento más físicamente realista y estable en acumulaciones de botellas.
* Se ha reemplazado el sistema `ProcessPressureSystem` por un nuevo sistema de calibración visual `HighSlippery` que permite ajustar las fricciones de cada componente. Cada botella tiene una propiedad `HighSlippery` (rango 0-9) con sistema de heatup/cooldown que se incrementa durante deslizamiento y se decrementa durante adherencia. El valor se muestra como número en el centro de cada botella para calibración visual. Los `SpringSettings` se mantienen >= 30 para evitar compresiones irreales. El sistema permite calibrar las tasas de heatup/cooldown específicas para transportes (0.2/0.1) y curvas (0.15/0.08), y el umbral de cambio estático/dinámico (HighSlippery > 3). Este sistema elimina el problema del cooldown que reactivaba prematuramente la fricción estática.
# Sistema de Limitación de Fuerzas para Contactos Múltiples Simultáneos (ELIMINADO)
## Problema Identificado
Durante la simulación de "trenes largos" de botellas que impactan simultáneamente, se generaban fuerzas armónicas irreales que no ocurren en la vida real. Aunque tengas 50 botellas empujando, la fuerza máxima está limitada por la resistencia de los materiales.
## Solución Inicial Implementada (OBSOLETA)
### 1. Sistema de Tracking de Densidad de Contactos
- **Discretización espacial**: El espacio se dividía en regiones de 0.5m x 0.5m x 0.5m
- **Contador por región**: Se contaba el número de contactos simultáneos en cada región
- **Límite de contactos**: Máximo 10 contactos antes de aplicar limitación de fuerzas
### 2. Factor de Escala Dinámico
```csharp
// Sin limitación hasta 10 contactos
if (contactCount <= 10) return 1.0f;
// Reducción progresiva: 5% por cada contacto extra
var scaleFactor = 1.0f - (excessContacts * 0.05f);
return Math.Max(scaleFactor, 0.3f); // Mínimo 30% de fuerza original
```
### 3. Aplicación Integral
- **SpringSettings**: Se escalaban proporcionalmente (frecuencia reducida)
- **MaximumRecoveryVelocity**: Se reducía proporcionalmente
- **Todos los tipos de contacto**: Botella-botella, botella-transporte, botella-curva, etc.
## Razón para la Eliminación
Este sistema se **eliminó completamente** porque:
- Era un enfoque indirecto modificando parámetros de material
- El nuevo sistema de **intervención post-solver** es más efectivo
- La cancelación directa de velocidades Z es más elegante y simple
- Reducía la complejidad del código sin comprometer la funcionalidad
# Sistema de Intervención Post-Solver para Control de Velocidades Z
## Problema Identificado
El sistema anterior de limitación de fuerzas pre-solver no controlaba directamente las velocidades Z excesivas generadas por el solver de BEPU. Las botellas seguían elevándose debido a fuerzas de separación acumuladas que se aplicaban después de las restricciones iniciales. Además, el sistema de `forceScale` para contactos múltiples era un enfoque indirecto que no atacaba la causa raíz del problema.
## Solución Implementada
### 1. Enfoque Post-Solver como "Fusible de Energía"
- **Timing crítico**: Intervención en `OnSubstepEnded` - después del solver, antes de la integración de posición
- **Fusible de energía**: Elimina velocidades Z excesivas sin interferir con el comportamiento normal del solver
- **Preservación de física**: Permite al solver trabajar con fuerzas realistas, interviniendo solo cuando es necesario
### 2. Control de Velocidades Z Granular
```csharp
// Límites configurables
const float maxUpwardVelocity = 0.15f; // Anti-elevación
const float maxDownwardVelocity = 2.0f; // Anti-hundimiento
const float zDampingFactor = 0.7f; // Amortiguamiento suave
// Intervención directa en velocidades
if (velocity.Linear.Z > maxUpwardVelocity) {
velocity.Linear.Z = maxUpwardVelocity;
}
```
### 3. Corrección de Posición Extrema
- **Detección de altura excesiva**: Si la botella está muy por encima del nivel normal
- **Corrección suave**: Aplicar velocidad correctiva hacia abajo (máximo 0.5 m/s)
- **Cálculo proporcional**: Velocidad correctiva basada en la altura excesiva
## Simplificación del Sistema
### 1. Eliminación de `ApplyZForceLimitation`
- **Antes**: Limitación pre-solver que interfería con el comportamiento normal
- **Ahora**: Intervención post-solver más directa y efectiva
### 2. Simplificación de `IntegrateVelocity`
- **Antes**: Control complejo de velocidades Z durante la integración
- **Ahora**: Solo control angular básico (anti-vuelco) y damping en Z
- **Beneficio**: Menor interferencia con la física normal de BEPU
## Beneficios del Enfoque Post-Solver
- **Más efectivo**: Intervención después de que el solver haya calculado todas las fuerzas
- **Menos interferencia**: Permite comportamiento físico normal hasta el punto de intervención
- **Más directo**: Control directo de velocidades en lugar de modificar parámetros de material
- **Más predecible**: Comportamiento determinista independiente de las configuraciones del solver
- **Mantenimiento de rigidez**: Las fuerzas normales en XY permanecen intactas
- **Solución integral**: Resuelve tanto el problema de elevación como el de contactos múltiples simultáneos
- **Código más simple**: Elimina la complejidad del sistema de tracking de densidad de contactos

View File

@ -0,0 +1,90 @@
# Sistema de Persistencia de Configuración del Entorno de Trabajo
## Resumen
Se ha implementado un sistema completo para guardar y restaurar la configuración del entorno de trabajo para cada imagen, incluyendo la posición del GridSplitter, el estado del canvas (zoom, posición, offset) y el estado de `Is3DUpdateEnabled`.
## Archivos Creados/Modificados
### 1. Nuevo Archivo: `Models/WorkspaceConfiguration.cs`
- **Propósito**: Contiene la configuración del entorno de trabajo
- **Propiedades principales**:
- `GridSplitterPosition`: Altura del GridSplitter que separa vista 2D y 3D
- `CanvasCenterX/Y`: Posición central del canvas en píxeles
- `ZoomLevel`: Nivel de zoom actual del canvas
- `CanvasOffsetX/Y`: Offset de transformación del canvas
- `Is3DUpdateEnabled`: Estado de las actualizaciones 3D
- `HorizontalScrollOffset/VerticalScrollOffset`: Posición del scroll
### 2. Modificado: `MainViewModel.cs`
- **Agregado**: Propiedad `WorkspaceConfig` para manejar la configuración
- **Nuevos métodos**:
- `CaptureWorkspaceState()`: Captura el estado actual desde la UI
- `RestoreWorkspaceState()`: Restaura el estado a la UI
- `InitializeDefaultWorkspaceConfig()`: Inicializa configuración por defecto
### 3. Modificado: `SimulationData` (en MainViewModel.cs)
- **Agregado**: Propiedad `WorkspaceConfig` para persistir la configuración junto con cada imagen
### 4. Modificado: `Serialization/StateSerializer.cs`
- **Modificado `SaveState()`**: Ahora captura automáticamente el estado del workspace antes de guardar
- **Modificado `LoadCurrentPageState()`**: Carga y restaura la configuración del workspace
### 5. Modificado: `MainWindow.xaml`
- **Agregado**: Nombres a los elementos Grid para poder accederlos desde código:
- `x:Name="MainGrid"` al Grid principal
- `x:Name="MiddleColumnGrid"` al Grid de la segunda columna
## Funcionamiento
### Guardado Automático
1. Cuando se guarda el estado de una imagen (`SaveState()`), el sistema automáticamente:
- Captura la posición actual del GridSplitter
- Captura el estado actual del canvas (zoom, posición, scroll)
- Captura el estado de `Is3DUpdateEnabled`
- Guarda toda esta información junto con los datos de la imagen
### Carga Automática
1. Cuando se carga una imagen (`LoadState()`), el sistema:
- Carga la configuración del workspace guardada para esa imagen
- Restaura automáticamente todos los valores a la UI
- Si no hay configuración guardada, usa valores por defecto
### Persistencia por Imagen
- Cada imagen tiene su propia configuración independiente
- La configuración se guarda en el mismo archivo JSON que los objetos simulables
- Compatible con versiones anteriores (si no hay configuración, usa valores por defecto)
## Características Técnicas
### Manejo de Errores
- Todos los métodos de captura y restauración tienen manejo de excepciones
- Los errores se registran en Debug pero no interrumpen el proceso de guardado/carga
### Compatibilidad
- Compatible con archivos existentes (retro-compatibilidad)
- Si un archivo no tiene configuración del workspace, se crea una por defecto
### Uso del Dispatcher
- La restauración del estado usa `Dispatcher.BeginInvoke()` para asegurar que la UI esté lista
- Prioridad `Background` para no bloquear la interfaz
## Beneficios
1. **Consistencia**: Cada imagen mantiene su configuración visual específica
2. **Productividad**: Los usuarios no necesitan reconfigurar la vista cada vez que cambian de imagen
3. **Flexibilidad**: Diferentes imágenes pueden tener diferentes configuraciones de trabajo
4. **Transparencia**: El sistema funciona automáticamente sin intervención del usuario
5. **Robustez**: Manejo de errores y compatibilidad con versiones anteriores
## Uso
El sistema es completamente automático. Los usuarios simplemente trabajan normalmente y:
- Al cambiar de imagen, la configuración se guarda automáticamente
- Al seleccionar una imagen, su configuración se restaura automáticamente
- El estado de `Is3DUpdateEnabled` se mantiene independiente para cada imagen
- La posición del GridSplitter se recuerda para cada imagen
## Notas de Implementación
- Se usa `TransformGroup`, `ScaleTransform` y `TranslateTransform` para manejar el canvas
- El GridSplitter se maneja a través de las `RowDefinitions` del Grid de la segunda columna
- La configuración se serializa junto con los datos de simulación en formato JSON
- Se mantiene la estructura existente del sistema de serialización

View File

@ -24,7 +24,8 @@ using System.Text.RegularExpressions;
using System.Collections.Specialized; using System.Collections.Specialized;
using CtrEditor.Serialization; // Add this line using CtrEditor.Serialization; // Add this line
using CtrEditor.Controls; // Add this using directive using CtrEditor.Controls; // Add this using directive
using CtrEditor.PopUps; // Add this using directive using System.Linq;
using System.Windows.Media;
namespace CtrEditor namespace CtrEditor
@ -273,6 +274,9 @@ namespace CtrEditor
[ObservableProperty] [ObservableProperty]
private PLCViewModel pLCViewModel; private PLCViewModel pLCViewModel;
[ObservableProperty]
private Models.WorkspaceConfiguration workspaceConfig;
[ObservableProperty] [ObservableProperty]
private string selectedImage; private string selectedImage;
@ -442,8 +446,138 @@ namespace CtrEditor
// NOTA: La conexión del manager 3D se hace en MainWindow_Loaded // NOTA: La conexión del manager 3D se hace en MainWindow_Loaded
// después de que se cree la instancia de BEPUVisualization3DManager // después de que se cree la instancia de BEPUVisualization3DManager
// Inicializar configuración del workspace
WorkspaceConfig = new Models.WorkspaceConfiguration();
} }
#region Workspace Configuration Management
/// <summary>
/// Captura el estado actual del workspace desde la UI
/// </summary>
public void CaptureWorkspaceState()
{
if (MainWindow == null) return;
try
{
// Capturar posición del GridSplitter (altura de la fila 3 que contiene la vista 3D)
var middleGrid = MainWindow.FindName("MiddleColumnGrid") as Grid;
if (middleGrid != null && middleGrid.RowDefinitions.Count > 3)
{
// Capturar la altura actual de la fila 3 (vista 3D) en píxeles
var row3ActualHeight = middleGrid.RowDefinitions[3].ActualHeight;
WorkspaceConfig.GridSplitterPosition = row3ActualHeight;
}
// Capturar estado del canvas y scroll
var scrollViewer = MainWindow.FindName("ImagenEnTrabajoScrollViewer") as ScrollViewer;
if (scrollViewer != null)
{
WorkspaceConfig.HorizontalScrollOffset = scrollViewer.HorizontalOffset;
WorkspaceConfig.VerticalScrollOffset = scrollViewer.VerticalOffset;
}
// Capturar transformaciones del canvas
var canvas = MainWindow.FindName("ImagenEnTrabajoCanvas") as Canvas;
if (canvas?.RenderTransform is TransformGroup transformGroup)
{
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
if (scaleTransform != null)
{
WorkspaceConfig.ZoomLevel = scaleTransform.ScaleX;
}
if (translateTransform != null)
{
WorkspaceConfig.CanvasOffsetX = translateTransform.X;
WorkspaceConfig.CanvasOffsetY = translateTransform.Y;
}
}
// Capturar centro del canvas
var centerPixels = MainWindow.ObtenerCentroCanvasPixels();
WorkspaceConfig.CanvasCenterX = centerPixels.X;
WorkspaceConfig.CanvasCenterY = centerPixels.Y;
// Capturar estado de Is3DUpdateEnabled
WorkspaceConfig.Is3DUpdateEnabled = Is3DUpdateEnabled;
}
catch (Exception ex)
{
// Log error but don't throw to avoid interrupting save process
System.Diagnostics.Debug.WriteLine($"Error capturing workspace state: {ex.Message}");
}
}
/// <summary>
/// Restaura el estado del workspace a la UI
/// </summary>
public void RestoreWorkspaceState()
{
if (MainWindow == null || WorkspaceConfig == null) return;
try
{
// Restaurar posición del GridSplitter (altura de la fila 3 que contiene la vista 3D)
var middleGrid = MainWindow.FindName("MiddleColumnGrid") as Grid;
if (middleGrid != null && middleGrid.RowDefinitions.Count > 3)
{
// Establecer la altura de la vista 3D en píxeles absolutos
middleGrid.RowDefinitions[3].Height = new GridLength(WorkspaceConfig.GridSplitterPosition, GridUnitType.Pixel);
}
// Restaurar estado del canvas y scroll
var scrollViewer = MainWindow.FindName("ImagenEnTrabajoScrollViewer") as ScrollViewer;
if (scrollViewer != null)
{
scrollViewer.ScrollToHorizontalOffset(WorkspaceConfig.HorizontalScrollOffset);
scrollViewer.ScrollToVerticalOffset(WorkspaceConfig.VerticalScrollOffset);
}
// Restaurar transformaciones del canvas
var canvas = MainWindow.FindName("ImagenEnTrabajoCanvas") as Canvas;
if (canvas?.RenderTransform is TransformGroup transformGroup)
{
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
if (scaleTransform != null)
{
scaleTransform.ScaleX = WorkspaceConfig.ZoomLevel;
scaleTransform.ScaleY = WorkspaceConfig.ZoomLevel;
}
if (translateTransform != null)
{
translateTransform.X = WorkspaceConfig.CanvasOffsetX;
translateTransform.Y = WorkspaceConfig.CanvasOffsetY;
}
}
// Restaurar estado de Is3DUpdateEnabled
Is3DUpdateEnabled = WorkspaceConfig.Is3DUpdateEnabled;
}
catch (Exception ex)
{
// Log error but don't throw to avoid interrupting load process
System.Diagnostics.Debug.WriteLine($"Error restoring workspace state: {ex.Message}");
}
}
/// <summary>
/// Inicializa la configuración del workspace con valores por defecto
/// </summary>
public void InitializeDefaultWorkspaceConfig()
{
WorkspaceConfig = new Models.WorkspaceConfiguration();
}
#endregion
// Métodos para manejo de datos de imágenes // Métodos para manejo de datos de imágenes
private void RenameImage(string imageName) private void RenameImage(string imageName)
{ {
@ -1385,6 +1519,9 @@ namespace CtrEditor
// Diccionario para almacenar datos expandidos de imágenes // Diccionario para almacenar datos expandidos de imágenes
public Dictionary<string, Models.ImageData>? ImageDataDictionary { get; set; } public Dictionary<string, Models.ImageData>? ImageDataDictionary { get; set; }
// Configuración del entorno de trabajo para cada imagen
public Models.WorkspaceConfiguration? WorkspaceConfig { get; set; }
// Compatibilidad con versiones anteriores - OBSOLETO // Compatibilidad con versiones anteriores - OBSOLETO
[System.Obsolete("Use ImageDataDictionary instead")] [System.Obsolete("Use ImageDataDictionary instead")]
public Dictionary<string, string>? ImageCustomNames { get; set; } public Dictionary<string, string>? ImageCustomNames { get; set; }

View File

@ -76,7 +76,7 @@
</MenuItem> </MenuItem>
</Menu> </Menu>
<Grid Margin="0,20,0,0"> <Grid x:Name="MainGrid" Margin="0,20,0,0">
<!-- Margen superior para el menú --> <!-- Margen superior para el menú -->
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" MinWidth="200" /> <ColumnDefinition Width="1*" MinWidth="200" />
@ -120,7 +120,7 @@
<GridSplitter Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right" Width="5" Background="LightGray" /> <GridSplitter Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right" Width="5" Background="LightGray" />
<!-- Segunda Columna --> <!-- Segunda Columna -->
<Grid Grid.Column="1"> <Grid x:Name="MiddleColumnGrid" Grid.Column="1">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="3*" /> <RowDefinition Height="3*" />

View File

@ -107,6 +107,18 @@ namespace CtrEditor
// Conectar el manager 3D con el simulation manager (orden correcto de inicialización) // Conectar el manager 3D con el simulation manager (orden correcto de inicialización)
viewModel.simulationManager.Visualization3DManager = _visualization3DManager; viewModel.simulationManager.Visualization3DManager = _visualization3DManager;
// ✅ NUEVO: Si ya hay una imagen de fondo cargada, agregarla al viewport 3D
if (imagenDeFondo?.Source is BitmapImage existingImage)
{
// Necesitamos la ruta original de la imagen, usar el UriSource si está disponible
var imageUri = existingImage.UriSource?.LocalPath ?? existingImage.UriSource?.ToString();
if (!string.IsNullOrEmpty(imageUri))
{
_visualization3DManager.SetBackgroundImage(imageUri, existingImage.PixelWidth, existingImage.PixelHeight);
System.Diagnostics.Debug.WriteLine($"[3D Background] ✅ Imagen existente agregada al viewport 3D: {imageUri}");
}
}
} }
} }
@ -168,6 +180,12 @@ namespace CtrEditor
Canvas.SetLeft(imagenDeFondo, 0); Canvas.SetLeft(imagenDeFondo, 0);
Canvas.SetTop(imagenDeFondo, 0); Canvas.SetTop(imagenDeFondo, 0);
// ✅ NUEVO: Agregar la imagen de fondo al viewport 3D con la misma escala
if (_visualization3DManager != null)
{
_visualization3DManager.SetBackgroundImage(imagePath, bitmap.Width, bitmap.Height);
}
// Eliminar solo los ROIs, no la imagen de fondo ni el área de panning // Eliminar solo los ROIs, no la imagen de fondo ni el área de panning
for (int i = ImagenEnTrabajoCanvas.Children.Count - 1; i >= 0; i--) for (int i = ImagenEnTrabajoCanvas.Children.Count - 1; i >= 0; i--)
{ {
@ -367,12 +385,10 @@ namespace CtrEditor
} }
} }
private void MainWindow_KeyDown(object sender, KeyEventArgs e) private void MainWindow_KeyDown(object sender, KeyEventArgs e)
{ {
// Only force canvas focus if PanelEdicion doesn't have focus // Only force canvas focus if PanelEdicion doesn't have focus AND PLCControl doesn't have focus
if (!PanelEdicion.IsKeyboardFocusWithin) if (!PanelEdicion.IsKeyboardFocusWithin && !PLCSim.IsKeyboardFocusWithin)
{ {
if (!ImagenEnTrabajoCanvas.IsFocused) if (!ImagenEnTrabajoCanvas.IsFocused)
{ {
@ -539,6 +555,59 @@ namespace CtrEditor
_objectManager.ClearUndoHistory(); _objectManager.ClearUndoHistory();
} }
/// <summary>
/// ✅ NUEVO: Establece la imagen de fondo en el viewport 3D
/// </summary>
/// <param name="imagePath">Ruta de la imagen de fondo</param>
public void SetBackgroundImage3D(string imagePath)
{
if (_visualization3DManager != null && imagenDeFondo?.Source is BitmapImage bitmap)
{
_visualization3DManager.SetBackgroundImage(imagePath, bitmap.Width, bitmap.Height);
}
}
/// <summary>
/// ✅ NUEVO: Remueve la imagen de fondo del viewport 3D
/// </summary>
public void RemoveBackgroundImage3D()
{
_visualization3DManager?.RemoveBackgroundImage();
}
/// <summary>
/// ✅ NUEVO: Sincroniza la imagen de fondo existente con el viewport 3D
/// Útil cuando se necesita actualizar la imagen 3D basándose en la imagen 2D actual
/// </summary>
public void SyncBackgroundImage3D()
{
if (_visualization3DManager == null) return;
if (imagenDeFondo?.Source is BitmapImage existingImage)
{
// Intentar obtener la ruta de la imagen desde diferentes fuentes
var imageUri = existingImage.UriSource?.LocalPath ??
existingImage.UriSource?.ToString() ??
existingImage.BaseUri?.LocalPath;
if (!string.IsNullOrEmpty(imageUri))
{
_visualization3DManager.SetBackgroundImage(imageUri, existingImage.PixelWidth, existingImage.PixelHeight);
System.Diagnostics.Debug.WriteLine($"[3D Background] ✅ Imagen sincronizada con viewport 3D");
}
else
{
System.Diagnostics.Debug.WriteLine($"[3D Background] ⚠️ No se pudo obtener la ruta de la imagen para sincronizar");
}
}
else
{
// Si no hay imagen cargada, remover la imagen de fondo 3D
_visualization3DManager.RemoveBackgroundImage();
System.Diagnostics.Debug.WriteLine($"[3D Background] ✅ Imagen de fondo 3D removida (no hay imagen 2D)");
}
}
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e) private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{ {
// Aceptar el evento si viene del canvas o de la imagen de fondo // Aceptar el evento si viene del canvas o de la imagen de fondo
@ -750,8 +819,8 @@ namespace CtrEditor
private void Canvas_KeyDown(object sender, KeyEventArgs e) private void Canvas_KeyDown(object sender, KeyEventArgs e)
{ {
// Only handle if PanelEdicion doesn't have focus // Only handle if PanelEdicion doesn't have focus AND PLCControl doesn't have focus
if (!PanelEdicion.IsKeyboardFocusWithin) if (!PanelEdicion.IsKeyboardFocusWithin && !PLCSim.IsKeyboardFocusWithin)
{ {
HandleKeyDown(e); HandleKeyDown(e);
} }
@ -759,8 +828,8 @@ namespace CtrEditor
private void ScrollViewer_PreviewKeyDown(object sender, KeyEventArgs e) private void ScrollViewer_PreviewKeyDown(object sender, KeyEventArgs e)
{ {
// Only handle if PanelEdicion doesn't have focus // Only handle if PanelEdicion doesn't have focus AND PLCControl doesn't have focus
if (!PanelEdicion.IsKeyboardFocusWithin && if (!PanelEdicion.IsKeyboardFocusWithin && !PLCSim.IsKeyboardFocusWithin &&
(e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Up || e.Key == Key.Down)) (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Up || e.Key == Key.Down))
{ {
HandleKeyDown(e); HandleKeyDown(e);

View File

@ -0,0 +1,104 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel;
namespace CtrEditor.Models
{
/// <summary>
/// Configuración del entorno de trabajo que se guarda por imagen.
/// Incluye posiciones de splitters, canvas, zoom y otras configuraciones del workspace.
/// </summary>
public partial class WorkspaceConfiguration : ObservableObject
{
[ObservableProperty]
[property: Description("Height of the 3D view row in pixels")]
[property: Category("Splitters:")]
private double gridSplitterPosition = 100.0; // Valor por defecto en píxeles
[ObservableProperty]
[property: Description("X position of the canvas center in pixels")]
[property: Category("Canvas Position:")]
private double canvasCenterX = 0.0;
[ObservableProperty]
[property: Description("Y position of the canvas center in pixels")]
[property: Category("Canvas Position:")]
private double canvasCenterY = 0.0;
[ObservableProperty]
[property: Description("Current zoom level of the canvas")]
[property: Category("Canvas View:")]
private double zoomLevel = 1.0;
[ObservableProperty]
[property: Description("X offset of the canvas")]
[property: Category("Canvas View:")]
private double canvasOffsetX = 0.0;
[ObservableProperty]
[property: Description("Y offset of the canvas")]
[property: Category("Canvas View:")]
private double canvasOffsetY = 0.0;
[ObservableProperty]
[property: Description("Whether 3D updates are enabled")]
[property: Category("3D Settings:")]
private bool is3DUpdateEnabled = true;
[ObservableProperty]
[property: Description("Horizontal scroll position of the canvas")]
[property: Category("Canvas View:")]
private double horizontalScrollOffset = 0.0;
[ObservableProperty]
[property: Description("Vertical scroll position of the canvas")]
[property: Category("Canvas View:")]
private double verticalScrollOffset = 0.0;
/// <summary>
/// Constructor por defecto con valores iniciales
/// </summary>
public WorkspaceConfiguration()
{
// Valores por defecto ya establecidos en las propiedades
}
/// <summary>
/// Crea una copia de la configuración actual
/// </summary>
/// <returns>Nueva instancia con los mismos valores</returns>
public WorkspaceConfiguration Clone()
{
return new WorkspaceConfiguration
{
GridSplitterPosition = this.GridSplitterPosition,
CanvasCenterX = this.CanvasCenterX,
CanvasCenterY = this.CanvasCenterY,
ZoomLevel = this.ZoomLevel,
CanvasOffsetX = this.CanvasOffsetX,
CanvasOffsetY = this.CanvasOffsetY,
Is3DUpdateEnabled = this.Is3DUpdateEnabled,
HorizontalScrollOffset = this.HorizontalScrollOffset,
VerticalScrollOffset = this.VerticalScrollOffset
};
}
/// <summary>
/// Copia los valores de otra configuración
/// </summary>
/// <param name="source">Configuración fuente</param>
public void CopyFrom(WorkspaceConfiguration source)
{
if (source == null) return;
GridSplitterPosition = source.GridSplitterPosition;
CanvasCenterX = source.CanvasCenterX;
CanvasCenterY = source.CanvasCenterY;
ZoomLevel = source.ZoomLevel;
CanvasOffsetX = source.CanvasOffsetX;
CanvasOffsetY = source.CanvasOffsetY;
Is3DUpdateEnabled = source.Is3DUpdateEnabled;
HorizontalScrollOffset = source.HorizontalScrollOffset;
VerticalScrollOffset = source.VerticalScrollOffset;
}
}
}

View File

@ -7,6 +7,7 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
using Color = System.Windows.Media.Color; using Color = System.Windows.Media.Color;
using System;
namespace CtrEditor namespace CtrEditor
{ {
@ -602,9 +603,24 @@ namespace CtrEditor
var panelEdicion = _mainWindow.PanelEdicion; var panelEdicion = _mainWindow.PanelEdicion;
if (panelEdicion != null && panelEdicion.IsKeyboardFocusWithin) if (panelEdicion != null && panelEdicion.IsKeyboardFocusWithin)
{ {
// Solo forzar actualización si NO es el PLCControl el que tiene el foco
var plcControl = _mainWindow.PLCSim;
if (plcControl != null && plcControl.IsKeyboardFocusWithin)
{
System.Diagnostics.Debug.WriteLine("PLCControl tiene foco - NO forzar actualización");
System.IO.File.AppendAllText("c:\\temp\\debug_focus.txt",
$"{DateTime.Now}: PLCControl tiene foco - NO forzar actualización\n");
// No forzar actualización si el PLCControl tiene el foco
}
else
{
System.Diagnostics.Debug.WriteLine("PanelEdicion tiene foco - forzar actualización");
System.IO.File.AppendAllText("c:\\temp\\debug_focus.txt",
$"{DateTime.Now}: PanelEdicion tiene foco - forzar actualización\n");
UserControlFactory.ForzarActualizacionBindings(panelEdicion.PropertyGrid); UserControlFactory.ForzarActualizacionBindings(panelEdicion.PropertyGrid);
} }
} }
}
foreach (var obj in _selectedObjects.ToList()) foreach (var obj in _selectedObjects.ToList())
{ {

View File

@ -5,8 +5,21 @@
<vm:osBotella /> <vm:osBotella />
</UserControl.DataContext> </UserControl.DataContext>
<Grid>
<Ellipse Height="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}" <Ellipse Height="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}"
Stroke="{Binding ColorButton_oculto}" Fill="Gray" Stroke="{Binding ColorButton_oculto}" Fill="Gray"
Width="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}" StrokeThickness="0.5" /> Width="{Binding Diametro, Converter={StaticResource MeterToPixelConverter}}" StrokeThickness="0.5" />
<!-- ✅ NUEVO: Número del nivel de deslizamiento en el centro -->
<TextBlock Text="{Binding NivelDeslizamiento, StringFormat='{}{0:F0}'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="4"
FontWeight="Bold"
Foreground="White"
Background="Gray"
Padding="1"
Opacity="0.8" />
</Grid>
</UserControl> </UserControl>

View File

@ -93,6 +93,12 @@ namespace CtrEditor.ObjetosSim
[property: Name("En Transporte con Freno")] [property: Name("En Transporte con Freno")]
private bool enTransporteConFreno; private bool enTransporteConFreno;
[ObservableProperty]
[property: Category("Calibración")]
[property: Description("Nivel de deslizamiento/presión (0-9) para calibración de fricción")]
[property: Name("Nivel de Deslizamiento")]
private float nivelDeslizamiento;
[ObservableProperty] [ObservableProperty]
[property: Category("Simulación")] [property: Category("Simulación")]
[property: Description("Masa del objeto en kg")] [property: Description("Masa del objeto en kg")]
@ -174,7 +180,7 @@ namespace CtrEditor.ObjetosSim
// Sistema de colores jerarquizado para diferentes estados // Sistema de colores jerarquizado para diferentes estados
if (SimGeometria.isOnBrakeTransport) if (SimGeometria.isOnBrakeTransport)
ColorButton_oculto = Brushes.Blue; // En transporte con freno (prioridad sobre presión) ColorButton_oculto = Brushes.Blue; // En transporte con freno (prioridad sobre presión)
else if (!SimGeometria.isOnBrakeTransport && SimGeometria.PressureBuildup>1) else if (!SimGeometria.isOnBrakeTransport && SimGeometria.ContactPressure > 0)
ColorButton_oculto = Brushes.Red; // La botella tiene mucha presion ColorButton_oculto = Brushes.Red; // La botella tiene mucha presion
else else
ColorButton_oculto = Brushes.Gray; // 5. Estado libre ColorButton_oculto = Brushes.Gray; // 5. Estado libre
@ -189,6 +195,7 @@ namespace CtrEditor.ObjetosSim
Inercia_desde_simulacion = SimGeometria.Mass; // En BEPU usamos masa en lugar de inercia directa Inercia_desde_simulacion = SimGeometria.Mass; // En BEPU usamos masa en lugar de inercia directa
Porcentaje_Traccion = SimGeometria.OverlapPercentage; Porcentaje_Traccion = SimGeometria.OverlapPercentage;
EnTransporteConFreno = SimGeometria.isOnBrakeTransport; // Actualizar estado de freno EnTransporteConFreno = SimGeometria.isOnBrakeTransport; // Actualizar estado de freno
NivelDeslizamiento = 10-SimGeometria.forceScale*10; // ✅ NUEVO: Mostrar nivel de deslizamiento para calibración
} }
public override void ucLoaded() public override void ucLoaded()

View File

@ -53,6 +53,7 @@ namespace CtrEditor.ObjetosSim
[property: Name("Tag Consenso")] [property: Name("Tag Consenso")]
private string tag_consenso; private string tag_consenso;
[ObservableProperty] [ObservableProperty]
[property: Category("Simulación")] [property: Category("Simulación")]
[property: Description("Estado del consenso")] [property: Description("Estado del consenso")]

View File

@ -2,13 +2,14 @@
using System.Windows.Controls; using System.Windows.Controls;
using CtrEditor.Simulacion; using CtrEditor.Simulacion;
using System.Reflection; using System.Reflection;
using System;
using System.Windows.Data; using System.Windows.Data;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using CtrEditor.ObjetosSim.UserControls; using CtrEditor.ObjetosSim.UserControls;
using System.Collections; using System.Collections;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Input;
using System.Windows.Shapes; using System.Windows.Shapes;
using Xceed.Wpf.Toolkit.PropertyGrid; using Xceed.Wpf.Toolkit.PropertyGrid;
using System.ComponentModel; using System.ComponentModel;
@ -101,7 +102,27 @@ namespace CtrEditor.ObjetosSim
{ {
try try
{ {
// Método 1: Forzar el foco fuera del PropertyGrid para activar LostFocus // Verificar si el PropertyGrid pertenece al PLCControl para no interferir con su edición
var plcControl = FindParentOfType<LibS7Adv.PLCControl>(propertyGrid);
if (plcControl != null)
{
// Debug - log que encontramos el PLCControl
System.Diagnostics.Debug.WriteLine("PLCControl detectado - saltando forzado de foco");
System.IO.File.AppendAllText("c:\\temp\\debug_focus.txt",
$"{DateTime.Now}: PLCControl detectado - saltando forzado de foco\n");
// Si es del PLCControl, solo forzar actualización de bindings sin robar el foco
var plcBindingExpression = BindingOperations.GetBindingExpression(propertyGrid, PropertyGrid.SelectedObjectProperty);
plcBindingExpression?.UpdateSource();
ForzarActualizacionControlesEditores(propertyGrid);
return;
}
// Debug - log que NO es PLCControl
System.Diagnostics.Debug.WriteLine("NO es PLCControl - aplicando forzado de foco normal");
System.IO.File.AppendAllText("c:\\temp\\debug_focus.txt",
$"{DateTime.Now}: NO es PLCControl - aplicando forzado de foco normal\n");
// Método 1: Forzar el foco fuera del PropertyGrid para activar LostFocus (solo para PropertyGrid principal)
if (propertyGrid.IsKeyboardFocusWithin) if (propertyGrid.IsKeyboardFocusWithin)
{ {
// Crear un elemento temporal para robar el foco // Crear un elemento temporal para robar el foco
@ -121,8 +142,8 @@ namespace CtrEditor.ObjetosSim
} }
// Método 2: Forzar la actualización de todos los bindings del PropertyGrid // Método 2: Forzar la actualización de todos los bindings del PropertyGrid
var bindingExpression = BindingOperations.GetBindingExpression(propertyGrid, PropertyGrid.SelectedObjectProperty); var propertyGridBindingExpression = BindingOperations.GetBindingExpression(propertyGrid, PropertyGrid.SelectedObjectProperty);
bindingExpression?.UpdateSource(); propertyGridBindingExpression?.UpdateSource();
// Método 3: Buscar controles de edición activos y forzar su actualización // Método 3: Buscar controles de edición activos y forzar su actualización
ForzarActualizacionControlesEditores(propertyGrid); ForzarActualizacionControlesEditores(propertyGrid);
@ -232,6 +253,25 @@ namespace CtrEditor.ObjetosSim
propertyGrid.SelectedObject = selectedObject; propertyGrid.SelectedObject = selectedObject;
} }
/// <summary>
/// Busca un control padre de un tipo específico en el árbol visual
/// </summary>
/// <typeparam name="T">El tipo de control padre a buscar</typeparam>
/// <param name="child">El control hijo desde donde empezar la búsqueda</param>
/// <returns>El control padre del tipo especificado o null si no se encuentra</returns>
private static T FindParentOfType<T>(DependencyObject child) where T : DependencyObject
{
DependencyObject parent = VisualTreeHelper.GetParent(child);
if (parent == null)
return null;
if (parent is T parentOfType)
return parentOfType;
return FindParentOfType<T>(parent);
}
} }

View File

@ -65,12 +65,16 @@ namespace CtrEditor.Serialization
} }
} }
// Paso 2: Guardar datos de la p<>gina actual // Paso 2: Capturar estado del workspace antes de guardar
_mainViewModel.CaptureWorkspaceState();
// Paso 3: Guardar datos de la página actual
var dataToSerialize = new SimulationData var dataToSerialize = new SimulationData
{ {
ObjetosSimulables = objetosSimulables.Count > 0 ? objetosSimulables : null, ObjetosSimulables = objetosSimulables.Count > 0 ? objetosSimulables : null,
UnitConverter = PixelToMeter.Instance.calc, UnitConverter = PixelToMeter.Instance.calc,
PLC_ConnectionData = _mainViewModel.PLCViewModel, PLC_ConnectionData = _mainViewModel.PLCViewModel,
WorkspaceConfig = _mainViewModel.WorkspaceConfig,
DatosLocalesObjetos = datosLocalesObjetos.Count > 0 ? datosLocalesObjetos : null, DatosLocalesObjetos = datosLocalesObjetos.Count > 0 ? datosLocalesObjetos : null,
ImageDataDictionary = _mainViewModel._imageDataDictionary.Count > 0 ? _mainViewModel._imageDataDictionary : null ImageDataDictionary = _mainViewModel._imageDataDictionary.Count > 0 ? _mainViewModel._imageDataDictionary : null
}; };
@ -114,9 +118,16 @@ namespace CtrEditor.Serialization
// Paso 1: Cargar objetos globales // Paso 1: Cargar objetos globales
LoadAllPagesState(settings); LoadAllPagesState(settings);
// Paso 2: Cargar datos de la p<EFBFBD>gina actual // Paso 2: Cargar datos de la página actual
var simulationData = LoadCurrentPageState(selectedImage, settings); var simulationData = LoadCurrentPageState(selectedImage, settings);
// Paso 2.1: Restaurar configuración del workspace
// Usar un dispatcher para asegurar que la UI esté lista
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
_mainViewModel.RestoreWorkspaceState();
}), System.Windows.Threading.DispatcherPriority.Background);
// Paso 3: Crear snapshots de los objetos globales // Paso 3: Crear snapshots de los objetos globales
// Nota: No filtramos por Enable_Local_Data aqu<71> ya que se establecer<65> despu<70>s // Nota: No filtramos por Enable_Local_Data aqu<71> ya que se establecer<65> despu<70>s
foreach (var obj in _mainViewModel.ObjetosSimulables) foreach (var obj in _mainViewModel.ObjetosSimulables)
@ -188,6 +199,12 @@ namespace CtrEditor.Serialization
else else
_mainViewModel.PLCViewModel = new PLCViewModel(); _mainViewModel.PLCViewModel = new PLCViewModel();
// Cargar configuración del workspace
if (simulationData.WorkspaceConfig is not null)
_mainViewModel.WorkspaceConfig = simulationData.WorkspaceConfig;
else
_mainViewModel.InitializeDefaultWorkspaceConfig();
// Solo sobrescribir el UnitConverter si existe uno guardado // Solo sobrescribir el UnitConverter si existe uno guardado
if (simulationData.UnitConverter != null) if (simulationData.UnitConverter != null)
PixelToMeter.Instance.calc = simulationData.UnitConverter; PixelToMeter.Instance.calc = simulationData.UnitConverter;

View File

@ -87,14 +87,16 @@ namespace CtrEditor.Simulacion
public bool ConfigureContactManifold<TManifold>(int workerIndex, CollidablePair pair, ref TManifold manifold, out PairMaterialProperties pairMaterial) where TManifold : unmanaged, IContactManifold<TManifold> public bool ConfigureContactManifold<TManifold>(int workerIndex, CollidablePair pair, ref TManifold manifold, out PairMaterialProperties pairMaterial) where TManifold : unmanaged, IContactManifold<TManifold>
{ {
// ✅ CONFIGURACIÓN BÁSICA de materiales físicos // ✅ CONFIGURACIÓN BÁSICA de materiales físicos con valores rígidos pero estables
pairMaterial = new PairMaterialProperties pairMaterial = new PairMaterialProperties
{ {
FrictionCoefficient = 0.3f, FrictionCoefficient = 0.3f,
MaximumRecoveryVelocity = 1f, MaximumRecoveryVelocity = 0.8f, // ✅ AJUSTADO: Velocidad de separación moderada
SpringSettings = new SpringSettings(80, 6) SpringSettings = new SpringSettings(120, 8) // ✅ AJUSTADO: Más rígido pero bien amortiguado
}; };
// ✅ ELIMINADO: Sistema de limitación de fuerzas forceScale - reemplazado por intervención post-solver
if (_simulationManager != null) if (_simulationManager != null)
{ {
var botellaA = GetBotellaFromCollidable(pair.A); var botellaA = GetBotellaFromCollidable(pair.A);
@ -124,9 +126,7 @@ namespace CtrEditor.Simulacion
// ✅ CONTACTO BOTELLA-TRANSPORTE: USAR FRICCIÓN Y VELOCIDAD CINEMÁTICA // ✅ CONTACTO BOTELLA-TRANSPORTE: USAR FRICCIÓN Y VELOCIDAD CINEMÁTICA
if (botella != null && (transportA != null || transportB != null)) if (botella != null && (transportA != null || transportB != null))
{ {
// La velocidad del cuerpo cinemático del transporte se establece directamente // ✅ ELIMINADO: botella.forceScale = forceScale;
// en la clase simTransporte. El motor de físicas se encarga del resto.
// ✅ NUEVO: Lógica de control de presión/flotación // ✅ NUEVO: Lógica de control de presión/flotación
botella.IsTouchingTransport = true; botella.IsTouchingTransport = true;
var botellaCollidable = GetBotellaFromCollidable(pair.A) != null ? pair.A : pair.B; var botellaCollidable = GetBotellaFromCollidable(pair.A) != null ? pair.A : pair.B;
@ -135,16 +135,11 @@ namespace CtrEditor.Simulacion
var body = _simulationManager.simulation.Bodies.GetBodyReference(botellaCollidable.BodyHandle); var body = _simulationManager.simulation.Bodies.GetBodyReference(botellaCollidable.BodyHandle);
botella.LastTransportCollisionZ = body.Pose.Position.Z; botella.LastTransportCollisionZ = body.Pose.Position.Z;
} }
// Decrementamos la presión acumulada al tocar un transporte. Se reduce más rápido de lo que aumenta.
botella.PressureBuildup = Math.Max(0, botella.PressureBuildup - 0.1f);
// ✅ Fricción ajustada para un arrastre firme pero no excesivo.
var transport = transportA ?? transportB; var transport = transportA ?? transportB;
if (transport.isBrake) if (transport.isBrake)
{ {
botella.PressureBuildup = 0;
// ✅ NUEVO: Centrar la botella en el transporte de frenado la primera vez que entra. // ✅ NUEVO: Centrar la botella en el transporte de frenado la primera vez que entra.
// if (!botella.isOnBrakeTransport)
{ {
if (botella.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botella.BodyHandle) && if (botella.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botella.BodyHandle) &&
transport.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(transport.BodyHandle)) transport.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(transport.BodyHandle))
@ -172,35 +167,24 @@ namespace CtrEditor.Simulacion
} }
botella.isOnBrakeTransport = true; botella.isOnBrakeTransport = true;
pairMaterial.FrictionCoefficient = 14f; pairMaterial.FrictionCoefficient = 8f;
pairMaterial.MaximumRecoveryVelocity = 1.0f; pairMaterial.MaximumRecoveryVelocity = 1f; // ✅ REMOVIDO: * forceScale
pairMaterial.SpringSettings = new SpringSettings(80, 8); pairMaterial.SpringSettings = new SpringSettings(30, 1f); // ✅ REMOVIDO: * forceScale
}
else
{
if (botella.PressureBuildup > minPressureToLostFriction)
{
botella.isOnBrakeTransport = false;
pairMaterial.FrictionCoefficient = 0.1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
} }
else else
{ {
botella.isOnBrakeTransport = false; botella.isOnBrakeTransport = false;
pairMaterial.FrictionCoefficient = 1.2f; // ✅ NUEVO: Usar fricción dinámica basada en deslizamiento
pairMaterial.MaximumRecoveryVelocity = 1.0f; pairMaterial.FrictionCoefficient = CalculateTransportFriction(botella, transport, botellaCollidable);
pairMaterial.SpringSettings = new SpringSettings(80, 1); pairMaterial.MaximumRecoveryVelocity = 1f; // ✅ REMOVIDO: * forceScale
} pairMaterial.SpringSettings = new SpringSettings(30, 1f); // ✅ REMOVIDO: * forceScale
} }
} }
// ✅ CONTACTO BOTELLA-CURVA: USAR FRICCIÓN Y VELOCIDAD CINEMÁTICA // ✅ CONTACTO BOTELLA-CURVA: USAR FRICCIÓN Y VELOCIDAD CINEMÁTICA
else if (botella != null && (curveA != null || curveB != null)) else if (botella != null && (curveA != null || curveB != null))
{ {
// El motor de físicas usará la velocidad angular del cuerpo cinemático de la curva // ✅ ELIMINADO: botella.forceScale = forceScale;
// para calcular las fuerzas de fricción y arrastrar la botella.
// ✅ NUEVO: Lógica de control de presión/flotación // ✅ NUEVO: Lógica de control de presión/flotación
botella.IsTouchingTransport = true; botella.IsTouchingTransport = true;
botella.isOnBrakeTransport = false; botella.isOnBrakeTransport = false;
@ -210,59 +194,42 @@ namespace CtrEditor.Simulacion
var body = _simulationManager.simulation.Bodies.GetBodyReference(botellaCollidable.BodyHandle); var body = _simulationManager.simulation.Bodies.GetBodyReference(botellaCollidable.BodyHandle);
botella.LastTransportCollisionZ = body.Pose.Position.Z; botella.LastTransportCollisionZ = body.Pose.Position.Z;
} }
// Decrementamos la presión acumulada al tocar una curva.
botella.PressureBuildup = Math.Max(0, botella.PressureBuildup - 0.1f);
// ✅ Fricción ajustada para un arrastre firme pero no excesivo. // ✅ NUEVO: Usar fricción dinámica basada en deslizamiento
if (botella.PressureBuildup > minPressureToLostFriction) var curve = curveA ?? curveB;
{ pairMaterial.FrictionCoefficient = CalculateCurveFriction(botella, curve, botellaCollidable);
pairMaterial.FrictionCoefficient = 0.1f; pairMaterial.MaximumRecoveryVelocity = 1f; // ✅ REMOVIDO: * forceScale
pairMaterial.MaximumRecoveryVelocity = 1.0f; pairMaterial.SpringSettings = new SpringSettings(30, 1f); // ✅ REMOVIDO: * forceScale
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}else
{
pairMaterial.FrictionCoefficient = 1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}
} }
// ✅ CONTACTO BOTELLA-GUÍA: Configuración específica de fricción // ✅ CONTACTO BOTELLA-GUÍA: Configuración específica de fricción
else if (botella != null && (GetGuiaFromCollidable(pair.A) != null || GetGuiaFromCollidable(pair.B) != null)) else if (botella != null && (GetGuiaFromCollidable(pair.A) != null || GetGuiaFromCollidable(pair.B) != null))
{ {
// Fricción baja para las guías, deben deslizar, no arrastrar. // Fricción baja para las guías, deben deslizar, no arrastrar.
pairMaterial.FrictionCoefficient = 0.01f; pairMaterial.FrictionCoefficient = 0.01f;
pairMaterial.MaximumRecoveryVelocity = 2f; pairMaterial.MaximumRecoveryVelocity = 0.95f; // ✅ REMOVIDO: * forceScale
pairMaterial.SpringSettings = new SpringSettings(20, 1); pairMaterial.SpringSettings = new SpringSettings(40, 1f); // ✅ REMOVIDO: * forceScale
} }
// ✅ NUEVO: CONTACTO BOTELLA-BOTELLA: Configuración más suave para evitar el comportamiento "pegajoso". // ✅ NUEVO: CONTACTO BOTELLA-BOTELLA: Lógica de presión proactiva basada en la penetración.
else if (botellaA != null && botellaB != null) else if (botellaA != null && botellaB != null)
{ {
if (botella.isOnBrakeTransport) // ✅ NUEVO: Usar método especializado para contactos botella-botella
{ CalculateBottlePression(ref manifold, botellaA, botellaB);
pairMaterial.FrictionCoefficient = 2.0f; // Un poco menos de fricción.
pairMaterial.MaximumRecoveryVelocity = 1.5f; // Menos rebote.
pairMaterial.SpringSettings = new SpringSettings(80, 1f); // Muelle MUCHO más suave y críticamente amortiguado.
}
else
{
pairMaterial.FrictionCoefficient = 0.01f; // Un poco menos de fricción.
pairMaterial.MaximumRecoveryVelocity = 1.5f; // Menos rebote.
pairMaterial.SpringSettings = new SpringSettings(20, 0.5f); // Muelle MUCHO más suave y críticamente amortiguado.
}
// Si una botella tiene presion esta presion es transmitia a el resto para que el resto pierda el coeficiente de friccion sobre los transportes
if (botellaA.PressureBuildup > 0 && botellaB.PressureBuildup == 0)
botellaB.PressureBuildup = botellaA.PressureBuildup;
if (botellaB.PressureBuildup > 0 && botellaA.PressureBuildup == 0)
botellaA.PressureBuildup = botellaB.PressureBuildup;
// ✅ NUEVO: Limitar fuerzas basadas en profundidad de penetración
//var maxPenetrationDepth = GetMaxPenetrationDepth(ref manifold);
// Fuerzas normales para contactos superficiales - botellas rígidas
pairMaterial.FrictionCoefficient = 0.02f;
pairMaterial.MaximumRecoveryVelocity = 1f; // ✅ REMOVIDO: * forceScale
pairMaterial.SpringSettings = new SpringSettings(40, 10f); // ✅ REMOVIDO: * forceScale
} }
// Ajustes básicos para otras botellas // Ajustes básicos para otras botellas
else if (botella != null) else if (botella != null)
{ {
System.Diagnostics.Debug.WriteLine($"[ConfigureContactManifold] ERROR: contacto no resuelto!");
// Fricción moderada para colisiones entre botellas. // Fricción moderada para colisiones entre botellas.
pairMaterial.FrictionCoefficient = 0.3f; pairMaterial.FrictionCoefficient = 0f;
pairMaterial.MaximumRecoveryVelocity = 1f; pairMaterial.MaximumRecoveryVelocity = 1f; // ✅ REMOVIDO: * forceScale
pairMaterial.SpringSettings = new SpringSettings(80, 1); pairMaterial.SpringSettings = new SpringSettings(80, 1); // ✅ REMOVIDO: * forceScale
} }
} }
@ -348,9 +315,174 @@ namespace CtrEditor.Simulacion
/// <summary> /// <summary>
/// ✅ NUEVO MÉTODO: Aplicar fuerzas de frenado directamente en el manifold /// ✅ NUEVO: Calcula la fricción dinámica para contactos botella-transporte basada en el deslizamiento relativo
/// </summary> /// </summary>
private float CalculateTransportFriction(simBotella botella, simTransporte transport, CollidableReference botellaCollidable)
{
try
{
if (botella?.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botella.BodyHandle) &&
transport?.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(transport.BodyHandle))
{
var bottleBody = _simulationManager.simulation.Bodies.GetBodyReference(botella.BodyHandle);
var transportBody = _simulationManager.simulation.Bodies.GetBodyReference(transport.BodyHandle);
var bottleVelocity = bottleBody.Velocity.Linear;
var transportVelocity = transport.CalcularVelocidadCinematica(); // La velocidad cinemática se actualiza en OnSubstepStarted
var slipVelocity = bottleVelocity - transportVelocity;
slipVelocity.Z = 0; // Ignoramos el deslizamiento vertical
var slipSpeed = slipVelocity.Length();
float slipSpeedThreshold = transportVelocity.Length()*0.85f; // m/s - umbral para cambiar de fricción estática a dinámica
const float heatupRate = 1f; // Qué tan rápido sube HighSlippery
const float cooldownRate = 1f; // Qué tan rápido baja HighSlippery
const float staticFriction = 0.50f;
const float dynamicFriction = 0.30f;
// Sistema de heatup/cooldown para HighSlippery
if (slipSpeed > slipSpeedThreshold) // && botella.ContactPressure > 0
botella.HighSlippery = Math.Min(9f, botella.HighSlippery + heatupRate);
else
botella.HighSlippery = Math.Max(0f, botella.HighSlippery - cooldownRate);
// Calcular fricción basada en HighSlippery
// HighSlippery > 3 = fricción dinámica, <= 3 = fricción estática
if (botella.ContactPressure > 0)
return dynamicFriction / 10;
else
{
if (botella.HighSlippery > 1f)
return dynamicFriction;
else
return staticFriction;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[CalculateTransportFriction] Error: {ex.Message}");
}
// Valor por defecto si hay algún error
return 1.0f;
}
/// <summary>
/// ✅ NUEVO: Calcula la fricción dinámica para contactos botella-curva basada en el deslizamiento relativo
/// </summary>
private float CalculateCurveFriction(simBotella botella, simCurve curve, CollidableReference botellaCollidable)
{
try
{
if (botella?.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botella.BodyHandle) &&
curve?.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(curve.BodyHandle))
{
var bottleBody = _simulationManager.simulation.Bodies.GetBodyReference(botella.BodyHandle);
var curveBody = _simulationManager.simulation.Bodies.GetBodyReference(curve.BodyHandle);
// Aproximamos la velocidad de la superficie de la curva en la posición de la botella.
// v = ω x r
var r = bottleBody.Pose.Position - curveBody.Pose.Position;
var curveVelocityAtPoint = Vector3.Cross(curve.CalcularVelocidadCinematica(), r);
var slipVelocity = bottleBody.Velocity.Linear - curveVelocityAtPoint;
slipVelocity.Z = 0; // Ignoramos el deslizamiento vertical
var slipSpeed = slipVelocity.Length();
float slipSpeedThreshold = curveVelocityAtPoint.Length() * 0.85f; // m/s
const float heatupRate = 1f; // Qué tan rápido sube HighSlippery en curvas
const float cooldownRate = 1f; // Qué tan rápido baja HighSlippery en curvas
const float staticFriction = 0.50f;
const float dynamicFriction = 0.30f;
// Sistema de heatup/cooldown para HighSlippery
if (slipSpeed > slipSpeedThreshold) // && botella.ContactPressure > 0
botella.HighSlippery = Math.Min(9f, botella.HighSlippery + heatupRate);
else
botella.HighSlippery = Math.Max(0f, botella.HighSlippery - cooldownRate);
// Calcular fricción basada en HighSlippery
// HighSlippery > 3 = fricción dinámica, <= 3 = fricción estática
if (botella.ContactPressure > 0)
return dynamicFriction / 10;
else
{
if (botella.HighSlippery > 1f)
return dynamicFriction;
else
return staticFriction;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[CalculateCurveFriction] Error: {ex.Message}");
}
// Valor por defecto si hay algún error
return 0.8f;
}
/// <summary>
/// ✅ NUEVO: Calcula los parámetros de material para contactos botella-botella basado en la profundidad de penetración
/// </summary>
private void CalculateBottlePression<TManifold>(ref TManifold manifold, simBotella botellaA, simBotella botellaB) where TManifold : unmanaged, IContactManifold<TManifold>
{
var pairMaterial = new PairMaterialProperties();
try
{
const float penetrationDepthThreshold = 0.001f; // Umbral de penetración en metros para detectar alta presión.
// Iteramos sobre todos los puntos de contacto en el manifold.
for (int i = 0; i < manifold.Count; ++i)
{
// Si la profundidad de la penetración supera nuestro umbral,
// consideramos que este contacto está bajo alta presión.
if (manifold.GetDepth(i) > penetrationDepthThreshold)
{
botellaA.ContactPressure = Math.Max(botellaA.ContactPressure, manifold.GetDepth(i) - penetrationDepthThreshold);
botellaB.ContactPressure = Math.Max(botellaB.ContactPressure, manifold.GetDepth(i) - penetrationDepthThreshold);
// break; // Un punto de alta presión es suficiente.
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[CalculateBottleToBottleMaterial] Error: {ex.Message}");
// Valores por defecto seguros
}
}
/// <summary>
/// ✅ NUEVA FUNCIÓN: Obtiene la profundidad máxima de penetración en un manifold
/// </summary>
private float GetMaxPenetrationDepth<TManifold>(ref TManifold manifold) where TManifold : unmanaged, IContactManifold<TManifold>
{
try
{
float maxDepth = 0f;
for (int i = 0; i < manifold.Count; ++i)
{
var depth = manifold.GetDepth(i);
if (depth > maxDepth)
{
maxDepth = depth;
}
}
return maxDepth;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[GetMaxPenetrationDepth] Error: {ex.Message}");
return 0f;
}
}
public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB, ref ConvexContactManifold manifold) public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB, ref ConvexContactManifold manifold)
@ -396,7 +528,8 @@ namespace CtrEditor.Simulacion
{ {
} }
// ✅ REESCRITO COMPLETAMENTE: para corregir errores de compilación y aplicar la lógica de amortiguamiento de forma segura. // ✅ SIMPLIFICADO: IntegrateVelocity ahora solo aplica física básica
// El control de velocidades Z se maneja en OnSubstepEnded (post-solver)
public void IntegrateVelocity(Vector<int> bodyIndices, Vector3Wide position, QuaternionWide orientation, BodyInertiaWide localInertia, Vector<int> integrationMask, int workerIndex, Vector<float> dt, ref BodyVelocityWide velocity) public void IntegrateVelocity(Vector<int> bodyIndices, Vector3Wide position, QuaternionWide orientation, BodyInertiaWide localInertia, Vector<int> integrationMask, int workerIndex, Vector<float> dt, ref BodyVelocityWide velocity)
{ {
// 1. APLICAR GRAVEDAD Y AMORTIGUAMIENTO ESTÁNDAR (VECTORIZADO) // 1. APLICAR GRAVEDAD Y AMORTIGUAMIENTO ESTÁNDAR (VECTORIZADO)
@ -411,7 +544,7 @@ namespace CtrEditor.Simulacion
velocity.Linear *= linearDampingFactor; velocity.Linear *= linearDampingFactor;
velocity.Angular *= angularDampingFactor; velocity.Angular *= angularDampingFactor;
// 2. LÓGICA PERSONALIZADA PARA BOTELLAS (ESCALAR) // 2. LÓGICA PERSONALIZADA MÍNIMA PARA BOTELLAS (ESCALAR)
const float bottleExtraZAngularDamping = 0.8f; const float bottleExtraZAngularDamping = 0.8f;
var bottleZAngularDampingFactor = MathF.Pow(MathHelper.Clamp(1 - bottleExtraZAngularDamping, 0, 1), dt[0]); var bottleZAngularDampingFactor = MathF.Pow(MathHelper.Clamp(1 - bottleExtraZAngularDamping, 0, 1), dt[0]);
@ -427,13 +560,27 @@ namespace CtrEditor.Simulacion
var handle = _simulationManager.simulation.Bodies.ActiveSet.IndexToHandle[bodyIndex]; var handle = _simulationManager.simulation.Bodies.ActiveSet.IndexToHandle[bodyIndex];
if (_simulationManager.CollidableData.TryGetValue(handle.Value, out simBase baseObj) && baseObj is simBotella) if (_simulationManager.CollidableData.TryGetValue(handle.Value, out simBase baseObj) && baseObj is simBotella)
{ {
// B. DAMPING ANGULAR EXTRA EN EJE Z // EXTRAER VELOCIDADES DEL PAQUETE A ARRAYS
// Para modificar un solo carril de un Vector<T>, la forma más segura es extraer var angularX = new float[Vector<float>.Count];
// los valores a un array, modificar el índice y crear un nuevo Vector<T>. var angularY = new float[Vector<float>.Count];
var zVel = new float[Vector<float>.Count]; var angularZ = new float[Vector<float>.Count];
velocity.Angular.Z.CopyTo(zVel, 0);
zVel[i] *= bottleZAngularDampingFactor; velocity.Angular.X.CopyTo(angularX, 0);
velocity.Angular.Z = new Vector<float>(zVel); velocity.Angular.Y.CopyTo(angularY, 0);
velocity.Angular.Z.CopyTo(angularZ, 0);
// CONTROL ANGULAR BÁSICO PARA BOTELLAS
// Anti-vuelco: Anular rotaciones que tumben la botella
angularX[i] = 0;
angularY[i] = 0;
// Damping angular extra en Z
angularZ[i] *= bottleZAngularDampingFactor;
// RECONSTRUIR LOS VECTORES 'WIDE' CON LOS VALORES MODIFICADOS
velocity.Angular.X = new Vector<float>(angularX);
velocity.Angular.Y = new Vector<float>(angularY);
velocity.Angular.Z = new Vector<float>(angularZ);
} }
} }
} }
@ -487,6 +634,8 @@ namespace CtrEditor.Simulacion
public Dictionary<int, simBase> CollidableData = new Dictionary<int, simBase>(); public Dictionary<int, simBase> CollidableData = new Dictionary<int, simBase>();
public float GlobalTime { get; private set; } public float GlobalTime { get; private set; }
// ✅ ELIMINADO: Sistema de limitación de fuerzas - reemplazado por intervención post-solver
/// <summary> /// <summary>
/// Obtiene el objeto simBase correspondiente a un BodyHandle /// Obtiene el objeto simBase correspondiente a un BodyHandle
/// </summary> /// </summary>
@ -511,6 +660,10 @@ namespace CtrEditor.Simulacion
// ✅ NUEVOS - gestión automática de clasificación // ✅ NUEVOS - gestión automática de clasificación
private void RegisterObjectHandle(simBase obj) private void RegisterObjectHandle(simBase obj)
{ {
if (obj != null && obj.BodyHandle.Value >= 0)
{
CollidableData[obj.BodyHandle.Value] = obj;
}
switch (obj) switch (obj)
{ {
case simBotella bottle: case simBotella bottle:
@ -540,6 +693,10 @@ namespace CtrEditor.Simulacion
private void UnregisterObjectHandle(simBase obj) private void UnregisterObjectHandle(simBase obj)
{ {
if (obj != null && obj.BodyHandle.Value >= 0)
{
CollidableData.Remove(obj.BodyHandle.Value);
}
switch (obj) switch (obj)
{ {
case simBotella bottle: case simBotella bottle:
@ -624,8 +781,8 @@ namespace CtrEditor.Simulacion
// ✅ MODIFICADO: Pasar referencia de this al PoseIntegratorCallbacks // ✅ MODIFICADO: Pasar referencia de this al PoseIntegratorCallbacks
var poseIntegratorCallbacks = new PoseIntegratorCallbacks( var poseIntegratorCallbacks = new PoseIntegratorCallbacks(
gravity: new Vector3(0, 0, -9.81f), // Gravedad en Z gravity: new Vector3(0, 0, -9.81f), // Gravedad en Z
linearDamping: 0.999f, // Amortiguamiento lineal suave linearDamping: 0.03f, // 0.999f, // Amortiguamiento lineal suave
angularDamping: 0.995f, // Amortiguamiento angular más fuerte para detener rotaciones angularDamping: 0.03f, // 0.995f, // Amortiguamiento angular más fuerte para detener rotaciones
simulationManager: this // ✅ NUEVA REFERENCIA simulationManager: this // ✅ NUEVA REFERENCIA
); );
@ -656,10 +813,71 @@ namespace CtrEditor.Simulacion
/// <summary> /// <summary>
/// ✅ NUEVO: Se ejecuta al final de cada sub-paso de la simulación, pero antes de la integración de la pose. /// ✅ NUEVO: Se ejecuta al final de cada sub-paso de la simulación, pero antes de la integración de la pose.
/// Pone a cero las velocidades de los cuerpos cinemáticos para evitar que el PoseIntegrator mueva su posición. /// Actúa como "fusible de energía" para eliminar velocidades Z excesivas y mantener objetos cinemáticos estáticos.
/// </summary> /// </summary>
private void OnSubstepEnded(int substepIndex) private void OnSubstepEnded(int substepIndex)
{ {
// 1. INTERVENCIÓN POST-SOLVER: Control de velocidades Z para botellas
// Esto actúa como un "fusible de energía" que elimina velocidades Z excesivas
// después de que el solver haya hecho su trabajo
foreach (var botella in Cuerpos.OfType<simBotella>())
{
try
{
if (botella?.BodyHandle.Value >= 0 && simulation?.Bodies?.BodyExists(botella.BodyHandle) == true)
{
var body = simulation.Bodies.GetBodyReference(botella.BodyHandle);
var velocity = body.Velocity;
var pose = body.Pose;
// CONFIGURACIÓN DE LÍMITES
const float maxUpwardVelocity = 0f; // Velocidad Z máxima hacia arriba (m/s)
const float maxXYVelocity = 1.0f; // Límite de velocidad en el plano XY (m/s)
// LIMITAR VELOCIDADES Z EXCESIVAS
if (velocity.Linear.Z > maxUpwardVelocity)
{
// Cancelar velocidades hacia arriba excesivas (anti-elevación)
velocity.Linear.Z = 0;
pose.Position.Z = botella.Radius + simBase.zPos_Transporte + simBase.zAltura_Transporte;
body.Pose = pose; // Se actualiza la pose completa
}
// LIMITAR VELOCIDAD MÁXIMA EN PLANO XY
var velocityXY = new Vector2(velocity.Linear.X, velocity.Linear.Y);
if (velocityXY.LengthSquared() > maxXYVelocity * maxXYVelocity)
{
var speedXY = velocityXY.Length();
var scale = maxXYVelocity / speedXY;
velocity.Linear.X *= scale;
velocity.Linear.Y *= scale;
}
// Aplicar siempre la velocidad, ya que puede haber sido modificada en Z o XY
body.Velocity = velocity;
//// CONTROL DE POSICIÓN EXTREMA
//// Si la botella está muy por encima del nivel normal, aplicar corrección
//var maxAllowedZ = simBase.zPos_Transporte + simBase.zAltura_Transporte + botella.Radius * 2;
//if (position.Z > maxAllowedZ)
//{
// // Aplicar velocidad correctiva hacia abajo
// var excessHeight = position.Z - maxAllowedZ;
// var correctionVelocity = -Math.Min(excessHeight * 5f, 0.5f); // Máximo 0.5 m/s hacia abajo
// velocity.Linear.Z = correctionVelocity;
// body.Velocity = velocity;
//}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[OnSubstepEnded] Error procesando botella {botella?.BodyHandle}: {ex.Message}");
}
}
// 2. LIMPIAR VELOCIDADES DE OBJETOS CINEMÁTICOS
// Mantener transportes y curvas estáticos después del solver
foreach (var transport in Cuerpos.OfType<simTransporte>()) foreach (var transport in Cuerpos.OfType<simTransporte>())
{ {
if (transport.BodyHandle.Value >= 0 && simulation.Bodies.BodyExists(transport.BodyHandle)) if (transport.BodyHandle.Value >= 0 && simulation.Bodies.BodyExists(transport.BodyHandle))
@ -799,8 +1017,13 @@ namespace CtrEditor.Simulacion
foreach (var botella in Cuerpos.OfType<simBotella>()) foreach (var botella in Cuerpos.OfType<simBotella>())
{ {
botella.IsTouchingTransport = false; botella.IsTouchingTransport = false;
botella.ContactPressure = 0f;
} }
// ✅ ELIMINADO: Reset del mapa de densidad de contactos ya no necesario
// ✅ ELIMINADO: ApplyZForceLimitation - ahora usamos intervención post-solver en OnSubstepEnded
// ✅ CONSERVAR - validación de deltaTime // ✅ CONSERVAR - validación de deltaTime
var currentTime = stopwatch.Elapsed.TotalMilliseconds; var currentTime = stopwatch.Elapsed.TotalMilliseconds;
var deltaTime = (float)((currentTime - stopwatch_last) / 1000.0); var deltaTime = (float)((currentTime - stopwatch_last) / 1000.0);
@ -857,27 +1080,26 @@ namespace CtrEditor.Simulacion
// ✅ CONSERVAR - limitación de rotación y mantener botellas despiertas //// ✅ CONSERVAR - limitación de rotación y mantener botellas despiertas
foreach (var cuerpo in Cuerpos.OfType<simBotella>().ToList()) //foreach (var cuerpo in Cuerpos.OfType<simBotella>().ToList())
{ //{
try // try
{ // {
if (simulation.Bodies.BodyExists(cuerpo.BodyHandle)) // if (simulation.Bodies.BodyExists(cuerpo.BodyHandle))
{ // {
// cuerpo.LimitRotationToXYPlane(); // // cuerpo.LimitRotationToXYPlane();
// simulation.Awakener.AwakenBody(cuerpo.BodyHandle); // // simulation.Awakener.AwakenBody(cuerpo.BodyHandle);
} // }
} // }
catch (Exception ex) // catch (Exception ex)
{ // {
// Error limiting rotation for bottle - continue // // Error limiting rotation for bottle - continue
} // }
} //}
// ✅ CONSERVAR - sistemas que funcionan bien // ✅ CONSERVAR - sistemas que funcionan bien
ProcessBarreraContacts(); ProcessBarreraContacts();
ProcessDescarteContacts(); ProcessDescarteContacts();
ProcessPressureSystem();
ProcessCleanupSystem(); ProcessCleanupSystem();
// ✅ SIMPLIFICAR - limpiar solo contactos que usamos // ✅ SIMPLIFICAR - limpiar solo contactos que usamos
@ -904,59 +1126,7 @@ namespace CtrEditor.Simulacion
/// <summary>
/// ✅ NUEVO: Sistema para controlar la "presión" y evitar que las botellas floten
/// Si una botella no toca un transporte por varios frames, se reestablece su altura.
/// </summary>
private void ProcessPressureSystem()
{
try
{
var botellas = Cuerpos.OfType<simBotella>().ToList();
foreach (var botella in botellas)
{
try
{
if (botella == null || !simulation.Bodies.BodyExists(botella.BodyHandle))
continue;
if (!botella.IsTouchingTransport && botella.LastTransportCollisionZ.HasValue)
{
var body = simulation.Bodies.GetBodyReference(botella.BodyHandle);
var currentZ = body.Pose.Position.Z;
// ✅ NUEVA CONDICIÓN: Solo actuar si la botella ha subido
if (currentZ > botella.LastTransportCollisionZ.Value)
{
// Incrementar el contador de presión.
botella.PressureBuildup++;
// Si se acumula suficiente presión (más de 2 frames sin contacto y subiendo),
// es un indicio de que está flotando. La reestablecemos a su última Z conocida.
if (botella.PressureBuildup > 2)
{
var pose = body.Pose;
pose.Position.Z = botella.LastTransportCollisionZ.Value;
body.Pose = pose;
// Opcional: resetear el contador para no aplicarlo constantemente en cada frame
// botella.PressureBuildup = 0;
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ProcessPressureSystem] Error processing bottle {botella?.BodyHandle}: {ex.Message}");
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ProcessPressureSystem] Critical error: {ex.Message}");
}
}
/// <summary> /// <summary>
/// ✅ NUEVO: Marca un objeto para eliminación diferida (más seguro) /// ✅ NUEVO: Marca un objeto para eliminación diferida (más seguro)
@ -1414,6 +1584,11 @@ namespace CtrEditor.Simulacion
return lineStart + lineDirection * projectionLength; return lineStart + lineDirection * projectionLength;
} }
// ✅ ELIMINADO: Funciones del sistema de limitación de fuerzas forceScale
// GetContactRegion, RegisterContactAndGetForceScale, GetContactPosition
// ✅ ELIMINADO: ApplyZForceLimitation - reemplazado por intervención post-solver en OnSubstepEnded
public void Dispose() public void Dispose()
{ {
Clear(); Clear();

View File

@ -10,6 +10,7 @@ using System.Linq;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using System.Windows; using System.Windows;
using System.Windows.Media.Imaging;
namespace CtrEditor.Simulacion namespace CtrEditor.Simulacion
{ {
@ -80,6 +81,9 @@ namespace CtrEditor.Simulacion
// ✅ NUEVO: Diccionario para gestionar animaciones activas // ✅ NUEVO: Diccionario para gestionar animaciones activas
private Dictionary<simBase, Storyboard> activeAnimations; private Dictionary<simBase, Storyboard> activeAnimations;
// ✅ NUEVO: Referencia al modelo visual de la imagen de fondo
private ModelVisual3D? _backgroundImageVisual;
// ✅ CORREGIDO: Flag de debug para mostrar triángulos individuales de curvas (true temporalmente para verificar) // ✅ CORREGIDO: Flag de debug para mostrar triángulos individuales de curvas (true temporalmente para verificar)
public static bool DebugShowIndividualTriangles { get; set; } = false; public static bool DebugShowIndividualTriangles { get; set; } = false;
@ -847,6 +851,9 @@ namespace CtrEditor.Simulacion
} }
simBaseToModelMap.Clear(); simBaseToModelMap.Clear();
lastKnownDimensions.Clear(); lastKnownDimensions.Clear();
// ✅ NUEVO: También limpiar la imagen de fondo
RemoveBackgroundImage();
} }
/// <summary> /// <summary>
@ -1578,6 +1585,117 @@ namespace CtrEditor.Simulacion
System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ ERROR recreando geometría: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ ERROR recreando geometría: {ex.Message}");
} }
} }
/// <summary>
/// ✅ NUEVO: Agrega la imagen de fondo como un plano 2D estático en el viewport 3D
/// con la misma escala que el canvas WPF usando las convenciones WPF-BEPU correctas
///
/// Esta funcionalidad permite tener coherencia visual entre el canvas 2D WPF y la visualización 3D:
/// - La imagen se posiciona en el plano XY con Z=0
/// - Usa el sistema de conversión PixelToMeter para mantener la escala correcta
/// - Aplica las conversiones WPF-BEPU: WPF (x,y) -> BEPU (x,-y)
/// - Se aplica como textura difusa en un rectángulo plano
/// - Respeta las convenciones: WPF Y hacia abajo, BEPU Y hacia arriba
///
/// Ejemplo de uso:
/// SetBackgroundImage(@"C:\imagen.png", 1920, 1080);
/// </summary>
/// <param name="imagePath">Ruta absoluta de la imagen de fondo</param>
/// <param name="canvasWidth">Ancho del canvas WPF en píxeles</param>
/// <param name="canvasHeight">Alto del canvas WPF en píxeles</param>
public void SetBackgroundImage(string imagePath, double canvasWidth, double canvasHeight)
{
try
{
// Remover imagen de fondo anterior si existe
RemoveBackgroundImage();
// Convertir dimensiones de píxeles a metros usando el sistema de conversión
float widthInMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasWidth);
float heightInMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasHeight);
// Crear la geometría del plano
var meshBuilder = new MeshBuilder();
// ✅ CORREGIDO: Usar convenciones WPF-BEPU correctas
// En WPF: (0,0) = esquina superior izquierda, Y hacia abajo
// En BEPU: Y hacia arriba, entonces convertimos WPF (x,y) -> BEPU (x,-y)
// Definir las esquinas en coordenadas WPF
var wpfTopLeft = new System.Numerics.Vector2(0, 0);
var wpfTopRight = new System.Numerics.Vector2(widthInMeters, 0);
var wpfBottomRight = new System.Numerics.Vector2(widthInMeters, heightInMeters);
var wpfBottomLeft = new System.Numerics.Vector2(0, heightInMeters);
// Convertir a coordenadas BEPU usando CoordinateConverter
var bepuTopLeft = CoordinateConverter.WpfToBepuVector2(wpfTopLeft);
var bepuTopRight = CoordinateConverter.WpfToBepuVector2(wpfTopRight);
var bepuBottomRight = CoordinateConverter.WpfToBepuVector2(wpfBottomRight);
var bepuBottomLeft = CoordinateConverter.WpfToBepuVector2(wpfBottomLeft);
// Crear los puntos 3D con Z=0
var p1 = new Point3D(bepuTopLeft.X, bepuTopLeft.Y, 0); // Esquina superior izquierda WPF
var p2 = new Point3D(bepuTopRight.X, bepuTopRight.Y, 0); // Esquina superior derecha WPF
var p3 = new Point3D(bepuBottomRight.X, bepuBottomRight.Y, 0); // Esquina inferior derecha WPF
var p4 = new Point3D(bepuBottomLeft.X, bepuBottomLeft.Y, 0); // Esquina inferior izquierda WPF
// ✅ CORREGIDO: Crear quad con orientación correcta
// Orden antihorario para normal hacia arriba (visible desde arriba)
meshBuilder.AddQuad(p1, p2, p3, p4);
// Crear el material con la imagen como textura
var imageSource = new BitmapImage(new Uri(imagePath, UriKind.Absolute));
var imageBrush = new ImageBrush(imageSource)
{
Stretch = Stretch.Fill,
TileMode = TileMode.None
};
var frontMaterial = new DiffuseMaterial(imageBrush);
var backMaterial = new DiffuseMaterial(imageBrush); // ✅ NUEVO: Material para la cara trasera
// Crear el modelo 3D con material en ambas caras
var geometry = meshBuilder.ToMesh();
var model = new GeometryModel3D(geometry, frontMaterial)
{
BackMaterial = backMaterial // ✅ NUEVO: Hace visible desde ambos lados
};
// Crear el visual 3D
_backgroundImageVisual = new ModelVisual3D { Content = model };
// Agregar al viewport
viewport3D.Children.Add(_backgroundImageVisual);
System.Diagnostics.Debug.WriteLine($"[3D Background] ✅ Imagen de fondo agregada con convenciones WPF-BEPU: {widthInMeters:F2}m x {heightInMeters:F2}m");
System.Diagnostics.Debug.WriteLine($"[3D Background] WPF (0,0) -> BEPU ({bepuTopLeft.X:F2},{bepuTopLeft.Y:F2})");
System.Diagnostics.Debug.WriteLine($"[3D Background] WPF ({widthInMeters:F2},{heightInMeters:F2}) -> BEPU ({bepuBottomRight.X:F2},{bepuBottomRight.Y:F2})");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Background] ❌ ERROR agregando imagen de fondo: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Remueve la imagen de fondo del viewport 3D
/// </summary>
public void RemoveBackgroundImage()
{
try
{
if (_backgroundImageVisual != null && viewport3D.Children.Contains(_backgroundImageVisual))
{
viewport3D.Children.Remove(_backgroundImageVisual);
System.Diagnostics.Debug.WriteLine($"[3D Background] ✅ Imagen de fondo removida");
}
_backgroundImageVisual = null;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Background] ❌ ERROR removiendo imagen de fondo: {ex.Message}");
}
}
} }
public enum CameraView public enum CameraView

View File

@ -33,9 +33,13 @@ namespace CtrEditor.Simulacion
// ✅ NUEVO: Propiedades para control de flotación/presión // ✅ NUEVO: Propiedades para control de flotación/presión
public float? LastTransportCollisionZ { get; set; } public float? LastTransportCollisionZ { get; set; }
public float PressureBuildup { get; set; } = 0;
public bool IsTouchingTransport { get; set; } = false; public bool IsTouchingTransport { get; set; } = false;
// ✅ NUEVO: Sistema de fricción dinámica con cooldown/heatup
public float HighSlippery { get; set; } = 0f; // Rango 0-9, donde 9 es máxima presión/deslizamiento
public float ContactPressure { get; set; } = 0f; // Rango 0-9, donde 9 es mínima presión/deslizamiento
public float forceScale { get; set; } = 1f;
// ✅ NUEVO: Propiedades para la física personalizada // ✅ NUEVO: Propiedades para la física personalizada
public float BaseInverseMass { get; private set; } public float BaseInverseMass { get; private set; }
public Vector3 InitialPosition3D { get; private set; } public Vector3 InitialPosition3D { get; private set; }

View File

@ -70,6 +70,23 @@ namespace CtrEditor.Simulacion
ActualizarVelocidadCinematica(); ActualizarVelocidadCinematica();
} }
public Vector3 CalcularVelocidadCinematica()
{
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
return new Vector3(0, 0, angularSpeed);
}
return Vector3.Zero; // Si no hay cuerpo o radio efectivo es muy pequeño, no hay velocidad angular
}
/// <summary> /// <summary>
/// ✅ NUEVO: Aplica la velocidad angular al cuerpo cinemático de BEPU. /// ✅ NUEVO: Aplica la velocidad angular al cuerpo cinemático de BEPU.
/// </summary> /// </summary>
@ -79,20 +96,7 @@ namespace CtrEditor.Simulacion
{ {
var body = _simulation.Bodies.GetBodyReference(BodyHandle); var body = _simulation.Bodies.GetBodyReference(BodyHandle);
float effectiveRadius = (_innerRadius + _outerRadius) / 2.0f; body.Velocity.Angular = CalcularVelocidadCinematica();
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;
}
} }
} }

View File

@ -149,6 +149,14 @@ namespace CtrEditor.Simulacion
ActualizarVelocidadCinematica(); ActualizarVelocidadCinematica();
} }
/// <summary>
/// ✅ NUEVO: Aplica la velocidad al cuerpo cinemático de BEPU.
/// </summary>
public Vector3 CalcularVelocidadCinematica()
{
return this.DirectionVector * (this.Speed / SpeedConversionFactor);
}
/// <summary> /// <summary>
/// ✅ NUEVO: Aplica la velocidad al cuerpo cinemático de BEPU. /// ✅ NUEVO: Aplica la velocidad al cuerpo cinemático de BEPU.
/// </summary> /// </summary>
@ -158,7 +166,7 @@ namespace CtrEditor.Simulacion
{ {
var body = _simulation.Bodies.GetBodyReference(BodyHandle); var body = _simulation.Bodies.GetBodyReference(BodyHandle);
// ✅ CORREGIDO: Convertir velocidad de m/min a m/s para la simulación // ✅ CORREGIDO: Convertir velocidad de m/min a m/s para la simulación
body.Velocity.Linear = this.DirectionVector * (this.Speed / SpeedConversionFactor); body.Velocity.Linear = CalcularVelocidadCinematica();
} }
} }