Editor/EffigyEditor/EffigyStudioPreview.cs
using Editor;
using Editor.Assets;
using Effigy;
using Sandbox;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Marionette.EditorTools;
/// <summary>
/// The asset browser's preview of a .effigy part studio: the model the document actually builds,
/// turning on the spot, rather than the generic document icon every unrecognised file gets.
///
/// WHY A PREVIEW AND NOT A THUMBNAIL RENDERER. The engine's thumbnail path is
/// AssetThumbnail.RenderAssetThumb, and it walks [Asset.ThumbnailRenderer] methods in priority
/// order. One of those already exists — AssetPreview.RenderAssetThumbnail in the tools addon —
/// and all it does is look for an [AssetPreview] claiming the extension, build its scene and
/// photograph it. Registering here rather than writing another ThumbnailRenderer means the SAME
/// class also fills the inspector's preview panel, which a thumbnail renderer would not, and the
/// scene, camera, lighting and turntable come from AssetPreview instead of being written again.
///
/// IT IS ALSO WHAT STOPS THE BROWSER RETRYING. RenderAssetThumb calls Asset.LoadResource() before
/// it reaches any of this, and a part studio is line-based text rather than a serialised
/// GameResource, so that load has nothing to return. The console message it used to leave is gone
/// for a different reason — EffigyPartStudioAsset is abstract, and that is the one exit in
/// Asset.TryLoadGameResource with no Log.Warning attached — but the RETRYING was this: a null
/// thumbnail is never written to the on-disk cache, so the browser re-rendered every time the tile
/// came back into view. A real bitmap gets cached as a PNG, and the whole path runs once per save.
///
/// EVERYTHING IS BEST-EFFORT. This runs against whatever is on disk, including a document saved
/// by a newer build, one whose sculpt blobs have been deleted, or one with a broken feature
/// halfway down the tree. None of that is worth an error in somebody's console while they are
/// scrolling a folder, so a failure here just leaves the icon alone.
/// </summary>
[AssetPreview( "effigy" )]
internal sealed class EffigyStudioPreview : AssetPreview
{
public override bool IsAnimatedPreview => true;
public override float PreviewWidgetCycleSpeed => 0.2f;
public EffigyStudioPreview( Asset asset ) : base( asset )
{
}
public override async Task InitializeAsset()
{
await Task.Yield();
var path = Asset?.AbsolutePath;
if ( string.IsNullOrEmpty( path ) || !File.Exists( path ) )
return;
PartStudio studio;
try
{
studio = StudioDocument.ReadFile( path );
}
catch ( Exception )
{
return;
}
// Before the rebuild, for the reason LoadDocument gives: a sculpt feature is handed its
// bytes here and only turns them into geometry on the rebuild below, once the cage it
// sculpts has been built by the features above it. Missing blobs are not fatal — the
// feature rebuilds as its unsculpted cage, which is still worth a picture.
try
{
SculptSidecar.Load( studio, path );
}
catch ( Exception )
{
}
try
{
ImportSidecar.Load( studio, path );
}
catch ( Exception )
{
}
using ( Scene.Push() )
using ( EditorUtility.DisableTextureStreaming() )
{
Model model;
try
{
// The report is deliberately ignored. A document with a failing feature still
// builds everything above it, and that partial part is a better thumbnail than
// no thumbnail — the same call the window makes for the same reason.
studio.Rebuild();
// Visible bodies only, and wearing their real vmats, so the browser shows what
// the window shows rather than a flat grey stand-in.
model = EffigyPreview.Build( studio.ToVisibleMesh(),
slot => studio.MaterialNames.TryGetValue( slot, out var name ) ? name : null );
}
catch ( Exception )
{
return;
}
if ( model is null || model.MeshCount == 0 )
return;
PrimaryObject = new GameObject( true, "effigy part studio" );
PrimaryObject.WorldTransform = Transform.Zero;
var renderer = PrimaryObject.AddComponent<ModelRenderer>();
renderer.Model = model;
SceneSize = model.RenderBounds.Size;
SceneCenter = renderer.WorldRotation * model.RenderBounds.Center;
}
}
}