251 lines
8.2 KiB
C#
251 lines
8.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Media;
|
|
using Avalonia.Rendering.Composition.Animations;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using DelaunatorSharp;
|
|
using DelauneyTriangulation.Models;
|
|
using DelauneyTriangulation.Views;
|
|
|
|
namespace DelauneyTriangulation.ViewModels;
|
|
|
|
public partial class MainWindowViewModel : ViewModelBase
|
|
{
|
|
public MainWindow? View { get; set; } = null;
|
|
[ObservableProperty] private int? _pointCount = 20;
|
|
[ObservableProperty] private int? _minPointDistance = 5;
|
|
[ObservableProperty] private double _panX;
|
|
[ObservableProperty] private double _panY;
|
|
[ObservableProperty] private double _zoom = 1.0;
|
|
|
|
[ObservableProperty] private bool _pointsVisible = true;
|
|
[ObservableProperty] private bool _edgesVisible = true;
|
|
[ObservableProperty] private bool _circlesVisible = true;
|
|
[ObservableProperty] private bool _voronoiVisible = true;
|
|
[ObservableProperty] private bool _trianglesVisible = true;
|
|
|
|
[ObservableProperty] private int? _generationDelay = 20;
|
|
private CancellationTokenSource? _genCts;
|
|
|
|
public double Width { get; set; } = 1280;
|
|
public double Height { get; set; } = 720;
|
|
|
|
private double LeftOffset => Width / 4;
|
|
private double TopOffset => Height - 50;
|
|
|
|
public ObservableCollection<Geometries.Point> Points { get; set; } = new();
|
|
public ObservableCollection<Geometries.Edge> Edges { get; } = new();
|
|
public ObservableCollection<Geometries.Circle> Circles { get; } = new();
|
|
public ObservableCollection<Geometries.Edge> VoronoiDiagram { get; } = new();
|
|
public ObservableCollection<Geometries.Triangle> Triangles { get; } = new();
|
|
|
|
public IAsyncRelayCommand GenerateCommand { get; }
|
|
|
|
// gets distance of a and b edge to calc hypotenuse aka dist of two points
|
|
double PointDistance(Geometries.Point p1, Geometries.Point p2) => Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2));
|
|
|
|
async Task GenerateRandomPoints(int count, CancellationToken ct)
|
|
{
|
|
bool CheckAdd(Geometries.Point p1, IList<Geometries.Point> p) => !p.Any(x => PointDistance(p1, x) <= MinPointDistance);
|
|
|
|
Points.Clear();
|
|
var rand = new Random();
|
|
var rgb = (byte)(255 - PointCount);
|
|
const byte a = 255;
|
|
var color = new Color(rgb, rgb, rgb, a);
|
|
|
|
List<Geometries.Point> l = new();
|
|
int retry = 0;
|
|
|
|
for (var i = 0; i < PointCount; i++)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
if (retry == 10) break;
|
|
|
|
var x = rand.NextDouble() * (Width - LeftOffset) + LeftOffset;
|
|
var y = rand.NextDouble() * (Height - TopOffset * 2) + TopOffset;
|
|
var p = new Geometries.Point(x, y);
|
|
if (CheckAdd(p, l))
|
|
{
|
|
p.Color = color;
|
|
rgb++;
|
|
color = new Color(rgb, rgb, rgb, a);
|
|
|
|
l.Add(p);
|
|
retry = 0;
|
|
}
|
|
else
|
|
{
|
|
i--;
|
|
retry++;
|
|
};
|
|
}
|
|
|
|
l.Sort(Geometries.PointComparer.Instance);
|
|
foreach (var p in l)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
Points.Add(p);
|
|
await DelayAsync(ct);
|
|
}
|
|
}
|
|
|
|
void ConnectPoint(Geometries.Point p1, Geometries.Point p2) => Edges.Add(new Geometries.Edge(p1.X, p1.Y, p2.X, p2.Y));
|
|
|
|
async Task ConnectPoints(CancellationToken ct)
|
|
{
|
|
if(Points.Count == 0) return;
|
|
Edges.Clear();
|
|
var toConnect = Points.ToList();
|
|
var p1 = toConnect[0];
|
|
|
|
while (toConnect.Count > 0)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var p2 = toConnect.MinBy(x => PointDistance(p1, x));
|
|
if (p2 is null) continue;
|
|
|
|
ConnectPoint(p1, p2);
|
|
|
|
toConnect.Remove(p1);
|
|
p1 = p2;
|
|
await DelayAsync(ct);
|
|
}
|
|
}
|
|
|
|
void AddTriangle(Geometries.Point p1, Geometries.Point p2, Geometries.Point p3) => Triangles.Add(new(p1, p2, p3));
|
|
async Task Triangulate(CancellationToken ct)
|
|
{
|
|
Triangles.Clear();
|
|
if (Points.Count < 3) return;
|
|
|
|
var toTriangulate = Points.ToList();
|
|
|
|
var p1 = toTriangulate[0];
|
|
var p2 = toTriangulate.Where(x => !ReferenceEquals(x, p1)).MinBy(x => PointDistance(p1, x));
|
|
if (p2 is null) return;
|
|
|
|
static double TwiceArea(Geometries.Point a, Geometries.Point b, Geometries.Point c) => Math.Abs((b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X));
|
|
|
|
while (toTriangulate.Count >= 3)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var candidates = toTriangulate.Where(x => !ReferenceEquals(x, p1) && !ReferenceEquals(x, p2));
|
|
|
|
Geometries.Point? p3 = null;
|
|
foreach (var c in candidates.OrderBy(x => PointDistance(p1, x) + PointDistance(p2, x)))
|
|
{
|
|
if (TwiceArea(p1, p2, c) > 1e-6) p3 = c; break;
|
|
}
|
|
|
|
if (p3 is null) break;
|
|
|
|
AddTriangle(p1, p2, p3);
|
|
|
|
toTriangulate.Remove(p1);
|
|
p1 = p2;
|
|
p2 = p3;
|
|
|
|
await DelayAsync(ct);
|
|
}
|
|
}
|
|
|
|
async Task DelaunayTriangulateAsync(CancellationToken ct)
|
|
{
|
|
Triangles.Clear();
|
|
Edges.Clear();
|
|
|
|
if(Points.Count < 3) return;
|
|
|
|
var pts = Points.Select(p => (DelaunatorSharp.IPoint)new DelaunatorSharp.Point(p.X, p.Y)).ToArray();
|
|
|
|
var d = new Delaunator(pts);
|
|
|
|
// Delaunay Triangle Extraction
|
|
for (int i = 0; i < d.Triangles.Length; i += 3)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
int i0 = d.Triangles[i];
|
|
int i1 = d.Triangles[i + 1];
|
|
int i2 = d.Triangles[i + 2];
|
|
|
|
var p0 = Points[i0];
|
|
var p1 = Points[i1];
|
|
var p2 = Points[i2];
|
|
|
|
Triangles.Add(new Geometries.Triangle(p0, p1, p2));
|
|
await DelayAsync(ct);
|
|
}
|
|
|
|
// Konvex Hull Extraction
|
|
var hullPoints = d.GetHullPoints();
|
|
var first = hullPoints.First();
|
|
var last = hullPoints.Last();
|
|
var p1h = hullPoints[0];
|
|
for (int h = 1; h < d.Hull.Length; h++)
|
|
{
|
|
var p2h = hullPoints[h];
|
|
Edges.Add(new Geometries.Edge(p1h.X, p1h.Y, p2h.X, p2h.Y));
|
|
p1h = p2h;
|
|
await DelayAsync(ct);
|
|
}
|
|
Edges.Add(new Geometries.Edge(first.X, first.Y, last.X, last.Y));
|
|
|
|
// Voronoi Diagram Extraction
|
|
foreach (var cell in d.GetVoronoiCells())
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var f = cell.Points[0];
|
|
for (int i = 1; i < cell.Points.Length; i++)
|
|
{
|
|
var s = cell.Points[i];
|
|
VoronoiDiagram.Add(new Geometries.Edge(f.X, f.Y, s.X, s.Y));
|
|
Circles.Add(new Geometries.Circle(f.X, f.Y, Points.Min(x => PointDistance(new Geometries.Point(f.X, f.Y), x))));
|
|
|
|
f = s;
|
|
await DelayAsync(ct);
|
|
}
|
|
}
|
|
}
|
|
|
|
private Task DelayAsync(CancellationToken ct) => GenerationDelay > 0 ? Task.Delay(GenerationDelay ?? 0, ct) : Task.CompletedTask;
|
|
|
|
async Task RunGenerateAsync()
|
|
{
|
|
_genCts?.CancelAsync();
|
|
_genCts = new CancellationTokenSource();
|
|
try { await Generate(_genCts.Token); }
|
|
catch (OperationCanceledException) { }
|
|
|
|
}
|
|
|
|
private async Task Generate(CancellationToken ct)
|
|
{
|
|
Points.Clear();
|
|
Edges.Clear();
|
|
Circles.Clear();
|
|
Triangles.Clear();
|
|
VoronoiDiagram.Clear();
|
|
|
|
await GenerateRandomPoints(PointCount ?? 20, ct);
|
|
// await ConnectPoints(ct);
|
|
// await Triangulate(ct);
|
|
await DelaunayTriangulateAsync(ct);
|
|
}
|
|
|
|
private bool CanGenerate() => true;
|
|
|
|
public MainWindowViewModel()
|
|
{
|
|
GenerateCommand = new AsyncRelayCommand(RunGenerateAsync, CanGenerate);
|
|
}
|
|
} |