@@ -204,21 +204,29 @@ let currentGrid: FeatureCollection<Polygon> = { type: "FeatureCollection", featu
204204let currentError : string | null = null ;
205205let cachedTextFont : string [ ] | null = null ;
206206let pendingRefresh : number | null = null ;
207+ /** Bumped on every activate/deactivate so an in-flight WASM load cannot attach after teardown. */
208+ let activationGeneration = 0 ;
207209
208210let dggsPromise : Promise < DggridEngine > | null = null ;
209211
210212/**
211213 * Load the webdggrid WASM module once and reuse the instance. Imported
212214 * dynamically so the ~270 kB module stays out of the main bundle until the
213215 * plugin is activated. `Webdggrid.load()` is typed as returning the class but
214- * resolves to an instance, hence the cast.
216+ * resolves to an instance, hence the cast. A failed load clears the cache so
217+ * the next activate can retry.
215218 */
216219export function loadDggrid ( ) : Promise < DggridEngine > {
217- dggsPromise ??= import ( "webdggrid" ) . then ( async ( module ) => {
218- const instance = ( await module . Webdggrid . load ( ) ) as unknown as DggridEngine ;
219- instance . setDggs ( { ...DGGRID_CONFIG } , DEFAULT_DGGRID_GRID_SETTINGS . resolution ) ;
220- return instance ;
221- } ) ;
220+ dggsPromise ??= import ( "webdggrid" )
221+ . then ( async ( module ) => {
222+ const instance = ( await module . Webdggrid . load ( ) ) as unknown as DggridEngine ;
223+ instance . setDggs ( { ...DGGRID_CONFIG } , DEFAULT_DGGRID_GRID_SETTINGS . resolution ) ;
224+ return instance ;
225+ } )
226+ . catch ( ( error ) => {
227+ dggsPromise = null ;
228+ throw error ;
229+ } ) ;
222230 return dggsPromise ;
223231}
224232
@@ -496,13 +504,32 @@ function ringIntersectsBounds(
496504 north : number ,
497505) : boolean {
498506 if ( ! ring ?. length ) return false ;
499- const last = ring . length - 1 ;
500- const closed = ring [ 0 ] [ 0 ] === ring [ last ] [ 0 ] && ring [ 0 ] [ 1 ] === ring [ last ] [ 1 ] ;
501- const count = closed ? ring . length - 1 : ring . length ;
507+ // Unwrap for continuity, then shift into the rect's longitude window so
508+ // vertex / containment / segment tests share one frame (raw ±180 mixes break
509+ // antimeridian cells).
510+ const framed : Position [ ] = [ ] ;
511+ for ( const [ rawLon , lat ] of ring ) {
512+ let lon = rawLon ;
513+ if ( framed . length > 0 ) {
514+ const reference = framed [ 0 ] [ 0 ] ;
515+ while ( lon - reference > 180 ) lon -= 360 ;
516+ while ( lon - reference < - 180 ) lon += 360 ;
517+ }
518+ framed . push ( [ lon , lat ] ) ;
519+ }
520+ const mid = ( west + east ) / 2 ;
521+ const shift = Math . round ( ( mid - framed [ 0 ] [ 0 ] ) / 360 ) * 360 ;
522+ const normalized =
523+ shift === 0 ? framed : framed . map ( ( [ lon , lat ] ) => [ lon + shift , lat ] as Position ) ;
524+
525+ const last = normalized . length - 1 ;
526+ const closed =
527+ normalized [ 0 ] [ 0 ] === normalized [ last ] [ 0 ] && normalized [ 0 ] [ 1 ] === normalized [ last ] [ 1 ] ;
528+ const count = closed ? normalized . length - 1 : normalized . length ;
502529
503530 for ( let i = 0 ; i < count ; i += 1 ) {
504- const lon = normalizeLon ( ring [ i ] [ 0 ] ) ;
505- const lat = ring [ i ] [ 1 ] ;
531+ const lon = normalized [ i ] [ 0 ] ;
532+ const lat = normalized [ i ] [ 1 ] ;
506533 if ( lon >= west && lon <= east && lat >= south && lat <= north ) return true ;
507534 }
508535 for ( const [ lon , lat ] of [
@@ -512,16 +539,16 @@ function ringIntersectsBounds(
512539 [ east , north ] ,
513540 [ ( west + east ) / 2 , ( south + north ) / 2 ] ,
514541 ] ) {
515- if ( pointInRing ( lon , lat , ring ) ) return true ;
542+ if ( pointInRing ( lon , lat , normalized ) ) return true ;
516543 }
517544 for ( let i = 0 ; i < count ; i += 1 ) {
518545 const j = ( i + 1 ) % count ;
519546 if (
520547 segmentCrossesLonLatRect (
521- normalizeLon ( ring [ i ] [ 0 ] ) ,
522- ring [ i ] [ 1 ] ,
523- normalizeLon ( ring [ j ] [ 0 ] ) ,
524- ring [ j ] [ 1 ] ,
548+ normalized [ i ] [ 0 ] ,
549+ normalized [ i ] [ 1 ] ,
550+ normalized [ j ] [ 0 ] ,
551+ normalized [ j ] [ 1 ] ,
525552 west ,
526553 south ,
527554 east ,
@@ -833,7 +860,20 @@ function refresh(): void {
833860 applyStyle ( ) ;
834861 ( map . getSource ( SOURCE_ID ) as GeoJSONSource | undefined ) ?. setData ( currentGrid ) ;
835862 updateSelectedSource ( ) ;
836- if ( panelContainer ) renderPanel ( panelContainer ) ;
863+ // Pan/zoom only changes the cell count status — rebuild the whole panel and
864+ // the open color picker / focused inputs are destroyed mid-gesture.
865+ updatePanelStatus ( ) ;
866+ }
867+
868+ /** Update the status line without recreating the rest of the panel controls. */
869+ function updatePanelStatus ( ) : void {
870+ const status = panelContainer ?. querySelector < HTMLElement > ( "[data-dggrid-status]" ) ;
871+ if ( ! status ) {
872+ if ( panelContainer ) renderPanel ( panelContainer ) ;
873+ return ;
874+ }
875+ status . textContent = currentError ?? labels . cellCount ( currentGrid . features . length ) ;
876+ status . style . color = currentError ? "#dc2626" : "" ;
837877}
838878
839879/**
@@ -1085,6 +1125,7 @@ function renderPanel(container: HTMLElement): void {
10851125 }
10861126
10871127 const status = document . createElement ( "div" ) ;
1128+ status . dataset . dggridStatus = "" ;
10881129 status . textContent = currentError ?? labels . cellCount ( currentGrid . features . length ) ;
10891130 status . style . color = currentError ? "#dc2626" : "" ;
10901131 section . appendChild ( status ) ;
@@ -1210,7 +1251,12 @@ export const maplibreDggridPlugin: GeoLibrePlugin = {
12101251 activate : async ( app ) => {
12111252 const activeMap = app . getMap ?.( ) ;
12121253 if ( ! activeMap ) return false ;
1213- dggs = await loadDggrid ( ) ;
1254+ const generation = ( activationGeneration += 1 ) ;
1255+ // Await WASM before mutating map/panel state so a deactivate during the
1256+ // load cannot race a late attach (leaked listeners / panels / layers).
1257+ const engine = await loadDggrid ( ) ;
1258+ if ( generation !== activationGeneration ) return false ;
1259+ dggs = engine ;
12141260 map = activeMap ;
12151261 appRef = app ;
12161262 moveHandler = ( ) => scheduleRefresh ( ) ;
@@ -1249,6 +1295,7 @@ export const maplibreDggridPlugin: GeoLibrePlugin = {
12491295 app . openRightPanel ?.( PANEL_ID ) ;
12501296 } ,
12511297 deactivate : ( app ) => {
1298+ activationGeneration += 1 ;
12521299 cancelScheduledRefresh ( ) ;
12531300 if ( map && moveHandler ) map . off ( "moveend" , moveHandler ) ;
12541301 if ( map && clickHandler ) map . off ( "click" , clickHandler ) ;
@@ -1265,6 +1312,7 @@ export const maplibreDggridPlugin: GeoLibrePlugin = {
12651312 currentGrid = { type : "FeatureCollection" , features : [ ] } ;
12661313 currentError = null ;
12671314 cachedTextFont = null ;
1315+ dggs = null ;
12681316 map = null ;
12691317 appRef = null ;
12701318 app . closeRightPanel ?.( PANEL_ID ) ;
0 commit comments