Compare commits

...

6 Commits

Author SHA1 Message Date
Miguel d1ec7f4d12 Se mejoró el sistema de guías curvas para incluir apertura en cono en los extremos, facilitando la entrada y salida de botellas. Se añadió un nuevo parámetro `AnguloAperturaGuias` para ajustar la apertura, optimizando el flujo de materiales. Además, se implementaron nuevas propiedades en `ucBottGenerator` para gestionar la distancia mínima sin botellas y se realizaron ajustes en la lógica de colisiones y visualización en 3D. 2025-07-06 00:07:36 +02:00
Miguel 83fc828a4c Se implementaron mejoras en la visualización y animación de transportes en movimiento, incluyendo un indicador visual que cambia de color según el estado de movimiento. Se añadió un sistema de animaciones automáticas utilizando StoryBoard de WPF, que combina rotación y pulsación de color para los transportes activos. Además, se ajustaron las propiedades de presión en las botellas para mejorar la gestión de fricción y se optimizó la lógica de animación y limpieza de objetos en la simulación. 2025-07-05 20:20:59 +02:00
Miguel c91b1419b4 Se implementó un sistema para controlar la presión y evitar que las botellas floten en la simulación. Ahora, las botellas registran su última altura de contacto con transportes y ajustan su posición Z si no están en contacto durante varios frames. Además, se centraron las botellas en el eje longitudinal de los transportes al entrar en contacto con transportes de frenado. Se añadieron nuevas propiedades en simBotella para gestionar la presión y el estado de contacto. 2025-07-05 18:43:28 +02:00
Miguel 71c08d8047 Se ajustaron los coeficientes de fricción y configuraciones de resorte en la simulación de botellas, diferenciando entre transportes con y sin freno. Además, se modificó la visibilidad de triángulos en la visualización 3D, mejorando la iluminación y corrigiendo la creación de mallas continuas a partir de triángulos de BEPU. Se eliminaron métodos obsoletos relacionados con la geometría de curvas, optimizando la lógica de creación de mallas. 2025-07-05 17:53:22 +02:00
Miguel eb6ed62d5b Eliminación del archivo "Memoria de evolución" y cambios en la creación de botellas en la simulación. Se reemplazó el método AddCircle por AddBotella en ucBotella y ucBotellaCuello, y se ajustaron propiedades en simBotella para mejorar la gestión de inercia y fricción. Se eliminaron referencias a barreras físicas en BEPU, optimizando la lógica de colisiones y ajustando parámetros de fricción para mejorar la simulación. 2025-07-05 17:04:39 +02:00
Miguel de0e3ce5f0 Eliminacion de LinearAxisMotor por solo velocity y fricciones normales en los cuerpos 2025-07-05 15:24:54 +02:00
13 changed files with 855 additions and 901 deletions

4
.cursor/rules/reglas.mdc Normal file
View File

@ -0,0 +1,4 @@
---
alwaysApply: true
---
Quisiera que con los conocimientos importantes y desiciones importantes que hemos adquirido y utilizado los agregues a Documentation\MemoriadeEvolucion.md manteniendo el estilo que ya tenemos de texto simple sin demasiado codigo y una semantica resumida.

View File

@ -0,0 +1,32 @@
### Aqui se mantiene la memoria de evolucion de las distintas decisiones que fueron tomadas y porque
BEPU.cs : SimulationManagerBEPU gestor de la simulacion con el motor BEPUphysics y punto de creacion y modificacion de los objetos dentro del mundo que se derivan des simBase
simBase : Clase base que da un marco a el resto de los objetos
simBarrera : Simula una fotocelula con un espejo usando RayCast
simBotella : Simula una botella que puede transitar por los transportes simTransporte o simCurve
simCurve : Simula una curva o arco de curva de transporte
simDescarte : Permite la eliminacion localizada de botellas en un punto del mundo
simGuia : Es un box con el que las botellas pueden colisionar y cambiar de direccion.
simTransporte : Es un box por donde las botellas pueden desplazarse usando un truco de aplicar la Velocity.Linear pero sin integrar esta velocidad para que no se mueva el objeto transporte.
* Se usaron esferas en vez de cilindros para mejorar la eficiencia. En el Debug3D si se usan cilindros. **Revertido**: Se ha vuelto a usar `Cylinder` para las `simBotella` ya que el nuevo sistema de fricción debería prevenir la rotación indeseada que ocurría con los `LinearAxisMotor`.
* Se reemplazó el sistema de `LinearAxisMotor` que actuaba sobre las `simBotella` por un sistema basado en fricción. Los transportes (`simTransporte` y `simCurve`) ahora son cuerpos cinemáticos que no se mueven de su sitio. Para lograr que arrastren a las botellas, se les asigna una velocidad (`Velocity.Linear` o `Velocity.Angular`) justo antes de que el solver se ejecute (en `OnSubstepStarted`) y se les quita justo después (en `OnSubstepEnded`). Esto permite que los cuerpos cinemáticos transmitan su velocidad a través de la fricción durante la simulación, pero evita que el `PoseIntegrator` los desplace de su posición original, ya que su velocidad es cero cuando se integra la pose.
* Para aumentar la estabilidad de las `simBotella` y evitar que roten descontroladamente sobre su eje Z al ser arrastradas, se implementó un callback `IntegrateVelocity` personalizado. Este callback identifica las botellas durante la integración y les aplica un amortiguamiento angular (`AngularDamping`) adicional solo en el eje Z. Se descartó la idea inicial de modificar dinámicamente la masa de las botellas dentro de este callback, ya que la arquitectura de BEPUphysics no permite cambiar la masa o la inercia de un cuerpo durante la fase de integración de velocidad.
* Originalmente se habia puesto todos los objetos en awaken para poder usar las colisiones constantemente incluso con objetos en modo sleep para que los simBarrera puedan detectar las colisiones. Ahora que se usar RayCast podemos dejar que las simBotellas se duerman
* La unica clase que se ha terminado de refactorizar respecto a el cambio de coordenadas es simBarrera y ucPhotocell. El concepto es poder separar usando metodos de SimulationManagerBEPU y estructuras como BarreraData la conversion de coordenadas de WPF a coordenadas BEPU. Para esto se usa CoordinateConverter que permite bidireccionalmente convertir las coordenadas pero esto solo se debe usar en SimulationManagerBEPU las clases derivadas de osBase solo deben manejar coordenadas WPF, mientras que las clases dervadas de simBase solo deben almacenar y usar coordenadas BEPU. La z tambien es algo que se debe transferir a SimulationManagerBEPU ya que los objetos simBase deberian recibir tambien sus coordenadas de Z desde SimulationManagerBEPU y ser SimulationManagerBEPU el que gestione las Z.
* Se ha implementado un sistema para evitar que las botellas (`simBotella`) "floten" o se eleven de manera irreal por la acumulación de presión en la simulación. Cada botella ahora registra si está en contacto con un transporte y almacena la última coordenada Z válida durante dicho contacto. Si la botella deja de tener contacto con transportes por varios frames consecutivos, se incrementa un contador de "presión". Al superar un umbral, el sistema reestablece la posición Z de la botella a su última altura conocida, previniendo la flotación. Este contador de presión se decrementa rápidamente al volver a hacer contacto con un transporte.
* Cuando una botella (`simBotella`) entra en contacto con un transporte de frenado (`simTransporte` con `isBrake = true`), su posición se ajusta automáticamente para centrarla en el eje longitudinal del transporte. Esto se realiza una única vez, en el primer contacto, para asegurar un acoplamiento suave y predecible. La posición de la botella se proyecta sobre la línea central del transporte y su velocidad lateral se anula, evitando que la botella se desvíe mientras frena y se alinea con el flujo de salida.
* Se ha implementado un indicador visual simple para mostrar cuando los transportes (`simTransporte`) están en movimiento. El sistema detecta automáticamente si un transporte tiene velocidad (`Speed > 0.001`) y cambia su color a verde brillante. Los transportes detenidos se muestran en verde normal. Esta funcionalidad se integra en el sistema de materiales del `BEPUVisualization3DManager` y se actualiza automáticamente cuando cambia el estado del transporte, proporcionando una retroalimentación visual inmediata del estado de operación.
* Se ha implementado un sistema de animaciones automáticas usando StoryBoard de WPF para los transportes en movimiento. Los transportes activos muestran una animación continua que combina: (1) rotación sutil muy lenta alrededor del eje Z (20 segundos por vuelta completa) y (2) pulsación cíclica del color del material (1.5 segundos por ciclo). Las animaciones se crean y destruyen automáticamente según el estado del transporte, sin necesidad de actualización manual en cada frame. El sistema gestiona las animaciones activas en un diccionario y las limpia correctamente cuando se eliminan objetos. Se resolvió el problema de `InvalidOperationException` al animar brushes inmutables creando una función `CreateAnimatableMaterial` que genera materiales específicamente diseñados para ser animados sin estar "frozen", proporcionando una experiencia visual fluida y eficiente.
* Se ha mejorado el sistema de guías curvas (`ucTransporteCurvaGuias`) para incluir apertura en cono en los extremos de entrada y salida. Se agregó el parámetro `AnguloAperturaGuias` (por defecto 5 grados) que permite configurar la apertura modificando los radios de las guías en los puntos extremos. En lugar de cambiar ángulos, se reduce el radio de la guía superior (externa) y se aumenta el radio de la guía inferior (interna) en los segmentos inicial y final, creando naturalmente la apertura en cono. La modificación del radio se calcula usando `Math.Sin(anguloApertura)` para obtener el desplazamiento apropiado. Esta apertura facilita la entrada y salida de botellas del transporte curvo, reduciendo atascos y mejorando el flujo de materiales manteniendo la continuidad geométrica de las guías.

View File

