Editor dock panel UI for the Architecture tool. It builds the dock contents (targets, context, actions), updates when the active ArchTool changes, and exposes utility buttons to rebuild, bake, cull, terrain ops, copy debug text to clipboard, load palettes and open shelf settings.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// A real dock, not an overlay - tabbable beside the Hierarchy. HOW you look at the plan is the toolbar's job, not this one.
[Dock( "Editor", "Architecture", "domain" )]
public sealed class ArchDockPanel : Widget
{
public const string DockName = "Architecture";
Layout targets;
Layout context;
Layout actions;
object trackedItem;
int trackedLevel = int.MinValue;
int trackedRoom = int.MinValue;
int trackedBuilding = int.MinValue;
int trackedParts = int.MinValue;
bool trackedActive;
public ArchDockPanel( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 8;
Layout.Spacing = 4;
targets = Layout.AddColumn();
Layout.AddSeparator();
context = Layout.AddColumn();
Layout.AddSeparator();
actions = Layout.AddColumn();
Layout.AddStretchCell();
Rebuild();
}
public static void Open()
{
EditorWindow.DockManager.SetDockState( DockName, true );
EditorWindow.DockManager.RaiseDock( DockName );
}
// The dock outlives the tool, so it reads whichever Architecture tab is up and says so when none is.
static ArchTool Tool => ArchTool.Active;
[EditorEvent.Frame]
void OnFrame()
{
var tool = Tool;
var item = tool?.Picked?.Item;
// NextId doubles as a stamp for "something was added" - the panel has to notice additions.
var parts = tool?.Plan.NextId ?? 0;
if ( ReferenceEquals( item, trackedItem )
&& (tool is not null) == trackedActive
&& (tool?.Level ?? int.MinValue) == trackedLevel
&& (tool?.ActiveRoomId ?? int.MinValue) == trackedRoom
&& (tool?.ActiveBuildingId ?? int.MinValue) == trackedBuilding
&& parts == trackedParts )
{
return;
}
trackedActive = tool is not null;
trackedItem = item;
trackedLevel = tool?.Level ?? int.MinValue;
trackedRoom = tool?.ActiveRoomId ?? int.MinValue;
trackedBuilding = tool?.ActiveBuildingId ?? int.MinValue;
trackedParts = parts;
Rebuild();
}
void Rebuild()
{
targets.Clear( true );
context.Clear( true );
actions.Clear( true );
if ( Tool is not { } tool )
{
targets.Add( ArchPartUi.Wrapped( "Pick the Architecture or Roads tool in the scene view to edit this scene's plan." ) );
return;
}
tool.BuildTargets( targets, Rebuild );
BuildStatus( tool );
BuildActions( tool );
}
// The status line: what the document is, not what is selected - that is the Layer Inspector's.
void BuildStatus( ArchTool tool )
{
var layers = tool.Plan.Layers.Count;
var groups = tool.Plan.Assemblies.Count;
var line = new Label( $"{tool.Plan.Buildings.Count} buildings · {tool.Plan.Roads().Count} roads · {groups} groups · {layers} layer records" );
line.SetStyles( "color: rgba(255,255,255,0.45);" );
line.WordWrap = true;
context.Add( line );
}
void BuildActions( ArchTool tool )
{
var row = actions.AddRow();
row.Spacing = 4;
row.Add( new Button.Primary( "Rebuild", "refresh" ) { Clicked = () => tool.RebuildCold() } );
// Disown first: both edit the built meshes, so what is standing stops being what the tool emitted and the
// next rebuild has to emit all of it rather than leave the polish in place.
row.Add( new Button( "Bake", "merge_type" )
{
Clicked = () => { tool.Disown(); Log.Info( $"Architecture: merged {ArchBake.MergeScene( tool.Scene )} meshes." ); }
} );
row.Add( new Button( "", "cleaning_services" )
{
ToolTip = "Delete hidden interior faces",
Clicked = () => { tool.Disown(); Log.Info( $"Architecture: deleted {ArchCull.Clean( tool.Scene, tool.Plan, tool.Kit )} hidden faces." ); }
} );
row.Add( new Button( "", "vertical_align_top" )
{
ToolTip = "Open room under the ground so a stamp has somewhere to carve to. The surface does not move. Not undoable.",
Clicked = () => ArchTerrain.Headroom( tool.Scene )
} );
row.Add( new Button( "", "landscape" )
{
ToolTip = "Stamp the heightmap under every building, road and drive. Not undoable.",
Clicked = () => ArchTerrain.Flatten( tool.Scene, tool.Plan, tool.Kit )
} );
row.Add( new Button( "", "content_copy" )
{
ToolTip = "Copy wall debug - names, endpoints, grid status and resolved joins - to the clipboard",
Clicked = () => CopyWallDebug( tool )
} );
row.Add( new Button( "", "grid_on" )
{
ToolTip = "Copy mesh debug for the selected objects - face counts, materials, texel scales and every face whose texture axes came out NaN, collapsed or off its own plane",
Clicked = () => CopyMeshDebug( tool )
} );
row.Add( new Button( "", "palette" ) { ToolTip = "Load map palette", Clicked = () => LoadMapPalette( tool ) } );
row.Add( new Button( "", "tune" )
{
ToolTip = "Which subtools stand on the shelves",
Clicked = () => new ArchShelfSettingsDialog( this ).Show()
} );
row.Add( new Button.Danger( "", "delete_forever" ) { ToolTip = "Clear plan", Clicked = () => { tool.ClearPlan(); Rebuild(); } } );
row.AddStretchCell();
}
void CopyWallDebug( ArchTool tool )
{
var report = ArchReport.WallDebug( tool.Plan, tool.Kit );
EditorUtility.Clipboard.Copy( report );
Log.Info( $"Architecture: wall debug copied to the clipboard ({report.Length} characters)." );
}
// The whole selection and everything under it: one building's geometry is a tree of meshes, and the
// face that reads wrong is never the one the object list has highlighted.
void CopyMeshDebug( ArchTool tool )
{
var picked = SceneEditorSession.Active?.Selection.OfType<GameObject>().ToList() ?? new List<GameObject>();
var meshes = picked.Count > 0
? picked.SelectMany( target => target.Components.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants ) )
: tool.Scene.GetAllObjects( false ).SelectMany( target => target.Components.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants ) );
var report = ArchReport.MeshDebug( meshes.Distinct() );
EditorUtility.Clipboard.Copy( report );
Log.Info( $"Architecture: mesh debug copied to the clipboard ({report.Length} characters)." );
}
void LoadMapPalette( ArchTool tool )
{
ArchMapPalette.ShowPicker( this, tool.Kit.Palette, () =>
{
ArchStyle.InvalidateCache();
ArchStorage.SaveKit( tool.Kit );
tool.Commit();
Rebuild();
} );
}
}