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.
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;
}
}