@@ -9,9 +9,7 @@ public partial class Index(IJSRuntime JS, ILogger<Index> logger) : ComponentBase
99{
1010 private VoronoiGameState State { get ; set ; } = new ( ) ;
1111 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
12+ private int CanvasSize = 600 ;
1513 private bool isUpdating = false ;
1614
1715 private class DOMRectData
@@ -28,9 +26,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
2826 {
2927 // Measure the actual SVG size and use it for calculations
3028 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 ) ;
29+ CanvasSize = ( int ) rect . Width ;
30+ logger . LogInformation ( "SVG dimensions: {Width}x{Height}" , CanvasSize , CanvasSize ) ;
3431 StateHasChanged ( ) ;
3532 }
3633 }
@@ -39,11 +36,11 @@ private async Task OnCanvasClick(MouseEventArgs e)
3936 {
4037 // Get click position relative to SVG
4138 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 ) ;
39+ int x = ( int ) ( ( e . ClientX - rect . Left ) * CanvasSize / rect . Width ) ;
40+ int y = ( int ) ( ( e . ClientY - rect . Top ) * CanvasSize / rect . Height ) ;
4441
45- x = Math . Max ( 0 , Math . Min ( CanvasWidth - 1 , x ) ) ;
46- y = Math . Max ( 0 , Math . Min ( CanvasHeight - 1 , y ) ) ;
42+ x = Math . Max ( 0 , Math . Min ( CanvasSize - 1 , x ) ) ;
43+ y = Math . Max ( 0 , Math . Min ( CanvasSize - 1 , y ) ) ;
4744
4845 logger . LogInformation ( "SVG clicked at ({X}, {Y})" , x , y ) ;
4946 State . AddPoint ( x , y ) ;
@@ -74,14 +71,14 @@ private async Task UpdateVoronoiDiagram()
7471 isUpdating = true ;
7572 try
7673 {
77- if ( State . GeneratingPoints . Count == 0 )
74+ if ( State . VoronoiPoints . Count == 0 )
7875 {
7976 Regions . Clear ( ) ;
8077 return ;
8178 }
8279
8380 // Compute regions on background thread
84- Regions = await Task . Run ( ( ) => ComputeVoronoiRegions ( ) ) ;
81+ Regions = await Task . Run ( ( ) => State . BuildVoronoiDiagram ( CanvasSize ) ) ;
8582 logger . LogInformation ( "Voronoi diagram computed with {RegionCount} regions." , Regions . Count ) ;
8683 }
8784 finally
@@ -90,171 +87,4 @@ private async Task UpdateVoronoiDiagram()
9087 StateHasChanged ( ) ;
9188 }
9289 }
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 ;
257-
258- return Math . Sqrt ( Math . Pow ( point . x - closestX , 2 ) + Math . Pow ( point . y - closestY , 2 ) ) ;
259- }
26090}
0 commit comments