using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Worldgen.Tools;
///
/// An implementation of Poisson Disk Sampling, for evenly spreading points across a given area.
///
public sealed class PoissonDiskSampler
{
public const int DefaultPointsPerIteration = 30;
[Dependency] private readonly IRobustRandom _random = default!;
///
/// Samples for points within the given circle.
///
/// Center of the sample
/// Radius of the sample
/// Minimum distance between points. Must be above 0!
/// The number of points placed per iteration of the algorithm
/// An enumerator of points
public SampleEnumerator SampleCircle(Vector2 center, float radius, float minimumDistance,
int pointsPerIteration = DefaultPointsPerIteration)
{
return Sample(center - new Vector2(radius, radius), center + new Vector2(radius, radius), radius,
minimumDistance, pointsPerIteration);
}
///
/// Samples for points within the given rectangle.
///
/// The top left of the rectangle
/// The bottom right of the rectangle
/// Minimum distance between points. Must be above 0!
/// The number of points placed per iteration of the algorithm
/// An enumerator of points
public SampleEnumerator SampleRectangle(Vector2 topLeft, Vector2 lowerRight, float minimumDistance,
int pointsPerIteration = DefaultPointsPerIteration)
{
return Sample(topLeft, lowerRight, null, minimumDistance, pointsPerIteration);
}
///
/// Samples for points within the given rectangle, with an optional rejection distance.
///
/// The top left of the rectangle
/// The bottom right of the rectangle
/// The distance at which points will be discarded, if any
/// Minimum distance between points. Must be above 0!
/// The number of points placed per iteration of the algorithm
/// An enumerator of points
public SampleEnumerator Sample(Vector2 topLeft, Vector2 lowerRight, float? rejectionDistance,
float minimumDistance, int pointsPerIteration)
{
// This still doesn't guard against dangerously low but non-zero distances, but this will do for now.
DebugTools.Assert(minimumDistance > 0, "Minimum distance must be above 0, or else an infinite number of points would be generated.");
var settings = new SampleSettings
{
TopLeft = topLeft, LowerRight = lowerRight,
Dimensions = lowerRight - topLeft,
Center = (topLeft + lowerRight) / 2,
CellSize = minimumDistance / (float) Math.Sqrt(2),
MinimumDistance = minimumDistance,
RejectionSqDistance = rejectionDistance * rejectionDistance
};
settings.GridWidth = (int) (settings.Dimensions.X / settings.CellSize) + 1;
settings.GridHeight = (int) (settings.Dimensions.Y / settings.CellSize) + 1;
var state = new State
{
Grid = new Vector2?[settings.GridWidth, settings.GridHeight],
ActivePoints = new List()
};
return new SampleEnumerator(this, state, settings, pointsPerIteration);
}
private Vector2 AddFirstPoint(ref SampleSettings settings, ref State state)
{
while (true)
{
var d = _random.NextDouble();
var xr = settings.TopLeft.X + settings.Dimensions.X * d;
d = _random.NextDouble();
var yr = settings.TopLeft.Y + settings.Dimensions.Y * d;
var p = new Vector2((float) xr, (float) yr);
if (settings.RejectionSqDistance != null &&
(settings.Center - p).LengthSquared > settings.RejectionSqDistance)
continue;
var index = Denormalize(p, settings.TopLeft, settings.CellSize);
state.Grid[(int) index.X, (int) index.Y] = p;
state.ActivePoints.Add(p);
return p;
}
}
private Vector2? AddNextPoint(Vector2 point, ref SampleSettings settings, ref State state)
{
var q = GenerateRandomAround(point, settings.MinimumDistance);
if (q.X >= settings.TopLeft.X && q.X < settings.LowerRight.X &&
q.Y > settings.TopLeft.Y && q.Y < settings.LowerRight.Y &&
(settings.RejectionSqDistance == null ||
(settings.Center - q).LengthSquared <= settings.RejectionSqDistance))
{
var qIndex = Denormalize(q, settings.TopLeft, settings.CellSize);
var tooClose = false;
for (var i = (int) Math.Max(0, qIndex.X - 2);
i < Math.Min(settings.GridWidth, qIndex.X + 3) && !tooClose;
i++)
for (var j = (int) Math.Max(0, qIndex.Y - 2);
j < Math.Min(settings.GridHeight, qIndex.Y + 3) && !tooClose;
j++)
{
if (state.Grid[i, j].HasValue && (state.Grid[i, j]!.Value - q).Length < settings.MinimumDistance)
tooClose = true;
}
if (!tooClose)
{
state.ActivePoints.Add(q);
state.Grid[(int) qIndex.X, (int) qIndex.Y] = q;
return q;
}
}
return null;
}
private Vector2 GenerateRandomAround(Vector2 center, float minimumDistance)
{
var d = _random.NextDouble();
var radius = minimumDistance + minimumDistance * d;
d = _random.NextDouble();
var angle = Math.PI * 2 * d;
var newX = radius * Math.Sin(angle);
var newY = radius * Math.Cos(angle);
return new Vector2((float) (center.X + newX), (float) (center.Y + newY));
}
private static Vector2 Denormalize(Vector2 point, Vector2 origin, double cellSize)
{
return new Vector2((int) ((point.X - origin.X) / cellSize), (int) ((point.Y - origin.Y) / cellSize));
}
public struct SampleEnumerator
{
private PoissonDiskSampler _pds;
private State _state;
private SampleSettings _settings;
// These variables make up the state machine.
private bool _returnedFirstPoint;
private int _pointsPerIteration;
private int _iterationListIndex;
private bool _iterationFound;
private int _iterationPosition;
// This has internal access because C# nested type access is being weird.
internal SampleEnumerator(PoissonDiskSampler pds, State state, SampleSettings settings, int ppi)
{
_pds = pds;
_state = state;
_settings = settings;
_pointsPerIteration = ppi;
}
public bool MoveNext([NotNullWhen(true)] out Vector2? point)
{
// First point is chosen via a very particular method.
if (!_returnedFirstPoint)
{
_returnedFirstPoint = true;
point = _pds.AddFirstPoint(ref _settings, ref _state);
return true;
}
// Remaining points have to be fed out carefully.
// We can be interrupted (by a successful point) mid-stream.
while (_state.ActivePoints.Count != 0)
{
if (_iterationPosition == 0)
{
// First point of iteration.
_iterationListIndex = _pds._random.Next(_state.ActivePoints.Count);
_iterationFound = false;
}
var basePoint = _state.ActivePoints[_iterationListIndex];
point = _pds.AddNextPoint(basePoint, ref _settings, ref _state);
// Set this now, return later after processing is complete.
_iterationFound |= point != null;
// Iteration loop advance.
_iterationPosition++;
if (_iterationPosition == _pointsPerIteration)
{
// Reached end of this iteration.
_iterationPosition = 0;
if (!_iterationFound)
_state.ActivePoints.RemoveAt(_iterationListIndex);
}
if (point != null)
return true;
}
point = null;
return false;
}
}
internal struct State
{
public Vector2?[,] Grid;
public List ActivePoints;
}
internal struct SampleSettings
{
public Vector2 TopLeft, LowerRight, Center;
public Vector2 Dimensions;
public float? RejectionSqDistance;
public float MinimumDistance;
public float CellSize;
public int GridWidth, GridHeight;
}
}