Se implementaron mejoras en la visualización y animación de transportes en movimiento, incluyendo un indicador visual que cambia de color según el estado de movimiento. Se añadió un sistema de animaciones automáticas utilizando StoryBoard de WPF, que combina rotación y pulsación de color para los transportes activos. Además, se ajustaron las propiedades de presión en las botellas para mejorar la gestión de fricción y se optimizó la lógica de animación y limpieza de objetos en la simulación.

This commit is contained in:
Miguel 2025-07-05 20:20:59 +02:00
parent c91b1419b4
commit 83fc828a4c
5 changed files with 282 additions and 29 deletions

View File

@ -24,3 +24,7 @@ simTransporte : Es un box por donde las botellas pueden desplazarse usando un tr
* 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.

View File

@ -172,14 +172,12 @@ namespace CtrEditor.ObjetosSim
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
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
else
ColorButton_oculto = Brushes.Gray; // Estado libre
ColorButton_oculto = Brushes.Gray; // 5. Estado libre
// Ha sido marcada para remover
if (SimGeometria.Descartar)

View File

@ -25,6 +25,7 @@ namespace CtrEditor.Simulacion
public struct NarrowPhaseCallbacks : INarrowPhaseCallbacks
{
private SimulationManagerBEPU _simulationManager;
private const float minPressureToLostFriction = 1;
public NarrowPhaseCallbacks(SimulationManagerBEPU simulationManager)
{
@ -135,14 +136,15 @@ namespace CtrEditor.Simulacion
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 - 5);
botella.PressureBuildup = Math.Max(0, botella.PressureBuildup - 0.1f);
// ✅ Fricción ajustada para un arrastre firme pero no excesivo.
var transport = transportA ?? transportB;
if (transport.isBrake)
{
botella.PressureBuildup = 0;
// ✅ NUEVO: Centrar la botella en el transporte de frenado la primera vez que entra.
// if (!botella.isOnBrakeTransport)
// if (!botella.isOnBrakeTransport)
{
if (botella.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botella.BodyHandle) &&
transport.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(transport.BodyHandle))
@ -176,10 +178,20 @@ namespace CtrEditor.Simulacion
}
else
{
botella.isOnBrakeTransport = false;
pairMaterial.FrictionCoefficient = 1.2f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
if (botella.PressureBuildup > minPressureToLostFriction)
{
botella.isOnBrakeTransport = false;
pairMaterial.FrictionCoefficient = 0.1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}
else
{
botella.isOnBrakeTransport = false;
pairMaterial.FrictionCoefficient = 1.2f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}
}
}
@ -191,6 +203,7 @@ namespace CtrEditor.Simulacion
// ✅ NUEVO: Lógica de control de presión/flotación
botella.IsTouchingTransport = true;
botella.isOnBrakeTransport = false;
var botellaCollidable = GetBotellaFromCollidable(pair.A) != null ? pair.A : pair.B;
if (botellaCollidable.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botellaCollidable.BodyHandle))
{
@ -198,19 +211,27 @@ namespace CtrEditor.Simulacion
botella.LastTransportCollisionZ = body.Pose.Position.Z;
}
// Decrementamos la presión acumulada al tocar una curva.
botella.PressureBuildup = Math.Max(0, botella.PressureBuildup - 5);
botella.PressureBuildup = Math.Max(0, botella.PressureBuildup - 0.1f);
// ✅ Fricción ajustada para un arrastre firme pero no excesivo.
pairMaterial.FrictionCoefficient = 1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
if (botella.PressureBuildup > minPressureToLostFriction)
{
pairMaterial.FrictionCoefficient = 0.1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
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
else if (botella != null && (GetGuiaFromCollidable(pair.A) != null || GetGuiaFromCollidable(pair.B) != null))
{
// Fricción baja para las guías, deben deslizar, no arrastrar.
pairMaterial.FrictionCoefficient = 0.1f;
pairMaterial.MaximumRecoveryVelocity = 1f;
pairMaterial.FrictionCoefficient = 0.01f;
pairMaterial.MaximumRecoveryVelocity = 2f;
pairMaterial.SpringSettings = new SpringSettings(20, 1);
}
// ✅ NUEVO: CONTACTO BOTELLA-BOTELLA: Configuración más suave para evitar el comportamiento "pegajoso".
@ -228,6 +249,12 @@ namespace CtrEditor.Simulacion
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;
}
// Ajustes básicos para otras botellas
else if (botella != null)

