Editor/PbrMaterialPreviewWidget.cs
using System;
using System.Collections.Generic;
using System.IO;
using Editor;
using Sandbox;
using Widget = Editor.Widget;

public sealed class PbrMaterialPreviewWidget : SceneRenderingWidget
{
	private GameObject previewObject;
	private ModelRenderer previewRenderer;
	private Model sphereModel;
	private Model planeModel;
	private Model activeModel;
	private Material previewMaterial;
	private Material fallbackMaterial;
	private readonly List<string> temporaryPreviewFiles = new();
	private readonly string previewSessionId = Guid.NewGuid().ToString( "N" );
	private string temporaryPreviewFolder;
	private PbrPreviewShape previewShape = PbrPreviewShape.Sphere;
	private Vector2 lastCursorPosition;
	private Vector2 orbitAngles = new( 135f, 28f );
	private Vector3 cameraOrigin = Vector3.Zero;
	private float cameraDistance = 150f;
	private float actualCameraDistance = 150f;
	private float modelRotation;
	private bool orbitControl;
	private bool zoomControl;
	private bool panControl;
	private bool hasGeneratedMaterial;
	private bool previewPipelineEnabled;
	private bool parallaxEnabled;
	private string previewWarning;

	public Action<string> WarningLogged { get; set; }

	private const string PreviewMaterialFileName = "preview_pbr.vmat";
	private const string PreviewParallaxHeightScale = "0.020";

	private static Vector2 CursorPosition => Editor.Application.UnscaledCursorPosition;

	public PbrMaterialPreviewWidget( Widget parent ) : base( parent )
	{
		MouseTracking = true;
		FocusMode = FocusMode.Click;
		MinimumSize = new Vector2( 480, 220 );
		SetStyles( "background-color: #101215; border: 1px solid #2a3038; border-radius: 6px;" );

		ResetView();
	}

	public void SetPreviewEnabled( bool enabled )
	{
		if ( previewPipelineEnabled == enabled )
			return;

		previewPipelineEnabled = enabled;

		if ( previewPipelineEnabled )
		{
			previewWarning = null;
			EnsurePreviewScene();
			ApplyMaterialToRenderer();
			Update();
			return;
		}

		hasGeneratedMaterial = false;
		previewWarning = null;
		orbitControl = false;
		zoomControl = false;
		panControl = false;
		DisposePreviewResources();
		DestroyPreviewScene();
		Update();
	}

	public void SetParallaxEnabled( bool enabled )
	{
		parallaxEnabled = enabled;
	}

	public void SetResult( PbrGeneratorResult result )
	{
		if ( !previewPipelineEnabled )
		{
			hasGeneratedMaterial = false;
			previewWarning = null;
			Update();
			return;
		}

		EnsurePreviewScene();

		hasGeneratedMaterial = false;
		previewWarning = null;

		if ( result == null || result.Albedo == null || !result.Albedo.IsValid )
		{
			DisposePreviewResources();
			ApplyMaterialToRenderer();
			Update();
			return;
		}

		var material = CreateTemporaryPreviewMaterial( result );

		if ( material == null || !material.IsValid )
		{
			previewMaterial = null;
			previewWarning = "Could not load the generated preview material; showing fallback material.";
			LogPreviewWarning( previewWarning );
			DisposePreviewResources();
			ApplyMaterialToRenderer();
			Update();
			return;
		}

		previewMaterial = material;
		hasGeneratedMaterial = true;
		ApplyMaterialToRenderer();
		Update();
	}

