Editor/PbrPreviewWidget.cs
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;
public sealed class PbrPreviewWidget : Widget
{
public Action<string[]> FilesDropped { get; set; }
private static readonly PbrMapType[] AtlasOrder =
{
PbrMapType.Albedo,
PbrMapType.Height,
PbrMapType.Normal,
PbrMapType.Roughness,
PbrMapType.AmbientOcclusion,
PbrMapType.Metallic,
PbrMapType.Orm
};
private Pixmap pixmap;
private readonly Dictionary<PbrMapType, Pixmap> atlasPixmaps = new();
private PbrPreviewLayout previewLayout = PbrPreviewLayout.Atlas;
private string caption = "Drop an albedo texture here or use Load Texture";
private bool isDragHovering;
private bool isPanning;
private bool isLoading;
private bool fitToView = true;
private float zoom = 1f;
private Vector2 panOffset;
private Vector2 lastCursorPosition;
private DateTime loadingStartedTime;
public PbrPreviewWidget( Widget parent ) : base( parent )
{
AcceptDrops = true;
MouseTracking = true;
MinimumSize = new Vector2( 480, 360 );
SetStyles( "background-color: #111316; border: 1px solid #2d333a; border-radius: 6px;" );
}
public void SetBitmap( Bitmap bitmap )
{
pixmap = UpdatePixmapFromBitmap( pixmap, bitmap, true );
Update();
}
public void SetResult( PbrGeneratorResult result )
{
atlasPixmaps.Clear();
if ( result != null )
{
foreach ( var mapType in AtlasOrder )
{
var map = result.GetMap( mapType );
var mapPixmap = UpdatePixmapFromBitmap( null, map, false );
if ( mapPixmap != null )
atlasPixmaps[mapType] = mapPixmap;
}
}
Update();
}
public void SetPreviewLayout( PbrPreviewLayout layout )
{
previewLayout = layout;
Update();
}
public void SetCaption( string text )
{
caption = text;
Update();
}
public void SetLoading( bool loading )
{
if ( isLoading == loading )
{
if ( isLoading )
Update();
return;
}
isLoading = loading;
loadingStartedTime = DateTime.Now;
Update();
}
public void FitToView()
{
fitToView = true;
zoom = 1f;
panOffset = Vector2.Zero;
Update();
}
public void SetOneToOne()
{
fitToView = false;
zoom = 1f;
panOffset = Vector2.Zero;
Update();
}
public override void OnDragHover( Widget.DragEvent ev )
{
isDragHovering = SeamlessSuiteImageUtility.GetFilesFromDragData( ev.Data ).Count > 0;
ev.Action = isDragHovering ? DropAction.Copy : DropAction.Ignore;
Update();
}
public override void OnDragLeave()
{
isDragHovering = false;
Update();
}
public override void OnDragDrop( Widget.DragEvent ev )
{
isDragHovering = false;
var files = SeamlessSuiteImageUtility.GetFilesFromDragData( ev.Data );
if ( files.Count > 0 )
{
ev.Action = DropAction.Copy;
FilesDropped?.Invoke( files.ToArray() );
}
else
{
ev.Action = DropAction.Ignore;
}
Update();
}
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
if ( !HasVisiblePreview() || !e.LeftMouseButton )
return;
isPanning = true;
lastCursorPosition = e.ScreenPosition;
e.Accepted = true;
}
protected override void OnMouseReleased( MouseEvent e )
{
base.OnMouseReleased( e );
if ( e.LeftMouseButton )
{
isPanning = false;
e.Accepted = true;
}
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
if ( !isPanning )
return;
fitToView = false;
panOffset += e.ScreenPosition - lastCursorPosition;
lastCursorPosition = e.ScreenPosition;
e.Accepted = true;
Update();
}
protected override void OnMouseWheel( WheelEvent e )
{
base.OnMouseWheel( e );
if ( !HasVisiblePreview() )
return;
fitToView = false;
var scale = e.Delta > 0f ? 1.1f : 0.9f;
zoom = Math.Clamp( zoom * scale, 0.05f, 16f );
e.Accept();
Update();
}
protected override void OnPaint()
{
var rect = LocalRect;
Paint.ClearPen();
Paint.SetBrush( Theme.WindowBackground );
Paint.DrawRect( rect, 6 );
DrawCheckerboard( rect.Shrink( 16 ) );
if ( !HasVisiblePreview() )
{
DrawEmptyState( rect );
DrawLoadingOverlay( rect );
DrawDropOverlay( rect );
return;
}
var imageRect = rect.Shrink( 18 );
imageRect.Top += 20;
if ( previewLayout == PbrPreviewLayout.Atlas )
DrawAtlasPreview( imageRect );
else if ( previewLayout == PbrPreviewLayout.Single )
DrawSinglePreview( imageRect );
else
DrawTiledPreview( imageRect );
DrawCaption( rect );
DrawLoadingOverlay( rect );
DrawDropOverlay( rect );
}
private Pixmap UpdatePixmapFromBitmap( Pixmap currentPixmap, Bitmap bitmap, bool resetViewWhenRecreated )
{
if ( bitmap == null || !bitmap.IsValid )
return null;
if ( currentPixmap != null && currentPixmap.Width == bitmap.Width && currentPixmap.Height == bitmap.Height )
{
if ( currentPixmap.UpdateFromPixels( bitmap ) )
return currentPixmap;
}
if ( resetViewWhenRecreated )
FitToView();
return Pixmap.FromBitmap( bitmap );
}
private bool HasVisiblePreview()
{
if ( previewLayout == PbrPreviewLayout.Atlas )
return atlasPixmaps.Count > 0;
return pixmap != null;
}
private void DrawSinglePreview( Rect area )
{
var scale = GetPreviewScale( area, 1 );
var width = pixmap.Width * scale;
var height = pixmap.Height * scale;
var center = GetPreviewCenter( area );
var targetRect = new Rect( center.x - width * 0.5f, center.y - height * 0.5f, width, height );
Paint.BilinearFiltering = true;
Paint.Draw( targetRect, pixmap, 1f, 3 );
Paint.SetPen( Theme.BorderLight, 1, PenStyle.Solid );
Paint.ClearBrush();
Paint.DrawRect( targetRect, 3 );
}
private void DrawTiledPreview( Rect area )
{
var count = previewLayout == PbrPreviewLayout.Tiled3X3 ? 3 : 2;
var scale = GetPreviewScale( area, count );
var tileWidth = pixmap.Width * scale;
var tileHeight = pixmap.Height * scale;
var totalWidth = tileWidth * count;
var totalHeight = tileHeight * count;
var center = GetPreviewCenter( area );
var startX = center.x - totalWidth * 0.5f;
var startY = center.y - totalHeight * 0.5f;
Paint.BilinearFiltering = true;
for ( var y = 0; y < count; y++ )
{
for ( var x = 0; x < count; x++ )
{
var tileRect = new Rect( startX + x * tileWidth, startY + y * tileHeight, tileWidth + 0.5f, tileHeight + 0.5f );
Paint.Draw( tileRect, pixmap, 1f, 0 );
}
}
Paint.SetPen( Theme.BorderLight, 1, PenStyle.Solid );
Paint.ClearBrush();
Paint.DrawRect( new Rect( startX, startY, totalWidth, totalHeight ), 3 );
}
private void DrawAtlasPreview( Rect area )
{
var columns = 4;
var rows = 2;
var gap = 10f;
var cellWidth = (area.Width - gap * (columns - 1)) / columns;
var cellHeight = (area.Height - gap * (rows - 1)) / rows;
Paint.BilinearFiltering = true;
for ( var index = 0; index < AtlasOrder.Length; index++ )
{
var column = index % columns;
var row = index / columns;
var mapType = AtlasOrder[index];
var cell = new Rect(
area.Left + column * (cellWidth + gap),
area.Top + row * (cellHeight + gap),
cellWidth,
cellHeight
);
DrawAtlasCell( cell, mapType );
}
}
private void DrawAtlasCell( Rect cell, PbrMapType mapType )
{
Paint.ClearPen();
Paint.SetBrush( new Color( 0.07f, 0.078f, 0.086f, 0.92f ) );
Paint.DrawRect( cell, 4 );
Paint.SetPen( Theme.BorderLight, 1, PenStyle.Solid );
Paint.ClearBrush();
Paint.DrawRect( cell, 4 );
var labelRect = cell.Shrink( 7 );
labelRect.Height = 18;
Paint.SetDefaultFont( 9, 700, false, false );
Paint.SetPen( Theme.TextLight );
Paint.DrawText( labelRect, PbrGenerator.GetMapLabel( mapType ), TextFlag.LeftCenter );
if ( !atlasPixmaps.TryGetValue( mapType, out var mapPixmap ) || mapPixmap == null )
{
var missingRect = cell.Shrink( 8 );
missingRect.Top += 20;
Paint.SetDefaultFont( 9, 400, false, false );
Paint.SetPen( Theme.TextDisabled );
Paint.DrawText( missingRect, "No map", TextFlag.Center );
return;
}
var imageArea = cell.Shrink( 7 );
imageArea.Top += 20;
var scaleX = imageArea.Width / Math.Max( 1f, mapPixmap.Width );
var scaleY = imageArea.Height / Math.Max( 1f, mapPixmap.Height );
var scale = MathF.Min( scaleX, scaleY );
var width = mapPixmap.Width * scale;
var height = mapPixmap.Height * scale;
var targetRect = new Rect(
imageArea.Left + (imageArea.Width - width) * 0.5f,
imageArea.Top + (imageArea.Height - height) * 0.5f,
width,
height
);
Paint.Draw( targetRect, mapPixmap, 1f, 2 );
}
private float GetPreviewScale( Rect area, int tileCount )
{
if ( pixmap == null )
return 1f;
if ( !fitToView )
return zoom;
var widthScale = area.Width / Math.Max( 1f, pixmap.Width * tileCount );
var heightScale = area.Height / Math.Max( 1f, pixmap.Height * tileCount );
return Math.Clamp( MathF.Min( widthScale, heightScale ), 0.01f, 1f );
}
private Vector2 GetPreviewCenter( Rect area )
{
return new Vector2(
area.Left + area.Width * 0.5f + panOffset.x,
area.Top + area.Height * 0.5f + panOffset.y
);
}
private void DrawCheckerboard( Rect area )
{
var cellSize = 20f;
var columns = (int)MathF.Ceiling( area.Width / cellSize );
var rows = (int)MathF.Ceiling( area.Height / cellSize );
Paint.ClearPen();
for ( var y = 0; y < rows; y++ )
{
for ( var x = 0; x < columns; x++ )
{
var color = (x + y) % 2 == 0
? new Color( 0.145f, 0.155f, 0.165f, 1f )
: new Color( 0.105f, 0.115f, 0.125f, 1f );
Paint.SetBrush( color );
Paint.DrawRect( new Rect( area.Left + x * cellSize, area.Top + y * cellSize, cellSize, cellSize ) );
}
}
}
private void DrawEmptyState( Rect rect )
{
Paint.SetDefaultFont( 14, 500, false, false );
Paint.SetPen( Theme.TextLight );
Paint.DrawText( rect, "Drop an albedo texture here", TextFlag.Center );
var hintRect = rect;
hintRect.Top += 36;
Paint.SetDefaultFont( 10, 400, false, false );
Paint.SetPen( Theme.TextDisabled );
Paint.DrawText( hintRect, "PNG, JPG, WEBP, BMP, TGA, TIF, PSD, SVG", TextFlag.Center );
}
private void DrawCaption( Rect rect )
{
var captionRect = rect.Shrink( 14 );
captionRect.Height = 20;
Paint.SetDefaultFont( 10, 500, false, false );
Paint.SetPen( Theme.TextLight );
Paint.DrawText( captionRect, caption, TextFlag.LeftCenter );
}
private void DrawLoadingOverlay( Rect rect )
{
if ( !isLoading )
return;
var width = Math.Clamp( rect.Width - 32f, 150f, 190f );
var overlayRect = new Rect( rect.Right - width - 16f, rect.Top + 14f, width, 34f );
if ( overlayRect.Left < rect.Left + 16f )
overlayRect.Left = rect.Left + 16f;
Paint.ClearPen();
Paint.SetBrush( new Color( 0.05f, 0.06f, 0.07f, 0.88f ) );
Paint.DrawRect( overlayRect, 5 );
DrawLoadingSpinner( new Vector2( overlayRect.Left + 18f, overlayRect.Center.y ) );
var elapsed = (DateTime.Now - loadingStartedTime).TotalSeconds;
var dotCount = (int)(elapsed * 3.5) % 4;
var text = $"Building maps{new string( '.', dotCount )}";
var textRect = overlayRect;
textRect.Left += 36f;
textRect.Right -= 10f;
Paint.SetDefaultFont( 10, 600, false, false );
Paint.SetPen( Theme.TextLight );
Paint.DrawText( textRect, text, TextFlag.LeftCenter );
}
private void DrawLoadingSpinner( Vector2 center )
{
var elapsed = (float)(DateTime.Now - loadingStartedTime).TotalSeconds;
var activeDot = (int)(elapsed * 10f) % 8;
for ( var i = 0; i < 8; i++ )
{
var angle = i / 8f * MathF.PI * 2f;
var position = center + new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * 8f;
var distance = (i - activeDot + 8) % 8;
var alpha = 0.25f + (7 - distance) / 7f * 0.55f;
Paint.ClearPen();
Paint.SetBrush( Theme.Blue.WithAlpha( alpha ) );
Paint.DrawCircle( new Rect( position.x - 2f, position.y - 2f, 4f, 4f ) );
}
}
private void DrawDropOverlay( Rect rect )
{
if ( !isDragHovering )
return;
Paint.ClearPen();
Paint.SetBrush( Theme.Blue.WithAlpha( 0.22f ) );
Paint.DrawRect( rect.Shrink( 4 ), 6 );
Paint.SetPen( Theme.TextSelected );
Paint.SetDefaultFont( 16, 700, false, false );
Paint.DrawText( rect, "Release to load texture", TextFlag.Center );
}
}