DiamondOutline.cs
using System;
using System.Collections.Generic;
using static Diamonds.DiamondBoard;
namespace Diamonds;
/// <summary>Exposed perimeter edges of actual bonded groups, in board coordinates.</summary>
public static class DiamondOutline
{
public readonly record struct Edge( float X1, float Y1, float X2, float Y2, int ColorIndex );
/// <summary>Whether a traced perimeter can be drawn as a closed polygon.</summary>
public static bool IsClosed( IReadOnlyList<Edge> contour ) => contour.Count >= 3
&& MathF.Abs( contour[^1].X2 - contour[0].X1 ) <= 0.01f
&& MathF.Abs( contour[^1].Y2 - contour[0].Y1 ) <= 0.01f;
/// <summary>Order one group's perimeter into contours, including holes.
/// Animated positions can leave open chains; use IsClosed before closing a path.
/// At a shared tip, follow the rightmost turn so touching loops stay separate.</summary>
public static List<List<Edge>> Contours( IReadOnlyList<Edge> edges )
{
var contours = new List<List<Edge>>();
var used = new bool[edges.Count];
for ( int start = 0; start < edges.Count; start++ )
{
if ( used[start] ) continue;
var contour = new List<Edge>();
int current = start;
while ( current >= 0 && !used[current] )
{
var edge = edges[current];
used[current] = true;
contour.Add( edge );
int next = -1;
float bestTurn = float.NegativeInfinity;
for ( int candidate = 0; candidate < edges.Count; candidate++ )
{
var other = edges[candidate];
if ( (used[candidate] && candidate != start) || other.ColorIndex != edge.ColorIndex ||
MathF.Abs( other.X1 - edge.X2 ) > 0.01f || MathF.Abs( other.Y1 - edge.Y2 ) > 0.01f ) continue;
float dx = edge.X2 - edge.X1, dy = edge.Y2 - edge.Y1;
float ox = other.X2 - other.X1, oy = other.Y2 - other.Y1;
float turn = MathF.Atan2( dx * oy - dy * ox, dx * ox + dy * oy );
if ( turn > bestTurn ) { next = candidate; bestTurn = turn; }
}
current = next;
}
contours.Add( contour );
}
return contours;
}
public static List<Edge> Build( IReadOnlyList<Diamond> cells, IReadOnlyList<IReadOnlyList<int>> groups )
{
var edges = new List<Edge>();
foreach ( var group in groups )
foreach ( int index in group )
{
var cell = cells[index];
int shared = 0;
foreach ( int otherIndex in group )
{
if ( index == otherIndex ) continue;
var other = cells[otherIndex];
if ( cell.ColorIndex != other.ColorIndex || !DiamondMatching.Touching( cell, other ) ) continue;
int side = other.Lane > cell.Lane ? (other.Y < cell.Y ? 0 : 1) : (other.Y > cell.Y ? 2 : 3);
shared |= 1 << side;
}
float x = LaneX( cell.Lane ), y = cell.Y;
float w = Width * 0.5f, h = Height * 0.5f;
if ( (shared & 1) == 0 ) edges.Add( new( x, y - h, x + w, y, cell.ColorIndex ) );
if ( (shared & 2) == 0 ) edges.Add( new( x + w, y, x, y + h, cell.ColorIndex ) );
if ( (shared & 4) == 0 ) edges.Add( new( x, y + h, x - w, y, cell.ColorIndex ) );
if ( (shared & 8) == 0 ) edges.Add( new( x - w, y, x, y - h, cell.ColorIndex ) );
}
return edges;
}
}