	public void SetPreviewShape( PbrPreviewShape shape )
	{
		var shapeChanged = previewShape != shape;
		previewShape = shape;

		if ( !previewPipelineEnabled )
			return;

		EnsurePreviewScene();

		var needsModelRefresh = shapeChanged
			|| activeModel == null
			|| !activeModel.IsValid
			|| previewRenderer == null
			|| previewRenderer.Model == null
			|| !previewRenderer.Model.IsValid;

		if ( !needsModelRefresh )
		{
			UpdatePreviewTransform();
			ApplyMaterialToRenderer();
			Update();
			return;
		}

		activeModel = GetModelForShape( previewShape );

		if ( previewRenderer != null )
			previewRenderer.Model = activeModel;

		ResetViewDistance();
		UpdatePreviewTransform();
		ApplyMaterialToRenderer();
		Update();
	}

	public void ResetView()
	{
		orbitAngles = new Vector2( 135f, 28f );
		cameraOrigin = Vector3.Zero;
		modelRotation = 0f;
		ResetViewDistance();
		actualCameraDistance = cameraDistance;
		UpdatePreviewTransform();
		UpdateCameraPosition();
		Update();
	}

	public override void OnDestroyed()
	{
		DisposePreviewResources();
		DestroyPreviewScene();
		base.OnDestroyed();
	}

	protected override void PreFrame()
	{
		if ( !previewPipelineEnabled || Scene == null )
			return;

		using ( Scene.Push() )
		{
			Scene.EditorTick( RealTime.Now, RealTime.Delta );
			UpdateInputControls();
			UpdatePreviewTransform();
			UpdateCameraPosition();
		}

		Update();
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		if ( !previewPipelineEnabled )
			return;

		if ( e.LeftMouseButton )
		{
			orbitControl = true;
			lastCursorPosition = CursorPosition;
			e.Accepted = true;
			return;
		}

		if ( e.RightMouseButton )
		{
			zoomControl = true;
			lastCursorPosition = CursorPosition;
			e.Accepted = true;
			return;
		}

		if ( e.MiddleMouseButton )
		{
			panControl = true;
			lastCursorPosition = CursorPosition;
			e.Accepted = true;
		}
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		base.OnMouseReleased( e );

		if ( !previewPipelineEnabled )
			return;

		if ( e.LeftMouseButton )
		{
			orbitControl = false;
			e.Accepted = true;
		}

		if ( e.RightMouseButton )
		{
			zoomControl = false;
			e.Accepted = true;
		}

		if ( e.MiddleMouseButton )
		{
			panControl = false;
			e.Accepted = true;
		}
	}

	protected override void OnMouseWheel( WheelEvent e )
	{
		base.OnMouseWheel( e );

		if ( !previewPipelineEnabled )
			return;

		Zoom( e.Delta * -0.12f );
		e.Accept();
		Update();
	}

	protected override void OnPaint()
	{
		if ( !previewPipelineEnabled )
		{
			var disabledRect = LocalRect;
			Paint.ClearPen();
			Paint.SetBrush( Theme.WindowBackground );
			Paint.DrawRect( disabledRect, 4 );
			return;
		}

		base.OnPaint();

		if ( hasGeneratedMaterial && string.IsNullOrWhiteSpace( previewWarning ) )
			return;

		var rect = LocalRect.Shrink( 12 );
		Paint.SetDefaultFont( 12, 500, false, false );
		Paint.SetPen( string.IsNullOrWhiteSpace( previewWarning ) ? Theme.TextDisabled : Theme.Yellow );
		Paint.DrawText( rect, string.IsNullOrWhiteSpace( previewWarning ) ? "Generate maps to preview the material" : previewWarning, TextFlag.Center );
	}

	private void EnsurePreviewScene()
	{
		if ( Scene != null && previewObject.IsValid() && previewRenderer != null )
			return;

		CleanStalePreviewFolders();
		DestroyPreviewScene();

		Scene = Scene.CreateEditorScene();
		Scene.Name = "Seam-Less PBR Preview";

		using ( Scene.Push() )
		{
			CreateCameraAndLighting();
			sphereModel = CreateTessellatedSphereModel();
			planeModel = CreatePlaneModel();
			fallbackMaterial = Material.Load( "materials/dev/gray_grid_8.vmat" );
			CreatePreviewObject();
		}

		ResetView();
	}

