Se realizaron mejoras en la gestión de barreras en el sistema de simulación. Se eliminó la dependencia de cuerpos físicos para las barreras, implementando un sistema de detección basado en RayCast. Se actualizaron los métodos de creación y actualización de barreras, así como su visualización en 3D, optimizando la representación del haz de luz. Además, se simplificó la lógica de detección y se eliminaron métodos obsoletos relacionados con la geometría de barreras.

This commit is contained in:
Miguel 2025-07-04 22:45:26 +02:00
parent 095228144a
commit a6cbd8c4ab
12 changed files with 2603 additions and 2089 deletions

View File

@ -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;
}
}

View File

@ -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
}
}
/// <summary>
/// Verifica si las dimensiones de una barrera han cambiado
/// </summary>
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;
}
}
/// <summary>
/// Verifica si las dimensiones de una guía han cambiado
/// </summary>

File diff suppressed because it is too large Load Diff

View File

@ -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}");
}
}
/// <summary>
/// ✅ NUEVO: Crea la visualización del haz de luz para simBarrera
/// Muestra una línea semi-transparente que representa el RayCast
/// </summary>
/// <param name="barrera">Barrera que usa RayCast para detección</param>
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}");
}
}
/// <summary>
/// ✅ NUEVO: Actualiza la visualización existente de simBarrera
/// </summary>
/// <param name="barrera">Barrera a actualizar</param>
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}");
}
}
/// <summary>
/// ✅ NUEVO: Crea la geometría del haz de luz (línea semi-transparente)
/// </summary>
/// <param name="barrera">Barrera para crear el haz</param>
/// <returns>Modelo visual del haz de luz</returns>
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;
}
}
/// <summary>
/// ✅ NUEVO: Actualiza la transformación (posición y rotación) de la barrera
/// </summary>
/// <param name="barrera">Barrera a transformar</param>
/// <param name="visual">Visual 3D a transformar</param>
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}");
}
}
/// <summary>
/// ✅ NUEVO: Obtiene las dimensiones de una barrera para detectar cambios
/// </summary>
/// <param name="barrera">Barrera de la cual obtener dimensiones</param>
/// <returns>Dimensiones de la barrera</returns>
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
};
}
/// <summary>
/// ✅ NUEVO: Recrea la geometría de la barrera cuando cambian las dimensiones
/// </summary>
/// <param name="barrera">Barrera a recrear</param>
/// <param name="visual">Visual 3D a actualizar</param>
/// <param name="dimensions">Nuevas dimensiones</param>
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

View File

