diff --git a/DelauneyTriangulation/DelauneyTriangulation.csproj b/DelauneyTriangulation/DelauneyTriangulation.csproj
index d0397d6..cd441fd 100644
--- a/DelauneyTriangulation/DelauneyTriangulation.csproj
+++ b/DelauneyTriangulation/DelauneyTriangulation.csproj
@@ -20,5 +20,7 @@
+
+
diff --git a/DelauneyTriangulation/Models/Geometries.cs b/DelauneyTriangulation/Models/Geometries.cs
index 396a959..d66ea8a 100644
--- a/DelauneyTriangulation/Models/Geometries.cs
+++ b/DelauneyTriangulation/Models/Geometries.cs
@@ -1,19 +1,44 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media;
using CommunityToolkit.Mvvm.Input;
namespace DelauneyTriangulation.Models;
public static class Geometries
{
- public sealed class Point(double x, double y, double z = 3)
+ public sealed class Point(double x, double y, double r = 3, Color? color = null)
{
public double X { get; } = x;
public double Y { get; } = y;
- public double R { get; } = z;
+ public double R { get; } = r;
+
+ public Color Color { get; set; } = color ?? new Color(255, 255, 255, 255);
public double Left => X - R;
- public double Top => Y - R;
+ public double Top => Y - R;
public double Size => 2 * R;
}
+
+ public sealed class PointComparer : IComparer
+ {
+ public static readonly PointComparer Instance = new();
+
+ public int Compare(Point? a, Point? b)
+ {
+ if (ReferenceEquals(a, b)) return 0;
+ if (a is null) return -1;
+ if (b is null) return 1;
+
+ int c = a.X.CompareTo(b.X);
+ if (c != 0) return c;
+
+ c = a.Y.CompareTo(b.Y);
+ if (c != 0) return c;
+
+ return a.R.CompareTo(b.R);
+ }
+ }
public sealed class Edge(double x1, double y1, double x2, double y2)
{
@@ -23,7 +48,7 @@ public static class Geometries
public double Y2 { get; } = y2;
public Avalonia.Point Start => new(X1, Y1);
- public Avalonia.Point End => new(X2, Y2);
+ public Avalonia.Point End => new(X2, Y2);
}
public sealed class Circle(double x, double y, double r)
@@ -33,7 +58,16 @@ public static class Geometries
public double R { get; } = r;
public double Left => X - R;
- public double Top => Y - R;
+ public double Top => Y - R;
public double Diameter => 2 * R;
}
+
+ public sealed class Triangle(Point p1, Point p2, Point p3)
+ {
+ public Avalonia.Point X { get; } = new(p1.X, p1.Y);
+ public Avalonia.Point Y { get; } = new(p2.X, p2.Y);
+ public Avalonia.Point Z { get; } = new(p3.X, p3.Y);
+
+ public IReadOnlyList Vertices => [ X, Y, Z ];
+ }
}
\ No newline at end of file
diff --git a/DelauneyTriangulation/ViewModels/MainWindowViewModel.cs b/DelauneyTriangulation/ViewModels/MainWindowViewModel.cs
index 58bb017..19f4cb9 100644
--- a/DelauneyTriangulation/ViewModels/MainWindowViewModel.cs
+++ b/DelauneyTriangulation/ViewModels/MainWindowViewModel.cs
@@ -1,50 +1,248 @@
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;
namespace DelauneyTriangulation.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
- private int _pointCount = 20;
+ [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 = false;
+ [ObservableProperty] private bool _circlesVisible = false;
+ [ObservableProperty] private bool _trianglesVisible = false;
+
+ [ObservableProperty] private int _generationDelay = 20;
+ private CancellationTokenSource? _genCts;
+
public double Width { get; set; } = 1280;
public double Height { get; set; } = 720;
- public int PointCount
- {
- get => _pointCount;
- set
- {
- _pointCount = value;
- OnPropertyChanged(nameof(PointCount));
- GenerateRandomPoints(value);
- }
- }
+ private double LeftOffset => Width / 4;
+ private double TopOffset => Height - 50;
- public ObservableCollection Points { get; } = new();
+ public ObservableCollection Points { get; set; } = new();
public ObservableCollection Edges { get; } = new();
public ObservableCollection Circles { get; } = new();
+ public ObservableCollection VoronoiDiagram { get; } = new();
+ public ObservableCollection Triangles { get; } = new();
- void GenerateRandomPoints(int count)
+ 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 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 l = new();
+ int retry = 0;
for (var i = 0; i < PointCount; i++)
{
- var x = rand.NextDouble() * Width;
- var y = rand.NextDouble() * Height;
- Points.Add(new Geometries.Point(x, y));
+ 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();
+
+ 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.GetVoronoiCellsBasedOnCircumcenters())
+ {
+ 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));
+ f = s;
+ await DelayAsync(ct);
+ }
+ }
+ */
+ }
+
+ private Task DelayAsync(CancellationToken ct) => GenerationDelay > 0 ? Task.Delay(GenerationDelay, 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, ct);
+ // await ConnectPoints(ct);
+ // await Triangulate(ct);
+ await DelaunayTriangulateAsync(ct);
+ }
+
+ private bool CanGenerate() => true;
+
public MainWindowViewModel()
{
- GenerateRandomPoints(_pointCount);
+ GenerateCommand = new AsyncRelayCommand(RunGenerateAsync, CanGenerate);
}
}
\ No newline at end of file
diff --git a/DelauneyTriangulation/Views/MainWindow.axaml b/DelauneyTriangulation/Views/MainWindow.axaml
index 166b166..6c015ac 100644
--- a/DelauneyTriangulation/Views/MainWindow.axaml
+++ b/DelauneyTriangulation/Views/MainWindow.axaml
@@ -15,8 +15,8 @@
ExtendClientAreaTitleBarHeightHint="32"
TransparencyLevelHint="AcrylicBlur, Blur, Transparent"
Background="Transparent"
- Width="{Binding Width}"
- Height="{Binding Height}"
+ Width="{Binding Width, Mode=TwoWay}"
+ Height="{Binding Height, Mode=TwoWay}"
>
@@ -42,24 +42,61 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+