@ -146,7 +146,9 @@ namespace CtrEditor.ObjetosSim
// Se llama cuando inicia la simulación - crear geometría si no existe
if (SimGeometria == null)
{
SimGeometria = simulationManager.AddCircle(Diametro, GetCentro(), Mass);
SimGeometria = simulationManager.AddBotella(Diametro, GetCentro(), Mass);
SimGeometria.SimObjectType = "Botella";
SimGeometria.WpfObject = this;
}
else
{
@ -170,14 +172,12 @@ namespace CtrEditor.ObjetosSim
SetCentro(SimGeometria.Center);
// Sistema de colores jerarquizado para diferentes estados
if (SimGeometria.isRestricted)
ColorButton_oculto = Brushes.Yellow; // Estado restringido (prioridad alta)
else if (SimGeometria.isOnBrakeTransport)
ColorButton_oculto = Brushes.Blue; // En transporte con freno - NUEVO ESTADO
else if (SimGeometria.IsOnAnyTransport())
ColorButton_oculto = Brushes.Red; // En transporte normal
if (SimGeometria.isOnBrakeTransport)
ColorButton_oculto = Brushes.Blue; // En transporte con freno (prioridad sobre presión)
else if (!SimGeometria.isOnBrakeTransport && SimGeometria.PressureBuildup>1)
ColorButton_oculto = Brushes.Red; // La botella tiene mucha presion
else
ColorButton_oculto = Brushes.Gray; // Estado libre
ColorButton_oculto = Brushes.Gray; // 5. Estado libre
// Ha sido marcada para remover
if (SimGeometria.Descartar)
@ -196,7 +196,7 @@ namespace CtrEditor.ObjetosSim
// El UserControl ya se ha cargado y podemos obtener las coordenadas para
// crear el objeto de simulacion
base.ucLoaded();
SimGeometria = simulationManager.AddCircle(Diametro, GetCentro(), Mass);
SimGeometria = simulationManager.AddBotella(Diametro, GetCentro(), Mass);
}
public override void ucUnLoaded()
{

View File

@ -180,7 +180,7 @@ namespace CtrEditor.ObjetosSim
// El UserControl ya se ha cargado y podemos obtener las coordenadas para
// crear el objeto de simulacion
base.ucLoaded();
SimGeometria = simulationManager.AddCircle(Diametro, GetCentro(), Mass);
SimGeometria = simulationManager.AddBotella(Diametro, GetCentro(), Mass);
}
public override void ucUnLoaded()
{

View File

@ -87,6 +87,7 @@ namespace CtrEditor.ObjetosSim
[property: Category("Información")]
[property: Name("Salida Filtro")]
bool filter_Output;
[ObservableProperty]
[property: Description("Cantidad de botellas generadas por hora")]
[property: Category("Configuración")]
@ -113,11 +114,32 @@ namespace CtrEditor.ObjetosSim
[property: Name("Velocidad Actual (%)")]
private float velocidad_actual_percentual;
[ObservableProperty]
[property: Description("Distancia libre minima sin botellas sobre el transporte para colocar otra botella")]
[property: Category("Simulación")]
[property: Name("Diametro libre minimo")]
private float distancia_libre;
[ObservableProperty]
[property: Description("Diámetro de las botellas generadas")]
[property: Category("Configuración")]
[property: Name("Diámetro Botella")]
private float diametro_botella;
public osBottGenerator()
{
Ancho = 0.40f;
Alto = 0.40f;
Angulo = 0;
Velocidad_actual_percentual = 100;
Diametro_botella = 0.1f;
Botellas_hora = 10000;
Consenso = true;
Distancia_libre = 0.1f;
}
public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds)
{
if (Consenso_NC)
@ -128,7 +150,7 @@ namespace CtrEditor.ObjetosSim
private bool HayEspacioParaNuevaBotella(float X, float Y)
{
float radioMinimo = Diametro_botella / 4; // Distancia mínima entre centros
float radioMinimo = Distancia_libre; // Distancia mínima entre centros
float radioMinimoCuadrado = radioMinimo * radioMinimo;
// Buscar todas las botellas cercanas

View File

@ -48,7 +48,7 @@ namespace CtrEditor.ObjetosSim
// ✅ SISTEMA INTELIGENTE: Solo recrear si las dimensiones han cambiado
if (HasGuideDimensionsChanged(SimGeometria, Ancho, AltoGuia))
{
System.Diagnostics.Debug.WriteLine($"[osGuia] Recreando guía por cambio de dimensiones: {Ancho}x{AltoGuia}");
//System.Diagnostics.Debug.WriteLine($"[osGuia] Recreando guía por cambio de dimensiones: {Ancho}x{AltoGuia}");
// ✅ RECREAR COMPLETAMENTE: Las dimensiones cambiaron
SimGeometria.Create(Ancho, AltoGuia, topLeft, Angulo);
@ -56,7 +56,7 @@ namespace CtrEditor.ObjetosSim
}
else
{
System.Diagnostics.Debug.WriteLine($"[osGuia] Solo actualizando posición/rotación: Left={Left}, Top={Top}, Angulo={Angulo}");
// System.Diagnostics.Debug.WriteLine($"[osGuia] Solo actualizando posición/rotación: Left={Left}, Top={Top}, Angulo={Angulo}");
// ✅ SOLO ACTUALIZAR POSICIÓN/ROTACIÓN: Usar dimensiones reales para conversión correcta
SimGeometria.UpdateFromWpfParameters(topLeft, Angulo, Ancho, AltoGuia);

View File

@ -134,6 +134,17 @@ namespace CtrEditor.ObjetosSim
ActualizarGeometrias();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Ángulo de apertura en grados para los extremos de las guías")]
[property: Name("Ángulo Apertura Guías")]
private float anguloAperturaGuias = 5f;
partial void OnAnguloAperturaGuiasChanged(float value)
{
ActualizarGeometrias();
}
[ObservableProperty]
[property: Category("Configuración")]
[property: Description("Distancia de separación de las guías desde el borde")]
@ -336,6 +347,7 @@ namespace CtrEditor.ObjetosSim
float anguloInicioRad = simBase.GradosARadianes(Angulo);
float anguloFinalRad = simBase.GradosARadianes(AnguloFinal);
float rangoAngular = anguloFinalRad - anguloInicioRad;
float anguloAperturaRad = simBase.GradosARadianes(AnguloAperturaGuias);
// Calcular el paso angular entre segmentos
float pasoAngular = rangoAngular / NumeroSegmentosGuias;
@ -349,14 +361,27 @@ namespace CtrEditor.ObjetosSim
float angulo1 = anguloInicioRad + i * pasoAngular;
float angulo2 = anguloInicioRad + (i + 1) * pasoAngular;
// Calcular radios ajustados para crear apertura en cono
// Para guía superior: reducir radio en los extremos para crear apertura
float radio1 = radioGuiaSuperior;
float radio2 = radioGuiaSuperior;
// Calcular reducción del radio basada en el ángulo de apertura
float reduccionRadio = radioGuiaSuperior * (float)Math.Sin(anguloAperturaRad/10);
if (InvertirDireccion && i == 0) // Primer segmento
radio1 += reduccionRadio;
if (!InvertirDireccion && i == NumeroSegmentosGuias - 1) // Último segmento
radio2 += reduccionRadio;
Vector2 punto1 = new Vector2(
radioGuiaSuperior * (float)Math.Cos(angulo1),
radioGuiaSuperior * (float)Math.Sin(angulo1)
radio1 * (float)Math.Cos(angulo1),
radio1 * (float)Math.Sin(angulo1)
);
Vector2 punto2 = new Vector2(
radioGuiaSuperior * (float)Math.Cos(angulo2),
radioGuiaSuperior * (float)Math.Sin(angulo2)
radio2 * (float)Math.Cos(angulo2),
radio2 * (float)Math.Sin(angulo2)
);
// Ajustar por la posición del objeto
@ -374,14 +399,27 @@ namespace CtrEditor.ObjetosSim
float angulo1 = anguloInicioRad + i * pasoAngular;
float angulo2 = anguloInicioRad + (i + 1) * pasoAngular;
// Calcular radios ajustados para crear apertura en cono
// Para guía inferior: aumentar radio en los extremos para crear apertura
float radio1 = radioGuiaInferior;
float radio2 = radioGuiaInferior;
// Calcular aumento del radio basada en el ángulo de apertura
float aumentoRadio = radioGuiaInferior * (float)Math.Sin(anguloAperturaRad/10);
if (InvertirDireccion && i == 0) // Primer segmento
radio1 -= aumentoRadio;
if (InvertirDireccion && i == NumeroSegmentosGuias - 1) // Último segmento
radio2 -= aumentoRadio;
Vector2 punto1 = new Vector2(
radioGuiaInferior * (float)Math.Cos(angulo1),
radioGuiaInferior * (float)Math.Sin(angulo1)
radio1 * (float)Math.Cos(angulo1),
radio1 * (float)Math.Sin(angulo1)
);
Vector2 punto2 = new Vector2(
radioGuiaInferior * (float)Math.Cos(angulo2),
radioGuiaInferior * (float)Math.Sin(angulo2)
radio2 * (float)Math.Cos(angulo2),
radio2 * (float)Math.Sin(angulo2)
);
// Ajustar por la posición del objeto

View File

@ -25,6 +25,7 @@ namespace CtrEditor.Simulacion
public struct NarrowPhaseCallbacks : INarrowPhaseCallbacks
{
private SimulationManagerBEPU _simulationManager;
private const float minPressureToLostFriction = 1;
public NarrowPhaseCallbacks(SimulationManagerBEPU simulationManager)
{
@ -98,26 +99,18 @@ namespace CtrEditor.Simulacion
{
var botellaA = GetBotellaFromCollidable(pair.A);
var botellaB = GetBotellaFromCollidable(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;
// ✅ 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)
{
var descarte = descarteA ?? descarteB;
if (botella != null)
{
_simulationManager.RegisterDescarteContact(descarte, botella);
// ✅ CORREGIDO: Marcar la botella para eliminar directamente, en lugar de registrar el contacto.
// Esto es más directo y evita el error de compilación.
botella.Descartar = true;
}
return false; // NO generar contacto físico
}
@ -128,50 +121,148 @@ namespace CtrEditor.Simulacion
var curveA = GetCurveFromCollidable(pair.A);
var curveB = GetCurveFromCollidable(pair.B);
// ✅ CONTACTO BOTELLA-TRANSPORTE: Crear o actualizar motor inmediatamente
// ✅ CONTACTO BOTELLA-TRANSPORTE: USAR FRICCIÓN Y VELOCIDAD CINEMÁTICA
if (botella != null && (transportA != null || transportB != null))
{
// La velocidad del cuerpo cinemático del transporte se establece directamente
// en la clase simTransporte. El motor de físicas se encarga del resto.
// ✅ NUEVO: Lógica de control de presión/flotación
botella.IsTouchingTransport = true;
var botellaCollidable = GetBotellaFromCollidable(pair.A) != null ? pair.A : pair.B;
if (botellaCollidable.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botellaCollidable.BodyHandle))
{
var body = _simulationManager.simulation.Bodies.GetBodyReference(botellaCollidable.BodyHandle);
botella.LastTransportCollisionZ = body.Pose.Position.Z;
}
// Decrementamos la presión acumulada al tocar un transporte. Se reduce más rápido de lo que aumenta.
botella.PressureBuildup = Math.Max(0, botella.PressureBuildup - 0.1f);
// ✅ Fricción ajustada para un arrastre firme pero no excesivo.
var transport = transportA ?? transportB;
if (transport.isBrake)
{
botella.PressureBuildup = 0;
// ✅ NUEVO: Centrar la botella en el transporte de frenado la primera vez que entra.
// if (!botella.isOnBrakeTransport)
{
if (botella.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botella.BodyHandle) &&
transport.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(transport.BodyHandle))
{
var bottleBody = _simulationManager.simulation.Bodies.GetBodyReference(botella.BodyHandle);
var transportBody = _simulationManager.simulation.Bodies.GetBodyReference(transport.BodyHandle);
// ✅ CREAR O ACTUALIZAR MOTOR DINÁMICO INMEDIATAMENTE
transport.UpdateCachedProperties();
var direction = transport.DirectionVector;
var speed = transport.Speed;
botella.CreateOrUpdateMotor(transport, direction, speed);
var bottlePose = bottleBody.Pose;
var transportPose = transportBody.Pose;
// Proyectar la posición de la botella sobre el eje longitudinal del transporte.
Vector3 transportForward = transport.DirectionVector;
Vector3 vectorToBottle = bottlePose.Position - transportPose.Position;
float projectionLength = Vector3.Dot(vectorToBottle, transportForward);
Vector3 newPositionOnCenterline = transportPose.Position + transportForward * projectionLength;
// Crear la nueva pose manteniendo la Z original de la botella para no hundirla.
var newBottlePosition = new Vector3(newPositionOnCenterline.X, newPositionOnCenterline.Y, bottlePose.Position.Z);
bottleBody.Pose.Position = newBottlePosition;
// Opcional: Anular la velocidad lateral para un acoplamiento más suave.
var lateralVelocity = Vector3.Dot(bottleBody.Velocity.Linear, transport.DirectionVector);
bottleBody.Velocity.Linear = transport.DirectionVector * lateralVelocity;
}
}
botella.isOnBrakeTransport = true;
pairMaterial.FrictionCoefficient = 14f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 8);
}
else
{
if (botella.PressureBuildup > minPressureToLostFriction)
{
botella.isOnBrakeTransport = false;
pairMaterial.FrictionCoefficient = 0.1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}
else
{
botella.isOnBrakeTransport = false;
pairMaterial.FrictionCoefficient = 1.2f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}
}
//Fricción alta para transportes
pairMaterial.FrictionCoefficient = 0.01f;
pairMaterial.MaximumRecoveryVelocity = 1f;
pairMaterial.SpringSettings = new SpringSettings(80, 6);
}
// ✅ CONTACTO BOTELLA-CURVA: Crear o actualizar motor inmediatamente
// ✅ CONTACTO BOTELLA-CURVA: USAR FRICCIÓN Y VELOCIDAD CINEMÁTICA
else if (botella != null && (curveA != null || curveB != null))
{
var curve = curveA ?? curveB;
// El motor de físicas usará la velocidad angular del cuerpo cinemático de la curva
// para calcular las fuerzas de fricción y arrastrar la botella.
// ✅ CREAR O ACTUALIZAR MOTOR DINÁMICO INMEDIATAMENTE
var direction = _simulationManager.CalculateCurveDirectionFromBottlePosition(curve, botella);
botella.CreateOrUpdateMotor(curve, direction, curve.Speed);
// ✅ NUEVO: Lógica de control de presión/flotación
botella.IsTouchingTransport = true;
botella.isOnBrakeTransport = false;
var botellaCollidable = GetBotellaFromCollidable(pair.A) != null ? pair.A : pair.B;
if (botellaCollidable.BodyHandle.Value >= 0 && _simulationManager.simulation.Bodies.BodyExists(botellaCollidable.BodyHandle))
{
var body = _simulationManager.simulation.Bodies.GetBodyReference(botellaCollidable.BodyHandle);
botella.LastTransportCollisionZ = body.Pose.Position.Z;
}
// Decrementamos la presión acumulada al tocar una curva.
botella.PressureBuildup = Math.Max(0, botella.PressureBuildup - 0.1f);
// Fricción alta para curvas
pairMaterial.FrictionCoefficient = 0.01f;
pairMaterial.MaximumRecoveryVelocity = 1f;
pairMaterial.SpringSettings = new SpringSettings(80, 6);
// ✅ Fricción ajustada para un arrastre firme pero no excesivo.
if (botella.PressureBuildup > minPressureToLostFriction)
{
pairMaterial.FrictionCoefficient = 0.1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}else
{
pairMaterial.FrictionCoefficient = 1f;
pairMaterial.MaximumRecoveryVelocity = 1.0f;
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}
}
// ✅ 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
// Fricción baja para las guías, deben deslizar, no arrastrar.
pairMaterial.FrictionCoefficient = 0.01f;
pairMaterial.MaximumRecoveryVelocity = 1f;
pairMaterial.SpringSettings = new SpringSettings(80, 6);
pairMaterial.MaximumRecoveryVelocity = 2f;
pairMaterial.SpringSettings = new SpringSettings(20, 1);
}
// ✅ NUEVO: CONTACTO BOTELLA-BOTELLA: Configuración más suave para evitar el comportamiento "pegajoso".
else if (botellaA != null && botellaB != null)
{
if (botella.isOnBrakeTransport)
{
pairMaterial.FrictionCoefficient = 2.0f; // Un poco menos de fricción.
pairMaterial.MaximumRecoveryVelocity = 1.5f; // Menos rebote.
pairMaterial.SpringSettings = new SpringSettings(80, 1f); // Muelle MUCHO más suave y críticamente amortiguado.
}
else
{
pairMaterial.FrictionCoefficient = 0.01f; // Un poco menos de fricción.
pairMaterial.MaximumRecoveryVelocity = 1.5f; // Menos rebote.
pairMaterial.SpringSettings = new SpringSettings(20, 0.5f); // Muelle MUCHO más suave y críticamente amortiguado.
}
// Si una botella tiene presion esta presion es transmitia a el resto para que el resto pierda el coeficiente de friccion sobre los transportes
if (botellaA.PressureBuildup > 0 && botellaB.PressureBuildup == 0)
botellaB.PressureBuildup = botellaA.PressureBuildup;
if (botellaB.PressureBuildup > 0 && botellaA.PressureBuildup == 0)
botellaA.PressureBuildup = botellaB.PressureBuildup;
}
// Ajustes básicos para otras botellas
else if (botella != null)
{
pairMaterial.FrictionCoefficient = 0.01f;
// Fricción moderada para colisiones entre botellas.
pairMaterial.FrictionCoefficient = 0.3f;
pairMaterial.MaximumRecoveryVelocity = 1f;
pairMaterial.SpringSettings = new SpringSettings(80, 6);
pairMaterial.SpringSettings = new SpringSettings(80, 1);
}
}
@ -305,30 +396,54 @@ namespace CtrEditor.Simulacion
{
}
// ✅ REESCRITO COMPLETAMENTE: para corregir errores de compilación y aplicar la lógica de amortiguamiento de forma segura.
public void IntegrateVelocity(Vector<int> bodyIndices, Vector3Wide position, QuaternionWide orientation, BodyInertiaWide localInertia, Vector<int> integrationMask, int workerIndex, Vector<float> dt, ref BodyVelocityWide velocity)
{
// Aplicar gravedad
// 1. APLICAR GRAVEDAD Y AMORTIGUAMIENTO ESTÁNDAR (VECTORIZADO)
var gravityWide = Vector3Wide.Broadcast(Gravity);
velocity.Linear += gravityWide * dt;
velocity.Linear.X += gravityWide.X * dt;
velocity.Linear.Y += gravityWide.Y * dt;
velocity.Linear.Z += gravityWide.Z * dt;
// ✅ ELIMINADO COMPLETAMENTE - ya no se necesita lógica especial de frenado
// El sistema LinearAxisMotor maneja automáticamente todas las fuerzas
var linearDampingFactor = new Vector<float>(MathF.Pow(MathHelper.Clamp(1 - LinearDamping, 0, 1), dt[0]));
var angularDampingFactor = new Vector<float>(MathF.Pow(MathHelper.Clamp(1 - AngularDamping, 0, 1), dt[0]));
// Aplicar amortiguamiento lineal y angular para simular resistencia del aire
// Esto es crucial para que los cilindros se detengan de forma realista
var linearDampingWide = Vector<float>.One * LinearDamping;
var angularDampingWide = Vector<float>.One * AngularDamping;
velocity.Linear *= linearDampingFactor;
velocity.Angular *= angularDampingFactor;
velocity.Linear *= linearDampingWide;
velocity.Angular *= angularDampingWide; // ¡Esto es clave para detener rotaciones infinitas!
// 2. LÓGICA PERSONALIZADA PARA BOTELLAS (ESCALAR)
const float bottleExtraZAngularDamping = 0.8f;
var bottleZAngularDampingFactor = MathF.Pow(MathHelper.Clamp(1 - bottleExtraZAngularDamping, 0, 1), dt[0]);
// Recorremos cada "carril" del lote de simulación.
for (int i = 0; i < Vector<int>.Count; ++i)
{
if (integrationMask[i] != 0)
{
var bodyIndex = bodyIndices[i];
// Asegurarse de que el índice del cuerpo es válido para el conjunto activo.
if (bodyIndex < _simulationManager.simulation.Bodies.ActiveSet.Count)
{
var handle = _simulationManager.simulation.Bodies.ActiveSet.IndexToHandle[bodyIndex];
if (_simulationManager.CollidableData.TryGetValue(handle.Value, out simBase baseObj) && baseObj is simBotella)
{
// B. DAMPING ANGULAR EXTRA EN EJE Z
// Para modificar un solo carril de un Vector<T>, la forma más segura es extraer
// los valores a un array, modificar el índice y crear un nuevo Vector<T>.
var zVel = new float[Vector<float>.Count];
velocity.Angular.Z.CopyTo(zVel, 0);
zVel[i] *= bottleZAngularDampingFactor;
velocity.Angular.Z = new Vector<float>(zVel);
}
}
}
}
}
}
public class SimulationManagerBEPU
public class SimulationManagerBEPU : IDisposable
{
public Simulation simulation;
public List<simBase> Cuerpos;
@ -368,6 +483,10 @@ namespace CtrEditor.Simulacion
private object _contactsLock = new object();
// ✅ NUEVO: Diccionario para mapear BodyHandle a simBase para acceso O(1).
public Dictionary<int, simBase> CollidableData = new Dictionary<int, simBase>();
public float GlobalTime { get; private set; }
/// <summary>
/// Obtiene el objeto simBase correspondiente a un BodyHandle
/// </summary>
@ -386,37 +505,8 @@ namespace CtrEditor.Simulacion
return simBase as simCurve;
}
/// <summary>
/// ✅ NUEVO: Obtiene todas las botellas que están conectadas a un elemento específico
/// </summary>
/// <param name="element">Elemento (simTransporte o simCurve) del cual obtener las botellas conectadas</param>
/// <returns>Lista de botellas conectadas al elemento</returns>
public List<simBotella> GetBottlesConnectedToElement(simBase element)
{
var connectedBottles = new List<simBotella>();
try
{
if (element == null) return connectedBottles;
// ✅ BUSCAR BOTELLAS QUE TENGAN ESTE ELEMENTO COMO TARGET
foreach (var bottle in Cuerpos.OfType<simBotella>())
{
if (bottle != null && bottle.HasMotor && bottle.CurrentMotorTarget == element)
{
connectedBottles.Add(bottle);
}
}
System.Diagnostics.Debug.WriteLine($"[GetBottlesConnectedToElement] Encontradas {connectedBottles.Count} botellas conectadas a {element.GetType().Name}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[GetBottlesConnectedToElement] ❌ ERROR: {ex.Message}");
}
return connectedBottles;
}
// ✅ ELIMINADO: GetBottlesConnectedToElement ya no es necesario.
// La conexión ahora es implícita a través de la física de contacto.
// ✅ NUEVOS - gestión automática de clasificación
private void RegisterObjectHandle(simBase obj)
@ -543,7 +633,55 @@ namespace CtrEditor.Simulacion
simulation = Simulation.Create(bufferPool, narrowPhaseCallbacks, poseIntegratorCallbacks, solveDescription);
// ✅ NUEVO: Suscribirse a los eventos del solver para controlar la velocidad de los cuerpos cinemáticos.
simulation.Solver.SubstepStarted += OnSubstepStarted;
simulation.Solver.SubstepEnded += OnSubstepEnded;
}
/// <summary>
/// ✅ NUEVO: Se ejecuta al inicio de cada sub-paso de la simulación.
/// Restaura las velocidades de los cuerpos cinemáticos para que el solver pueda usarlas para calcular la fricción.
/// </summary>
private void OnSubstepStarted(int substepIndex)
{
foreach (var transport in Cuerpos.OfType<simTransporte>())
{
transport.ActualizarVelocidadCinematica();
}
foreach (var curve in Cuerpos.OfType<simCurve>())
{
curve.ActualizarVelocidadCinematica();
}
}
/// <summary>
/// ✅ NUEVO: Se ejecuta al final de cada sub-paso de la simulación, pero antes de la integración de la pose.
/// Pone a cero las velocidades de los cuerpos cinemáticos para evitar que el PoseIntegrator mueva su posición.
/// </summary>
private void OnSubstepEnded(int substepIndex)
{
foreach (var transport in Cuerpos.OfType<simTransporte>())
{
if (transport.BodyHandle.Value >= 0 && simulation.Bodies.BodyExists(transport.BodyHandle))
{
var body = simulation.Bodies.GetBodyReference(transport.BodyHandle);
if (body.Kinematic)
{
body.Velocity.Linear = Vector3.Zero;
}
}
}
foreach (var curve in Cuerpos.OfType<simCurve>())
{
if (curve.BodyHandle.Value >= 0 && simulation.Bodies.BodyExists(curve.BodyHandle))
{
var body = simulation.Bodies.GetBodyReference(curve.BodyHandle);
if (body.Kinematic)
{
body.Velocity.Angular = Vector3.Zero;
}
}
}
}
public void Clear()
@ -614,6 +752,10 @@ namespace CtrEditor.Simulacion
// Limpiar visualización 3D
Visualization3DManager?.Clear();
// ✅ NUEVO: Limpiar también el nuevo diccionario.
CollidableData.Clear();
GlobalTime = 0f;
}
catch (Exception ex)
{
@ -625,7 +767,7 @@ namespace CtrEditor.Simulacion
{
try
{
// ✅ INICIALIZAR PROPIEDADES CACHEADAS
// ✅ INICIALIZAR PROPIEDADES CACHEADAS Y VELOCIDADES CINEMÁTICAS
foreach (var transport in Cuerpos.OfType<simTransporte>())
{
transport.UpdateCachedProperties();
@ -633,8 +775,7 @@ namespace CtrEditor.Simulacion
foreach (var curve in Cuerpos.OfType<simCurve>())
{
// ✅ CORREGIDO: Usar método público para actualizar velocidad
curve.SetSpeed(curve.Speed); // ✅ SIMPLIFICADO: Reinicializar velocidad
curve.SetSpeed(curve.Speed);
}
stopwatch.Start();
@ -654,6 +795,12 @@ namespace CtrEditor.Simulacion
{
_frameCount++;
// ✅ NUEVO: Resetear el estado de contacto para el sistema de presión
foreach (var botella in Cuerpos.OfType<simBotella>())
{
botella.IsTouchingTransport = false;
}
// ✅ CONSERVAR - validación de deltaTime
var currentTime = stopwatch.Elapsed.TotalMilliseconds;
var deltaTime = (float)((currentTime - stopwatch_last) / 1000.0);
@ -717,8 +864,8 @@ namespace CtrEditor.Simulacion
{
if (simulation.Bodies.BodyExists(cuerpo.BodyHandle))
{
cuerpo.LimitRotationToXYPlane();
simulation.Awakener.AwakenBody(cuerpo.BodyHandle);
// cuerpo.LimitRotationToXYPlane();
// simulation.Awakener.AwakenBody(cuerpo.BodyHandle);
}
}
catch (Exception ex)
@ -727,16 +874,10 @@ namespace CtrEditor.Simulacion
}
}
// ✅ NUEVO: Detener motores de botellas que no están en contacto con elementos
// Esto se ejecuta cada 10 frames para eficiencia
if (_frameCount % 10 == 0)
{
//StopMotorsForBottlesNotInContact();
}
// ✅ CONSERVAR - sistemas que funcionan bien
ProcessBarreraContacts();
ProcessDescarteContacts();
ProcessPressureSystem();
ProcessCleanupSystem();
// ✅ SIMPLIFICAR - limpiar solo contactos que usamos
@ -763,53 +904,83 @@ namespace CtrEditor.Simulacion
/// <summary>
/// ✅ NUEVO: Sistema para controlar la "presión" y evitar que las botellas floten
/// Si una botella no toca un transporte por varios frames, se reestablece su altura.
/// </summary>
private void ProcessPressureSystem()
{
try
{
var botellas = Cuerpos.OfType<simBotella>().ToList();
foreach (var botella in botellas)
{
try
{
if (botella == null || !simulation.Bodies.BodyExists(botella.BodyHandle))
continue;
if (!botella.IsTouchingTransport && botella.LastTransportCollisionZ.HasValue)
{
var body = simulation.Bodies.GetBodyReference(botella.BodyHandle);
var currentZ = body.Pose.Position.Z;
// ✅ NUEVA CONDICIÓN: Solo actuar si la botella ha subido
if (currentZ > botella.LastTransportCollisionZ.Value)
{
// Incrementar el contador de presión.
botella.PressureBuildup++;
// Si se acumula suficiente presión (más de 2 frames sin contacto y subiendo),
// es un indicio de que está flotando. La reestablecemos a su última Z conocida.
if (botella.PressureBuildup > 2)
{
var pose = body.Pose;
pose.Position.Z = botella.LastTransportCollisionZ.Value;
body.Pose = pose;
// Opcional: resetear el contador para no aplicarlo constantemente en cada frame
// botella.PressureBuildup = 0;
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ProcessPressureSystem] Error processing bottle {botella?.BodyHandle}: {ex.Message}");
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ProcessPressureSystem] Critical error: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Marca un objeto para eliminación diferida (más seguro)
/// </summary>
public void Remove(simBase Objeto)
{
if (Objeto == null) return;
try
if (Objeto != null)
{
// ✅ SIMPLIFICADO - eliminar lógica de masa especial
UnregisterObjectHandle(Objeto); // ✅ NUEVO
Objeto.RemoverBody(); // ✅ CONSERVAR - remover el cuerpo de BEPU
// ✅ NUEVO - Limpiar dimensiones almacenadas en osBase
CtrEditor.ObjetosSim.osBase.ClearStoredDimensions(Objeto);
// ✅ REMOVER de la lista inmediatamente para evitar referencias colgantes
Cuerpos.Remove(Objeto);
if (Is3DUpdateEnabled)
// Aplazar la eliminación para evitar problemas de concurrencia
_deferredActions.Add(() =>
{
Visualization3DManager?.SynchronizeWorld();
}
//System.Diagnostics.Debug.WriteLine($"[Remove] ✅ Objeto marcado para eliminación diferida: {Objeto.GetType().Name}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[Remove] ❌ Error marcando objeto {Objeto?.GetType().Name}: {ex.Message}");
// ✅ CRÍTICO: Siempre remover de la lista incluso si falló
Cuerpos.Remove(Objeto);
UnregisterObjectHandle(Objeto);
Objeto.RemoverBody();
Cuerpos.Remove(Objeto);
});
}
}
public simBotella AddCircle(float diameter, Vector2 position, float mass)
public simBotella AddBotella(float diameter, Vector2 position, float mass)
{
var botella = new simBotella(simulation, _deferredActions, diameter, position, mass);
Cuerpos.Add(botella);
RegisterObjectHandle(botella); // ✅ NUEVO
if (Is3DUpdateEnabled)
{
Visualization3DManager?.SynchronizeWorld();
}
RegisterObjectHandle(botella);
return botella;
}
@ -960,223 +1131,6 @@ namespace CtrEditor.Simulacion
return new BarreraData(false, false);
}
/// <summary>
/// ✅ NUEVO: Detiene motores de botellas que no están en contacto con elementos
/// </summary>
private void StopMotorsForBottlesNotInContact()
{
try
{
var allBottles = Cuerpos.OfType<simBotella>().ToList();
foreach (var bottle in allBottles)
{
if (bottle != null && bottle.HasMotor && bottle.IsOnElement)
{
// ✅ VERIFICAR SI LA BOTELLA ESTÁ REALMENTE EN CONTACTO CON ALGÚN ELEMENTO
bool isInContact = false;
// Verificar contacto con transportes
foreach (var transport in Cuerpos.OfType<simTransporte>())
{
if (IsBottleInContactWithTransport(bottle, transport))
{
isInContact = true;
break;
}
}
// Verificar contacto con curvas
if (!isInContact)
{
foreach (var curve in Cuerpos.OfType<simCurve>())
{
if (IsBottleInContactWithCurve(bottle, curve))
{
isInContact = true;
break;
}
}
}
// ✅ DETENER MOTOR SI NO ESTÁ EN CONTACTO
if (!isInContact)
{
bottle.StopMotor();
System.Diagnostics.Debug.WriteLine($"[StopMotorsForBottlesNotInContact] ⏹️ Detenido: {bottle.BodyHandle}");
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[StopMotorsForBottlesNotInContact] ❌ ERROR: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Verifica si una botella está en contacto con un transporte
/// </summary>
private bool IsBottleInContactWithTransport(simBotella bottle, simTransporte transport)
{
try
{
var bottlePos = bottle.GetPosition();
var transportPos = transport.GetPosition();
var distance = Vector2.Distance(
new Vector2(bottlePos.X, bottlePos.Y),
new Vector2(transportPos.X, transportPos.Y)
);
// Verificar si está dentro del área del transporte + tolerancia
var maxDistance = Math.Max(transport.Width, transport.Height) / 2f + bottle.Radius + 0.1f;
return distance <= maxDistance;
}
catch
{
return false;
}
}
/// <summary>
/// ✅ NUEVO: Verifica si una botella está en contacto con una curva
/// </summary>
private bool IsBottleInContactWithCurve(simBotella bottle, simCurve curve)
{
try
{
var bottlePos = bottle.GetPosition();
var curveCenter = curve.CurveCenter; // ✅ NUEVO: Usar centro real
var distance = Vector2.Distance(
new Vector2(bottlePos.X, bottlePos.Y),
new Vector2(curveCenter.X, curveCenter.Y)
);
// Verificar si está dentro del área de la curva + tolerancia
var maxDistance = curve.OuterRadius + bottle.Radius + 0.1f;
return distance <= maxDistance;
}
catch
{
return false;
}
}
/// <summary>
/// ✅ NUEVO: Calcula la dirección tangencial específica basada en la posición de la botella
/// </summary>
public Vector3 CalculateCurveDirectionFromBottlePosition(simCurve curve, simBotella bottle)
{
try
{
// ✅ NUEVO: Usar el centro real almacenado de la curva
var curveCenter = curve.CurveCenter;
var bottlePosition = bottle.GetPosition();
// Calcular el vector desde el centro de la curva hasta la botella (en el plano XY)
var radiusVector = new Vector3(bottlePosition.X - curveCenter.X, bottlePosition.Y - curveCenter.Y, 0f);
var radius = radiusVector.Length();
if (radius < 0.001f)
{
System.Diagnostics.Debug.WriteLine($"[CalculateCurveDirectionFromBottlePosition] ⚠️ Botella muy cerca del centro de la curva");
return Vector3.UnitX; // Fallback
}
// Normalizar el vector radial
var normalizedRadius = radiusVector / radius;
// Calcular la dirección tangencial (perpendicular al radio)
// En coordenadas 2D: si r = (x, y), entonces t = (-y, x) es tangente
var tangentDirection = new Vector3(-normalizedRadius.Y, normalizedRadius.X, 0f);
// Verificar que la dirección tangencial apunte en el sentido correcto según la velocidad de la curva
if (curve.Speed < 0)
{
//tangentDirection = -tangentDirection;
}
//System.Diagnostics.Debug.WriteLine($"[CalculateCurveDirectionFromBottlePosition] 📐 Dirección calculada:");
//System.Diagnostics.Debug.WriteLine($" Centro curva: {curveCenter}");
//System.Diagnostics.Debug.WriteLine($" Posición botella: {bottlePosition}");
//System.Diagnostics.Debug.WriteLine($" Radio: {radius:F3}");
//System.Diagnostics.Debug.WriteLine($" Vector radial: {normalizedRadius}");
//System.Diagnostics.Debug.WriteLine($" Dirección tangencial: {tangentDirection} (Longitud: {tangentDirection.Length():F3})");
return -tangentDirection;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[CalculateCurveDirectionFromBottlePosition] ❌ ERROR: {ex.Message}");
return Vector3.UnitX; // Fallback
}
}
public void Dispose()
{
Clear();
simulation?.Dispose();
bufferPool?.Clear();
}
/// <summary>
/// Registra un contacto entre una barrera y una botella para detección de paso
/// </summary>
/// <param name="barrera">Barrera que detecta el paso</param>
/// <param name="botella">Botella que está pasando</param>
internal void RegisterBarreraContact(simBarrera barrera, simBotella botella)
{
if (barrera != null && botella != null)
{
lock (_contactsLock)
{
if (!_barreraContacts.ContainsKey(barrera))
{
_barreraContacts[barrera] = new List<simBotella>();
}
if (!_barreraContacts[barrera].Contains(botella))
{
_barreraContacts[barrera].Add(botella);
}
}
}
}
/// <summary>
/// Registra un contacto entre un descarte y una botella para marcar eliminación
/// </summary>
/// <param name="descarte">Descarte que detecta la botella</param>
/// <param name="botella">Botella que debe ser eliminada</param>
public void RegisterDescarteContact(simDescarte descarte, simBotella botella)
{
if (descarte != null && botella != null)
{
lock (_contactsLock)
{
// Marcar la botella para eliminación
_botellasParaEliminar.Add(botella);
botella.Descartar = true;
// También registrar el contacto para la lista del descarte
if (!_descarteContacts.ContainsKey(descarte))
{
_descarteContacts[descarte] = new List<simBotella>();
}
if (!_descarteContacts[descarte].Contains(botella))
{
_descarteContacts[descarte].Add(botella);
}
}
}
}
/// <summary>
/// ✅ NUEVO: Procesa todas las barreras usando RayCast de BEPU
/// Elimina la dependencia de contactos físicos y la necesidad de mantener botellas despiertas
@ -1219,9 +1173,6 @@ namespace CtrEditor.Simulacion
}
}
// ✅ ELIMINADO: CalculateBarreraDetectionGeometric - ya no se usa
// El nuevo sistema usa RayCast nativo de BEPU a través de simBarrera.PerformRaycast()
/// <summary>
/// Procesa contactos de descarte para eliminar botellas marcadas usando detección geométrica
/// </summary>
@ -1258,12 +1209,6 @@ namespace CtrEditor.Simulacion
// Eliminar botellas marcadas para eliminación (después de procesamiento)
RemoveMarkedBottles();
// Limpiar contactos ya que no los usamos más
lock (_contactsLock)
{
_descarteContacts.Clear();
}
}
catch (Exception ex)
{
@ -1361,6 +1306,15 @@ namespace CtrEditor.Simulacion
List<simBotella> botellasAEliminar;
lock (_contactsLock)
{
// ✅ NUEVO: Añadir botellas marcadas como "Descartar" desde los callbacks
foreach (var botella in Cuerpos.OfType<simBotella>())
{
if (botella.Descartar)
{
_botellasParaEliminar.Add(botella);
}
}
botellasAEliminar = new List<simBotella>(_botellasParaEliminar);
_botellasParaEliminar.Clear();
}
@ -1459,5 +1413,36 @@ namespace CtrEditor.Simulacion
return lineStart + lineDirection * projectionLength;
}
public void Dispose()
{
Clear();
simulation?.Dispose();
bufferPool?.Clear();
}
/// <summary>
/// Registra un contacto entre una barrera y una botella para detección de paso
/// </summary>
/// <param name="barrera">Barrera que detecta el paso</param>
/// <param name="botella">Botella que está pasando</param>
internal void RegisterBarreraContact(simBarrera barrera, simBotella botella)
{
if (barrera != null && botella != null)
{
lock (_contactsLock)
{
if (!_barreraContacts.ContainsKey(barrera))
{
_barreraContacts[barrera] = new List<simBotella>();
}
if (!_barreraContacts[barrera].Contains(botella))
{
_barreraContacts[barrera].Add(botella);
}
}
}
}
}
}

View File

@ -8,6 +8,8 @@ using BepuPhysics.Collidables;
using HelixToolkit.Wpf;
using System.Linq;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows;
namespace CtrEditor.Simulacion
{
@ -28,6 +30,9 @@ namespace CtrEditor.Simulacion
public float StartAngle { get; set; }
public float EndAngle { get; set; }
// ✅ NUEVO: Estado de movimiento para transportes
public bool IsMoving { get; set; }
public bool Equals(ShapeDimensions other)
{
return Math.Abs(Width - other.Width) < 0.001f &&
@ -38,7 +43,8 @@ namespace CtrEditor.Simulacion
Math.Abs(OuterRadius - other.OuterRadius) < 0.001f &&
Math.Abs(StartAngle - other.StartAngle) < 0.001f &&
Math.Abs(EndAngle - other.EndAngle) < 0.001f &&
ShapeType == other.ShapeType;
ShapeType == other.ShapeType &&
IsMoving == other.IsMoving;
}
}
@ -71,8 +77,11 @@ namespace CtrEditor.Simulacion
private Dictionary<simBase, ModelVisual3D> simBaseToModelMap;
private Dictionary<simBase, ShapeDimensions> lastKnownDimensions;
// ✅ NUEVO: Diccionario para gestionar animaciones activas
private Dictionary<simBase, Storyboard> activeAnimations;
// ✅ CORREGIDO: Flag de debug para mostrar triángulos individuales de curvas (true temporalmente para verificar)
public static bool DebugShowIndividualTriangles { get; set; } = true;
public static bool DebugShowIndividualTriangles { get; set; } = false;
public HelixViewport3D Viewport3D
{
@ -92,6 +101,7 @@ namespace CtrEditor.Simulacion
simulationManager = simManager;
simBaseToModelMap = new Dictionary<simBase, ModelVisual3D>();
lastKnownDimensions = new Dictionary<simBase, ShapeDimensions>();
activeAnimations = new Dictionary<simBase, Storyboard>();
InitializeViewport();
}
@ -112,14 +122,14 @@ namespace CtrEditor.Simulacion
// Agregar luces
var directionalLight = new DirectionalLight
{
Color = Colors.White,
Direction = new Vector3D(0, 0, -1)
Color = Color.FromRgb(150, 150, 150), // Luz direccional menos intensa
Direction = new Vector3D(-0.2, -0.3, -1) // Ligeramente angulada para evitar reflejos directos
};
viewport3D.Children.Add(new ModelVisual3D { Content = directionalLight });
var ambientLight = new AmbientLight
{
Color = Color.FromRgb(64, 64, 64)
Color = Color.FromRgb(120, 120, 120) // Más luz ambiente para suavizar sombras
};
viewport3D.Children.Add(new ModelVisual3D { Content = ambientLight });
@ -243,6 +253,12 @@ namespace CtrEditor.Simulacion
foreach (var simObj in objectsToRemove)
{
// ✅ NUEVO: Limpiar animaciones activas para objetos eliminados
if (simObj is simTransporte transporteToRemove && activeAnimations.ContainsKey(transporteToRemove))
{
StopTransportAnimation(transporteToRemove);
}
simBaseToModelMap.Remove(simObj);
lastKnownDimensions.Remove(simObj);
}
@ -397,7 +413,7 @@ namespace CtrEditor.Simulacion
}
else
{
// ✅ Usar directamente los triángulos originales de BEPU (superficie continua)
// ✅ CORREGIDO: Crea un arco de mesh continuo y simple desde los triángulos de BEPU
CreateCurveMeshFromBEPUTriangles(meshBuilder, curve);
}
@ -422,44 +438,57 @@ namespace CtrEditor.Simulacion
}
/// <summary>
/// ✅ CORREGIDO: Crea mesh desde los triángulos locales de BEPU
/// Extrae la geometría exacta en coordenadas locales para evitar transformación duplicada
/// ✅ CORREGIDO: Crea un arco de mesh continuo y simple desde los triángulos de BEPU
/// Genera una superficie plana que forma un arco suave y eficiente
/// </summary>
private void CreateCurveMeshFromBEPUTriangles(MeshBuilder meshBuilder, simCurve curve)
{
try
{
// ✅ EXTRAER TRIÁNGULOS LOCALES DE BEPU (sin transformación duplicada)
// Obtener los triángulos de BEPU
var localTriangles = curve.GetRealBEPUTriangles();
System.Diagnostics.Debug.WriteLine($"[3D Curve] Creating continuous arc mesh from {localTriangles.Length} BEPU triangles");
if (localTriangles.Length == 0)
{
System.Diagnostics.Debug.WriteLine($"[3D BEPU] WARNING: No se pudieron extraer triángulos locales, usando fallback");
CreateCurveMeshFallback(meshBuilder, curve);
System.Diagnostics.Debug.WriteLine($"[3D Curve] WARNING: No triangles found in BEPU curve");
return;
}
System.Diagnostics.Debug.WriteLine($"[3D BEPU] Creando mesh desde {localTriangles.Length} triángulos locales de BEPU");
// ✅ CORREGIDO: Usar altura más visible y verificar coordenadas
const float curveHeight = 0.0f; // 0.02f; // 2cm de altura para que sea claramente visible
int triangleCount = 0;
// ✅ USAR TRIÁNGULOS LOCALES DE BEPU (la transformación se aplicará automáticamente en UpdateVisualization)
foreach (var triangle in localTriangles)
{
// Convertir triángulos de BEPU a puntos 3D de Helix
var pointA = new Point3D(triangle.A.X, triangle.A.Y, triangle.A.Z);
var pointB = new Point3D(triangle.B.X, triangle.B.Y, triangle.B.Z);
var pointC = new Point3D(triangle.C.X, triangle.C.Y, triangle.C.Z);
// ✅ LOGGING: Verificar las coordenadas del primer triángulo
if (triangleCount == 0)
{
System.Diagnostics.Debug.WriteLine($"[3D Curve] First triangle: A({triangle.A.X:F3}, {triangle.A.Y:F3}, {triangle.A.Z:F3})");
System.Diagnostics.Debug.WriteLine($"[3D Curve] First triangle: B({triangle.B.X:F3}, {triangle.B.Y:F3}, {triangle.B.Z:F3})");
System.Diagnostics.Debug.WriteLine($"[3D Curve] First triangle: C({triangle.C.X:F3}, {triangle.C.Y:F3}, {triangle.C.Z:F3})");
}
// Agregar triángulo en coordenadas locales al mesh
// Crear triángulo en la superficie superior con altura visible
var pointA = new Point3D(triangle.A.X, triangle.A.Y, triangle.A.Z + curveHeight);
var pointB = new Point3D(triangle.B.X, triangle.B.Y, triangle.B.Z + curveHeight);
var pointC = new Point3D(triangle.C.X, triangle.C.Y, triangle.C.Z + curveHeight);
// ✅ CORREGIDO: Probar ambos órdenes de vértices para asegurar normales correctas
meshBuilder.AddTriangle(pointA, pointB, pointC);
// También agregar el triángulo con orden inverso para que sea visible desde ambos lados
meshBuilder.AddTriangle(pointA, pointC, pointB);
triangleCount++;
}
System.Diagnostics.Debug.WriteLine($"[3D BEPU] ✅ Mesh creado usando triángulos locales de BEPU (transformación aplicada automáticamente)");
System.Diagnostics.Debug.WriteLine($"[3D Curve] ✅ Continuous arc mesh created with {triangleCount} triangles (height: {curveHeight})");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D BEPU] ERROR extrayendo triángulos locales: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"[3D BEPU] Fallback a geometría recreada");
CreateCurveMeshFallback(meshBuilder, curve);
System.Diagnostics.Debug.WriteLine($"[3D Curve] ERROR creating continuous arc mesh: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"[3D Curve] Stack trace: {ex.StackTrace}");
}
}
@ -477,7 +506,7 @@ namespace CtrEditor.Simulacion
if (localTriangles.Length == 0)
{
System.Diagnostics.Debug.WriteLine($"[3D Debug] WARNING: No hay triángulos locales para debug, usando fallback");
CreateCurveMeshFallback(meshBuilder, curve);
CreateBasicDebugGeometry(meshBuilder, curve);
return;
}
@ -574,81 +603,6 @@ namespace CtrEditor.Simulacion
}
}
/// <summary>
/// Método fallback que recrea la geometría (mantener por compatibilidad)
/// </summary>
private void CreateCurveMeshFallback(MeshBuilder meshBuilder, simCurve curve)
{
// Obtener parámetros de la curva
float innerRadius = curve.InnerRadius;
float outerRadius = curve.OuterRadius;
float startAngle = curve.StartAngle;
float endAngle = curve.EndAngle;
System.Diagnostics.Debug.WriteLine($"[3D Fallback] Parameters - Inner: {innerRadius}, Outer: {outerRadius}, Start: {startAngle}, End: {endAngle}");
// Configuración de segmentos
const float SegmentationFactor = 32f / 3f;
const int MinSegments = 8;
const int MaxSegments = 64;
// Calcular número de segmentos basado en el tamaño del arco
float arcLength = (endAngle - startAngle) * ((innerRadius + outerRadius) / 2f);
int segments = (int)(arcLength * SegmentationFactor);
segments = Math.Max(MinSegments, Math.Min(segments, MaxSegments));
float angleStep = (endAngle - startAngle) / segments;
// Altura muy pequeña para simular triángulos planos
const float curveHeight = 0.05f;
// Generar vértices para el arco interior y exterior
var innerBottomPoints = new Point3D[segments + 1];
var innerTopPoints = new Point3D[segments + 1];
var outerBottomPoints = new Point3D[segments + 1];
var outerTopPoints = new Point3D[segments + 1];
for (int i = 0; i <= segments; i++)
{
float angle = startAngle + i * angleStep;
float cosAngle = (float)Math.Cos(angle);
float sinAngle = (float)Math.Sin(angle);
// Puntos en la parte inferior (Z = 0)
innerBottomPoints[i] = new Point3D(innerRadius * cosAngle, innerRadius * sinAngle, 0);
outerBottomPoints[i] = new Point3D(outerRadius * cosAngle, outerRadius * sinAngle, 0);
// Puntos en la parte superior (Z = curveHeight)
innerTopPoints[i] = new Point3D(innerRadius * cosAngle, innerRadius * sinAngle, curveHeight);
outerTopPoints[i] = new Point3D(outerRadius * cosAngle, outerRadius * sinAngle, curveHeight);
}
// Crear la superficie superior de la curva
for (int i = 0; i < segments; i++)
{
meshBuilder.AddTriangle(innerTopPoints[i], outerTopPoints[i], outerTopPoints[i + 1]);
meshBuilder.AddTriangle(innerTopPoints[i], outerTopPoints[i + 1], innerTopPoints[i + 1]);
}
// Crear la superficie inferior de la curva
for (int i = 0; i < segments; i++)
{
meshBuilder.AddTriangle(innerBottomPoints[i], outerBottomPoints[i + 1], outerBottomPoints[i]);
meshBuilder.AddTriangle(innerBottomPoints[i], innerBottomPoints[i + 1], outerBottomPoints[i + 1]);
}
// Crear las paredes laterales
for (int i = 0; i < segments; i++)
{
meshBuilder.AddQuad(innerBottomPoints[i], innerBottomPoints[i + 1], innerTopPoints[i + 1], innerTopPoints[i]);
meshBuilder.AddQuad(outerBottomPoints[i], outerTopPoints[i], outerTopPoints[i + 1], outerBottomPoints[i + 1]);
}
// Crear las paredes de los extremos
meshBuilder.AddQuad(innerBottomPoints[0], innerTopPoints[0], outerTopPoints[0], outerBottomPoints[0]);
meshBuilder.AddQuad(outerBottomPoints[segments], outerTopPoints[segments], innerTopPoints[segments], innerBottomPoints[segments]);
}
private void UpdateVisualizationFromSimBase(simBase simObj)
{
if (!simBaseToModelMap.TryGetValue(simObj, out var visual))
@ -688,6 +642,12 @@ namespace CtrEditor.Simulacion
// System.Diagnostics.Debug.WriteLine($"[3D Update] No geometry changes needed for {simObj.GetType().Name}");
}
// ✅ NUEVO: Gestionar animaciones para transportes
if (simObj is simTransporte transporte)
{
ManageTransportAnimation(transporte, visual);
}
// Actualizar transformación del modelo 3D
var transform = new Transform3DGroup();
@ -751,6 +711,12 @@ namespace CtrEditor.Simulacion
return dimensions;
}
// ✅ NUEVO: Para simTransporte, detectar si está en movimiento
if (simObj is simTransporte transporte)
{
dimensions.IsMoving = Math.Abs(transporte.Speed) > 0.001f; // Consideramos movimiento si la velocidad es mayor que 0.001
}
// Para otros objetos, usar la forma real de BEPU
if (shapeIndex.Type == BepuPhysics.Collidables.Sphere.Id)
{
@ -868,6 +834,13 @@ namespace CtrEditor.Simulacion
public void Clear()
{
// ✅ NUEVO: Detener todas las animaciones activas antes de limpiar
foreach (var kvp in activeAnimations)
{
kvp.Value.Stop();
}
activeAnimations.Clear();
foreach (var model in simBaseToModelMap.Values)
{
viewport3D.Children.Remove(model);
@ -895,6 +868,40 @@ namespace CtrEditor.Simulacion
);
}
/// <summary>
/// ✅ NUEVO: Crea un material animable (no frozen) para poder animar sus propiedades
/// </summary>
/// <param name="color">Color del material</param>
/// <param name="specularPower">Potencia especular</param>
/// <param name="ambient">Nivel de luz ambiente</param>
/// <returns>Material que puede ser animado</returns>
private Material CreateAnimatableMaterial(Color color, double specularPower = 100, byte ambient = 190)
{
// Crear brush animable (no frozen)
var diffuseBrush = new SolidColorBrush(color);
// Crear material difuso
var diffuseMaterial = new DiffuseMaterial(diffuseBrush);
// Crear material especular - ✅ CORREGIDO: Usar un gris claro en lugar de blanco puro para reducir el brillo excesivo
var specularBrush = new SolidColorBrush(Color.FromRgb(128, 128, 128));
var specularMaterial = new SpecularMaterial(specularBrush, specularPower);
// Crear material "ambiente" (usando Emissive) - ✅ CORREGIDO: Usar un color mucho más oscuro para no sobreexponer
var ambientColor = Color.FromRgb((byte)(ambient / 4), (byte)(ambient / 4), (byte)(ambient / 4));
var ambientBrush = new SolidColorBrush(ambientColor);
var ambientMaterial = new EmissiveMaterial(ambientBrush);
// Combinar todos los materiales
var materialGroup = new MaterialGroup();
materialGroup.Children.Add(diffuseMaterial);
materialGroup.Children.Add(specularMaterial);
materialGroup.Children.Add(ambientMaterial);
// NO hacer freeze para mantener animable
return materialGroup;
}
/// <summary>
/// Función centralizada para obtener el material apropiado según el tipo de simBase
/// </summary>
@ -902,14 +909,24 @@ namespace CtrEditor.Simulacion
/// <returns>Material configurado para el tipo de objeto</returns>
private Material GetMaterialForSimBase(simBase simObj)
{
if (simObj is simTransporte)
if (simObj is simTransporte transporte)
{
// Material plástico verde para transportes
return MaterialHelper.CreateMaterial(
new SolidColorBrush(Color.FromRgb(80, 180, 80)),
specularPower: 140,
ambient: 200
);
// ✅ NUEVO: Cambiar color según si está en movimiento
bool isMoving = Math.Abs(transporte.Speed) > 0.001f;
if (isMoving)
{
// ✅ CORREGIDO: Usar material animable para transportes en movimiento
return CreateAnimatableMaterial(Color.FromRgb(40, 255, 40), 180, 240);
}
else
{
// Material plástico verde normal para transportes detenidos
return MaterialHelper.CreateMaterial(
new SolidColorBrush(Color.FromRgb(80, 180, 80)),
specularPower: 140,
ambient: 200
);
}
}
else if (simObj is simGuia)
{
@ -1002,6 +1019,150 @@ namespace CtrEditor.Simulacion
}
}
/// <summary>
/// ✅ NUEVO: Crea una animación de movimiento para transportes usando StoryBoard
/// Combina rotación sutil + pulsación de brillo para simular movimiento
/// </summary>
/// <param name="transporte">Transporte al cual aplicar la animación</param>
/// <param name="visual">Visual3D del transporte</param>
private void CreateTransportMovementAnimation(simTransporte transporte, ModelVisual3D visual)
{
try
{
// Detener animación anterior si existe
StopTransportAnimation(transporte);
// Crear nuevo StoryBoard
var storyboard = new Storyboard();
storyboard.RepeatBehavior = RepeatBehavior.Forever;
// ✅ ANIMACIÓN 1: Rotación sutil continua alrededor del eje Z (muy lenta)
var rotationAnimation = new DoubleAnimation
{
From = 0,
To = 360,
Duration = TimeSpan.FromSeconds(20), // 20 segundos por vuelta completa (muy lento)
RepeatBehavior = RepeatBehavior.Forever
};
// Crear transform group si no existe
if (visual.Transform is not Transform3DGroup transformGroup)
{
transformGroup = new Transform3DGroup();
if (visual.Transform != null)
transformGroup.Children.Add(visual.Transform);
visual.Transform = transformGroup;
}
// Agregar rotación de animación
var animationRotation = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 0, 1), 0));
transformGroup.Children.Add(animationRotation);
// Configurar target para la rotación
Storyboard.SetTarget(rotationAnimation, animationRotation);
Storyboard.SetTargetProperty(rotationAnimation, new PropertyPath("Rotation.Angle"));
storyboard.Children.Add(rotationAnimation);
// ✅ ANIMACIÓN 2: Pulsación de brillo del material
if (visual.Content is GeometryModel3D geometryModel &&
geometryModel.Material is MaterialGroup materialGroup)
{
// Buscar el material difuso para animar (debe ser el primero en mi CreateAnimatableMaterial)
var diffuseMaterial = materialGroup.Children.OfType<DiffuseMaterial>().FirstOrDefault();
if (diffuseMaterial?.Brush is SolidColorBrush diffuseBrush)
{
// ✅ CORREGIDO: El brush ya debe ser animable si se creó con CreateAnimatableMaterial
var colorAnimation = new ColorAnimation
{
From = Color.FromRgb(40, 255, 40), // Verde brillante
To = Color.FromRgb(80, 255, 80), // Verde más brillante
Duration = TimeSpan.FromSeconds(1.5), // Pulsación cada 1.5 segundos
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever
};
Storyboard.SetTarget(colorAnimation, diffuseBrush);
Storyboard.SetTargetProperty(colorAnimation, new PropertyPath("Color"));
storyboard.Children.Add(colorAnimation);
}
}
// Iniciar animación
storyboard.Begin();
// Almacenar referencia para poder detenerla después
activeAnimations[transporte] = storyboard;
System.Diagnostics.Debug.WriteLine($"[3D Animation] ✅ Animación de movimiento creada para transporte");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Animation] ❌ Error creando animación: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Detiene la animación de movimiento de un transporte
/// </summary>
/// <param name="transporte">Transporte cuya animación se desea detener</param>
private void StopTransportAnimation(simTransporte transporte)
{
try
{
if (activeAnimations.TryGetValue(transporte, out var storyboard))
{
storyboard.Stop();
activeAnimations.Remove(transporte);
// Limpiar transforms de animación del visual
if (simBaseToModelMap.TryGetValue(transporte, out var visual) &&
visual.Transform is Transform3DGroup transformGroup)
{
// Remover solo la rotación de animación (la última agregada)
if (transformGroup.Children.Count > 1)
{
transformGroup.Children.RemoveAt(transformGroup.Children.Count - 1);
}
}
System.Diagnostics.Debug.WriteLine($"[3D Animation] ✅ Animación de transporte detenida");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Animation] ❌ Error deteniendo animación: {ex.Message}");
}
}
/// <summary>
/// ✅ NUEVO: Gestiona automáticamente las animaciones de transportes según su estado
/// </summary>
/// <param name="transporte">Transporte a gestionar</param>
/// <param name="visual">Visual3D del transporte</param>
private void ManageTransportAnimation(simTransporte transporte, ModelVisual3D visual)
{
try
{
bool isMoving = Math.Abs(transporte.Speed) > 0.001f;
bool hasActiveAnimation = activeAnimations.ContainsKey(transporte);
if (isMoving && !hasActiveAnimation)
{
// Crear animación si el transporte está en movimiento y no tiene animación
CreateTransportMovementAnimation(transporte, visual);
}
else if (!isMoving && hasActiveAnimation)
{
// Detener animación si el transporte está detenido y tiene animación
StopTransportAnimation(transporte);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[3D Animation] ❌ Error gestionando animación: {ex.Message}");
}
}
/// <summary>
/// ✅ MODO DEBUG DE TRIÁNGULOS DE CURVAS:
///
@ -1372,7 +1533,8 @@ namespace CtrEditor.Simulacion
InnerRadius = 0,
OuterRadius = 0,
StartAngle = 0,
EndAngle = 0
EndAngle = 0,
IsMoving = false // Las barreras no se mueven
};
}