@ -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
{
/// <summary>
/// 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
/// </summary>
public static class CoordinateConverter
{
/// <summary>
/// Convierte ángulo de WPF a BEPU (invierte signo) - USO INTERNO
/// </summary>
private static float WpfAngleToBepuAngle(float wpfAngle)
{
return -wpfAngle;
}
/// <summary>
/// Convierte ángulo de BEPU a WPF (invierte signo) - USO INTERNO
/// </summary>
private static float BepuAngleToWpfAngle(float bepuAngle)
{
return -bepuAngle;
}
/// <summary>
/// Convierte posición Y de WPF a BEPU (invierte signo) - USO INTERNO
/// </summary>
internal static float WpfYToBepuY(float wpfY)
{
return -wpfY;
}
/// <summary>
/// Convierte posición Y de BEPU a WPF (invierte signo) - RETORNA VALOR WPF
/// </summary>
public static float BepuYToWpfY(float bepuY)
{
return -bepuY;
}
/// <summary>
/// Convierte Vector2 de WPF a BEPU (solo invierte Y) - USO INTERNO
/// </summary>
internal static Vector2 WpfToBepuVector2(Vector2 wpfVector)
{
return new Vector2(wpfVector.X, -wpfVector.Y);
}
/// <summary>
/// Convierte Vector2 de BEPU a WPF (solo invierte Y) - RETORNA VALOR WPF
/// </summary>
public static Vector2 BepuToWpfVector2(Vector2 bepuVector)
{
return new Vector2(bepuVector.X, -bepuVector.Y);
}
/// <summary>
/// Convierte Vector3 de BEPU a Vector2 WPF (proyección XY con Y invertida) - RETORNA VALOR WPF
/// </summary>
public static Vector2 BepuVector3ToWpfVector2(Vector3 bepuVector)
{
return new Vector2(bepuVector.X, -bepuVector.Y);
}
/// <summary>
/// Calcula la posición del centro en coordenadas BEPU desde Top-Left WPF - USO INTERNO
/// Maneja correctamente la rotación del objeto
/// </summary>
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;
}
/// <summary>
/// Calcula la posición Top-Left WPF desde el centro BEPU - RETORNA VALOR WPF
/// Maneja correctamente la rotación del objeto
/// </summary>
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);
}
/// <summary>
/// Crea un Quaternion para BEPU desde un ángulo WPF - USO INTERNO
/// </summary>
internal static Quaternion CreateBepuQuaternionFromWpfAngle(float wpfAngle)
{
var bepuAngle = WpfAngleToBepuAngle(wpfAngle);
return Quaternion.CreateFromAxisAngle(Vector3.UnitZ, simBase.GradosARadianes(bepuAngle));
}
/// <summary>
/// Extrae el ángulo WPF desde un Quaternion BEPU - RETORNA VALOR WPF
/// </summary>
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);
}
/// <summary>
/// Actualiza la posición de un body BEPU manteniendo su rotación actual - USO INTERNO
/// </summary>
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;
}
}
/// <summary>
/// Actualiza la rotación de un body BEPU manteniendo su posición actual - USO INTERNO
/// </summary>
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);
}
}
/// <summary>
/// Actualiza posición y rotación de un body BEPU simultáneamente - USO INTERNO
/// </summary>
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);
}
}
/// <summary>
/// Obtiene la posición del centro en coordenadas BEPU - USO INTERNO
/// </summary>
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;
}
/// <summary>
/// Obtiene el ángulo WPF desde un body BEPU - RETORNA VALOR WPF
/// </summary>
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;
}
/// <summary>
/// ✅ 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
/// </summary>
internal static float WpfDegreesToBepuRadians(float wpfDegrees)
{
return simBase.GradosARadianes(WpfAngleToBepuAngle(wpfDegrees));
}
/// <summary>
/// ✅ 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
/// </summary>
public static float BepuRadiansToWpfDegrees(float bepuRadians)
{
return BepuAngleToWpfAngle(simBase.RadianesAGrados(bepuRadians));
}
}
}

344
Simulacion/simBarrera.cs Normal file
View File

@ -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
{
/// <summary>
/// ✅ CORREGIDO: RayHitHandler personalizado para simBarrera
/// Calcula la distancia mínima del rayo al centro de cada botella
/// </summary>
public struct BarreraRayHitHandler : IRayHitHandler
{
private simBarrera _barrera;
private List<simBotella> _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<simBotella>();
_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}");
}
}
/// <summary>
/// ✅ NUEVO: Calcula la distancia mínima de un rayo (línea) a un punto
/// </summary>
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<simBotella> botellas)
{
luzCortada = _fullDetected;
luzCortadaNeck = _neckDetected;
distancia = _minDistance == float.MaxValue ? 0f : _minDistance;
botellas = new List<simBotella>(_detectedBottles);
}
}
/// <summary>
/// Estructura para datos de barrera que se exponen a WPF
/// </summary>
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<simBotella> 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<simBotella>();
// ✅ 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<simBotella>();
// ✅ 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);
}
/// <summary>
/// ✅ INTERNO: Actualiza posición y rotación desde parámetros BEPU internos
/// </summary>
internal void Update(Vector3 bepuPosition, float bepuRadians)
{
Position = bepuPosition;
AngleRadians = bepuRadians;
}
public void SetDimensions(float width, float height)
{
Width = width;
Height = height;
}
/// <summary>
/// ✅ INTERNO: Crea la posición usando parámetros BEPU internos
/// </summary>
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;
}
/// <summary>
/// ✅ CORREGIDO: Realiza raycast para detectar botellas usando coordenadas BEPU internas
/// </summary>
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<simBotella> 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}");
}
}
/// <summary>
/// ✅ INTERNO: Métodos Create que trabajan solo con parámetros BEPU internos
/// </summary>
internal void Create(float width, float height, Vector3 bepuPosition, float bepuRadians = 0, bool detectectNeck = false)
{
CreatePosition(width, height, bepuPosition, bepuRadians);
}
/// <summary>
/// ✅ OVERRIDE: RemoverBody no hace nada porque no hay body físico
/// </summary>
public new void RemoverBody()
{
// ✅ NO HAY BODY FÍSICO QUE REMOVER
_bodyCreated = false;
_poseInitialized = false;
}
}
}

