diff --git a/ObjetosSim/osBase.cs b/ObjetosSim/osBase.cs index 644b457..3b603cf 100644 --- a/ObjetosSim/osBase.cs +++ b/ObjetosSim/osBase.cs @@ -61,6 +61,25 @@ namespace CtrEditor.ObjetosSim } + /// + /// Estructura para almacenar las dimensiones de los objetos de simulación + /// + public struct SimObjectDimensions + { + public float Width { get; set; } + public float Height { get; set; } + public float Radius { get; set; } + public int ObjectType { get; set; } // Tipo de objeto: 1=Transport, 2=Barrier, 3=Guide, etc. + + public bool Equals(SimObjectDimensions other) + { + return Math.Abs(Width - other.Width) < 0.001f && + Math.Abs(Height - other.Height) < 0.001f && + Math.Abs(Radius - other.Radius) < 0.001f && + ObjectType == other.ObjectType; + } + } + public abstract partial class osBase : ObservableObject { public virtual string Nombre { get; set; } = "osBase"; @@ -70,6 +89,10 @@ namespace CtrEditor.ObjetosSim [JsonIgnore] private System.Threading.Timer timer = null; + // ✅ SISTEMA DE DIMENSIONES: Para evitar recreación innecesaria de objetos físicos + [JsonIgnore] + private static Dictionary _lastKnownDimensions = new Dictionary(); + [ObservableProperty] [property: JsonIgnore] @@ -1531,8 +1554,24 @@ namespace CtrEditor.ObjetosSim if (simRect != null) { var topLeft2D = GetRectangleTopLeft(wpfRect); - // simRect.Create maneja internamente la conversión a 3D con pivot correcto - simRect.Create(Ancho, Alto, topLeft2D, Angulo); + + // ✅ SISTEMA INTELIGENTE: Solo recrear si las dimensiones han cambiado + bool dimensionsChanged = HasTransportDimensionsChanged(simRect, Ancho, Alto); + + if (dimensionsChanged) + { + // Las dimensiones cambiaron, recrear el objeto físico + simRect.Create(Ancho, Alto, topLeft2D, Angulo); + simRect.SetDimensions(Ancho, Alto); // Actualizar dimensiones internas + } + else + { + // ✅ CORREGIDO: Usar UpdateFromWpfParameters para manejar correctamente Top-Left + Ángulo + simRect.UpdateFromWpfParameters(topLeft2D, Angulo); + } + + // Actualizar propiedades cacheadas + simRect.UpdateCachedProperties(); } } @@ -1541,8 +1580,21 @@ namespace CtrEditor.ObjetosSim if (simRect != null) { var topLeft2D = GetRectangleTopLeft(wpfRect); - // simRect.Create maneja internamente la conversión a 3D con pivot correcto - simRect.Create(Ancho, Alto, topLeft2D, Angulo); + + // ✅ SISTEMA INTELIGENTE: Solo recrear si las dimensiones han cambiado + bool dimensionsChanged = HasBarrierDimensionsChanged(simRect, Ancho, Alto); + + if (dimensionsChanged) + { + // Las dimensiones cambiaron, recrear el objeto físico + simRect.Create(Ancho, Alto, topLeft2D, Angulo); + simRect.SetDimensions(Ancho, Alto); // Actualizar dimensiones internas + } + else + { + // ✅ CORREGIDO: Usar UpdateFromWpfParameters para manejar correctamente Top-Left + Ángulo + simRect.UpdateFromWpfParameters(topLeft2D, Angulo); + } } } @@ -1585,9 +1637,23 @@ namespace CtrEditor.ObjetosSim // Actualizar las propiedades desde el objeto osGuia if (this is osGuia guiaObj) { + // ✅ SISTEMA INTELIGENTE: Solo recrear si las dimensiones han cambiado + bool dimensionsChanged = HasGuideDimensionsChanged(simGuia, guiaObj.Ancho, guiaObj.AltoGuia); + + if (dimensionsChanged) + { + // Las dimensiones cambiaron, recrear el objeto físico + simGuia.Create(guiaObj.Ancho, guiaObj.AltoGuia, topLeft2D, guiaObj.Angulo); + simGuia.SetDimensions(guiaObj.Ancho, guiaObj.AltoGuia); // Actualizar dimensiones internas + } + else + { + // Solo actualizar posición y rotación + simGuia.SetPosition(topLeft2D, guiaObj.Angulo); + } + + // Actualizar propiedades internas simGuia.UpdateProperties(guiaObj.Ancho, guiaObj.AltoGuia, guiaObj.Angulo); - // Crear usando Top-Left + dimensiones + ángulo - simGuia.Create(guiaObj.Ancho, guiaObj.AltoGuia, topLeft2D, guiaObj.Angulo); } } } @@ -1676,6 +1742,128 @@ namespace CtrEditor.ObjetosSim } } + // ✅ MÉTODOS PARA GESTIÓN INTELIGENTE DE DIMENSIONES + + /// + /// Verifica si las dimensiones de un transporte han cambiado + /// + private bool HasTransportDimensionsChanged(simTransporte transport, float newWidth, float newHeight) + { + if (transport == null) return true; + + var newDimensions = new SimObjectDimensions + { + Width = newWidth, + Height = newHeight, + ObjectType = 1 // Transport + }; + + if (_lastKnownDimensions.TryGetValue(transport, out var lastDimensions)) + { + bool changed = !newDimensions.Equals(lastDimensions); + if (changed) + { + _lastKnownDimensions[transport] = newDimensions; + System.Diagnostics.Debug.WriteLine($"[Dimensions] Transport dimensions CHANGED: {newWidth}x{newHeight}"); + } + return changed; + } + else + { + // Primera vez - consideramos como cambio + _lastKnownDimensions[transport] = newDimensions; + System.Diagnostics.Debug.WriteLine($"[Dimensions] Transport first time: {newWidth}x{newHeight}"); + return true; + } + } + + /// + /// Verifica si las dimensiones de una barrera han cambiado + /// + private bool HasBarrierDimensionsChanged(simBarrera barrier, float newWidth, float newHeight) + { + if (barrier == null) return true; + + var newDimensions = new SimObjectDimensions + { + Width = newWidth, + Height = newHeight, + ObjectType = 2 // Barrier + }; + + if (_lastKnownDimensions.TryGetValue(barrier, out var lastDimensions)) + { + bool changed = !newDimensions.Equals(lastDimensions); + if (changed) + { + _lastKnownDimensions[barrier] = newDimensions; + System.Diagnostics.Debug.WriteLine($"[Dimensions] Barrier dimensions CHANGED: {newWidth}x{newHeight}"); + } + return changed; + } + else + { + // Primera vez - consideramos como cambio + _lastKnownDimensions[barrier] = newDimensions; + System.Diagnostics.Debug.WriteLine($"[Dimensions] Barrier first time: {newWidth}x{newHeight}"); + return true; + } + } + + /// + /// Verifica si las dimensiones de una guía han cambiado + /// + private bool HasGuideDimensionsChanged(simGuia guide, float newWidth, float newHeight) + { + if (guide == null) return true; + + var newDimensions = new SimObjectDimensions + { + Width = newWidth, + Height = newHeight, + ObjectType = 3 // Guide + }; + + if (_lastKnownDimensions.TryGetValue(guide, out var lastDimensions)) + { + bool changed = !newDimensions.Equals(lastDimensions); + if (changed) + { + _lastKnownDimensions[guide] = newDimensions; + System.Diagnostics.Debug.WriteLine($"[Dimensions] Guide dimensions CHANGED: {newWidth}x{newHeight}"); + } + return changed; + } + else + { + // Primera vez - consideramos como cambio + _lastKnownDimensions[guide] = newDimensions; + System.Diagnostics.Debug.WriteLine($"[Dimensions] Guide first time: {newWidth}x{newHeight}"); + return true; + } + } + + /// + /// Limpia las dimensiones almacenadas para un objeto específico + /// + public static void ClearStoredDimensions(simBase simObj) + { + if (simObj != null && _lastKnownDimensions.ContainsKey(simObj)) + { + _lastKnownDimensions.Remove(simObj); + System.Diagnostics.Debug.WriteLine($"[Dimensions] Cleared stored dimensions for {simObj.GetType().Name}"); + } + } + + /// + /// Limpia todas las dimensiones almacenadas (útil al cerrar simulación) + /// + public static void ClearAllStoredDimensions() + { + _lastKnownDimensions.Clear(); + System.Diagnostics.Debug.WriteLine($"[Dimensions] Cleared ALL stored dimensions"); + } + } public class UniqueId { diff --git a/Simulacion/BEPU.cs b/Simulacion/BEPU.cs index 359c7ad..70bd328 100644 --- a/Simulacion/BEPU.cs +++ b/Simulacion/BEPU.cs @@ -17,12 +17,223 @@ using CtrEditor.FuncionesBase; namespace CtrEditor.Simulacion { + /// + /// Clase centralizada para manejar todas las conversiones entre coordenadas WPF y BEPU + /// WPF: Y hacia abajo, ángulos en sentido horario, Top-Left como referencia + /// BEPU: Y hacia arriba, ángulos en sentido antihorario, Center como referencia + /// + public static class CoordinateConverter + { + /// + /// Convierte ángulo de WPF a BEPU (invierte signo) + /// + public static float WpfAngleToBepuAngle(float wpfAngle) + { + return -wpfAngle; + } + + /// + /// Convierte ángulo de BEPU a WPF (invierte signo) + /// + public static float BepuAngleToWpfAngle(float bepuAngle) + { + return -bepuAngle; + } + + /// + /// Convierte posición Y de WPF a BEPU (invierte signo) + /// + public static float WpfYToBepuY(float wpfY) + { + return -wpfY; + } + + /// + /// Convierte posición Y de BEPU a WPF (invierte signo) + /// + public static float BepuYToWpfY(float bepuY) + { + return -bepuY; + } + + /// + /// Convierte Vector2 de WPF a BEPU (solo invierte Y) + /// + public static Vector2 WpfToBepuVector2(Vector2 wpfVector) + { + return new Vector2(wpfVector.X, -wpfVector.Y); + } + + /// + /// Convierte Vector2 de BEPU a WPF (solo invierte Y) + /// + public static Vector2 BepuToWpfVector2(Vector2 bepuVector) + { + return new Vector2(bepuVector.X, -bepuVector.Y); + } + + /// + /// Convierte Vector3 de BEPU a Vector2 WPF (proyección XY con Y invertida) + /// + public static Vector2 BepuVector3ToWpfVector2(Vector3 bepuVector) + { + return new Vector2(bepuVector.X, -bepuVector.Y); + } + + /// + /// Calcula la posición del centro en coordenadas BEPU desde Top-Left WPF + /// Maneja correctamente la rotación del objeto + /// + public static Vector3 CalculateBepuCenterFromWpfTopLeft(Vector2 wpfTopLeft, float width, float height, float wpfAngle, float zPosition) + { + // Calcular el offset del centro desde Top-Left (sin rotación) + var offsetX = width / 2f; + var offsetY = height / 2f; + + // Convertir ángulo WPF a radianes para cálculos trigonométricos + var angleRadians = simBase.GradosARadianes(wpfAngle); + var cos = (float)Math.Cos(angleRadians); + var sin = (float)Math.Sin(angleRadians); + + // Rotar el offset alrededor del Top-Left usando el ángulo WPF + var rotatedOffsetX = offsetX * cos - offsetY * sin; + var rotatedOffsetY = offsetX * sin + offsetY * cos; + + // Calcular nueva posición del centro manteniendo Top-Left fijo + var centerX = wpfTopLeft.X + rotatedOffsetX; + var centerY = wpfTopLeft.Y + rotatedOffsetY; + + // Convertir a 3D con Y invertida para BEPU + return new Vector3(centerX, WpfYToBepuY(centerY), zPosition); + } + + /// + /// Calcula la posición Top-Left WPF desde el centro BEPU + /// Maneja correctamente la rotación del objeto + /// + public static Vector2 CalculateWpfTopLeftFromBepuCenter(Vector3 bepuCenter, float width, float height, float wpfAngle) + { + // Convertir centro BEPU a WPF + var wpfCenterX = bepuCenter.X; + var wpfCenterY = BepuYToWpfY(bepuCenter.Y); + + // Calcular el offset del centro al Top-Left (sin rotación) + var offsetX = -width / 2f; + var offsetY = -height / 2f; + + // Convertir ángulo WPF a radianes para cálculos trigonométricos + var angleRadians = simBase.GradosARadianes(wpfAngle); + var cos = (float)Math.Cos(angleRadians); + var sin = (float)Math.Sin(angleRadians); + + // Rotar el offset usando el ángulo WPF + var rotatedOffsetX = offsetX * cos - offsetY * sin; + var rotatedOffsetY = offsetX * sin + offsetY * cos; + + // Calcular Top-Left desde el centro + var topLeftX = wpfCenterX + rotatedOffsetX; + var topLeftY = wpfCenterY + rotatedOffsetY; + + return new Vector2(topLeftX, topLeftY); + } + + /// + /// Crea un Quaternion para BEPU desde un ángulo WPF + /// + public static Quaternion CreateBepuQuaternionFromWpfAngle(float wpfAngle) + { + var bepuAngle = WpfAngleToBepuAngle(wpfAngle); + return Quaternion.CreateFromAxisAngle(Vector3.UnitZ, simBase.GradosARadianes(bepuAngle)); + } + + /// + /// Extrae el ángulo WPF desde un Quaternion BEPU + /// + public static float ExtractWpfAngleFromBepuQuaternion(Quaternion bepuQuaternion) + { + // Extraer ángulo Z del quaternion + var bepuAngleRadians = (float)Math.Atan2( + 2.0 * (bepuQuaternion.W * bepuQuaternion.Z + bepuQuaternion.X * bepuQuaternion.Y), + 1.0 - 2.0 * (bepuQuaternion.Y * bepuQuaternion.Y + bepuQuaternion.Z * bepuQuaternion.Z) + ); + + var bepuAngleDegrees = simBase.RadianesAGrados(bepuAngleRadians); + return BepuAngleToWpfAngle(bepuAngleDegrees); + } + + /// + /// Actualiza la posición de un body BEPU manteniendo su rotación actual + /// + public static void UpdateBepuBodyPosition(Simulation simulation, BodyHandle bodyHandle, Vector3 newBepuPosition) + { + if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) + { + var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); + bodyReference.Pose.Position = newBepuPosition; + } + } + + /// + /// Actualiza la rotación de un body BEPU manteniendo su posición actual + /// + public static void UpdateBepuBodyRotation(Simulation simulation, BodyHandle bodyHandle, float wpfAngle) + { + if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) + { + var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); + bodyReference.Pose.Orientation = CreateBepuQuaternionFromWpfAngle(wpfAngle); + } + } + + /// + /// Actualiza posición y rotación de un body BEPU simultáneamente + /// + public static void UpdateBepuBodyPose(Simulation simulation, BodyHandle bodyHandle, Vector3 newBepuPosition, float wpfAngle) + { + if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) + { + var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); + bodyReference.Pose.Position = newBepuPosition; + bodyReference.Pose.Orientation = CreateBepuQuaternionFromWpfAngle(wpfAngle); + } + } + + /// + /// Obtiene la posición del centro en coordenadas BEPU + /// + public static Vector3 GetBepuBodyPosition(Simulation simulation, BodyHandle bodyHandle) + { + if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) + { + var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); + return bodyReference.Pose.Position; + } + return Vector3.Zero; + } + + /// + /// Obtiene el ángulo WPF desde un body BEPU + /// + public static float GetWpfAngleFromBepuBody(Simulation simulation, BodyHandle bodyHandle) + { + if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) + { + var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); + return ExtractWpfAngleFromBepuQuaternion(bodyReference.Pose.Orientation); + } + return 0f; + } + } + public class simBase { public BodyHandle BodyHandle { get; protected set; } public Simulation _simulation; protected bool _bodyCreated = false; // Bandera para saber si hemos creado un cuerpo + // ✅ NUEVA CONSTANTE - unificar conversión de velocidad + public const float SPEED_CONVERSION_FACTOR = 1f; //18.5f; // Factor de conversión de velocidad interna a m/s + // Constantes para las posiciones Z de los objetos 3D public const float zPos_Transporte = 0f; // Z de la parte baja public const float zAltura_Transporte = 0.1f; // Altura del transporte sobre zPos @@ -48,6 +259,38 @@ namespace CtrEditor.Simulacion } } + /// + /// ✅ NUEVO: Cambia la forma de un body existente, limpiando la forma anterior para evitar memory leaks + /// + protected void ChangeBodyShape(TypedIndex newShapeIndex) + { + if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) + { + var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); + + // ✅ CRÍTICO: Obtener la forma anterior para limpiarla del pool de shapes + var oldShapeIndex = bodyReference.Collidable.Shape; + + // Cambiar a la nueva forma + _simulation.Bodies.SetShape(BodyHandle, newShapeIndex); + + // ✅ CRÍTICO: Limpiar la forma anterior del pool para evitar memory leaks + // Nota: Solo limpiar si es diferente (para evitar limpiar la forma que acabamos de asignar) + if (oldShapeIndex.Packed != newShapeIndex.Packed) + { + try + { + _simulation.Shapes.RemoveAndDispose(oldShapeIndex, _simulation.BufferPool); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[simBase] Warning: Could not dispose old shape: {ex.Message}"); + // Continuar - esto no es crítico para la funcionalidad + } + } + } + } + public static float Min(float Value, float Min = 0.01f) { return Math.Max(Value, Min); @@ -65,56 +308,35 @@ namespace CtrEditor.Simulacion public void SetPosition(float x, float y, float z = 0) { - if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) - { - var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); - bodyReference.Pose.Position = new Vector3(x, y, z); - } + CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, new Vector3(x, y, z)); } - public void SetPosition(Vector2 position) + public void SetPosition(Vector2 wpfPosition) { - // Invertir Y para convertir de WPF (Y hacia abajo) a 3D (Y hacia arriba) // Mantener la coordenada Z actual para preservar la altura del objeto - var currentPosition = GetPosition(); - SetPosition(position.X, -position.Y, currentPosition.Z); + var currentBepuPosition = CoordinateConverter.GetBepuBodyPosition(_simulation, BodyHandle); + var newBepuPosition = new Vector3(wpfPosition.X, CoordinateConverter.WpfYToBepuY(wpfPosition.Y), currentBepuPosition.Z); + CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, newBepuPosition); } - public void SetPosition(Vector3 position) + public void SetPosition(Vector3 bepuPosition) { - SetPosition(position.X, position.Y, position.Z); + CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, bepuPosition); } public Vector3 GetPosition() { - if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) - { - var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); - return bodyReference.Pose.Position; - } - return Vector3.Zero; + return CoordinateConverter.GetBepuBodyPosition(_simulation, BodyHandle); } - public void SetRotation(float angleZ) + public void SetRotation(float wpfAngle) { - if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) - { - var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); - // ✅ Convertir de grados a radianes para Quaternion.CreateFromAxisAngle - bodyReference.Pose.Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, GradosARadianes(angleZ)); - } + CoordinateConverter.UpdateBepuBodyRotation(_simulation, BodyHandle, wpfAngle); } public float GetRotationZ() { - if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) - { - var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); - // Extraer el ángulo Z del quaternion - var q = bodyReference.Pose.Orientation; - return (float)Math.Atan2(2.0 * (q.W * q.Z + q.X * q.Y), 1.0 - 2.0 * (q.Y * q.Y + q.Z * q.Z)); - } - return 0f; + return CoordinateConverter.GetWpfAngleFromBepuBody(_simulation, BodyHandle); } } @@ -130,15 +352,30 @@ namespace CtrEditor.Simulacion public float Width { get; set; } public float Height { get; set; } - public simTransporte(Simulation simulation, List deferredActions, float width, float height, Vector2 topLeft, float angle = 0) + // ✅ NUEVAS PROPIEDADES - cachear cálculos costosos + public Vector3 DirectionVector { get; private set; } + public float SpeedMetersPerSecond { get; private set; } + public List BottlesOnTransport { get; private set; } = new List(); + + // ✅ NUEVO EVENTO - para actualización de motores + public event Action OnSpeedChanged; + + // ✅ NUEVA REFERENCIA - para limpiar motors + private SimulationManagerBEPU _simulationManager; + + public simTransporte(Simulation simulation, List deferredActions, float width, float height, Vector2 topLeft, float angle = 0, SimulationManagerBEPU simulationManager = null) { _simulation = simulation; _deferredActions = deferredActions; + _simulationManager = simulationManager; // ✅ NUEVA REFERENCIA Width = width; Height = height; // Usar el nuevo método Create que maneja Top-Left correctamente Create(width, height, topLeft, angle); + + // ✅ INICIALIZAR PROPIEDADES CRÍTICAS + UpdateCachedProperties(); } public float Angle @@ -157,10 +394,57 @@ namespace CtrEditor.Simulacion { base.SetPosition(x, y, z); } + + /// + /// ✅ NUEVO: Actualiza posición desde Top-Left WPF con dimensiones y ángulo actuales + /// + public void SetPositionFromWpfTopLeft(Vector2 wpfTopLeft) + { + var currentWpfAngle = GetRotationZ(); // Ya usa CoordinateConverter + var zPosition = GetPosition().Z; // Mantener Z actual + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, currentWpfAngle, zPosition); + SetPosition(bepuCenter); + } + + /// + /// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual + /// + public Vector2 GetWpfTopLeft() + { + var bepuCenter = GetPosition(); + var wpfAngle = GetRotationZ(); // Ya usa CoordinateConverter + return CoordinateConverter.CalculateWpfTopLeftFromBepuCenter(bepuCenter, Width, Height, wpfAngle); + } + + /// + /// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF + /// + public void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle) + { + var zPosition = GetPosition().Z; // Mantener Z actual + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, wpfAngle, zPosition); + CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); + + // Actualizar propiedades cacheadas después del cambio + UpdateCachedProperties(); + } + // ✅ NUEVO MÉTODO - actualizar propiedades cacheadas + public void UpdateCachedProperties() + { + // Calcular dirección basada en rotación actual + var angle = GetRotationZ(); + DirectionVector = new Vector3((float)Math.Cos(angle), (float)Math.Sin(angle), 0); + SpeedMetersPerSecond = Speed / SPEED_CONVERSION_FACTOR; + } + + // ✅ MODIFICAR MÉTODO EXISTENTE - disparar evento public void SetSpeed(float speed) { Speed = speed; + UpdateCachedProperties(); + // Disparar evento para actualizar motores activos + OnSpeedChanged?.Invoke(this); } /// @@ -170,7 +454,7 @@ namespace CtrEditor.Simulacion /// Velocidad en m/s (típicamente entre -5.0 y 5.0) public void SetTransportSpeed(float speedMeterPerSecond) { - Speed = speedMeterPerSecond; + SetSpeed(speedMeterPerSecond * SPEED_CONVERSION_FACTOR); } /// @@ -178,7 +462,7 @@ namespace CtrEditor.Simulacion /// public void StopTransport() { - Speed = 0f; + SetSpeed(0f); } /// @@ -186,7 +470,7 @@ namespace CtrEditor.Simulacion /// public void ReverseTransport() { - Speed = -Speed; + SetSpeed(-Speed); } public void SetDimensions(float width, float height) @@ -194,49 +478,71 @@ namespace CtrEditor.Simulacion Width = width; Height = height; - // Actualizar la forma del cuerpo existente + // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var box = new Box(width, height, zAltura_Transporte); var shapeIndex = _simulation.Shapes.Add(box); - _simulation.Bodies.SetShape(BodyHandle, shapeIndex); + ChangeBodyShape(shapeIndex); } } - public void Create(float width, float height, Vector2 topLeft, float angle = 0) + public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle = 0) { - // Calcular el offset del centro desde Top-Left (sin rotación) - var offsetX = width / 2f; - var offsetY = height / 2f; - - // Rotar el offset alrededor del Top-Left usando el ángulo - // ✅ Convertir de grados a radianes antes de usar Math.Cos/Sin - var angleRadians = GradosARadianes(angle); - var cos = (float)Math.Cos(angleRadians); - var sin = (float)Math.Sin(angleRadians); - - var rotatedOffsetX = offsetX * cos - offsetY * sin; - var rotatedOffsetY = offsetX * sin + offsetY * cos; - - // Calcular nueva posición del centro manteniendo Top-Left fijo - var centerX = topLeft.X + rotatedOffsetX; - var centerY = topLeft.Y + rotatedOffsetY; - - // Convertir a 3D e invertir Y - Transportes en Z = Depth/2 para estar sobre el suelo - var center3D = new Vector3(centerX, -centerY, zAltura_Transporte / 2f + zPos_Transporte); - - Create(width, height, center3D, -angle); // ✅ Invertir ángulo cuando se invierte Y + // ✅ USAR COORDINATECONVERTER para conversión centralizada + var zPosition = zAltura_Transporte / 2f + zPos_Transporte; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); + + Create(width, height, bepuCenter, wpfAngle); } - public void Create(float width, float height, Vector3 position, float angle = 0) + /// + /// ✅ MODIFICADO: RemoverBody que limpia motors conectados antes de eliminar el body + /// + public new void RemoverBody() + { + if (_bodyCreated && _simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) + { + // ✅ CRÍTICO: Limpiar todos los motors conectados a este transporte ANTES de eliminar el body + RemoveConnectedMotors(); + + _simulation.Bodies.Remove(BodyHandle); + _bodyCreated = false; + } + } + + /// + /// ✅ NUEVO: Limpia todos los motors conectados a este transporte + /// + private void RemoveConnectedMotors() + { + try + { + // ✅ USAR REFERENCIA DIRECTA al SimulationManager + if (_simulationManager != null) + { + _simulationManager.RemoveMotorsConnectedToBody(BodyHandle); + } + + // Limpiar la lista local de botellas + BottlesOnTransport.Clear(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[simTransporte] Error removing connected motors: {ex.Message}"); + } + } + + public void Create(float width, float height, Vector3 bepuPosition, float wpfAngle = 0) { RemoverBody(); var box = new Box(width, height, zAltura_Transporte); var shapeIndex = _simulation.Shapes.Add(box); + // ✅ USAR COORDINATECONVERTER para conversión centralizada var bodyDescription = BodyDescription.CreateKinematic( - new RigidPose(position, Quaternion.CreateFromAxisAngle(Vector3.UnitZ, GradosARadianes(angle))), + new RigidPose(bepuPosition, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)), new CollidableDescription(shapeIndex, 0.1f), new BodyActivityDescription(0.01f) ); @@ -288,44 +594,60 @@ namespace CtrEditor.Simulacion { base.SetPosition(x, y, z); } + + /// + /// ✅ NUEVO: Actualiza posición desde Top-Left WPF con dimensiones y ángulo actuales + /// + public void SetPositionFromWpfTopLeft(Vector2 wpfTopLeft) + { + var currentWpfAngle = GetRotationZ(); // Ya usa CoordinateConverter + var zPosition = GetPosition().Z; // Mantener Z actual + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, currentWpfAngle, zPosition); + SetPosition(bepuCenter); + } + + /// + /// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual + /// + public Vector2 GetWpfTopLeft() + { + var bepuCenter = GetPosition(); + var wpfAngle = GetRotationZ(); // Ya usa CoordinateConverter + return CoordinateConverter.CalculateWpfTopLeftFromBepuCenter(bepuCenter, Width, Height, wpfAngle); + } + + /// + /// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF + /// + public void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle) + { + var zPosition = GetPosition().Z; // Mantener Z actual + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, wpfAngle, zPosition); + CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); + } public void SetDimensions(float width, float height) { _height = height; - // Actualizar la forma del cuerpo existente + // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var box = new Box(width, height, zAltura_Barrera); // Altura de 20cm para detectar botellas var shapeIndex = _simulation.Shapes.Add(box); - _simulation.Bodies.SetShape(BodyHandle, shapeIndex); + ChangeBodyShape(shapeIndex); } } - public void Create(float width, float height, Vector2 topLeft, float angle = 0, bool detectectNeck = false) + public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle = 0, bool detectectNeck = false) { - // Calcular el offset del centro desde Top-Left (sin rotación) - var offsetX = width / 2f; - var offsetY = height / 2f; - - // Rotar el offset alrededor del Top-Left usando el ángulo - // ✅ Convertir de grados a radianes antes de usar Math.Cos/Sin - var angleRadians = GradosARadianes(angle); - var cos = (float)Math.Cos(angleRadians); - var sin = (float)Math.Sin(angleRadians); - - var rotatedOffsetX = offsetX * cos - offsetY * sin; - var rotatedOffsetY = offsetX * sin + offsetY * cos; - - // Calcular nueva posición del centro manteniendo Top-Left fijo - var centerX = topLeft.X + rotatedOffsetX; - var centerY = topLeft.Y + rotatedOffsetY; - - // Convertir a 3D e invertir Y - Sensores centrados en z=0.1 (rango 0-0.2) para detectar botellas - var center3D = new Vector3(centerX, -centerY, zPos_Barrera + zAltura_Barrera / 2f); - Create(width, height, center3D, -angle, detectectNeck); // ✅ Invertir ángulo cuando se invierte Y + // ✅ USAR COORDINATECONVERTER para conversión centralizada + var zPosition = zPos_Barrera + zAltura_Barrera / 2f; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); + + Create(width, height, bepuCenter, wpfAngle, detectectNeck); } - public void Create(float width, float height, Vector3 position, float angle = 0, bool detectectNeck = false) + public void Create(float width, float height, Vector3 bepuPosition, float wpfAngle = 0, bool detectectNeck = false) { RemoverBody(); @@ -337,8 +659,9 @@ namespace CtrEditor.Simulacion // Configurar actividad para NUNCA dormir - los sensores deben estar siempre activos var activityDescription = new BodyActivityDescription(-1f); // -1 = nunca dormir + // ✅ USAR COORDINATECONVERTER para conversión centralizada var bodyDescription = BodyDescription.CreateKinematic( - new RigidPose(position, Quaternion.CreateFromAxisAngle(Vector3.UnitZ, GradosARadianes(angle))), + new RigidPose(bepuPosition, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)), new CollidableDescription(shapeIndex, 0f), // Speculative margin 0 para sensores activityDescription ); @@ -365,39 +688,21 @@ namespace CtrEditor.Simulacion Create(width, height, topLeft, angle); } - public void Create(float width, float height, Vector2 topLeft, float angle) + public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle) { RemoverBody(); - // Calcular el offset del centro desde Top-Left (sin rotación) - var offsetX = width / 2f; - var offsetY = height / 2f; - - // Rotar el offset alrededor del Top-Left usando el ángulo - // ✅ Convertir de grados a radianes antes de usar Math.Cos/Sin - var angleRadians = GradosARadianes(angle); - var cos = (float)Math.Cos(angleRadians); - var sin = (float)Math.Sin(angleRadians); - - var rotatedOffsetX = offsetX * cos - offsetY * sin; - var rotatedOffsetY = offsetX * sin + offsetY * cos; - - // Calcular nueva posición del centro manteniendo Top-Left fijo - var centerX = topLeft.X + rotatedOffsetX; - var centerY = topLeft.Y + rotatedOffsetY; - - // Convertir a 3D e invertir Y - Guías más altas que transportes - var center3D = new Vector3(centerX, -centerY, zAltura_Guia / 2 + zPos_Guia); + // ✅ USAR COORDINATECONVERTER para conversión centralizada + var zPosition = zAltura_Guia / 2 + zPos_Guia; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); // Crear el Box con las dimensiones dadas - más alto para contener botellas var box = new Box(width, GuideThickness, zAltura_Guia); var shapeIndex = _simulation.Shapes.Add(box); - // ✅ Invertir ángulo cuando se invierte Y para coherencia de rotación - var orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, GradosARadianes(-angle)); - + // ✅ USAR COORDINATECONVERTER para conversión centralizada var bodyDescription = BodyDescription.CreateKinematic( - new RigidPose(center3D, orientation), + new RigidPose(bepuCenter, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)), new CollidableDescription(shapeIndex, 0.1f), new BodyActivityDescription(0.01f) ); @@ -412,6 +717,38 @@ namespace CtrEditor.Simulacion // Nota: ancho y angulo se ignoran ya que se calculan automáticamente desde start->end GuideThickness = altoGuia; } + + /// + /// ✅ NUEVO - Actualiza solo las dimensiones sin recrear el objeto + /// + public void SetDimensions(float width, float height) + { + GuideThickness = height; + + // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior + if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) + { + var box = new Box(width, GuideThickness, zAltura_Guia); + var shapeIndex = _simulation.Shapes.Add(box); + ChangeBodyShape(shapeIndex); + } + } + + /// + /// ✅ NUEVO - Actualiza solo la posición sin recrear el objeto usando CoordinateConverter + /// + public void SetPosition(Vector2 wpfTopLeft, float wpfAngle = 0) + { + // ✅ USAR COORDINATECONVERTER para conversión centralizada + var zPosition = zAltura_Guia / 2 + zPos_Guia; + // Usar las dimensiones actuales de la guía + var width = GuideThickness; // Para guías, el ancho puede ser derivado del espesor + var height = GuideThickness; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); + + // Actualizar posición y rotación simultáneamente + CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); + } } public class simBotella : simBase @@ -424,12 +761,18 @@ namespace CtrEditor.Simulacion public List ListOnTransports; public bool isRestricted; public bool isNoMoreRestricted; - public float OriginalMass; + public simTransporte ConveyorRestrictedTo; public float OverlapPercentage; public float _neckRadius; public bool isOnBrakeTransport; // Nueva propiedad para marcar si está en un transporte con freno public simTransporte CurrentBrakeTransport { get; set; } // ✅ NUEVA: Referencia al transporte de freno actual + + // ✅ NUEVAS PROPIEDADES - gestión de motores + public simTransporte CurrentTransport { get; set; } + public ConstraintHandle CurrentMotor { get; set; } + public bool HasActiveMotor => CurrentMotor.Value != 0; + private List _deferredActions; public simBotella(Simulation simulation, List deferredActions, float diameter, Vector2 position, float mass, float neckRadius = 0) @@ -439,7 +782,6 @@ namespace CtrEditor.Simulacion Radius = diameter / 2f; Height = diameter; // Altura igual al diámetro para mantener proporciones similares _mass = mass; - OriginalMass = mass; _neckRadius = neckRadius; ListOnTransports = new List(); @@ -550,12 +892,12 @@ namespace CtrEditor.Simulacion Radius = diameter / 2f; Height = diameter; // Mantener altura igual al diámetro para proporciones consistentes - // Actualizar la forma del cuerpo existente + // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var sphere = new Sphere(Radius); var shapeIndex = _simulation.Shapes.Add(sphere); - _simulation.Bodies.SetShape(BodyHandle, shapeIndex); + ChangeBodyShape(shapeIndex); } } @@ -563,12 +905,12 @@ namespace CtrEditor.Simulacion { Height = height; - // Actualizar la forma del cuerpo existente + // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var sphere = new Sphere(Radius); var shapeIndex = _simulation.Shapes.Add(sphere); - _simulation.Bodies.SetShape(BodyHandle, shapeIndex); + ChangeBodyShape(shapeIndex); } } @@ -634,26 +976,26 @@ namespace CtrEditor.Simulacion return isOnTransports > 0; } - /// - /// ✅ NUEVO: Método de seguridad para restaurar la masa original si la botella tiene masa aumentada - /// - public void RestoreOriginalMassIfNeeded() + // ✅ NUEVOS MÉTODOS - gestión de estado + public void AssignToTransport(simTransporte transport, ConstraintHandle motor) { - if (OriginalMass > 0) // Solo si hay masa original guardada + CurrentTransport = transport; + CurrentMotor = motor; + if (transport.isBrake) { - try - { - SetMass(OriginalMass); - OriginalMass = 0; // Limpiar para evitar confusión + isOnBrakeTransport = true; + CurrentBrakeTransport = transport; + } + } + + public void RemoveFromTransport() + { + CurrentTransport = null; + CurrentMotor = default; isOnBrakeTransport = false; CurrentBrakeTransport = null; } - catch (Exception ex) - { - // Error restoring original mass - continue - } - } - } + } public class simCurve : simBase @@ -668,6 +1010,16 @@ namespace CtrEditor.Simulacion internal List _triangleBodyHandles; // Lista de todos los triángulos que componen la curva private List> _originalTriangles; // ✅ NUEVO: Triángulos originales para visualización debug + // ✅ NUEVAS PROPIEDADES - optimización + public float SpeedMetersPerSecond { get; private set; } + public List BottlesOnCurve { get; private set; } = new List(); + + // ✅ NUEVO EVENTO - para actualización de motores + public event Action OnSpeedChanged; + + // ✅ NUEVA REFERENCIA - para limpiar motors + private SimulationManagerBEPU _simulationManager; + public float InnerRadius => _innerRadius; public float OuterRadius => _outerRadius; public float StartAngle => _startAngle; @@ -678,10 +1030,11 @@ namespace CtrEditor.Simulacion /// public List> GetOriginalTriangles() => _originalTriangles ?? new List>(); - public simCurve(Simulation simulation, List deferredActions, float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 topLeft, float unused = 0) + public simCurve(Simulation simulation, List deferredActions, float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 topLeft, float unused = 0, SimulationManagerBEPU simulationManager = null) { _simulation = simulation; _deferredActions = deferredActions; + _simulationManager = simulationManager; // ✅ NUEVA REFERENCIA _innerRadius = innerRadius; _outerRadius = outerRadius; // ✅ SENTIDO HORARIO: Convertir ángulos para que vayan en sentido horario @@ -694,9 +1047,12 @@ namespace CtrEditor.Simulacion Create(innerRadius, outerRadius, startAngle, endAngle, topLeft, 0); } + // ✅ MODIFICAR MÉTODO EXISTENTE - disparar evento public void SetSpeed(float speed) { Speed = -speed; + SpeedMetersPerSecond = Math.Abs(Speed) / SPEED_CONVERSION_FACTOR; + OnSpeedChanged?.Invoke(this); } public void UpdateCurve(float innerRadius, float outerRadius, float startAngle, float endAngle) @@ -713,6 +1069,9 @@ namespace CtrEditor.Simulacion public new void RemoverBody() { + // ✅ CRÍTICO: Limpiar todos los motors conectados a esta curva ANTES de eliminar los bodies + RemoveConnectedMotors(); + // Remover todos los triángulos de la curva if (_triangleBodyHandles != null && _simulation != null) { @@ -735,8 +1094,40 @@ namespace CtrEditor.Simulacion // Remover el cuerpo principal (si existe) base.RemoverBody(); } + + /// + /// ✅ NUEVO: Limpia todos los motors conectados a esta curva + /// + private void RemoveConnectedMotors() + { + try + { + // ✅ USAR REFERENCIA DIRECTA al SimulationManager + if (_simulationManager != null) + { + // Limpiar motors del cuerpo principal + _simulationManager.RemoveMotorsConnectedToBody(BodyHandle); + + // Limpiar motors de todos los triángulos + if (_triangleBodyHandles != null) + { + foreach (var triangleHandle in _triangleBodyHandles) + { + _simulationManager.RemoveMotorsConnectedToBody(triangleHandle); + } + } + } + + // Limpiar la lista local de botellas + BottlesOnCurve.Clear(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[simCurve] Error removing connected motors: {ex.Message}"); + } + } - public void Create(float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 topLeft, float unused = 0) + public void Create(float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 wpfTopLeft, float unused = 0) { // ✅ CORRIGIDO: Como Aether - startAngle y endAngle definen directamente el sector // No hay rotación separada del objeto @@ -747,20 +1138,15 @@ namespace CtrEditor.Simulacion _startAngle = GradosARadianes(startAngle); _endAngle = GradosARadianes(endAngle); - // Calcular el offset del centro desde Top-Left + // ✅ USAR COORDINATECONVERTER para conversión centralizada // Para curvas, el "tamaño" es el diámetro del radio exterior var curveSize = outerRadius * 2f; - var offsetX = curveSize / 2f; - var offsetY = curveSize / 2f; - + var zPosition = zAltura_Curve / 2f + zPos_Curve; + // Calcular nueva posición del centro (sin rotación - el sector ya está definido por los ángulos) - var centerX = topLeft.X + offsetX; - var centerY = topLeft.Y + offsetY; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, curveSize, curveSize, 0f, zPosition); - // Convertir a 3D e invertir Y - Curvas en el mismo nivel que transportes - var center3D = new Vector3(centerX, -centerY, zAltura_Curve / 2f + zPos_Curve); - - Create(center3D); // Sin rotación adicional + Create(bepuCenter); // Sin rotación adicional } private void Create(Vector3 position) @@ -935,6 +1321,13 @@ namespace CtrEditor.Simulacion return Vector3.Zero; } } + + // ✅ MÉTODO MEJORADO - versión optimizada + public Vector3 GetCachedTangentialDirection(Vector3 bottlePosition) + { + var direction = GetTangentialDirection(bottlePosition); + return Speed < 0 ? -direction : direction; + } } public class simDescarte : simBase @@ -1031,118 +1424,77 @@ namespace CtrEditor.Simulacion public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, ref TManifold manifold, out PairMaterialProperties pairMaterial) where TManifold : unmanaged, IContactManifold { - // Configuración de materiales físicos por defecto + // ✅ SIMPLIFICADO - configuración básica de materiales físicos pairMaterial = new PairMaterialProperties { - FrictionCoefficient = 0.6f, // Fricción moderada + FrictionCoefficient = 0.6f, // Fricción moderada por defecto MaximumRecoveryVelocity = 2f, // Velocidad máxima de recuperación - SpringSettings = new SpringSettings(60, 4) // Rigidez y amortiguamiento para colisiones sólidas + SpringSettings = new SpringSettings(60, 4) // Rigidez y amortiguamiento estándar }; - // Detectar contactos que involucren botellas para aplicar propiedades especiales if (_simulationManager != null) { var botellaA = GetBotellaFromCollidable(pair.A); var botellaB = GetBotellaFromCollidable(pair.B); - var transportA = GetTransportFromCollidable(pair.A); - var transportB = GetTransportFromCollidable(pair.B); var barreraA = GetBarreraFromCollidable(pair.A); var barreraB = GetBarreraFromCollidable(pair.B); - var curveA = GetCurveFromCollidable(pair.A); - var curveB = GetCurveFromCollidable(pair.B); - var botella = botellaA ?? botellaB; - - // Detectar contactos con descarte var descarteA = GetDescarteFromCollidable(pair.A); var descarteB = GetDescarteFromCollidable(pair.B); + var botella = botellaA ?? botellaB; - // IMPORTANTE: Si hay una barrera involucrada, NO generar contacto físico + // ✅ CONSERVAR - barreras como sensores puros if (barreraA != null || barreraB != null) { - // Ya no necesitamos registrar contactos - usamos detección geométrica pura - // Las barreras son sensores completamente virtuales - - // NO generar contacto físico para barreras - son sensores puros - return false; + return false; // NO generar contacto físico para barreras } - // Si hay un descarte involucrado, NO generar contacto físico pero registrar para eliminación + // ✅ CONSERVAR - descartes como sensores puros if (descarteA != null || descarteB != null) { var descarte = descarteA ?? descarteB; - - // Registrar la botella para eliminación si hay contacto if (botella != null) { _simulationManager.RegisterDescarteContact(descarte, botella); } - - // NO generar contacto físico para descartes - son sensores puros - return false; + return false; // NO generar contacto físico para descartes } - // Si hay una botella en contacto (pero no con barrera), aplicar mayor fricción para estabilidad - if (botella != null) + // ✅ PROCESAR CREACIÓN DE MOTORES para transportes y curvas + var transportA = GetTransportFromCollidable(pair.A); + var transportB = GetTransportFromCollidable(pair.B); + var curveA = GetCurveFromCollidable(pair.A); + var curveB = GetCurveFromCollidable(pair.B); + + // ✅ RESTAURADO - llamar métodos públicos del SimulationManager + // Si hay contacto botella-transporte, crear motor LinearAxisMotor + if (botella != null && (transportA != null || transportB != null)) { - // Aumentar mucho la fricción para botellas (más estables) + var transport = transportA ?? transportB; + _simulationManager.TryCreateTransportMotor(botella, transport); // ✅ Llamar método público + + // Fricción alta para transportes pairMaterial.FrictionCoefficient = 0.9f; - - // Reducir la velocidad de recuperación para evitar rebotes excesivos pairMaterial.MaximumRecoveryVelocity = 1f; - - // Hacer las colisiones más suaves y estables pairMaterial.SpringSettings = new SpringSettings(80, 6); } - - // Si hay un transporte y una botella en contacto - if ((transportA != null || transportB != null) && botella != null) - { - var transporte = transportA ?? transportB; - - // ✅ NUEVA LÓGICA: Detectar contactos con transporte de freno PRIMERO - if (transporte.isBrake) - { - // FRICCIÓN EXTREMA para transporte con freno - simula frenos mecánicos - pairMaterial.FrictionCoefficient = 5.0f; // Fricción extremadamente alta - - // Reducir velocidad de recuperación para evitar rebotes - pairMaterial.MaximumRecoveryVelocity = 0.1f; - - // Hacer el contacto más rígido para mejor control - pairMaterial.SpringSettings = new SpringSettings(200, 20); - - // ✅ APLICAR FUERZAS DE FRENADO DIRECTAMENTE EN EL MANIFOLD - ApplyBrakeForces(ref manifold, botella, transporte); - - // Registrar contacto con transporte de freno - _simulationManager.RegisterBrakeTransportContact(botella, transporte); - } - else - { - // ✅ NUEVA LÓGICA: Aplicar fuerzas de transporte DIRECTAMENTE en el manifold - if (Math.Abs(transporte.Speed) > 0.001f) // Solo si hay velocidad significativa - { - pairMaterial.FrictionCoefficient = 1.2f; // Fricción muy alta - - // ✅ APLICAR FUERZAS DE TRANSPORTE DIRECTAMENTE EN EL MANIFOLD - ApplyTransportForces(ref manifold, botella, transporte); - } - } - } - - // Si hay una curva y una botella en contacto - if ((curveA != null || curveB != null) && botella != null) + // Si hay contacto botella-curva, crear motor LinearAxisMotor + else if (botella != null && (curveA != null || curveB != null)) { var curve = curveA ?? curveB; - - // Solo aplicar si hay velocidad significativa en la curva - if (Math.Abs(curve.Speed) > 0.001f) - { - pairMaterial.FrictionCoefficient = 1.0f; // Fricción alta para curvas - - // Aplicar fuerzas de curva directamente en el manifold - ApplyCurveForces(ref manifold, botella, curve); - } + _simulationManager.TryCreateCurveMotor(botella, curve); // ✅ Llamar método público + + // Fricción alta para curvas + pairMaterial.FrictionCoefficient = 0.9f; + pairMaterial.MaximumRecoveryVelocity = 1f; + pairMaterial.SpringSettings = new SpringSettings(80, 6); + } + // Solo ajustes básicos de fricción para otras botellas + else if (botella != null) + { + // Fricción alta para mayor estabilidad de botellas + pairMaterial.FrictionCoefficient = 0.9f; + pairMaterial.MaximumRecoveryVelocity = 1f; + pairMaterial.SpringSettings = new SpringSettings(80, 6); } } @@ -1223,165 +1575,7 @@ namespace CtrEditor.Simulacion /// /// ✅ NUEVO MÉTODO: Aplicar fuerzas de frenado directamente en el manifold /// - private void ApplyBrakeForces(ref TManifold manifold, simBotella botella, simTransporte transporte) - where TManifold : unmanaged, IContactManifold - { - try - { - if (_simulationManager?.simulation == null) return; - var botellaBody = _simulationManager.simulation.Bodies.GetBodyReference(botella.BodyHandle); - var transporteBody = _simulationManager.simulation.Bodies.GetBodyReference(transporte.BodyHandle); - - // Calcular velocidad relativa - var relativeVelocity = botellaBody.Velocity.Linear - transporteBody.Velocity.Linear; - - // Si la botella se mueve más rápido que el transporte, aplicar fuerza de frenado - var transportDirection = GetTransportDirection(transporte); - var velocityInTransportDirection = Vector3.Dot(relativeVelocity, transportDirection); - - // if (Math.Abs(velocityInTransportDirection) > Math.Abs(transporte.Speed / 18.5f)) - // { - // Calcular fuerza de frenado proporcional a la diferencia de velocidad - var targetSpeed = transporte.Speed / 18.5f; - var currentSpeed = Vector3.Dot(botellaBody.Velocity.Linear, transportDirection); - var speedDifference = currentSpeed - targetSpeed; - - // Aplicar impulso de frenado - esto se hace DURANTE la resolución de colisiones - var brakeImpulse = -transportDirection * speedDifference * botella.Mass * 0.5f; - botellaBody.ApplyLinearImpulse(brakeImpulse); - // } - } - catch (Exception ex) - { - // Error applying brake forces during collision resolution - } - } - - /// - /// ✅ NUEVO MÉTODO: Obtener dirección del transporte - /// - private Vector3 GetTransportDirection(simTransporte transporte) - { - var angle = transporte.GetRotationZ(); - return new Vector3((float)Math.Cos(angle), (float)Math.Sin(angle), 0); - } - - /// - /// ✅ NUEVO MÉTODO: Aplicar fuerzas de transporte normal directamente en el manifold - /// - private void ApplyTransportForces(ref TManifold manifold, simBotella botella, simTransporte transporte) - where TManifold : unmanaged, IContactManifold - { - try - { - if (_simulationManager?.simulation == null) return; - - var botellaBody = _simulationManager.simulation.Bodies.GetBodyReference(botella.BodyHandle); - var transporteBody = _simulationManager.simulation.Bodies.GetBodyReference(transporte.BodyHandle); - - // Calcular la dirección del transporte - var transportDirection = GetTransportDirection(transporte); - - // Calcular la fuerza proporcional a la velocidad deseada - var targetVelocity = transportDirection * transporte.Speed / 18.5f; - var currentVelocity = botellaBody.Velocity.Linear; - - // Solo aplicar fuerza en el plano horizontal (X-Y) - var horizontalVelocity = new Vector3(currentVelocity.X, currentVelocity.Y, 0); - - // Para transportes con freno (si aplica), limitar la velocidad máxima de la botella - bool isOnBrakeTransport = botella.isOnBrakeTransport; - if (isOnBrakeTransport) - { - var transportSpeed = Math.Abs(transporte.Speed / 18.5f); - var currentSpeed = horizontalVelocity.Length(); - - // Si la botella va más rápido que el transporte, limitar su velocidad - if (currentSpeed > transportSpeed * 1.1f) // 10% de tolerancia - { - var limitedVelocity = Vector3.Normalize(horizontalVelocity) * transportSpeed; - botellaBody.Velocity.Linear = new Vector3(limitedVelocity.X, limitedVelocity.Y, currentVelocity.Z); - horizontalVelocity = limitedVelocity; - } - } - - var velocityDiff = targetVelocity - horizontalVelocity; - - // Aplicar una fuerza proporcional a la diferencia de velocidad - var forceMultiplier = isOnBrakeTransport ? 3.0f : 1.0f; // 3x más fuerte para botellas con guías - var baseForceMagnitude = velocityDiff.Length() * botella.Mass * 10f; // Factor base - var forceMagnitude = baseForceMagnitude * forceMultiplier; // Aplicar multiplicador - - if (forceMagnitude > 0.01f) - { - var maxForce = botella.Mass * 50f * forceMultiplier; // Límite también escalado - var force = Vector3.Normalize(velocityDiff) * Math.Min(forceMagnitude, maxForce); - - // Aplicar impulso directamente durante la resolución de colisiones - botellaBody.ApplyLinearImpulse(force * (1f / 60f)); // Simular deltaTime típico - } - } - catch (Exception ex) - { - // Error applying transport forces during collision resolution - } - } - - /// - /// ✅ SIMPLIFICADO: Aplicar fuerzas de curva directamente en el manifold - /// Si hay colisión física, la botella está sobre la curva - no necesita cálculos adicionales - /// - private void ApplyCurveForces(ref TManifold manifold, simBotella botella, simCurve curve) - where TManifold : unmanaged, IContactManifold - { - try - { - if (_simulationManager?.simulation == null) return; - - var botellaBody = _simulationManager.simulation.Bodies.GetBodyReference(botella.BodyHandle); - var botellaPosition = botellaBody.Pose.Position; - - // ✅ SIMPLIFICADO: Si hay colisión física, aplicar fuerzas directamente - // Obtener la dirección tangencial - var tangentialDirection = curve.GetTangentialDirection(botellaPosition); - - if (tangentialDirection.Length() > 0.001f) - { - // Calcular velocidad objetivo - var speedMetersPerSecond = Math.Abs(curve.Speed) / 18.5f; // Convertir a m/s - var targetVelocity = tangentialDirection * speedMetersPerSecond; - - // Obtener velocidad actual - var currentVelocity = botellaBody.Velocity.Linear; - var horizontalVelocity = new Vector3(currentVelocity.X, currentVelocity.Y, 0); - - // Usar directamente la dirección tangencial (sin interpolación compleja) - var newDirection = tangentialDirection; - - // Usar la velocidad mayor entre la actual y la de la curva para continuidad - var currentSpeed = horizontalVelocity.Length(); - var targetSpeed = Math.Max(currentSpeed, speedMetersPerSecond); - - // Calcular nueva velocidad - var newVelocity = newDirection * targetSpeed; - var velocityDiff = newVelocity - horizontalVelocity; - - // Aplicar impulso directamente (sin factor de overlap - la colisión es suficiente) - var forceMagnitude = velocityDiff.Length() * botella.Mass * 8f; - if (forceMagnitude > 0.01f) - { - var maxForce = botella.Mass * 40f; - var force = Vector3.Normalize(velocityDiff) * Math.Min(forceMagnitude, maxForce); - botellaBody.ApplyLinearImpulse(force * (1f / 60f)); // Simular deltaTime típico - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[simCurve] Error applying curve forces: {ex.Message}"); - } - } public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB, ref ConvexContactManifold manifold) { @@ -1432,11 +1626,8 @@ namespace CtrEditor.Simulacion var gravityWide = Vector3Wide.Broadcast(Gravity); velocity.Linear += gravityWide * dt; - // ✅ NUEVA FUNCIONALIDAD: Aplicar fuerzas de frenado durante la integración - if (_simulationManager != null) - { - ApplyBrakeForcesInIntegration(bodyIndices, ref velocity, dt, workerIndex); - } + // ✅ ELIMINADO COMPLETAMENTE - ya no se necesita lógica especial de frenado + // El sistema LinearAxisMotor maneja automáticamente todas las fuerzas // Aplicar amortiguamiento lineal y angular para simular resistencia del aire // Esto es crucial para que los cilindros se detengan de forma realista @@ -1447,31 +1638,296 @@ namespace CtrEditor.Simulacion velocity.Angular *= angularDampingWide; // ¡Esto es clave para detener rotaciones infinitas! } - /// - /// ✅ NUEVO MÉTODO: Aplicar fuerzas de frenado durante la integración de velocidades - /// ✅ SIMPLIFICADO: Solo aplicar amortiguamiento extra para botellas en transportes con freno - /// - private void ApplyBrakeForcesInIntegration(Vector bodyIndices, ref BodyVelocityWide velocity, Vector dt, int workerIndex) + + } + + public class MotorManager + { + private SimulationManagerBEPU _simulationManager; + private Dictionary _activeMotors; + private Dictionary _motorToBottle; + + public MotorManager(SimulationManagerBEPU simulationManager) + { + _simulationManager = simulationManager; + _activeMotors = new Dictionary(); + _motorToBottle = new Dictionary(); + } + + public void CreateTransportMotor(simBotella bottle, simTransporte transport) { try { - // ✅ SIMPLIFICADO: Aplicar amortiguamiento extra para efectos de frenado - // Esto es más compatible con la arquitectura vectorizada de BEPU - - // Amortiguamiento adicional para simular efecto de frenado - var brakeDampingWide = Vector.One * 0.95f; // Más agresivo que el damping normal - - // Aplicar solo si tenemos referencia al simulation manager - if (_simulationManager != null) + // ✅ VALIDACIONES CRÍTICAS + if (bottle == null || transport == null || _simulationManager?.simulation == null) + return; + + // Validar que los BodyHandle existan + if (!_simulationManager.simulation.Bodies.BodyExists(bottle.BodyHandle) || + !_simulationManager.simulation.Bodies.BodyExists(transport.BodyHandle)) + return; + + // Asegurar que DirectionVector esté inicializado + transport.UpdateCachedProperties(); + + // Validar que DirectionVector no sea cero + if (transport.DirectionVector.Length() < 0.001f) + return; + + // ✅ SIMPLIFICAR - Usar LinearAxisMotor básico + var motor = new LinearAxisMotor() + { + LocalOffsetA = Vector3.Zero, + LocalOffsetB = Vector3.Zero, + LocalAxis = transport.DirectionVector, + TargetVelocity = transport.SpeedMetersPerSecond, + Settings = new MotorSettings(Math.Max(bottle.Mass * 30f, 10f), 5f) + }; + + var motorHandle = _simulationManager.simulation.Solver.Add( + transport.BodyHandle, + bottle.BodyHandle, + motor + ); + + _activeMotors[bottle.BodyHandle] = motorHandle; + _motorToBottle[motorHandle] = bottle; + bottle.AssignToTransport(transport, motorHandle); + transport.BottlesOnTransport.Add(bottle); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MotorManager] Error creating transport motor: {ex.Message}"); + } + } + + public void CreateCurveMotor(simBotella bottle, simCurve curve) + { + try + { + // ✅ VALIDACIONES CRÍTICAS + if (bottle == null || curve == null || _simulationManager?.simulation == null) + return; + + // Validar que los BodyHandle existan + if (!_simulationManager.simulation.Bodies.BodyExists(bottle.BodyHandle) || + !_simulationManager.simulation.Bodies.BodyExists(curve.BodyHandle)) + return; + + var direction = curve.GetCachedTangentialDirection(bottle.GetPosition()); + + // Validar que la dirección no sea cero + if (direction.Length() < 0.001f) + return; + + // ✅ USAR CUERPO DE LA CURVA EN LUGAR DE REFERENCIA ESTÁTICA + var motor = new LinearAxisMotor() + { + LocalOffsetA = Vector3.Zero, + LocalOffsetB = Vector3.Zero, + LocalAxis = direction, + TargetVelocity = curve.SpeedMetersPerSecond, + Settings = new MotorSettings(Math.Max(bottle.Mass * 20f, 8f), 4f) + }; + + var motorHandle = _simulationManager.simulation.Solver.Add( + curve.BodyHandle, // ✅ USAR CUERPO DE LA CURVA + bottle.BodyHandle, + motor + ); + + _activeMotors[bottle.BodyHandle] = motorHandle; + _motorToBottle[motorHandle] = bottle; + bottle.CurrentMotor = motorHandle; + curve.BottlesOnCurve.Add(bottle); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MotorManager] Error creating curve motor: {ex.Message}"); + } + } + + public void RemoveMotor(simBotella bottle) + { + try + { + if (bottle == null || _simulationManager?.simulation == null) + return; + + if (_activeMotors.TryGetValue(bottle.BodyHandle, out var motorHandle)) { - // El amortiguamiento extra se aplica globalmente, - // las fuerzas específicas se manejan en ApplyBrakeForces del NarrowPhaseCallbacks - velocity.Linear *= brakeDampingWide; + // Validar que el solver existe antes de remover + if (_simulationManager.simulation.Solver != null) + { + _simulationManager.simulation.Solver.Remove(motorHandle); + } + + _activeMotors.Remove(bottle.BodyHandle); + _motorToBottle.Remove(motorHandle); + + // Limpiar referencias de forma segura + bottle.CurrentTransport?.BottlesOnTransport.Remove(bottle); + bottle.RemoveFromTransport(); } } catch (Exception ex) { - // Error during brake force integration + System.Diagnostics.Debug.WriteLine($"[MotorManager] Error removing motor: {ex.Message}"); + } + } + + public void UpdateTransportSpeed(simTransporte transport) + { + // TODO: Implementar actualización de velocidad cuando se resuelva la API correcta + // Por ahora, recreamos los motores como alternativa + foreach (var bottle in transport.BottlesOnTransport.ToList()) + { + if (_activeMotors.ContainsKey(bottle.BodyHandle)) + { + RemoveMotor(bottle); + CreateTransportMotor(bottle, transport); + } + } + } + + public void UpdateCurveSpeed(simCurve curve) + { + // ✅ Las curvas requieren recrear motores cuando cambia la velocidad + // Esto es porque la dirección tangencial puede cambiar y es más eficiente recrear + foreach (var bottle in curve.BottlesOnCurve.ToList()) + { + if (_activeMotors.ContainsKey(bottle.BodyHandle)) + { + RemoveMotor(bottle); + CreateCurveMotor(bottle, curve); + } + } + } + + public void UpdateCurveMotorDirection(simBotella bottle, simCurve curve) + { + // TODO: Implementar actualización de dirección cuando se resuelva la API correcta + // Por ahora, recreamos el motor como alternativa para curvas + if (_activeMotors.ContainsKey(bottle.BodyHandle)) + { + RemoveMotor(bottle); + CreateCurveMotor(bottle, curve); + } + } + + /// + /// ✅ NUEVO: Elimina todos los motors conectados a un body específico (transportes y curvas) + /// + public void RemoveMotorsByBodyHandle(BodyHandle bodyHandle) + { + try + { + if (_simulationManager?.simulation?.Solver == null) + return; + + // Encontrar todos los motors que involucran este body + var motorsToRemove = new List<(BodyHandle bottleHandle, ConstraintHandle motorHandle)>(); + + foreach (var kvp in _activeMotors) + { + var bottleHandle = kvp.Key; + var motorHandle = kvp.Value; + + // Verificar si esta botella tiene un motor conectado al body que se va a eliminar + if (_motorToBottle.TryGetValue(motorHandle, out var bottle)) + { + bool shouldRemove = false; + + // Verificar transporte + if (bottle.CurrentTransport?.BodyHandle.Equals(bodyHandle) == true) + { + shouldRemove = true; + } + + // Verificar curvas (buscar en el cuerpo principal y triángulos) + if (!shouldRemove) + { + var curves = _simulationManager.Cuerpos.OfType(); + foreach (var curve in curves) + { + // Verificar cuerpo principal de la curva + if (curve.BodyHandle.Equals(bodyHandle)) + { + shouldRemove = true; + break; + } + + // Verificar triángulos de la curva + if (curve._triangleBodyHandles != null && + curve._triangleBodyHandles.Contains(bodyHandle)) + { + shouldRemove = true; + break; + } + } + } + + if (shouldRemove) + { + motorsToRemove.Add((bottleHandle, motorHandle)); + } + } + } + + // Eliminar todos los motors encontrados + foreach (var (bottleHandle, motorHandle) in motorsToRemove) + { + try + { + _simulationManager.simulation.Solver.Remove(motorHandle); + _activeMotors.Remove(bottleHandle); + + if (_motorToBottle.TryGetValue(motorHandle, out var bottle)) + { + bottle.RemoveFromTransport(); // Esto limpia tanto transporte como curva + _motorToBottle.Remove(motorHandle); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MotorManager] Error removing specific motor for body {bodyHandle}: {ex.Message}"); + } + } + + System.Diagnostics.Debug.WriteLine($"[MotorManager] Removed {motorsToRemove.Count} motors connected to body {bodyHandle}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MotorManager] Error in RemoveMotorsByBodyHandle: {ex.Message}"); + } + } + + + + public void Clear() + { + try + { + if (_simulationManager?.simulation?.Solver != null) + { + foreach (var motorHandle in _activeMotors.Values) + { + try + { + _simulationManager.simulation.Solver.Remove(motorHandle); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MotorManager] Error removing motor during clear: {ex.Message}"); + } + } + } + _activeMotors.Clear(); + _motorToBottle.Clear(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MotorManager] Error during clear: {ex.Message}"); } } } @@ -1491,16 +1947,31 @@ namespace CtrEditor.Simulacion // Propiedad para controlar si la actualización 3D está habilitada public bool Is3DUpdateEnabled { get; set; } = true; - // Sistema de contactos de transporte para aplicar fuerzas - private Dictionary _transportContacts; - // Sistema de contactos de barrera para detección de paso + // ✅ NUEVAS - para filtrado eficiente de callbacks + private HashSet _transportHandles; + private HashSet _curveHandles; + private HashSet _barrierHandles; + private HashSet _discardHandles; + private HashSet _bottleHandles; + + // ✅ NUEVO - sistema de motores + private MotorManager _motorManager; + + // ✅ NUEVO - tabla de exclusiones para callbacks silenciados + private HashSet<(BodyHandle bottle, BodyHandle transport)> _silencedPairs; + + // ✅ NUEVO - contador de frames para optimizaciones + private int _frameCount = 0; + + // ✅ CONSERVAR - sistemas existentes que funcionan bien private Dictionary> _barreraContacts; - // Sistema de contactos de descarte para marcar botellas para eliminación private Dictionary> _descarteContacts; - // Lista de botellas marcadas para eliminación private HashSet _botellasParaEliminar; - // Sistema de contactos de transporte con freno para marcar botellas - private Dictionary _brakeTransportContacts; + + // ✅ ELIMINAR COMPLETAMENTE + // private Dictionary _transportContacts; // NO SE USA MÁS + // private Dictionary _brakeTransportContacts; // NO SE USA MÁS + private object _contactsLock = new object(); /// @@ -1532,15 +2003,121 @@ namespace CtrEditor.Simulacion return simBase as simCurve; } + // ✅ NUEVOS - gestión automática de clasificación + private void RegisterObjectHandle(simBase obj) + { + switch (obj) + { + case simBotella bottle: + _bottleHandles.Add(bottle.BodyHandle); + break; + case simTransporte transport: + _transportHandles.Add(transport.BodyHandle); + // Suscribirse a cambios de velocidad + transport.OnSpeedChanged += _motorManager.UpdateTransportSpeed; + // Calcular propiedades iniciales + transport.UpdateCachedProperties(); + break; + case simCurve curve: + _curveHandles.Add(curve.BodyHandle); + // También añadir todos sus triángulos + if (curve._triangleBodyHandles != null) + { + foreach (var triangleHandle in curve._triangleBodyHandles) + { + _curveHandles.Add(triangleHandle); + } + } + curve.OnSpeedChanged += _motorManager.UpdateCurveSpeed; + break; + case simBarrera barrier: + _barrierHandles.Add(barrier.BodyHandle); + break; + case simDescarte discard: + _discardHandles.Add(discard.BodyHandle); + break; + } + } + + private void UnregisterObjectHandle(simBase obj) + { + switch (obj) + { + case simBotella bottle: + _bottleHandles.Remove(bottle.BodyHandle); + // Eliminar motor si existe + _motorManager.RemoveMotor(bottle); + // Eliminar de pares silenciados + RemoveFromSilencedPairs(bottle.BodyHandle); + break; + case simTransporte transport: + _transportHandles.Remove(transport.BodyHandle); + transport.OnSpeedChanged -= _motorManager.UpdateTransportSpeed; + break; + case simCurve curve: + _curveHandles.Remove(curve.BodyHandle); + if (curve._triangleBodyHandles != null) + { + foreach (var triangleHandle in curve._triangleBodyHandles) + { + _curveHandles.Remove(triangleHandle); + } + } + curve.OnSpeedChanged -= _motorManager.UpdateCurveSpeed; + break; + case simBarrera barrier: + _barrierHandles.Remove(barrier.BodyHandle); + break; + case simDescarte discard: + _discardHandles.Remove(discard.BodyHandle); + break; + } + } + + private void RemoveFromSilencedPairs(BodyHandle bottleHandle) + { + _silencedPairs.RemoveWhere(pair => pair.bottle == bottleHandle); + } + + // ✅ NUEVOS MÉTODOS - obtener objetos por handle + private simBotella GetBottleByHandle(BodyHandle handle) + { + return Cuerpos.OfType().FirstOrDefault(b => b.BodyHandle.Equals(handle)); + } + + private simTransporte GetTransportByHandle(BodyHandle handle) + { + return Cuerpos.OfType().FirstOrDefault(t => t.BodyHandle.Equals(handle)); + } + + private simCurve GetCurveByHandle(BodyHandle handle) + { + // Primero buscar en cuerpos principales + var curve = Cuerpos.OfType().FirstOrDefault(c => c.BodyHandle.Equals(handle)); + if (curve != null) return curve; + + // Luego buscar en triángulos + return GetCurveFromTriangleBodyHandle(handle); + } + public SimulationManagerBEPU() { + // ✅ EXISTENTES - conservar Cuerpos = new List(); _deferredActions = new List(); - _transportContacts = new Dictionary(); _barreraContacts = new Dictionary>(); _descarteContacts = new Dictionary>(); _botellasParaEliminar = new HashSet(); - _brakeTransportContacts = new Dictionary(); + + // ✅ NUEVOS - sistemas de filtrado y motores + _transportHandles = new HashSet(); + _curveHandles = new HashSet(); + _barrierHandles = new HashSet(); + _discardHandles = new HashSet(); + _bottleHandles = new HashSet(); + _silencedPairs = new HashSet<(BodyHandle, BodyHandle)>(); + + // ✅ CONSERVAR - resto del constructor igual bufferPool = new BufferPool(); stopwatch = new Stopwatch(); @@ -1560,36 +2137,41 @@ namespace CtrEditor.Simulacion var solveDescription = new SolveDescription(8, 1); simulation = Simulation.Create(bufferPool, narrowPhaseCallbacks, poseIntegratorCallbacks, solveDescription); + + // ✅ CREAR MOTOR MANAGER DESPUÉS DE LA SIMULACIÓN + _motorManager = new MotorManager(this); } public void Clear() { try { - // ✅ NUEVO: Restaurar masas originales de todas las botellas antes de limpiar - foreach (var cuerpo in Cuerpos.OfType()) - { - try - { - cuerpo.RestoreOriginalMassIfNeeded(); - } - catch (Exception ex) - { - // Error restoring mass during clear - continue - } - } - - // Limpiar contactos primero para evitar referencias colgantes + // ✅ NUEVO - limpiar motor manager + _motorManager?.Clear(); + + // ✅ SIMPLIFICAR - eliminar lógica de masa especial + // foreach (var cuerpo in Cuerpos.OfType()) + // { + // cuerpo.RestoreOriginalMassIfNeeded(); // NO SE USA MÁS + // } + + // ✅ CONSERVAR - limpiar contactos lock (_contactsLock) { - _transportContacts.Clear(); _barreraContacts.Clear(); _descarteContacts.Clear(); _botellasParaEliminar.Clear(); - _brakeTransportContacts.Clear(); + _silencedPairs.Clear(); // ✅ NUEVO } + + // ✅ NUEVO - limpiar clasificaciones + _transportHandles.Clear(); + _curveHandles.Clear(); + _barrierHandles.Clear(); + _discardHandles.Clear(); + _bottleHandles.Clear(); - // Remover cuerpos de forma segura + // ✅ CONSERVAR - resto del método igual var cuerposToRemove = new List(Cuerpos); foreach (var cuerpo in cuerposToRemove) { @@ -1608,6 +2190,9 @@ namespace CtrEditor.Simulacion Cuerpos.Clear(); _deferredActions.Clear(); + + // ✅ NUEVO - Limpiar todas las dimensiones almacenadas + CtrEditor.ObjetosSim.osBase.ClearAllStoredDimensions(); // Limpiar la simulación completamente si existe if (simulation != null) @@ -1634,45 +2219,66 @@ namespace CtrEditor.Simulacion public void Start() { - stopwatch.Start(); - stopwatch_last = stopwatch.Elapsed.TotalMilliseconds; + try + { + // ✅ INICIALIZAR PROPIEDADES CACHEADAS + foreach (var transport in Cuerpos.OfType()) + { + transport.UpdateCachedProperties(); + } + + foreach (var curve in Cuerpos.OfType()) + { + // ✅ CORREGIDO: Usar método público para actualizar velocidad + curve.SetSpeed(curve.Speed); // Esto actualiza automáticamente SpeedMetersPerSecond internamente + } + + stopwatch.Start(); + stopwatch_last = stopwatch.Elapsed.TotalMilliseconds; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[SimulationManager] Error in Start: {ex.Message}"); + stopwatch.Start(); + stopwatch_last = stopwatch.Elapsed.TotalMilliseconds; + } } public void Step() { try { + _frameCount++; + + // ✅ CONSERVAR - validación de deltaTime var currentTime = stopwatch.Elapsed.TotalMilliseconds; var deltaTime = (float)((currentTime - stopwatch_last) / 1000.0); stopwatch_last = currentTime; - // Validar deltaTime para evitar valores problemáticos if (float.IsNaN(deltaTime) || float.IsInfinity(deltaTime) || deltaTime <= 0) { - deltaTime = 1f / 60f; // Fallback a 60 FPS + deltaTime = 1f / 60f; } - // Limitar deltaTime a 100ms máximo para evitar saltos tras pausas del debugger - const float maxDeltaTime = 0.1f; // 100ms + const float maxDeltaTime = 0.1f; if (deltaTime > maxDeltaTime) { - deltaTime = 1f / 60f; // Resetear a 60 FPS estándar si excede el límite + deltaTime = 1f / 60f; } - // Procesar acciones diferidas + // ✅ CONSERVAR - procesar acciones diferidas foreach (var action in _deferredActions) { action(); } _deferredActions.Clear(); - // Validar que la simulación esté en buen estado antes del timestep + // ✅ CONSERVAR - validaciones de simulación if (simulation?.Bodies == null) { return; } - // Validar que no hay cuerpos con handles inválidos var invalidBodies = Cuerpos.Where(c => c != null && !simulation.Bodies.BodyExists(c.BodyHandle)).ToList(); if (invalidBodies.Count > 0) { @@ -1682,8 +2288,8 @@ namespace CtrEditor.Simulacion } } - // Paso de simulación (genera las colisiones/contactos) - var timestepValue = Math.Max(deltaTime, 1f / 120f); // Mínimo 120 FPS para mejor estabilidad + // ✅ CONSERVAR - timestep + var timestepValue = Math.Max(deltaTime, 1f / 120f); try { @@ -1691,32 +2297,31 @@ namespace CtrEditor.Simulacion } catch (AccessViolationException ex) { - // Limpiar contactos que podrían estar corruptos lock (_contactsLock) { - // ✅ ELIMINADO: _transportContacts.Clear(); - Ya no se usa _barreraContacts.Clear(); } - throw; // Re-lanzar para debug + throw; } - // ✅ ELIMINADO: ApplyTransportForces - Ahora se aplica directamente en ConfigureContactManifold - // ApplyTransportForces(deltaTime); // ELIMINADO - Ahora en ConfigureContactManifold + // ✅ ELIMINAR COMPLETAMENTE + // ApplyTransportForces(deltaTime); // NO SE USA MÁS + // ProcessBrakeTransportContacts(); // NO SE USA MÁS - // Procesar contactos de transporte con freno para marcar/desmarcar botellas - ProcessBrakeTransportContacts(); + // ✅ NUEVO - verificación periódica de salidas de transporte + CheckBottleExitsFromTransports(); - // Limitar rotaciones de botellas solo al plano XY (siempre "de pie") - // Y GARANTIZAR que nunca entren en sleep mode - foreach (var cuerpo in Cuerpos.OfType().ToList()) // ToList para evitar modificación durante iteración + // ✅ NUEVO - actualización periódica de direcciones de curvas + UpdateCurveMotorDirections(); + + // ✅ CONSERVAR - limitación de rotación y mantener botellas despiertas + foreach (var cuerpo in Cuerpos.OfType().ToList()) { try { if (simulation.Bodies.BodyExists(cuerpo.BodyHandle)) { cuerpo.LimitRotationToXYPlane(); - - // FORZAR que la botella esté siempre despierta - doble seguridad simulation.Awakener.AwakenBody(cuerpo.BodyHandle); } } @@ -1726,25 +2331,19 @@ namespace CtrEditor.Simulacion } } - // Procesar contactos de barrera para detección de paso + // ✅ CONSERVAR - sistemas que funcionan bien ProcessBarreraContacts(); - - // Procesar contactos de descarte para eliminación de botellas ProcessDescarteContacts(); - - // Procesar sistema de limpieza para eliminar botellas debajo de transportes ProcessCleanupSystem(); - // Limpiar contactos DESPUÉS de usarlos + // ✅ SIMPLIFICAR - limpiar solo contactos que usamos lock (_contactsLock) { - // ✅ ELIMINADO: _transportContacts.Clear(); - Ya no se usa _barreraContacts.Clear(); _descarteContacts.Clear(); - _brakeTransportContacts.Clear(); } - // Sincronizar con la visualización 3D solo si está habilitado + // ✅ CONSERVAR - sincronización 3D if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); @@ -1752,38 +2351,88 @@ namespace CtrEditor.Simulacion } catch (Exception ex) { - // En caso de error crítico, limpiar contactos para evitar estado corrupto lock (_contactsLock) { - // ✅ ELIMINADO: _transportContacts.Clear(); - Ya no se usa _barreraContacts.Clear(); - _brakeTransportContacts.Clear(); + } + } + } + + private void CheckBottleExitsFromTransports() + { + // ✅ Ejecutar cada 10 frames para eficiencia + if (_frameCount % 10 != 0) return; + + var activeBottles = Cuerpos.OfType() + .Where(b => b.HasActiveMotor && b.CurrentTransport != null) + .ToList(); + + foreach (var bottle in activeBottles) + { + if (!IsBottleOnTransport(bottle, bottle.CurrentTransport)) + { + ProcessBottleExitsTransport(bottle); + } + } + } + + private bool IsBottleOnTransport(simBotella bottle, simTransporte transport) + { + var bottlePos = bottle.GetPosition(); + var transportPos = transport.GetPosition(); + var distance = Vector2.Distance( + new Vector2(bottlePos.X, bottlePos.Y), + new Vector2(transportPos.X, transportPos.Y) + ); + + // Verificar si está dentro del área del transporte + tolerancia + var maxDistance = Math.Max(transport.Width, transport.Height) / 2f + bottle.Radius + 0.1f; + return distance <= maxDistance; + } + + private void ProcessBottleExitsTransport(simBotella bottle) + { + UnsileceCurrentPair(bottle); + _motorManager.RemoveMotor(bottle); + } + + private void UnsileceCurrentPair(simBotella bottle) + { + if (bottle.CurrentTransport != null) + { + _silencedPairs.Remove((bottle.BodyHandle, bottle.CurrentTransport.BodyHandle)); + } + } + + private void UpdateCurveMotorDirections() + { + // ✅ Ejecutar cada 5 frames para eficiencia + if (_frameCount % 5 != 0) return; + + var curvesWithBottles = Cuerpos.OfType() + .Where(c => c.BottlesOnCurve.Count > 0) + .ToList(); + + foreach (var curve in curvesWithBottles) + { + foreach (var bottle in curve.BottlesOnCurve.ToList()) + { + _motorManager.UpdateCurveMotorDirection(bottle, curve); } } } public void Remove(simBase Objeto) { - // ✅ NUEVO: Si es una botella con masa aumentada, restaurar masa original antes de eliminar - if (Objeto is simBotella botella && botella.OriginalMass > 0) - { - try - { - botella.SetMass(botella.OriginalMass); - botella.OriginalMass = 0; - botella.isOnBrakeTransport = false; - botella.CurrentBrakeTransport = null; - } - catch (Exception ex) - { - // Error restoring mass during removal - continue with removal - } - } - + // ✅ SIMPLIFICADO - eliminar lógica de masa especial + UnregisterObjectHandle(Objeto); // ✅ NUEVO + + // ✅ NUEVO - Limpiar dimensiones almacenadas en osBase + CtrEditor.ObjetosSim.osBase.ClearStoredDimensions(Objeto); + Objeto.RemoverBody(); Cuerpos.Remove(Objeto); - // Actualizar visualización 3D tras eliminar objeto if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); @@ -1794,26 +2443,25 @@ namespace CtrEditor.Simulacion { var botella = new simBotella(simulation, _deferredActions, diameter, position, mass); Cuerpos.Add(botella); + RegisterObjectHandle(botella); // ✅ NUEVO - // Sincronizar con la visualización 3D if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } - return botella; } public simTransporte AddRectangle(float width, float height, Vector2 position, float angle) { - var transporte = new simTransporte(simulation, _deferredActions, width, height, position, angle); + var transporte = new simTransporte(simulation, _deferredActions, width, height, position, angle, this); // ✅ PASAR REFERENCIA Cuerpos.Add(transporte); - // Sincronizar con la visualización 3D + RegisterObjectHandle(transporte); // ✅ NUEVO - incluye UpdateCachedProperties() + if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } - return transporte; } @@ -1821,12 +2469,12 @@ namespace CtrEditor.Simulacion { var barrera = new simBarrera(simulation, _deferredActions, width, height, position, angle, detectarCuello); Cuerpos.Add(barrera); - // Sincronizar con la visualización 3D + RegisterObjectHandle(barrera); // ✅ NUEVO + if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } - return barrera; } @@ -1836,12 +2484,12 @@ namespace CtrEditor.Simulacion { var guia = new simGuia(simulation, _deferredActions, width, height, topLeft, angle); Cuerpos.Add(guia); - // Sincronizar con la visualización 3D + // ✅ NOTA: simGuia no requiere registro especial + if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } - return guia; } @@ -1849,28 +2497,36 @@ namespace CtrEditor.Simulacion { var descarte = new simDescarte(simulation, _deferredActions, diameter, position); Cuerpos.Add(descarte); - // Sincronizar con la visualización 3D + RegisterObjectHandle(descarte); // ✅ NUEVO + if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } - return descarte; } public simCurve AddCurve(float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 topLeft, float unused = 0) { - var curve = new simCurve(simulation, _deferredActions, innerRadius, outerRadius, startAngle, endAngle, topLeft, unused); + var curve = new simCurve(simulation, _deferredActions, innerRadius, outerRadius, startAngle, endAngle, topLeft, unused, this); // ✅ PASAR REFERENCIA Cuerpos.Add(curve); - // Sincronizar con la visualización 3D + RegisterObjectHandle(curve); // ✅ NUEVO + if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } - return curve; } + /// + /// ✅ NUEVO: Método público para limpiar motors conectados a un body específico + /// + public void RemoveMotorsConnectedToBody(BodyHandle bodyHandle) + { + _motorManager?.RemoveMotorsByBodyHandle(bodyHandle); + } + public void Dispose() { Clear(); @@ -1878,52 +2534,67 @@ namespace CtrEditor.Simulacion bufferPool?.Clear(); } + + + + /// - /// Registra un contacto entre una botella y un transporte para aplicar fuerzas + /// ✅ IMPLEMENTADO: Intenta crear un motor de transporte si no existe uno activo /// - /// Botella en contacto - /// Transporte en contacto - public void RegisterTransportContact(simBotella botella, simTransporte transporte) + public void TryCreateTransportMotor(simBotella bottle, simTransporte transport) { - if (botella != null && transporte != null) + try { - lock (_contactsLock) - { - _transportContacts[botella] = transporte; - } + // Validaciones básicas + if (bottle == null || transport == null || _motorManager == null) + return; + + // Verificar si ya tiene un motor activo o está silenciado + if (bottle.HasActiveMotor) + return; + + var pairKey = (bottle.BodyHandle, transport.BodyHandle); + if (_silencedPairs.Contains(pairKey)) + return; + + // Asegurar que el transporte tenga propiedades inicializadas + if (Math.Abs(transport.Speed) < 0.001f) + return; // No crear motor para transportes detenidos + + _motorManager.CreateTransportMotor(bottle, transport); + _silencedPairs.Add(pairKey); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[SimulationManager] Error in TryCreateTransportMotor: {ex.Message}"); } } /// - /// Registra un contacto entre una botella y un transporte con freno para marcar la botella - /// ✅ NUEVO: También aumenta la masa de la botella 10x para simular conexión mecánica + /// ✅ IMPLEMENTADO: Intenta crear un motor de curva si no existe uno activo /// - /// Botella en contacto - /// Transporte con freno en contacto - public void RegisterBrakeTransportContact(simBotella botella, simTransporte transporte) + public void TryCreateCurveMotor(simBotella bottle, simCurve curve) { - if (botella != null && transporte != null && transporte.isBrake) + try { - lock (_contactsLock) - { - _brakeTransportContacts[botella] = transporte; - // ✅ NUEVO: Mantener referencia directa en la botella - botella.CurrentBrakeTransport = transporte; - botella.ConveyorRestrictedTo = transporte; - - // ✅ NUEVO: Aumentar masa 10x para simular conexión mecánica con el transporte - if (!botella.isOnBrakeTransport) // Solo si no estaba ya en un transporte con freno - { - // Guardar masa original si no está guardada ya - if (botella.OriginalMass <= 0) - { - botella.OriginalMass = botella.Mass; - } - - // Aumentar masa 10 veces para simular "enganche" mecánico - botella.SetMass(botella.OriginalMass * 100f); - } - } + // Validaciones básicas + if (bottle == null || curve == null || _motorManager == null) + return; + + // Verificar si ya tiene un motor activo + if (bottle.HasActiveMotor) + return; + + // Verificar que la curva tenga velocidad + if (Math.Abs(curve.Speed) < 0.001f) + return; // No crear motor para curvas detenidas + + // ✅ Para curvas no silenciamos porque la dirección cambia constantemente + _motorManager.CreateCurveMotor(bottle, curve); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[SimulationManager] Error in TryCreateCurveMotor: {ex.Message}"); } } @@ -1980,226 +2651,7 @@ namespace CtrEditor.Simulacion } } - /// - /// Aplica fuerzas de transporte a las botellas en contacto - /// - /// Tiempo transcurrido desde el último paso - private void ApplyTransportForces(float deltaTime) - { - try - { - // Validar deltaTime - if (float.IsNaN(deltaTime) || float.IsInfinity(deltaTime) || deltaTime <= 0) - return; - lock (_contactsLock) - { - // Crear copia de contactos para evitar modificación durante iteración - var contactsCopy = new Dictionary(_transportContacts); - - foreach (var contact in contactsCopy) - { - try - { - var botella = contact.Key; - var transporte = contact.Value; - - // Validar objetos - if (botella == null || transporte == null || simulation?.Bodies == null) - continue; - - // Verificar que ambos objetos aún existen - if (!simulation.Bodies.BodyExists(botella.BodyHandle) || !simulation.Bodies.BodyExists(transporte.BodyHandle)) - continue; - - // Solo aplicar si hay velocidad significativa - if (Math.Abs(transporte.Speed) <= 0.001f) - continue; - - // Determinar si es una botella en transporte con freno (guías laterales) - bool isOnBrakeTransport = botella.isOnBrakeTransport; - float forceMultiplier = isOnBrakeTransport ? 3.0f : 1.0f; // 3x más fuerte para botellas con guías - - // Aplicar fuerzas de movimiento (longitudinales) con control de velocidad - ApplyLongitudinalForces(botella, transporte, deltaTime, forceMultiplier, isOnBrakeTransport); - - } - catch (Exception ex) - { - // Error processing contact - continue - } - } - } - } - catch (Exception ex) - { - // Critical error in ApplyTransport - continue - } - } - - /// - /// Aplica fuerzas longitudinales para el movimiento del transporte con control de velocidad - /// - private void ApplyLongitudinalForces(simBotella botella, simTransporte transporte, float deltaTime, float forceMultiplier, bool isOnBrakeTransport) - { - try - { - // Calcular la dirección del transporte de forma segura - var angle = transporte.GetRotationZ(); - - // Validar ángulo - if (float.IsNaN(angle) || float.IsInfinity(angle)) - return; - - var cos = (float)Math.Cos(angle); - var sin = (float)Math.Sin(angle); - - // Validar valores trigonométricos - if (float.IsNaN(cos) || float.IsNaN(sin)) - return; - - // Vector de dirección del transporte (en el plano X-Y) - var transportDirection = new Vector3(cos, sin, 0); - - // Calcular la fuerza proporcional a la velocidad deseada - var targetVelocity = transportDirection * transporte.Speed / 18.5f; - var currentVelocity = botella.GetLinearVelocity(); - - // Validar velocidades - if (float.IsNaN(currentVelocity.X) || float.IsNaN(currentVelocity.Y) || float.IsNaN(currentVelocity.Z)) - return; - - // Solo aplicar fuerza en el plano horizontal (X-Y) - var horizontalVelocity = new Vector3(currentVelocity.X, currentVelocity.Y, 0); - - // Para transportes con freno, limitar la velocidad máxima de la botella - if (isOnBrakeTransport) - { - var transportSpeed = Math.Abs(transporte.Speed / 18.5f); - var currentSpeed = horizontalVelocity.Length(); - - // Si la botella va más rápido que el transporte, limitar su velocidad - if (currentSpeed > transportSpeed * 1f) // 10% de tolerancia - { - var limitedVelocity = Vector3.Normalize(horizontalVelocity) * transportSpeed; - botella.ApplyLinearVelocity(new Vector3(limitedVelocity.X, limitedVelocity.Y, currentVelocity.Z)); - horizontalVelocity = limitedVelocity; - } - } - - var velocityDiff = targetVelocity - horizontalVelocity; - - // Validar diferencia de velocidad - if (float.IsNaN(velocityDiff.X) || float.IsNaN(velocityDiff.Y) || float.IsNaN(velocityDiff.Z)) - return; - - // Aplicar una fuerza proporcional a la diferencia de velocidad - var baseForceMagnitude = velocityDiff.Length() * botella.Mass * 10f; // Factor base - var forceMagnitude = baseForceMagnitude * forceMultiplier; // Aplicar multiplicador - - // Validar magnitud de fuerza - if (float.IsNaN(forceMagnitude) || float.IsInfinity(forceMagnitude)) - return; - - if (forceMagnitude > 0.01f) - { - var maxForce = botella.Mass * 50f * forceMultiplier; // Límite también escalado - var force = Vector3.Normalize(velocityDiff) * Math.Min(forceMagnitude, maxForce); - - // Validar fuerza final - if (float.IsNaN(force.X) || float.IsNaN(force.Y) || float.IsNaN(force.Z)) - return; - - var bodyReference = simulation.Bodies.GetBodyReference(botella.BodyHandle); - bodyReference.ApplyLinearImpulse(force * deltaTime); - } - } - catch (Exception ex) - { - // Error in ApplyLongitudinalForces - continue - } - } - - - /// - /// Procesa contactos de transporte con freno para marcar/desmarcar botellas - /// ✅ NUEVO: También gestiona el cambio de masa - restaura masa original al salir del transporte - /// - private void ProcessBrakeTransportContacts() - { - try - { - // Primero, recopilar botellas que estaban en transporte con freno para detectar cambios - var todasLasBotellas = Cuerpos.OfType().ToList(); - var botellasQueEstabanEnBrakeTransport = new HashSet(); - - foreach (var botella in todasLasBotellas) - { - if (botella != null && botella.isOnBrakeTransport) - { - botellasQueEstabanEnBrakeTransport.Add(botella); - botella.isOnBrakeTransport = false; // Asumir que no están en contacto - } - } - - // Luego, marcar las botellas que SÍ están en contacto con transportes de freno - lock (_contactsLock) - { - var contactsCopy = new Dictionary(_brakeTransportContacts); - - foreach (var contact in contactsCopy) - { - try - { - var botella = contact.Key; - var transporte = contact.Value; - - // Validar objetos - if (botella == null || transporte == null || simulation?.Bodies == null) - continue; - - // Verificar que ambos objetos aún existen - if (!simulation.Bodies.BodyExists(botella.BodyHandle) || !simulation.Bodies.BodyExists(transporte.BodyHandle)) - continue; - - // Verificar que el transporte sigue teniendo freno activado - if (transporte.isBrake) - { - botella.isOnBrakeTransport = true; - // Quitar de la lista de botellas que estaban en brake transport - botellasQueEstabanEnBrakeTransport.Remove(botella); - } - } - catch (Exception ex) - { - // Error processing brake transport contact - continue - } - } - } - - // ✅ NUEVO: Restaurar masa original de botellas que salieron del transporte con freno - foreach (var botella in botellasQueEstabanEnBrakeTransport) - { - try - { - if (botella.OriginalMass > 0) // Solo si tenemos masa original guardada - { - botella.SetMass(botella.OriginalMass); - botella.OriginalMass = 0; // Limpiar para evitar confusión - botella.CurrentBrakeTransport = null; // Limpiar referencia - } - } - catch (Exception ex) - { - // Error restoring original mass - continue - } - } - } - catch (Exception ex) - { - // Critical error in ProcessBrakeTransportContacts - continue - } - } /// /// Procesa todas las barreras usando detección geométrica pura