View File

@ -11,6 +11,8 @@ namespace CtrEditor.Simulacion
{
public class simBase
{
public object WpfObject { get; set; }
public string SimObjectType { get; set; }
public BodyHandle BodyHandle { get; protected set; }
public Simulation _simulation;
protected bool _bodyCreated = false; // Bandera para saber si hemos creado un cuerpo

View File

@ -8,6 +8,8 @@ using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using DocumentFormat.OpenXml.Spreadsheet;
using System.Windows.Media.Media3D;
using System.Windows.Shapes;
namespace CtrEditor.Simulacion
{
@ -29,17 +31,14 @@ namespace CtrEditor.Simulacion
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;
// ✅ NUEVO: Propiedades para control de flotación/presión
public float? LastTransportCollisionZ { get; set; }
public float PressureBuildup { get; set; } = 0;
public bool IsTouchingTransport { get; set; } = false;
// ✅ 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
// ✅ NUEVO: Propiedades para la física personalizada
public float BaseInverseMass { get; private set; }
public Vector3 InitialPosition3D { get; private set; }
private List<Action> _deferredActions;
@ -56,8 +55,6 @@ namespace CtrEditor.Simulacion
// ✅ 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
@ -121,9 +118,16 @@ namespace CtrEditor.Simulacion
_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);
var cylinder = new Cylinder(Radius, Height);
var inertia = cylinder.ComputeInertia(_mass);
// Modificamos el tensor de inercia para evitar que la botella vuelque.
var inverseInertia = inertia.InverseInertiaTensor;
inverseInertia.XX = 0f; // Resistencia infinita a la rotación en el eje X (vuelco).
inverseInertia.YY = 0f; // Resistencia infinita a la rotación en el eje Y (vuelco).
// La resistencia en ZZ (eje Z) se mantiene, permitiendo que la botella gire sobre su base.
inertia.InverseInertiaTensor = inverseInertia;
_simulation.Bodies.SetLocalInertia(BodyHandle, inertia);
}
}
@ -133,324 +137,41 @@ namespace CtrEditor.Simulacion
{
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 cylinder = new Cylinder(Radius, Height);
var shapeIndex = _simulation.Shapes.Add(cylinder);
// 1. Creamos el cuerpo con una inercia por defecto.
var defaultInertia = cylinder.ComputeInertia(_mass);
var activityDescription = new BodyActivityDescription(-1f);
var bodyDescription = BodyDescription.CreateDynamic(
new RigidPose(position),
new BodyVelocity(),
inertia, // Inercia estándar de esfera
new CollidableDescription(shapeIndex, 0.001f),
defaultInertia,
new CollidableDescription(shapeIndex, 0),
activityDescription
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true; // Marcar que hemos creado un cuerpo
_bodyCreated = true;
// 2. Inmediatamente después de crearlo, aplicamos la inercia personalizada.
// Esto asegura que SetLocalInertia se llame para todos los objetos nuevos.
var inertia = cylinder.ComputeInertia(_mass);
var inverseInertia = inertia.InverseInertiaTensor;
inverseInertia.XX = 0f;
inverseInertia.YY = 0f;
inertia.InverseInertiaTensor = inverseInertia;
_simulation.Bodies.SetLocalInertia(BodyHandle, inertia);
// ✅ NUEVO: Almacenar la masa inversa base después de crear el cuerpo
// Esto nos sirve como referencia para la lógica de masa dinámica.
var bodyReference = _simulation.Bodies.GetBodyReference(BodyHandle);
this.BaseInverseMass = bodyReference.LocalInertia.InverseMass;
// ✅ CORREGIDO: Usar el diccionario del SimulationManager y la clave .Value del handle.
if (_simulationManager != null)
_simulationManager.CollidableData[BodyHandle.Value] = this;
}
/// <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.SpeedConversionFactor);
return;
}
// ✅ CREAR NUEVO MOTOR SI ES NECESARIO
if (needsNewMotor && target != null)
{
CreateMotorForTarget(target, direction, speed / simBase.SpeedConversionFactor);
}
}
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), 0f)
};
// ✅ 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;
@ -459,8 +180,8 @@ namespace CtrEditor.Simulacion
// ✅ 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);
var cylinder = new Cylinder(Radius, Height);
var shapeIndex = _simulation.Shapes.Add(cylinder);
ChangeBodyShape(shapeIndex);
}
}
@ -472,8 +193,8 @@ namespace CtrEditor.Simulacion
// ✅ 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);
var cylinder = new Cylinder(Radius, Height);
var shapeIndex = _simulation.Shapes.Add(cylinder);
ChangeBodyShape(shapeIndex);
}
}
@ -499,8 +220,8 @@ namespace CtrEditor.Simulacion
// 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);
// ✅ CORREGIDO: Calificar explícitamente para evitar ambigüedad de tipos.
var correctedOrientation = System.Numerics.Quaternion.CreateFromAxisAngle(Vector3.UnitZ, rotationZ);
// Aplicar la orientación corregida
bodyReference.Pose.Orientation = correctedOrientation;