139
Simulacion/simBase.cs Normal file
View File

@ -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
}
}
/// <summary>
/// ✅ NUEVO: Cambia la forma de un body existente, limpiando la forma anterior para evitar memory leaks
/// </summary>
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);
}
}
}

553
Simulacion/simBotella.cs Normal file
View File

@ -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<simBase> 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<Action> _deferredActions;
public simBotella(Simulation simulation, List<Action> 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<simBase>();
// ✅ 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
}
/// <summary>
/// ✅ NUEVO: Crea o actualiza el motor conectándolo al transporte/curva actual
/// </summary>
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}");
}
}
/// <summary>
/// ✅ NUEVO: Crea un motor específico para un transporte o curva
/// </summary>
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}");
}
}
/// <summary>
/// ✅ NUEVO: Actualiza solo la velocidad del motor existente
/// </summary>
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}");
}
}
/// <summary>
/// ✅ NUEVO: Elimina el motor actual
/// </summary>
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;
}
}
/// <summary>
/// ✅ NUEVO: Detiene el motor (velocidad 0) pero mantiene la conexión
/// </summary>
public void StopMotor()
{
UpdateMotorSpeed(CurrentDirection, 0f);
}
/// <summary>
/// ✅ NUEVO: Limpia las restricciones asociadas a un elemento específico
/// Solo limpia los datos internos de simBotella, no elimina las restricciones de BEPU
/// </summary>
/// <param name="target">Elemento (simTransporte o simCurve) del cual limpiar las restricciones</param>
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;
}
/// <summary>
/// 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
/// </summary>
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;
}
/// <summary>
/// ✅ SIMPLIFICADO: RemoverBody que confía en BEPU para limpiar constraints
/// </summary>
public new void RemoverBody()
{
base.RemoverBody();
}
}
}

368
Simulacion/simCurve.cs Normal file
View File

@ -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
{
/// <summary>
/// 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
/// </summary>
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<Action> _deferredActions;
// ✅ NUEVO: Almacenar el centro real de la curva
private Vector3 _curveCenter;
// ✅ SIMPLIFICADO: Propiedades esenciales únicamente
public List<simBotella> BottlesOnCurve { get; private set; } = new List<simBotella>();
// ✅ EVENTO para actualización de motores
public event Action<simCurve> 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<Action> 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);
}
/// <summary>
/// ✅ NUEVO: Configura la velocidad de la curva en metros por segundo
/// Valores positivos mueven en sentido horario, negativos en sentido antihorario
/// </summary>
/// <param name="speedMeterPerSecond">Velocidad en m/s (típicamente entre -5.0 y 5.0)</param>
public void SetCurveSpeed(float speedMeterPerSecond)
{
SetSpeed(speedMeterPerSecond * simBase.SPEED_CONVERSION_FACTOR);
}
/// <summary>
/// ✅ NUEVO: Detiene completamente la curva
/// </summary>
public void StopCurve()
{
SetSpeed(0f);
}
/// <summary>
/// ✅ NUEVO: Invierte la dirección de la curva manteniendo la misma velocidad
/// </summary>
public void ReverseCurve()
{
SetSpeed(-Speed);
}
/// <summary>
/// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF
/// </summary>
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);
}
/// <summary>
/// ✅ NUEVO: Actualiza posición desde Top-Left WPF manteniendo parámetros actuales
/// </summary>
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);
}
/// <summary>
/// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual
/// </summary>
internal Vector2 GetWpfTopLeft()
{
var curveSize = _outerRadius * 2f;
return CoordinateConverter.CalculateWpfTopLeftFromBepuCenter(_curveCenter, curveSize, curveSize, 0f);
}
/// <summary>
/// ✅ CORREGIDO: Obtiene los triángulos reales creados para la curva
/// Devuelve los triángulos en coordenadas locales para evitar transformación duplicada
/// </summary>
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];
}
}
/// <summary>
/// ✅ NUEVO: Obtiene los triángulos transformados a coordenadas mundiales (solo para debugging)
/// </summary>
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];
}
}
/// <summary>
/// ✅ NUEVO: Transforma triángulos de coordenadas locales a mundiales
/// </summary>
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<Triangle>(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
/// <summary>
/// ✅ 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
/// </summary>
/// <param name="innerRadius">Radio interno del arco</param>
/// <param name="outerRadius">Radio externo del arco</param>
/// <param name="startAngle">Ángulo inicial en radianes BEPU</param>
/// <param name="endAngle">Ángulo final en radianes BEPU</param>
/// <returns>Array de triángulos nativos de BEPU en coordenadas locales</returns>
private Triangle[] CreateSimpleArcTriangles(float innerRadius, float outerRadius, float startAngle, float endAngle)
{
var triangles = new List<Triangle>();
// ✅ 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
}
}

