Editor/Effigy/MeshAdjacency.cs
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// Vertex-to-face adjacency in compressed sparse row form: one flat array of face indices, and an
/// offset per vertex saying where its run starts.
///
/// WHY THIS EXISTS ALONGSIDE <see cref="PolyMesh.BuildVertexFaces"/>, which answers the same
/// question. That one returns a <c>List<int>[]</c>, so a mesh with 240k vertices allocates
/// 240k List objects and 240k backing arrays to hold four ints each - about 90 MB of garbage for
/// 4 MB of answer, and the GC pause that goes with it lands in the middle of a viewport rebuild.
/// Two flat arrays hold the same data with two allocations.
///
/// The list version is kept because a dozen callers use it and most of them run once on a shape
/// small enough not to care. This one is for the paths that run per rebuild or per stroke.
///
/// NOT A HALF-EDGE STRUCTURE, and for the reason PolyMesh gives: this is derived on demand and
/// thrown away, so it cannot go stale. Build it, use it, drop it.
/// </summary>
public sealed class VertexFaces
{
/// <summary>Where vertex v's run starts. Length is VertexCount + 1, so a run is
/// Offsets[v]..Offsets[v+1] and the last entry needs no special case.</summary>
public readonly int[] Offsets;
/// <summary>Face indices, grouped by vertex.</summary>
public readonly int[] Items;
VertexFaces( int[] offsets, int[] items )
{
Offsets = offsets;
Items = items;
}
public int VertexCount => Offsets.Length - 1;
/// <summary>How many faces touch this vertex. Its valence, for a manifold interior vertex.</summary>
public int CountAt( int vertex ) => Offsets[vertex + 1] - Offsets[vertex];
/// <summary>The faces touching a vertex, as a view into <see cref="Items"/>. No allocation.</summary>
public ReadOnlySpan<int> this[int vertex] =>
new( Items, Offsets[vertex], Offsets[vertex + 1] - Offsets[vertex] );
/// <summary>
/// Counting sort in two passes: count each vertex's faces, prefix-sum that into offsets, then
/// scatter. Linear, and allocates exactly the two arrays it returns.
/// </summary>
public static VertexFaces Build( PolyMesh mesh )
{
if ( mesh is null )
throw new ArgumentNullException( nameof( mesh ) );
var vertexCount = mesh.Positions.Count;
var offsets = new int[vertexCount + 1];
var faces = mesh.Faces;
// Pass one: how many faces per vertex, counted into offsets[v + 1] so the prefix sum
// below turns the counts into starts in place.
for ( var fi = 0; fi < faces.Count; fi++ )
{
var indices = faces[fi].Indices;
for ( var i = 0; i < indices.Length; i++ )
{
// A malformed face can list the same vertex twice, and counting it twice would
// inflate the valence - which is what skews the Catmull-Clark vertex rule and the
// area-weighted normal. BuildVertexFaces spends a LINQ Distinct() per face on
// this; a scan of the corners before this one is the same answer without the
// enumerator, and faces have three or four corners.
if ( Repeats( indices, i ) )
continue;
offsets[indices[i] + 1]++;
}
}
for ( var v = 0; v < vertexCount; v++ )
offsets[v + 1] += offsets[v];
// Pass two: scatter. cursor walks a copy of the starts so offsets survives intact.
var items = new int[offsets[vertexCount]];
var cursor = new int[vertexCount];
Array.Copy( offsets, cursor, vertexCount );
for ( var fi = 0; fi < faces.Count; fi++ )
{
var indices = faces[fi].Indices;
for ( var i = 0; i < indices.Length; i++ )
{
if ( Repeats( indices, i ) )
continue;
items[cursor[indices[i]]++] = fi;
}
}
return new VertexFaces( offsets, items );
}
/// <summary>Whether this corner's vertex already appeared earlier in the same face.</summary>
static bool Repeats( int[] indices, int corner )
{
var v = indices[corner];
for ( var j = 0; j < corner; j++ )
{
if ( indices[j] == v )
return true;
}
return false;
}
}