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();
+ }
+ }
+
+}