554 lines
23 KiB
C#
554 lines
23 KiB
C#
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();
|
|
}
|
|
|
|
}
|
|
|
|
}
|