editor/AdvancedMappingTools/Core/MeshDiagnostics.cs

Mesh diagnostics core for the AdvancedMappingTools editor package. Walks a PolygonMesh or a PartPlan's parts and reports open edges, unwelded seams, flipped neighbours, inverted shells, degenerate or non planer faces, coplanar/back to back overlaps, self and shell intersections, buried faces, unused vertices and faces the engine rejected. Also has a Repair pass that removes duplicate, degenerate and engine-rejected faces plus loose vertices.

namespace AdvancedMappingTools;

public enum MeshIssueKind
{
	[Title( "Open Edge" )] OpenEdge,
	[Title( "Unwelded Seam" )] UnweldedSeam,
	[Title( "Flipped Neighbour" )] FlippedNeighbour,
	[Title( "Inverted Shell" )] InvertedShell,
	[Title( "Degenerate Face" )] DegenerateFace,
	[Title( "Bad Triangulation" )] BadTriangulation,
	[Title( "Non Planar Face" )] NonPlanarFace,
	[Title( "Duplicate Face" )] DuplicateFace,
	[Title( "Coplanar Overlap" )] CoplanarOverlap,
	[Title( "Back To Back Faces" )] BackToBackFaces,
	[Title( "Buried Face" )] BuriedFace,
	[Title( "Self Intersection" )] SelfIntersection,
	[Title( "Shell Intersection" )] ShellIntersection,
	[Title( "Object Intersection" )] ObjectIntersection,
	[Title( "Unused Vertex" )] UnusedVertex,
	[Title( "Engine Bad Face" )] EngineBadFace
}

public enum MeshIssueLevel
{
	Info,
	Warning,
	Error
}

/// <summary>
/// One thing found in a mesh and where it is. <see cref="Owner"/> is the index of the mesh it
/// belongs to when several were checked together.
/// </summary>
public readonly record struct MeshIssue( MeshIssueKind Kind, MeshIssueLevel Level, string Text, Vector3 Position, int Owner, HalfEdgeMesh.FaceHandle Face );

public sealed class MeshReport
{
	const int MaxStored = 64;

	readonly Dictionary<MeshIssueKind, int> _counts = [];

	public List<MeshIssue> Issues { get; } = [];
	public int Faces { get; set; }
	public int Triangles { get; set; }
	public int Shells { get; set; }
	public int ClosedShells { get; set; }

	public int Count( MeshIssueKind kind ) => _counts.TryGetValue( kind, out var count ) ? count : 0;

	public int Errors { get; private set; }
	public int Warnings { get; private set; }

	public bool IsClean => Errors == 0;

	public void Add( MeshIssue issue )
	{
		_counts[issue.Kind] = Count( issue.Kind ) + 1;

		if ( issue.Level == MeshIssueLevel.Error ) Errors++;
		if ( issue.Level == MeshIssueLevel.Warning ) Warnings++;

		// Per kind, so a few real faults are not lost behind hundreds of notes.
		if ( Count( issue.Kind ) <= MaxStored ) Issues.Add( issue );
	}

	public string Summary()
	{
		var parts = _counts.OrderBy( x => x.Key ).Select( x => $"{x.Key}={x.Value}" );
		return $"faces={Faces} tris={Triangles} shells={Shells} closed={ClosedShells} errors={Errors} warnings={Warnings} {string.Join( " ", parts )}".TrimEnd();
	}
}

/// <summary>
/// Checks meshes for what makes geometry unreliable: open and misconnected edges, flipped
/// shells, broken triangulation, faces lying on top of each other and bodies cutting through
/// one another. It only reads and it uses the triangles the engine itself makes.
/// Meshes checked together count as independent objects: where they touch or cross, that is
/// reported apart from the same thing happening inside one mesh.
/// </summary>
public static class MeshDiagnostics
{
	const float Weld = 0.02f;
	const float PlaneDistance = 0.02f;
	const float FacePlaneDistance = 0.06f;
	const float MinArea = 0.0005f;
	const float MinOverlap = 0.02f;
	const float SmallestStrip = 0.08f;
	const float Penetration = 0.03f;
	const float ParallelDot = 0.9995f;
	const float PlanarLimit = 0.05f;