	private void DestroyPreviewScene()
	{
		DestroyPreviewObject();
		sphereModel = null;
		planeModel = null;
		activeModel = null;
		fallbackMaterial = null;
		Camera = null;
		Scene?.Destroy();
		Scene = null;
	}

	private void CreateCameraAndLighting()
	{
		var cameraObject = new GameObject( true, "PBR Preview Camera" );
		var camera = cameraObject.AddComponent<CameraComponent>();
		camera.BackgroundColor = new Color( 0.075f, 0.082f, 0.09f );
		camera.WorldRotation = new Angles( 20f, 225f, 0f );
		camera.FieldOfView = 45f;
		camera.ZNear = 0.1f;
		camera.ZFar = 15000f;
		Camera = camera;

		var ambientObject = new GameObject( true, "PBR Preview Ambient" );
		var ambient = ambientObject.AddComponent<AmbientLight>();
		ambient.Color = Color.Cyan * 0.055f;

		var sunObject = new GameObject( true, "PBR Preview Sun" );
		var sun = sunObject.AddComponent<DirectionalLight>();
		sun.Shadows = true;
		sun.WorldRotation = new Angles( 50f, 45f, 0f );
		sun.LightColor = Color.White * 0.65f;

		var envmapObject = new GameObject( true, "PBR Preview Envmap" );
		var envmap = envmapObject.AddComponent<EnvmapProbe>();
		envmap.Mode = EnvmapProbe.EnvmapProbeMode.CustomTexture;
		envmap.Texture = Texture.Load( "textures/cubemaps/default2.vtex" );
		envmap.Bounds = BBox.FromPositionAndSize( Vector3.Zero, 100000f );
		envmap.WorldPosition = Vector3.Zero;
		envmap.TintColor = Color.White * 0.1f;
	}

	private void CreatePreviewObject()
	{
		DestroyPreviewObject();

		activeModel = GetModelForShape( previewShape );

		if ( activeModel == null || !activeModel.IsValid )
		{
			activeModel = Model.Sphere;
			previewWarning = "Preview model failed to load; using default sphere.";
			LogPreviewWarning( previewWarning );
		}

		previewObject = new GameObject( true, "PBR Preview Model" );
		previewObject.WorldTransform = Transform.Zero;
		previewRenderer = previewObject.AddComponent<ModelRenderer>();
		previewRenderer.Model = activeModel;
		previewRenderer.MaterialOverride = fallbackMaterial;
		UpdatePreviewTransform();
	}

	private void DestroyPreviewObject()
	{
		if ( previewObject.IsValid() )
			previewObject.Destroy();

		previewObject = null;
		previewRenderer = null;
	}

	private void UpdatePreviewTransform()
	{
		if ( !previewObject.IsValid() )
			return;

		previewObject.WorldPosition = Vector3.Zero;
		previewObject.WorldRotation = Rotation.FromYaw( modelRotation );
		previewObject.WorldScale = GetModelScale( previewShape );
	}

	private void ApplyMaterialToRenderer()
	{
		if ( previewRenderer == null )
			return;

		var material = hasGeneratedMaterial && previewMaterial != null && previewMaterial.IsValid
			? previewMaterial
			: fallbackMaterial;

		previewRenderer.MaterialOverride = material != null && material.IsValid ? material : null;
	}

