@@ -69,8 +69,8 @@ class ModelSource extends Evented<SourceEvents> implements ISource {
6969 uri : string ;
7070 models : Array < Model > ;
7171 _options : ModelSourceSpecification ;
72+ _abortController : AbortController | null ;
7273
73- onRemove : undefined ;
7474 abortTile : undefined ;
7575 unloadTile : undefined ;
7676 hasTile : undefined ;
@@ -89,67 +89,82 @@ class ModelSource extends Evented<SourceEvents> implements ISource {
8989 this . models = [ ] ;
9090 this . _options = options ;
9191 this . _modelsInfo = new Map ( ) ;
92+ this . _abortController = null ;
9293 }
9394
94- private loadGLTFFromURI ( uri : string ) : Promise < void | GLTF > {
95- return loadGLTF ( this . map . _requestManager . transformRequest ( uri , ResourceType . Model ) . url ) ;
95+ private cancelModelRequests ( ) {
96+ if ( this . _abortController ) {
97+ this . _abortController . abort ( ) ;
98+ this . _abortController = null ;
99+ }
96100 }
97101
98- load ( ) : void {
99- for ( const modelId in this . _options . models ) {
100- const modelSpec = this . _options . models [ modelId ] ;
101-
102- const modelInfo = this . _modelsInfo . get ( modelId ) ;
103- if ( modelInfo ) {
104- modelInfo . modelSpec = modelSpec ;
105- // Update model if loaded
106- const model = modelInfo . model ;
107- if ( model ) {
108- model . position = modelSpec . position != null ? new LngLat ( modelSpec . position [ 0 ] , modelSpec . position [ 1 ] ) : new LngLat ( 0 , 0 ) ;
109- model . orientation = modelSpec . orientation != null ? modelSpec . orientation : [ 0 , 0 , 0 ] ;
110- ModelSource . applyModelSpecification ( model , modelSpec ) ;
111- model . computeBoundsAndApplyParent ( ) ;
112- this . models . push ( model ) ;
113- }
114- } else {
115- // Model neither currently loading nor already loaded
116- this . _modelsInfo . set ( modelId , { modelSpec, model : null } ) ;
117- // eslint-disable-next-line @typescript-eslint/no-floating-promises
118- this . loadModel ( modelId , modelSpec ) ;
119- }
120- }
121- // Fire data event if all models are already loaded (i.e model source is empty or there are no more requests pending)
122- if ( this . loaded ( ) ) {
123- this . fire ( new Event ( 'data' , { dataType : 'source' , sourceDataType : 'metadata' } ) ) ;
124- }
102+ private loadGLTFFromURI ( uri : string , signal ?: AbortSignal ) : Promise < GLTF > {
103+ return loadGLTF ( this . map . _requestManager . transformRequest ( uri , ResourceType . Model ) . url , signal ) ;
125104 }
126105
127- private async loadModel ( modelId : string , modelSpec : ModelSourceModelSpecification ) : Promise < void > {
106+ private async loadModel ( modelId : string , modelSpec : ModelSourceModelSpecification , signal : AbortSignal ) : Promise < void > {
128107 try {
129- const gltf = await this . loadGLTFFromURI ( modelSpec . uri ) ;
130- if ( ! gltf ) return ;
108+ const gltf = await this . loadGLTFFromURI ( modelSpec . uri , signal ) ;
109+ if ( signal . aborted ) return ;
131110
132- // Check if model is still active
133111 const modelInfo = this . _modelsInfo . get ( modelId ) ;
134- if ( ! modelInfo ) return ;
112+ if ( ! modelInfo ) return ; // source modified during async gap
135113
136114 const nodes = convertModel ( gltf ) ;
137- const currentModelSpec = modelInfo . modelSpec ;
138- const model = new Model ( modelId , currentModelSpec . uri , currentModelSpec . position , currentModelSpec . orientation , nodes ) ;
139- ModelSource . applyModelSpecification ( model , currentModelSpec ) ;
115+ const currentSpec = modelInfo . modelSpec ;
116+ const model = new Model ( modelId , currentSpec . uri , currentSpec . position , currentSpec . orientation , nodes ) ;
117+ ModelSource . applyModelSpecification ( model , currentSpec ) ;
140118 model . computeBoundsAndApplyParent ( ) ;
141119
142120 this . models . push ( model ) ;
143121 modelInfo . model = model ;
122+ } catch ( err : unknown ) {
123+ if ( err instanceof Error && err . name === 'AbortError' ) return ;
124+ const message = err instanceof Error ? err . message : 'Unknown error' ;
125+ this . fire ( new ErrorEvent ( new Error ( `Could not load model ${ modelId } from ${ modelSpec . uri } : ${ message } ` ) ) ) ;
126+ }
127+ }
144128
145- // If all models are loaded, fire data event
129+ async load ( ) : Promise < void > {
130+ if ( ! this . _abortController ) {
131+ this . _abortController = new AbortController ( ) ;
132+ }
133+ const signal = this . _abortController . signal ;
134+
135+ const loadPromises : Promise < void > [ ] = [ ] ;
136+
137+ for ( const modelId in this . _options . models ) {
138+ const modelSpec = this . _options . models [ modelId ] ;
139+
140+ const existingInfo = this . _modelsInfo . get ( modelId ) ;
141+ if ( existingInfo && existingInfo . model ) {
142+ existingInfo . modelSpec = modelSpec ;
143+ const model = existingInfo . model ;
144+ model . position = modelSpec . position != null ? new LngLat ( modelSpec . position [ 0 ] , modelSpec . position [ 1 ] ) : new LngLat ( 0 , 0 ) ;
145+ model . orientation = modelSpec . orientation != null ? modelSpec . orientation : [ 0 , 0 , 0 ] ;
146+ ModelSource . applyModelSpecification ( model , modelSpec ) ;
147+ model . computeBoundsAndApplyParent ( ) ;
148+ this . models . push ( model ) ;
149+ } else if ( ! existingInfo ) {
150+ this . _modelsInfo . set ( modelId , { modelSpec, model : null } ) ;
151+ loadPromises . push ( this . loadModel ( modelId , modelSpec , signal ) ) ;
152+ } else {
153+ existingInfo . modelSpec = modelSpec ;
154+ }
155+ }
156+
157+ if ( loadPromises . length === 0 ) {
146158 if ( this . loaded ( ) ) {
147159 this . fire ( new Event ( 'data' , { dataType : 'source' , sourceDataType : 'metadata' } ) ) ;
148160 }
149- } catch ( err ) {
150-
151- this . fire ( new ErrorEvent ( new Error ( `Could not load model ${ modelId } from ${ modelSpec . uri } : ${ ( err as Error ) . message } ` ) ) ) ;
161+ return ;
152162 }
163+
164+ await Promise . allSettled ( loadPromises ) ;
165+
166+ if ( signal . aborted ) return ; // new load will fire data event
167+ this . fire ( new Event ( 'data' , { dataType : 'source' , sourceDataType : 'metadata' } ) ) ;
153168 }
154169
155170 private static applyModelSpecification ( model : Model , modelSpec : ModelSourceModelSpecification ) {
@@ -246,6 +261,7 @@ class ModelSource extends Evented<SourceEvents> implements ISource {
246261
247262 onAdd ( map : MapboxMap ) {
248263 this . map = map ;
264+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
249265 this . load ( ) ;
250266 }
251267
@@ -278,13 +294,19 @@ class ModelSource extends Evented<SourceEvents> implements ISource {
278294 }
279295
280296 reload ( ) {
297+ this . cancelModelRequests ( ) ;
281298 const fqid = makeFQID ( this . id , this . scope ) ;
282299 this . map . style . clearSource ( fqid ) ;
283300 this . models = [ ] ;
284301 this . _modelsInfo . clear ( ) ;
302+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
285303 this . load ( ) ;
286304 }
287305
306+ onRemove ( _map : MapboxMap ) {
307+ this . cancelModelRequests ( ) ;
308+ }
309+
288310 /**
289311 * Sets the list of models along with their properties.
290312 *
@@ -300,23 +322,32 @@ class ModelSource extends Evented<SourceEvents> implements ISource {
300322 * });
301323 */
302324 setModels ( modelSpecs : ModelSourceModelsSpecification ) {
303- // Mimic behavior of native `ModelSource::updateModelData` implementation
304325 this . models = [ ] ;
305326
306- // Only preserve model info entries for ids present in new model specification
307327 const updatedModelsInfo = new Map < string , ModelSourceModelInfo > ( ) ;
308328 for ( const modelId in modelSpecs ) {
309329 const modelSpec = modelSpecs [ modelId ] ;
310- if ( this . _modelsInfo . has ( modelId ) ) {
311- const entry = this . _modelsInfo . get ( modelId ) ;
312- // Only preserve if uri did not change
313- if ( entry && entry . modelSpec . uri === modelSpec . uri ) {
314- updatedModelsInfo . set ( modelId , entry ) ;
315- }
330+ const entry = this . _modelsInfo . get ( modelId ) ;
331+ if ( entry && entry . modelSpec . uri === modelSpec . uri ) {
332+ updatedModelsInfo . set ( modelId , entry ) ;
316333 }
317334 }
335+
336+ // Only cancel requests when models are actually removed or URIs change.
337+ // Property-only updates (position, orientation) are high-frequency (animation)
338+ // and should not restart in-flight model loads.
339+ const modelsChanged = this . _modelsInfo . size !== updatedModelsInfo . size ;
340+ if ( modelsChanged ) {
341+ this . cancelModelRequests ( ) ;
342+ // Remove pending entries - their cancelled requests won't complete
343+ for ( const [ id , info ] of updatedModelsInfo ) {
344+ if ( ! info . model ) updatedModelsInfo . delete ( id ) ;
345+ }
346+ }
347+
318348 this . _modelsInfo = updatedModelsInfo ;
319349 this . _options . models = modelSpecs ;
350+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
320351 this . load ( ) ;
321352 }
322353}
0 commit comments