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();
}
}