using Sandbox.Physics;
namespace Editor;
///
/// Simulate rigid bodies in editor
///
[EditorTool( "tools.physics-tool" )]
[Title( "Physics Tool" )]
[Icon( "panorama_fish_eye" )]
[Alias( "physics" )]
[Group( "1" )]
public class PhysicsEditorTool : EditorTool
{
public bool IsSimulating { get; private set; }
public int RigidBodyCount => RigidBodies.Count;
private HashSet RigidBodies = new();
private Rigidbody GrabbedBody;
private float GrabbedDistance;
private Vector3 LocalOffset;
private PhysicsBody MouseBody;
private Sandbox.Physics.FixedJoint MouseJoint;
private PhysicsWidgetWindow Overlay;
public override void OnEnabled()
{
base.OnEnabled();
Scene.EnableEditorPhysics( true );
IsSimulating = false;
AllowGameObjectSelection = true;
Selection.OnItemAdded += OnItemAdded;
Selection.OnItemRemoved += OnItemRemoved;
foreach ( var item in Selection )
{
OnItemAdded( item );
}
Overlay = new PhysicsWidgetWindow( this );
AddOverlay( Overlay, TextFlag.RightBottom, 10 );
MouseBody = new PhysicsBody( Scene.PhysicsWorld )
{
BodyType = PhysicsBodyType.Keyframed
};
}
public override void OnDisabled()
{
base.OnDisabled();
Scene.EnableEditorPhysics( false );
IsSimulating = false;
Selection.OnItemAdded -= OnItemAdded;
Selection.OnItemRemoved -= OnItemRemoved;
Scene.DisableEditorRigidBodies();
RigidBodies.Clear();
if ( MouseJoint.IsValid() )
MouseJoint.Remove();
if ( MouseBody.IsValid() )
MouseBody.Remove();
MouseBody = null;
MouseJoint = null;
SceneOverlay.Parent.Cursor = CursorShape.Arrow;
}
public override void OnUpdate()
{
base.OnUpdate();
var aimTransform = Gizmo.CameraTransform;
var hovered = false;
var tr = Trace.Run();
if ( RigidBodies.Contains( tr.Component ) )
hovered = true;
SceneOverlay.Parent.Cursor = GrabbedBody.IsValid() ?
CursorShape.ClosedHand : hovered ? CursorShape.OpenHand : CursorShape.Arrow;
if ( GrabbedBody.IsValid() )
{
if ( !Gizmo.IsLeftMouseDown )
{
if ( MouseJoint.IsValid() )
MouseJoint.Remove();
MouseJoint = null;
GrabbedBody = null;
return;
}
var plane = new Plane( aimTransform.Forward, GrabbedDistance );
if ( plane.TryTrace( Gizmo.CurrentRay, out var hitPoint, true ) )
{
MouseBody.Position = hitPoint;
}
return;
}
if ( !Gizmo.IsLeftMouseDown )
return;
if ( hovered )
{
StartSimulation();
GrabbedBody = tr.Component as Rigidbody;
LocalOffset = tr.Body.Transform.PointToLocal( tr.HitPosition );
GrabbedDistance = tr.HitPosition.Dot( aimTransform.Forward );
MouseBody.Position = tr.HitPosition;
MouseJoint = PhysicsJoint.CreateFixed( new PhysicsPoint( MouseBody ), new PhysicsPoint( tr.Body ) );
MouseJoint.Point1 = new PhysicsPoint( MouseBody );
MouseJoint.Point2 = new PhysicsPoint( tr.Body, LocalOffset );
var maxForce = 100.0f * tr.Body.Mass * Scene.PhysicsWorld.Gravity.Length;
MouseJoint.SpringLinear = new PhysicsSpring( 15, 1, maxForce );
MouseJoint.SpringAngular = new PhysicsSpring( 0, 0, 0 );
}
}
private void UpdateSelection()
{
RigidBodies = Selection.OfType()
.SelectMany( x => x.Components.GetAll( FindMode.EnabledInSelfAndDescendants | FindMode.EnabledInSelfAndChildren ) )
.Where( x => x.IsValid() )
.ToHashSet();
Overlay?.UpdateButton();
StopSimulation();
}
private void OnItemAdded( object e )
{
if ( Manager.CurrentTool != this )
return;
UpdateSelection();
}
private void OnItemRemoved( object e )
{
if ( Manager.CurrentTool != this )
return;
UpdateSelection();
}
public void StartSimulation()
{
if ( IsSimulating )
return;
IsSimulating = true;
foreach ( var rb in RigidBodies )
{
Scene.EnableEditorRigidBody( rb, true );
}
Overlay.UpdateButton();
}
public void StopSimulation()
{
if ( !IsSimulating )
return;
IsSimulating = false;
Scene.DisableEditorRigidBodies();
Overlay.UpdateButton();
}
public void ToggleSimulation()
{
if ( Manager.CurrentTool != this )
return;
if ( IsSimulating )
{
StopSimulation();
}
else
{
StartSimulation();
}
}
private class PhysicsWidgetWindow : WidgetWindow
{
private readonly PhysicsEditorTool Tool;
private readonly Button Button;
public PhysicsWidgetWindow( PhysicsEditorTool tool ) : base( tool.SceneOverlay, "Physics Tool" )
{
Tool = tool;
Layout = Layout.Row();
Layout.Margin = 8;
FixedWidth = 200.0f;
Button = new Button( "Simulate" )
{
Enabled = false,
Clicked = () => Tool.ToggleSimulation()
};
UpdateButton();
var buttonRow = Layout.AddRow();
buttonRow.Spacing = 2;
buttonRow.Add( Button );
}
public void UpdateButton()
{
if ( !Button.IsValid() )
return;
var count = Tool.RigidBodyCount;
if ( Tool.IsSimulating )
{
Button.Text = "Stop Simulation";
Button.Enabled = true;
}
else
{
Button.Text = count > 0 ? $"Simulate {count} Objects" : "Simulate";
Button.Enabled = count > 0;
}
}
[Shortcut( "tools.physics-toggle", "Space", typeof( SceneViewportWidget ) )]
public void ToggleSimulation()
{
Tool.ToggleSimulation();
}
}
}