	private Material CreateTemporaryPreviewMaterial( PbrGeneratorResult result )
	{
		try
		{
			temporaryPreviewFolder ??= Path.Combine( GetPreviewCacheRoot(), previewSessionId );
			Directory.CreateDirectory( temporaryPreviewFolder );
			temporaryPreviewFiles.Clear();

			var albedoPath = WriteTemporaryPreviewTexture( result.Albedo, "albedo", "materials/default/default_color.tga" );
			var normalPath = WriteTemporaryPreviewTexture( result.Normal, "normal", "materials/default/default_normal.tga" );
			var roughnessPath = WriteTemporaryPreviewTexture( result.Roughness, "roughness", "materials/default/default_rough.tga" );
			var aoPath = WriteTemporaryPreviewTexture( result.AmbientOcclusion, "ao", "materials/default/default_ao.tga" );
			var metallicPath = WriteTemporaryPreviewTexture( result.Metallic, "metallic", "" );
			var heightPath = parallaxEnabled ? WriteTemporaryPreviewTexture( result.Height, "height", "" ) : "";
			var materialPath = Path.Combine( temporaryPreviewFolder, PreviewMaterialFileName );
			var materialContent = BuildTemporaryVmat( albedoPath, normalPath, roughnessPath, aoPath, metallicPath, heightPath );
			var materialAsset = default( Editor.Asset );

			if ( ShouldWritePreviewMaterial( materialPath, materialContent ) )
			{
				if ( WritePreviewTextFile( materialPath, materialContent ) )
					materialAsset = RegisterAndCompilePreviewFile( materialPath );
			}

			temporaryPreviewFiles.Add( materialPath );

			var material = LoadPreviewMaterial( materialAsset, materialPath );

			if ( material != null && material.IsValid )
				return material;
		}
		catch ( Exception ex )
		{
			LogPreviewWarning( $"Temporary preview material failed: {ex.Message}" );
		}

		return null;
	}

	private bool ShouldWritePreviewMaterial( string path, string content )
	{
		if ( !File.Exists( path ) )
			return true;

		try
		{
			return !string.Equals( File.ReadAllText( path ), content, StringComparison.Ordinal );
		}
		catch
		{
			return true;
		}
	}

	private string WriteTemporaryPreviewTexture( Bitmap bitmap, string suffix, string fallback )
	{
		if ( bitmap == null || !bitmap.IsValid )
			return fallback;

		var path = Path.Combine( temporaryPreviewFolder, $"preview_{suffix}.png" );
		if ( !WritePreviewBinaryFile( path, bitmap.ToPng() ) && !File.Exists( path ) )
			return fallback;

		temporaryPreviewFiles.Add( path );
		var asset = RegisterAndCompilePreviewFile( path );

		if ( asset != null && !string.IsNullOrWhiteSpace( asset.RelativePath ) )
			return NormalizeMaterialPath( asset.RelativePath );

		return GetLibraryAssetReferencePath( path );
	}

	private bool WritePreviewTextFile( string path, string content )
	{
		var tempPath = GetPreviewTempWritePath( path );

		try
		{
			Directory.CreateDirectory( Path.GetDirectoryName( path ) );
			File.WriteAllText( tempPath, content );
			return MovePreviewTempFileIntoPlace( tempPath, path );
		}
		catch ( Exception ex )
		{
			LogPreviewWarning( $"Could not write preview file {Path.GetFileName( path )}: {ex.Message}" );
			DeletePreviewTempFile( tempPath );
			return false;
		}
	}

	private bool WritePreviewBinaryFile( string path, byte[] bytes )
	{
		var tempPath = GetPreviewTempWritePath( path );

		try
		{
			Directory.CreateDirectory( Path.GetDirectoryName( path ) );
			File.WriteAllBytes( tempPath, bytes );
			return MovePreviewTempFileIntoPlace( tempPath, path );
		}
		catch ( Exception ex )
		{
			LogPreviewWarning( $"Could not write preview texture {Path.GetFileName( path )}: {ex.Message}" );
			DeletePreviewTempFile( tempPath );
			return false;
		}
	}

	private string GetPreviewTempWritePath( string path )
	{
		var folder = Path.GetDirectoryName( path );
		var filename = Path.GetFileName( path );
		return Path.Combine( folder, $".{filename}.{Guid.NewGuid():N}.tmp" );
	}

