UI/Story/ModelPanelInspector.cs
using Opium;
using Sandbox.UI;
using Sandbox.UI.Construct;

public class ModelPanelInspector : Panel
{
	public string Model { get; set; }
	public string MaterialSkin { get; set; }

	private SceneWorld World;
	private ScenePanel ScenePanel;
	private SceneModel SceneModel;
	private Vector3 camoffset;
	protected override void OnParametersSet()
	{
		base.OnParametersSet();

		Rebuild();
	}

	public void Rebuild()
	{
		ScenePanel?.Delete( true );
		ScenePanel = null;

		World?.Delete();
		World = null;

		targetRotation = Rotation.From( 0, 0, 0 );

		if ( string.IsNullOrWhiteSpace( Model ) )
			return;

		World = new();

		SceneModel = new SceneModel( World, Model, Transform.Zero );
		if ( !string.IsNullOrWhiteSpace( MaterialSkin ) )
			SceneModel.SetMaterialOverride( Material.Load( MaterialSkin ) );
		SceneModel.Position = Vector3.Zero;
		SceneModel.Attributes.SetCombo( "D_VERTEX_SNAP", GameSettingsSystem.Current.EnableVertexSnapping );

		camoffset = SceneModel.Bounds.Center + Vector3.Forward * 158;
		var camrot = Rotation.LookAt( camoffset + Vector3.Forward * 10.5f );
		var campos = GetFocusPosition( SceneModel.Model.Bounds, camrot, 75 );

		ScenePanel = Add.ScenePanel( World, campos, camrot, 75 );

		new SceneLight( World, new Vector3( -100, 100, 18 ), 300, Color.White * 15 );
		new SceneLight( World, new Vector3( 70, -30, 18 ), 300, Color.White * 15 );
		new SceneLight( World, new Vector3( -70, -250, 17 ), 300, Color.White * 15 );

		ScenePanel.Style.Width = Length.Percent( 100 );
		ScenePanel.Style.Height = Length.Percent( 100 );
	}

	Vector3 panOffset;
	Rotation targetRotation;
	Vector3 constraintBoxSize = new Vector3( 0, 20, 6 );
	public override void Tick()
	{
		base.Tick();

		if ( SceneModel == null )
		{
			targetRotation = Rotation.From( 0, 0, 0 );
			return;
		}

		var mouseDelta = Input.Down( "attack1" ) ? Input.MouseDelta : 0;

		DoRotation( mouseDelta );
		SceneModel.Rotation = Rotation.Lerp( SceneModel.Rotation, targetRotation, 10 * Time.Delta );

		var pos = SceneModel.Bounds.Center;
		pos -= SceneModel.Bounds.Center;

		SceneModel.Position = pos + panOffset;

		if(Input.Down("attack2"))
		{
			var boundsSize = SceneModel.Bounds.Size;
			var scale = Math.Max( boundsSize.x, Math.Max( boundsSize.y, boundsSize.z ) ) * 0.01f;
			scale = Math.Max( scale, 0.1f );

			var pan = Input.MouseDelta * scale;
			panOffset += new Vector3( 0, -pan.x, -pan.y );

			var totalClampSize = constraintBoxSize + boundsSize;

			var clampBoundsY = totalClampSize.y * 0.85f;
			var clampBoundsZ = totalClampSize.z * 0.35f;

			panOffset.y = Math.Clamp( panOffset.y, -clampBoundsY, clampBoundsY);
			panOffset.z = Math.Clamp( panOffset.z, -clampBoundsZ, clampBoundsZ );

			var targetPos = SceneModel.Bounds.Center + panOffset;
			SceneModel.Position = Vector3.Lerp( SceneModel.Position, targetPos - SceneModel.Bounds.Center, 10 * Time.Delta );
		}
	}

	public void DoRotation(Vector3 input )
	{
		var localRot = Rotation.From( new Angles( 0, SceneModel.Rotation.y, 0 ) );
		localRot *= Rotation.FromAxis( Vector3.Up, input.x * 0.125f );
		localRot *= Rotation.FromAxis( Vector3.Right, input.y * 0.125f );

		localRot = Rotation.From(new Angles( 0, SceneModel.Rotation.y, 0 ) ).Inverse * localRot; 

		targetRotation = localRot * targetRotation;
	}

	private Vector3 GetFocusPosition( BBox bounds, Rotation cameraRot, float fov )
	{
		var focusDist = 0.85f;
		var maxSize = new[] { bounds.Size.x, bounds.Size.y, bounds.Size.z }.Max();
		var cameraView = 2.0f * (float)Math.Tan( 0.5f * 0.017453292f * fov );
		var distance = focusDist * maxSize / cameraView;
		distance += 1.75f * maxSize;
		return bounds.Center - distance * cameraRot.Forward;
	}
}