View File

@ -32,9 +32,6 @@ namespace CtrEditor.Simulacion
// ✅ 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: Almacenar triángulos creados para acceso directo
private Triangle[] _storedTriangles;
@ -66,11 +63,37 @@ namespace CtrEditor.Simulacion
Create(innerRadius, outerRadius, startAngle, endAngle, topLeft, 0);
}
// ✅ SIMPLIFICADO: Configurar velocidad angular para AngularAxisMotor
// ✅ MODIFICADO: Actualizar la velocidad y luego la del cuerpo cinemático
public void SetSpeed(float speed)
{
Speed = speed; // Velocidad angular directa (sin inversión)
OnSpeedChanged?.Invoke(this);
Speed = speed;
ActualizarVelocidadCinematica();
}
/// <summary>
/// ✅ NUEVO: Aplica la velocidad angular al cuerpo cinemático de BEPU.
/// </summary>
public void ActualizarVelocidadCinematica()
{
if (_simulation != null && this.BodyHandle.Value >= 0 && _simulation.Bodies.BodyExists(this.BodyHandle))
{
var body = _simulation.Bodies.GetBodyReference(BodyHandle);
float effectiveRadius = (_innerRadius + _outerRadius) / 2.0f;
if (effectiveRadius > 0.001f)
{
// La velocidad tangencial (Speed) se convierte a velocidad angular (ω = v / r)
// ✅ CORREGIDO: Convertir velocidad de m/min a m/s para la simulación
float angularSpeed = (Speed / SpeedConversionFactor) / effectiveRadius;
// La rotación es alrededor del eje Z en el sistema de coordenadas de BEPU
body.Velocity.Angular = new Vector3(0, 0, angularSpeed);
}
else
{
body.Velocity.Angular = Vector3.Zero;
}
}
}
/// <summary>
@ -220,19 +243,8 @@ namespace CtrEditor.Simulacion
}
}
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;
@ -242,7 +254,7 @@ namespace CtrEditor.Simulacion
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
// ✅ CORREGIDO: Como Aether - startAngle y endAngle definen directamente el sector
// No hay rotación separada del objeto
// Actualizar parámetros internos
@ -253,53 +265,45 @@ namespace CtrEditor.Simulacion
_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
Create(_curveCenter); // Llamar a la sobrecarga privada
}
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)
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");
System.Diagnostics.Debug.WriteLine($"[simCurve] No se crearon triángulos, no se creará el cuerpo.");
return;
}
}
_storedTriangles = triangles;
// ✅ 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
// ✅ CORREGIDO: Convertir el array de triángulos a un Buffer<Triangle> de BEPU.
_simulation.BufferPool.Take(triangles.Length, out BepuUtilities.Memory.Buffer<Triangle> triangleBuffer);
for (int i = 0; i < triangles.Length; i++)
{
triangleBuffer[i] = triangles[i];
}
var mesh = new Mesh(triangleBuffer, Vector3.One, _simulation.BufferPool);
var shapeIndex = _simulation.Shapes.Add(mesh);
var bodyDescription = BodyDescription.CreateKinematic(
new RigidPose(position, Quaternion.Identity), // La rotación se maneja con la velocidad angular, no con la pose inicial
new CollidableDescription(shapeIndex, 0.1f),
new BodyActivityDescription(-1f) // ✅ CORREGIDO: -1f para que el cuerpo cinemático NUNCA duerma y no se mueva por su cuenta.
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);
_bodyCreated = true;
// ✅ NUEVO: Establecer la velocidad cinemática inicial al crear el cuerpo.
ActualizarVelocidadCinematica();
}
/// <summary>
/// ✅ SIMPLIFICADO: Crea triángulos usando Triangle de BEPU directamente
@ -340,11 +344,5 @@ namespace CtrEditor.Simulacion
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
}
}

