before candybox editor update

This commit is contained in:
meelstorm
2025-09-16 10:42:43 +02:00
parent e5746ef766
commit dc12b3e51a
103 changed files with 96379 additions and 343 deletions

View File

@@ -0,0 +1,79 @@
namespace CandyboxPlugin.Voronoi
{
public class DelaunayTriangulator
{
private double MaxX { get; set; }
private double MaxY { get; set; }
private IEnumerable<Triangle> border;
public void GenerateBorder(double maxX, double maxY)
{
MaxX = maxX;
MaxY = maxY;
// TODO make more beautiful
var point0 = new VoronoiPoint(0, 0);
var point1 = new VoronoiPoint(0, MaxY);
var point2 = new VoronoiPoint(MaxX, MaxY);
var point3 = new VoronoiPoint(MaxX, 0);
var points = new List<VoronoiPoint>() { point0, point1, point2, point3 };
var tri1 = new Triangle(point0, point1, point2);
var tri2 = new Triangle(point0, point2, point3);
border = new List<Triangle>() { tri1, tri2 };
}
public IEnumerable<Triangle> BowyerWatson(IEnumerable<VoronoiPoint> points)
{
//var supraTriangle = GenerateSupraTriangle();
var triangulation = new HashSet<Triangle>(border);
foreach (var point in points)
{
var badTriangles = FindBadTriangles(point, triangulation);
var polygon = FindHoleBoundaries(badTriangles);
foreach (var triangle in badTriangles)
{
foreach (var vertex in triangle.Vertices)
{
vertex.AdjacentTriangles.Remove(triangle);
}
}
triangulation.RemoveWhere(o => badTriangles.Contains(o));
foreach (var edge in polygon.Where(possibleEdge => possibleEdge.Point1 != point && possibleEdge.Point2 != point))
{
var triangle = new Triangle(point, edge.Point1, edge.Point2);
triangulation.Add(triangle);
}
}
return triangulation;
}
private List<Edge> FindHoleBoundaries(ISet<Triangle> badTriangles)
{
var edges = new List<Edge>();
foreach (var triangle in badTriangles)
{
edges.Add(new Edge(triangle.Vertices[0], triangle.Vertices[1]));
edges.Add(new Edge(triangle.Vertices[1], triangle.Vertices[2]));
edges.Add(new Edge(triangle.Vertices[2], triangle.Vertices[0]));
}
var grouped = edges.GroupBy(o => o);
var boundaryEdges = edges.GroupBy(o => o).Where(o => o.Count() == 1).Select(o => o.First());
return boundaryEdges.ToList();
}
private ISet<Triangle> FindBadTriangles(VoronoiPoint point, HashSet<Triangle> triangles)
{
var badTriangles = triangles.Where(o => o.IsPointInsideCircumcircle(point));
return new HashSet<Triangle>(badTriangles);
}
}
}

View File

@@ -0,0 +1,31 @@
namespace CandyboxPlugin.Voronoi
{
public class Edge
{
public VoronoiPoint Point1 { get; }
public VoronoiPoint Point2 { get; }
public Edge(VoronoiPoint point1, VoronoiPoint point2)
{
Point1 = point1;
Point2 = point2;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
if (obj.GetType() != GetType()) return false;
var edge = obj as Edge;
var samePoints = Point1 == edge.Point1 && Point2 == edge.Point2;
var samePointsReversed = Point1 == edge.Point2 && Point2 == edge.Point1;
return samePoints || samePointsReversed;
}
public override int GetHashCode()
{
int hCode = (int)Point1.X ^ (int)Point1.Y ^ (int)Point2.X ^ (int)Point2.Y;
return hCode.GetHashCode();
}
}
}

View File

