diff --git a/ObjetosSim/SensoresComandos/ucPhotocell.xaml.cs b/ObjetosSim/SensoresComandos/ucPhotocell.xaml.cs index e876e09..61ccf28 100644 --- a/ObjetosSim/SensoresComandos/ucPhotocell.xaml.cs +++ b/ObjetosSim/SensoresComandos/ucPhotocell.xaml.cs @@ -94,12 +94,6 @@ namespace CtrEditor.ObjetosSim [property: Name("Ancho del Haz")] float ancho_Haz_De_Luz; - [ObservableProperty] - [property: Description("Distancia al cuello de la botella")] - [property: Category("Información")] - [property: Name("Distancia al Cuello")] - float distancia_cuello; - [ObservableProperty] [property: Description("Tipo de detección: cuello de botella o botella completa")] [property: Category("Configuración")] @@ -110,7 +104,7 @@ namespace CtrEditor.ObjetosSim { if (Simulation_Photocell == null) return; - Simulation_Photocell.DetectNeck = value; + simulationManager.SetBarreraDetectNeck(Simulation_Photocell, value); } @@ -221,11 +215,14 @@ namespace CtrEditor.ObjetosSim } public override void UpdateControl(int elapsedMilliseconds) { - Distancia_cuello = Simulation_Photocell.Distancia; + if (Simulation_Photocell == null) return; + + var barreraData = simulationManager.GetBarreraData(Simulation_Photocell); + if (DetectarCuello) - LuzCortada = Simulation_Photocell.LuzCortadaNeck; + LuzCortada = barreraData.LuzCortadaNeck; else - LuzCortada = Simulation_Photocell.LuzCortada; // Ahora es bool directamente + LuzCortada = barreraData.LuzCortada; } public override void UpdatePLCPrimerCiclo() { @@ -250,7 +247,11 @@ namespace CtrEditor.ObjetosSim { // El UserControl se esta eliminando // eliminar el objeto de simulacion - simulationManager.Remove(Simulation_Photocell); + if (Simulation_Photocell != null) + { + simulationManager.RemoveBarrera(Simulation_Photocell); + Simulation_Photocell = null; + } } diff --git a/ObjetosSim/osBase.cs b/ObjetosSim/osBase.cs index 2f2e580..7738670 100644 --- a/ObjetosSim/osBase.cs +++ b/ObjetosSim/osBase.cs @@ -1575,46 +1575,15 @@ namespace CtrEditor.ObjetosSim } } - public void UpdateRectangle(simBarrera simRect, Rectangle wpfRect, float Alto, float Ancho, float Angulo) + public void UpdateRectangle(simBarrera barrera, Rectangle wpfRect, float Alto, float Ancho, float Angulo) { - if (simRect != null) + if (barrera != null) { var topLeft2D = GetRectangleTopLeft(wpfRect); - - // ✅ 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); - } + simulationManager.UpdateBarrera(barrera, Ancho, Alto, topLeft2D, Angulo); } } - // Nota: simCurve no está implementado en BEPU, estos métodos están comentados por compatibilidad - /* - public void UpdateCurve(simCurve curva, float RadioInterno, float RadioExterno, float startAngle, float endAngle) - { - var center2D = GetCurveCenterInMeter(RadioExterno); - var center3D = new Vector3(center2D.X, center2D.Y, 0); - curva.Create(RadioInterno, RadioExterno, startAngle, endAngle, center3D); - } - - public simCurve AddCurve(float RadioInterno, float RadioExterno, float startAngle, float endAngle) - { - var center2D = GetCurveCenterInMeter(RadioExterno); - var center3D = new Vector3(center2D.X, center2D.Y, 0); - return simulationManager.AddCurve(RadioInterno, RadioExterno, startAngle, endAngle, center3D); - } - */ - public simTransporte AddRectangle(SimulationManagerBEPU simulationManager, Rectangle wpfRect, float Alto, float Ancho, float Angulo) { var topLeft2D = GetRectangleTopLeft(wpfRect); @@ -1624,7 +1593,8 @@ namespace CtrEditor.ObjetosSim public simBarrera AddBarrera(SimulationManagerBEPU simulationManager, Rectangle wpfRect, float Alto, float Ancho, float Angulo, bool detectarCuello) { var topLeft2D = GetRectangleTopLeft(wpfRect); - return simulationManager.AddBarrera(Ancho, Alto, topLeft2D, Angulo, detectarCuello); + // Convertir Vector2 a Vector3 para BEPU (Z = 0 para objetos 2D) + return simulationManager.CreateBarrera(Ancho, Alto, topLeft2D, Angulo, detectarCuello); } public void UpdateOrCreateLine(simGuia simGuia, Rectangle wpfRect) @@ -1777,39 +1747,6 @@ namespace CtrEditor.ObjetosSim } } - /// - /// Verifica si las dimensiones de una barrera han cambiado - /// - protected 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 /// diff --git a/Simulacion/BEPU.cs b/Simulacion/BEPU.cs index d5098b8..5b6c272 100644 --- a/Simulacion/BEPU.cs +++ b/Simulacion/BEPU.cs @@ -11,1807 +11,15 @@ using BepuPhysics; using BepuPhysics.Collidables; using BepuPhysics.CollisionDetection; using BepuPhysics.Constraints; +using BepuPhysics.Trees; using BepuUtilities; using BepuUtilities.Memory; using CtrEditor.FuncionesBase; using DocumentFormat.OpenXml.Vml; +using DocumentFormat.OpenXml.Spreadsheet; 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 - protected SimulationManagerBEPU _simulationManager; // ✅ NUEVO: Referencia al manager - - // ✅ CORREGIDO: Restaurar factor de conversión correcto - public const float SPEED_CONVERSION_FACTOR = 1f; // 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, SimulationManagerBEPU simulationManager = null) - { - _simulation = simulation; - _deferredActions = deferredActions; - _simulationManager = simulationManager; // ✅ NUEVO: Almacenar referencia - - Width = width; - Height = height; - - // Usar el nuevo método Create que maneja Top-Left correctamente - Create(width, height, topLeft, angle); - - // ✅ INICIALIZAR PROPIEDADES CRÍTICAS - UpdateCachedProperties(); - } - - public float Angle - { - get - { - return GetRotationZ(); - } - set - { - SetRotation(value); - } - } - - /// - /// ✅ 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 limpia restricciones y elimina el body - /// - public new void RemoverBody() - { - // ✅ NUEVO: Limpiar restricciones de botellas conectadas antes de eliminar el cuerpo - if (_simulationManager != null) - { - var connectedBottles = _simulationManager.GetBottlesConnectedToElement(this); - foreach (var bottle in connectedBottles) - { - bottle.CleanRestrictions(this); - } - } - - 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 ConstraintHandle CurrentDistanceLimit { get; private set; } = default; // ✅ NUEVO: Para curvas - 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; - } - - // ✅ NORMALIZAR LA DIRECCIÓN TANGENCIAL - var tangentDir = Vector3.Normalize(direction); - - // ✅ CREAR MOTOR CONECTADO AL TARGET - var motor = new LinearAxisMotor() - { - LocalOffsetA = Vector3.Zero, // Target - LocalOffsetB = Vector3.Zero, // Botella - LocalAxis = tangentDir, // ✅ CORREGIDO: Usar la dirección tangencial calculada - TargetVelocity = speed, // ✅ CORREGIDO: Usar la velocidad directamente - Settings = new MotorSettings(Math.Max(_mass * 20f, 8f), 4f) - }; - - // ✅ CONECTAR BOTELLA CON EL TARGET (transporte o curva) - CurrentMotor = _simulation.Solver.Add(target.BodyHandle, BodyHandle, motor); - - CurrentMotorTarget = target; - _hasMotor = true; // ✅ ESTABLECER BANDERA - - //if (target is simCurve curva) { - // // Calcular el vector desde el centro de la curva hasta la botella (en el plano XY) - // var curveCenter = curva.CurveCenter; - // var bottlePosition = GetPosition(); - - // var radiusVector = new Vector3(bottlePosition.X - curveCenter.X, bottlePosition.Y - curveCenter.Y, 0f); - // var radius = radiusVector.Length(); - - // if (radius > 1e-3f) - // { - - // // Calcular offsets locales - // var localOffsetA = curveCenter; // Desde el centro de la curva hasta el punto de anclaje - // var localOffsetB = Vector3.Zero; // ✅ SIMPLIFICADO: Conectar al centro de la botella - - // var distanceLimit = new DistanceLimit() - // { - // LocalOffsetA = Vector3.Zero, - // LocalOffsetB = Vector3.Zero, - // MinimumDistance = radius- 4*Radius, // Distancia mínima = radio actual - // MaximumDistance = radius+ 4*Radius, // Distancia máxima = radio actual (mantener distancia fija) - // SpringSettings = new SpringSettings(30f, 0f) - // }; - - // //CurrentDistanceLimit = _simulation.Solver.Add(target.BodyHandle, BodyHandle, distanceLimit); - - // //System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget-Curve] 📏 DistanceLimit creado:"); - // //System.Diagnostics.Debug.WriteLine($" Radio actual: {radius:F3}"); - // //System.Diagnostics.Debug.WriteLine($" Punto de anclaje: {anchorPoint}"); - // //System.Diagnostics.Debug.WriteLine($" LocalOffsetA (curva): {localOffsetA}"); - // //System.Diagnostics.Debug.WriteLine($" LocalOffsetB (botella): {localOffsetB} (centro)"); - // //System.Diagnostics.Debug.WriteLine($" Distancia objetivo: {radius:F3}"); - // //System.Diagnostics.Debug.WriteLine($" DistanceLimit Handle: {CurrentDistanceLimit}"); - // } - //} - // ✅ 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; - } - - // ✅ NORMALIZAR LA DIRECCIÓN TANGENCIAL - var tangentDir = Vector3.Normalize(direction); - - // ✅ OBTENER LA DESCRIPCIÓN ACTUAL DEL MOTOR - _simulation.Solver.GetDescription(CurrentMotor, out LinearAxisMotor motor); - - // ✅ ACTUALIZAR DIRECCIÓN Y VELOCIDAD - motor.LocalAxis = tangentDir; - motor.TargetVelocity = speed; - - // ✅ 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}"); - } - } - - // ✅ NUEVO: Eliminar DistanceLimit si existe - if (CurrentDistanceLimit.Value != 0) - { - try - { - _simulation.Solver.Remove(CurrentDistanceLimit); - System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] 🗑️ DistanceLimit eliminado: {CurrentDistanceLimit}"); - } - catch (Exception removeEx) - { - System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] ⚠️ Error eliminando DistanceLimit {CurrentDistanceLimit}: {removeEx.Message}"); - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] ❌ ERROR: {ex.Message}"); - } - finally - { - // ✅ LIMPIAR REFERENCIAS SIEMPRE - CurrentMotor = default; - CurrentDistanceLimit = default; // ✅ NUEVO: Limpiar DistanceLimit - 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: Limpia las restricciones asociadas a un elemento específico - /// Solo limpia los datos internos de simBotella, no elimina las restricciones de BEPU - /// - /// Elemento (simTransporte o simCurve) del cual limpiar las restricciones - public void CleanRestrictions(simBase target) - { - try - { - // ✅ VERIFICAR SI EL TARGET COINCIDE CON EL ACTUAL - if (CurrentMotorTarget == target) - { - // ✅ LIMPIAR SOLO LOS DATOS INTERNOS (no eliminar restricciones de BEPU) - CurrentMotorTarget = null; - _hasMotor = false; // ✅ CRÍTICO: Limpiar el flag del motor - CurrentDirection = Vector3.UnitX; - CurrentSpeed = 0f; - IsOnElement = false; - - // ✅ NO LIMPIAR CurrentMotor ni CurrentDistanceLimit - BEPU los maneja automáticamente - - System.Diagnostics.Debug.WriteLine($"[CleanRestrictions] ✅ Restricciones limpiadas para {target?.GetType().Name}"); - } - else - { - System.Diagnostics.Debug.WriteLine($"[CleanRestrictions] ⚠️ Target no coincide: actual={CurrentMotorTarget?.GetType().Name}, solicitado={target?.GetType().Name}"); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[CleanRestrictions] ❌ ERROR: {ex.Message}"); - } - } - - - - 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: Propiedad para velocidad convertida (similar a simTransporte) - public float SpeedMetersPerSecond { get; private set; } - - - // ✅ 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, SimulationManagerBEPU simulationManager = null) - { - _simulation = simulation; - _deferredActions = deferredActions; - _simulationManager = simulationManager; // ✅ NUEVO: Almacenar referencia - _innerRadius = innerRadius; - _outerRadius = outerRadius; - // ✅ SIMPLIFICADO: Usar conversión directa WPF grados → BEPU radianes - _startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle); - _endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle); - - // ✅ NUEVO: Calcular y almacenar el centro real de la curva - var curveSize = outerRadius * 2f; - var zPosition = 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); - - // ✅ NUEVO: Inicializar SpeedMetersPerSecond - SpeedMetersPerSecond = Speed / simBase.SPEED_CONVERSION_FACTOR; - } - - // ✅ SIMPLIFICADO: Configurar velocidad angular para AngularAxisMotor - public void SetSpeed(float speed) - { - Speed = speed; // Velocidad angular directa (sin inversión) - SpeedMetersPerSecond = Speed / simBase.SPEED_CONVERSION_FACTOR; // ✅ NUEVO: Calcular velocidad convertida - OnSpeedChanged?.Invoke(this); - } - - /// - /// ✅ NUEVO: Configura la velocidad de la curva en metros por segundo - /// Valores positivos mueven en sentido horario, negativos en sentido antihorario - /// - /// Velocidad en m/s (típicamente entre -5.0 y 5.0) - public void SetCurveSpeed(float speedMeterPerSecond) - { - SetSpeed(speedMeterPerSecond * simBase.SPEED_CONVERSION_FACTOR); - } - - /// - /// ✅ NUEVO: Detiene completamente la curva - /// - public void StopCurve() - { - SetSpeed(0f); - } - - /// - /// ✅ NUEVO: Invierte la dirección de la curva manteniendo la misma velocidad - /// - public void ReverseCurve() - { - SetSpeed(-Speed); - } - - /// - /// ✅ 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 restricciones de botellas conectadas antes de eliminar el cuerpo - if (_simulationManager != null) - { - var connectedBottles = _simulationManager.GetBottlesConnectedToElement(this); - foreach (var bottle in connectedBottles) - { - bottle.CleanRestrictions(this); - } - } - - // ✅ 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 @@ -1833,8 +41,9 @@ namespace CtrEditor.Simulacion var curveB = _simulationManager.GetCurveFromTriangleBodyHandle(b.BodyHandle); var transportA = GetTransportFromCollidable(a); var transportB = GetTransportFromCollidable(b); - var barrierA = GetBarreraFromCollidable(a); - var barrierB = GetBarreraFromCollidable(b); + // ✅ ELIMINADO: Las barreras ya no tienen body físico + // var barrierA = GetBarreraFromCollidable(a); + // var barrierB = GetBarreraFromCollidable(b); var discardA = GetDescarteFromCollidable(a); var discardB = GetDescarteFromCollidable(b); var botellaA = GetBotellaFromCollidable(a); @@ -1852,9 +61,10 @@ namespace CtrEditor.Simulacion 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); + // ✅ FILTRO 3: No permitir colisiones entre elementos estáticos (transportes, curvas, descartes) + // ✅ ELIMINADO: barrierA y barrierB ya que las barreras no tienen body físico + var staticA = (transportA != null || curveA != null || discardA != null); + var staticB = (transportB != null || curveB != null || discardB != null); if (staticA && staticB) { @@ -1879,26 +89,27 @@ namespace CtrEditor.Simulacion // ✅ CONFIGURACIÓN BÁSICA de materiales físicos pairMaterial = new PairMaterialProperties { - FrictionCoefficient = 0.6f, - MaximumRecoveryVelocity = 2f, - SpringSettings = new SpringSettings(60, 4) + FrictionCoefficient = 0.3f, + MaximumRecoveryVelocity = 1f, + SpringSettings = new SpringSettings(80, 6) }; if (_simulationManager != null) { var botellaA = GetBotellaFromCollidable(pair.A); var botellaB = GetBotellaFromCollidable(pair.B); - var barreraA = GetBarreraFromCollidable(pair.A); - var barreraB = GetBarreraFromCollidable(pair.B); + // ✅ ELIMINADO: Las barreras ya no tienen body físico + // 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 - } + // ✅ ELIMINADO: Las barreras ya no participan en colisiones físicas + // if (barreraA != null || barreraB != null) + // { + // return false; // NO generar contacto físico + // } // ✅ DESCARTES como sensores puros if (descarteA != null || descarteB != null) @@ -1925,13 +136,13 @@ namespace CtrEditor.Simulacion // ✅ CREAR O ACTUALIZAR MOTOR DINÁMICO INMEDIATAMENTE transport.UpdateCachedProperties(); var direction = transport.DirectionVector; - var speed = transport.SpeedMetersPerSecond; + var speed = transport.Speed; botella.CreateOrUpdateMotor(transport, direction, speed); //Fricción alta para transportes - //pairMaterial.FrictionCoefficient = 0.9f; - //pairMaterial.MaximumRecoveryVelocity = 1f; - //pairMaterial.SpringSettings = new SpringSettings(80, 6); + pairMaterial.FrictionCoefficient = 0.3f; + 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)) @@ -1944,14 +155,22 @@ namespace CtrEditor.Simulacion botella.CreateOrUpdateMotor(curve, direction, speed); // Fricción alta para curvas - pairMaterial.FrictionCoefficient = 0.9f; + pairMaterial.FrictionCoefficient = 0.3f; + pairMaterial.MaximumRecoveryVelocity = 1f; + pairMaterial.SpringSettings = new SpringSettings(80, 6); + } + // ✅ CONTACTO BOTELLA-GUÍA: Configuración específica de fricción + else if (botella != null && (GetGuiaFromCollidable(pair.A) != null || GetGuiaFromCollidable(pair.B) != null)) + { + // Configuración específica para guías usando propiedades configurables + pairMaterial.FrictionCoefficient = 0.3f; pairMaterial.MaximumRecoveryVelocity = 1f; pairMaterial.SpringSettings = new SpringSettings(80, 6); } // Ajustes básicos para otras botellas else if (botella != null) { - pairMaterial.FrictionCoefficient = 0.9f; + pairMaterial.FrictionCoefficient = 0.3f; pairMaterial.MaximumRecoveryVelocity = 1f; pairMaterial.SpringSettings = new SpringSettings(80, 6); } @@ -1990,19 +209,8 @@ namespace CtrEditor.Simulacion 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; - } + // ✅ ELIMINADO: GetBarreraFromCollidable - las barreras ya no tienen body físico + // private simBarrera GetBarreraFromCollidable(CollidableReference collidable) { ... } private simDescarte GetDescarteFromCollidable(CollidableReference collidable) { @@ -2032,6 +240,20 @@ namespace CtrEditor.Simulacion return null; } + private simGuia GetGuiaFromCollidable(CollidableReference collidable) + { + if (_simulationManager?.simulation != null) + { + if (collidable.Mobility == CollidableMobility.Kinematic) + { + var bodyHandle = collidable.BodyHandle; + var simBase = _simulationManager.GetSimBaseFromBodyHandle(bodyHandle); + return simBase as simGuia; + } + } + return null; + } + @@ -2106,6 +328,7 @@ namespace CtrEditor.Simulacion } + public class SimulationManagerBEPU { public Simulation simulation; @@ -2127,6 +350,7 @@ namespace CtrEditor.Simulacion private HashSet _barrierHandles; private HashSet _discardHandles; private HashSet _bottleHandles; + private HashSet _guiaHandles; // ✅ NUEVO - contador de frames para optimizaciones @@ -2137,6 +361,12 @@ namespace CtrEditor.Simulacion private Dictionary> _descarteContacts; private HashSet _botellasParaEliminar; + // ✅ NUEVAS - propiedades configurables para fricción de guías + public float GuiaFrictionCoefficient { get; set; } = 0.1f; + public float GuiaMaxRecoveryVelocity { get; set; } = 0.5f; + public float GuiaSpringFrequency { get; set; } = 40f; + public float GuiaSpringDampingRatio { get; set; } = 2f; + private object _contactsLock = new object(); /// @@ -2207,11 +437,15 @@ namespace CtrEditor.Simulacion // ✅ SIMPLIFICADO: Ya no hay triángulos separados, solo el cuerpo principal break; case simBarrera barrier: - _barrierHandles.Add(barrier.BodyHandle); + // ✅ NUEVO: Las barreras ya no tienen body físico, no registrar handle + // _barrierHandles.Add(barrier.BodyHandle); // ELIMINADO break; case simDescarte discard: _discardHandles.Add(discard.BodyHandle); break; + case simGuia guia: + _guiaHandles.Add(guia.BodyHandle); + break; } } @@ -2231,11 +465,15 @@ namespace CtrEditor.Simulacion // ✅ SIMPLIFICADO: Ya no hay triángulos separados break; case simBarrera barrier: - _barrierHandles.Remove(barrier.BodyHandle); + // ✅ NUEVO: Las barreras ya no tienen body físico, no desregistrar handle + // _barrierHandles.Remove(barrier.BodyHandle); // ELIMINADO break; case simDescarte discard: _discardHandles.Remove(discard.BodyHandle); break; + case simGuia guia: + _guiaHandles.Remove(guia.BodyHandle); + break; } } @@ -2262,6 +500,11 @@ namespace CtrEditor.Simulacion return GetCurveFromTriangleBodyHandle(handle); } + private simGuia GetGuiaByHandle(BodyHandle handle) + { + return Cuerpos.OfType().FirstOrDefault(g => g.BodyHandle.Equals(handle)); + } + public SimulationManagerBEPU() { // ✅ EXISTENTES - conservar @@ -2271,12 +514,13 @@ namespace CtrEditor.Simulacion _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(); + // ✅ NUEVOS - sistemas de filtrado y contactos + _transportHandles = new HashSet(); + _curveHandles = new HashSet(); + _barrierHandles = new HashSet(); + _discardHandles = new HashSet(); + _bottleHandles = new HashSet(); + _guiaHandles = new HashSet(); // ✅ CONSERVAR - resto del constructor igual @@ -2324,12 +568,13 @@ namespace CtrEditor.Simulacion } - // ✅ NUEVO - limpiar clasificaciones - _transportHandles.Clear(); - _curveHandles.Clear(); - _barrierHandles.Clear(); - _discardHandles.Clear(); - _bottleHandles.Clear(); + // ✅ NUEVO - limpiar clasificaciones + _transportHandles.Clear(); + _curveHandles.Clear(); + _barrierHandles.Clear(); + _discardHandles.Clear(); + _bottleHandles.Clear(); + _guiaHandles.Clear(); // ✅ CONSERVAR - resto del método igual var cuerposToRemove = new List(Cuerpos); @@ -2582,12 +827,17 @@ namespace CtrEditor.Simulacion return transporte; } - public simBarrera AddBarrera(float width, float height, Vector2 position, float angle, bool detectarCuello) + internal simBarrera AddBarrera(float width, float height, Vector2 wpfTopLeft, float wpfAngle, bool detectarCuello) { - var barrera = new simBarrera(simulation, _deferredActions, width, height, position, angle, detectarCuello); - Cuerpos.Add(barrera); - RegisterObjectHandle(barrera); // ✅ NUEVO + // ✅ CONVERTIR coordenadas WPF a BEPU internas + var zPosition = simBase.zPos_Barrera; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); + var bepuRadians = CoordinateConverter.WpfDegreesToBepuRadians(wpfAngle); + var barrera = new simBarrera(simulation, width, height, bepuCenter, bepuRadians, detectarCuello, this); + Cuerpos.Add(barrera); + RegisterObjectHandle(barrera); + if (Is3DUpdateEnabled) { Visualization3DManager?.SynchronizeWorld(); @@ -2636,9 +886,80 @@ namespace CtrEditor.Simulacion return curve; } + // ✅ NUEVA INTERFAZ PARA BARRERAS - Solo accesible desde SimulationManagerBEPU + + /// + /// Crea una nueva barrera y devuelve un handle opaco para WPF + /// + /// Ancho en metros + /// Alto en metros + /// Posición central en coordenadas BEPU + /// Ángulo en radianes BEPU + /// Si debe detectar el cuello de las botellas + /// Handle opaco para referenciar la barrera desde WPF + public simBarrera CreateBarrera(float width, float height, Vector2 TopLeft, float wpfAngle, bool detectarCuello) + { + var barrera = new simBarrera(simulation, width, height, CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(TopLeft, width, height, wpfAngle, simBase.zPos_Barrera), CoordinateConverter.WpfDegreesToBepuRadians(wpfAngle), detectarCuello, this); + Cuerpos.Add(barrera); + RegisterObjectHandle(barrera); + + if (Is3DUpdateEnabled) + { + Visualization3DManager?.SynchronizeWorld(); + } + + return barrera; + } + + public void UpdateBarrera(simBarrera barrera, float width, float height, Vector2 TopLeft, float wpfAngle) + { + barrera.Update(width, height, CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(TopLeft, width, height, wpfAngle, simBase.zPos_Barrera), CoordinateConverter.WpfDegreesToBepuRadians(wpfAngle)); + + if (Is3DUpdateEnabled) + { + Visualization3DManager?.SynchronizeWorld(); + } + } + /// + /// Elimina una barrera usando su handle + /// + /// Handle de la barrera a eliminar + public void RemoveBarrera(simBarrera barrera) + { + if (barrera != null) + { + Remove(barrera); + } + } + /// + /// Configura si la barrera debe detectar el cuello de las botellas + /// + /// Handle de la barrera + /// True para detectar cuello, false para detectar botella completa + public void SetBarreraDetectNeck(simBarrera barrera, bool detectarCuello) + { + if (barrera != null) + { + barrera.DetectNeck = detectarCuello; + } + } + + /// + /// Obtiene los datos de estado de la barrera + /// + /// Handle de la barrera + /// Estructura con los datos de luz cortada + public BarreraData GetBarreraData(simBarrera barrera) + { + if (barrera != null) + { + return new BarreraData(barrera.LuzCortada, barrera.LuzCortadaNeck); + } + return new BarreraData(false, false); + } /// /// ✅ NUEVO: Detiene motores de botellas que no están en contacto con elementos @@ -2784,7 +1105,7 @@ namespace CtrEditor.Simulacion //System.Diagnostics.Debug.WriteLine($" Vector radial: {normalizedRadius}"); //System.Diagnostics.Debug.WriteLine($" Dirección tangencial: {tangentDirection} (Longitud: {tangentDirection.Length():F3})"); - return tangentDirection; + return -tangentDirection; } catch (Exception ex) { @@ -2807,7 +1128,7 @@ namespace CtrEditor.Simulacion /// /// Barrera que detecta el paso /// Botella que está pasando - public void RegisterBarreraContact(simBarrera barrera, simBotella botella) + internal void RegisterBarreraContact(simBarrera barrera, simBotella botella) { if (barrera != null && botella != null) { @@ -2858,43 +1179,32 @@ namespace CtrEditor.Simulacion /// - /// 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 + /// ✅ NUEVO: Procesa todas las barreras usando RayCast de BEPU + /// Elimina la dependencia de contactos físicos y la necesidad de mantener botellas despiertas /// private void ProcessBarreraContacts() { try { - // Obtener todas las barreras y botellas + // Obtener todas las barreras 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) + // Verificar que la barrera aún existe (solo verificar que no sea null) + if (barrera == null) continue; - // Resetear flags - barrera.LuzCortada = false; - barrera.LuzCortadaNeck = false; - barrera.Distancia = float.MaxValue; + // ✅ USAR RAYCAST NATIVO DE BEPU + barrera.PerformRaycast(); - // 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); + // System.Diagnostics.Debug.WriteLine($"[ProcessBarreraContacts] Barrera procesada: LuzCortada={barrera.LuzCortada}, LuzCortadaNeck={barrera.LuzCortadaNeck}, Distancia={barrera.Distancia:F3}"); } catch (Exception ex) { - // Error processing barrera - continue + System.Diagnostics.Debug.WriteLine($"[ProcessBarreraContacts] Error procesando barrera: {ex.Message}"); } } @@ -2906,135 +1216,12 @@ namespace CtrEditor.Simulacion } catch (Exception ex) { - // Critical error in ProcessBarrera - continue + System.Diagnostics.Debug.WriteLine($"[ProcessBarreraContacts] Error crítico: {ex.Message}"); } } - /// - /// 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; - } - } - } + // ✅ ELIMINADO: CalculateBarreraDetectionGeometric - ya no se usa + // El nuevo sistema usa RayCast nativo de BEPU a través de simBarrera.PerformRaycast() /// /// Procesa contactos de descarte para eliminar botellas marcadas usando detección geométrica @@ -3273,9 +1460,5 @@ namespace CtrEditor.Simulacion return lineStart + lineDirection * projectionLength; } - - - - } } \ No newline at end of file diff --git a/Simulacion/BEPUVisualization3D.cs b/Simulacion/BEPUVisualization3D.cs index 20ff637..389d925 100644 --- a/Simulacion/BEPUVisualization3D.cs +++ b/Simulacion/BEPUVisualization3D.cs @@ -163,6 +163,26 @@ namespace CtrEditor.Simulacion // Sincronizar cada objeto simBase foreach (var simObj in simulationManager.Cuerpos) { + // ✅ NUEVO: Caso especial para simBarrera (no tiene BodyHandle físico) + if (simObj is simBarrera barrera) + { + //System.Diagnostics.Debug.WriteLine($"[3D Sync] 🔍 Procesando simBarrera..."); + if (simBaseToModelMap.ContainsKey(simObj)) + { + //System.Diagnostics.Debug.WriteLine($"[3D Sync] 🔄 Actualizando visualización existente de barrera"); + // Actualizar visualización existente de barrera + UpdateBarreraVisualization(barrera); + } + else + { + //System.Diagnostics.Debug.WriteLine($"[3D Sync] 🆕 Creando nueva visualización de barrera"); + // Crear nueva visualización de barrera + CreateBarreraVisualization(barrera); + } + continue; + } + + // Para objetos con BodyHandle físico if (simObj == null || !simulationManager.simulation.Bodies.BodyExists(simObj.BodyHandle)) continue; @@ -193,15 +213,31 @@ namespace CtrEditor.Simulacion var simObj = kvp.Key; var model = kvp.Value; - // Verificar si el objeto aún está en la lista de cuerpos activos - if (!simulationManager.Cuerpos.Contains(simObj) || - !simulationManager.simulation.Bodies.BodyExists(simObj.BodyHandle)) + // ✅ NUEVO: Caso especial para simBarrera (no tiene BodyHandle físico) + if (simObj is simBarrera) { - if (model != null) + // Verificar si la barrera aún está en la lista de cuerpos activos + if (!simulationManager.Cuerpos.Contains(simObj)) { - viewport3D.Children.Remove(model); + if (model != null) + { + viewport3D.Children.Remove(model); + } + objectsToRemove.Add(simObj); + } + } + else + { + // Para objetos con BodyHandle físico + if (!simulationManager.Cuerpos.Contains(simObj) || + !simulationManager.simulation.Bodies.BodyExists(simObj.BodyHandle)) + { + if (model != null) + { + viewport3D.Children.Remove(model); + } + objectsToRemove.Add(simObj); } - objectsToRemove.Add(simObj); } } @@ -888,13 +924,13 @@ namespace CtrEditor.Simulacion } else if (simObj is simBarrera) { - // Material semi-transparente amarillo para barreras (haz de luz) - var yellowBrush = new SolidColorBrush(Color.FromRgb(255, 255, 0)); - yellowBrush.Opacity = 0.3; // 30% opacidad para simular haz de luz + // ✅ MEJORADO: Material para haz de luz más realista + var lightBeamBrush = new SolidColorBrush(Color.FromRgb(255, 255, 100)); // Amarillo cálido + lightBeamBrush.Opacity = 0.6; // 60% opacidad para simular haz de luz visible return MaterialHelper.CreateMaterial( - yellowBrush, - specularPower: 50, - ambient: 150 + lightBeamBrush, + specularPower: 80, // Algo de brillo para simular luz + ambient: 200 // Ambiente alto para que "emita" luz ); } else if (simObj is simBotella) @@ -1173,6 +1209,213 @@ namespace CtrEditor.Simulacion System.Diagnostics.Debug.WriteLine($"[3D Camera] ERROR subscribing to camera events: {ex.Message}"); } } + + /// + /// ✅ NUEVO: Crea la visualización del haz de luz para simBarrera + /// Muestra una línea semi-transparente que representa el RayCast + /// + /// Barrera que usa RayCast para detección + private void CreateBarreraVisualization(simBarrera barrera) + { + try + { + var visual3D = CreateLightBeamVisualization(barrera); + + if (visual3D != null) + { + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ✅ Visual3D creado, actualizando transformación..."); + + // Posicionar y orientar el haz de luz + UpdateBarreraTransform(barrera, visual3D); + + // Agregar a la vista 3D y asociar con simBase + viewport3D.Children.Add(visual3D); + simBaseToModelMap[barrera] = visual3D; + + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ✅ Haz de luz creado exitosamente y agregado al viewport"); + System.Diagnostics.Debug.WriteLine($" Total children en viewport: {viewport3D.Children.Count}"); + } + else + { + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ CreateLightBeamVisualization devolvió null"); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ ERROR creando visualización: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($" Stack trace: {ex.StackTrace}"); + } + } + + /// + /// ✅ NUEVO: Actualiza la visualización existente de simBarrera + /// + /// Barrera a actualizar + private void UpdateBarreraVisualization(simBarrera barrera) + { + try + { + if (!simBaseToModelMap.TryGetValue(barrera, out var visual)) + return; + + // Verificar si las dimensiones han cambiado + var currentDimensions = GetBarreraDimensions(barrera); + bool dimensionsChanged = false; + + if (lastKnownDimensions.TryGetValue(barrera, out var lastDimensions)) + { + dimensionsChanged = !currentDimensions.Equals(lastDimensions); + } + else + { + dimensionsChanged = true; // Primera vez + } + + // Si las dimensiones cambiaron, recrear la geometría + if (dimensionsChanged) + { + RecreateBarreraGeometry(barrera, visual, currentDimensions); + lastKnownDimensions[barrera] = currentDimensions; + } + + // Actualizar transformación (posición y rotación) + UpdateBarreraTransform(barrera, visual); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ ERROR actualizando visualización: {ex.Message}"); + } + } + + /// + /// ✅ NUEVO: Crea la geometría del haz de luz (línea semi-transparente) + /// + /// Barrera para crear el haz + /// Modelo visual del haz de luz + private ModelVisual3D CreateLightBeamVisualization(simBarrera barrera) + { + try + { + var meshBuilder = new MeshBuilder(); + + // Crear una línea delgada que representa el haz de luz + var halfWidth = barrera.Width / 2f; + var beamThickness = 0.005f; // 5mm de grosor para el haz + var beamHeight = 0.02f; // 2cm de altura para que sea visible + + // Puntos del haz de luz (de extremo a extremo) + var startPoint = new Point3D(-halfWidth, 0, beamHeight / 2); + var endPoint = new Point3D(halfWidth, 0, beamHeight / 2); + + // Crear cilindro delgado para el haz principal + meshBuilder.AddCylinder(startPoint, endPoint, beamThickness, 8, false, false); + + // ✅ OPCIONAL: Agregar pequeñas esferas en los extremos para marcar emisor y receptor + meshBuilder.AddSphere(new Point3D(-halfWidth, 0, 2 * beamThickness), beamThickness * 2, 6, 6); // Emisor + //meshBuilder.AddSphere(new Point3D(halfWidth, 0, 2 * beamThickness), beamThickness * 2, 6, 6); // Receptor + + // Material semi-transparente para simular haz de luz + var material = GetMaterialForSimBase(barrera); + var model = new GeometryModel3D(meshBuilder.ToMesh(), material); + var visual = new ModelVisual3D { Content = model }; + + return visual; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ ERROR creando geometría del haz: {ex.Message}"); + return null; + } + } + + /// + /// ✅ NUEVO: Actualiza la transformación (posición y rotación) de la barrera + /// + /// Barrera a transformar + /// Visual 3D a transformar + private void UpdateBarreraTransform(simBarrera barrera, ModelVisual3D visual) + { + try + { + var transform = new Transform3DGroup(); + + // ✅ CORREGIDO: rotationZ ya está en grados WPF, no necesita conversión adicional + transform.Children.Add(new RotateTransform3D( + new AxisAngleRotation3D(new Vector3D(0, 0, 1), barrera.AngleRadians))); + + // Traslación + transform.Children.Add(new TranslateTransform3D(barrera.Position.X, barrera.Position.Y, barrera.Position.Z)); + + visual.Transform = transform; + + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ ERROR actualizando transformación: {ex.Message}"); + } + } + + /// + /// ✅ NUEVO: Obtiene las dimensiones de una barrera para detectar cambios + /// + /// Barrera de la cual obtener dimensiones + /// Dimensiones de la barrera + private ShapeDimensions GetBarreraDimensions(simBarrera barrera) + { + return new ShapeDimensions + { + Width = barrera.Width, + Height = barrera.Height, + Length = 0, + Radius = 0, + ShapeType = -3, // Tipo especial para barreras + InnerRadius = 0, + OuterRadius = 0, + StartAngle = 0, + EndAngle = 0 + }; + } + + /// + /// ✅ NUEVO: Recrea la geometría de la barrera cuando cambian las dimensiones + /// + /// Barrera a recrear + /// Visual 3D a actualizar + /// Nuevas dimensiones + private void RecreateBarreraGeometry(simBarrera barrera, ModelVisual3D visual, ShapeDimensions dimensions) + { + if (visual?.Content is not GeometryModel3D geometryModel) + return; + + try + { + var meshBuilder = new MeshBuilder(); + + // Recrear la geometría del haz con las nuevas dimensiones + var halfWidth = dimensions.Width / 2f; + var beamThickness = 0.005f; + var beamHeight = 0.02f; + + var startPoint = new Point3D(-halfWidth, 0, beamHeight / 2); + var endPoint = new Point3D(halfWidth, 0, beamHeight / 2); + + meshBuilder.AddCylinder(startPoint, endPoint, beamThickness, 8, false, false); + meshBuilder.AddSphere(new Point3D(-halfWidth, 0, 2*beamThickness), beamThickness * 2, 6, 6); + //meshBuilder.AddSphere(new Point3D(halfWidth, 0, 2*beamThickness), beamThickness * 2, 6, 6); + + var newGeometry = meshBuilder.ToMesh(); + var newMaterial = GetMaterialForSimBase(barrera); + + geometryModel.Geometry = newGeometry; + geometryModel.Material = newMaterial; + + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ✅ Geometría recreada con ancho: {dimensions.Width}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[3D Barrera] ❌ ERROR recreando geometría: {ex.Message}"); + } + } } public enum CameraView diff --git a/Simulacion/CoordinateConverter.cs b/Simulacion/CoordinateConverter.cs new file mode 100644 index 0000000..ae9d237 --- /dev/null +++ b/Simulacion/CoordinateConverter.cs @@ -0,0 +1,240 @@ +using BepuPhysics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +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 + /// + private static float WpfAngleToBepuAngle(float wpfAngle) + { + return -wpfAngle; + } + + /// + /// Convierte ángulo de BEPU a WPF (invierte signo) - USO INTERNO + /// + private 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 + var bepuY = WpfYToBepuY(centerY); + var result = new Vector3(centerX, bepuY, zPosition); + + return result; + } + + /// + /// 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)); + } + } + +} diff --git a/Simulacion/simBarrera.cs b/Simulacion/simBarrera.cs new file mode 100644 index 0000000..cbbb3db --- /dev/null +++ b/Simulacion/simBarrera.cs @@ -0,0 +1,344 @@ +using BepuPhysics; +using BepuPhysics.Collidables; +using BepuPhysics.Trees; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace CtrEditor.Simulacion +{ + /// + /// ✅ CORREGIDO: RayHitHandler personalizado para simBarrera + /// Calcula la distancia mínima del rayo al centro de cada botella + /// + public struct BarreraRayHitHandler : IRayHitHandler + { + private simBarrera _barrera; + private List _detectedBottles; + private SimulationManagerBEPU _simulationManager; + private float _minDistance; + private bool _neckDetected; + private bool _fullDetected; + private Vector3 _rayOrigin; + private Vector3 _rayDirection; + + internal BarreraRayHitHandler(simBarrera barrera, SimulationManagerBEPU simulationManager, Vector3 rayOrigin, Vector3 rayDirection) + { + _barrera = barrera; + _simulationManager = simulationManager; + _detectedBottles = new List(); + _minDistance = float.MaxValue; + _neckDetected = false; + _fullDetected = false; + _rayOrigin = rayOrigin; + _rayDirection = rayDirection; + } + + public bool AllowTest(CollidableReference collidable) + { + // Solo testear botellas dinámicas + if (collidable.Mobility == CollidableMobility.Dynamic) + { + var bottle = _simulationManager?.GetSimBaseFromBodyHandle(collidable.BodyHandle) as simBotella; + return bottle != null; + } + return false; + } + + public bool AllowTest(CollidableReference collidable, int childIndex) + { + // Para botellas (esferas), childIndex siempre es 0 + return AllowTest(collidable); + } + + public void OnRayHit(in RayData ray, ref float maximumT, float t, Vector3 normal, CollidableReference collidable, int childIndex) + { + try + { + // Obtener la botella que fue hit + var bottle = _simulationManager?.GetSimBaseFromBodyHandle(collidable.BodyHandle) as simBotella; + if (bottle == null) return; + + // Obtener posición del centro de la botella + var bottleCenter = bottle.GetPosition(); + + // ✅ CORREGIDO: Calcular la distancia mínima del rayo (línea) al centro de la botella + var minDistanceToCenter = CalculateMinimumDistanceFromRayToPoint(_rayOrigin, _rayDirection, bottleCenter); + + // Actualizar distancia mínima global + if (minDistanceToCenter < _minDistance) + { + _minDistance = minDistanceToCenter; + } + + // ✅ CORREGIDO: Verificar detección completa usando distancia mínima del rayo al centro + if (minDistanceToCenter <= bottle.Radius) + { + _fullDetected = true; + if (!_detectedBottles.Contains(bottle)) + { + _detectedBottles.Add(bottle); + } + } + + // ✅ CORREGIDO: Verificar detección de cuello usando circunferencia imaginaria con radio/2 + if (_barrera.DetectNeck && minDistanceToCenter <= (bottle.Radius / 2f)) + { + _neckDetected = true; + } + + // System.Diagnostics.Debug.WriteLine($"[BarreraRayHitHandler] Hit: Botella={bottle.BodyHandle}, distanciaMinima={minDistanceToCenter:F3}, radio={bottle.Radius:F3}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[BarreraRayHitHandler] Error en OnRayHit: {ex.Message}"); + } + } + + /// + /// ✅ NUEVO: Calcula la distancia mínima de un rayo (línea) a un punto + /// + private float CalculateMinimumDistanceFromRayToPoint(Vector3 rayOrigin, Vector3 rayDirection, Vector3 point) + { + // Vector desde el origen del rayo al punto + var rayToPoint = point - rayOrigin; + + // Proyección del vector rayToPoint sobre la dirección del rayo + var projection = Vector3.Dot(rayToPoint, rayDirection); + + // Punto más cercano en el rayo al punto dado + var closestPointOnRay = rayOrigin + rayDirection * projection; + + // Distancia mínima del rayo al punto + return Vector3.Distance(closestPointOnRay, point); + } + + // ✅ NUEVO: Método para obtener los resultados del raycast + public void GetResults(out bool luzCortada, out bool luzCortadaNeck, out float distancia, out List botellas) + { + luzCortada = _fullDetected; + luzCortadaNeck = _neckDetected; + distancia = _minDistance == float.MaxValue ? 0f : _minDistance; + botellas = new List(_detectedBottles); + } + } + + + /// + /// Estructura para datos de barrera que se exponen a WPF + /// + public struct BarreraData + { + public bool LuzCortada; + public bool LuzCortadaNeck; + + public BarreraData(bool luzCortada, bool luzCortadaNeck) + { + LuzCortada = luzCortada; + LuzCortadaNeck = luzCortadaNeck; + } + } + + public class simBarrera : simBase + { + internal bool LuzCortada; + internal bool LuzCortadaNeck; + public List ListSimBotellaContact; + public float Width { get; set; } + public float Height { get; set; } + public bool DetectNeck { get; set; } + + // ✅ NUEVO: Propiedades para raycast + private SimulationManagerBEPU _simulationManager; + + // ✅ CORREGIDO: Almacenar ángulo internamente como radianes BEPU (como simCurve) + private float _angleRadians; // ✅ SIEMPRE en radianes BEPU (ya convertido desde WPF) + + internal simBarrera(Simulation simulation, float width, float height, Vector3 bepuPosition, float bepuRadians = 0, bool detectectNeck = false, SimulationManagerBEPU simulationManager = null) + { + _simulation = simulation; + _simulationManager = simulationManager; + Width = width; + Height = height; + DetectNeck = detectectNeck; + ListSimBotellaContact = new List(); + + // ✅ CREAR POSICIÓN SIN BODY FÍSICO usando parámetros BEPU internos + CreatePosition(width, height, bepuPosition, bepuRadians); + } + + internal void Update(float width, float height, Vector3 bepuPosition, float bepuRadians = 0) + { + Width = width; + Height = height; + ListSimBotellaContact = new List(); + + // ✅ CREAR POSICIÓN SIN BODY FÍSICO usando parámetros BEPU internos + CreatePosition(width, height, bepuPosition, bepuRadians); + } + + + // ✅ NUEVO: Variables para almacenar pose sin crear body físico + private Vector3 _position; + private Quaternion _orientation; + private bool _poseInitialized = false; + + // ✅ NUEVO: Propiedad pública para acceder a la posición + public Vector3 Position + { + get => _poseInitialized ? _position : Vector3.Zero; + set + { + _position = value; + _poseInitialized = true; + } + } + + // ✅ NUEVO: Propiedad pública para acceder al ángulo en radianes BEPU + public float AngleRadians + { + get => _poseInitialized ? _angleRadians : 0f; + set + { + _angleRadians = value; + UpdateOrientationFromInternalAngle(); + _poseInitialized = true; + } + } + + // ✅ NUEVO: Método interno para actualizar orientación desde ángulo interno + private void UpdateOrientationFromInternalAngle() + { + // ✅ CREAR QUATERNION DESDE RADIANES BEPU INTERNOS + _orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, _angleRadians); + } + + /// + /// ✅ INTERNO: Actualiza posición y rotación desde parámetros BEPU internos + /// + internal void Update(Vector3 bepuPosition, float bepuRadians) + { + Position = bepuPosition; + AngleRadians = bepuRadians; + } + + public void SetDimensions(float width, float height) + { + Width = width; + Height = height; + } + + /// + /// ✅ INTERNO: Crea la posición usando parámetros BEPU internos + /// + internal void CreatePosition(float width, float height, Vector3 bepuPosition, float bepuRadians) + { + Width = width; + Height = height; + + // ✅ ALMACENAR DIRECTAMENTE parámetros BEPU internos + Position = bepuPosition; + AngleRadians = bepuRadians; + + // ✅ NO CREAR BODY FÍSICO - solo almacenar datos + _bodyCreated = false; + } + + /// + /// ✅ CORREGIDO: Realiza raycast para detectar botellas usando coordenadas BEPU internas + /// + public void PerformRaycast() + { + try + { + // Resetear flags + LuzCortada = false; + LuzCortadaNeck = false; + ListSimBotellaContact?.Clear(); + + // Validar que tenemos simulación y manager + if (_simulation == null || _simulationManager == null || !_poseInitialized) + { + System.Diagnostics.Debug.WriteLine("[PerformRaycast] Simulación, manager o pose no inicializados"); + return; + } + + // ✅ CORREGIDO: Usar coordenadas BEPU internas para cálculos + var bepuPosition = Position; + var Orientation = _orientation; + + // ✅ CORREGIDO: Crear puntos del ray a la altura correcta + var halfWidth = Width / 2f; + var rayStartLocal = new Vector3(-halfWidth, 0, 0); + var rayEndLocal = new Vector3(halfWidth, 0, 0); + + // ✅ CORREGIDO: Ajustar la posición de la barrera para que el ray esté a la altura correcta + var barreraPosition = new Vector3(bepuPosition.X, bepuPosition.Y, bepuPosition.Z); + + // ✅ TRANSFORMAR PUNTOS LOCALES A COORDENADAS MUNDIALES A LA ALTURA CORRECTA + var worldStart = barreraPosition + Vector3.Transform(rayStartLocal, Orientation); + var worldEnd = barreraPosition + Vector3.Transform(rayEndLocal, Orientation); + + // ✅ CALCULAR DIRECCIÓN Y DISTANCIA DEL RAY + var rayDirection = worldEnd - worldStart; + var rayDistance = rayDirection.Length(); + + if (rayDistance < 0.001f) + { + System.Diagnostics.Debug.WriteLine("[PerformRaycast] Ray demasiado corto"); + return; + } + + rayDirection = Vector3.Normalize(rayDirection); + + // ✅ CORREGIDO: CREAR HANDLER PARA PROCESAR HITS con parámetros correctos + var rayHandler = new BarreraRayHitHandler(this, _simulationManager, worldStart, rayDirection); + + // ✅ REALIZAR RAYCAST + _simulation.RayCast(worldStart, rayDirection, rayDistance, ref rayHandler); + + // ✅ OBTENER RESULTADOS + rayHandler.GetResults(out bool luzCortada, out bool luzCortadaNeck, out float distancia, out List botellas); + + // ✅ ASIGNAR RESULTADOS + LuzCortada = luzCortada; + LuzCortadaNeck = luzCortadaNeck; + + if (ListSimBotellaContact != null) + { + ListSimBotellaContact.Clear(); + ListSimBotellaContact.AddRange(botellas); + } + + //System.Diagnostics.Debug.WriteLine($"[PerformRaycast] {worldStart} - {worldEnd} - {rayDistance} - {rayDirection} "); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[PerformRaycast] Error: {ex.Message}"); + } + } + + /// + /// ✅ INTERNO: Métodos Create que trabajan solo con parámetros BEPU internos + /// + internal void Create(float width, float height, Vector3 bepuPosition, float bepuRadians = 0, bool detectectNeck = false) + { + CreatePosition(width, height, bepuPosition, bepuRadians); + } + + /// + /// ✅ OVERRIDE: RemoverBody no hace nada porque no hay body físico + /// + public new void RemoverBody() + { + // ✅ NO HAY BODY FÍSICO QUE REMOVER + _bodyCreated = false; + _poseInitialized = false; + } + + } + +} diff --git a/Simulacion/simBase.cs b/Simulacion/simBase.cs new file mode 100644 index 0000000..6bef779 --- /dev/null +++ b/Simulacion/simBase.cs @@ -0,0 +1,139 @@ +using BepuPhysics.Collidables; +using BepuPhysics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace CtrEditor.Simulacion +{ + public class simBase + { + public BodyHandle BodyHandle { get; protected set; } + public Simulation _simulation; + protected bool _bodyCreated = false; // Bandera para saber si hemos creado un cuerpo + protected SimulationManagerBEPU _simulationManager; // ✅ NUEVO: Referencia al manager + + // ✅ CORREGIDO: Restaurar factor de conversión correcto + public const float SPEED_CONVERSION_FACTOR = 1.55f; // 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 zPos_Barrera = zPos_Transporte + zAltura_Transporte + 0.05f; // Z de la parte baja - 0.1 Altura Botella + public const float zPos_Descarte = 0.1f; // Z de la parte baja + + // Constantes para configuración + public const float zPos_Curve = zPos_Transporte+ zAltura_Transporte; // Z de la parte alta de la curva + + + public void RemoverBody() + { + try + { + // Solo intentar remover si realmente hemos creado un cuerpo antes + if (_bodyCreated && _simulation != null && _simulation.Bodies != null && _simulation.Bodies.BodyExists(BodyHandle)) + { + _simulation.Bodies.Remove(BodyHandle); + _bodyCreated = false; // Marcar como no creado después de remover + //System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ✅ Body eliminado: {BodyHandle}"); + } + //else + //{ + // System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ⚠️ Body no existe o no creado: {BodyHandle}"); + //} + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[simBase.RemoverBody] ❌ ERROR: {ex.Message}"); + _bodyCreated = false; // Marcar como no creado en caso de error + } + } + + /// + /// ✅ 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); + } + } + +} diff --git a/Simulacion/simBotella.cs b/Simulacion/simBotella.cs new file mode 100644 index 0000000..92e3c42 --- /dev/null +++ b/Simulacion/simBotella.cs @@ -0,0 +1,553 @@ +using BepuPhysics.Collidables; +using BepuPhysics.Constraints; +using BepuPhysics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using DocumentFormat.OpenXml.Spreadsheet; + +namespace CtrEditor.Simulacion +{ + + public class simBotella : simBase + { + public float Radius; + public float Height; // Altura para la visualización del cilindro en Helix + private float _mass; + public bool Descartar = false; + public int isOnTransports; + public List 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 ConstraintHandle CurrentDistanceLimit { get; private set; } = default; // ✅ NUEVO: Para curvas + 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 / simBase.SPEED_CONVERSION_FACTOR); + return; + } + + // ✅ CREAR NUEVO MOTOR SI ES NECESARIO + if (needsNewMotor && target != null) + { + CreateMotorForTarget(target, direction, speed / simBase.SPEED_CONVERSION_FACTOR); + } + } + 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; + } + + // ✅ NORMALIZAR LA DIRECCIÓN TANGENCIAL + var tangentDir = Vector3.Normalize(direction); + + // ✅ CREAR MOTOR CONECTADO AL TARGET + var motor = new LinearAxisMotor() + { + LocalOffsetA = Vector3.Zero, // Target + LocalOffsetB = Vector3.Zero, // Botella + LocalAxis = tangentDir, // ✅ CORREGIDO: Usar la dirección tangencial calculada + TargetVelocity = speed, // ✅ CORREGIDO: Usar la velocidad directamente + Settings = new MotorSettings(Math.Max(_mass * 20f, 8f), 4f) + }; + + // ✅ CONECTAR BOTELLA CON EL TARGET (transporte o curva) + CurrentMotor = _simulation.Solver.Add(target.BodyHandle, BodyHandle, motor); + + CurrentMotorTarget = target; + _hasMotor = true; // ✅ ESTABLECER BANDERA + + //if (target is simCurve curva) { + // // Calcular el vector desde el centro de la curva hasta la botella (en el plano XY) + // var curveCenter = curva.CurveCenter; + // var bottlePosition = GetPosition(); + + // var radiusVector = new Vector3(bottlePosition.X - curveCenter.X, bottlePosition.Y - curveCenter.Y, 0f); + // var radius = radiusVector.Length(); + + // if (radius > 1e-3f) + // { + + // // Calcular offsets locales + // var localOffsetA = curveCenter; // Desde el centro de la curva hasta el punto de anclaje + // var localOffsetB = Vector3.Zero; // ✅ SIMPLIFICADO: Conectar al centro de la botella + + // var distanceLimit = new DistanceLimit() + // { + // LocalOffsetA = Vector3.Zero, + // LocalOffsetB = Vector3.Zero, + // MinimumDistance = radius- 4*Radius, // Distancia mínima = radio actual + // MaximumDistance = radius+ 4*Radius, // Distancia máxima = radio actual (mantener distancia fija) + // SpringSettings = new SpringSettings(30f, 0f) + // }; + + // //CurrentDistanceLimit = _simulation.Solver.Add(target.BodyHandle, BodyHandle, distanceLimit); + + // //System.Diagnostics.Debug.WriteLine($"[CreateMotorForTarget-Curve] 📏 DistanceLimit creado:"); + // //System.Diagnostics.Debug.WriteLine($" Radio actual: {radius:F3}"); + // //System.Diagnostics.Debug.WriteLine($" Punto de anclaje: {anchorPoint}"); + // //System.Diagnostics.Debug.WriteLine($" LocalOffsetA (curva): {localOffsetA}"); + // //System.Diagnostics.Debug.WriteLine($" LocalOffsetB (botella): {localOffsetB} (centro)"); + // //System.Diagnostics.Debug.WriteLine($" Distancia objetivo: {radius:F3}"); + // //System.Diagnostics.Debug.WriteLine($" DistanceLimit Handle: {CurrentDistanceLimit}"); + // } + //} + // ✅ 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; + } + + // ✅ NORMALIZAR LA DIRECCIÓN TANGENCIAL + var tangentDir = Vector3.Normalize(direction); + + // ✅ OBTENER LA DESCRIPCIÓN ACTUAL DEL MOTOR + _simulation.Solver.GetDescription(CurrentMotor, out LinearAxisMotor motor); + + // ✅ ACTUALIZAR DIRECCIÓN Y VELOCIDAD + motor.LocalAxis = tangentDir; + motor.TargetVelocity = speed; + + // ✅ 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}"); + } + } + + // ✅ NUEVO: Eliminar DistanceLimit si existe + if (CurrentDistanceLimit.Value != 0) + { + try + { + _simulation.Solver.Remove(CurrentDistanceLimit); + System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] 🗑️ DistanceLimit eliminado: {CurrentDistanceLimit}"); + } + catch (Exception removeEx) + { + System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] ⚠️ Error eliminando DistanceLimit {CurrentDistanceLimit}: {removeEx.Message}"); + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[RemoveCurrentMotor] ❌ ERROR: {ex.Message}"); + } + finally + { + // ✅ LIMPIAR REFERENCIAS SIEMPRE + CurrentMotor = default; + CurrentDistanceLimit = default; // ✅ NUEVO: Limpiar DistanceLimit + 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: Limpia las restricciones asociadas a un elemento específico + /// Solo limpia los datos internos de simBotella, no elimina las restricciones de BEPU + /// + /// Elemento (simTransporte o simCurve) del cual limpiar las restricciones + public void CleanRestrictions(simBase target) + { + try + { + // ✅ VERIFICAR SI EL TARGET COINCIDE CON EL ACTUAL + if (CurrentMotorTarget == target) + { + // ✅ LIMPIAR SOLO LOS DATOS INTERNOS (no eliminar restricciones de BEPU) + CurrentMotorTarget = null; + _hasMotor = false; // ✅ CRÍTICO: Limpiar el flag del motor + CurrentDirection = Vector3.UnitX; + CurrentSpeed = 0f; + IsOnElement = false; + + // ✅ NO LIMPIAR CurrentMotor ni CurrentDistanceLimit - BEPU los maneja automáticamente + + System.Diagnostics.Debug.WriteLine($"[CleanRestrictions] ✅ Restricciones limpiadas para {target?.GetType().Name}"); + } + else + { + System.Diagnostics.Debug.WriteLine($"[CleanRestrictions] ⚠️ Target no coincide: actual={CurrentMotorTarget?.GetType().Name}, solicitado={target?.GetType().Name}"); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[CleanRestrictions] ❌ ERROR: {ex.Message}"); + } + } + + + + 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(); + } + + } + +} diff --git a/Simulacion/simCurve.cs b/Simulacion/simCurve.cs new file mode 100644 index 0000000..ac56a8d --- /dev/null +++ b/Simulacion/simCurve.cs @@ -0,0 +1,368 @@ +using BepuPhysics.Collidables; +using BepuPhysics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace CtrEditor.Simulacion +{ + /// + /// 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: Propiedad para velocidad convertida (similar a simTransporte) + public float SpeedMetersPerSecond { get; private set; } + + + // ✅ 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, SimulationManagerBEPU simulationManager = null) + { + _simulation = simulation; + _deferredActions = deferredActions; + _simulationManager = simulationManager; // ✅ NUEVO: Almacenar referencia + _innerRadius = innerRadius; + _outerRadius = outerRadius; + // ✅ SIMPLIFICADO: Usar conversión directa WPF grados → BEPU radianes + _startAngle = CoordinateConverter.WpfDegreesToBepuRadians(startAngle); + _endAngle = CoordinateConverter.WpfDegreesToBepuRadians(endAngle); + + // ✅ NUEVO: Calcular y almacenar el centro real de la curva + var curveSize = outerRadius * 2f; + var zPosition = zPos_Curve; + _curveCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(topLeft, curveSize, curveSize, 0f, zPosition); + + // ✅ SIMPLIFICADO: Crear la curva directamente + Create(innerRadius, outerRadius, startAngle, endAngle, topLeft, 0); + + // ✅ NUEVO: Inicializar SpeedMetersPerSecond + SpeedMetersPerSecond = Speed / simBase.SPEED_CONVERSION_FACTOR; + } + + // ✅ SIMPLIFICADO: Configurar velocidad angular para AngularAxisMotor + public void SetSpeed(float speed) + { + Speed = speed; // Velocidad angular directa (sin inversión) + SpeedMetersPerSecond = Speed / simBase.SPEED_CONVERSION_FACTOR; // ✅ NUEVO: Calcular velocidad convertida + OnSpeedChanged?.Invoke(this); + } + + /// + /// ✅ NUEVO: Configura la velocidad de la curva en metros por segundo + /// Valores positivos mueven en sentido horario, negativos en sentido antihorario + /// + /// Velocidad en m/s (típicamente entre -5.0 y 5.0) + public void SetCurveSpeed(float speedMeterPerSecond) + { + SetSpeed(speedMeterPerSecond * simBase.SPEED_CONVERSION_FACTOR); + } + + /// + /// ✅ NUEVO: Detiene completamente la curva + /// + public void StopCurve() + { + SetSpeed(0f); + } + + /// + /// ✅ NUEVO: Invierte la dirección de la curva manteniendo la misma velocidad + /// + public void ReverseCurve() + { + SetSpeed(-Speed); + } + + /// + /// ✅ 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 = 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 restricciones de botellas conectadas antes de eliminar el cuerpo + if (_simulationManager != null) + { + var connectedBottles = _simulationManager.GetBottlesConnectedToElement(this); + foreach (var bottle in connectedBottles) + { + bottle.CleanRestrictions(this); + } + } + + // ✅ 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 = 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 + } + +} diff --git a/Simulacion/simDescarte.cs b/Simulacion/simDescarte.cs new file mode 100644 index 0000000..12653cd --- /dev/null +++ b/Simulacion/simDescarte.cs @@ -0,0 +1,94 @@ +using BepuPhysics.Collidables; +using BepuPhysics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace CtrEditor.Simulacion +{ + + public class simDescarte : simBase + { + private float _radius; + private List _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 + } + } + +} diff --git a/Simulacion/simGuia.cs b/Simulacion/simGuia.cs new file mode 100644 index 0000000..a0df6c2 --- /dev/null +++ b/Simulacion/simGuia.cs @@ -0,0 +1,155 @@ +using BepuPhysics.Collidables; +using BepuPhysics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace CtrEditor.Simulacion +{ + + public class simGuia : simBase + { + private List _deferredActions; + + // Propiedades para acceder a las dimensiones del objeto WPF + public float GuideThickness { get; set; } // Espesor de la guía + + // ✅ NUEVO: Agregar propiedades Width y Height para almacenar dimensiones reales + public float Width { get; set; } + public float Height { get; set; } + + public simGuia(Simulation simulation, List deferredActions, float width, float height, Vector2 topLeft, float angle) + { + _simulation = simulation; + _deferredActions = deferredActions; + + // ✅ NUEVO: Almacenar dimensiones reales + Width = width; + Height = height; + GuideThickness = height; // Mantener compatibilidad + + Create(width, height, topLeft, angle); + } + + public void Create(float width, float height, Vector2 wpfTopLeft, float wpfAngle) + { + RemoverBody(); + + // ✅ NUEVO: Actualizar dimensiones almacenadas + Width = width; + Height = height; + GuideThickness = height; // Mantener compatibilidad + + // ✅ USAR COORDINATECONVERTER para conversión centralizada + var zPosition = zAltura_Guia / 2 + zPos_Guia; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); + + // ✅ CRÍTICO: Crear el Box con las dimensiones correctas almacenadas + var box = new Box(Width, GuideThickness, zAltura_Guia); + var shapeIndex = _simulation.Shapes.Add(box); + + // ✅ USAR COORDINATECONVERTER para conversión centralizada + var bodyDescription = BodyDescription.CreateKinematic( + new RigidPose(bepuCenter, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)), + new CollidableDescription(shapeIndex, 0.1f), + new BodyActivityDescription(0.01f) + ); + + BodyHandle = _simulation.Bodies.Add(bodyDescription); + _bodyCreated = true; + } + + // Método para actualizar las propiedades desde el objeto WPF + public void UpdateProperties(float ancho, float altoGuia, float angulo) + { + // ✅ CORREGIDO: Actualizar todas las dimensiones + Width = ancho; + Height = altoGuia; + GuideThickness = altoGuia; // Mantener compatibilidad + } + + /// + /// ✅ CORREGIDO - Actualiza las dimensiones y recrea el cuerpo físico si es necesario + /// + public void SetDimensions(float width, float height) + { + // ✅ NUEVO: Actualizar dimensiones almacenadas + Width = width; + Height = height; + GuideThickness = height; // Mantener compatibilidad + + // ✅ CORREGIDO: Usar ChangeBodyShape para limpiar la forma anterior + if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) + { + var box = new Box(width, GuideThickness, zAltura_Guia); + var shapeIndex = _simulation.Shapes.Add(box); + ChangeBodyShape(shapeIndex); + } + } + + /// + /// ✅ CORREGIDO - Actualiza posición usando las dimensiones reales almacenadas + /// + public void SetPosition(Vector2 wpfTopLeft, float wpfAngle, float actualWidth, float actualHeight) + { + // ✅ NUEVO: Actualizar dimensiones almacenadas + Width = actualWidth; + Height = actualHeight; + GuideThickness = actualHeight; // Mantener compatibilidad + + // ✅ CRÍTICO: Recrear el box colisionador con las nuevas dimensiones + if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) + { + var box = new Box(actualWidth, GuideThickness, zAltura_Guia); + var shapeIndex = _simulation.Shapes.Add(box); + ChangeBodyShape(shapeIndex); + } + + // ✅ USAR COORDINATECONVERTER para conversión centralizada con dimensiones reales + var zPosition = zAltura_Guia / 2 + zPos_Guia; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, actualWidth, actualHeight, wpfAngle, zPosition); + + // Actualizar posición y rotación simultáneamente + CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); + } + + /// + /// ✅ CORREGIDO: Actualiza tanto posición como rotación usando dimensiones reales + /// + internal void UpdateFromWpfParameters(Vector2 wpfTopLeft, float wpfAngle, float width, float height) + { + // ✅ NUEVO: Actualizar dimensiones almacenadas + Width = width; + Height = height; + GuideThickness = height; // Mantener compatibilidad + + // ✅ CRÍTICO: Recrear el box colisionador con las nuevas dimensiones + if (_simulation != null && _simulation.Bodies.BodyExists(BodyHandle)) + { + var box = new Box(width, GuideThickness, zAltura_Guia); + var shapeIndex = _simulation.Shapes.Add(box); + ChangeBodyShape(shapeIndex); + } + + // ✅ USAR COORDINATECONVERTER para conversión centralizada con dimensiones reales + var zPosition = zAltura_Guia / 2 + zPos_Guia; + var bepuCenter = CoordinateConverter.CalculateBepuCenterFromWpfTopLeft(wpfTopLeft, width, height, wpfAngle, zPosition); + + // Actualizar posición y rotación simultáneamente + CoordinateConverter.UpdateBepuBodyPose(_simulation, BodyHandle, bepuCenter, wpfAngle); + } + + /// + /// ✅ CORREGIDO - Usar dimensiones reales almacenadas en lugar de aproximaciones + /// + public void SetPosition(Vector2 wpfTopLeft, float wpfAngle = 0) + { + // ✅ CORREGIDO: Usar dimensiones reales almacenadas en lugar de aproximaciones + SetPosition(wpfTopLeft, wpfAngle, Width, Height); + } + } + +} diff --git a/Simulacion/simTransporte.cs b/Simulacion/simTransporte.cs new file mode 100644 index 0000000..9e4923b --- /dev/null +++ b/Simulacion/simTransporte.cs @@ -0,0 +1,257 @@ +using BepuPhysics.Collidables; +using BepuPhysics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace CtrEditor.Simulacion +{ + + public class simTransporte : simBase + { + public float Speed { get; set; } // Velocidad para efectos de cinta transportadora (m/s) + public float Friction { get; set; } // Friccion para efectos de cinta transportadora + public float DistanceGuide2Guide { get; set; } + public bool isBrake { get; set; } + + public bool TransportWithGuides = false; + private List _deferredActions; + public float Width { get; set; } + public float Height { get; set; } + + // ✅ NUEVAS PROPIEDADES - cachear cálculos costosos + public Vector3 DirectionVector { get; private set; } + public List 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, SimulationManagerBEPU simulationManager = null) + { + _simulation = simulation; + _deferredActions = deferredActions; + _simulationManager = simulationManager; // ✅ NUEVO: Almacenar referencia + + Width = width; + Height = height; + + // Usar el nuevo método Create que maneja Top-Left correctamente + Create(width, height, topLeft, angle); + + // ✅ INICIALIZAR PROPIEDADES CRÍTICAS + UpdateCachedProperties(); + } + + public float Angle + { + get + { + return GetRotationZ(); + } + set + { + SetRotation(value); + } + } + + /// + /// ✅ 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()}"); + } + } + + // ✅ 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 limpia restricciones y elimina el body + /// + public new void RemoverBody() + { + // ✅ NUEVO: Limpiar restricciones de botellas conectadas antes de eliminar el cuerpo + if (_simulationManager != null) + { + var connectedBottles = _simulationManager.GetBottlesConnectedToElement(this); + foreach (var bottle in connectedBottles) + { + bottle.CleanRestrictions(this); + } + } + + 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(); + } + } + +}