94
Simulacion/simDescarte.cs Normal file
View File

@ -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<Action> _deferredActions;
public List<simBotella> ListSimBotellaContact;
public simDescarte(Simulation simulation, List<Action> deferredActions, float diameter, Vector2 position)
{
_simulation = simulation;
_deferredActions = deferredActions;
_radius = diameter / 2f;
ListSimBotellaContact = new List<simBotella>();
// ✅ 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);
}
/// <summary>
/// ✅ NUEVO: Actualiza posición usando coordenadas WPF apropiadas
/// </summary>
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
}
}
}

155
Simulacion/simGuia.cs Normal file
View File

@ -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<Action> _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<Action> 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
}
/// <summary>
/// ✅ CORREGIDO - Actualiza las dimensiones y recrea el cuerpo físico si es necesario
/// </summary>
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);
}
}
/// <summary>
/// ✅ CORREGIDO - Actualiza posición usando las dimensiones reales almacenadas
/// </summary>
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);
}
/// <summary>
/// ✅ CORREGIDO: Actualiza tanto posición como rotación usando dimensiones reales
/// </summary>
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);
}
/// <summary>
/// ✅ CORREGIDO - Usar dimensiones reales almacenadas en lugar de aproximaciones
/// </summary>
public void SetPosition(Vector2 wpfTopLeft, float wpfAngle = 0)
{
// ✅ CORREGIDO: Usar dimensiones reales almacenadas en lugar de aproximaciones
SetPosition(wpfTopLeft, wpfAngle, Width, Height);
}
}
}

257
Simulacion/simTransporte.cs Normal file
View File

@ -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<Action> _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<simBotella> BottlesOnTransport { get; private set; } = new List<simBotella>();
// ✅ NUEVO EVENTO - para actualización de motores
public event Action<simTransporte> OnSpeedChanged;
public simTransporte(Simulation simulation, List<Action> 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);
}
}
/// <summary>
/// ✅ SOBRESCRITO: SetRotation que actualiza automáticamente las propiedades cacheadas
/// </summary>
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);
}
/// <summary>
/// ✅ NUEVO: Actualiza posición desde Top-Left WPF con dimensiones y ángulo actuales
/// </summary>
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);
}
/// <summary>
/// ✅ NUEVO: Obtiene Top-Left WPF desde la posición actual
/// </summary>
internal Vector2 GetWpfTopLeft()
{
var bepuCenter = GetPosition();
var wpfAngle = GetRotationZ(); // Ya usa CoordinateConverter
return CoordinateConverter.CalculateWpfTopLeftFromBepuCenter(bepuCenter, Width, Height, wpfAngle);
}
/// <summary>
/// ✅ NUEVO: Actualiza tanto posición como rotación desde parámetros WPF
/// </summary>
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);
}
/// <summary>
/// Configura la velocidad del transporte en metros por segundo
/// Valores positivos mueven en la dirección del transporte, negativos en dirección opuesta
/// </summary>
/// <param name="speedMeterPerSecond">Velocidad en m/s (típicamente entre -5.0 y 5.0)</param>
public void SetTransportSpeed(float speedMeterPerSecond)
{
SetSpeed(speedMeterPerSecond * simBase.SPEED_CONVERSION_FACTOR);
}
/// <summary>
/// Detiene completamente el transporte
/// </summary>
public void StopTransport()
{
SetSpeed(0f);
}
/// <summary>
/// Invierte la dirección del transporte manteniendo la misma velocidad
/// </summary>
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);
}
/// <summary>
/// ✅ SIMPLIFICADO: RemoverBody que limpia restricciones y elimina el body
/// </summary>
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();
}
}
}