@@ -0,0 +1,113 @@
namespace CandyboxPlugin.Voronoi
{
public class Triangle
{
public VoronoiPoint[] Vertices { get; } = new VoronoiPoint[3];
public VoronoiPoint Circumcenter { get; private set; }
public double RadiusSquared;
public IEnumerable<Triangle> TrianglesWithSharedEdge {
get {
var neighbors = new HashSet<Triangle>();
foreach (var vertex in Vertices)
{
var trianglesWithSharedEdge = vertex.AdjacentTriangles.Where(o =>
{
return o != this && SharesEdgeWith(o);
});
neighbors.UnionWith(trianglesWithSharedEdge);
}
return neighbors;
}
}
public Triangle(VoronoiPoint point1, VoronoiPoint point2, VoronoiPoint point3)
{
// In theory this shouldn't happen, but it was at one point so this at least makes sure we're getting a
// relatively easily-recognised error message, and provides a handy breakpoint for debugging.
if (point1 == point2 || point1 == point3 || point2 == point3)
{
throw new ArgumentException("Must be 3 distinct points");
}
if (!IsCounterClockwise(point1, point2, point3))
{
Vertices[0] = point1;
Vertices[1] = point3;
Vertices[2] = point2;
}
else
{
Vertices[0] = point1;
Vertices[1] = point2;
Vertices[2] = point3;
}
Vertices[0].AdjacentTriangles.Add(this);
Vertices[1].AdjacentTriangles.Add(this);
Vertices[2].AdjacentTriangles.Add(this);
UpdateCircumcircle();
}
private void UpdateCircumcircle()
{
// https://codefound.wordpress.com/2013/02/21/how-to-compute-a-circumcircle/#more-58
// https://en.wikipedia.org/wiki/Circumscribed_circle
var p0 = Vertices[0];
var p1 = Vertices[1];
var p2 = Vertices[2];
var dA = p0.X * p0.X + p0.Y * p0.Y;
var dB = p1.X * p1.X + p1.Y * p1.Y;
var dC = p2.X * p2.X + p2.Y * p2.Y;
var aux1 = (dA * (p2.Y - p1.Y) + dB * (p0.Y - p2.Y) + dC * (p1.Y - p0.Y));
var aux2 = -(dA * (p2.X - p1.X) + dB * (p0.X - p2.X) + dC * (p1.X - p0.X));
var div = (2 * (p0.X * (p2.Y - p1.Y) + p1.X * (p0.Y - p2.Y) + p2.X * (p1.Y - p0.Y)));
if (div == 0)
{
throw new DivideByZeroException();
}
var center = new VoronoiPoint(aux1 / div, aux2 / div);
Circumcenter = center;
RadiusSquared = (center.X - p0.X) * (center.X - p0.X) + (center.Y - p0.Y) * (center.Y - p0.Y);
}
private bool IsCounterClockwise(VoronoiPoint point1, VoronoiPoint point2, VoronoiPoint point3)
{
var result = (point2.X - point1.X) * (point3.Y - point1.Y) -
(point3.X - point1.X) * (point2.Y - point1.Y);
return result > 0;
}
public bool SharesEdgeWith(Triangle triangle)
{
var sharedVertices = Vertices.Where(o => triangle.Vertices.Contains(o)).Count();
return sharedVertices == 2;
}
public bool IsPointInsideCircumcircle(VoronoiPoint point)
{
var d_squared = (point.X - Circumcenter.X) * (point.X - Circumcenter.X) +
(point.Y - Circumcenter.Y) * (point.Y - Circumcenter.Y);
return d_squared < RadiusSquared;
}
public bool IsInside(VoronoiPoint point)
{
var b1 = Sign(point, Vertices[0], Vertices[1]) < 0.0;
var b2 = Sign(point, Vertices[1], Vertices[2]) < 0.0;
var b3 = Sign(point, Vertices[2], Vertices[0]) < 0.0;
return ((b1 == b2) && (b2 == b3));
}
private double Sign(VoronoiPoint p1, VoronoiPoint p2, VoronoiPoint p3)
{
return (p1.X - p3.X) * (p2.Y - p3.Y) - (p2.X - p3.X) * (p1.Y - p3.Y);
}
}
}

View File

@@ -0,0 +1,23 @@
namespace CandyboxPlugin.Voronoi
{
public class Voronoi
{
public static List<Edge> GenerateEdgesFromDelaunay(IEnumerable<Triangle> triangulation)
{
var voronoiEdges = new HashSet<Edge>();
foreach (var triangle in triangulation)
{
foreach (var neighbor in triangle.TrianglesWithSharedEdge)
{
var edge = new Edge(triangle.Circumcenter, neighbor.Circumcenter);
voronoiEdges.Add(edge);
}
}
return voronoiEdges.ToList();
}
}
}

View File

@@ -0,0 +1,32 @@
namespace CandyboxPlugin.Voronoi
{
public class VoronoiPoint
{
/// <summary>
/// Used only for generating a unique ID for each instance of this class that gets generated
/// </summary>
private static int _counter;
/// <summary>
/// Used for identifying an instance of a class; can be useful in troubleshooting when geometry goes weird
/// (e.g. when trying to identify when Triangle objects are being created with the same Point object twice)
/// </summary>
private readonly int _instanceId = _counter++;
public double X { get; }
public double Y { get; }
public HashSet<Triangle> AdjacentTriangles { get; } = new HashSet<Triangle>();
public VoronoiPoint(double x, double y)
{
X = x;
Y = y;
}
public override string ToString()
{
// Simple way of seeing what's going on in the debugger when investigating weirdness
return $"{nameof(VoronoiPoint)} {_instanceId} {X:0.##}@{Y:0.##}";
}
}
}