@@ -28,6 +28,7 @@ import { ScrollCapability, ScrollPlugin } from '@embedpdf/plugin-scroll';
2828
2929import {
3030 cachePageGeometry ,
31+ evictPageGeometry ,
3132 setSelection ,
3233 SelectionAction ,
3334 endSelection ,
@@ -93,6 +94,9 @@ export class SelectionPlugin extends BasePlugin<
9394 /** Page callbacks for rect updates, per document */
9495 private pageCallbacks = new Map < string , Map < number , ( data : SelectionRectsCallback ) => void > > ( ) ;
9596
97+ /** LRU access order for geometry cache, per document (oldest first) */
98+ private geoAccessOrder = new Map < string , number [ ] > ( ) ;
99+
96100 private readonly menuPlacement$ = createScopedEmitter <
97101 SelectionMenuPlacement | null ,
98102 SelectionMenuPlacementEvent ,
@@ -229,6 +233,7 @@ export class SelectionPlugin extends BasePlugin<
229233 ] ) ,
230234 ) ;
231235 this . pageCallbacks . set ( documentId , new Map ( ) ) ;
236+ this . geoAccessOrder . set ( documentId , [ ] ) ;
232237 this . selecting . set ( documentId , false ) ;
233238 this . anchor . set ( documentId , undefined ) ;
234239 this . hasTextAnchor . set ( documentId , false ) ;
@@ -238,6 +243,7 @@ export class SelectionPlugin extends BasePlugin<
238243 this . dispatch ( cleanupSelectionState ( documentId ) ) ;
239244 this . enabledModesPerDoc . delete ( documentId ) ;
240245 this . pageCallbacks . delete ( documentId ) ;
246+ this . geoAccessOrder . delete ( documentId ) ;
241247 this . selecting . delete ( documentId ) ;
242248 this . hasTextAnchor . delete ( documentId ) ;
243249 this . anchor . delete ( documentId ) ;
@@ -395,6 +401,27 @@ export class SelectionPlugin extends BasePlugin<
395401 boundingRect : selector . selectBoundingRectForPage ( docState , pageIndex ) ,
396402 } ) ;
397403
404+ // When geometry arrives (possibly re-fetched after eviction), recompute
405+ // rects for this page if it falls within an active selection.
406+ geoTask . wait ( ( geo ) => {
407+ const currentState = this . getDocumentState ( documentId ) ;
408+ const sel = currentState . selection ;
409+ if ( ! sel || pageIndex < sel . start . page || pageIndex > sel . end . page ) return ;
410+
411+ const sb = sliceBounds ( sel , geo , pageIndex ) ;
412+ if ( ! sb ) return ;
413+
414+ const pageRects = rectsWithinSlice ( geo , sb . from , sb . to ) ;
415+ this . dispatch ( setRects ( documentId , { ...currentState . rects , [ pageIndex ] : pageRects } ) ) ;
416+ this . dispatch (
417+ setSlices ( documentId , {
418+ ...currentState . slices ,
419+ [ pageIndex ] : { start : sb . from , count : sb . to - sb . from + 1 } ,
420+ } ) ,
421+ ) ;
422+ this . notifyPage ( documentId , pageIndex ) ;
423+ } , ignore ) ;
424+
398425 // Create text selection handler
399426 const textHandler = createTextSelectionHandler ( {
400427 getGeometry : ( ) => this . getDocumentState ( documentId ) . geometry [ pageIndex ] ,
@@ -690,18 +717,59 @@ export class SelectionPlugin extends BasePlugin<
690717 const task = this . engine . getPageGeometry ( coreDoc . document , page ) ;
691718 task . wait ( ( geo ) => {
692719 this . dispatch ( cachePageGeometry ( documentId , pageIdx , geo ) ) ;
720+ this . touchGeometry ( documentId , pageIdx ) ;
693721 } , ignore ) ;
694722 return task ;
695723 }
696724
697725 /* ── geometry cache ───────────────────────────────────── */
698726 private getOrLoadGeometry ( documentId : string , pageIdx : number ) : PdfTask < PdfPageGeometry > {
699727 const cached = this . getDocumentState ( documentId ) . geometry [ pageIdx ] ;
700- if ( cached ) return PdfTaskHelper . resolve ( cached ) ;
728+ if ( cached ) {
729+ this . touchGeometry ( documentId , pageIdx ) ;
730+ return PdfTaskHelper . resolve ( cached ) ;
731+ }
701732
702733 return this . getNewPageGeometryAndCache ( documentId , pageIdx ) ;
703734 }
704735
736+ /* ── geometry LRU eviction ──────────────────────────────── */
737+
738+ private touchGeometry ( documentId : string , pageIdx : number ) : void {
739+ const order = this . geoAccessOrder . get ( documentId ) ;
740+ if ( ! order ) return ;
741+
742+ const idx = order . indexOf ( pageIdx ) ;
743+ if ( idx > - 1 ) order . splice ( idx , 1 ) ;
744+ order . push ( pageIdx ) ;
745+
746+ this . evictGeometryIfNeeded ( documentId ) ;
747+ }
748+
749+ private evictGeometryIfNeeded ( documentId : string ) : void {
750+ const max = this . config . maxCachedGeometries ?? 50 ;
751+ const order = this . geoAccessOrder . get ( documentId ) ;
752+ if ( ! order || order . length <= max ) return ;
753+
754+ const pinned = this . pageCallbacks . get ( documentId ) ;
755+ const toEvict : number [ ] = [ ] ;
756+
757+ while ( order . length - toEvict . length > max ) {
758+ const candidate = order . find ( ( p ) => ! toEvict . includes ( p ) && ! pinned ?. has ( p ) ) ;
759+ if ( candidate === undefined ) break ;
760+ toEvict . push ( candidate ) ;
761+ }
762+
763+ if ( toEvict . length === 0 ) return ;
764+
765+ for ( const p of toEvict ) {
766+ const idx = order . indexOf ( p ) ;
767+ if ( idx > - 1 ) order . splice ( idx , 1 ) ;
768+ }
769+
770+ this . dispatch ( evictPageGeometry ( documentId , toEvict ) ) ;
771+ }
772+
705773 /* ── selection state updates ───────────────────────────── */
706774 private beginSelection ( documentId : string , page : number , index : number , modeId : string ) {
707775 this . selecting . set ( documentId , true ) ;
0 commit comments