Editor/Tool/ArchToolFaces.cs
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 );
}
}