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