UI/ShipSelect/ShipPortrait3d.cs
using Sandbox.UI;

/// <summary>
/// Renders a 3D preview of a ship model inside a ScenePanel using its built-in RenderScene.
/// Pass a prefab path via PrefabPath; the model is extracted automatically.
/// </summary>
public sealed class ShipPortrait3d : Panel
{
	private ScenePanel _scenePanel;
	private GameObject _cameraGo;
	private CameraComponent _camera;
	private GameObject _modelGo;

	private string _prefabPath;
	private string _materialSkin;

	public string PrefabPath
	{
		get => _prefabPath;
		set
		{
			if ( _prefabPath == value ) return;
			_prefabPath = value;
			LoadModelFromPrefab( value );
		}
	}

	public string MaterialSkin
	{
		get => _materialSkin;
		set { _materialSkin = value; UpdateSkin(); }
	}

	// 45° above-and-behind: equal parts backward and up
	private static readonly Vector3 CamDir = (Vector3.Backward + Vector3.Up).Normal;

	public ShipPortrait3d()
	{
		_scenePanel = new ScenePanel();
		var scene = _scenePanel.RenderScene;

		// Camera — 45° elevated, looking down at origin
		_cameraGo = scene.CreateObject();
		_camera = _cameraGo.Components.Create<CameraComponent>();
		_camera.FieldOfView = 45f;
		_camera.ZNear = 1f;
		_camera.ZFar = 10000f;
		_camera.BackgroundColor = Color.Transparent;
		_cameraGo.WorldPosition = CamDir * 250f;
		_cameraGo.WorldRotation = Rotation.LookAt( -CamDir );

		// Three-point lighting
		AddLight( scene, Vector3.Up * 100f + Vector3.Right * 80f, Color.White * 3f, 500f );
		AddLight( scene, Vector3.Up * 100f + Vector3.Left * 80f + Vector3.Forward * 40f, new Color( 0.4f, 0.6f, 1f ) * 1.5f, 500f );
		AddLight( scene, Vector3.Down * 50f, new Color( 1f, 0.8f, 0.5f ), 300f );

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

	private static void AddLight( Scene scene, Vector3 position, Color color, float radius )
	{
		var go = scene.CreateObject();
		go.WorldPosition = position;
		var light = go.Components.Create<PointLight>();
		light.LightColor = color;
		light.Radius = radius;
	}

	private void LoadModelFromPrefab( string prefabPath )
	{
		_modelGo?.Destroy();
		_modelGo = null;

		if ( string.IsNullOrEmpty( prefabPath ) ) return;

		var prefabFile = ResourceLibrary.Get<PrefabFile>( prefabPath );
		if ( prefabFile == null ) return;

		// Briefly clone into the game scene to read the model, then discard
		var tempGo = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
		if ( tempGo == null ) return;

		var mr = tempGo.Components.Get<ModelRenderer>( FindMode.EverythingInSelfAndDescendants );
		var model = mr?.Model;
		tempGo.Destroy();

		if ( model == null ) return;

		_modelGo = _scenePanel.RenderScene.CreateObject();
		_modelGo.WorldScale = Vector3.One * 0.5f;
		var renderer = _modelGo.Components.Create<ModelRenderer>();
		renderer.Model = model;
		renderer.SceneObject?.Attributes.Set( "damagefade", 0f );

		UpdateSkin();

		// Frame camera to fit model, keeping 45° angle
		var size = renderer.Bounds.Size.Length;
		_cameraGo.WorldPosition = CamDir * MathF.Max( size * 1.6f, 80f );
		_cameraGo.WorldRotation = Rotation.LookAt( -CamDir );
	}

	private void UpdateSkin()
	{
		if ( _modelGo == null ) return;
		var renderer = _modelGo.Components.Get<ModelRenderer>();
		if ( renderer == null ) return;
		renderer.MaterialOverride = !string.IsNullOrEmpty( _materialSkin )
			? Material.Load( _materialSkin )
			: null;
	}

	public override void Tick()
	{
		base.Tick();
		if ( _modelGo == null ) return;
		_modelGo.WorldRotation *= Rotation.FromYaw( 0.5f );
	}
}