	sealed class Tri
	{
		public Vector3 A, B, C, Normal, Mins, Maxs;
		public int Face;
	}

	sealed class Item
	{
		public HalfEdgeMesh.FaceHandle Handle;
		public int Owner, Shell;
		public int[] Ring;
		public Vector3[] Points;
		public Vector3 Normal, Center, Mins, Maxs;
		public float Area;
		public readonly List<Tri> Tris = [];
	}

	public static MeshReport Analyze( PolygonMesh mesh ) => Analyze( [(mesh, new Transform( Vector3.Zero, Rotation.Identity ))] );

	/// <summary>
	/// Take out what is wrong beyond doubt: faces without area, faces that are there twice and
	/// vertices nothing uses. Bodies that cross each other stay, that can be on purpose.
	/// Gives back how much was removed.
	/// </summary>
	public static int Repair( PolygonMesh mesh )
	{
		var removed = 0;

		for ( var pass = 0; pass < 32; pass++ )
		{
			var report = Analyze( mesh );
			var faces = report.Issues
				.Where( x => x.Kind is MeshIssueKind.DuplicateFace or MeshIssueKind.DegenerateFace or MeshIssueKind.EngineBadFace )
				.Select( x => x.Face )
				.Distinct()
				.ToList();

			if ( faces.Count == 0 ) break;

			mesh.RemoveFaces( faces );
			removed += faces.Count;
		}

		var loose = mesh.VertexHandles.Where( x => mesh.GetFacesConnectedToVertex( x, out var faces ) is var found && (!found || faces is null || faces.Count == 0) ).ToList();

		if ( loose.Count > 0 ) mesh.RemoveVertices( loose );

		return removed + loose.Count;
	}

	/// <summary>
	/// Check every part of a plan as an independent object.
	/// </summary>
	public static MeshReport Analyze( PartPlan plan )
	{
		var meshes = new List<(PolygonMesh, Transform)>();

		void Visit( PartPlan part, Transform parent )
		{
			var world = parent.ToWorld( part.Local );

			if ( part.Mesh is not null ) meshes.Add( (part.Mesh, world) );

			foreach ( var child in part.Children )
				Visit( child, world );
		}

		Visit( plan, new Transform( Vector3.Zero, Rotation.Identity ) );

		return Analyze( meshes );
	}

