|
| 1 | +using Home.Pages.Games.Voronoi.Models; |
| 2 | +using Microsoft.AspNetCore.Components; |
| 3 | +using Microsoft.AspNetCore.Components.Web; |
| 4 | +using Microsoft.JSInterop; |
| 5 | + |
1 | 6 | namespace Home.Pages.Games.Voronoi; |
2 | 7 |
|
3 | | -public partial class Index |
| 8 | +public partial class Index(IJSRuntime JS, ILogger<Index> logger) : ComponentBase |
4 | 9 | { |
| 10 | + private VoronoiGameState State { get; set; } = new(); |
| 11 | + private List<VoronoiRegion> Regions { get; set; } = []; |
| 12 | + private int CanvasWidth = 600; |
| 13 | + private int CanvasHeight = 600; // Keep square |
| 14 | + private const int GridResolution = 4; // Sample every 4 pixels for boundary detection |
| 15 | + private bool isUpdating = false; |
| 16 | + |
| 17 | + private class DOMRectData |
| 18 | + { |
| 19 | + public double Left { get; set; } |
| 20 | + public double Top { get; set; } |
| 21 | + public double Width { get; set; } |
| 22 | + public double Height { get; set; } |
| 23 | + } |
| 24 | + |
| 25 | + protected override async Task OnAfterRenderAsync(bool firstRender) |
| 26 | + { |
| 27 | + if (firstRender) |
| 28 | + { |
| 29 | + // Measure the actual SVG size and use it for calculations |
| 30 | + DOMRectData rect = await JS.InvokeAsync<DOMRectData>("eval", "document.querySelector('svg').getBoundingClientRect()"); |
| 31 | + CanvasWidth = (int)rect.Width; |
| 32 | + CanvasHeight = (int)rect.Height; |
| 33 | + logger.LogInformation("SVG dimensions: {Width}x{Height}", CanvasWidth, CanvasHeight); |
| 34 | + StateHasChanged(); |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + private async Task OnCanvasClick(MouseEventArgs e) |
| 39 | + { |
| 40 | + // Get click position relative to SVG |
| 41 | + DOMRectData rect = await JS.InvokeAsync<DOMRectData>("eval", "document.querySelector('svg').getBoundingClientRect()"); |
| 42 | + int x = (int)((e.ClientX - rect.Left) * CanvasWidth / rect.Width); |
| 43 | + int y = (int)((e.ClientY - rect.Top) * CanvasHeight / rect.Height); |
| 44 | + |
| 45 | + x = Math.Max(0, Math.Min(CanvasWidth - 1, x)); |
| 46 | + y = Math.Max(0, Math.Min(CanvasHeight - 1, y)); |
| 47 | + |
| 48 | + logger.LogInformation("SVG clicked at ({X}, {Y})", x, y); |
| 49 | + State.AddPoint(x, y); |
| 50 | + await UpdateVoronoiDiagram(); |
| 51 | + } |
| 52 | + |
| 53 | + private async Task OnManhattanToggle(ChangeEventArgs e) |
| 54 | + { |
| 55 | + State.ToggleDistanceMode(); |
| 56 | + await UpdateVoronoiDiagram(); |
| 57 | + } |
| 58 | + |
| 59 | + private async Task OnClear() |
| 60 | + { |
| 61 | + State.Clear(); |
| 62 | + Regions.Clear(); |
| 63 | + logger.LogInformation("Voronoi diagram cleared."); |
| 64 | + await Task.CompletedTask; |
| 65 | + } |
| 66 | + |
| 67 | + private async Task UpdateVoronoiDiagram() |
| 68 | + { |
| 69 | + if (isUpdating) |
| 70 | + { |
| 71 | + return; |
| 72 | + } |
| 73 | + |
| 74 | + isUpdating = true; |
| 75 | + try |
| 76 | + { |
| 77 | + if (State.GeneratingPoints.Count == 0) |
| 78 | + { |
| 79 | + Regions.Clear(); |
| 80 | + return; |
| 81 | + } |
| 82 | + |
| 83 | + // Compute regions on background thread |
| 84 | + Regions = await Task.Run(() => ComputeVoronoiRegions()); |
| 85 | + logger.LogInformation("Voronoi diagram computed with {RegionCount} regions.", Regions.Count); |
| 86 | + } |
| 87 | + finally |
| 88 | + { |
| 89 | + isUpdating = false; |
| 90 | + StateHasChanged(); |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + private List<VoronoiRegion> ComputeVoronoiRegions() |
| 95 | + { |
| 96 | + // Create a grid mapping each cell to its closest point |
| 97 | + int gridWidth = (CanvasWidth / GridResolution) + 1; |
| 98 | + int gridHeight = (CanvasHeight / GridResolution) + 1; |
| 99 | + VoronoiPoint[,] grid = new VoronoiPoint[gridWidth, gridHeight]; |
| 100 | + |
| 101 | + // Sample the grid |
| 102 | + for (int gy = 0; gy < gridHeight; gy++) |
| 103 | + { |
| 104 | + for (int gx = 0; gx < gridWidth; gx++) |
| 105 | + { |
| 106 | + int sampleX = gx * GridResolution; |
| 107 | + int sampleY = gy * GridResolution; |
| 108 | + |
| 109 | + VoronoiPoint closestPoint = State.GeneratingPoints[0]; |
| 110 | + int closestDist = closestPoint.Distance(sampleX, sampleY, State.ManhattanDistance); |
| 111 | + |
| 112 | + for (int i = 1; i < State.GeneratingPoints.Count; i++) |
| 113 | + { |
| 114 | + int dist = State.GeneratingPoints[i].Distance(sampleX, sampleY, State.ManhattanDistance); |
| 115 | + if (dist < closestDist) |
| 116 | + { |
| 117 | + closestDist = dist; |
| 118 | + closestPoint = State.GeneratingPoints[i]; |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + grid[gx, gy] = closestPoint; |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + // Find boundaries and create regions |
| 127 | + Dictionary<VoronoiPoint, List<(int x, int y)>> regions = []; |
| 128 | + HashSet<(int, int)> visited = []; |
| 129 | + |
| 130 | + for (int gy = 0; gy < gridHeight; gy++) |
| 131 | + { |
| 132 | + for (int gx = 0; gx < gridWidth; gx++) |
| 133 | + { |
| 134 | + VoronoiPoint point = grid[gx, gy]; |
| 135 | + |
| 136 | + if (!regions.ContainsKey(point)) |
| 137 | + { |
| 138 | + regions[point] = []; |
| 139 | + } |
| 140 | + |
| 141 | + // Add the pixel coordinates that are on the boundary of this point |
| 142 | + int pixelX = gx * GridResolution; |
| 143 | + int pixelY = gy * GridResolution; |
| 144 | + |
| 145 | + // Check if this is a boundary cell (different from a neighbor) |
| 146 | + bool isBoundary = false; |
| 147 | + if (gx == 0 || gx == gridWidth - 1 || gy == 0 || gy == gridHeight - 1) |
| 148 | + { |
| 149 | + isBoundary = true; |
| 150 | + } |
| 151 | + else if (gx > 0 && grid[gx - 1, gy] != point) |
| 152 | + { |
| 153 | + isBoundary = true; |
| 154 | + } |
| 155 | + else if (gx < gridWidth - 1 && grid[gx + 1, gy] != point) |
| 156 | + { |
| 157 | + isBoundary = true; |
| 158 | + } |
| 159 | + else if (gy > 0 && grid[gx, gy - 1] != point) |
| 160 | + { |
| 161 | + isBoundary = true; |
| 162 | + } |
| 163 | + else if (gy < gridHeight - 1 && grid[gx, gy + 1] != point) |
| 164 | + { |
| 165 | + isBoundary = true; |
| 166 | + } |
| 167 | + |
| 168 | + if (isBoundary && !visited.Contains((gx, gy))) |
| 169 | + { |
| 170 | + regions[point].Add((pixelX, pixelY)); |
| 171 | + visited.Add((gx, gy)); |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + // Convert to VoronoiRegion objects and compute final polygon boundaries |
| 177 | + List<VoronoiRegion> result = []; |
| 178 | + foreach ((VoronoiPoint? point, List<(int x, int y)>? pixels) in regions) |
| 179 | + { |
| 180 | + VoronoiRegion region = new() |
| 181 | + { |
| 182 | + Point = point |
| 183 | + }; |
| 184 | + |
| 185 | + if (pixels.Count > 0) |
| 186 | + { |
| 187 | + // Sort them radially around the point's center |
| 188 | + List<(int x, int y)> sortedPixels = pixels |
| 189 | + .OrderBy(p => Math.Atan2(p.y - point.Y, p.x - point.X)) |
| 190 | + .ToList(); |
| 191 | + |
| 192 | + // Simplify the polygon to smooth jagged edges |
| 193 | + List<(int x, int y)> simplifiedPixels = SimplifyPolygon(sortedPixels, tolerance: 10); |
| 194 | + region.PolygonPoints = simplifiedPixels; |
| 195 | + } |
| 196 | + |
| 197 | + if (region.PolygonPoints.Count > 2) |
| 198 | + { |
| 199 | + result.Add(region); |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + return result; |
| 204 | + } |
| 205 | + |
| 206 | + private List<(int x, int y)> SimplifyPolygon(List<(int x, int y)> points, double tolerance) |
| 207 | + { |
| 208 | + if (points.Count <= 2) |
| 209 | + { |
| 210 | + return points; |
| 211 | + } |
| 212 | + |
| 213 | + // Douglas-Peucker algorithm for polygon simplification |
| 214 | + double dmax = 0.0; |
| 215 | + int index = 0; |
| 216 | + |
| 217 | + for (int i = 1; i < points.Count - 1; i++) |
| 218 | + { |
| 219 | + double d = PointToLineDistance(points[i], points[0], points[^1]); |
| 220 | + if (d > dmax) |
| 221 | + { |
| 222 | + index = i; |
| 223 | + dmax = d; |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + if (dmax > tolerance) |
| 228 | + { |
| 229 | + List<(int x, int y)> rec1 = SimplifyPolygon(points.GetRange(0, index + 1), tolerance); |
| 230 | + List<(int x, int y)> rec2 = SimplifyPolygon(points.GetRange(index, points.Count - index), tolerance); |
| 231 | + |
| 232 | + List<(int x, int y)> result = rec1.GetRange(0, rec1.Count - 1); |
| 233 | + result.AddRange(rec2); |
| 234 | + return result; |
| 235 | + } |
| 236 | + else |
| 237 | + { |
| 238 | + return [points[0], points[^1]]; |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + private double PointToLineDistance((int x, int y) point, (int x, int y) lineStart, (int x, int y) lineEnd) |
| 243 | + { |
| 244 | + int dx = lineEnd.x - lineStart.x; |
| 245 | + int dy = lineEnd.y - lineStart.y; |
| 246 | + |
| 247 | + if (dx == 0 && dy == 0) |
| 248 | + { |
| 249 | + return Math.Sqrt(Math.Pow(point.x - lineStart.x, 2) + Math.Pow(point.y - lineStart.y, 2)); |
| 250 | + } |
| 251 | + |
| 252 | + int t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (dx * dx + dy * dy); |
| 253 | + t = Math.Max(0, Math.Min(1, t)); |
| 254 | + |
| 255 | + int closestX = lineStart.x + t * dx; |
| 256 | + int closestY = lineStart.y + t * dy; |
5 | 257 |
|
| 258 | + return Math.Sqrt(Math.Pow(point.x - closestX, 2) + Math.Pow(point.y - closestY, 2)); |
| 259 | + } |
6 | 260 | } |
0 commit comments