View File

@ -8,6 +8,8 @@ using BepuPhysics.Collidables;
using HelixToolkit.Wpf;
using System.Linq;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows;
namespace CtrEditor.Simulacion
{
@ -27,6 +29,9 @@ namespace CtrEditor.Simulacion
public float OuterRadius { get; set; }
public float StartAngle { get; set; }
public float EndAngle { get; set; }
// ✅ NUEVO: Estado de movimiento para transportes
public bool IsMoving { get; set; }
public bool Equals(ShapeDimensions other)
{
@ -38,7 +43,8 @@ namespace CtrEditor.Simulacion
Math.Abs(OuterRadius - other.OuterRadius) < 0.001f &&
Math.Abs(StartAngle - other.StartAngle) < 0.001f &&
Math.Abs(EndAngle - other.EndAngle) < 0.001f &&
ShapeType == other.ShapeType;
ShapeType == other.ShapeType &&
IsMoving == other.IsMoving;
}
}
@ -70,6 +76,9 @@ namespace CtrEditor.Simulacion
private SimulationManagerBEPU simulationManager;
private Dictionary<simBase, ModelVisual3D> simBaseToModelMap;
private Dictionary<simBase, ShapeDimensions> lastKnownDimensions;
// ✅ NUEVO: Diccionario para gestionar animaciones activas
private Dictionary<simBase, Storyboard> activeAnimations;
// ✅ CORREGIDO: Flag de debug para mostrar triángulos individuales de curvas (true temporalmente para verificar)
public static bool DebugShowIndividualTriangles { get; set; } = false;
@ -92,6 +101,7 @@ namespace CtrEditor.Simulacion
simulationManager = simManager;
simBaseToModelMap = new Dictionary<simBase, ModelVisual3D>();
lastKnownDimensions = new Dictionary<simBase, ShapeDimensions>();
activeAnimations = new Dictionary<simBase, Storyboard>();
InitializeViewport();
}
@ -243,6 +253,12 @@ namespace CtrEditor.Simulacion
foreach (var simObj in objectsToRemove)
{
// ✅ NUEVO: Limpiar animaciones activas para objetos eliminados
if (simObj is simTransporte transporteToRemove && activeAnimations.ContainsKey(transporteToRemove))
{
StopTransportAnimation(transporteToRemove);
}
simBaseToModelMap.Remove(simObj);
lastKnownDimensions.Remove(simObj);
}
@ -626,6 +642,12 @@ namespace CtrEditor.Simulacion
// System.Diagnostics.Debug.WriteLine($"[3D Update] No geometry changes needed for {simObj.GetType().Name}");
}
// ✅ NUEVO: Gestionar animaciones para transportes
if (simObj is simTransporte transporte)
{
ManageTransportAnimation(transporte, visual);
}
// Actualizar transformación del modelo 3D
var transform = new Transform3DGroup();
@ -689,6 +711,12 @@ namespace CtrEditor.Simulacion
return dimensions;
}
// ✅ NUEVO: Para simTransporte, detectar si está en movimiento
if (simObj is simTransporte transporte)
{
dimensions.IsMoving = Math.Abs(transporte.Speed) > 0.001f; // Consideramos movimiento si la velocidad es mayor que 0.001
}
// Para otros objetos, usar la forma real de BEPU
if (shapeIndex.Type == BepuPhysics.Collidables.Sphere.Id)
{
@ -806,6 +834,13 @@ namespace CtrEditor.Simulacion
public void Clear()
{
// ✅ NUEVO: Detener todas las animaciones activas antes de limpiar
foreach (var kvp in activeAnimations)
{
kvp.Value.Stop();
}
activeAnimations.Clear();
foreach (var model in simBaseToModelMap.Values)
{
viewport3D.Children.Remove(model);
@ -833,6 +868,40 @@ namespace CtrEditor.Simulacion
);
}
/// <summary>
/// ✅ NUEVO: Crea un material animable (no frozen) para poder animar sus propiedades
/// </summary>
/// <param name="color">Color del material</param>
/// <param name="specularPower">Potencia especular</param>
/// <param name="ambient">Nivel de luz ambiente</param>
/// <returns>Material que puede ser animado</returns>
private Material CreateAnimatableMaterial(Color color, double specularPower = 100, byte ambient = 190)
{
// Crear brush animable (no frozen)
var diffuseBrush = new SolidColorBrush(color);
// Crear material difuso
var diffuseMaterial = new DiffuseMaterial(diffuseBrush);
// Crear material especular - ✅ CORREGIDO: Usar un gris claro en lugar de blanco puro para reducir el brillo excesivo
var specularBrush = new SolidColorBrush(Color.FromRgb(128, 128, 128));
var specularMaterial = new SpecularMaterial(specularBrush, specularPower);
// Crear material "ambiente" (usando Emissive) - ✅ CORREGIDO: Usar un color mucho más oscuro para no sobreexponer
var ambientColor = Color.FromRgb((byte)(ambient / 4), (byte)(ambient / 4), (byte)(ambient / 4));
var ambientBrush = new SolidColorBrush(ambientColor);
var ambientMaterial = new EmissiveMaterial(ambientBrush);
// Combinar todos los materiales
var materialGroup = new MaterialGroup();
materialGroup.Children.Add(diffuseMaterial);
materialGroup.Children.Add(specularMaterial);
materialGroup.Children.Add(ambientMaterial);
// NO hacer freeze para mantener animable
return materialGroup;
}
/// <summary>
/// Función centralizada para obtener el material apropiado según el tipo de simBase
/// </summary>
@ -840,14 +909,24 @@ namespace CtrEditor.Simulacion
/// <returns>Material configurado para el tipo de objeto</returns>
private Material GetMaterialForSimBase(simBase simObj)
{
if (simObj is simTransporte)
if (simObj is simTransporte transporte)
{
// Material plástico verde para transportes
return MaterialHelper.CreateMaterial(
new SolidColorBrush(Color.FromRgb(80, 180, 80)),
specularPower: 140,
ambient: 200
);
// ✅ NUEVO: Cambiar color según si está en movimiento
bool isMoving = Math.Abs(transporte.Speed) > 0.001f;
if (isMoving)
{
// ✅ CORREGIDO: Usar material animable para transportes en movimiento
return CreateAnimatableMaterial(Color.FromRgb(40, 255, 40), 180, 240);
}
else
{
// Material plástico verde normal para transportes detenidos
return MaterialHelper.CreateMaterial(
new SolidColorBrush(Color.FromRgb(80, 180, 80)),
specularPower: 140,
ambient: 200
);
}
}
else if (simObj is simGuia)
{
@ -940,6 +1019,150 @@ namespace CtrEditor.Simulacion
}
}
/// <summary>
/// ✅ NUEVO: Crea una animación de movimiento para transportes usando StoryBoard
/// Combina rotación sutil + pulsación de brillo para simular movimiento
/// </summary>
/// <param name="transporte">Transporte al cual aplicar la animación</param>
/// <param name="visual">Visual3D del transporte</param>
private void CreateTransportMovementAnimation(simTransporte transporte, ModelVisual3D visual)
{
try
{
// Detener animación anterior si existe
StopTransportAnimation(transporte);
// Crear nuevo StoryBoard
var storyboard = new Storyboard();
storyboard.RepeatBehavior = RepeatBehavior.Forever;
// ✅ ANIMACIÓN 1: Rotación sutil continua alrededor del eje Z (muy lenta)
var rotationAnimation = new DoubleAnimation
{
From = 0,
To = 360,
Duration = TimeSpan.FromSeconds(20), // 20 segundos por vuelta completa (muy lento)
RepeatBehavior = RepeatBehavior.Forever
};
// Crear transform group si no existe
if (visual.Transform is not Transform3DGroup transformGroup)
{
transformGroup = new Transform3DGroup();
if (visual.Transform != null)
transformGroup.Children.Add(visual.Transform);
visual.Transform = transformGroup;
}
// Agregar rotación de animación
var animationRotation = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 0, 1), 0));
transformGroup.Children.Add(animationRotation);
// Configurar target para la rotación
Storyboard.SetTarget(rotationAnimation, animationRotation);
Storyboard.SetTargetProperty(rotationAnimation, new PropertyPath("Rotation.Angle"));
storyboard.Children.Add(rotationAnimation);
// ✅ ANIMACIÓN 2: Pulsación de brillo del material
if (visual.Content is GeometryModel3D geometryModel &&
geometryModel.Material is MaterialGroup materialGroup)
{
// Buscar el material difuso para animar (debe ser el primero en mi CreateAnimatableMaterial)
var diffuseMaterial = materialGroup.Children.OfType<DiffuseMaterial>().FirstOrDefault();
if (diffuseMaterial?.Brush is SolidColorBrush diffuseBrush)
{
// ✅ CORREGIDO: El brush ya debe ser animable si se creó con CreateAnimatableMaterial
var colorAnimation = new ColorAnimation
{
From = Color.FromRgb(40, 255, 40), // Verde brillante
To = Color.FromRgb(80, 255, 80), // Verde más brillante
Duration = TimeSpan.FromSeconds(1.5), // Pulsación cada 1.5 segundos
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever
};
Storyboard.SetTarget(colorAnimation, diffuseBrush);
Storyboard.SetTargetProperty(colorAnimation, new PropertyPath("Color"));
storyboard.Children.Add(colorAnimation);
}
}
// Iniciar animación
storyboard.Begin();
// Almacenar referencia para poder detenerla después
activeAnimations[transporte] = storyboard;
System.Diagnostics.Debug.WriteLine($"[3D Animation] ✅ Animación de movimiento creada para transporte");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Animation] ❌ Error creando animación: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Detiene la animación de movimiento de un transporte
/// </summary>
/// <param name="transporte">Transporte cuya animación se desea detener</param>
private void StopTransportAnimation(simTransporte transporte)
{
try
{
if (activeAnimations.TryGetValue(transporte, out var storyboard))
{
storyboard.Stop();
activeAnimations.Remove(transporte);
// Limpiar transforms de animación del visual
if (simBaseToModelMap.TryGetValue(transporte, out var visual) &&
visual.Transform is Transform3DGroup transformGroup)
{
// Remover solo la rotación de animación (la última agregada)
if (transformGroup.Children.Count > 1)
{
transformGroup.Children.RemoveAt(transformGroup.Children.Count - 1);
}
}
System.Diagnostics.Debug.WriteLine($"[3D Animation] ✅ Animación de transporte detenida");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Animation] ❌ Error deteniendo animación: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Gestiona automáticamente las animaciones de transportes según su estado
/// </summary>
/// <param name="transporte">Transporte a gestionar</param>
/// <param name="visual">Visual3D del transporte</param>
private void ManageTransportAnimation(simTransporte transporte, ModelVisual3D visual)
{
try
{
bool isMoving = Math.Abs(transporte.Speed) > 0.001f;
bool hasActiveAnimation = activeAnimations.ContainsKey(transporte);
if (isMoving && !hasActiveAnimation)
{
// Crear animación si el transporte está en movimiento y no tiene animación
CreateTransportMovementAnimation(transporte, visual);
}
else if (!isMoving && hasActiveAnimation)
{
// Detener animación si el transporte está detenido y tiene animación
StopTransportAnimation(transporte);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Animation] ❌ Error gestionando animación: {ex.Message}");
}
}
/// <summary>
/// ✅ MODO DEBUG DE TRIÁNGULOS DE CURVAS:
///
@ -1310,7 +1533,8 @@ namespace CtrEditor.Simulacion
InnerRadius = 0,
OuterRadius = 0,
StartAngle = 0,
EndAngle = 0
EndAngle = 0,
IsMoving = false // Las barreras no se mueven
};
}

View File

@ -33,7 +33,7 @@ namespace CtrEditor.Simulacion
// ✅ NUEVO: Propiedades para control de flotación/presión
public float? LastTransportCollisionZ { get; set; }
public int PressureBuildup { get; set; } = 0;
public float PressureBuildup { get; set; } = 0;
public bool IsTouchingTransport { get; set; } = false;
// ✅ NUEVO: Propiedades para la física personalizada