	/// <summary>
	/// Check meshes together, each with its transform into a shared space.
	/// </summary>
	public static MeshReport Analyze( IReadOnlyList<(PolygonMesh Mesh, Transform World)> meshes )
	{
		var report = new MeshReport();
		var welded = new Dictionary<(int, int, int), int>();
		var items = new List<Item>();
		var shellOffset = 0;
		var open = new List<(int Owner, HalfEdgeMesh.FaceHandle Face, int From, int To, Vector3 A, Vector3 B, int Shell)>();

		int WeldId( Vector3 p )
		{
			var key = ((int)MathF.Round( p.x / Weld ), (int)MathF.Round( p.y / Weld ), (int)MathF.Round( p.z / Weld ));

			if ( !welded.TryGetValue( key, out var id ) )
				welded[key] = id = welded.Count;

			return id;
		}

		for ( var owner = 0; owner < meshes.Count; owner++ )
		{
			var (mesh, world) = meshes[owner];
			var handles = mesh.FaceHandles.ToArray();

			// The triangles of a face only exist once the mesh was built.
			if ( handles.Length > 0 && mesh.CreateFace( handles[0], world, Color.White ) is null )
				mesh.Rebuild();

			var parent = new Dictionary<int, int>();

			int Find( int x )
			{
				while ( parent[x] != x ) x = parent[x] = parent[parent[x]];
				return x;
			}

			void Join( int a, int b ) => parent[Find( a )] = Find( b );

			var first = items.Count;
			var directed = new Dictionary<(int, int), HalfEdgeMesh.FaceHandle>();
			var edgePoints = new Dictionary<(int, int), (Vector3, Vector3)>();
			var used = new HashSet<int>();

			foreach ( var handle in handles )
			{
				var ring = mesh.GetFaceVertices( handle );
				var points = ring.Select( x => world.PointToWorld( mesh.GetVertexPosition( x ) ) ).ToArray();
				var item = new Item { Handle = handle, Owner = owner, Points = points, Ring = [.. points.Select( WeldId )] };

				var normal = Vector3.Zero;
				for ( var i = 0; i < points.Length; i++ )
					normal += Vector3.Cross( points[i], points[(i + 1) % points.Length] );

				item.Area = normal.Length * 0.5f;
				item.Normal = item.Area > 0.0f ? normal.Normal : Vector3.Zero;
				item.Center = points.Aggregate( Vector3.Zero, ( a, b ) => a + b ) / points.Length;
				item.Mins = points.Aggregate( points[0], Low );
				item.Maxs = points.Aggregate( points[0], High );

				for ( var i = 0; i < ring.Length; i++ )
				{
					var a = ring[i].Index;
					var b = ring[(i + 1) % ring.Length].Index;

					used.Add( a );

					if ( !parent.ContainsKey( a ) ) parent[a] = a;
					if ( !parent.ContainsKey( b ) ) parent[b] = b;

					Join( a, b );

					directed[(a, b)] = handle;
					edgePoints[(a, b)] = (points[i], points[(i + 1) % ring.Length]);
				}

				AddTriangles( mesh, handle, world, item, report );
				items.Add( item );
			}

			foreach ( var ((a, b), face) in directed )
			{
				if ( directed.ContainsKey( (b, a) ) ) continue;

				var (pa, pb) = edgePoints[(a, b)];
				open.Add( (owner, face, WeldId( pa ), WeldId( pb ), pa, pb, Find( a )) );
			}

			var shells = new Dictionary<int, int>();

			for ( var i = first; i < items.Count; i++ )
			{
				var root = Find( mesh.GetFaceVertices( items[i].Handle )[0].Index );

				if ( !shells.TryGetValue( root, out var shell ) )
					shells[root] = shell = shellOffset + shells.Count;

				items[i].Shell = shell;
			}

			var openShells = open.Where( x => x.Owner == owner ).Select( x => shells[x.Shell] ).ToHashSet();

			foreach ( var group in items.Skip( first ).GroupBy( x => x.Shell ) )
			{
				report.Shells++;

				if ( openShells.Contains( group.Key ) ) continue;

				report.ClosedShells++;

				// Measured from the body itself: far from the origin float rounding would outweigh a thin body.
				var anchor = group.First().Center;
				var volume = group.SelectMany( x => x.Tris ).Sum( t => Vector3.Dot( t.A - anchor, Vector3.Cross( t.B - anchor, t.C - anchor ) ) / 6.0f );

				if ( volume < 0.0f )
					report.Add( new MeshIssue( MeshIssueKind.InvertedShell, MeshIssueLevel.Error, $"A closed body faces inward, volume {volume:0}.", group.First().Center, owner, group.First().Handle ) );
			}

			shellOffset += shells.Count;

			foreach ( var vertex in mesh.VertexHandles )
			{
				if ( !used.Contains( vertex.Index ) )
					report.Add( new MeshIssue( MeshIssueKind.UnusedVertex, MeshIssueLevel.Warning, "A vertex belongs to no face.", world.PointToWorld( mesh.GetVertexPosition( vertex ) ), owner, default ) );
			}

			foreach ( var face in mesh.BadFaces )
				report.Add( new MeshIssue( MeshIssueKind.EngineBadFace, MeshIssueLevel.Error, "The engine rejected this face.", Vector3.Zero, owner, face ) );
		}

		report.Faces = items.Count;
		report.Triangles = items.Sum( x => x.Tris.Count );

		CheckOpenEdges( open, report );
		CheckFaces( items, report );
		CheckOverlaps( items, report );
		CheckCrossing( items, report );
		CheckBuried( items, report );

		return report;
	}

