Editor/Tool/ArchToolFaces.cs

Editor-side part of the ArchTool. Manages and caches built face data and debug selection state, provides queries for faces under a ray, drawing debug overlays, copying face info to clipboard, and invalidating cached build data.

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

namespace Sunless.Architecture;

// The built faces: probed here and nowhere else, because the stack's rows, the viewport picker and the
// copied report all have to be talking about the SAME face objects - identity is what lets a click in
// the viewport select the row in the tree. Dropped whenever the plan is revised, since every face of it
// has just been rebuilt.
public partial class ArchTool
{
	static readonly Color FaceFault = new( 1f, 0.36f, 0.3f );

	readonly List<ArchFaceDetail> debugFaces = new();
	readonly Dictionary<int, List<ArchFacePiece>> probed = new();

	Dictionary<int, List<MeshComponent>> builtMeshes;
	Dictionary<int, List<ArchBuiltPiece>> builtPieces;

	public IReadOnlyList<ArchFaceDetail> DebugFaces => debugFaces;

	// Bumped on every change of pick, so the stack can tell a pick it did not make from one it did.
	public int FacePick { get; private set; }

	public Dictionary<int, List<MeshComponent>> BuiltMeshes => builtMeshes ??= ArchAudit.BuiltByLayer( Scene );

	public Dictionary<int, List<ArchBuiltPiece>> BuiltPieces => builtPieces ??= ArchBuiltPieces.Index( Scene );

	// The named pieces this layer came out as - the rows an author hides and collides one at a time.
	public IReadOnlyList<ArchBuiltPiece> PiecesOf( int layerId )
	{
		return BuiltPieces.TryGetValue( layerId, out var pieces ) ? pieces : Array.Empty<ArchBuiltPiece>();
	}

	public List<ArchFacePiece> BuiltFaces( int layerId )
	{
		if ( probed.TryGetValue( layerId, out var pieces ) )
		{
			return pieces;
		}

		pieces = BuiltMeshes.TryGetValue( layerId, out var meshes )
			? ArchAudit.Faces( this, meshes )
			: new List<ArchFacePiece>();

		probed[layerId] = pieces;

		return pieces;
	}

	public IEnumerable<ArchFacePiece> EveryBuiltPiece()
	{
		return BuiltMeshes.Keys.ToList().SelectMany( BuiltFaces );
	}

	public void ForgetBuiltFaces()
	{
		builtMeshes = null;
		builtPieces = null;
		probed.Clear();

		if ( debugFaces.Count > 0 )
		{
			SelectFaces( null );
		}
	}

	// Every face the ray crosses, nearest first: a coplanar pair sits at one distance, so a picker with a
	// single winner can never reach the other half of it.
	public List<ArchFaceDetail> FacesUnder( Ray ray ) => ArchAudit.FacesAt( EveryBuiltPiece(), ray );

	public List<ArchFaceDetail> Fighting( ArchFaceDetail face ) => ArchAudit.Fighting( EveryBuiltPiece(), face );

	public void SelectFaces( IEnumerable<ArchFaceDetail> faces )
	{
		var picked = faces?.ToList() ?? new List<ArchFaceDetail>();

		if ( picked.Count == debugFaces.Count && picked.All( face => debugFaces.Contains( face ) ) )
		{
			return;
		}

		debugFaces.Clear();
		debugFaces.AddRange( picked );
		FacePick++;
	}

	// Copies what arch_faces returns, so a paste from the panel and a query over MCP say the same thing
	// about the same face. Plain is the prose form for reading; the briefing is the same facts keyed by
	// face id with a verdict on top, which is what an agent can act on without re-deriving anything.
	public string CopyFaceDebug( bool briefing = false )
	{
		var text = briefing
			? ArchAudit.FaceBriefing( this, debugFaces )
			: ArchAudit.FaceDebug( this, debugFaces );

		EditorUtility.Clipboard.Copy( text );

		return text;
	}

	void DrawDebugFaces()
	{
		if ( debugFaces.Count == 0 )
		{
			return;
		}

		foreach ( var face in debugFaces )
		{
			Gizmo.Draw.Color = FaceFault.WithAlpha( 0.22f );

			for ( var corner = 1; corner < face.Corners.Length - 1; corner++ )
			{
				Gizmo.Draw.SolidTriangle( face.Corners[0], face.Corners[corner], face.Corners[corner + 1] );
			}

			Gizmo.Draw.Color = FaceFault;
			Gizmo.Draw.LineThickness = 3f;

			for ( var corner = 0; corner < face.Corners.Length; corner++ )
			{
				Gizmo.Draw.Line( face.Corners[corner], face.Corners[(corner + 1) % face.Corners.Length] );
			}
		}

		Gizmo.Draw.LineThickness = 2f;

		ArchGhost.Tag( debugFaces[0].Centre,
			debugFaces.Count == 1 ? debugFaces[0].Label : $"{debugFaces.Count} faces picked",
			FaceFault );
	}
}