Editor utility that inspects MeshComponent objects and produces a plaintext diagnostic report. It enumerates faces, collects material counts and texel scale values, checks per-face texture axes, scale and geometry for faults (NaN, collapsed axes, non-orthogonal axes, too few corners, or axes not in face plane) and formats human-readable lines for display.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using HalfEdgeMesh;
using Sandbox;
namespace Sunless.Architecture;
// What a scene MeshComponent actually carries, face by face - the numbers the mapping tool's Face Mode
// shows one face at a time. Read here because a texture axis that came out NaN is invisible in the
// viewport: the face renders, it just smears, and only its parameters say why.
public static partial class ArchReport
{
public static string MeshDebug( IEnumerable<MeshComponent> meshes )
{
var lines = new List<string> { "# Architecture mesh debug" };
var components = meshes?.Where( mesh => mesh.IsValid() ).ToList() ?? new List<MeshComponent>();
if ( components.Count == 0 )
{
lines.Add( "nothing selected carries a mesh - pick a game object with a MeshComponent on it or under it" );
return string.Join( "\n", lines );
}
foreach ( var component in components )
{
Describe( lines, component );
}
return string.Join( "\n", lines );
}
static void Describe( List<string> lines, MeshComponent component )
{
var mesh = component.Mesh;
var path = Path( component.GameObject );
if ( mesh is null )
{
lines.Add( $"\n## {path}\nno polygon mesh" );
return;
}
var faces = mesh.FaceHandles.ToList();
var transform = component.WorldTransform;
var broken = new List<string>();
var materials = new Dictionary<string, int>();
var scales = new HashSet<string>();
foreach ( var face in faces )
{
mesh.GetFaceTextureParameters( face, out var axisU, out var axisV, out var scale );
var material = mesh.GetFaceMaterial( face )?.ResourcePath ?? "none";
materials[material] = materials.TryGetValue( material, out var seen ) ? seen + 1 : 1;
scales.Add( $"{scale.x:0.####}x{scale.y:0.####}" );
if ( Fault( mesh, face, transform, axisU, axisV, scale ) is not { } fault )
{
continue;
}
broken.Add( $" {fault} at {transform.PointToWorld( mesh.GetFaceCenter( face ) )} · u {axisU} · v {axisV} · scale {scale} · {material}" );
}
lines.Add( $"\n## {path}" );
lines.Add( $"{faces.Count} faces · {mesh.VertexHandles.Count()} vertices · at {component.WorldPosition}" );
lines.Add( $"texel scales: {string.Join( ", ", scales.OrderBy( entry => entry ) )} (the density law is {ArchMesh.TexelScale:0.####})" );
foreach ( var material in materials.OrderByDescending( entry => entry.Value ) )
{
lines.Add( $" {material.Value,5} {material.Key}" );
}
if ( broken.Count == 0 )
{
lines.Add( "no faulted faces - every face has finite axes, a real normal and three or more distinct corners" );
return;
}
lines.Add( $"{broken.Count} FAULTED faces:" );
lines.AddRange( broken.Take( 60 ) );
if ( broken.Count > 60 )
{
lines.Add( $" ... and {broken.Count - 60} more" );
}
}
// Everything that makes a face render but read wrong. NaN axes come first because they are the one
// fault no screenshot shows: the face is there, the tiling is a smear, and the mapping tool's texture
// selection reads NaN.
static string Fault( PolygonMesh mesh, FaceHandle face, Transform transform, Vector4 axisU, Vector4 axisV, Vector2 scale )
{
if ( !Finite( axisU ) || !Finite( axisV ) || !float.IsFinite( scale.x ) || !float.IsFinite( scale.y ) )
{
return "NaN texture parameters";
}
if ( ((Vector3)axisU).IsNearZeroLength || ((Vector3)axisV).IsNearZeroLength )
{
return "collapsed texture axis";
}
if ( MathF.Abs( Vector3.Dot( ((Vector3)axisU).Normal, ((Vector3)axisV).Normal ) ) > 0.01f )
{
return "texture axes are not square to each other";
}
var corners = mesh.GetFaceVertexPositions( face, transform ).ToList();
if ( corners.Count < 3 )
{
return $"{corners.Count} corners";
}
var normal = ArchMesh.Newell( corners );
if ( MathF.Abs( Vector3.Dot( normal, ((Vector3)axisU).Normal ) ) > 0.01f )
{
return "texture axis is not in the face's own plane, so the projection stretches";
}
return null;
}
static bool Finite( Vector4 value )
{
return float.IsFinite( value.x ) && float.IsFinite( value.y ) && float.IsFinite( value.z ) && float.IsFinite( value.w );
}
static string Path( GameObject target )
{
var names = new List<string>();
for ( var at = target; at.IsValid(); at = at.Parent )
{
names.Add( at.Name );
}
names.Reverse();
return string.Join( "/", names );
}
}