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 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +