Delaunay Triangulation - currently with DelaunatorSharp - VoronoiDiagram work in progress
This commit is contained in:
@@ -20,5 +20,7 @@
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.6"/>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
|
||||
<PackageReference Include="Delaunator" Version="1.0.11" />
|
||||
<PackageReference Include="Xaml.Behaviors.Interactions" Version="11.3.6.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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<Point>
|
||||
{
|
||||
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<Avalonia.Point> Vertices => [ X, Y, Z ];
|
||||
}
|
||||
}
|
||||
@@ -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<Geometries.Point> Points { get; } = new();
|
||||
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();
|
||||
|
||||
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<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++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
>
|
||||
|
||||
<Design.DataContext>
|
||||
@@ -42,24 +42,61 @@
|
||||
</Border>
|
||||
<Grid Grid.Row="1" ZIndex="1000" Margin="20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Classes="InnerCard">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" Color="#aa000000" OffsetX="5" OffsetY="5"/>
|
||||
</Border.Effect>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Border Classes="InnerCard">
|
||||
<TextBlock Text="Delaunay Triangulation" FontSize="18" Foreground="White"/>
|
||||
</Border>
|
||||
<Border Classes="InnerCard">
|
||||
<Grid ColumnDefinitions="4*,1*">
|
||||
<Slider Grid.Column="0" Value="{Binding PointCount, Mode=TwoWay}" Margin="5" Minimum="20" Maximum="500"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding PointCount, Mode=TwoWay}" FontSize="18" Foreground="White" Background="#30ffffff" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Grid RowDefinitions="*, auto">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Border Classes="InnerCard">
|
||||
<TextBlock Text="Delaunay Triangulation" FontSize="18" Foreground="White"/>
|
||||
</Border>
|
||||
<Border Classes="InnerCard">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Maximal Point Count" Foreground="White"/>
|
||||
<Grid ColumnDefinitions="4*,1*">
|
||||
<Slider Grid.Column="0" Value="{Binding PointCount, Mode=TwoWay}" Margin="5" Minimum="20" Maximum="200"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding PointCount, Mode=TwoWay}" FontSize="18" Foreground="White" Background="#30ffffff" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="InnerCard">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Minimal Point Distance" Foreground="White"/>
|
||||
<Grid ColumnDefinitions="4*,1*">
|
||||
<Slider Grid.Column="0" Value="{Binding MinPointDistance, Mode=TwoWay}" Margin="5" Minimum="1" Maximum="10"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding MinPointDistance, Mode=TwoWay}" FontSize="18" Foreground="White" Background="#30ffffff" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="InnerCard">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Genearation Speed" Foreground="White"/>
|
||||
<Grid ColumnDefinitions="4*,1*">
|
||||
<Slider Grid.Column="0" Value="{Binding GenerationDelay, Mode=TwoWay}" Margin="5" Minimum="0" Maximum="1000"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding GenerationDelay, Mode=TwoWay}" FontSize="18" Foreground="White" Background="#30ffffff" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="InnerCard">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Enabled Visuals:" Foreground="White"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Content="Points" IsChecked="{Binding PointsVisible}" Foreground="White" Margin="10,0"/>
|
||||
<CheckBox Content="Convex Hull" IsChecked="{Binding EdgesVisible}" Foreground="White" Margin="10,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Content="Circles" IsChecked="{Binding CirclesVisible}" Foreground="White" Margin="10,0"/>
|
||||
<CheckBox Content="Triangles" IsChecked="{Binding TrianglesVisible}" Foreground="White" Margin="10,0"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button Grid.Row="1" Foreground="White" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Generate" Command="{Binding GenerateCommand}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Canvas Grid.Row="0" Grid.RowSpan="2"
|
||||
@@ -84,14 +121,19 @@
|
||||
IsHitTestVisible="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
<Canvas IsVisible="{Binding EdgesVisible}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Geometries+Edge">
|
||||
<Line StartPoint="{Binding Start}"
|
||||
EndPoint="{Binding End}"
|
||||
Stroke="LightGray" StrokeThickness="1"/>
|
||||
Stroke="GreenYellow" StrokeThickness="2"
|
||||
>
|
||||
<Line.RenderTransform>
|
||||
<TranslateTransform X="{Binding Start}" Y="{Binding End}"/>
|
||||
</Line.RenderTransform>
|
||||
</Line>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@@ -101,7 +143,7 @@
|
||||
IsHitTestVisible="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
<Canvas IsVisible="{Binding CirclesVisible}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
@@ -109,9 +151,33 @@
|
||||
<Ellipse Stroke="DarkOrange" StrokeThickness="1.5"
|
||||
Width="{Binding Diameter}"
|
||||
Height="{Binding Diameter}"
|
||||
Canvas.Left="{Binding Left}"
|
||||
Canvas.Top="{Binding Top}"
|
||||
/>
|
||||
>
|
||||
<Ellipse.RenderTransform>
|
||||
<TranslateTransform X="{Binding Left}" Y="{Binding Top}"/>
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ItemsControl ItemsSource="{Binding VoronoiDiagram}"
|
||||
Width="{Binding Bounds.Width, ElementName=Scene}"
|
||||
Height="{Binding Bounds.Height, ElementName=Scene}"
|
||||
IsHitTestVisible="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas IsVisible="{Binding CirclesVisible}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Geometries+Edge">
|
||||
<Line StartPoint="{Binding Start}"
|
||||
EndPoint="{Binding End}"
|
||||
Stroke="GreenYellow" StrokeThickness="2"
|
||||
>
|
||||
<Line.RenderTransform>
|
||||
<TranslateTransform X="{Binding Start}" Y="{Binding End}"/>
|
||||
</Line.RenderTransform>
|
||||
</Line>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@@ -122,23 +188,40 @@
|
||||
>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
<Canvas IsVisible="{Binding PointsVisible}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Geometries+Point">
|
||||
<Ellipse Fill="White" Stroke="Black" StrokeThickness="1"
|
||||
<Ellipse Fill="Transparent" Stroke="White" StrokeThickness="1"
|
||||
Width="{Binding Size}"
|
||||
Height="{Binding Size}"
|
||||
>
|
||||
<Ellipse.RenderTransform>
|
||||
<!-- Nutze Left/Top aus dem VM (oder X/Y, wenn das deine Top-Left-Koords sind) -->
|
||||
<TranslateTransform X="{Binding Left}" Y="{Binding Top}"/>
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ItemsControl ItemsSource="{Binding Triangles}"
|
||||
Width="{Binding Bounds.Width, ElementName=Scene}"
|
||||
Height="{Binding Bounds.Height, ElementName=Scene}"
|
||||
IsHitTestVisible="False"
|
||||
>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas IsVisible="{Binding TrianglesVisible}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Geometries+Triangle">
|
||||
<Canvas>
|
||||
<Polygon Points="{Binding Vertices}" Stroke="CornflowerBlue" StrokeThickness="1" Fill="Transparent"/>
|
||||
</Canvas>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Canvas>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user