DiamondDebris.Overlap.cs
using System;
using Sandbox;
using static Diamonds.DiamondBoard;
namespace Diamonds;
public sealed partial class DiamondDebris
{
static bool NearDiamond( Vector2 position, float radius, Vector2 center ) =>
MathF.Abs( position.x - center.x ) < Width / 2 + radius &&
MathF.Abs( position.y - center.y ) < Height / 2 + radius;
static void TransformShard( Particle shard, Span<Vector2> vertices )
=> TransformShard( shard, shard.Body.Transform, vertices );
static void TransformShard( Particle shard, Transform transform, Span<Vector2> vertices )
{
var position = transform.Position;
var rotation = transform.Rotation;
for ( int i = 0; i < vertices.Length; i++ )
{
var vertex = shard.Vertices[i];
var point = position + rotation * new Vector3( vertex.x, vertex.y, 0 );
vertices[i] = new( point.x, point.y );
}
}
bool ClearOfDiamonds( ReadOnlySpan<Vector2> vertices, Vector2 position, float radius )
{
foreach ( var diamond in crushDiamonds )
{
if ( !NearDiamond( position, radius, diamond.Center ) ) continue;
if ( Separation( vertices, diamond.Center ).Length > 0.55f ) return false;
}
return true;
}
bool RestoreClearShardPose( Particle shard, Span<Vector2> vertices )
{
if ( !shard.HasClearTransform ) return false;
var position = shard.LastClearTransform.Position;
TransformShard( shard, shard.LastClearTransform, vertices );
if ( !ClearOfDiamonds( vertices, new Vector2( position.x, position.y ), shard.Radius ) ) return false;
shard.Body.Transform = shard.LastClearTransform;
// Discard the solver's squeeze impulse, otherwise it repeats next step.
// Diamond containment takes precedence over transient shard overlap.
shard.Body.Velocity = Vector3.Zero;
shard.Body.AngularVelocity = Vector3.Zero;
shard.Body.Sleeping = false;
return true;
}
static float PolygonArea( ReadOnlySpan<Vector2> vertices )
{
float area = 0;
for ( int i = 1; i + 1 < vertices.Length; i++ )
{
var a = vertices[i] - vertices[0];
var b = vertices[i + 1] - vertices[0];
area += a.x * b.y - a.y * b.x;
}
return MathF.Abs( area ) * 0.5f;
}
static float CoveredArea( ReadOnlySpan<Vector2> vertices, Vector2 center )
{
// Clip the actual rotated polygon against all four diamond half-planes.
// Fixed scratch buffers avoid allocations in this per-contact path.
Span<Vector2> input = stackalloc Vector2[12];
Span<Vector2> output = stackalloc Vector2[12];
vertices.CopyTo( input );
int count = vertices.Length;
for ( int face = 0; face < 4 && count > 0; face++ )
{
var normal = new Vector2( (face & 1) == 0 ? Height : -Height, (face & 2) == 0 ? Width : -Width );
int written = 0;
var previous = input[count - 1];
float previousDistance = Vector2.Dot( previous - center, normal ) - Width * Height / 2;
for ( int i = 0; i < count; i++ )
{
var current = input[i];
float distance = Vector2.Dot( current - center, normal ) - Width * Height / 2;
if ( (distance <= 0) != (previousDistance <= 0) )
output[written++] = previous + (current - previous) * (previousDistance / (previousDistance - distance));
if ( distance <= 0 ) output[written++] = current;
previous = current;
previousDistance = distance;
}
count = written;
var swap = input; input = output; output = swap;
}
return PolygonArea( input[..count] );
}
static Vector2 Separation( ReadOnlySpan<Vector2> vertices, Vector2 center )
{
float depth = float.PositiveInfinity;
Vector2 correction = default;
for ( int axis = 0; axis < vertices.Length + 2; axis++ )
{
Vector2 normal;
if ( axis < 2 ) normal = new Vector2( Height, axis == 0 ? Width : -Width ).Normal;
else
{
int edgeIndex = axis - 2;
var edge = vertices[(edgeIndex + 1) % vertices.Length] - vertices[edgeIndex];
normal = new Vector2( edge.y, -edge.x ).Normal;
}
float extent = MathF.Max( MathF.Abs( normal.x ) * Width / 2, MathF.Abs( normal.y ) * Height / 2 );
float min = float.PositiveInfinity, max = float.NegativeInfinity;
foreach ( var vertex in vertices )
{
float projection = Vector2.Dot( vertex - center, normal );
min = MathF.Min( min, projection ); max = MathF.Max( max, projection );
}
float forward = extent - min, backward = max + extent;
if ( forward <= 0 || backward <= 0 ) return Vector2.Zero;
float overlap = MathF.Min( forward, backward );
if ( overlap >= depth ) continue;
depth = overlap;
correction = normal * (forward < backward ? overlap + 0.05f : -overlap - 0.05f);
}
return correction;
}
void ResolveShardOverlaps()
{
#if STANDALONE
#endif
Span<Vector2> polygon = stackalloc Vector2[8];
Span<Vector2> candidate = stackalloc Vector2[8];
foreach ( var shard in particles )
{
if ( shard.IsSpark ) continue;
var position = shard.Position;
var vertices = polygon[..shard.Vertices.Length];
bool transformed = false;
foreach ( var diamond in crushDiamonds )
{
if ( !NearDiamond( position, shard.Radius, diamond.Center ) ) continue;
if ( !transformed ) { TransformShard( shard, vertices ); transformed = true; }
var correction = Separation( vertices, diamond.Center );
if ( correction.Length <= 0.55f ) continue; // Ordinary Box2D contact slop.
var moved = candidate[..vertices.Length];
bool clear = true;
for ( int i = 0; i < vertices.Length; i++ )
{
moved[i] = vertices[i] + correction;
if ( !HasRoom( moved[i], 0 ) ) clear = false;
}
if ( !clear ) continue;
foreach ( var other in crushDiamonds )
{
if ( !NearDiamond( position + correction, shard.Radius, other.Center ) ) continue;
if ( Separation( moved, other.Center ).Length > 0.55f ) { clear = false; break; }
}
if ( !clear ) continue; // Never resolve one face by pushing into another gem.
shard.Body.Position += new Vector3( correction.x, correction.y, 0 );
shard.Body.Sleeping = false;
var normal = new Vector3( correction.x, correction.y, 0 ).Normal;
var velocity = shard.Body.Velocity;
float closing = Vector3.Dot( velocity - new Vector3( diamond.Velocity.x, diamond.Velocity.y, 0 ), normal );
if ( closing < 0 ) shard.Body.Velocity = velocity - normal * closing;
shard.LastClearTransform = shard.Body.Transform;
shard.HasClearTransform = true;
break;
}
}
}
}