Editor dock panel and viewport tool for mesh diagnostics in the Advanced Mapping Tools addon. DiagnosticsSession runs MeshDiagnostics.Analyze over the meshes of the current selection, stores the report, jumps the editor into the MeshTool/FaceTool to select a reported face, and applies MeshDiagnostics.Repair with undo scopes. DiagnosticsPanel lists the counts and issues, and MeshDiagnosticsTool draws red/yellow spheres at each issue that select the face when clicked.
namespace AdvancedMappingTools;
/// <summary>
/// The last check and what it was run on, shared by the panel and the viewport tool.
/// </summary>
public static class DiagnosticsSession
{
const int SelectFrames = 8;
static MeshFace _pendingFace;
static int _framesUntilSelect;
public static MeshReport Report { get; private set; }
public static List<MeshComponent> Checked { get; private set; } = [];
/// <summary>
/// Check the selected objects and everything inside them. Each mesh counts as an independent object.
/// </summary>
public static MeshReport CheckSelection()
{
var session = SceneEditorSession.Active;
var selected = session?.Selection.OfType<GameObject>().ToList() ?? [];
Checked = [.. selected
.SelectMany( x => x.Components.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants ) )
.Where( x => x.Mesh is not null )
.Distinct()];
Report = MeshDiagnostics.Analyze( [.. Checked.Select( x => (x.Mesh, x.WorldTransform) )] );
return Report;
}
/// <summary>
/// Select the face of an issue in the face tool, or the object if there is no face.
/// </summary>
public static void Select( MeshIssue issue )
{
var selection = SceneEditorSession.Active.Selection;
if ( issue.Owner < 0 || issue.Owner >= Checked.Count ) return;
var component = Checked[issue.Owner];
selection.Clear();
if ( !component.IsValid() ) return;
if ( !issue.Face.IsValid )
{
selection.Add( component.GameObject );
return;
}
// A tool only changes on the next frame and clears what is selected, so the face comes after it.
_pendingFace = new MeshFace( component, issue.Face );
_framesUntilSelect = SelectFrames;
EditorToolManager.SetTool( nameof( MeshTool ) );
EditorToolManager.SetSubTool( nameof( FaceTool ) );
}
[EditorEvent.Frame]
public static void Frame()
{
if ( _framesUntilSelect == 0 ) return;
// How many frames a tool change takes is not for this to know, so it holds on for a few.
_framesUntilSelect--;
var selection = SceneEditorSession.Active?.Selection;
if ( selection is null || selection.OfType<MeshFace>().Any() ) return;
selection.Clear();
selection.Add( _pendingFace );
}
/// <summary>
/// Repair what can be repaired without guessing, as one undo step.
/// </summary>
public static int RepairChecked()
{
var session = SceneEditorSession.Active;
var removed = 0;
foreach ( var component in Checked.Where( x => x.IsValid() ) )
{
using ( session.UndoScope( "Repair Mesh" ).WithComponentChanges( component ).Push() )
{
var count = MeshDiagnostics.Repair( component.Mesh );
if ( count == 0 ) continue;
removed += count;
component.RebuildMesh();
}
}
return removed;
}
}
/// <summary>
/// Lists what is wrong with the selected meshes: open edges, flipped bodies, faces lying on each
/// other, bodies cutting through each other. A click on an entry selects the face.
/// Open it from the View menu of the editor and dock it wherever you like.
/// </summary>
[Dock( "Editor", "Davelix Diagnostics", "troubleshoot" )]
public sealed class DiagnosticsPanel : Widget
{
const int MaxListed = 32;
readonly Sheet _sheet;
public DiagnosticsPanel( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 4;
var scroll = Layout.Add( new ScrollArea( this ), 1 );
_sheet = new Sheet( scroll );
scroll.Canvas = _sheet;
_sheet.Rebuild();
}
sealed class Sheet : ToolSidebarWidget
{
public Sheet( Widget parent ) : base( parent )
{
Layout.Margin = 0;
}
public void Rebuild()
{
Layout.Clear( true );
var actions = AddGroup( "Check" );
actions.Add( new ListButton( "Check Selection", "troubleshoot" )
{
ToolTip = "Checks the selected objects and everything inside them.",
Clicked = () =>
{
DiagnosticsSession.CheckSelection();
Rebuild();
}
} );
actions.Add( new ListButton( "Show In Viewport", "my_location" )
{
ToolTip = "Marks every issue in the viewport. Click a mark to select its face.",
Clicked = MeshDiagnosticsTool.Activate
} );
var report = DiagnosticsSession.Report;
if ( report is null )
{
Layout.AddStretchCell();
return;
}
var summary = AddGroup( "Result" );
summary.Add( new Label( $"{report.Faces} faces in {report.Shells} bodies, {report.ClosedShells} closed" ) );
summary.Add( new Label( $"{report.Errors} errors, {report.Warnings} warnings" ) );
summary.Add( new ListButton( "Repair", "healing" )
{
ToolTip = "Removes faces without area, doubled faces and loose vertices. Bodies that cross on purpose stay.",
Clicked = () =>
{
DiagnosticsSession.RepairChecked();
DiagnosticsSession.CheckSelection();
Rebuild();
}
} );
foreach ( var group in report.Issues.GroupBy( x => x.Kind ).OrderBy( x => x.Key ) )
{
var title = EditorTypeLibrary.GetEnumDescription( typeof( MeshIssueKind ) ).GetEntry( group.Key ).Title;
var list = AddGroup( $"{title} ({report.Count( group.Key )})", collapsible: true );
foreach ( var issue in group.Take( MaxListed ) )
{
list.Add( new ListButton( issue.Text )
{
ToolTip = $"{issue.Position}",
Clicked = () => DiagnosticsSession.Select( issue )
} );
}
}
Layout.AddStretchCell();
}
}
}
/// <summary>
/// Marks the issues of the last check in the viewport. A click on a mark selects its face.
/// </summary>
[EditorTool( "amt.diagnostics" )]
[Title( "Davelix Diagnostics" )]
[Icon( "troubleshoot" )]
public sealed class MeshDiagnosticsTool : EditorTool
{
const float MarkSize = 2.0f;
public override void OnEnabled() => AllowGameObjectSelection = false;
public static void Activate() => EditorToolManager.SetTool( nameof( MeshDiagnosticsTool ) );
public override void OnUpdate()
{
var report = DiagnosticsSession.Report;
if ( report is null ) return;
Gizmo.Draw.IgnoreDepth = true;
var index = 0;
foreach ( var issue in report.Issues.Where( x => x.Level != MeshIssueLevel.Info ) )
{
using ( Gizmo.Scope( $"issue {index++}" ) )
{
Gizmo.Hitbox.DepthBias = 0.01f;
Gizmo.Hitbox.Sphere( new Sphere( issue.Position, MarkSize * 2.0f ) );
Gizmo.Draw.Color = issue.Level == MeshIssueLevel.Error ? Color.Red : Color.Yellow;
Gizmo.Draw.SolidSphere( issue.Position, Gizmo.IsHovered ? MarkSize * 2.0f : MarkSize );
if ( Gizmo.IsHovered )
Gizmo.Draw.ScreenText( issue.Text, Gizmo.Camera.ToScreen( issue.Position ) + Vector2.Up * 12.0f );
if ( Gizmo.IsHovered && Gizmo.WasLeftMousePressed )
DiagnosticsSession.Select( issue );
}
}
}
}