	static void AddTriangles( PolygonMesh mesh, HalfEdgeMesh.FaceHandle handle, Transform world, Item item, MeshReport report )
	{
		var vertices = mesh.CreateFace( handle, world, Color.White );

		if ( vertices is null )
		{
			report.Add( new MeshIssue( MeshIssueKind.BadTriangulation, MeshIssueLevel.Error, "The engine made no triangles for this face.", item.Center, item.Owner, handle ) );
			return;
		}

		var area = 0.0f;
		var flipped = false;

		for ( var i = 0; i + 2 < vertices.Length; i += 3 )
		{
			var a = vertices[i].Position;
			var b = vertices[i + 1].Position;
			var c = vertices[i + 2].Position;
			var cross = Vector3.Cross( b - a, c - a );
			var size = cross.Length * 0.5f;

			area += size;

			if ( size > MinArea && cross.Normal.Dot( item.Normal ) < 0.0f ) flipped = true;

			item.Tris.Add( new Tri
			{
				A = a, B = b, C = c, Face = -1, Normal = size > 0.0f ? cross.Normal : Vector3.Zero,
				Mins = Low( Low( a, b ), c ),
				Maxs = High( High( a, b ), c )
			} );
		}

		// On a warped face the triangles cover more than its outline by nature.
		var planar = item.Points.Max( p => MathF.Abs( (p - item.Center).Dot( item.Normal ) ) ) <= PlanarLimit;

		if ( item.Area > MinArea && (flipped || (planar && MathF.Abs( area - item.Area ) > MathF.Max( item.Area * 0.02f, 0.05f ))) )
		{
			report.Add( new MeshIssue( MeshIssueKind.BadTriangulation, MeshIssueLevel.Error,
				$"Triangles cover {area:0.0} of a face with {item.Area:0.0} units of area.", item.Center, item.Owner, handle ) );
		}
	}

	static void CheckOpenEdges( List<(int Owner, HalfEdgeMesh.FaceHandle Face, int From, int To, Vector3 A, Vector3 B, int Shell)> open, MeshReport report )
	{
		var lookup = open.ToLookup( x => (x.Owner, x.From, x.To) );

		foreach ( var edge in open )
		{
			var middle = (edge.A + edge.B) * 0.5f;

			if ( lookup[(edge.Owner, edge.To, edge.From)].Any() )
				report.Add( new MeshIssue( MeshIssueKind.UnweldedSeam, MeshIssueLevel.Error, "Two edges lie on each other but are not connected.", middle, edge.Owner, edge.Face ) );
			else if ( lookup[(edge.Owner, edge.From, edge.To)].Count() > 1 )
				report.Add( new MeshIssue( MeshIssueKind.FlippedNeighbour, MeshIssueLevel.Error, "Two faces run along an edge in the same direction.", middle, edge.Owner, edge.Face ) );
			else
				report.Add( new MeshIssue( MeshIssueKind.OpenEdge, MeshIssueLevel.Error, "This edge has a face on one side only.", middle, edge.Owner, edge.Face ) );
		}
	}

	static void CheckFaces( List<Item> items, MeshReport report )
	{
		foreach ( var item in items )
		{
			if ( item.Area < MinArea )
			{
				report.Add( new MeshIssue( MeshIssueKind.DegenerateFace, MeshIssueLevel.Error, "This face has no area.", item.Center, item.Owner, item.Handle ) );
				continue;
			}

			var off = item.Points.Max( p => MathF.Abs( (p - item.Center).Dot( item.Normal ) ) );

			if ( off > PlanarLimit )
				report.Add( new MeshIssue( MeshIssueKind.NonPlanarFace, MeshIssueLevel.Info, $"Corners leave the plane of this face by {off:0.00}.", item.Center, item.Owner, item.Handle ) );
		}
	}

	static Vector3 Low( Vector3 a, Vector3 b ) => new( MathF.Min( a.x, b.x ), MathF.Min( a.y, b.y ), MathF.Min( a.z, b.z ) );

