Editor/Tool/ArchLayerGeometryRows.cs

Editor UI tree rows for architecture geometry. Defines TreeNode subclasses that represent a layer's generated geometry: head row listing pieces, piece rows grouping faces or subgroups, group rows, and face rows; they build children from ArchTool probe data and render text/icons and context menus.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// What a layer actually BUILT, under the layer that authored it: its pieces, and the faces of each.
// The authored stack answers "what did I ask for"; these rows answer "what came out", which is the
// only surface on which a z-fight can be pointed at rather than described.
//
// Built when the row is opened, never before: a face row per face over a whole plan is tens of
// thousands of rows, and the probe walks every mesh in the scene to make them.
sealed class ArchGeometryHeadRow : TreeNode
{
	readonly int layerId;

	// Only the pieces THIS layer emitted. Handed the room's whole subtree instead, a room listed its
	// walls' meshes and the wall rows underneath it listed them again.
	public ArchGeometryHeadRow( int layerId ) : base( (layerId, "geometry") )
	{
		this.layerId = layerId;
		Height = 20;
	}

	public override bool HasChildren => true;

	// Reconciled rather than appended, off the tool's own probe: the tree asks a node to build its
	// children more than once, and a fresh probe each time both duplicated every row and swapped the
	// face objects the selection - and the viewport picker - hold on to.
	protected override void BuildChildren()
	{
		if ( ArchTool.Active is not { } tool )
		{
			return;
		}

		SetChildren( tool.BuiltFaces( layerId ), piece => new ArchGeometryPieceRow( piece ) );
	}

	public override void OnPaint( VirtualWidget item )
	{
		Paint.SetPen( Theme.Text.WithAlpha( 0.4f ) );
		Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), "widgets", 13, TextFlag.LeftCenter );
		Paint.DrawText( item.Rect.Shrink( 22, 0, 0, 0 ), "GEOMETRY", TextFlag.LeftCenter );
	}

	public override string GetTooltip() => "The pieces and faces this layer generated. Ctrl-click faces to pick several, then Copy Debug Info.";
}

sealed class ArchGeometryPieceRow : TreeNode
{
	readonly ArchFacePiece piece;

	public ArchGeometryPieceRow( ArchFacePiece piece ) : base( piece )
	{
		this.piece = piece;
		Height = 20;
	}

	public override bool HasChildren => piece.Faces.Count > 0;

	// Lumps when the piece has them - three hundred rows all called Gutter is a list you cannot read.
	protected override void BuildChildren()
	{
		if ( piece.Groups.Count > 0 )
		{
			SetChildren( piece.Groups, group => new ArchGeometryGroupRow( group ) );

			return;
		}

		SetChildren( piece.Faces, face => new ArchGeometryFaceRow( face ) );
	}

	public override void OnPaint( VirtualWidget item )
	{
		PaintSelection( item );

		var name = piece.Name.Split( '/' ).Last();

		Paint.SetPen( Theme.Text.WithAlpha( 0.8f ) );
		Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), "category", 13, TextFlag.LeftCenter );
		Paint.DrawText( item.Rect.Shrink( 22, 0, 0, 0 ), name, TextFlag.LeftCenter );

		Paint.SetPen( Theme.Text.WithAlpha( 0.3f ) );
		Paint.DrawText( item.Rect.Shrink( 0, 0, 6, 0 ), $"{piece.Faces.Count} faces", TextFlag.RightCenter );
	}

	public override string GetTooltip() => $"{piece.Name}\n{piece.Faces.Count} faces · {piece.Bounds.Mins} to {piece.Bounds.Maxs}";
}

// A lump of the piece, or a side of it. Selecting the row picks every face under it, which is what
// "tell me about the north gutter" means.
sealed class ArchGeometryGroupRow : TreeNode
{
	readonly ArchFaceGroup group;

	public ArchGeometryGroupRow( ArchFaceGroup group ) : base( group )
	{
		this.group = group;
		Height = 20;
	}

	public override bool HasChildren => group.Children.Count > 0 || group.Faces.Count > 0;

	protected override void BuildChildren()
	{
		if ( group.Children.Count > 0 )
		{
			SetChildren( group.Children, child => new ArchGeometryGroupRow( child ) );

			return;
		}

		SetChildren( group.Faces, face => new ArchGeometryFaceRow( face ) );
	}

	public override void OnPaint( VirtualWidget item )
	{
		PaintSelection( item );

		Paint.SetPen( Theme.Text.WithAlpha( 0.75f ) );
		Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), "folder", 12, TextFlag.LeftCenter );
		Paint.DrawText( item.Rect.Shrink( 22, 0, 0, 0 ), group.Name, TextFlag.LeftCenter );

		Paint.SetPen( Theme.Text.WithAlpha( 0.3f ) );
		Paint.DrawText( item.Rect.Shrink( 0, 0, 6, 0 ), $"{group.Count} faces", TextFlag.RightCenter );
	}

	public override string GetTooltip() => $"{group.Name} · {group.Count} faces · {group.Bounds.Mins} to {group.Bounds.Maxs}";

	public override bool OnContextMenu()
	{
		var menu = new ContextMenu( TreeView );

		menu.AddOption( "Copy debug info", "content_copy", () =>
		{
			ArchTool.Active?.SelectFaces( group.Every );
			ArchTool.Active?.CopyFaceDebug();
		} );

		menu.OpenAtCursor();

		return true;
	}
}

sealed class ArchGeometryFaceRow : TreeNode
{
	readonly ArchFaceDetail face;

	public ArchGeometryFaceRow( ArchFaceDetail face ) : base( face )
	{
		this.face = face;
		Height = 20;
	}

	public ArchFaceDetail Face => face;

	public override void OnPaint( VirtualWidget item )
	{
		PaintSelection( item );

		// It reads as a checkbox, so it has to tick: a picked face is what gets copied, and five rows
		// that all look empty say nothing about which ones are in the report.
		Paint.SetPen( item.Selected ? Theme.Primary : Theme.Text.WithAlpha( 0.5f ) );
		Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), item.Selected ? "check_box" : "check_box_outline_blank", 13, TextFlag.LeftCenter );

		Paint.SetPen( Theme.Text.WithAlpha( 0.7f ) );
		Paint.DrawText( item.Rect.Shrink( 22, 0, 0, 0 ), $"{face.Index}  {face.Role}", TextFlag.LeftCenter );

		Paint.SetPen( Theme.Text.WithAlpha( 0.3f ) );
		Paint.DrawText( item.Rect.Shrink( 0, 0, 6, 0 ), $"{face.Size} · {face.Plane}", TextFlag.RightCenter );
	}

	public override string GetTooltip() => $"{face.Label}\nnormal {face.Normal} · centre {face.Centre}\n{face.Material}";

	public override bool OnContextMenu()
	{
		var menu = new ContextMenu( TreeView );

		menu.AddOption( "Copy debug info", "content_copy", () => ArchTool.Active?.CopyFaceDebug() );
		menu.OpenAtCursor();

		return true;
	}
}