	private bool MovePreviewTempFileIntoPlace( string tempPath, string path )
	{
		try
		{
			if ( File.Exists( path ) )
			{
				File.Replace( tempPath, path, null, true );
				return true;
			}

			File.Move( tempPath, path );
			return true;
		}
		catch ( Exception ex )
		{
			LogPreviewWarning( $"Could not swap preview file {Path.GetFileName( path )}: {ex.Message}" );
			DeletePreviewTempFile( tempPath );
			return false;
		}
	}

	private void DeletePreviewTempFile( string tempPath )
	{
		try
		{
			if ( !string.IsNullOrWhiteSpace( tempPath ) && File.Exists( tempPath ) )
				File.Delete( tempPath );
		}
		catch
		{
		}
	}

	private string BuildTemporaryVmat( string albedo, string normal, string roughness, string ao, string metallic, string height )
	{
		var metalnessLine = string.IsNullOrWhiteSpace( metallic )
			? "\tg_flMetalness \"0.000\"\n"
			: $"\tTextureMetalness \"{metallic}\"\n";
		var parallaxBlock = string.IsNullOrWhiteSpace( height )
			? ""
			: "\tF_PARALLAX_OCCLUSION 1\n" +
				$"\tg_flHeightMapScale \"{PreviewParallaxHeightScale}\"\n" +
				$"\tTextureHeight \"{height}\"\n";

		return
			"Layer0\n" +
			"{\n" +
			"\tshader \"shaders/complex.shader\"\n" +
			"\tF_SPECULAR 1\n" +
			parallaxBlock +
			"\tg_flAmbientOcclusionDirectDiffuse \"0.000\"\n" +
			"\tg_flAmbientOcclusionDirectSpecular \"0.000\"\n" +
			$"\tTextureAmbientOcclusion \"{ao}\"\n" +
			"\tg_flModelTintAmount \"1.000\"\n" +
			"\tg_vColorTint \"[1.000000 1.000000 1.000000 0.000000]\"\n" +
			$"\tTextureColor \"{albedo}\"\n" +
			"\tg_bFogEnabled \"0\"\n" +
			metalnessLine +
			$"\tTextureNormal \"{normal}\"\n" +
			"\tg_flRoughnessScaleFactor \"1.000\"\n" +
			$"\tTextureRoughness \"{roughness}\"\n" +
			"\tg_vTexCoordOffset \"[0.000 0.000]\"\n" +
			"\tg_vTexCoordScale \"[1.000 1.000]\"\n" +
			"\tg_vTexCoordScrollSpeed \"[0.000 0.000]\"\n" +
			"}\n";
	}

	private Editor.Asset RegisterAndCompilePreviewFile( string path )
	{
		try
		{
			var asset = AssetSystem.RegisterFile( path );

			if ( asset != null && asset.CanRecompile )
				asset.Compile( false );

			return asset;
		}
		catch ( Exception ex )
		{
			LogPreviewWarning( $"Preview asset registration failed for {Path.GetFileName( path )}: {ex.Message}" );
			return null;
		}
	}

	private Material LoadPreviewMaterial( Editor.Asset materialAsset, string materialPath )
	{
		var candidates = new List<string>();

		if ( materialAsset != null )
		{
			AddMaterialLoadCandidate( candidates, materialAsset.Path );
			AddMaterialLoadCandidate( candidates, materialAsset.RelativePath );
		}

		AddMaterialLoadCandidate( candidates, GetLibraryAssetReferencePath( materialPath ) );
		AddMaterialLoadCandidate( candidates, NormalizeMaterialPath( materialPath ) );

		foreach ( var candidate in candidates )
		{
			try
			{
				var material = Material.Load( candidate );

				if ( material != null && material.IsValid )
					return material;
			}
			catch ( Exception ex )
			{
				LogPreviewWarning( $"Preview material load failed for {candidate}: {ex.Message}" );
			}
		}

		return null;
	}