	static Vector3 High( Vector3 a, Vector3 b ) => new( MathF.Max( a.x, b.x ), MathF.Max( a.y, b.y ), MathF.Max( a.z, b.z ) );

	static float Axis( Vector3 v, int axis ) => axis == 0 ? v.x : axis == 1 ? v.y : v.z;

	/// <summary>
	/// All pairs whose bounds touch, sorted along X so most pairs are never looked at.
	/// </summary>
	static IEnumerable<(T A, T B)> Pairs<T>( IReadOnlyList<T> list, Func<T, Vector3> mins, Func<T, Vector3> maxs, float margin )
	{
		var sorted = list.OrderBy( x => mins( x ).x ).ToArray();

		for ( var i = 0; i < sorted.Length; i++ )
		{
			var a = sorted[i];
			var amin = mins( a );
			var amax = maxs( a );

			for ( var j = i + 1; j < sorted.Length; j++ )
			{
				var b = sorted[j];
				var bmin = mins( b );

				if ( bmin.x > amax.x + margin ) break;

				var bmax = maxs( b );

				if ( bmin.y > amax.y + margin || bmax.y < amin.y - margin ) continue;
				if ( bmin.z > amax.z + margin || bmax.z < amin.z - margin ) continue;

				yield return (a, b);
			}
		}
	}

	static void CheckOverlaps( List<Item> items, MeshReport report )
	{
		var flat = items.Where( x => x.Area >= MinArea ).ToList();

		foreach ( var (a, b) in Pairs( flat, x => x.Mins, x => x.Maxs, PlaneDistance ) )
		{
			var dot = a.Normal.Dot( b.Normal );

			if ( MathF.Abs( dot ) < ParallelDot ) continue;
			if ( MathF.Abs( (b.Center - a.Center).Dot( a.Normal ) ) > PlaneDistance ) continue;

			// The middle of a face can touch the plane of another by chance, where a surface folds.
			// In one place means all of it, so the smaller face has to lie in that plane as a whole.
			var (large, small) = a.Area >= b.Area ? (a, b) : (b, a);

			if ( small.Points.Any( x => MathF.Abs( (x - large.Center).Dot( large.Normal ) ) > FacePlaneDistance ) ) continue;

			var overlap = OverlapArea( a, b );
			if ( overlap < MinOverlap ) continue;

			// A strip thinner than this covers less than a pixel from any distance worth looking from.
			var reach = MathF.Min( (a.Maxs - a.Mins).Length, (b.Maxs - b.Mins).Length );

			if ( overlap / MathF.Max( reach, 0.01f ) < SmallestStrip ) continue;

			var position = (a.Center + b.Center) * 0.5f;
			var same = a.Ring.Length == b.Ring.Length && a.Ring.Distinct().OrderBy( x => x ).SequenceEqual( b.Ring.Distinct().OrderBy( x => x ) );

			if ( a.Owner != b.Owner )
			{
				// Independent objects resting on each other is normal, two surfaces fighting for the same pixels is not.
				if ( dot > 0.0f )
					report.Add( new MeshIssue( MeshIssueKind.CoplanarOverlap, MeshIssueLevel.Warning, $"Two objects show a surface in the same place, {overlap:0.0} units of area.", position, a.Owner, a.Handle ) );

				continue;
			}

			if ( dot > 0.0f )
				report.Add( new MeshIssue( same ? MeshIssueKind.DuplicateFace : MeshIssueKind.CoplanarOverlap, MeshIssueLevel.Error, $"Two faces of one mesh lie on each other, {overlap:0.0} units of area.", position, a.Owner, a.Handle ) );
			else
				report.Add( new MeshIssue( MeshIssueKind.BackToBackFaces, MeshIssueLevel.Warning, $"Two faces of one mesh touch back to back inside it, {overlap:0.0} units of area.", position, a.Owner, a.Handle ) );
		}
	}

