243 lines
7.2 KiB
C#
243 lines
7.2 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Assets.Common;
|
|
using Assets.Map;
|
|
using Priority_Queue;
|
|
|
|
namespace Assets.Voronoi
|
|
{
|
|
public interface IEvent
|
|
{
|
|
double Priority { get; }
|
|
bool IsValid { get; }
|
|
}
|
|
|
|
public class SiteEvent : IEvent
|
|
{
|
|
public Site Site { get; internal set; }
|
|
|
|
public double Priority => Site.Point.y;
|
|
public bool IsValid => true;
|
|
}
|
|
|
|
public class EdgeEvent : IEvent
|
|
{
|
|
public RedBlackNode<Parabola> node;
|
|
public Point vertex;
|
|
public bool IsValid { get; set; } = true;
|
|
|
|
public double Priority => vertex.y + Point.Dist(vertex, node.Value.Site.Point);
|
|
}
|
|
|
|
public class Site
|
|
{
|
|
public Point Point { get; internal set; }
|
|
public int Index { get; internal set; }
|
|
public List<int> Vertices { get; internal set; } = new List<int>();
|
|
public List<(int, int)> Edges { get; internal set; } = new List<(int, int)>();
|
|
|
|
public Site(Point point, int index)
|
|
{
|
|
Point = point;
|
|
Index = index;
|
|
}
|
|
}
|
|
|
|
public class VoronoiGenerator
|
|
{
|
|
public IList<Site> Sites { get; }
|
|
|
|
public IPriorityQueue<IEvent, double> Queue { get; internal set; }
|
|
|
|
public Graph<Point> Voronoi;
|
|
public Graph<Site> Delaunay;
|
|
|
|
public List<HalfEdge> HalfEdges;
|
|
|
|
public BeachLine Line { get; private set; }
|
|
|
|
public bool Done => Queue.Count == 0;
|
|
|
|
public VoronoiGenerator(IList<Point> sites)
|
|
{
|
|
int i = 0;
|
|
Sites = sites.Select(x => new Site(x, i++)).ToList();
|
|
|
|
Reset();
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
Queue = new SimplePriorityQueue<IEvent, double>();
|
|
Line = new BeachLine();
|
|
|
|
Voronoi = new Graph<Point>();
|
|
Delaunay = new Graph<Site>();
|
|
HalfEdges = new List<HalfEdge>();
|
|
|
|
Delaunay.Vertices.AddRange(Sites);
|
|
|
|
foreach (var site in Sites)
|
|
{
|
|
var @event = new SiteEvent { Site = site };
|
|
Queue.Enqueue(@event, @event.Priority);
|
|
}
|
|
}
|
|
|
|
public void Step()
|
|
{
|
|
while (Queue.Count > 0)
|
|
{
|
|
var ev = Queue.Dequeue();
|
|
if (!ev.IsValid) continue;
|
|
|
|
Line.Directrix = ev.Priority;
|
|
|
|
switch (ev)
|
|
{
|
|
case SiteEvent site:
|
|
HandleSiteEvent(site);
|
|
return;
|
|
|
|
case EdgeEvent edge:
|
|
HandleEdgeEvent(edge);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Generate()
|
|
{
|
|
Reset();
|
|
|
|
while (Queue.Count > 0)
|
|
Step();
|
|
}
|
|
|
|
private void Enqueue(EdgeEvent @event)
|
|
{
|
|
if (@event.Priority > Line.Directrix)
|
|
Queue.Enqueue(@event, @event.Priority);
|
|
}
|
|
|
|
private void HandleEdgeEvent(EdgeEvent @event)
|
|
{
|
|
var node = @event.node;
|
|
|
|
var previous = node.Previous;
|
|
var next = node.Next;
|
|
|
|
if (previous?.Value.Event != null) previous.Value.Event.IsValid = false;
|
|
if (next?.Value.Event != null) next.Value.Event.IsValid = false;
|
|
|
|
Voronoi.Vertices.Add(@event.vertex);
|
|
|
|
var index = Voronoi.Vertices.Count - 1;
|
|
|
|
node.Value.LeftEdge.End = @event.vertex;
|
|
node.Value.LeftEdge.EndVertex = index;
|
|
|
|
node.Value.RightEdge.End = @event.vertex;
|
|
node.Value.RightEdge.EndVertex = index;
|
|
|
|
HalfEdges.Add(node.Value.LeftEdge);
|
|
HalfEdges.Add(node.Value.RightEdge);
|
|
|
|
node.Value.Site.Vertices.Add(index);
|
|
previous?.Value.Site.Vertices.Add(index);
|
|
next?.Value.Site.Vertices.Insert(0, index);
|
|
|
|
if (node.Value.LeftEdge.IsComplete)
|
|
{
|
|
var edge = node.Value.LeftEdge.Edge;
|
|
|
|
node.Value.Site.Edges.Insert(0, FixEdgeDirection(node.Value, edge));
|
|
previous?.Value.Site.Edges.Add(FixEdgeDirection(previous.Value, edge));
|
|
|
|
Voronoi.AddEdge(edge.Item1, edge.Item2);
|
|
}
|
|
|
|
if (node.Value.RightEdge.IsComplete)
|
|
{
|
|
var edge = node.Value.RightEdge.Edge;
|
|
|
|
node.Value.Site.Edges.Add(FixEdgeDirection(node.Value, edge));
|
|
next?.Value.Site.Edges.Insert(0, FixEdgeDirection(next.Value, edge));
|
|
|
|
Voronoi.AddEdge(edge.Item1, edge.Item2);
|
|
}
|
|
|
|
var newEdge = new HalfEdge() { Start = @event.vertex, StartVertex = index };
|
|
|
|
node.Previous.Value.RightEdge = newEdge;
|
|
node.Next.Value.LeftEdge = newEdge;
|
|
|
|
Line.RemoveParabola(node);
|
|
|
|
if (Line.CheckCircleEvent(previous) is EdgeEvent p)
|
|
Enqueue(p);
|
|
|
|
if (Line.CheckCircleEvent(next) is EdgeEvent n)
|
|
Enqueue(n);
|
|
|
|
if (previous != null && next != null)
|
|
Delaunay.AddEdge(next.Value.Site.Index, previous.Value.Site.Index);
|
|
}
|
|
|
|
private (int, int) FixEdgeDirection(Parabola parabola, (int, int) edge)
|
|
{
|
|
var center = parabola.Site.Point;
|
|
|
|
var a = Voronoi.Vertices[edge.Item1];
|
|
var b = Voronoi.Vertices[edge.Item2];
|
|
|
|
var xa = a.x - center.x;
|
|
var xb = b.x - center.x;
|
|
var ya = a.y - center.y;
|
|
var yb = b.y - center.y;
|
|
|
|
return xa * yb - xb * ya < 0 ? (edge.Item2, edge.Item1) : edge;
|
|
}
|
|
|
|
private void HandleSiteEvent(SiteEvent @event)
|
|
{
|
|
var site = @event.Site;
|
|
var start = new Point(site.Point.x, Line.Eval(site.Point.x));
|
|
|
|
var above = Line.FindParabola(site.Point.x);
|
|
|
|
if (above != null)
|
|
{
|
|
var left = above.Value.LeftEdge;
|
|
var right = above.Value.RightEdge;
|
|
|
|
var node = Line.AddParabola(@event.Site, above);
|
|
|
|
var newLeft = new HalfEdge() { Start = start };
|
|
var newRight = new HalfEdge() { Start = start, Twin = newLeft };
|
|
|
|
node.Previous.Value.LeftEdge = left;
|
|
node.Previous.Value.RightEdge = newLeft;
|
|
|
|
node.Value.LeftEdge = newLeft;
|
|
node.Value.RightEdge = newRight;
|
|
|
|
node.Next.Value.LeftEdge = newRight;
|
|
node.Next.Value.RightEdge = right;
|
|
|
|
if (Line.CheckCircleEvent(node.Previous) is EdgeEvent p)
|
|
Enqueue(p);
|
|
|
|
if (Line.CheckCircleEvent(node.Next) is EdgeEvent n)
|
|
Enqueue(n);
|
|
|
|
if (node.Previous != null)
|
|
Delaunay.AddEdge(@event.Site.Index, node.Previous.Value.Site.Index);
|
|
}
|
|
else
|
|
{
|
|
Line.AddParabola(@event.Site);
|
|
}
|
|
}
|
|
}
|
|
} |