Editor/Tool/ArchToolView.cs

Editor tool helper for architecture views. It defines view axes, computes which SceneViewportWidget is focused or available panes, maps between Axis and SceneViewportWidget.ViewMode, arranges viewports, frames views to an edited bounding box, and provides depth/plan mode helpers.

File AccessNative Interop
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public enum ArchViewAxis
{
	Free,
	Top,
	Front,
	Side
}

public abstract partial class ArchTool
{
	public float? SectionDepth { get; set; }

	// Read off the viewport, never stored - CTRL+SPACE and the gizmo change the view without passing through here.
	public ArchViewAxis Axis => AxisOf( FocusedViewport() );

	public bool InPlanMode => Axis != ArchViewAxis.Free;

	public bool InElevation => Axis is ArchViewAxis.Front or ArchViewAxis.Side;

	public static SceneViewportWidget FocusedViewport() => SceneViewWidget.Current?.LastSelectedViewportWidget;

	// Ordered by pane id - a dictionary hands them back in whatever order it likes.
	public static List<SceneViewportWidget> Panes()
	{
		if ( SceneViewWidget.Current is not { } view )
		{
			return new List<SceneViewportWidget>();
		}

		return view._viewports
			.OrderBy( entry => entry.Key )
			.Select( entry => entry.Value )
			.Where( pane => pane.IsValid() )
			.ToList();
	}

	public static ArchViewAxis AxisOf( SceneViewportWidget viewport )
	{
		return viewport?.State?.View switch
		{
			SceneViewportWidget.ViewMode.Top2d => ArchViewAxis.Top,
			SceneViewportWidget.ViewMode.Front2d => ArchViewAxis.Front,
			SceneViewportWidget.ViewMode.Side2d => ArchViewAxis.Side,
			_ => ArchViewAxis.Free
		};
	}

	static SceneViewportWidget.ViewMode ModeOf( ArchViewAxis axis )
	{
		return axis switch
		{
			ArchViewAxis.Top => SceneViewportWidget.ViewMode.Top2d,
			ArchViewAxis.Front => SceneViewportWidget.ViewMode.Front2d,
			ArchViewAxis.Side => SceneViewportWidget.ViewMode.Side2d,
			_ => SceneViewportWidget.ViewMode.Perspective
		};
	}

	// The viewport still owns which view it is in, so the engine's own controls stay first-class.
	public void Show( SceneViewportWidget viewport, ArchViewAxis axis )
	{
		if ( viewport is null )
		{
			return;
		}

		// Setting the mode picks the rotation, so framing follows.
		viewport.State.View = ModeOf( axis );

		if ( axis != ArchViewAxis.Free )
		{
			FrameOn( viewport, EditedExtent() );
		}

		viewport.SaveState();
	}

	// SetLayout destroys every viewport widget, so the split has to come before the assignments.
	public void Arrange( SceneViewWidget.ViewportLayoutMode layout, params ArchViewAxis[] axes )
	{
		if ( SceneViewWidget.Current is not { } view )
		{
			return;
		}

		view.SetLayout( layout );

		var panes = Panes();

		for ( var index = 0; index < panes.Count && index < axes.Length; index++ )
		{
			Show( panes[index], axes[index] );
		}

		view.SaveState();
	}

	public void Frame() => Frame( EditedExtent() );

	public void Frame( BBox bounds )
	{
		foreach ( var pane in Panes() )
		{
			if ( AxisOf( pane ) != ArchViewAxis.Free )
			{
				FrameOn( pane, bounds );
			}
		}
	}

	void FrameOn( SceneViewportWidget viewport, BBox bounds )
	{
		var size = viewport.Renderer?.Size ?? new Vector2( 1280f, 800f );
		var aspect = size.y > 1f ? size.x / size.y : 1.6f;

		viewport.State.CameraPosition = bounds.Center;
		viewport.State.CameraOrthoHeight = ArchShot.Framing( bounds, viewport.State.CameraRotation, aspect, 0.12f );
	}

	BBox EditedExtent() => Extent( EditedBuilding() );

	public float Depth( ArchViewAxis axis )
	{
		if ( SectionDepth is { } chosen )
		{
			return Grid.Height( chosen );
		}

		var centre = EditedExtent().Center;

		return Grid.Height( axis == ArchViewAxis.Front ? centre.x : centre.y );
	}

}