DiamondPanel.GeometryCache.cs
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	// Only editor comparisons use the original geometry-building path.
	bool drawingReferenceGeometry = false;
	readonly OutlineGeometryCache settledOutlineGeometry = new();
	readonly ShapeGeometryCache activeShapeGeometry = new(), nextShapeGeometry = new(), ghostShapeGeometry = new();
	readonly ConditionalWeakTable<DiamondDebris.Particle, ParticleGlowGeometry> particleGlowGeometry = new();

	sealed class ShapeGeometryCache
	{
		DiamondBoard.Piece? previous;
		public readonly List<DiamondBoard.Diamond> Cells = new( 4 );
		public IReadOnlyList<IReadOnlyList<int>> Groups = [];
		public readonly OutlineGeometryCache Outlines = new();

		public void Update( DiamondBoard.Piece piece )
		{
			DiamondBoard.CopyCellsOf( piece, Cells );
			var key = piece with { Lane = 0, Y = 0 };
			if ( previous == key ) return;
			previous = key;
			Groups = DiamondMatching.FindGroups( Cells );
		}
	}

	sealed class ParticleGlowGeometry
	{
		public Vector2[] Vertices;
		public Vector2[][] Layers;
	}

	Vector2[][] ParticleGlowLayers( DiamondDebris.Particle particle )
	{
		if ( drawingReferenceGeometry ) return null;
		var cached = particleGlowGeometry.GetOrCreateValue( particle );
		// Vertices are immutable for a particle's lifetime. A replacement also invalidates hotloaded data.
		if ( cached.Vertices != particle.Vertices )
		{
			cached.Vertices = particle.Vertices;
			cached.Layers = BuildParticleGlowLayers( particle.Vertices, particle.IsSpark ? 1 : 1.25f );
		}
		return cached.Layers;
	}

	static Vector2[][] BuildParticleGlowLayers( ReadOnlySpan<Vector2> vertices, float spread )
	{
		Span<Vector2> corners = stackalloc Vector2[4];
		ParticleGlowCorners( vertices, corners );
		var layers = new Vector2[3][];
		for ( int layer = 3; layer >= 1; layer-- )
		{
			var points = layers[3 - layer] = new Vector2[vertices.Length];
			for ( int i = 0; i < points.Length; i++ ) points[i] = vertices[i] + corners[i] * (layer * spread);
		}
		return layers;
	}

	sealed class OutlineGeometry
	{
		public DiamondOutline.Edge FirstEdge;
		public (Vector2[] Points, bool Closed)[] Contours;
		public Vector2 Offset;
		public Vector2[][] Miters;
		Vector2[][] originalPoints;
		public void MoveTo( Vector2 offset )
		{
			if ( Offset == offset ) return;
			originalPoints ??= Contours.Select( c => c.Points.ToArray() ).ToArray();
			for ( int i = 0; i < Contours.Length; i++ )
			for ( int j = 0; j < Contours[i].Points.Length; j++ )
				Contours[i].Points[j] = originalPoints[i][j] + offset;
			Offset = offset;
		}
	}

	sealed class OutlineGeometryCache
	{
		DiamondBoard.Diamond[][] previousCells = [];
		int[][] previousGroups = [];
		public OutlineGeometry[] Geometry = [];

		public OutlineGeometry[] Get( IReadOnlyList<DiamondBoard.Diamond> cells, IReadOnlyList<IReadOnlyList<int>> groups )
		{
			// Hotload can discard a field whose type changed while retaining the other arrays.
			if ( groups.Count != Geometry?.Length || groups.Count != previousCells?.Length || groups.Count != previousGroups?.Length )
			{
				Array.Resize( ref Geometry, groups.Count );
				Array.Resize( ref previousCells, groups.Count );
				Array.Resize( ref previousGroups, groups.Count );
			}
			for ( int i = 0; i < groups.Count; i++ )
			{
				var group = groups[i];
				if ( group.Count == 0 ) { Geometry[i] = null; continue; }
				var saved = previousCells[i];
				bool same = Geometry[i] is not null && saved?.Length == group.Count;
				float dx = same ? cells[group[0]].Lane - saved[0].Lane : 0;
				float dy = same ? cells[group[0]].Y - saved[0].Y : 0;
				for ( int j = 0; same && j < group.Count; j++ )
				{
					var cell = cells[group[j]];
					// Health does not change the perimeter. Rigid motion reuses the existing vertex buffers.
					same = previousGroups[i][j] == group[j] && cell.ColorIndex == saved[j].ColorIndex &&
						MathF.Abs( cell.Lane - saved[j].Lane - dx ) < 0.00001f &&
						MathF.Abs( cell.Y - saved[j].Y - dy ) < 0.0001f;
				}
				if ( same ) Geometry[i].MoveTo( new Vector2( dx * DiamondBoard.Width * 0.5f, dy ) );
				else
				{
					previousGroups[i] = group.ToArray();
					previousCells[i] = new DiamondBoard.Diamond[group.Count];
					for ( int j = 0; j < group.Count; j++ ) previousCells[i][j] = cells[group[j]];
					Geometry[i] = BuildOutlineGeometry( cells, new IReadOnlyList<int>[] { group } )[0];
				}
			}
			return Geometry;
		}
	}

	static OutlineGeometry[] BuildOutlineGeometry( IReadOnlyList<DiamondBoard.Diamond> cells,
		IReadOnlyList<IReadOnlyList<int>> groups )
	{
		var result = new List<OutlineGeometry>();
		foreach ( var group in groups )
		{
			var edges = DiamondOutline.Build( cells, new IReadOnlyList<int>[] { group } );
			if ( edges.Count == 0 ) continue;
			var contours = DiamondOutline.Contours( edges ).Select( contour =>
			{
				bool closed = DiamondOutline.IsClosed( contour );
				var points = new Vector2[contour.Count + (closed ? 0 : 1)];
				for ( int i = 0; i < contour.Count; i++ ) points[i] = new Vector2( contour[i].X1, contour[i].Y1 );
				if ( !closed ) points[^1] = new Vector2( contour[^1].X2, contour[^1].Y2 );
				return (points, closed);
			} ).ToArray();
			result.Add( new OutlineGeometry { FirstEdge = edges[0], Contours = contours,
				Miters = contours.Select( c => BuildPerimeterMiters( c.points, c.closed ) ).ToArray() } );
		}
		return result.ToArray();
	}
}