using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.Windows; using System.Diagnostics; using System; using System.Collections.Generic; using System.Numerics; using System.Linq; using BepuPhysics; using BepuPhysics.Collidables; using BepuPhysics.CollisionDetection; using BepuPhysics.Constraints; using BepuUtilities; using BepuUtilities.Memory; 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) - USO INTERNO /// internal static float WpfAngleToBepuAngle(float wpfAngle) { return -wpfAngle; } /// /// Convierte ángulo de BEPU a WPF (invierte signo) - USO INTERNO /// internal static float BepuAngleToWpfAngle(float bepuAngle) { return -bepuAngle; } /// /// Convierte posición Y de WPF a BEPU (invierte signo) - USO INTERNO /// internal static float WpfYToBepuY(float wpfY) { return -wpfY; } /// /// Convierte posición Y de BEPU a WPF (invierte signo) - RETORNA VALOR WPF /// public static float BepuYToWpfY(float bepuY) { return -bepuY; } /// /// Convierte Vector2 de WPF a BEPU (solo invierte Y) - USO INTERNO /// internal static Vector2 WpfToBepuVector2(Vector2 wpfVector) { return new Vector2(wpfVector.X, -wpfVector.Y); } /// /// Convierte Vector2 de BEPU a WPF (solo invierte Y) - RETORNA VALOR WPF /// 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) - RETORNA VALOR WPF /// 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 - USO INTERNO /// Maneja correctamente la rotación del objeto /// internal static Vector3 CalculateBepuCenterFromWpfTopLeft(Vector2 wpfTopLeft, float width, float height, float wpfAngle, float zPosition) { // Calcular el offset del centro desde Top-Left (sin rotación) var offsetX = width / 2f; var offsetY = height / 2f; // Convertir ángulo WPF a radianes para cálculos trigonométricos var angleRadians = simBase.GradosARadianes(wpfAngle); var cos = (float)Math.Cos(angleRadians); var sin = (float)Math.Sin(angleRadians); // Rotar el offset alrededor del Top-Left usando el ángulo WPF var rotatedOffsetX = offsetX * cos - offsetY * sin; var rotatedOffsetY = offsetX * sin + offsetY * cos; // Calcular nueva posición del centro manteniendo Top-Left fijo var centerX = wpfTopLeft.X + rotatedOffsetX; var centerY = wpfTopLeft.Y + rotatedOffsetY; // Convertir a 3D con Y invertida para BEPU return new Vector3(centerX, WpfYToBepuY(centerY), zPosition); } /// /// Calcula la posición Top-Left WPF desde el centro BEPU - RETORNA VALOR WPF /// 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 - USO INTERNO /// internal 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 - RETORNA VALOR WPF /// 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 - USO INTERNO /// internal static void UpdateBepuBodyPosition(Simulation simulation, BodyHandle bodyHandle, Vector3 newBepuPosition) { if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) { var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); bodyReference.Pose.Position = newBepuPosition; } } /// /// Actualiza la rotación de un body BEPU manteniendo su posición actual - USO INTERNO /// internal static void UpdateBepuBodyRotation(Simulation simulation, BodyHandle bodyHandle, float wpfAngle) { if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) { var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); bodyReference.Pose.Orientation = CreateBepuQuaternionFromWpfAngle(wpfAngle); } } /// /// Actualiza posición y rotación de un body BEPU simultáneamente - USO INTERNO /// internal static void UpdateBepuBodyPose(Simulation simulation, BodyHandle bodyHandle, Vector3 newBepuPosition, float wpfAngle) { if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) { var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); bodyReference.Pose.Position = newBepuPosition; bodyReference.Pose.Orientation = CreateBepuQuaternionFromWpfAngle(wpfAngle); } } /// /// Obtiene la posición del centro en coordenadas BEPU - USO INTERNO /// internal static Vector3 GetBepuBodyPosition(Simulation simulation, BodyHandle bodyHandle) { if (simulation != null && simulation.Bodies.BodyExists(bodyHandle)) { var bodyReference = simulation.Bodies.GetBodyReference(bodyHandle); return bodyReference.Pose.Position; } return Vector3.Zero; } /// /// Obtiene el ángulo WPF desde un body BEPU - RETORNA VALOR WPF /// 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; } /// /// ✅ NUEVO: Convierte directamente de grados WPF a radianes BEPU - USO INTERNO /// Maneja tanto la inversión de signo como la conversión a radianes en una sola operación /// internal static float WpfDegreesToBepuRadians(float wpfDegrees) { return simBase.GradosARadianes(WpfAngleToBepuAngle(wpfDegrees)); } /// /// ✅ NUEVO: Convierte directamente de radianes BEPU a grados WPF - RETORNA VALOR WPF /// Maneja tanto la conversión a grados como la inversión de signo en una sola operación /// public static float BepuRadiansToWpfDegrees(float bepuRadians) { return BepuAngleToWpfAngle(simBase.RadianesAGrados(bepuRadians)); } } public class simBase { public BodyHandle BodyHandle { get; protected set; } public Simulation _simulation; protected bool _bodyCreated = false; // Bandera para saber si hemos creado un cuerpo // ✅ CORREGIDO: Restaurar factor de conversión correcto public const float SPEED_CONVERSION_FACTOR = 1/2f; // Factor de conversión de velocidad interna a m/s - Para LinearAxisMotor es 0.5f // Constantes para las posiciones Z de los objetos 3D public const float zPos_Transporte = 0f; // Z de la parte baja public const float zAltura_Transporte = 0.1f; // Altura del transporte sobre zPos public const float zPos_Guia = 0.05f; // Z de la parte baja public const float zAltura_Guia = 0.20f; // Altura de la guía sobre zPos public const float zAltura_Barrera = zAltura_Guia; // Altura de la barrera sobre zPos public const float zPos_Barrera = zPos_Guia; // Z de la parte baja public const float zPos_Descarte = 0.1f; // Z de la parte baja // Constantes para configuración public const float zPos_Curve = zPos_Transporte; // Z de la parte baja de la curva public const float zAltura_Curve = zAltura_Transporte; // Altura de la curva (triángulos planos) public void RemoverBody() { try { // Solo intentar remover si realmente hemos creado un cuerpo antes if (_bodyCreated && _simulation != null && _simulation.Bodies != null && _simulation.Bodies.BodyExists(BodyHandle)) { _simulation.Bodies.Remove(BodyHandle); _bodyCreated = false; // Marcar como no creado después de remover System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ✅ Body eliminado: {BodyHandle}"); } else { System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ⚠️ Body no existe o no creado: {BodyHandle}"); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ❌ ERROR: {ex.Message}"); _bodyCreated = false; // Marcar como no creado en caso de error } } /// /// ✅ 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); } public static float GradosARadianes(float grados) { return grados * (float)Math.PI / 180f; } public static float RadianesAGrados(float radianes) { return radianes * 180f / (float)Math.PI; } public void SetPosition(float x, float y, float z = 0) { CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, new Vector3(x, y, z)); } public void SetPosition(Vector2 wpfPosition) { // Mantener la coordenada Z actual para preservar la altura del objeto var currentBepuPosition = CoordinateConverter.GetBepuBodyPosition(_simulation, BodyHandle); var newBepuPosition = new Vector3(wpfPosition.X, CoordinateConverter.WpfYToBepuY(wpfPosition.Y), currentBepuPosition.Z); CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, newBepuPosition); } public void SetPosition(Vector3 bepuPosition) { CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, bepuPosition); } public Vector3 GetPosition() { return CoordinateConverter.GetBepuBodyPosition(_simulation, BodyHandle); } public void SetRotation(float wpfAngle) { CoordinateConverter.UpdateBepuBodyRotation(_simulation, BodyHandle, wpfAngle); } public float GetRotationZ() { return CoordinateConverter.GetWpfAngleFromBepuBody(_simulation, BodyHandle); } } public class simTransporte : simBase { public float Speed { get; set; } // Velocidad para efectos de cinta transportadora (m/s) public float Friction { get; set; } // Friccion para efectos de cinta transportadora public float DistanceGuide2Guide { get; set; } public bool isBrake { get; set; } public bool TransportWithGuides = false; private List _deferredActions; public float Width { get; set; } public float Height { get; set; } // ✅ 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; public simTransporte(Simulation simulation, List deferredActions, float width, float height, Vector2 topLeft, float angle = 0) { _simulation = simulation; _deferredActions = deferredActions; Width = width; Height = height; // Usar el nuevo método Create que maneja Top-Left correctamente Create(width, height, topLeft, angle); // ✅ INICIALIZAR PROPIEDADES CRÍTICAS UpdateCachedProperties(); } public float Angle { get { return GetRotationZ(); } set { SetRotation(value); } } /// /// ✅ SOBRESCRITO: SetRotation que actualiza automáticamente las propiedades cacheadas /// public new void SetRotation(float wpfAngle) { base.SetRotation(wpfAngle); // ✅ CRÍTICO: Actualizar propiedades cacheadas después del cambio de rotación UpdateCachedProperties(); // ✅ CRÍTICO: Disparar evento para actualizar motores activos con nueva dirección OnSpeedChanged?.Invoke(this); } public new void SetPosition(float x, float y, float z = 0) { base.SetPosition(x, y, z); } /// /// ✅ NUEVO: Actualiza posición desde Top-Left WPF con dimensiones y ángulo actuales /// internal void SetPositionFromWpfTopLeft(Vector2 wpfTopLeft) { var currentWpfAngle = GetRotationZ(); // Ya usa CoordinateConverter var zPosition = GetPosition().Z; // Mantener Z actual var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, currentWpfAngle, zPosition); SetPosition(bepuCenter); } /// /// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual /// internal 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 /// internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle) { var zPosition = GetPosition().Z; // Mantener Z actual var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, wpfAngle, zPosition); CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); // ✅ CRÍTICO: Actualizar propiedades cacheadas después del cambio de orientación UpdateCachedProperties(); // ✅ CRÍTICO: Disparar evento para actualizar motores activos con nueva dirección OnSpeedChanged?.Invoke(this); } // ✅ NUEVO MÉTODO - actualizar propiedades cacheadas internal void UpdateCachedProperties() { // ✅ CORREGIDO: La dirección siempre es UnitX rotado por el ángulo del transporte // NO depende de las dimensiones (Width >= Height) sino solo de la rotación if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); var bepuQuaternion = bodyReference.Pose.Orientation; // ✅ SIEMPRE usar UnitX y aplicar la rotación DirectionVector = Vector3.Transform(Vector3.UnitX, bepuQuaternion); // 🔍 DEBUG: Agregar información detallada var wpfAngle = GetRotationZ(); System.Diagnostics.Debug.WriteLine($"[UpdateCached] WPF Angle: {wpfAngle}°, DirectionVector: {DirectionVector}, Length: {DirectionVector.Length()}"); } else { // ✅ CORREGIDO: Aplicar conversión de coordenadas WPF→BEPU en el vector var wpfAngle = GetRotationZ(); // Ángulo WPF en grados var wpfAngleRadians = simBase.GradosARadianes(wpfAngle); // Calcular el vector en coordenadas WPF var wpfX = (float)Math.Cos(wpfAngleRadians); var wpfY = (float)Math.Sin(wpfAngleRadians); // ✅ APLICAR CONVERSIÓN Y: En WPF Y+ es abajo, en BEPU Y+ es arriba DirectionVector = new Vector3(wpfX, -wpfY, 0); // Invertir Y para conversión WPF→BEPU // 🔍 DEBUG: Agregar información detallada System.Diagnostics.Debug.WriteLine($"[UpdateCached-Fallback] WPF Angle: {wpfAngle}°, WPF Vector: ({wpfX:F3}, {wpfY:F3}), BEPU DirectionVector: {DirectionVector}, Length: {DirectionVector.Length()}"); } SpeedMetersPerSecond = Speed / simBase.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); } /// /// Configura la velocidad del transporte en metros por segundo /// Valores positivos mueven en la dirección del transporte, negativos en dirección opuesta /// /// Velocidad en m/s (típicamente entre -5.0 y 5.0) public void SetTransportSpeed(float speedMeterPerSecond) { SetSpeed(speedMeterPerSecond * simBase.SPEED_CONVERSION_FACTOR); } /// /// Detiene completamente el transporte /// public void StopTransport() { SetSpeed(0f); } /// /// Invierte la dirección del transporte manteniendo la misma velocidad /// public void ReverseTransport() { SetSpeed(-Speed); } public void SetDimensions(float width, float height) { Width = width; Height = height; // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var box = new Box(width, height, zAltura_Transporte); var shapeIndex = _simulation.Shapes.Add(box); ChangeBodyShape(shapeIndex); } // ✅ CRÍTICO: Actualizar propiedades cacheadas después del cambio de dimensiones UpdateCachedProperties(); // ✅ CRÍTICO: Disparar evento para actualizar motores activos con nueva dirección OnSpeedChanged?.Invoke(this); } public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle = 0) { // ✅ USAR COORDINATECONVERTER para conversión centralizada var zPosition = zAltura_Transporte / 2f + zPos_Transporte; var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); Create(width, height, bepuCenter, wpfAngle); } /// /// ✅ SIMPLIFICADO: RemoverBody que solo elimina el body /// public new void RemoverBody() { base.RemoverBody(); } public void Create(float width, float height, Vector3 bepuPosition, float wpfAngle = 0) { RemoverBody(); var box = new Box(width, height, zAltura_Transporte); var shapeIndex = _simulation.Shapes.Add(box); // ✅ USAR COORDINATECONVERTER para conversión centralizada var bodyDescription = BodyDescription.CreateKinematic( new RigidPose(bepuPosition, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)), new CollidableDescription(shapeIndex, 0.1f), new BodyActivityDescription(0.01f) ); BodyHandle = _simulation.Bodies.Add(bodyDescription); _bodyCreated = true; // Marcar que hemos creado un cuerpo // ✅ CRÍTICO: Actualizar propiedades cacheadas después de crear el body UpdateCachedProperties(); } } public class simBarrera : simBase { public float Distancia; public bool LuzCortada; public bool LuzCortadaNeck; public bool DetectNeck; public List ListSimBotellaContact; float _height; private List _deferredActions; public float Width { get; set; } public float Height { get; set; } public simBarrera(Simulation simulation, List deferredActions, float width, float height, Vector2 topLeft, float angle = 0, bool detectectNeck = false) { _simulation = simulation; _deferredActions = deferredActions; Width = width; Height = height; _height = height; DetectNeck = detectectNeck; ListSimBotellaContact = new List(); // Usar el nuevo método Create que maneja Top-Left correctamente Create(width, height, topLeft, angle, detectectNeck); } public float Angle { get { return GetRotationZ(); } set { SetRotation(value); } } public new void SetPosition(float x, float y, float z = 0) { base.SetPosition(x, y, z); } /// /// ✅ NUEVO: Actualiza posición desde Top-Left WPF con dimensiones y ángulo actuales /// internal void SetPositionFromWpfTopLeft(Vector2 wpfTopLeft) { var currentWpfAngle = GetRotationZ(); // Ya usa CoordinateConverter var zPosition = GetPosition().Z; // Mantener Z actual var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, currentWpfAngle, zPosition); SetPosition(bepuCenter); } /// /// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual /// internal 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 /// internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle) { var zPosition = GetPosition().Z; // Mantener Z actual var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, Width, Height, wpfAngle, zPosition); CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); } public void SetDimensions(float width, float height) { _height = height; // ✅ 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); ChangeBodyShape(shapeIndex); } } public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle = 0, bool detectectNeck = false) { // ✅ 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 bepuPosition, float wpfAngle = 0, bool detectectNeck = false) { RemoverBody(); // Crear box con altura de 0.2m (desde z=0 hasta z=0.2) para funcionar como detector var box = new Box(width, height, zAltura_Barrera); // Altura de 20cm para detectar botellas completas var shapeIndex = _simulation.Shapes.Add(box); // Crear como SENSOR (Kinematic con speculative margin 0 para detección pura) // Configurar actividad para NUNCA dormir - los sensores deben estar siempre activos var activityDescription = new BodyActivityDescription(-1f); // -1 = nunca dormir // ✅ USAR COORDINATECONVERTER para conversión centralizada var bodyDescription = BodyDescription.CreateKinematic( new RigidPose(bepuPosition, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)), new CollidableDescription(shapeIndex, 0f), // Speculative margin 0 para sensores activityDescription ); BodyHandle = _simulation.Bodies.Add(bodyDescription); _bodyCreated = true; // Marcar que hemos creado un cuerpo } } public class simGuia : simBase { private List _deferredActions; // Propiedades para acceder a las dimensiones del objeto WPF public float GuideThickness { get; set; } // Espesor de la guía public simGuia(Simulation simulation, List deferredActions, float width, float height, Vector2 topLeft, float angle) { _simulation = simulation; _deferredActions = deferredActions; GuideThickness = height; Create(width, height, topLeft, angle); } public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle) { RemoverBody(); // ✅ 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); // ✅ USAR COORDINATECONVERTER para conversión centralizada var bodyDescription = BodyDescription.CreateKinematic( new RigidPose(bepuCenter, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)), new CollidableDescription(shapeIndex, 0.1f), new BodyActivityDescription(0.01f) ); BodyHandle = _simulation.Bodies.Add(bodyDescription); _bodyCreated = true; } // Método para actualizar las propiedades desde el objeto WPF public void UpdateProperties(float ancho, float altoGuia, float angulo) { // 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); } } /// /// ✅ CORREGIDO - Actualiza solo la posición sin recrear el objeto usando CoordinateConverter /// Requiere las dimensiones reales para conversión correcta Top-Left → Center /// public void SetPosition(Vector2 wpfTopLeft, float wpfAngle, float actualWidth, float actualHeight) { // ✅ USAR COORDINATECONVERTER para conversión centralizada con dimensiones reales var zPosition = zAltura_Guia / 2 + zPos_Guia; var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, actualWidth, actualHeight, wpfAngle, zPosition); // Actualizar posición y rotación simultáneamente CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); } /// /// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF (sobrecarga para compatibilidad) /// internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle, float width, float height) { // Usar el método SetPosition con dimensiones correctas SetPosition(wpfTopLeft, wpfAngle, width, height); // Actualizar propiedades internas GuideThickness = height; } /// /// ✅ LEGACY - Mantener compatibilidad con versión anterior (usar dimensiones almacenadas) /// public void SetPosition(Vector2 wpfTopLeft, float wpfAngle = 0) { // Fallback: usar GuideThickness como aproximación si no se proporcionan dimensiones SetPosition(wpfTopLeft, wpfAngle, GuideThickness * 10f, GuideThickness); } } public class simBotella : simBase { public float Radius; public float Height; // Altura para la visualización del cilindro en Helix private float _mass; public bool Descartar = false; public int isOnTransports; public List ListOnTransports; public bool isRestricted; public bool isNoMoreRestricted; public simTransporte ConveyorRestrictedTo; public float OverlapPercentage; public float _neckRadius; public bool isOnBrakeTransport; // Nueva propiedad para marcar si está en un transporte con freno public simTransporte CurrentBrakeTransport { get; set; } // ✅ NUEVA: Referencia al transporte de freno actual // ✅ NUEVO SISTEMA SIMPLIFICADO: Motor dinámico que se crea solo cuando es necesario public ConstraintHandle CurrentMotor { get; private set; } = default; public simBase CurrentMotorTarget { get; private set; } = null; // Transporte o curva actual public bool _hasMotor = false; // ✅ BANDERA PÚBLICA para acceso desde callbacks public bool HasMotor => _hasMotor; // ✅ NUEVAS PROPIEDADES para el motor dinámico public Vector3 CurrentDirection { get; private set; } = Vector3.UnitX; // Dirección por defecto (1,0,0) public float CurrentSpeed { get; private set; } = 0f; // Velocidad por defecto public bool IsOnElement { get; private set; } = false; // Si está sobre un elemento activo private List _deferredActions; public simBotella(Simulation simulation, List deferredActions, float diameter, Vector2 position, float mass, float neckRadius = 0) { _simulation = simulation; _deferredActions = deferredActions; Radius = diameter / 2f; Height = diameter; // Altura igual al diámetro para mantener proporciones similares _mass = mass; _neckRadius = neckRadius; ListOnTransports = new List(); // ✅ USAR COORDINATECONVERTER para conversión centralizada var position3D = new Vector3(position.X, CoordinateConverter.WpfYToBepuY(position.Y), Radius + zPos_Transporte + zAltura_Transporte); Create(position3D); // ✅ ELIMINADO: No crear motor permanente - se creará cuando sea necesario } public float CenterX { get { return GetPosition().X; } set { var pos = GetPosition(); SetPosition(value, pos.Y, pos.Z); } } public float CenterY { get { // ✅ USAR COORDINATECONVERTER para conversión centralizada return CoordinateConverter.BepuYToWpfY(GetPosition().Y); } set { var pos = GetPosition(); // ✅ USAR COORDINATECONVERTER para conversión centralizada SetPosition(pos.X, CoordinateConverter.WpfYToBepuY(value), pos.Z); } } public Vector2 Center { get { var pos3D = GetPosition(); // ✅ USAR COORDINATECONVERTER para conversión centralizada return CoordinateConverter.BepuVector3ToWpfVector2(pos3D); } set { // Mantener la Z actual, solo cambiar X, Y var currentPos = GetPosition(); // ✅ USAR COORDINATECONVERTER para conversión centralizada SetPosition(value.X, CoordinateConverter.WpfYToBepuY(value.Y), currentPos.Z); } } public float Mass { get { if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); return 1f / bodyReference.LocalInertia.InverseMass; } return _mass; } set { _mass = value; if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { // Usar esfera simple - sin complejidad de inercia personalizada var sphere = new Sphere(Radius); var inertia = sphere.ComputeInertia(value); _simulation.Bodies.SetLocalInertia(BodyHandle, inertia); } } } private void Create(Vector3 position) { RemoverBody(); // Usar ESFERA en BEPU para simplicidad matemática y eficiencia var sphere = new Sphere(Radius); var shapeIndex = _simulation.Shapes.Add(sphere); // Inercia estándar de esfera - sin complejidad adicional var inertia = sphere.ComputeInertia(_mass); // NUNCA DORMIR - Valor negativo significa que las botellas JAMÁS entran en sleep mode // Esto es crítico para detección continua de barreras, transportes y descartes var activityDescription = new BodyActivityDescription(-1f); // -1 = NUNCA dormir var bodyDescription = BodyDescription.CreateDynamic( new RigidPose(position), new BodyVelocity(), inertia, // Inercia estándar de esfera new CollidableDescription(shapeIndex, 0.001f), activityDescription ); BodyHandle = _simulation.Bodies.Add(bodyDescription); _bodyCreated = true; // Marcar que hemos creado un cuerpo } /// /// ✅ NUEVO: Crea o actualiza el motor conectándolo al transporte/curva actual /// public void CreateOrUpdateMotor(simBase target, Vector3 direction, float speed) { try { // ✅ VALIDAR DIRECCIÓN if (direction.Length() < 0.001f) { return; } // ✅ VALIDAR QUE EL TARGET Y LA SIMULACIÓN EXISTAN if (target == null || _simulation == null || !_simulation.Bodies.BodyExists(BodyHandle)) { return; } // ✅ VERIFICAR SI NECESITAMOS CREAR O ACTUALIZAR EL MOTOR bool needsNewMotor = false; if (!HasMotor) { // ✅ PRIMERA VEZ: Crear motor nuevo needsNewMotor = true; System.Diagnostics.Debug.WriteLine($"[CreateOrUpdateMotor] 🆕 Creando motor nuevo para {target?.GetType().Name}"); } else if (CurrentMotorTarget != target) { // ✅ CAMBIO DE OBJETO: Eliminar motor anterior y crear uno nuevo RemoveCurrentMotor(); needsNewMotor = true; System.Diagnostics.Debug.WriteLine($"[CreateOrUpdateMotor] 🔄 Cambiando motor de {CurrentMotorTarget?.GetType().Name} a {target?.GetType().Name}"); } else { // ✅ MISMO OBJETO: Solo actualizar velocidad UpdateMotorSpeed(direction, speed); return; } // ✅ CREAR NUEVO MOTOR SI ES NECESARIO if (needsNewMotor && target != null) { CreateMotorForTarget(target, direction, speed); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[CreateOrUpdateMotor] ❌ ERROR: {ex.Message}"); } } /// /// ✅ NUEVO: Crea un motor específico para un transporte o curva /// private void CreateMotorForTarget(simBase target, Vector3 direction, float speed) { try { if (_simulation == null || _simulation.Solver == null || !_simulation.Bodies.BodyExists(BodyHandle) || target == null) { System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget] ❌ Simulación, solver, body o target no disponible"); return; } // ✅ VERIFICAR QUE EL TARGET TENGA UN BODY VÁLIDO if (!_simulation.Bodies.BodyExists(target.BodyHandle)) { System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget] ❌ Target body no existe: {target.BodyHandle}"); return; } // ✅ VERIFICAR QUE NO TENGA YA UN MOTOR VÁLIDO if (HasMotor && CurrentMotor.Value != 0) { System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget] ⚠️ Ya existe un motor válido: {CurrentMotor}"); return; } // ✅ CALCULAR VELOCIDAD EFECTIVA var effectiveSpeed = CalculateEffectiveSpeed(direction, speed); // ✅ CREAR MOTOR CONECTADO AL TARGET var motor = new LinearAxisMotor() { LocalOffsetA = Vector3.Zero, // Botella LocalOffsetB = Vector3.Zero, // Target LocalAxis = Vector3.UnitX, // Dirección fija del motor TargetVelocity = effectiveSpeed, Settings = new MotorSettings(Math.Max(_mass * 20f, 8f), 4f) }; // ✅ CONECTAR BOTELLA CON EL TARGET (transporte o curva) CurrentMotor = _simulation.Solver.Add(BodyHandle, BodyHandle, motor); // ✅ VERIFICAR QUE EL MOTOR SE CREÓ CORRECTAMENTE if (CurrentMotor.Value == 0) { System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget] ❌ Error: Motor no se creó correctamente"); return; } CurrentMotorTarget = target; _hasMotor = true; // ✅ ESTABLECER BANDERA // ✅ ACTUALIZAR PROPIEDADES INTERNAS CurrentDirection = direction; CurrentSpeed = speed; IsOnElement = Math.Abs(speed) > 0.001f; System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget] ✅ Motor creado:"); System.Diagnostics.Debug.WriteLine($" Botella: {BodyHandle}"); System.Diagnostics.Debug.WriteLine($" Target: {target.BodyHandle} ({target.GetType().Name})"); System.Diagnostics.Debug.WriteLine($" Motor Handle: {CurrentMotor} (Value: {CurrentMotor.Value})"); System.Diagnostics.Debug.WriteLine($" Dirección: {direction}"); System.Diagnostics.Debug.WriteLine($" Velocidad efectiva: {effectiveSpeed:F3}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget] ❌ ERROR: {ex.Message}"); } } /// /// ✅ NUEVO: Actualiza solo la velocidad del motor existente /// private void UpdateMotorSpeed(Vector3 direction, float speed) { try { if (!HasMotor || _simulation == null) { return; } // ✅ OBTENER LA DESCRIPCIÓN ACTUAL DEL MOTOR _simulation.Solver.GetDescription(CurrentMotor, out LinearAxisMotor motor); // ✅ CALCULAR VELOCIDAD EFECTIVA var effectiveSpeed = CalculateEffectiveSpeed(direction, speed); // ✅ ACTUALIZAR SOLO LA VELOCIDAD motor.TargetVelocity = effectiveSpeed; // ✅ ACTUALIZAR EL MOTOR EN EL SOLVER _simulation.Solver.ApplyDescription(CurrentMotor, motor); // ✅ ACTUALIZAR PROPIEDADES INTERNAS CurrentDirection = direction; CurrentSpeed = speed; IsOnElement = Math.Abs(speed) > 0.001f; System.Diagnostics.Debug.WriteLine($"[UpdateMotorSpeed] 🔄 Velocidad actualizada: {effectiveSpeed:F3}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[UpdateMotorSpeed] ❌ ERROR: {ex.Message}"); } } /// /// ✅ NUEVO: Elimina el motor actual /// public void RemoveCurrentMotor() { try { if (HasMotor && _simulation != null && _simulation.Solver != null) { // ✅ VERIFICAR QUE EL MOTOR EXISTE ANTES DE ELIMINARLO if (CurrentMotor.Value != 0) { try { _simulation.Solver.Remove(CurrentMotor); System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] 🗑️ Motor eliminado: {CurrentMotor}"); } catch (Exception removeEx) { System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] ⚠️ Error eliminando motor {CurrentMotor}: {removeEx.Message}"); // Continuar con la limpieza incluso si falla la eliminación } } else { System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] ⚠️ Motor ya eliminado o inválido: {CurrentMotor}"); } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] ❌ ERROR: {ex.Message}"); } finally { // ✅ LIMPIAR REFERENCIAS SIEMPRE CurrentMotor = default; CurrentMotorTarget = null; _hasMotor = false; // ✅ LIMPIAR BANDERA CurrentDirection = Vector3.UnitX; CurrentSpeed = 0f; IsOnElement = false; } } /// /// ✅ NUEVO: Detiene el motor (velocidad 0) pero mantiene la conexión /// public void StopMotor() { UpdateMotorSpeed(CurrentDirection, 0f); } /// /// ✅ NUEVO MÉTODO: Calcular velocidad efectiva para dirección deseada /// private float CalculateEffectiveSpeed(Vector3 desiredDirection, float desiredSpeed) { // ✅ NORMALIZAR LA DIRECCIÓN DESEADA var normalizedDirection = Vector3.Normalize(desiredDirection); // ✅ CALCULAR LA PROYECCIÓN EN EL EJE X (dirección del motor fijo) // Como el motor está fijo en UnitX, solo podemos controlar el movimiento en X var projectionX = normalizedDirection.X; // ✅ CALCULAR VELOCIDAD EFECTIVA // Si la dirección es principalmente en X, usar la velocidad completa // Si la dirección es perpendicular a X, la velocidad será 0 var effectiveSpeed = projectionX * desiredSpeed; System.Diagnostics.Debug.WriteLine($"[CalculateEffectiveSpeed] 📐 Cálculo:"); System.Diagnostics.Debug.WriteLine($" Dirección deseada: {desiredDirection} (Normalizada: {normalizedDirection})"); System.Diagnostics.Debug.WriteLine($" Proyección en X: {projectionX:F3}"); System.Diagnostics.Debug.WriteLine($" Velocidad deseada: {desiredSpeed:F3}"); System.Diagnostics.Debug.WriteLine($" Velocidad efectiva: {effectiveSpeed:F3}"); return effectiveSpeed; } public void SetDiameter(float diameter) { Radius = diameter / 2f; Height = diameter; // Mantener altura igual al diámetro para proporciones consistentes // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var sphere = new Sphere(Radius); var shapeIndex = _simulation.Shapes.Add(sphere); ChangeBodyShape(shapeIndex); } } public void SetHeight(float height) { Height = height; // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var sphere = new Sphere(Radius); var shapeIndex = _simulation.Shapes.Add(sphere); ChangeBodyShape(shapeIndex); } } public void SetMass(float mass) { Mass = mass; } /// /// Limita la rotación de la botella solo al plano XY (siempre "de pie") /// Esto simplifica enormemente la simulación y es más realista para botellas /// public void LimitRotationToXYPlane() { if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); // Extraer solo la rotación en Z (plano XY) y eliminar las rotaciones en X e Y var currentOrientation = bodyReference.Pose.Orientation; // Convertir a ángulo en Z solamente var rotationZ = GetRotationZ(); // Crear nueva orientación solo con rotación en Z (botella siempre "de pie") var correctedOrientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, rotationZ); // Aplicar la orientación corregida bodyReference.Pose.Orientation = correctedOrientation; // También limitar la velocidad angular a solo rotación en Z var angularVelocity = bodyReference.Velocity.Angular; bodyReference.Velocity.Angular = new Vector3(0, 0, angularVelocity.Z); } } public void ApplyLinearVelocity(Vector3 velocity) { if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); bodyReference.Velocity.Linear = velocity; } } public Vector3 GetLinearVelocity() { if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) { var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle); return bodyReference.Velocity.Linear; } return Vector3.Zero; } public void CenterFixtureOnConveyor() { // Implementar lógica de centrado si es necesario } public bool IsOnAnyTransport() { return isOnTransports > 0; } /// /// ✅ SIMPLIFICADO: RemoverBody que confía en BEPU para limpiar constraints /// public new void RemoverBody() { base.RemoverBody(); } } /// /// Representa una curva o arco en la simulación física. /// ✅ IMPORTANTE: Los ángulos _startAngle y _endAngle se almacenan INTERNAMENTE en radianes BEPU /// (ya convertidos desde grados WPF usando CoordinateConverter.WpfDegreesToBepuRadians) /// Las propiedades públicas StartAngle/EndAngle devuelven grados WPF usando CoordinateConverter.BepuRadiansToWpfDegrees /// ✅ NUEVO: Cada triángulo actúa como un mini-transporte con su propia dirección tangencial fija /// public class simCurve : simBase { private float _innerRadius; private float _outerRadius; private float _startAngle; // ✅ SIEMPRE en radianes BEPU private float _endAngle; // ✅ SIEMPRE en radianes BEPU public float Speed { get; set; } // Velocidad para efectos de cinta transportadora (m/s) private List _deferredActions; // ✅ NUEVO: Almacenar el centro real de la curva private Vector3 _curveCenter; // ✅ SIMPLIFICADO: Propiedades esenciales únicamente public List BottlesOnCurve { get; private set; } = new List(); // ✅ EVENTO para actualización de motores public event Action OnSpeedChanged; // ✅ NUEVO: Almacenar triángulos creados para acceso directo private Triangle[] _storedTriangles; public float InnerRadius => _innerRadius; public float OuterRadius => _outerRadius; public float StartAngle => CoordinateConverter.BepuRadiansToWpfDegrees(_startAngle); // Convertir de radianes BEPU internos a grados WPF public float EndAngle => CoordinateConverter.BepuRadiansToWpfDegrees(_endAngle); // Convertir de radianes BEPU internos a grados WPF // ✅ NUEVO: Propiedad para acceder al centro real de la curva public Vector3 CurveCenter => _curveCenter; public simCurve(Simulation simulation, List deferredActions, float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 topLeft, float unused = 0) { _simulation = simulation; _deferredActions = deferredActions; _innerRadius = innerRadius; _outerRadius = outerRadius; // ✅ SIMPLIFICADO: Usar conversión directa WPF grados → BEPU radianes _startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle); _endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle); // ✅ NUEVO: Calcular y almacenar el centro real de la curva var curveSize = outerRadius * 2f; var zPosition = zAltura_Curve / 2f + zPos_Curve; _curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(topLeft, curveSize, curveSize, 0f, zPosition); // ✅ SIMPLIFICADO: Crear la curva directamente Create(innerRadius, outerRadius, startAngle, endAngle, topLeft, 0); } // ✅ SIMPLIFICADO: Configurar velocidad angular para AngularAxisMotor public void SetSpeed(float speed) { Speed = speed; // Velocidad angular directa (sin inversión) OnSpeedChanged?.Invoke(this); } /// /// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF /// internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle, float innerRadius, float outerRadius, float startAngle, float endAngle) { // Actualizar parámetros de la curva _innerRadius = innerRadius; _outerRadius = outerRadius; // ✅ CORREGIDO: Usar conversión directa WPF grados → BEPU radianes _startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle); _endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle); // ✅ NUEVO: Actualizar el centro real de la curva var curveSize = outerRadius * 2f; var zPosition = zAltura_Curve / 2f + zPos_Curve; _curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, curveSize, curveSize, 0f, zPosition); Create(_curveCenter); } /// /// ✅ NUEVO: Actualiza posición desde Top-Left WPF manteniendo parámetros actuales /// internal void SetPositionFromWpfTopLeft(Vector2 wpfTopLeft) { var curveSize = _outerRadius * 2f; var zPosition = GetPosition().Z; // Mantener Z actual _curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, curveSize, curveSize, 0f, zPosition); Create(_curveCenter); } /// /// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual /// internal Vector2 GetWpfTopLeft() { var curveSize = _outerRadius * 2f; return CoordinateConverter.CalculateWpfTopLeftFromBepuCenter(_curveCenter, curveSize, curveSize, 0f); } /// /// ✅ CORREGIDO: Obtiene los triángulos reales creados para la curva /// Devuelve los triángulos en coordenadas locales para evitar transformación duplicada /// public Triangle[] GetRealBEPUTriangles() { try { if (_storedTriangles == null || _storedTriangles.Length == 0) { System.Diagnostics.Debug.WriteLine($"[GetRealBEPUTriangles] No hay triángulos almacenados"); return new Triangle[0]; } // ✅ CORREGIDO: Devolver triángulos en coordenadas locales // La visualización 3D aplicará la transformación una sola vez System.Diagnostics.Debug.WriteLine($"[GetRealBEPUTriangles] Devolviendo {_storedTriangles.Length} triángulos en coordenadas locales"); return _storedTriangles; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[GetRealBEPUTriangles] Error: {ex.Message}"); return new Triangle[0]; } } /// /// ✅ NUEVO: Obtiene los triángulos transformados a coordenadas mundiales (solo para debugging) /// public Triangle[] GetWorldBEPUTriangles() { try { if (_storedTriangles == null || _storedTriangles.Length == 0) { return new Triangle[0]; } var worldTriangles = TransformTrianglesToWorldCoordinates(_storedTriangles); System.Diagnostics.Debug.WriteLine($"[GetWorldBEPUTriangles] Devolviendo {worldTriangles.Length} triángulos en coordenadas mundiales"); return worldTriangles; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[GetWorldBEPUTriangles] Error: {ex.Message}"); return new Triangle[0]; } } /// /// ✅ NUEVO: Transforma triángulos de coordenadas locales a mundiales /// private Triangle[] TransformTrianglesToWorldCoordinates(Triangle[] localTriangles) { try { if (_simulation == null || !_simulation.Bodies.BodyExists(BodyHandle)) { System.Diagnostics.Debug.WriteLine($"[TransformTriangles] Cuerpo no existe, devolviendo triángulos locales"); return localTriangles; // Fallback: devolver triángulos sin transformar } var body = _simulation.Bodies[BodyHandle]; var bodyPosition = body.Pose.Position; var bodyOrientation = body.Pose.Orientation; var transformedTriangles = new Triangle[localTriangles.Length]; for (int i = 0; i < localTriangles.Length; i++) { var localTriangle = localTriangles[i]; // Transformar cada vértice del triángulo a coordenadas mundiales var worldA = bodyPosition + Vector3.Transform(localTriangle.A, bodyOrientation); var worldB = bodyPosition + Vector3.Transform(localTriangle.B, bodyOrientation); var worldC = bodyPosition + Vector3.Transform(localTriangle.C, bodyOrientation); transformedTriangles[i] = new Triangle(worldA, worldB, worldC); } System.Diagnostics.Debug.WriteLine($"[TransformTriangles] Transformados {transformedTriangles.Length} triángulos. Body pos: {bodyPosition}"); return transformedTriangles; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[TransformTriangles] Error: {ex.Message}, devolviendo triángulos locales"); return localTriangles; // Fallback en caso de error } } public new void RemoverBody() { // ✅ NUEVO: Limpiar triángulos almacenados _storedTriangles = null; // ✅ SIMPLIFICADO: Solo remover el cuerpo principal (Mesh único) base.RemoverBody(); } public void Create(float innerRadius, float outerRadius, float startAngle, float endAngle, Vector2 wpfTopLeft, float unused = 0) { // ✅ CORRIGIDO: Como Aether - startAngle y endAngle definen directamente el sector // No hay rotación separada del objeto // Actualizar parámetros internos _innerRadius = innerRadius; _outerRadius = outerRadius; // ✅ CORREGIDO: Usar conversión directa WPF grados → BEPU radianes _startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle); _endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle); // ✅ NUEVO: Actualizar el centro real de la curva // Para curvas, el "tamaño" es el diámetro del radio exterior var curveSize = outerRadius * 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) _curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, curveSize, curveSize, 0f, zPosition); Create(_curveCenter); // Sin rotación adicional } private void Create(Vector3 position) { RemoverBody(); // ✅ SIMPLIFICADO: Crear superficie usando Triangle de BEPU directamente var triangles = CreateSimpleArcTriangles(_innerRadius, _outerRadius, _startAngle, _endAngle); // ✅ ALMACENAR triángulos para acceso directo _storedTriangles = triangles; // ✅ SIMPLIFICADO: Crear un solo cuerpo con múltiples Triangle shapes usando Mesh if (triangles.Length > 0) { // ✅ CREAR MESH CON LA API CORRECTA DE BEPU var triangleBuffer = new BepuUtilities.Memory.Buffer(triangles.Length, _simulation.BufferPool); for (int i = 0; i < triangles.Length; i++) { triangleBuffer[i] = triangles[i]; } var mesh = new BepuPhysics.Collidables.Mesh(triangleBuffer, Vector3.One, _simulation.BufferPool); var shapeIndex = _simulation.Shapes.Add(mesh); var bodyDescription = BodyDescription.CreateKinematic( new RigidPose(position), new CollidableDescription(shapeIndex, 0.001f), // ✅ SpeculativeMargin bajo para detección precisa new BodyActivityDescription(0.01f) ); BodyHandle = _simulation.Bodies.Add(bodyDescription); _bodyCreated = true; System.Diagnostics.Debug.WriteLine($"[simCurve] Creado con {triangles.Length} triángulos reales almacenados"); } } // ✅ MÉTODOS ELIMINADOS: CreateMainBody y CreateTriangleBody ya no son necesarios // La curva ahora se crea como un solo Mesh en el método Create simplificado /// /// ✅ SIMPLIFICADO: Crea triángulos usando Triangle de BEPU directamente /// Solo superficie superior, eliminando complejidad innecesaria /// Los triángulos se crean en coordenadas locales (Z=0) y la posición del cuerpo los ubica correctamente /// /// Radio interno del arco /// Radio externo del arco /// Ángulo inicial en radianes BEPU /// Ángulo final en radianes BEPU /// Array de triángulos nativos de BEPU en coordenadas locales private Triangle[] CreateSimpleArcTriangles(float innerRadius, float outerRadius, float startAngle, float endAngle) { var triangles = new List(); // ✅ SIMPLIFICADO: Menos segmentos, menos complejidad float arcLength = Math.Abs(endAngle - startAngle) * ((innerRadius + outerRadius) / 2f); int segments = Math.Max(8, Math.Min((int)(arcLength * 8), 32)); // Menos segmentos float angleStep = (endAngle - startAngle) / segments; // ✅ SIMPLIFICADO: Sin inversión compleja de ángulos for (int i = 0; i < segments; i++) { float angle1 = startAngle + i * angleStep; float angle2 = startAngle + (i + 1) * angleStep; // ✅ COORDENADAS LOCALES: Triángulos en Z=0, el cuerpo los posiciona correctamente var inner1 = new Vector3(innerRadius * (float)Math.Cos(angle1), innerRadius * (float)Math.Sin(angle1), 0); var outer1 = new Vector3(outerRadius * (float)Math.Cos(angle1), outerRadius * (float)Math.Sin(angle1), 0); var inner2 = new Vector3(innerRadius * (float)Math.Cos(angle2), innerRadius * (float)Math.Sin(angle2), 0); var outer2 = new Vector3(outerRadius * (float)Math.Cos(angle2), outerRadius * (float)Math.Sin(angle2), 0); // ✅ USAR Triangle NATIVO DE BEPU (solo superficie superior) triangles.Add(new Triangle(inner1, outer1, outer2)); triangles.Add(new Triangle(inner1, outer2, inner2)); } System.Diagnostics.Debug.WriteLine($"[CreateSimpleArcTriangles] Creados {triangles.Count} triángulos en coordenadas locales"); return triangles.ToArray(); } // ✅ MÉTODOS ELIMINADOS: Los cálculos tangenciales complejos ya no son necesarios // AngularAxisMotor maneja automáticamente la rotación en curvas } public class simDescarte : simBase { private float _radius; private List _deferredActions; public List ListSimBotellaContact; public simDescarte(Simulation simulation, List deferredActions, float diameter, Vector2 position) { _simulation = simulation; _deferredActions = deferredActions; _radius = diameter / 2f; ListSimBotellaContact = new List(); // ✅ USAR COORDINATECONVERTER para conversión centralizada var position3D = new Vector3(position.X, CoordinateConverter.WpfYToBepuY(position.Y), zPos_Descarte + _radius); Create(position3D); } public float Radius { get { return _radius; } set { _radius = Math.Max(value, 0.01f); // Mínimo 1cm // Recrear el cuerpo con el nuevo radio var currentPos = GetPosition(); Create(currentPos); } } public void SetDiameter(float diameter) { Radius = diameter / 2f; } public float GetDiameter() { return _radius * 2f; } public void Create(Vector2 position) { // ✅ USAR COORDINATECONVERTER para conversión centralizada var position3D = new Vector3(position.X, CoordinateConverter.WpfYToBepuY(position.Y), zPos_Descarte + _radius); Create(position3D); } /// /// ✅ NUEVO: Actualiza posición usando coordenadas WPF apropiadas /// internal void UpdateFromWpfCenter(Vector2 wpfCenter) { var position3D = new Vector3(wpfCenter.X, CoordinateConverter.WpfYToBepuY(wpfCenter.Y), zPos_Descarte + _radius); // Actualizar solo posición manteniendo orientación CoordinateConverter.UpdateBepuBodyPosition(_simulation, BodyHandle, position3D); } private void Create(Vector3 position) { RemoverBody(); // Crear esfera sensor para detección var sphere = new BepuPhysics.Collidables.Sphere(_radius); var shapeIndex = _simulation.Shapes.Add(sphere); // Crear como SENSOR (Kinematic con speculative margin 0 para detección pura) // Configurar actividad para NUNCA dormir - los sensores deben estar siempre activos var activityDescription = new BodyActivityDescription(-1f); // -1 = nunca dormir var bodyDescription = BodyDescription.CreateKinematic( new RigidPose(position), new CollidableDescription(shapeIndex, 0f), // Speculative margin 0 para sensores activityDescription ); BodyHandle = _simulation.Bodies.Add(bodyDescription); _bodyCreated = true; // Marcar que hemos creado un cuerpo } } // Callback handlers para BEPU public struct NarrowPhaseCallbacks : INarrowPhaseCallbacks { private SimulationManagerBEPU _simulationManager; public NarrowPhaseCallbacks(SimulationManagerBEPU simulationManager) { _simulationManager = simulationManager; } public bool AllowContactGeneration(int workerIndex, CollidableReference a, CollidableReference b, ref float speculativeMargin) { // ✅ NUEVO FILTRADO: Evitar colisiones innecesarias entre triángulos de curvas if (_simulationManager != null) { // Obtener información de los objetos involucrados var curveA = _simulationManager.GetCurveFromTriangleBodyHandle(a.BodyHandle); var curveB = _simulationManager.GetCurveFromTriangleBodyHandle(b.BodyHandle); var transportA = GetTransportFromCollidable(a); var transportB = GetTransportFromCollidable(b); var barrierA = GetBarreraFromCollidable(a); var barrierB = GetBarreraFromCollidable(b); var discardA = GetDescarteFromCollidable(a); var discardB = GetDescarteFromCollidable(b); var botellaA = GetBotellaFromCollidable(a); var botellaB = GetBotellaFromCollidable(b); // ✅ FILTRO 1: No permitir colisiones entre triángulos de curvas if (curveA != null && curveB != null) { return false; // Los triángulos de curva no deben colisionar entre sí } // ✅ FILTRO 2: No permitir colisiones entre transportes if (transportA != null && transportB != null) { return false; // Los transportes no deben colisionar entre sí } // ✅ FILTRO 3: No permitir colisiones entre elementos estáticos (transportes, curvas, barreras, descartes) var staticA = (transportA != null || curveA != null || barrierA != null || discardA != null); var staticB = (transportB != null || curveB != null || barrierB != null || discardB != null); if (staticA && staticB) { return false; // Los elementos estáticos no deben colisionar entre sí } // ✅ ELIMINADO: No bloquear colisiones físicas aquí // Las colisiones físicas deben permitirse siempre para mantener geometría sólida // Solo bloqueamos la creación de motores duplicados en ConfigureContactManifold } return true; // Permitir todas las demás colisiones (botella-estático, botella-botella) } public bool AllowContactGeneration(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB) { return true; } public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, ref TManifold manifold, out PairMaterialProperties pairMaterial) where TManifold : unmanaged, IContactManifold { // ✅ CONFIGURACIÓN BÁSICA de materiales físicos pairMaterial = new PairMaterialProperties { FrictionCoefficient = 0.6f, MaximumRecoveryVelocity = 2f, SpringSettings = new SpringSettings(60, 4) }; if (_simulationManager != null) { var botellaA = GetBotellaFromCollidable(pair.A); var botellaB = GetBotellaFromCollidable(pair.B); var barreraA = GetBarreraFromCollidable(pair.A); var barreraB = GetBarreraFromCollidable(pair.B); var descarteA = GetDescarteFromCollidable(pair.A); var descarteB = GetDescarteFromCollidable(pair.B); var botella = botellaA ?? botellaB; // ✅ BARRERAS como sensores puros if (barreraA != null || barreraB != null) { return false; // NO generar contacto físico } // ✅ DESCARTES como sensores puros if (descarteA != null || descarteB != null) { var descarte = descarteA ?? descarteB; if (botella != null) { _simulationManager.RegisterDescarteContact(descarte, botella); } return false; // NO generar contacto físico } // ✅ NUEVO SISTEMA SIMPLIFICADO: Solo registrar contactos para actualización en Step var transportA = GetTransportFromCollidable(pair.A); var transportB = GetTransportFromCollidable(pair.B); var curveA = GetCurveFromCollidable(pair.A); var curveB = GetCurveFromCollidable(pair.B); // ✅ CONTACTO BOTELLA-TRANSPORTE: Crear o actualizar motor inmediatamente if (botella != null && (transportA != null || transportB != null)) { var transport = transportA ?? transportB; // ✅ CREAR O ACTUALIZAR MOTOR DINÁMICO INMEDIATAMENTE transport.UpdateCachedProperties(); var direction = transport.DirectionVector; var speed = transport.SpeedMetersPerSecond; botella.CreateOrUpdateMotor(transport, direction, speed); // Fricción alta para transportes pairMaterial.FrictionCoefficient = 0.9f; pairMaterial.MaximumRecoveryVelocity = 1f; pairMaterial.SpringSettings = new SpringSettings(80, 6); } // ✅ CONTACTO BOTELLA-CURVA: Crear o actualizar motor inmediatamente else if (botella != null && (curveA != null || curveB != null)) { var curve = curveA ?? curveB; // ✅ CREAR O ACTUALIZAR MOTOR DINÁMICO INMEDIATAMENTE var direction = _simulationManager.CalculateCurveDirectionFromBottlePosition(curve, botella); var speed = curve.Speed; botella.CreateOrUpdateMotor(curve, direction, speed); // Fricción alta para curvas pairMaterial.FrictionCoefficient = 0.9f; pairMaterial.MaximumRecoveryVelocity = 1f; pairMaterial.SpringSettings = new SpringSettings(80, 6); } // Ajustes básicos para otras botellas else if (botella != null) { pairMaterial.FrictionCoefficient = 0.9f; pairMaterial.MaximumRecoveryVelocity = 1f; pairMaterial.SpringSettings = new SpringSettings(80, 6); } } return true; } private simTransporte GetTransportFromCollidable(CollidableReference collidable) { if (_simulationManager?.simulation != null) { if (collidable.Mobility == CollidableMobility.Kinematic) { var bodyHandle = collidable.BodyHandle; var simBase = _simulationManager.GetSimBaseFromBodyHandle(bodyHandle); return simBase as simTransporte; } } return null; } private simBotella GetBotellaFromCollidable(CollidableReference collidable) { if (_simulationManager?.simulation != null) { if (collidable.Mobility == CollidableMobility.Dynamic) { var bodyHandle = collidable.BodyHandle; var simBase = _simulationManager.GetSimBaseFromBodyHandle(bodyHandle); return simBase as simBotella; } } return null; } private simBarrera GetBarreraFromCollidable(CollidableReference collidable) { if (_simulationManager?.simulation != null) { if (collidable.Mobility == CollidableMobility.Kinematic) { var bodyHandle = collidable.BodyHandle; var simBase = _simulationManager.GetSimBaseFromBodyHandle(bodyHandle); return simBase as simBarrera; } } return null; } private simDescarte GetDescarteFromCollidable(CollidableReference collidable) { if (_simulationManager?.simulation != null) { if (collidable.Mobility == CollidableMobility.Kinematic) { var bodyHandle = collidable.BodyHandle; var simBase = _simulationManager.GetSimBaseFromBodyHandle(bodyHandle); return simBase as simDescarte; } } return null; } private simCurve GetCurveFromCollidable(CollidableReference collidable) { if (_simulationManager?.simulation != null) { if (collidable.Mobility == CollidableMobility.Kinematic) { var bodyHandle = collidable.BodyHandle; var simBase = _simulationManager.GetSimBaseFromBodyHandle(bodyHandle); return simBase as simCurve; } } return null; } /// /// ✅ NUEVO MÉTODO: Aplicar fuerzas de frenado directamente en el manifold /// public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB, ref ConvexContactManifold manifold) { return true; } public void Initialize(Simulation simulation) { } public void Dispose() { } } public struct PoseIntegratorCallbacks : IPoseIntegratorCallbacks { public Vector3 Gravity; public float LinearDamping; public float AngularDamping; private SimulationManagerBEPU _simulationManager; // ✅ NUEVA REFERENCIA public PoseIntegratorCallbacks(Vector3 gravity, float linearDamping = 0.999f, float angularDamping = 0.995f, SimulationManagerBEPU simulationManager = null) { Gravity = gravity; LinearDamping = linearDamping; AngularDamping = angularDamping; _simulationManager = simulationManager; // ✅ NUEVA INICIALIZACIÓN } public readonly AngularIntegrationMode AngularIntegrationMode => AngularIntegrationMode.Nonconserving; public readonly bool AllowSubstepsForUnconstrainedBodies => false; public readonly bool IntegrateVelocityForKinematics => false; public void Initialize(Simulation simulation) { } public void PrepareForIntegration(float dt) { } public void IntegrateVelocity(Vector bodyIndices, Vector3Wide position, QuaternionWide orientation, BodyInertiaWide localInertia, Vector integrationMask, int workerIndex, Vector dt, ref BodyVelocityWide velocity) { // Aplicar gravedad var gravityWide = Vector3Wide.Broadcast(Gravity); velocity.Linear += gravityWide * dt; // ✅ 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 var linearDampingWide = Vector.One * LinearDamping; var angularDampingWide = Vector.One * AngularDamping; velocity.Linear *= linearDampingWide; velocity.Angular *= angularDampingWide; // ¡Esto es clave para detener rotaciones infinitas! } } public class SimulationManagerBEPU { public Simulation simulation; public List Cuerpos; public List _deferredActions; private BufferPool bufferPool; private Stopwatch stopwatch; private double stopwatch_last; // Referencia al manager de visualización 3D public BEPUVisualization3DManager Visualization3DManager { get; set; } // Propiedad para controlar si la actualización 3D está habilitada public bool Is3DUpdateEnabled { get; set; } = true; // ✅ NUEVAS - para filtrado eficiente de callbacks private HashSet _transportHandles; private HashSet _curveHandles; private HashSet _barrierHandles; private HashSet _discardHandles; private HashSet _bottleHandles; // ✅ NUEVO - contador de frames para optimizaciones private int _frameCount = 0; // ✅ CONSERVAR - sistemas existentes que funcionan bien private Dictionary> _barreraContacts; private Dictionary> _descarteContacts; private HashSet _botellasParaEliminar; // ✅ NUEVO - sistema de eliminación diferida para evitar problemas de sincronización private Queue _pendingRemovals; private object _removalLock = new object(); private object _contactsLock = new object(); /// /// Obtiene el objeto simBase correspondiente a un BodyHandle /// public simBase GetSimBaseFromBodyHandle(BodyHandle bodyHandle) { return Cuerpos.FirstOrDefault(c => c.BodyHandle.Equals(bodyHandle)); } /// /// ✅ SIMPLIFICADO: Obtiene una simCurve desde su BodyHandle principal /// public simCurve GetCurveFromTriangleBodyHandle(BodyHandle bodyHandle) { // ✅ SIMPLIFICADO: Solo buscar en cuerpos principales (ya no hay triángulos separados) var simBase = GetSimBaseFromBodyHandle(bodyHandle); 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); // Calcular propiedades iniciales transport.UpdateCachedProperties(); break; case simCurve curve: _curveHandles.Add(curve.BodyHandle); // ✅ SIMPLIFICADO: Ya no hay triángulos separados, solo el cuerpo principal 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); break; case simTransporte transport: _transportHandles.Remove(transport.BodyHandle); break; case simCurve curve: _curveHandles.Remove(curve.BodyHandle); // ✅ SIMPLIFICADO: Ya no hay triángulos separados break; case simBarrera barrier: _barrierHandles.Remove(barrier.BodyHandle); break; case simDescarte discard: _discardHandles.Remove(discard.BodyHandle); break; } } // ✅ 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(); _barreraContacts = new Dictionary>(); _descarteContacts = new Dictionary>(); _botellasParaEliminar = new HashSet(); // ✅ NUEVOS - sistemas de filtrado y contactos _transportHandles = new HashSet(); _curveHandles = new HashSet(); _barrierHandles = new HashSet(); _discardHandles = new HashSet(); _bottleHandles = new HashSet(); // ✅ NUEVO - inicializar sistema de eliminación diferida _pendingRemovals = new Queue(); // ✅ CONSERVAR - resto del constructor igual bufferPool = new BufferPool(); stopwatch = new Stopwatch(); var narrowPhaseCallbacks = new NarrowPhaseCallbacks(this); // Configurar amortiguamiento para comportamiento realista: // - LinearDamping: 0.999f (muy leve, objetos siguen moviéndose pero eventualmente se detienen) // - AngularDamping: 0.995f (más agresivo para detener rotaciones infinitas de cilindros) // ✅ MODIFICADO: Pasar referencia de this al PoseIntegratorCallbacks var poseIntegratorCallbacks = new PoseIntegratorCallbacks( gravity: new Vector3(0, 0, -9.81f), // Gravedad en Z linearDamping: 0.999f, // Amortiguamiento lineal suave angularDamping: 0.995f, // Amortiguamiento angular más fuerte para detener rotaciones simulationManager: this // ✅ NUEVA REFERENCIA ); var solveDescription = new SolveDescription(8, 1); simulation = Simulation.Create(bufferPool, narrowPhaseCallbacks, poseIntegratorCallbacks, solveDescription); } public void Clear() { try { // ✅ 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) { _barreraContacts.Clear(); _descarteContacts.Clear(); _botellasParaEliminar.Clear(); } // ✅ NUEVO - limpiar clasificaciones _transportHandles.Clear(); _curveHandles.Clear(); _barrierHandles.Clear(); _discardHandles.Clear(); _bottleHandles.Clear(); // ✅ NUEVO - limpiar eliminaciones pendientes lock (_removalLock) { _pendingRemovals.Clear(); } // ✅ CONSERVAR - resto del método igual var cuerposToRemove = new List(Cuerpos); foreach (var cuerpo in cuerposToRemove) { try { if (cuerpo != null) { cuerpo.RemoverBody(); } } catch (Exception ex) { // Error removing body - continue with cleanup } } Cuerpos.Clear(); _deferredActions.Clear(); // ✅ NUEVO - Limpiar todas las dimensiones almacenadas CtrEditor.ObjetosSim.osBase.ClearAllStoredDimensions(); // Limpiar la simulación completamente si existe if (simulation != null) { try { // Forzar un timestep pequeño para limpiar contactos pendientes simulation.Timestep(1f / 1000f); // 1ms } catch (Exception ex) { // Warning during cleanup timestep - continue } } // Limpiar visualización 3D Visualization3DManager?.Clear(); } catch (Exception ex) { // Critical error during clear - operation failed } } public void Start() { 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); // ✅ SIMPLIFICADO: Reinicializar velocidad } 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; if (float.IsNaN(deltaTime) || float.IsInfinity(deltaTime) || deltaTime <= 0) { deltaTime = 1f / 60f; } const float maxDeltaTime = 0.1f; if (deltaTime > maxDeltaTime) { deltaTime = 1f / 60f; } // ✅ CONSERVAR - procesar acciones diferidas foreach (var action in _deferredActions) { action(); } _deferredActions.Clear(); // ✅ CONSERVAR - validaciones de simulación if (simulation?.Bodies == null) { return; } var invalidBodies = Cuerpos.Where(c => c != null && !simulation.Bodies.BodyExists(c.BodyHandle)).ToList(); if (invalidBodies.Count > 0) { foreach (var invalidBody in invalidBodies) { Cuerpos.Remove(invalidBody); } } // ✅ CONSERVAR - timestep var timestepValue = Math.Max(deltaTime, 1f / 120f); try { simulation.Timestep(timestepValue); } catch (AccessViolationException ex) { lock (_contactsLock) { _barreraContacts.Clear(); } throw; } // ✅ 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(); simulation.Awakener.AwakenBody(cuerpo.BodyHandle); } } catch (Exception ex) { // Error limiting rotation for bottle - continue } } // ✅ NUEVO: Detener motores de botellas que no están en contacto con elementos // Esto se ejecuta cada 10 frames para eficiencia if (_frameCount % 10 == 0) { StopMotorsForBottlesNotInContact(); } // ✅ CONSERVAR - sistemas que funcionan bien ProcessBarreraContacts(); ProcessDescarteContacts(); ProcessCleanupSystem(); // ✅ SIMPLIFICAR - limpiar solo contactos que usamos lock (_contactsLock) { _barreraContacts.Clear(); _descarteContacts.Clear(); } // ✅ CONSERVAR - sincronización 3D if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } } catch (Exception ex) { lock (_contactsLock) { _barreraContacts.Clear(); } } } /// /// ✅ NUEVO: Marca un objeto para eliminación diferida (más seguro) /// public void Remove(simBase Objeto) { if (Objeto == null) return; try { // ✅ SIMPLIFICADO - eliminar lógica de masa especial UnregisterObjectHandle(Objeto); // ✅ NUEVO // ✅ NUEVO - Limpiar dimensiones almacenadas en osBase CtrEditor.ObjetosSim.osBase.ClearStoredDimensions(Objeto); // ✅ NUEVO - Agregar a cola de eliminación diferida lock (_removalLock) { _pendingRemovals.Enqueue(Objeto); } // ✅ REMOVER de la lista inmediatamente para evitar referencias colgantes Cuerpos.Remove(Objeto); if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } System.Diagnostics.Debug.WriteLine($"[Remove] ✅ Objeto marcado para eliminación diferida: {Objeto.GetType().Name}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[Remove] ❌ Error marcando objeto {Objeto?.GetType().Name}: {ex.Message}"); // ✅ CRÍTICO: Siempre remover de la lista incluso si falló Cuerpos.Remove(Objeto); } } public simBotella AddCircle(float diameter, Vector2 position, float mass) { var botella = new simBotella(simulation, _deferredActions, diameter, position, mass); Cuerpos.Add(botella); RegisterObjectHandle(botella); // ✅ NUEVO 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); Cuerpos.Add(transporte); RegisterObjectHandle(transporte); // ✅ NUEVO - incluye UpdateCachedProperties() if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } return transporte; } public simBarrera AddBarrera(float width, float height, Vector2 position, float angle, bool detectarCuello) { var barrera = new simBarrera(simulation, _deferredActions, width, height, position, angle, detectarCuello); Cuerpos.Add(barrera); RegisterObjectHandle(barrera); // ✅ NUEVO if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } return barrera; } public simGuia AddLine(float width, float height, Vector2 topLeft, float angle) { var guia = new simGuia(simulation, _deferredActions, width, height, topLeft, angle); Cuerpos.Add(guia); // ✅ NOTA: simGuia no requiere registro especial if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } return guia; } public simDescarte AddDescarte(float diameter, Vector2 position) { var descarte = new simDescarte(simulation, _deferredActions, diameter, position); Cuerpos.Add(descarte); 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); Cuerpos.Add(curve); RegisterObjectHandle(curve); // ✅ NUEVO if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); } return curve; } /// /// ✅ NUEVO: Detiene motores de botellas que no están en contacto con elementos /// private void StopMotorsForBottlesNotInContact() { try { var allBottles = Cuerpos.OfType().ToList(); foreach (var bottle in allBottles) { if (bottle != null && bottle.HasMotor && bottle.IsOnElement) { // ✅ VERIFICAR SI LA BOTELLA ESTÁ REALMENTE EN CONTACTO CON ALGÚN ELEMENTO bool isInContact = false; // Verificar contacto con transportes foreach (var transport in Cuerpos.OfType()) { if (IsBottleInContactWithTransport(bottle, transport)) { isInContact = true; break; } } // Verificar contacto con curvas if (!isInContact) { foreach (var curve in Cuerpos.OfType()) { if (IsBottleInContactWithCurve(bottle, curve)) { isInContact = true; break; } } } // ✅ DETENER MOTOR SI NO ESTÁ EN CONTACTO if (!isInContact) { bottle.StopMotor(); System.Diagnostics.Debug.WriteLine($"[StopMotorsForBottlesNotInContact] ⏹️ Detenido: {bottle.BodyHandle}"); } } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[StopMotorsForBottlesNotInContact] ❌ ERROR: {ex.Message}"); } } /// /// ✅ NUEVO: Verifica si una botella está en contacto con un transporte /// private bool IsBottleInContactWithTransport(simBotella bottle, simTransporte transport) { try { 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; } catch { return false; } } /// /// ✅ NUEVO: Verifica si una botella está en contacto con una curva /// private bool IsBottleInContactWithCurve(simBotella bottle, simCurve curve) { try { var bottlePos = bottle.GetPosition(); var curveCenter = curve.CurveCenter; // ✅ NUEVO: Usar centro real var distance = Vector2.Distance( new Vector2(bottlePos.X, bottlePos.Y), new Vector2(curveCenter.X, curveCenter.Y) ); // Verificar si está dentro del área de la curva + tolerancia var maxDistance = curve.OuterRadius + bottle.Radius + 0.1f; return distance <= maxDistance; } catch { return false; } } /// /// ✅ NUEVO: Calcula la dirección tangencial específica basada en la posición de la botella /// public Vector3 CalculateCurveDirectionFromBottlePosition(simCurve curve, simBotella bottle) { try { // ✅ NUEVO: Usar el centro real almacenado de la curva var curveCenter = curve.CurveCenter; var bottlePosition = bottle.GetPosition(); // Calcular el vector desde el centro de la curva hasta la botella (en el plano XY) var radiusVector = new Vector3(bottlePosition.X - curveCenter.X, bottlePosition.Y - curveCenter.Y, 0f); var radius = radiusVector.Length(); if (radius < 0.001f) { System.Diagnostics.Debug.WriteLine($"[CalculateCurveDirectionFromBottlePosition] ⚠️ Botella muy cerca del centro de la curva"); return Vector3.UnitX; // Fallback } // Normalizar el vector radial var normalizedRadius = radiusVector / radius; // Calcular la dirección tangencial (perpendicular al radio) // En coordenadas 2D: si r = (x, y), entonces t = (-y, x) es tangente var tangentDirection = new Vector3(-normalizedRadius.Y, normalizedRadius.X, 0f); // Verificar que la dirección tangencial apunte en el sentido correcto según la velocidad de la curva if (curve.Speed < 0) { tangentDirection = -tangentDirection; } System.Diagnostics.Debug.WriteLine($"[CalculateCurveDirectionFromBottlePosition] 📐 Dirección calculada:"); System.Diagnostics.Debug.WriteLine($" Centro curva: {curveCenter}"); System.Diagnostics.Debug.WriteLine($" Posición botella: {bottlePosition}"); System.Diagnostics.Debug.WriteLine($" Radio: {radius:F3}"); System.Diagnostics.Debug.WriteLine($" Vector radial: {normalizedRadius}"); System.Diagnostics.Debug.WriteLine($" Dirección tangencial: {tangentDirection} (Longitud: {tangentDirection.Length():F3})"); return tangentDirection; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[CalculateCurveDirectionFromBottlePosition] ❌ ERROR: {ex.Message}"); return Vector3.UnitX; // Fallback } } public void Dispose() { Clear(); simulation?.Dispose(); bufferPool?.Clear(); } /// /// Registra un contacto entre una barrera y una botella para detección de paso /// /// Barrera que detecta el paso /// Botella que está pasando public void RegisterBarreraContact(simBarrera barrera, simBotella botella) { if (barrera != null && botella != null) { lock (_contactsLock) { if (!_barreraContacts.ContainsKey(barrera)) { _barreraContacts[barrera] = new List(); } if (!_barreraContacts[barrera].Contains(botella)) { _barreraContacts[barrera].Add(botella); } } } } /// /// Registra un contacto entre un descarte y una botella para marcar eliminación /// /// Descarte que detecta la botella /// Botella que debe ser eliminada public void RegisterDescarteContact(simDescarte descarte, simBotella botella) { if (descarte != null && botella != null) { lock (_contactsLock) { // Marcar la botella para eliminación _botellasParaEliminar.Add(botella); botella.Descartar = true; // También registrar el contacto para la lista del descarte if (!_descarteContacts.ContainsKey(descarte)) { _descarteContacts[descarte] = new List(); } if (!_descarteContacts[descarte].Contains(botella)) { _descarteContacts[descarte].Add(botella); } } } } /// /// Procesa todas las barreras usando detección geométrica pura /// Ya no depende de contactos físicos de BEPU - usa cálculo geométrico para ambos flags /// private void ProcessBarreraContacts() { try { // Obtener todas las barreras y botellas var barreras = Cuerpos.OfType().ToList(); var botellas = Cuerpos.OfType().ToList(); foreach (var barrera in barreras) { try { // Verificar que la barrera aún existe en la simulación if (barrera == null || !simulation?.Bodies?.BodyExists(barrera.BodyHandle) == true) continue; // Resetear flags barrera.LuzCortada = false; barrera.LuzCortadaNeck = false; barrera.Distancia = float.MaxValue; // Limpiar la lista de forma segura if (barrera.ListSimBotellaContact != null) { barrera.ListSimBotellaContact.Clear(); } // Calcular detección usando geometría pura para TODAS las botellas CalculateBarreraDetectionGeometric(barrera, botellas); } catch (Exception ex) { // Error processing barrera - continue } } // Limpiar contactos ya que no los usamos más lock (_contactsLock) { _barreraContacts.Clear(); } } catch (Exception ex) { // Critical error in ProcessBarrera - continue } } /// /// Calcula detección geométrica pura para una barrera específica contra todas las botellas /// Usa el mismo sistema geométrico para ambos flags: LuzCortada (radio completo) y LuzCortadaNeck (radio/2) /// /// Barrera que está detectando /// Lista de TODAS las botellas en la simulación private void CalculateBarreraDetectionGeometric(simBarrera barrera, List todasLasBotellas) { try { // Validaciones de seguridad if (barrera == null || todasLasBotellas == null || simulation?.Bodies == null) return; if (!simulation.Bodies.BodyExists(barrera.BodyHandle)) return; var barrierBody = simulation.Bodies[barrera.BodyHandle]; var barrierPosition = barrierBody.Pose.Position; var barrierOrientation = barrierBody.Pose.Orientation; // Validar valores de posición y orientación if (float.IsNaN(barrierPosition.X) || float.IsNaN(barrierPosition.Y) || float.IsNaN(barrierPosition.Z)) { return; } // Obtener las dimensiones de la barrera de forma segura var halfWidth = Math.Max(barrera.Width / 2f, 0.01f); // Mínimo 1cm float minDistance = float.MaxValue; var botellasDetectadas = new List(); // Procesar TODAS las botellas (no solo las en contacto físico) foreach (var botella in todasLasBotellas) { try { if (botella == null || !simulation.Bodies.BodyExists(botella.BodyHandle)) continue; var botellaBody = simulation.Bodies[botella.BodyHandle]; var botellaPosition = botellaBody.Pose.Position; // Validar posición de la botella if (float.IsNaN(botellaPosition.X) || float.IsNaN(botellaPosition.Y) || float.IsNaN(botellaPosition.Z)) { continue; } // CLAVE: Crear la línea del haz en el plano XY a la altura del centro de la botella var bottleZ = botellaPosition.Z; // Altura actual del centro de la botella // Puntos de la línea del haz en el plano XY a la altura de la botella var localStart = new Vector3(-halfWidth, 0, 0); var localEnd = new Vector3(halfWidth, 0, 0); // Transformar a coordenadas mundiales pero en el plano Z de la botella var worldStartXY = barrierPosition + Vector3.Transform(localStart, barrierOrientation); var worldEndXY = barrierPosition + Vector3.Transform(localEnd, barrierOrientation); // Ajustar Z para que esté en el plano de la botella worldStartXY = new Vector3(worldStartXY.X, worldStartXY.Y, bottleZ); worldEndXY = new Vector3(worldEndXY.X, worldEndXY.Y, bottleZ); // Calcular distancia desde el centro de la botella a la línea del haz var closestPoint = ProjectPointOntoLine(botellaPosition, worldStartXY, worldEndXY); var distance = Vector3.Distance(closestPoint, botellaPosition); // Validar distancia calculada if (float.IsNaN(distance) || float.IsInfinity(distance)) { continue; } // Actualizar distancia mínima if (distance < minDistance) { minDistance = distance; } // NUEVO: Verificar LuzCortada usando radio completo if (distance <= botella.Radius) { barrera.LuzCortada = true; botellasDetectadas.Add(botella); } // Verificar detección de cuello usando radio/2 (como antes) if (barrera.DetectNeck && botella.Radius > 0) { var neckRadius = botella.Radius / 2f; if (distance <= neckRadius) { barrera.LuzCortadaNeck = true; // No hacer break aquí - queremos procesar todas las botellas para LuzCortada } } } catch (Exception ex) { // Error processing bottle - continue } } // Asignar resultados de forma segura barrera.Distancia = minDistance == float.MaxValue ? 0f : minDistance; // Actualizar lista de botellas detectadas if (barrera.ListSimBotellaContact != null) { barrera.ListSimBotellaContact.AddRange(botellasDetectadas); } } catch (Exception ex) { // En caso de error, asignar valores seguros if (barrera != null) { barrera.Distancia = float.MaxValue; barrera.LuzCortada = false; barrera.LuzCortadaNeck = false; } } } /// /// Procesa contactos de descarte para eliminar botellas marcadas usando detección geométrica /// private void ProcessDescarteContacts() { try { // Obtener todos los descartes y botellas var descartes = Cuerpos.OfType().ToList(); var botellas = Cuerpos.OfType().ToList(); foreach (var descarte in descartes) { try { // Verificar que el descarte aún existe en la simulación if (descarte == null || !simulation?.Bodies?.BodyExists(descarte.BodyHandle) == true) continue; // Limpiar la lista de forma segura if (descarte.ListSimBotellaContact != null) { descarte.ListSimBotellaContact.Clear(); } // Calcular detección usando geometría pura para TODAS las botellas CalculateDescarteDetectionGeometric(descarte, botellas); } catch (Exception ex) { // Error processing descarte - continue } } // Eliminar botellas marcadas para eliminación (después de procesamiento) RemoveMarkedBottles(); // Limpiar contactos ya que no los usamos más lock (_contactsLock) { _descarteContacts.Clear(); } } catch (Exception ex) { // Critical error in ProcessDescarte - continue } } /// /// Calcula detección geométrica pura para un descarte específico contra todas las botellas /// Usa detección de esfera contra esfera para determinar si hay contacto /// private void CalculateDescarteDetectionGeometric(simDescarte descarte, List todasLasBotellas) { try { // Validaciones de seguridad if (descarte == null || todasLasBotellas == null || simulation?.Bodies == null) return; if (!simulation.Bodies.BodyExists(descarte.BodyHandle)) return; var descarteBody = simulation.Bodies[descarte.BodyHandle]; var descartePosition = descarteBody.Pose.Position; // Validar valores de posición if (float.IsNaN(descartePosition.X) || float.IsNaN(descartePosition.Y) || float.IsNaN(descartePosition.Z)) { return; } var botellasDetectadas = new List(); // Procesar TODAS las botellas para detección geométrica foreach (var botella in todasLasBotellas) { try { if (botella == null || !simulation.Bodies.BodyExists(botella.BodyHandle)) continue; var botellaBody = simulation.Bodies[botella.BodyHandle]; var botellaPosition = botellaBody.Pose.Position; // Validar posición de la botella if (float.IsNaN(botellaPosition.X) || float.IsNaN(botellaPosition.Y) || float.IsNaN(botellaPosition.Z)) { continue; } // Calcular distancia entre centros (detección esfera contra esfera) var distance = Vector3.Distance(descartePosition, botellaPosition); // Validar distancia calculada if (float.IsNaN(distance) || float.IsInfinity(distance)) { continue; } // Verificar si las esferas se superponen var totalRadius = descarte.Radius + botella.Radius; if (distance <= totalRadius) { // Marcar botella para eliminación botella.Descartar = true; _botellasParaEliminar.Add(botella); botellasDetectadas.Add(botella); } } catch (Exception ex) { // Error processing bottle - continue } } // Actualizar lista de botellas detectadas if (descarte.ListSimBotellaContact != null) { descarte.ListSimBotellaContact.AddRange(botellasDetectadas); } } catch (Exception ex) { // Critical error in CalculateDescarteGeometric - continue } } /// /// Elimina las botellas marcadas para eliminación de forma segura /// private void RemoveMarkedBottles() { try { List botellasAEliminar; lock (_contactsLock) { botellasAEliminar = new List(_botellasParaEliminar); _botellasParaEliminar.Clear(); } foreach (var botella in botellasAEliminar) { try { if (botella != null && Cuerpos.Contains(botella)) { System.Diagnostics.Debug.WriteLine($"[RemoveMarkedBottles] 🗑️ Marcando botella para eliminación: {botella.BodyHandle}"); // ✅ USAR ELIMINACIÓN DIFERIDA (más seguro) Remove(botella); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[RemoveMarkedBottles] ❌ Error marcando botella {botella?.BodyHandle}: {ex.Message}"); // ✅ INTENTAR remover de la lista incluso si Remove falló if (botella != null) { Cuerpos.Remove(botella); } } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[RemoveMarkedBottles] ❌ ERROR CRÍTICO: {ex.Message}"); } } /// /// Sistema de limpieza que elimina botellas que estén debajo de la altura de los transportes /// Cualquier botella con Z menor al nivel superior de los transportes será eliminada /// private void ProcessCleanupSystem() { try { // Altura máxima de los transportes (nivel superior) var maxTransportHeight = simBase.zPos_Transporte + simBase.zAltura_Transporte; // Obtener todas las botellas var botellas = Cuerpos.OfType().ToList(); foreach (var botella in botellas) { try { // Validar que la botella aún existe en la simulación if (botella == null || !simulation?.Bodies?.BodyExists(botella.BodyHandle) == true) continue; var posicion = botella.GetPosition(); // Validar posición if (float.IsNaN(posicion.Z) || float.IsInfinity(posicion.Z)) continue; // Si la botella está debajo del nivel de los transportes, marcarla para eliminación if (posicion.Z < (maxTransportHeight - 2 * botella.Radius) || posicion.Z > (maxTransportHeight + 2 * botella.Radius)) { lock (_contactsLock) { botella.Descartar = true; _botellasParaEliminar.Add(botella); } } } catch (Exception ex) { // Error processing bottle in cleanup - continue } } } catch (Exception ex) { // Critical error in ProcessCleanupSystem - continue } } /// /// Proyecta un punto sobre una línea definida por dos puntos /// private Vector3 ProjectPointOntoLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd) { var lineDirection = Vector3.Normalize(lineEnd - lineStart); var pointToStart = point - lineStart; var projectionLength = Vector3.Dot(pointToStart, lineDirection); // Restringir la proyección a los límites de la línea var lineLength = Vector3.Distance(lineStart, lineEnd); projectionLength = Math.Max(0, Math.Min(projectionLength, lineLength)); return lineStart + lineDirection * projectionLength; } } }