UI/LootIcon.cs

UI panel that renders a LootPrefab's 3D model into a ScenePanel for use as a live loot icon. It builds a small SceneWorld with lights and a SceneObject using the prefab's model, IconOffset and IconAngles, and configures the ScenePanel camera and layout.

using System;
using Sandbox;
using Sandbox.UI;

namespace BrickJam.UI;

/// <summary>
/// Renders a loot prefab's 3D model live in the UI. The legacy pre-rendered icon texture relied on
/// <c>Graphics.RenderToTexture( SceneCamera, Texture )</c>, which is now a no-op stub in the engine (it just
/// returns false), so loot icons rendered blank. Instead we build a tiny <see cref="SceneWorld"/> with the
/// model + lights and show it through a <see cref="ScenePanel"/> - the same proven approach as
/// <c>Lockpicker.razor</c>. The model is framed using the prefab's authored <c>IconOffset</c>/<c>IconAngles</c>.
/// </summary>
public class LootIcon : Panel
{
	private LootPrefab prefab;
	private ScenePanel scenePanel;
	private SceneWorld world;

	public LootPrefab Prefab
	{
		get => prefab;
		set
		{
			if ( prefab == value )
				return;

			prefab = value;
			Rebuild();
		}
	}

	private void Rebuild()
	{
		Teardown();

		if ( prefab is null || string.IsNullOrWhiteSpace( prefab.Model ) )
			return;

		var model = Model.Load( prefab.Model );
		if ( model is null || model.IsError )
			return;

		world = new SceneWorld { AmbientLightColor = Color.White * 0.35f };
		_ = new ScenePointLight( world, Vector3.Up * 15f + Vector3.Backward * 5f, 200f, Color.White );
		_ = new ScenePointLight( world, Vector3.Up * 25f + Vector3.Forward * 10f, 200f, Color.White );
		_ = new ScenePointLight( world, Vector3.Backward * 5f + Vector3.Down * 35f, 200f, Color.White * 0.5f );
		_ = new SceneObject( world, model, new Transform( prefab.IconOffset, prefab.IconAngles.ToRotation() ) );

		scenePanel = AddChild<ScenePanel>( "scene" );
		scenePanel.World = world;
		scenePanel.Camera.Position = Vector3.Backward * 10f;
		scenePanel.Camera.Rotation = Rotation.Identity;
		scenePanel.Camera.FieldOfView = 50f;
		scenePanel.Camera.ZNear = 5f;
		scenePanel.Camera.ZFar = 1000f;
		scenePanel.Camera.BackgroundColor = Color.Transparent;

		// Fill the host cell exactly (the parent .item clips with overflow:hidden) so the live render can't
		// spill across the grid.
		scenePanel.Style.Position = PositionMode.Absolute;
		scenePanel.Style.Left = Length.Percent( 0 );
		scenePanel.Style.Top = Length.Percent( 0 );
		scenePanel.Style.Width = Length.Percent( 100 );
		scenePanel.Style.Height = Length.Percent( 100 );
	}

	private void Teardown()
	{
		scenePanel?.Delete();
		scenePanel = null;
		world?.Delete();
		world = null;
	}

	public override void OnDeleted()
	{
		base.OnDeleted();
		Teardown();
	}
}