	private void AddMaterialLoadCandidate( List<string> candidates, string value )
	{
		if ( string.IsNullOrWhiteSpace( value ) )
			return;

		var normalized = NormalizeMaterialPath( value );

		foreach ( var candidate in candidates )
		{
			if ( string.Equals( candidate, normalized, StringComparison.OrdinalIgnoreCase ) )
				return;
		}

		candidates.Add( normalized );
	}

	private string GetPreviewCacheRoot()
	{
		return Path.Combine( GetLibraryFolder(), "Assets", "Preview", "Pbr" );
	}

	private string GetLibraryAssetsFolder()
	{
		return Path.Combine( GetLibraryFolder(), "Assets" );
	}

	private string GetLibraryFolder()
	{
		var projectRoot = Sandbox.Project.Current?.GetRootPath();

		if ( string.IsNullOrWhiteSpace( projectRoot ) )
			projectRoot = Environment.CurrentDirectory;

		var publishedFolder = Path.Combine( projectRoot, "Libraries", "forsomethings.seamless" );

		if ( Directory.Exists( publishedFolder ) )
			return publishedFolder;

		return Path.Combine( projectRoot, "Libraries", "SeamLess" );
	}

	private string GetLibraryAssetReferencePath( string path )
	{
		var fullPath = Path.GetFullPath( path );
		var fullAssetsPath = Path.GetFullPath( GetLibraryAssetsFolder() );
		var normalizedPath = NormalizeMaterialPath( fullPath );
		var normalizedAssetsPath = NormalizeMaterialPath( fullAssetsPath );

		if ( !normalizedAssetsPath.EndsWith( "/" ) )
			normalizedAssetsPath += "/";

		if ( normalizedPath.StartsWith( normalizedAssetsPath, StringComparison.OrdinalIgnoreCase ) )
			return normalizedPath.Substring( normalizedAssetsPath.Length );

		return normalizedPath;
	}

	private string NormalizeMaterialPath( string path )
	{
		return path.Replace( "\\", "/" ).Replace( "\"", "" );
	}

	private void DisposePreviewResources()
	{
		previewMaterial = null;
		ApplyMaterialToRenderer();
		ClearTemporaryPreviewFiles();
	}

	private void ClearTemporaryPreviewFiles()
	{
		DeletePreviewFiles( temporaryPreviewFiles );
		temporaryPreviewFiles.Clear();

		DeletePreviewFolder( temporaryPreviewFolder );
		temporaryPreviewFolder = null;
	}

	private void DeletePreviewFiles( IEnumerable<string> files )
	{
		foreach ( var file in files )
		{
			try
			{
				if ( File.Exists( file ) )
					File.Delete( file );
			}
			catch ( Exception ex )
			{
				LogPreviewWarning( $"Could not delete preview cache file {Path.GetFileName( file )}: {ex.Message}" );
			}
		}
	}

	private void DeletePreviewFolder( string folder )
	{
		try
		{
			if ( !string.IsNullOrWhiteSpace( folder ) && Directory.Exists( folder ) )
				Directory.Delete( folder, true );
		}
		catch ( Exception ex )
		{
			LogPreviewWarning( $"Could not delete preview cache folder {Path.GetFileName( folder )}: {ex.Message}" );
		}
	}

	private void CleanStalePreviewFolders()
	{
		var cacheRoot = GetPreviewCacheRoot();

		if ( !Directory.Exists( cacheRoot ) )
			return;

		foreach ( var folder in Directory.GetDirectories( cacheRoot ) )
		{
			var folderName = Path.GetFileName( folder );

			if ( string.Equals( folderName, previewSessionId, StringComparison.OrdinalIgnoreCase ) )
				continue;

			if ( !IsPreviewSessionFolderName( folderName ) )
				continue;

			DeletePreviewFolder( folder );
		}
	}

	private bool IsPreviewSessionFolderName( string folderName )
	{
		if ( string.IsNullOrWhiteSpace( folderName ) || folderName.Length != 32 )
			return false;

		foreach ( var character in folderName )
		{
			if ( !Uri.IsHexDigit( character ) )
				return false;
		}

		return true;
	}

