Editor/FenceLibrary/FenceDefinitionAssetPreview.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Editor.Assets;
namespace Editor;
[AssetPreview( "fencedef" )]
public sealed class FenceDefinitionAssetPreview : AssetPreview
{
private static readonly Color GuideColor = Color.Parse( "#f8a64c" ) ?? Color.Orange;
private FenceDefinition definition;
private GameObject previewRoot;
private readonly List<FenceGuideSceneObject> guides = [];
private int lastHash;
public FenceDefinitionAssetPreview( Asset asset ) : base( asset )
{
}
public override float PreviewWidgetCycleSpeed => 0.0f;
public override async Task InitializeAsset()
{
await base.InitializeAsset();
definition = Asset.LoadResource<FenceDefinition>();
if ( definition is null )
return;
using ( Scene.Push() )
{
RebuildPreview();
}
}
public override void UpdateScene( float cycle, float timeStep )
{
if ( definition is not null )
{
var currentHash = FenceDefinitionEditorUtility.ComputeDefinitionHash( definition );
if ( currentHash != lastHash )
{
using ( Scene.Push() )
{
RebuildPreview();
}
}
}
using ( Scene.Push() )
{
var rotation = new Angles( 18.0f, 225.0f, 0.0f ).ToRotation();
var distance = MathX.SphereCameraDistance( Math.Max( SceneSize.Length * 0.5f, 1.0f ), Camera.FieldOfView );
var aspect = ScreenSize.y > 0 ? (float)ScreenSize.x / ScreenSize.y : 1.0f;
if ( aspect > 1.0f )
{
distance *= aspect;
}
Camera.WorldRotation = rotation;
Camera.WorldPosition = SceneCenter + rotation.Forward * -distance;
}
TickScene( timeStep );
}
public override void Dispose()
{
using ( Scene?.Push() )
{
ClearPreview();
}
base.Dispose();
}
private void RebuildPreview()
{
FenceGuideSceneObject.EnsureAssetsLoaded();
ClearPreview();
previewRoot = new GameObject( true, "Fence Preview" );
PrimaryObject = previewRoot;
lastHash = FenceDefinitionEditorUtility.ComputeDefinitionHash( definition );
var prototypes = FenceDefinitionEditorUtility.BuildPrototypes( definition );
var cursor = 0.0f;
const float gap = 16.0f;
var hasBounds = false;
var bounds = BBox.FromPositionAndSize( Vector3.Zero, 8.0f );
foreach ( var prototype in prototypes )
{
var segmentRoot = SpawnPreviewSegment( prototype );
if ( !segmentRoot.IsValid() )
continue;
segmentRoot.SetParent( previewRoot, true );
var range = FenceDefinitionEditorUtility.MeasureProjectedRange( segmentRoot.GetBounds(), Vector3.Forward );
segmentRoot.WorldPosition += Vector3.Forward * (cursor - range.Min);
var segmentBounds = segmentRoot.GetBounds();
var guideZ = segmentBounds.Mins.z - 2.0f;
var guideStart = new Vector3( cursor, segmentBounds.Center.y, guideZ );
var guideEnd = guideStart + Vector3.Forward * prototype.CanonicalLength;
guides.Add( new FenceGuideSceneObject( Scene.SceneWorld, guideStart, guideEnd, GuideColor ) );
if ( !hasBounds )
{
bounds = segmentBounds;
hasBounds = true;
}
else
{
bounds = bounds.AddBBox( segmentBounds );
}
bounds = bounds.AddPoint( guideStart );
bounds = bounds.AddPoint( guideEnd );
cursor += prototype.CanonicalLength + gap;
}
SceneCenter = hasBounds ? bounds.Center : Vector3.Zero;
SceneSize = hasBounds ? bounds.Size : new Vector3( 64.0f, 64.0f, 64.0f );
}
private GameObject SpawnPreviewSegment( FencePrototype prototype )
{
if ( prototype is null )
return null;
var plan = new FencePlacementPlan
{
RootName = prototype.DisplayName,
Origin = Vector3.Zero,
Direction = Vector3.Forward,
TotalLength = prototype.CanonicalLength,
Segments =
[
new FencePlacementSegment
{
Prototype = prototype,
LinePoint = Vector3.Zero,
SurfacePoint = Vector3.Zero,
Direction = Vector3.Forward,
Up = Vector3.Up,
Length = prototype.CanonicalLength,
StartDistance = 0.0f,
EndDistance = prototype.CanonicalLength
}
]
};
var placed = FenceDefinitionEditorUtility.CommitPlacementPlan( plan );
if ( !placed.IsValid() )
return null;
var segment = placed.Children.Count > 0 ? placed.Children[0] : null;
if ( segment.IsValid() )
{
segment.Parent = null;
}
placed.Destroy();
return segment;
}
private void ClearPreview()
{
FenceDefinitionEditorUtility.DestroyHierarchy( previewRoot );
previewRoot = null;
PrimaryObject = null;
foreach ( var guide in guides )
{
guide?.Delete();
}
guides.Clear();
}
}
internal sealed class FenceGuideSceneObject : SceneCustomObject
{
private static Material lineMaterial;
private readonly Vertex[] vertices;
internal static void EnsureAssetsLoaded()
{
lineMaterial ??= Material.Load( "materials/gizmo/line.vmat" );
}
public FenceGuideSceneObject( SceneWorld world, Vector3 start, Vector3 end, Color color ) : base( world )
{
var direction = (end - start).Normal;
var arrowLength = Math.Max( (end - start).Length * 0.12f, 4.0f );
var arrowBase = end - direction * arrowLength;
var side = Vector3.Cross( direction, Vector3.Up ).Normal * Math.Max( arrowLength * 0.45f, 1.5f );
vertices =
[
new Vertex( start, color ),
new Vertex( end, color ),
new Vertex( end, color ),
new Vertex( arrowBase + side, color ),
new Vertex( end, color ),
new Vertex( arrowBase - side, color )
];
var bounds = BBox.FromPositionAndSize( start, 1.0f );
bounds = bounds.AddPoint( end );
bounds = bounds.AddPoint( arrowBase + side );
bounds = bounds.AddPoint( arrowBase - side );
Bounds = bounds;
}
public override void RenderSceneObject()
{
if ( lineMaterial is null )
return;
Graphics.Draw( vertices.AsSpan(), vertices.Length, lineMaterial, Attributes, Graphics.PrimitiveType.Lines );
}
}