@@ -3,6 +3,10 @@ import { forEachTileInBounds } from '../overlays/utils.js';
33import { DataCache } from '../utils/DataCache.js' ;
44import { SRGBColorSpace , CanvasTexture } from 'three' ;
55
6+ // Epsilon for comparing normalized tile bounds to determine if a region exactly matches a single
7+ // image tile.
8+ const BOUNDS_EPSILON = 1e-10 ;
9+
610export class RegionImageSource extends DataCache {
711
812 hasContent ( ...tokens ) {
@@ -21,6 +25,8 @@ export class TiledRegionImageSource extends RegionImageSource {
2125 this . tiledImageSource = tiledImageSource ;
2226 this . tileComposer = new TiledTextureComposer ( ) ;
2327 this . resolution = 256 ;
28+ this . IS_DIRECT_TILE = Symbol ( 'IS_DIRECT_TILE' ) ;
29+ this . LOCK_TOKENS = Symbol ( 'LOCK_TOKENS' ) ;
2430
2531 }
2632
@@ -40,22 +46,63 @@ export class TiledRegionImageSource extends RegionImageSource {
4046
4147 async fetchItem ( [ minX , minY , maxX , maxY , level ] , signal ) {
4248
49+ const { tiledImageSource, tileComposer, IS_DIRECT_TILE , LOCK_TOKENS } = this ;
4350 const range = [ minX , minY , maxX , maxY ] ;
44- const imageSource = this . tiledImageSource ;
45- const tileComposer = this . tileComposer ;
46- const tiling = imageSource . tiling ;
51+ const tiling = tiledImageSource . tiling ;
52+ const tokens = [ ...range , level ] ;
53+
54+ // lock tiles for the requested level
55+ await this . _markImages ( range , level , false ) ;
56+
57+ //
58+
59+ // Fast path: if the range maps to exactly one tile with matching bounds, use its
60+ // texture directly without compositing into an intermediate canvas to save memory.
61+ let singleTileBounds = null ;
62+ let tileCount = 0 ;
63+ forEachTileInBounds ( range , level , tiling , ( tx , ty , tl ) => {
64+
65+ tileCount ++ ;
66+ singleTileBounds = [ tx , ty , tl ] ;
67+
68+ } ) ;
69+
70+ if ( tileCount === 1 ) {
71+
72+ const [ tx , ty , tl ] = singleTileBounds ;
73+ const tileBounds = tiling . getTileBounds ( tx , ty , tl , true , false ) ;
74+ if (
75+ Math . abs ( tileBounds [ 0 ] - minX ) <= BOUNDS_EPSILON &&
76+ Math . abs ( tileBounds [ 1 ] - minY ) <= BOUNDS_EPSILON &&
77+ Math . abs ( tileBounds [ 2 ] - maxX ) <= BOUNDS_EPSILON &&
78+ Math . abs ( tileBounds [ 3 ] - maxY ) <= BOUNDS_EPSILON
79+ ) {
80+
81+ // Clone rather than returning the texture directly so each region cache entry owns
82+ // a distinct object. Returning the shared texture would cause symbol properties
83+ // to be overwritten or deleted by concurrent cache entries during race conditions,
84+ // (create, delete, create) leading to errors on disposal.
85+ // Cloning shares the underlying Source so no extra GPU memory is used.
86+ const clone = tiledImageSource . get ( tx , ty , tl ) . clone ( ) ;
87+ clone [ IS_DIRECT_TILE ] = true ;
88+ clone [ LOCK_TOKENS ] = tokens ;
89+ return clone ;
90+
91+ }
92+
93+ }
94+
95+ //
4796
97+ // Compose path: tiles must be composed into a single texture
4898 const canvas = document . createElement ( 'canvas' ) ;
4999 canvas . width = this . resolution ;
50100 canvas . height = this . resolution ;
51101
52102 const target = new CanvasTexture ( canvas ) ;
53103 target . colorSpace = SRGBColorSpace ;
54104 target . generateMipmaps = false ;
55- target . tokens = [ ...range , level ] ;
56-
57- // Start locking tiles for the requested level
58- await this . _markImages ( range , level , false ) ;
105+ target [ LOCK_TOKENS ] = tokens ;
59106
60107 // TODO: we could draw the parent tile data here if it's available just to make sure we
61108 // have something to display but the texture is not usable until it returns. Though it
@@ -69,7 +116,7 @@ export class TiledRegionImageSource extends RegionImageSource {
69116
70117 // draw using normalized bounds since the mercator bounds are non-linear
71118 const span = tiling . getTileBounds ( tx , ty , tl , true , false ) ;
72- const tex = imageSource . get ( tx , ty , tl ) ;
119+ const tex = tiledImageSource . get ( tx , ty , tl ) ;
73120 tileComposer . draw ( tex , span ) ;
74121
75122 } ) ;
@@ -80,10 +127,14 @@ export class TiledRegionImageSource extends RegionImageSource {
80127
81128 disposeItem ( target ) {
82129
130+ const { IS_DIRECT_TILE , LOCK_TOKENS } = this ;
131+ const [ minX , minY , maxX , maxY , level ] = target [ LOCK_TOKENS ] ;
132+
83133 target . dispose ( ) ;
134+ delete target [ IS_DIRECT_TILE ] ;
135+ delete target [ LOCK_TOKENS ] ;
84136
85- // Unlock the component tiles using the tokens stored on the target
86- const [ minX , minY , maxX , maxY , level ] = target . tokens ;
137+ // Unlock the component tiles using the stored tokens
87138 this . _markImages ( [ minX , minY , maxX , maxY ] , level , true ) ;
88139
89140 }
0 commit comments