	private void UpdateInputControls()
	{
		var cursorPosition = CursorPosition;
		var cursorDelta = cursorPosition - lastCursorPosition;

		if ( orbitControl )
		{
			if ( cursorDelta.Length > 0f )
			{
				orbitAngles.x += cursorDelta.x * 0.2f;
				orbitAngles.y += cursorDelta.y * 0.2f;
				orbitAngles.x = orbitAngles.x.NormalizeDegrees();
				orbitAngles.y = orbitAngles.y.Clamp( -85f, 85f );
				modelRotation -= cursorDelta.x * 0.08f;
			}

			Editor.Application.UnscaledCursorPosition = lastCursorPosition;
			Cursor = CursorShape.Blank;
			return;
		}

		if ( zoomControl )
		{
			if ( Math.Abs( cursorDelta.y ) > 0f )
				Zoom( cursorDelta.y * 0.22f );

			Editor.Application.UnscaledCursorPosition = lastCursorPosition;
			Cursor = CursorShape.Blank;
			return;
		}

		if ( panControl )
		{
			if ( cursorDelta.Length > 0f )
				Pan( cursorDelta );

			Editor.Application.UnscaledCursorPosition = lastCursorPosition;
			Cursor = CursorShape.Blank;
			return;
		}

		lastCursorPosition = cursorPosition;
		Cursor = CursorShape.None;
	}

	private void Pan( Vector2 cursorDelta )
	{
		if ( !Camera.IsValid() )
			return;

		var scale = Math.Max( 0.08f, cameraDistance * 0.0016f );
		cameraOrigin += Camera.WorldRotation.Right * cursorDelta.x * scale;
		cameraOrigin += Camera.WorldRotation.Down * cursorDelta.y * scale;
	}

	private void Zoom( float delta )
	{
		cameraDistance += delta;
		cameraDistance = cameraDistance.Clamp( 18f, 1200f );
	}

	private void ResetViewDistance()
	{
		var size = GetScaledModelSize().Length;
		cameraDistance = Math.Max( 80f, size * 1.75f );
		actualCameraDistance = cameraDistance;
	}

	private void UpdateCameraPosition()
	{
		if ( !Camera.IsValid() )
			return;

		Camera.WorldRotation = new Angles( orbitAngles.y, -orbitAngles.x, 0f );
		actualCameraDistance = actualCameraDistance.LerpTo( cameraDistance, RealTime.Delta * 14f );
		Camera.WorldPosition = cameraOrigin + GetScaledModelCenter() + Camera.WorldRotation.Backward * actualCameraDistance;
	}

	private Model GetModelForShape( PbrPreviewShape shape )
	{
		return shape switch
		{
			PbrPreviewShape.Cube => Model.Cube,
			PbrPreviewShape.Plane => planeModel,
			_ => sphereModel
		};
	}

	private Vector3 GetModelScale( PbrPreviewShape shape )
	{
		if ( shape == PbrPreviewShape.Cube )
		{
			var cubeSize = Model.Cube?.RenderBounds.Size.Length ?? 0f;
			var cubeScale = cubeSize > 0f ? 120f / cubeSize : 1f;
			return Vector3.One * cubeScale;
		}

		return Vector3.One;
	}

	private Vector3 GetScaledModelSize()
	{
		if ( activeModel == null || !activeModel.IsValid )
			return new Vector3( 64f, 64f, 64f );

		return MultiplyVectors( activeModel.RenderBounds.Size, GetModelScale( previewShape ) );
	}

	private Vector3 GetScaledModelCenter()
	{
		if ( activeModel == null || !activeModel.IsValid )
			return Vector3.Zero;

		return MultiplyVectors( activeModel.RenderBounds.Center, GetModelScale( previewShape ) );
	}