	static float OverlapArea( Item a, Item b )
	{
		var absolute = new Vector3( MathF.Abs( a.Normal.x ), MathF.Abs( a.Normal.y ), MathF.Abs( a.Normal.z ) );
		var drop = absolute.x > absolute.y ? (absolute.x > absolute.z ? 0 : 2) : (absolute.y > absolute.z ? 1 : 2);
		var u = drop == 0 ? 1 : 0;
		var v = drop == 2 ? 1 : 2;

		Vector2 Flat( Vector3 p ) => new( Axis( p, u ), Axis( p, v ) );

		var total = 0.0f;

		foreach ( var s in a.Tris )
		{
			foreach ( var t in b.Tris )
				total += ClippedArea( [Flat( s.A ), Flat( s.B ), Flat( s.C )], [Flat( t.A ), Flat( t.B ), Flat( t.C )] );
		}

		return total;
	}

	/// <summary>
	/// The area two triangles share, by cutting one along the edges of the other.
	/// </summary>
	static float ClippedArea( Vector2[] subject, Vector2[] clip )
	{
		var signed = (clip[1].x - clip[0].x) * (clip[2].y - clip[0].y) - (clip[1].y - clip[0].y) * (clip[2].x - clip[0].x);
		if ( MathF.Abs( signed ) < 1e-6f ) return 0.0f;

		var sign = signed > 0.0f ? 1.0f : -1.0f;
		var output = new List<Vector2>( subject );

		for ( var e = 0; e < 3 && output.Count > 0; e++ )
		{
			var p = clip[e];
			var q = clip[(e + 1) % 3];
			var input = output;

			output = [];

			float Side( Vector2 x ) => sign * ((q.x - p.x) * (x.y - p.y) - (q.y - p.y) * (x.x - p.x));

			for ( var i = 0; i < input.Count; i++ )
			{
				var a = input[i];
				var b = input[(i + 1) % input.Count];
				var sa = Side( a );
				var sb = Side( b );

				if ( sa >= 0.0f ) output.Add( a );
				if ( (sa >= 0.0f) != (sb >= 0.0f) ) output.Add( a + (b - a) * (sa / (sa - sb)) );
			}
		}

		var area = 0.0f;

		for ( var i = 0; i < output.Count; i++ )
		{
			var a = output[i];
			var b = output[(i + 1) % output.Count];
			area += a.x * b.y - b.x * a.y;
		}

		return MathF.Abs( area ) * 0.5f;
	}

	static void CheckCrossing( List<Item> items, MeshReport report )
	{
		var tris = new List<Tri>();

		for ( var i = 0; i < items.Count; i++ )
		{
			if ( items[i].Area < MinArea ) continue;

			foreach ( var tri in items[i].Tris )
			{
				tri.Face = i;
				tris.Add( tri );
			}
		}

		var seen = new HashSet<(int, int)>();

		foreach ( var (s, t) in Pairs( tris, x => x.Mins, x => x.Maxs, 0.0f ) )
		{
			if ( s.Face == t.Face ) continue;

			var a = items[s.Face];
			var b = items[t.Face];

			if ( a.Ring.Intersect( b.Ring ).Any() ) continue;

			var key = s.Face < t.Face ? (s.Face, t.Face) : (t.Face, s.Face);

			if ( seen.Contains( key ) || !Crosses( s, t ) ) continue;

			seen.Add( key );

			var position = (a.Center + b.Center) * 0.5f;

			// Independent objects may cross on purpose, so this is only ever a note.
			if ( a.Owner != b.Owner )
			{
				report.Add( new MeshIssue( MeshIssueKind.ObjectIntersection, MeshIssueLevel.Info, "Two independent objects cut through each other.", position, a.Owner, a.Handle ) );
				continue;
			}

			if ( a.Shell == b.Shell )
				report.Add( new MeshIssue( MeshIssueKind.SelfIntersection, MeshIssueLevel.Error, $"A body cuts through itself, between {a.Center:0} and {b.Center:0}.", position, a.Owner, a.Handle ) );
			else
				report.Add( new MeshIssue( MeshIssueKind.ShellIntersection, MeshIssueLevel.Warning, "Two bodies in one mesh cut through each other.", position, a.Owner, a.Handle ) );
		}
	}

