Files
DelaunayTriangulation/DelauneyTriangulation/ViewModels/MainWindowViewModel.cs
T
2025-11-04 21:56:39 +01:00

426 lines
13 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));
static bool Almost(double a, double b, double eps = 1e-9) => Math.Abs(a - b) <= eps;
bool IsPointInCircumcircle(Geometries.Triangle t, Geometries.Point p)
{
var a = new Geometries.Point(t.X.X, t.X.Y);
var b = new Geometries.Point(t.Y.X, t.Y.Y);
var c = new Geometries.Point(t.Z.X, t.Z.Y);
var circ = new Geometries.Circle(a, b, c);
var d = PointDistance(new(circ.X, circ.Y), p);
return d <= circ.R + 1e-9;
}
static bool UsesVertex(Geometries.Triangle t, Geometries.Point v)
{
bool isV(Avalonia.Point q) => Math.Abs(q.X - v.X) <= 1e-9 && Math.Abs(q.Y - v.Y) <= 1e-9;
return isV(t.X) || isV(t.Y) || isV(t.Z);
}
(Geometries.Point A, Geometries.Point B, Geometries.Point C) BuildSuperTriangle()
{
var minX = Points.Min(p => p.X);
var minY = Points.Min(p => p.Y);
var maxX = Points.Max(p => p.X);
var maxY = Points.Max(p => p.Y);
var dx = maxX - minX;
var dy = maxY - minY;
var delta = Math.Max(dx, dy);
var cx = (minX + maxX) * 0.5;
var cy = (minY + maxY) * 0.5;
var A = new Geometries.Point(cx - 2 * delta, cy - delta * 3);
var B = new Geometries.Point(cx, cy + 3 * delta);
var C = new Geometries.Point(cx + 2 * delta, cy - delta * 3);
return (A, B, C);
}
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);
}
}
List<Geometries.Edge> ExtractTriangleEdges(Geometries.Triangle t)
{
var e1 = new Geometries.Edge(t.X.X, t.X.Y, t.Y.X, t.Y.Y);
var e2 = new Geometries.Edge(t.Y.X, t.Y.Y, t.Z.X, t.Z.Y);
var e3 = new Geometries.Edge(t.Z.X, t.Z.Y, t.X.X, t.X.Y);
return [e1, e2, e3];
}
static string CanonKey(Geometries.Edge e)
{
var abFirst = (e.X1 < e.X2) || (Almost(e.X1, e.X2) && e.Y1 <= e.Y2);
var ax = abFirst ? e.X1 : e.X2;
var ay = abFirst ? e.Y1 : e.Y2;
var bx = abFirst ? e.X2 : e.X1;
var by = abFirst ? e.Y2 : e.Y1;
static double Q(double v) => Math.Round(v, 9, MidpointRounding.AwayFromZero);
return $"{Q(ax)},{Q(ay)}|{Q(bx)},{Q(by)}";
}
void DrawCircles(IEnumerable<Geometries.Triangle> triangles, Geometries.Point superA, Geometries.Point superB,
Geometries.Point superC)
{
Circles.Clear();
foreach (var t in triangles)
{
if (UsesVertex(t, superA) || UsesVertex(t, superB) || UsesVertex(t, superC)) continue;
try
{
var a = new Geometries.Point(t.X.X, t.X.Y);
var b = new Geometries.Point(t.Y.X, t.Y.Y);
var c = new Geometries.Point(t.Z.X, t.Z.Y);
var cc = new Geometries.Circle(a, b, c);
if (double.IsFinite(cc.X) && double.IsFinite(cc.Y) && double.IsFinite(cc.R) && cc.R > 0)
Circles.Add(cc);
}
catch (ArgumentException)
{
}
}
}
async Task BowyerWatson(CancellationToken ct)
{
Triangles.Clear();
if (Points.Count < 3) return;
var (sa, sb, sc) = BuildSuperTriangle();
Triangles.Add(new Geometries.Triangle(sa, sb, sc));
await DelayAsync(ct);
foreach (var p in Points)
{
ct.ThrowIfCancellationRequested();
var bad = new List<Geometries.Triangle>();
foreach (var t in Triangles)
{
try
{
if (IsPointInCircumcircle(t, p)) bad.Add(t);
}
catch (ArgumentException)
{
// kollinear -> explicit ignore
}
}
DrawCircles(bad, sa, sb, sc);
await DelayAsync(ct);
var edgeCount = new Dictionary<string, Geometries.Edge>();
var counter = new Dictionary<string, int>();
foreach (var bt in bad)
{
foreach (var e in ExtractTriangleEdges(bt))
{
string key = CanonKey(e);
if (!edgeCount.ContainsKey(key)) edgeCount[key] = e;
counter.TryGetValue(key, out int c);
counter[key] = c + 1;
}
}
var polygon = new List<Geometries.Edge>();
foreach (var kv in counter)
if (kv.Value == 1)
polygon.Add(edgeCount[kv.Key]);
// remove bad triangles form triangulation
foreach (var bt in bad) Triangles.Remove(bt);
// re-triangulate the polygonal hole
foreach (var e in polygon)
{
var a = new Geometries.Point(e.X1, e.Y1);
var b = new Geometries.Point(e.X2, e.Y2);
Triangles.Add(new Geometries.Triangle(a, b, p));
await DelayAsync(ct);
}
}
// remove triangles containing super triangle vertex
var toRemove = new List<Geometries.Triangle>();
foreach (var t in Triangles)
{
if (UsesVertex(t, sa) || UsesVertex(t, sb) || UsesVertex(t, sc)) toRemove.Add(t);
}
foreach (var t in toRemove) Triangles.Remove(t);
DrawCircles(Triangles, sa, sb, sc);
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);
await BowyerWatson(ct);
}
private bool CanGenerate() => true;
public MainWindowViewModel()
{
GenerateCommand = new AsyncRelayCommand(RunGenerateAsync, CanGenerate);
}
}