	private Vector3 MultiplyVectors( Vector3 a, Vector3 b )
	{
		return new Vector3( a.x * b.x, a.y * b.y, a.z * b.z );
	}

	private void LogPreviewWarning( string message )
	{
		if ( string.IsNullOrWhiteSpace( message ) )
			return;

		WarningLogged?.Invoke( message );
		Log.Warning( $"Seam-Less PBR Preview: {message}" );
	}

	private static Model CreatePlaneModel()
	{
		var material = Material.Load( "materials/dev/gray_grid_8.vmat" );
		var mesh = new Mesh( material );
		mesh.CreateVertexBuffer( 4, new[]
		{
			new Vertex( new Vector3( -80f, -80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 0f, 0f, 0f, 0f ) ),
			new Vertex( new Vector3( 80f, -80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 2f, 0f, 0f, 0f ) ),
			new Vertex( new Vector3( 80f, 80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 2f, 2f, 0f, 0f ) ),
			new Vertex( new Vector3( -80f, 80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 0f, 2f, 0f, 0f ) )
		} );
		mesh.CreateIndexBuffer( 6, new[] { 0, 1, 2, 2, 3, 0 } );
		mesh.Bounds = BBox.FromPositionAndSize( Vector3.Zero, 160f );

		return Model.Builder.AddMesh( mesh ).Create();
	}

	private static Model CreateTessellatedSphereModel()
	{
		const int uFacets = 64;
		const int vFacets = 64;
		const float maxU = 4f;
		const float maxV = 4f;
		const float radius = 36f;
		var material = Material.Load( "materials/dev/gray_grid_8.vmat" );
		var mesh = new Mesh( material );
		mesh.CreateVertexBuffer<Vertex>( (uFacets + 1) * (vFacets + 1) );
		mesh.CreateIndexBuffer( 2 * 3 * uFacets * vFacets );
		mesh.Bounds = BBox.FromPositionAndSize( Vector3.Zero, radius * 2f );

		mesh.LockVertexBuffer<Vertex>( vertices =>
		{
			var v = 0.5f;
			var index = 0;
			var dU = 1f / uFacets;
			var dV = 1f / vFacets;

			for ( var nV = 0; nV < vFacets + 1; nV++ )
			{
				var u = 0f;

				for ( var nU = 0; nU < uFacets + 1; nU++ )
				{
					var sinTheta = MathF.Sin( u * MathF.PI );
					var cosTheta = MathF.Cos( u * MathF.PI );
					var sinPhi = MathF.Sin( v * 2f * MathF.PI );
					var cosPhi = MathF.Cos( v * 2f * MathF.PI );
					var normal = new Vector3( sinTheta * cosPhi, sinTheta * sinPhi, cosTheta ).Normal;

					vertices[index++] = new Vertex
					{
						Position = radius * normal,
						Normal = normal,
						Tangent = new Vector4( new Vector3( -sinPhi, cosPhi, 0f ).Normal, -1f ),
						TexCoord0 = new Vector2( (v - 0.5f) * maxV, u * maxU ),
						TexCoord1 = new Vector2( (v - 0.5f) * maxV, u * maxU ) * -1f,
						Color = Color.White
					};

					u += dU;
				}

				v += dV;
			}
		} );

		mesh.LockIndexBuffer( indices =>
		{
			var index = 0;

			for ( var v = 0; v < vFacets; v++ )
			{
				for ( var u = 0; u < uFacets; u++ )
				{
					indices[index++] = v * (uFacets + 1) + u;
					indices[index++] = v * (uFacets + 1) + (u + 1);
					indices[index++] = (v + 1) * (uFacets + 1) + u;
					indices[index++] = v * (uFacets + 1) + (u + 1);
					indices[index++] = (v + 1) * (uFacets + 1) + (u + 1);
					indices[index++] = (v + 1) * (uFacets + 1) + u;
				}
			}
		} );

		return Model.Builder.AddMesh( mesh ).Create();
	}
}