	static bool Crosses( Tri a, Tri b )
	{
		if ( a.Normal == Vector3.Zero || b.Normal == Vector3.Zero ) return false;

		var da = new[] { (b.A - a.A).Dot( a.Normal ), (b.B - a.A).Dot( a.Normal ), (b.C - a.A).Dot( a.Normal ) };
		if ( da.Min() >= -Penetration || da.Max() <= Penetration ) return false;

		var db = new[] { (a.A - b.A).Dot( b.Normal ), (a.B - b.A).Dot( b.Normal ), (a.C - b.A).Dot( b.Normal ) };
		if ( db.Min() >= -Penetration || db.Max() <= Penetration ) return false;

		var line = Vector3.Cross( a.Normal, b.Normal );
		if ( line.Length < 1e-4f ) return false;

		var (a0, a1) = Interval( [a.A, a.B, a.C], db, line );
		var (b0, b1) = Interval( [b.A, b.B, b.C], da, line );

		return MathF.Min( a1, b1 ) - MathF.Max( a0, b0 ) > Penetration;
	}

	static (float, float) Interval( Vector3[] points, float[] distances, Vector3 line )
	{
		var low = float.MaxValue;
		var high = float.MinValue;

		for ( var i = 0; i < 3; i++ )
		{
			var j = (i + 1) % 3;

			if ( distances[i] > 0.0f == distances[j] > 0.0f ) continue;

			var p = points[i] + (points[j] - points[i]) * (distances[i] / (distances[i] - distances[j]));
			var t = p.Dot( line );

			low = MathF.Min( low, t );
			high = MathF.Max( high, t );
		}

		return (low, high);
	}

	/// <summary>
	/// Faces of one mesh that lie inside another closed body of that mesh, where nobody can ever see them.
	/// </summary>
	static void CheckBuried( List<Item> items, MeshReport report )
	{
		var direction = new Vector3( 0.5773f, 0.3827f, 0.7214f ).Normal;
		var shells = items.GroupBy( x => x.Shell ).Select( g => (Shell: g.Key, Owner: g.First().Owner, Faces: g.ToArray(),
			Mins: g.Select( x => x.Mins ).Aggregate( Low ), Maxs: g.Select( x => x.Maxs ).Aggregate( High )) ).ToArray();

		foreach ( var item in items )
		{
			if ( item.Area < MinArea ) continue;

			foreach ( var shell in shells )
			{
				if ( shell.Shell == item.Shell || shell.Owner != item.Owner ) continue;

				var p = item.Center;

				if ( p.x <= shell.Mins.x || p.y <= shell.Mins.y || p.z <= shell.Mins.z ) continue;
				if ( p.x >= shell.Maxs.x || p.y >= shell.Maxs.y || p.z >= shell.Maxs.z ) continue;

				var hits = 0;

				foreach ( var face in shell.Faces )
				{
					foreach ( var tri in face.Tris )
					{
						if ( RayHits( p, direction, tri ) ) hits++;
					}
				}

				if ( hits % 2 == 0 ) continue;

				report.Add( new MeshIssue( MeshIssueKind.BuriedFace, MeshIssueLevel.Warning, "This face lies inside another body of the same mesh.", p, item.Owner, item.Handle ) );
				break;
			}
		}
	}

	static bool RayHits( Vector3 origin, Vector3 direction, Tri tri )
	{
		var e1 = tri.B - tri.A;
		var e2 = tri.C - tri.A;
		var p = Vector3.Cross( direction, e2 );
		var det = e1.Dot( p );

		if ( MathF.Abs( det ) < 1e-8f ) return false;

		var inverse = 1.0f / det;
		var s = origin - tri.A;
		var u = s.Dot( p ) * inverse;

		if ( u < 0.0f || u > 1.0f ) return false;

		var q = Vector3.Cross( s, e1 );
		var v = direction.Dot( q ) * inverse;

		return v >= 0.0f && u + v <= 1.0f && e2.Dot( q ) * inverse > 0.0f;
	}
}