Editor/Tool/ArchLayerPieceRows.cs

An editor UI tree node representing a built architectural piece in a layer stack. It draws the piece row (icon, name, render/group tag, visibility and collision icons), provides tooltip and activation behavior (frame and select layer), and adds context menu options to toggle visibility, edit collisions, and frame the piece.

File AccessNetworking
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// A named piece of what one layer built, as a row under the layer that built it: House / Roof / Gutters /
// Fascia. It is not an authored layer - there is nothing to reshape, reorder or delete - so it carries the two
// controls that DO mean something about a piece of geometry: hide it, and say what it collides as.
sealed class ArchPieceRow : TreeNode
{
	readonly ArchBuiltPiece piece;

	public ArchPieceRow( ArchBuiltPiece piece ) : base( piece.Key )
	{
		this.piece = piece;
		Height = 22;

		foreach ( var child in piece.Children )
		{
			AddItem( new ArchPieceRow( child ) );
		}
	}

	public ArchBuiltPiece Built => piece;

	public override void OnPaint( VirtualWidget item )
	{
		var tool = ArchTool.Active;
		var rect = item.Rect;

		PaintSelection( item );

		var hidden = tool?.PieceHidden( piece.Owner, piece.Piece ) == true;
		var alpha = hidden ? 0.28f : 0.85f;

		Paint.SetPen( Theme.Text.WithAlpha( alpha ) );
		Paint.DrawIcon( rect.Shrink( 4, 0, 0, 0 ), ArchPieces.Glyph( piece.Name ), 14, TextFlag.LeftCenter );

		var body = rect.Shrink( 24, 0, ArchLayerStack.ControlWidth * 3f + 4f, 0 );

		Paint.DrawText( body, piece.Name, TextFlag.LeftCenter );

		// A piece with no mesh of its own is a folder over the ones under it, and saying so is what stops an
		// author hunting for the collision setting that would change nothing.
		Paint.SetPen( Theme.Text.WithAlpha( 0.3f * alpha ) );
		Paint.DrawText( body, piece.Renders ? "piece" : "group", TextFlag.RightCenter );

		Collision( rect );

		Paint.SetPen( Theme.Text.WithAlpha( hidden ? 0.75f : 0.3f ) );
		Paint.DrawIcon( Control( rect, 1 ), hidden ? "visibility_off" : "visibility", 14, TextFlag.Center );
	}

	void Collision( Rect rect )
	{
		if ( ArchTool.Active is not { } tool )
		{
			return;
		}

		var mode = ArchParts.Showing( tool.Plan, tool.Kit, tool.LayerTree, piece.Owner, piece.Piece, out var own );

		Paint.SetPen( !own
			? Theme.Text.WithAlpha( 0.3f )
			: mode == ArchCollisionMode.Complex ? Theme.Yellow : Theme.Blue );

		Paint.DrawIcon( Control( rect, 2 ), ArchCollision.Glyph( mode ), 14, TextFlag.Center );
	}

	static Rect Control( Rect row, int fromRight )
	{
		var width = ArchLayerStack.ControlWidth;

		return new Rect( row.Right - width * (fromRight + 1), row.Top, width, row.Height );
	}

	public override string GetTooltip()
	{
		return $"{piece.Piece} · built by layer {piece.Owner}"
			+ (piece.Renders ? "" : "\nA group over the pieces under it - it renders nothing itself.");
	}

	public override void OnActivated()
	{
		if ( ArchTool.Active is { } tool && piece.Object.IsValid() )
		{
			tool.Frame( piece.Object.GetBounds() );
			tool.SelectLayer( tool.LayerTree.Find( piece.Owner ) );
		}
	}

	// The whole subtree, because everything under this piece with no answer of its own was following it.
	public void Collisions( ContextMenu menu, ArchTool tool )
	{
		var showing = ArchParts.Showing( tool.Plan, tool.Kit, tool.LayerTree, piece.Owner, piece.Piece, out _ );
		var collisions = menu.AddMenu( "Collisions", ArchCollision.Glyph( showing ) );

		collisions.AddOption( ArchParts.Following( tool.Plan, tool.Kit, tool.LayerTree, piece.Owner, piece.Piece ),
			"settings", () => Collide( tool, null ) );

		collisions.AddSeparator();

		foreach ( var mode in new[] { ArchCollisionMode.Solids, ArchCollisionMode.Convex, ArchCollisionMode.Complex, ArchCollisionMode.None } )
		{
			var captured = mode;

			collisions.AddOption( ArchCollision.Describe( captured ), ArchCollision.Glyph( captured ),
				() => Collide( tool, captured ) );
		}
	}

	void Collide( ArchTool tool, ArchCollisionMode? mode )
	{
		if ( mode is null )
		{
			tool.Plan.Parts.RemoveAll( record => record.ItemId == piece.Owner && record.Piece == piece.Piece );
		}
		else
		{
			ArchParts.Record( tool.Plan, piece.Owner, piece.Piece ).Collision = mode;
		}

		tool.Affect( ArchLayerGroups.Holding( tool.Plan, piece.Owner )?.Id ?? piece.Owner );
		tool.Commit( $"Collisions {piece.Name}" );
	}

	public override bool OnContextMenu()
	{
		if ( ArchTool.Active is not { } tool )
		{
			return false;
		}

		var menu = new ContextMenu( TreeView );

		menu.AddOption( tool.PieceHidden( piece.Owner, piece.Piece ) ? "Show" : "Hide",
			tool.PieceHidden( piece.Owner, piece.Piece ) ? "visibility" : "visibility_off",
			() => tool.TogglePieceHidden( piece.Owner, piece.Piece ) );

		Collisions( menu, tool );

		menu.AddSeparator();
		menu.AddOption( "Frame", "filter_center_focus", OnActivated );

		menu.OpenAtCursor();

		return true;
	}
}