View File

@ -26,11 +26,6 @@ namespace CtrEditor.Simulacion
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;
@ -66,11 +61,8 @@ namespace CtrEditor.Simulacion
{
base.SetRotation(wpfAngle);
// ✅ CRÍTICO: Actualizar propiedades cacheadas después del cambio de rotación
// ✅ CRÍTICO: Actualizar propiedades cacheadas y velocidad 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)
@ -108,14 +100,11 @@ namespace CtrEditor.Simulacion
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
// ✅ CRÍTICO: Actualizar propiedades cacheadas y velocidad 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
// ✅ NUEVO MÉTODO - actualizar propiedades cacheadas y velocidad cinemática
internal void UpdateCachedProperties()
{
// ✅ CORREGIDO: La dirección siempre es UnitX rotado por el ángulo del transporte
@ -148,15 +137,29 @@ namespace CtrEditor.Simulacion
// 🔍 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()}");
}
// ✅ NUEVO: Actualizar la velocidad del cuerpo cinemático siempre que cambien las propiedades
ActualizarVelocidadCinematica();
}
// ✅ MODIFICAR MÉTODO EXISTENTE - disparar evento
// ✅ MODIFICADO: Actualizar la velocidad y luego la del cuerpo cinemático
public void SetSpeed(float speed)
{
Speed = speed;
UpdateCachedProperties();
// Disparar evento para actualizar motores activos
OnSpeedChanged?.Invoke(this);
ActualizarVelocidadCinematica();
}
/// <summary>
/// ✅ NUEVO: Aplica la velocidad al cuerpo cinemático de BEPU.
/// </summary>
public void ActualizarVelocidadCinematica()
{
if (_simulation != null && this.BodyHandle.Value >= 0 && _simulation.Bodies.BodyExists(this.BodyHandle))
{
var body = _simulation.Bodies.GetBodyReference(BodyHandle);
// ✅ CORREGIDO: Convertir velocidad de m/min a m/s para la simulación
body.Velocity.Linear = this.DirectionVector * (this.Speed / SpeedConversionFactor);
}
}
/// <summary>
@ -190,9 +193,6 @@ namespace CtrEditor.Simulacion
// ✅ 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)
@ -205,20 +205,10 @@ namespace CtrEditor.Simulacion
}
/// <summary>
/// ✅ SIMPLIFICADO: RemoverBody que limpia restricciones y elimina el body
/// ✅ SIMPLIFICADO: RemoverBody ya no necesita limpiar restricciones de botellas.
/// </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();
}
@ -233,7 +223,7 @@ namespace CtrEditor.Simulacion
var bodyDescription = BodyDescription.CreateKinematic(
new RigidPose(bepuPosition, CoordinateConverter.CreateBepuQuaternionFromWpfAngle(wpfAngle)),
new CollidableDescription(shapeIndex, 0.1f),
new BodyActivityDescription(0.01f)
new BodyActivityDescription(-1f)
);
BodyHandle = _simulation.Bodies.Add(bodyDescription);