From bb36e538f219fb059cba37ac61d3db251e633a40 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 17:32:07 +0900 Subject: [PATCH 01/11] Add a new "isAnnotationVisible" flag --- src/three/plugins/mvt/MVTAnnotationsPlugin.js | 36 +++++++++++++++++++ .../plugins/mvt/ScreenOccupationManager.js | 6 +++- src/three/plugins/mvt/SettlingManager.js | 8 +++++ .../mvt/annotations/TextAnchorAnnotation.js | 9 +++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index 92f6c83a8..b389df034 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -113,6 +113,19 @@ export class MVTAnnotationsDriver { } + /** + * Whether a parsed annotation should currently be displayed. Unlike `filterAnnotation` which + * decides what is parsed, this is a dynamic filter re-evaluated on `refreshFilter()`. + * @param {Object} properties - The feature's property map. + * @param {number} type - The MVT geometry type: `1` = point, `2` = line. + * @returns {boolean} True to display the annotation. + */ + isAnnotationEnabled( properties, type ) { + + return true; + + } + /** * Called each frame with the annotations whose visibility changed, for the caller to render. * @param {Set} added - Annotations that became visible this frame. @@ -400,10 +413,12 @@ export class MVTAnnotationsPlugin { if ( ann instanceof LineAnnotation ) { + ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 2 ); settlingManager.register( ann ); } else { + ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 1 ); pointManager.add( ann ); } @@ -494,6 +509,27 @@ export class MVTAnnotationsPlugin { } + // re-evaluate the driver's isAnnotationEnabled over every parsed annotation. occupancy picks up + // the change automatically ( items fade in / out ) and settling is re-queued so any newly + // enabled annotations get draped. + refreshFilter() { + + const { vectorTileInfo, settlingManager, tiles } = this; + for ( const { annotations } of vectorTileInfo.values() ) { + + for ( const ann of annotations ) { + + ann.enabled = this.driver.isAnnotationEnabled( ann.properties, ann instanceof LineAnnotation ? 2 : 1 ); + + } + + } + + settlingManager.needsUpdate = true; + tiles.dispatchEvent( { type: 'needs-update' } ); + + } + processTileModel( scene, tile ) { const { tiles, overlay } = this; diff --git a/src/three/plugins/mvt/ScreenOccupationManager.js b/src/three/plugins/mvt/ScreenOccupationManager.js index 4a809e27b..93d54a617 100644 --- a/src/three/plugins/mvt/ScreenOccupationManager.js +++ b/src/three/plugins/mvt/ScreenOccupationManager.js @@ -16,6 +16,7 @@ export class OccupancyAnnotation { this.visibleDuration = Infinity; this.visibleTime = Infinity; this.visible = false; + this.enabled = true; this.screenPos = new Vector3(); } @@ -240,7 +241,10 @@ export class ScreenOccupationManager extends EventDispatcher { const item = items[ i ]; this._id = i + 1; - if ( ndcMatrix !== null && item.evaluate( handle ) ) { + + // disabled items ( filtered out by the driver ) are skipped so they fall out of the + // visible set and fade out via the delayed manager, without being unregistered + if ( ndcMatrix !== null && item.enabled && item.evaluate( handle ) ) { visible.add( item ); if ( ! prevVisible.has( item ) ) { diff --git a/src/three/plugins/mvt/SettlingManager.js b/src/three/plugins/mvt/SettlingManager.js index 2fbccf412..1fc36fc25 100644 --- a/src/three/plugins/mvt/SettlingManager.js +++ b/src/three/plugins/mvt/SettlingManager.js @@ -316,6 +316,14 @@ export class SettlingManager { } + // skip disabled ( filtered-out ) items — they're drained here and re-queued by + // the plugin's refreshFilter() if they become enabled again + if ( ! item.enabled ) { + + continue; + + } + yield* this._settleItem( item ); // yield between items once the budget is spent diff --git a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js index 25c2017c5..34979fda4 100644 --- a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js +++ b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js @@ -49,6 +49,15 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { set properties( value ) {} + // the anchor's enabled state tracks its active line's ( set by the plugin from the driver ) + get enabled() { + + return this.getActiveReference().line.enabled; + + } + + set enabled( value ) {} + constructor( id ) { super(); From 45e7e750b671fc3eba4ecceca089e62d1b2b5766 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 17:47:16 +0900 Subject: [PATCH 02/11] Ensure text is updated on refresh, as well --- src/three/plugins/mvt/MVTAnnotationsPlugin.js | 20 ++++++++++++------- src/three/plugins/mvt/SettlingManager.js | 2 +- .../mvt/annotations/TextAnchorAnnotation.js | 10 ++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index b389df034..30f0c0486 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -115,7 +115,7 @@ export class MVTAnnotationsDriver { /** * Whether a parsed annotation should currently be displayed. Unlike `filterAnnotation` which - * decides what is parsed, this is a dynamic filter re-evaluated on `refreshFilter()`. + * decides what is parsed, this is a dynamic filter re-evaluated on `refresh()`. * @param {Object} properties - The feature's property map. * @param {number} type - The MVT geometry type: `1` = point, `2` = line. * @returns {boolean} True to display the annotation. @@ -182,7 +182,6 @@ export class MVTAnnotationsPlugin { // stable bound callbacks handed to sub-objects that invoke them ( preserves driver `this` ) this._measureChar = char => this.driver.measureChar( char ); this._filterAnnotation = ( layer, properties, type ) => this.driver.filterAnnotation( layer, properties, type ); - this._getText = properties => this.driver.getText( properties ); // hierarchy for managing tile loading and visibility this.hierarchy = new MVTHierarchy(); @@ -337,7 +336,7 @@ export class MVTAnnotationsPlugin { anchorManager.added.forEach( item => { item.measureChar = this._measureChar; - item.getText = this._getText; + item.text = this.driver.getText( item.properties ); occupancy.register( item ); } ); @@ -509,12 +508,12 @@ export class MVTAnnotationsPlugin { } - // re-evaluate the driver's isAnnotationEnabled over every parsed annotation. occupancy picks up - // the change automatically ( items fade in / out ) and settling is re-queued so any newly + // re-pull driver-derived state for every parsed annotation: `enabled` flags ( occupancy fades + // them in / out automatically ) and cached anchor text. settling is re-queued so any newly // enabled annotations get draped. - refreshFilter() { + refresh() { - const { vectorTileInfo, settlingManager, tiles } = this; + const { vectorTileInfo, anchorManager, settlingManager, tiles } = this; for ( const { annotations } of vectorTileInfo.values() ) { for ( const ann of annotations ) { @@ -525,6 +524,13 @@ export class MVTAnnotationsPlugin { } + // recompute label text ( getText may return a different string, e.g. on a language change ) + for ( const anchor of anchorManager.getAnchors() ) { + + anchor.text = this.driver.getText( anchor.properties ); + + } + settlingManager.needsUpdate = true; tiles.dispatchEvent( { type: 'needs-update' } ); diff --git a/src/three/plugins/mvt/SettlingManager.js b/src/three/plugins/mvt/SettlingManager.js index 1fc36fc25..65f0af018 100644 --- a/src/three/plugins/mvt/SettlingManager.js +++ b/src/three/plugins/mvt/SettlingManager.js @@ -317,7 +317,7 @@ export class SettlingManager { } // skip disabled ( filtered-out ) items — they're drained here and re-queued by - // the plugin's refreshFilter() if they become enabled again + // the plugin's refresh() if they become enabled again if ( ! item.enabled ) { continue; diff --git a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js index 34979fda4..8852dd22b 100644 --- a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js +++ b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js @@ -27,12 +27,6 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { } - get text() { - - return this.getText( this.properties ); - - } - get ready() { return this.getActiveReference().line.ready; @@ -78,8 +72,8 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { // per-character advance width provider (pixels) this.measureChar = () => 1; - // text provider — returns the string to display for the line's properties - this.getText = properties => properties.name ?? ''; + // display text, set by the plugin on init and on refresh + this.text = ''; // total advance width of the label ( screen px ) and per-character footprint radius, // recomputed each layout ( individual advances come from the cached measureChar ) From 4429f8cd98324d954bc044f0dd2487c556d95b75 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 17:58:54 +0900 Subject: [PATCH 03/11] Update glyph logic to be resilient to changing text --- .../three/src/plugins/mvt/CharacterPoints.js | 111 ++++++++---------- example/three/src/plugins/mvt/GlyphPoints.js | 14 +-- 2 files changed, 54 insertions(+), 71 deletions(-) diff --git a/example/three/src/plugins/mvt/CharacterPoints.js b/example/three/src/plugins/mvt/CharacterPoints.js index 864c69b24..2cdf334aa 100644 --- a/example/three/src/plugins/mvt/CharacterPoints.js +++ b/example/three/src/plugins/mvt/CharacterPoints.js @@ -23,16 +23,18 @@ export class CharacterPoints extends GlyphPoints { const fontSize = Math.round( glyphSize * 0.7 ); this._font = font ?? `400 ${ fontSize }px sans-serif`; - // canvas context for measuring advance widths, normalized to em units ( width / fontSize ) + // advance-width cache, keyed per character this._advanceCache = new Map(); // styling this._strokeStyle = strokeStyle; this._strokeWidth = strokeWidth; - // the refs and unused list of the character glyphs - this._refs = {}; - this._unused = new Set(); + // characters currently rasterized in the atlas, plus a reused scratch set of the characters + // needed this frame. the atlas is reconciled against what's actually rendered ( not against + // add / remove events ), so it stays correct even when an annotation's text changes. + this._drawn = new Set(); + this._needed = new Set(); this.glyphAtlas.resize( slotCount, glyphSize ); @@ -56,89 +58,76 @@ export class CharacterPoints extends GlyphPoints { } - update( added, removed ) { + // rasterize a character into the atlas, evicting a glyph that isn't needed this frame ( or + // growing the atlas if every rasterized glyph is still in use ) + _drawChar( char, needed ) { - // TODO: if the "text" suddenly changes (eg from a language change) this will break - // since we will never get a "removed" event associated with the original text string. - // We may have to track the characters that have been used in a geometry update, instead. - super.update( added, removed ); - const { _refs, _unused, _added, _removed, glyphAtlas } = this; + const { glyphAtlas, _drawn } = this; + if ( glyphAtlas.capacity === glyphAtlas.count ) { - // iterate over all the added annotations and increment the ref, drawing the - // glyph to the atlas if needed. - _added.forEach( ( { text } ) => { + let toRemove = null; + for ( const c of _drawn ) { - for ( let i = 0, l = text.length; i < l; i ++ ) { + if ( ! needed.has( c ) ) { - const char = text[ i ]; - if ( ! ( char in _refs ) ) { + toRemove = c; + break; - // if we've reached our capacity - if ( glyphAtlas.capacity === glyphAtlas.count ) { - - if ( _unused.size > 0 ) { - - // if there are unused slots then free up one of those - const unusedChar = _unused.values().next().value; - glyphAtlas.release( unusedChar ); - delete _refs[ unusedChar ]; - _unused.delete( unusedChar ); - - } else { - - // otherwise resize the atlas to fit more glyphs - glyphAtlas.resize( glyphAtlas.capacity * 2 ); + } - } + } - } + if ( toRemove !== null ) { - // draw the glyph - _refs[ char ] = 0; - glyphAtlas.drawChar( char, char, { - font: this._font, - color: 'white', - strokeStyle: this._strokeStyle, - strokeWidth: this._strokeWidth, - } ); + glyphAtlas.release( toRemove ); + _drawn.delete( toRemove ); - } + } else { - _unused.delete( char ); - _refs[ char ] ++; + glyphAtlas.resize( glyphAtlas.capacity * 2 ); } + } + + glyphAtlas.drawChar( char, char, { + font: this._font, + color: 'white', + strokeStyle: this._strokeStyle, + strokeWidth: this._strokeWidth, } ); + _drawn.add( char ); - // decrement the refs and queue up any characters as unused if they reach 0 - _removed.forEach( ( { text } ) => { + } - for ( let i = 0, l = text.length; i < l; i ++ ) { + _updateGeometry() { - const char = text[ i ]; - _refs[ char ] --; - if ( _refs[ char ] === 0 ) { + const { _orderedEntries, _needed, geometry, position, glyphAtlas } = this; - _unused.add( char ); + // collect the characters needed this frame from each items text, and the total glyph + // count + _needed.clear(); + let count = 0; + for ( const entry of _orderedEntries ) { - } + const { text, characterPositions } = entry.item; + count += characterPositions.length; + for ( let c = 0, l = text.length; c < l; c ++ ) { - } + _needed.add( text[ c ] ); - } ); + } - } + } - _updateGeometry() { + // make sure every needed character is rasterized before we look up its uv + for ( const char of _needed ) { - const { _orderedEntries, geometry, position, glyphAtlas } = this; + if ( ! glyphAtlas.has( char ) ) { - // total character count across all entries ( including fading ones ) - let count = 0; - for ( const entry of _orderedEntries ) { + this._drawChar( char, _needed ); - count += entry.item.characterPositions.length; + } } diff --git a/example/three/src/plugins/mvt/GlyphPoints.js b/example/three/src/plugins/mvt/GlyphPoints.js index 5b20587ab..aada1f3bc 100644 --- a/example/three/src/plugins/mvt/GlyphPoints.js +++ b/example/three/src/plugins/mvt/GlyphPoints.js @@ -38,8 +38,6 @@ export class GlyphPoints extends Points { this._entryMap = new Map(); this._orderedEntries = []; this._lastUpdateTime = - 1; - this._added = []; - this._removed = []; } @@ -58,9 +56,7 @@ export class GlyphPoints extends Points { const dt = this._lastUpdateTime < 0 ? 0 : Math.min( now - this._lastUpdateTime, 0.1 ); this._lastUpdateTime = now; - const { _entryMap, _orderedEntries, _added, _removed, fadeInDuration, fadeOutDuration } = this; - _added.length = 0; - _removed.length = 0; + const { _entryMap, _orderedEntries, fadeInDuration, fadeOutDuration } = this; for ( const item of added ) { @@ -70,7 +66,6 @@ export class GlyphPoints extends Points { const entry = { item, fade: 0, state: 'in' }; _entryMap.set( item.id, entry ); _orderedEntries.push( entry ); - _added.push( item ); } else { @@ -93,6 +88,7 @@ export class GlyphPoints extends Points { } + let didRemove = false; for ( const [ id, entry ] of _entryMap ) { if ( entry.state === 'in' ) { @@ -110,7 +106,7 @@ export class GlyphPoints extends Points { if ( entry.fade <= 0 ) { _entryMap.delete( id ); - _removed.push( entry.item ); + didRemove = true; } @@ -118,7 +114,7 @@ export class GlyphPoints extends Points { } - if ( _removed.length > 0 ) { + if ( didRemove ) { this._orderedEntries = _orderedEntries.filter( e => _entryMap.has( e.item.id ) ); @@ -126,8 +122,6 @@ export class GlyphPoints extends Points { this._updateGeometry(); - return _removed; - } onAfterRender( renderer, scene, camera ) { From f296b8507f3d832e1109b6f961c02fa08880f84c Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 17:59:41 +0900 Subject: [PATCH 04/11] Small fix --- src/three/plugins/images/sources/PMTilesImageSource.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/three/plugins/images/sources/PMTilesImageSource.js b/src/three/plugins/images/sources/PMTilesImageSource.js index adade0ebe..a48f4650f 100644 --- a/src/three/plugins/images/sources/PMTilesImageSource.js +++ b/src/three/plugins/images/sources/PMTilesImageSource.js @@ -62,7 +62,11 @@ class PMTilesContentCache extends MVTContentCache { getKey: () => this.url, getBytes: async ( offset, length, signal ) => { - signal.aborted.throwIfAborted(); + if ( signal ) { + + signal.aborted.throwIfAborted(); + + } const { fetchOptions, url } = this; const res = await this.fetchData( url, { From 4fe53bd4ab91562d3236df72eab5dda92a4c9da7 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 18:16:37 +0900 Subject: [PATCH 05/11] Adjust the logic to respond to function changes immediately --- src/three/plugins/mvt/MVTAnnotationsPlugin.js | 54 ++++++++----------- .../plugins/mvt/ScreenOccupationManager.js | 2 +- src/three/plugins/mvt/SettlingManager.js | 4 +- .../mvt/annotations/TextAnchorAnnotation.js | 12 ++--- 4 files changed, 29 insertions(+), 43 deletions(-) diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index 30f0c0486..4ee6b40c2 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -115,7 +115,7 @@ export class MVTAnnotationsDriver { /** * Whether a parsed annotation should currently be displayed. Unlike `filterAnnotation` which - * decides what is parsed, this is a dynamic filter re-evaluated on `refresh()`. + * decides what is parsed once. * @param {Object} properties - The feature's property map. * @param {number} type - The MVT geometry type: `1` = point, `2` = line. * @returns {boolean} True to display the annotation. @@ -179,9 +179,10 @@ export class MVTAnnotationsPlugin { this.camera = camera; this.driver = driver; - // stable bound callbacks handed to sub-objects that invoke them ( preserves driver `this` ) + // annotations call these live each frame so driver changes take effect immediately this._measureChar = char => this.driver.measureChar( char ); this._filterAnnotation = ( layer, properties, type ) => this.driver.filterAnnotation( layer, properties, type ); + this._getText = properties => this.driver.getText( properties ); // hierarchy for managing tile loading and visibility this.hierarchy = new MVTHierarchy(); @@ -303,6 +304,23 @@ export class MVTAnnotationsPlugin { this._onUpdateAfter = () => { + // recompute the dynamic display filter for every annotation before placement + for ( const { annotations } of this.vectorTileInfo.values() ) { + + for ( const ann of annotations ) { + + ann.enabled = this.driver.isAnnotationEnabled( ann.properties, ann instanceof LineAnnotation ? 2 : 1 ); + + } + + } + + for ( const anchor of anchorManager.getAnchors() ) { + + anchor.enabled = this.driver.isAnnotationEnabled( anchor.properties, 2 ); + + } + // sync camera and localToWorld matrix into occupancy grid const { camera } = this; if ( camera !== null ) { @@ -336,7 +354,7 @@ export class MVTAnnotationsPlugin { anchorManager.added.forEach( item => { item.measureChar = this._measureChar; - item.text = this.driver.getText( item.properties ); + item.getText = this._getText; occupancy.register( item ); } ); @@ -412,12 +430,10 @@ export class MVTAnnotationsPlugin { if ( ann instanceof LineAnnotation ) { - ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 2 ); settlingManager.register( ann ); } else { - ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 1 ); pointManager.add( ann ); } @@ -508,34 +524,6 @@ export class MVTAnnotationsPlugin { } - // re-pull driver-derived state for every parsed annotation: `enabled` flags ( occupancy fades - // them in / out automatically ) and cached anchor text. settling is re-queued so any newly - // enabled annotations get draped. - refresh() { - - const { vectorTileInfo, anchorManager, settlingManager, tiles } = this; - for ( const { annotations } of vectorTileInfo.values() ) { - - for ( const ann of annotations ) { - - ann.enabled = this.driver.isAnnotationEnabled( ann.properties, ann instanceof LineAnnotation ? 2 : 1 ); - - } - - } - - // recompute label text ( getText may return a different string, e.g. on a language change ) - for ( const anchor of anchorManager.getAnchors() ) { - - anchor.text = this.driver.getText( anchor.properties ); - - } - - settlingManager.needsUpdate = true; - tiles.dispatchEvent( { type: 'needs-update' } ); - - } - processTileModel( scene, tile ) { const { tiles, overlay } = this; diff --git a/src/three/plugins/mvt/ScreenOccupationManager.js b/src/three/plugins/mvt/ScreenOccupationManager.js index 93d54a617..840622338 100644 --- a/src/three/plugins/mvt/ScreenOccupationManager.js +++ b/src/three/plugins/mvt/ScreenOccupationManager.js @@ -8,6 +8,7 @@ export class OccupancyAnnotation { constructor() { + this.enabled = true; this.id = ''; this.layer = ''; this.properties = null; @@ -16,7 +17,6 @@ export class OccupancyAnnotation { this.visibleDuration = Infinity; this.visibleTime = Infinity; this.visible = false; - this.enabled = true; this.screenPos = new Vector3(); } diff --git a/src/three/plugins/mvt/SettlingManager.js b/src/three/plugins/mvt/SettlingManager.js index 65f0af018..074170956 100644 --- a/src/three/plugins/mvt/SettlingManager.js +++ b/src/three/plugins/mvt/SettlingManager.js @@ -316,10 +316,10 @@ export class SettlingManager { } - // skip disabled ( filtered-out ) items — they're drained here and re-queued by - // the plugin's refresh() if they become enabled again + // skip disabled items and mark them unsettled so they re-settle if re-enabled if ( ! item.enabled ) { + item.ready = false; continue; } diff --git a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js index 8852dd22b..5e85b1215 100644 --- a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js +++ b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js @@ -43,15 +43,13 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { set properties( value ) {} - // the anchor's enabled state tracks its active line's ( set by the plugin from the driver ) - get enabled() { + // display text, resolved live from the plugin-assigned getText each time it's read + get text() { - return this.getActiveReference().line.enabled; + return this.getText( this.properties ); } - set enabled( value ) {} - constructor( id ) { super(); @@ -72,8 +70,8 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { // per-character advance width provider (pixels) this.measureChar = () => 1; - // display text, set by the plugin on init and on refresh - this.text = ''; + // display text provider, assigned by the plugin from the driver's getText + this.getText = () => ''; // total advance width of the label ( screen px ) and per-character footprint radius, // recomputed each layout ( individual advances come from the cached measureChar ) From 428e3ba93a682024c62312f60f6244fdbe705308 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 18:19:07 +0900 Subject: [PATCH 06/11] update --- src/three/plugins/mvt/MVTAnnotationsPlugin.js | 3 +-- .../plugins/mvt/annotations/TextAnchorAnnotation.js | 11 ++--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index 4ee6b40c2..790ebd24e 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -182,7 +182,6 @@ export class MVTAnnotationsPlugin { // annotations call these live each frame so driver changes take effect immediately this._measureChar = char => this.driver.measureChar( char ); this._filterAnnotation = ( layer, properties, type ) => this.driver.filterAnnotation( layer, properties, type ); - this._getText = properties => this.driver.getText( properties ); // hierarchy for managing tile loading and visibility this.hierarchy = new MVTHierarchy(); @@ -318,6 +317,7 @@ export class MVTAnnotationsPlugin { for ( const anchor of anchorManager.getAnchors() ) { anchor.enabled = this.driver.isAnnotationEnabled( anchor.properties, 2 ); + anchor.text = this.driver.getText( anchor.properties ); } @@ -354,7 +354,6 @@ export class MVTAnnotationsPlugin { anchorManager.added.forEach( item => { item.measureChar = this._measureChar; - item.getText = this._getText; occupancy.register( item ); } ); diff --git a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js index 5e85b1215..50682269b 100644 --- a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js +++ b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js @@ -43,13 +43,6 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { set properties( value ) {} - // display text, resolved live from the plugin-assigned getText each time it's read - get text() { - - return this.getText( this.properties ); - - } - constructor( id ) { super(); @@ -70,8 +63,8 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { // per-character advance width provider (pixels) this.measureChar = () => 1; - // display text provider, assigned by the plugin from the driver's getText - this.getText = () => ''; + // display text, recomputed each frame by the plugin from the driver's getText + this.text = ''; // total advance width of the label ( screen px ) and per-character footprint radius, // recomputed each layout ( individual advances come from the cached measureChar ) From c8daa25d80aaa3e57874da3b8f0e8d73dcfb8c36 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 18:19:17 +0900 Subject: [PATCH 07/11] Small fix for signal --- src/three/plugins/images/sources/PMTilesImageSource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/three/plugins/images/sources/PMTilesImageSource.js b/src/three/plugins/images/sources/PMTilesImageSource.js index a48f4650f..637710db2 100644 --- a/src/three/plugins/images/sources/PMTilesImageSource.js +++ b/src/three/plugins/images/sources/PMTilesImageSource.js @@ -64,7 +64,7 @@ class PMTilesContentCache extends MVTContentCache { if ( signal ) { - signal.aborted.throwIfAborted(); + signal.throwIfAborted(); } From a2587ef550de6e87ff90cd5487a531dfc7430750 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 18:25:40 +0900 Subject: [PATCH 08/11] Update --- src/three/plugins/mvt/MVTAnnotationsPlugin.js | 16 +++++++++------- .../plugins/mvt/annotations/LineAnnotation.js | 3 +++ .../mvt/annotations/TextAnchorAnnotation.js | 17 ++++++++++++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index 790ebd24e..4b562dd58 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -303,21 +303,23 @@ export class MVTAnnotationsPlugin { this._onUpdateAfter = () => { - // recompute the dynamic display filter for every annotation before placement + // Recompute driver-derived state on every parsed annotation before placement for ( const { annotations } of this.vectorTileInfo.values() ) { for ( const ann of annotations ) { - ann.enabled = this.driver.isAnnotationEnabled( ann.properties, ann instanceof LineAnnotation ? 2 : 1 ); + if ( ann instanceof LineAnnotation ) { - } + ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 2 ); + ann.text = this.driver.getText( ann.properties ); - } + } else { - for ( const anchor of anchorManager.getAnchors() ) { + ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 1 ); - anchor.enabled = this.driver.isAnnotationEnabled( anchor.properties, 2 ); - anchor.text = this.driver.getText( anchor.properties ); + } + + } } diff --git a/src/three/plugins/mvt/annotations/LineAnnotation.js b/src/three/plugins/mvt/annotations/LineAnnotation.js index d968a0c12..538241a8f 100644 --- a/src/three/plugins/mvt/annotations/LineAnnotation.js +++ b/src/three/plugins/mvt/annotations/LineAnnotation.js @@ -22,6 +22,9 @@ export class LineAnnotation extends OccupancyAnnotation { super(); + // display text for this path + this.text = ''; + // the range of the tile this line is associated with this.range = null; diff --git a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js index 50682269b..b8838f312 100644 --- a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js +++ b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js @@ -43,6 +43,20 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { set properties( value ) {} + get enabled() { + + return this.getActiveReference().line.enabled; + + } + + set enabled( value ) {} + + get text() { + + return this.getActiveReference().line.text; + + } + constructor( id ) { super(); @@ -63,9 +77,6 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { // per-character advance width provider (pixels) this.measureChar = () => 1; - // display text, recomputed each frame by the plugin from the driver's getText - this.text = ''; - // total advance width of the label ( screen px ) and per-character footprint radius, // recomputed each layout ( individual advances come from the cached measureChar ) this._totalWidth = 0; From 9e25c63cf5e34f2897e297f7bc31ffc9ef576b03 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 18:31:03 +0900 Subject: [PATCH 09/11] Add language dropdown --- example/three/annotationsExample.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/example/three/annotationsExample.js b/example/three/annotationsExample.js index c4d1af7ac..a201e0ffd 100644 --- a/example/three/annotationsExample.js +++ b/example/three/annotationsExample.js @@ -102,8 +102,12 @@ const KIND_TO_ICON = { }; +// localized name variants exposed by the protomaps v4 basemap +const LANGUAGES = [ 'default', 'en', 'ja', 'ko' ]; + const params = { + language: 'default', occupancyGrid: false, annotationLines: false, tileHierarchy: false, @@ -128,6 +132,8 @@ class ExampleAnnotationsDriver extends MVTAnnotationsDriver { super(); + this.language = 'default'; + const dpr = renderer.getPixelRatio(); // icons for point annotations @@ -190,6 +196,19 @@ class ExampleAnnotationsDriver extends MVTAnnotationsDriver { } + getText( properties ) { + + const { language } = this; + if ( language !== 'default' ) { + + return properties[ `name:${ language }` ] ?? properties.name ?? ''; + + } + + return properties.name ?? ''; + + } + measureChar( char ) { return this.characterPoints.measureChar( char ); @@ -264,6 +283,7 @@ function reinstantiateTiles() { tiles.registerPlugin( new TilesFadePlugin() ); tiles.registerPlugin( new MeshBVHPlugin() ); driver = new ExampleAnnotationsDriver(); + driver.language = params.language; tiles.registerPlugin( new MVTAnnotationsPlugin( { overlay, camera, @@ -331,6 +351,12 @@ function init() { // GUI const gui = new GUI(); + gui.add( params, 'language', LANGUAGES ).onChange( v => { + + driver.language = v; + tiles.dispatchEvent( { type: 'needs-update' } ); + + } ); gui.add( params, 'occupancyGrid' ).onChange( v => { tiles.getPluginByName( 'MVT_ANNOTATIONS_PLUGIN' ).debug.occupancy.enabled = v; From bc099a5669b7921392015fa77e499c301bf3569e Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 18:38:54 +0900 Subject: [PATCH 10/11] Update UI dropdown --- example/three/annotationsExample.js | 35 ++++++++++++++++--- src/three/plugins/mvt/MVTAnnotationsPlugin.js | 4 +-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/example/three/annotationsExample.js b/example/three/annotationsExample.js index a201e0ffd..2aadddde2 100644 --- a/example/three/annotationsExample.js +++ b/example/three/annotationsExample.js @@ -108,6 +108,9 @@ const LANGUAGES = [ 'default', 'en', 'ja', 'ko' ]; const params = { language: 'default', + displayIcons: true, + displayPaths: true, + occupancyGrid: false, annotationLines: false, tileHierarchy: false, @@ -133,6 +136,8 @@ class ExampleAnnotationsDriver extends MVTAnnotationsDriver { super(); this.language = 'default'; + this.displayIcons = true; + this.displayPaths = true; const dpr = renderer.getPixelRatio(); @@ -196,6 +201,14 @@ class ExampleAnnotationsDriver extends MVTAnnotationsDriver { } + isAnnotationEnabled( layer, properties, type ) { + + if ( type === 1 && this.displayIcons ) return true; + if ( type === 2 && this.displayPaths ) return true; + return false; + + } + getText( properties ) { const { language } = this; @@ -354,20 +367,34 @@ function init() { gui.add( params, 'language', LANGUAGES ).onChange( v => { driver.language = v; - tiles.dispatchEvent( { type: 'needs-update' } ); + tiles.getPluginByName( 'UPDATE_ON_CHANGE_PLUGIN' ).needsUpdate = true; + + } ); + gui.add( params, 'displayIcons' ).onChange( v => { + + driver.displayIcons = v; + tiles.getPluginByName( 'UPDATE_ON_CHANGE_PLUGIN' ).needsUpdate = true; + + } ); + gui.add( params, 'displayPaths' ).onChange( v => { + + driver.displayPaths = v; + tiles.getPluginByName( 'UPDATE_ON_CHANGE_PLUGIN' ).needsUpdate = true; } ); - gui.add( params, 'occupancyGrid' ).onChange( v => { + + const debugFolder = gui.addFolder( 'Debug' ); + debugFolder.add( params, 'occupancyGrid' ).onChange( v => { tiles.getPluginByName( 'MVT_ANNOTATIONS_PLUGIN' ).debug.occupancy.enabled = v; } ); - gui.add( params, 'annotationLines' ).onChange( v => { + debugFolder.add( params, 'annotationLines' ).onChange( v => { tiles.getPluginByName( 'MVT_ANNOTATIONS_PLUGIN' ).debug.paths.enabled = v; } ); - gui.add( params, 'tileHierarchy' ).onChange( v => { + debugFolder.add( params, 'tileHierarchy' ).onChange( v => { tiles.getPluginByName( 'MVT_ANNOTATIONS_PLUGIN' ).debug.hierarchy.enabled = v; diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index 4b562dd58..296d9e283 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -310,12 +310,12 @@ export class MVTAnnotationsPlugin { if ( ann instanceof LineAnnotation ) { - ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 2 ); + ann.enabled = this.driver.isAnnotationEnabled( ann.layer, ann.properties, 2 ); ann.text = this.driver.getText( ann.properties ); } else { - ann.enabled = this.driver.isAnnotationEnabled( ann.properties, 1 ); + ann.enabled = this.driver.isAnnotationEnabled( ann.layer, ann.properties, 1 ); } From 7b2f4a6f60dfdf61dfcf060117fe1cb4dccbb8d2 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 2 Jul 2026 19:02:59 +0900 Subject: [PATCH 11/11] Support life updates --- src/three/plugins/mvt/MVTAnnotationsPlugin.js | 21 ++++----- src/three/plugins/mvt/TextAnchorManager.js | 45 ++++--------------- .../mvt/annotations/PointAnnotationManager.js | 26 ++++++----- .../mvt/debug/LineAnnotationOverlay.js | 4 +- 4 files changed, 34 insertions(+), 62 deletions(-) diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index 296d9e283..691911cff 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -303,28 +303,23 @@ export class MVTAnnotationsPlugin { this._onUpdateAfter = () => { - // Recompute driver-derived state on every parsed annotation before placement - for ( const { annotations } of this.vectorTileInfo.values() ) { + const { driver, camera } = this; - for ( const ann of annotations ) { - - if ( ann instanceof LineAnnotation ) { - - ann.enabled = this.driver.isAnnotationEnabled( ann.layer, ann.properties, 2 ); - ann.text = this.driver.getText( ann.properties ); + // Recalculate the field state per annotation + for ( const annotation of pointManager.points ) { - } else { + annotation.enabled = driver.isAnnotationEnabled( annotation.layer, annotation.properties, 1 ); - ann.enabled = this.driver.isAnnotationEnabled( ann.layer, ann.properties, 1 ); + } - } + for ( const line of anchorManager.lines ) { - } + line.enabled = driver.isAnnotationEnabled( line.layer, line.properties, 2 ); + line.text = driver.getText( line.properties ); } // sync camera and localToWorld matrix into occupancy grid - const { camera } = this; if ( camera !== null ) { tiles.getResolution( camera, occupancy.resolution ); diff --git a/src/three/plugins/mvt/TextAnchorManager.js b/src/three/plugins/mvt/TextAnchorManager.js index c05bd55f1..b88ac33ce 100644 --- a/src/three/plugins/mvt/TextAnchorManager.js +++ b/src/three/plugins/mvt/TextAnchorManager.js @@ -11,6 +11,11 @@ export class TextAnchorManager { this._anchorsById = new Map(); this._linesById = new Map(); + // flat sets of every tracked line / anchor, maintained on add / delete for allocation-free + // iteration + this.lines = new Set(); + this.anchors = new Set(); + } reset() { @@ -31,6 +36,7 @@ export class TextAnchorManager { if ( anchor.isEmpty() ) { anchors.delete( anchor ); + this.anchors.delete( anchor ); removed.add( anchor ); } @@ -48,42 +54,6 @@ export class TextAnchorManager { } - // retrieve all lines - getLines() { - - const res = []; - this._linesById.forEach( value => { - - value.forEach( line => { - - res.push( line ); - - } ); - - } ); - - return res; - - } - - // retrieve all anchors - getAnchors() { - - const target = []; - this._anchorsById.forEach( anchors => { - - anchors.forEach( anchor => { - - target.push( anchor ); - - } ); - - } ); - - return target; - - } - // add a set of lines // NOTE: This is is designed to be called with all lines from a single tile at once addLines( lines ) { @@ -188,6 +158,7 @@ export class TextAnchorManager { anchorPosition.ref = anchor; anchorSet.add( anchor ); + this.anchors.add( anchor ); added.add( anchor ); } @@ -207,6 +178,7 @@ export class TextAnchorManager { lines.forEach( line => { lineSet.add( line ); + this.lines.add( line ); } ); @@ -227,6 +199,7 @@ export class TextAnchorManager { const id = line.id; _linesById.get( id ).delete( line ); + this.lines.delete( line ); if ( _linesById.get( id ).size === 0 ) { _linesById.delete( id ); diff --git a/src/three/plugins/mvt/annotations/PointAnnotationManager.js b/src/three/plugins/mvt/annotations/PointAnnotationManager.js index 852f46ef4..504bee5b3 100644 --- a/src/three/plugins/mvt/annotations/PointAnnotationManager.js +++ b/src/three/plugins/mvt/annotations/PointAnnotationManager.js @@ -6,24 +6,27 @@ export class PointAnnotationManager { this.added = new Set(); this.removed = new Set(); - this.annotations = new Map(); + this.points = new Set(); + this._annotationsById = new Map(); + } add( annotation ) { - const { annotations, added } = this; + const { _annotationsById, points, added } = this; const { id } = annotation; - if ( ! annotations.has( id ) ) { + if ( ! _annotationsById.has( id ) ) { - annotations.set( id, { annotation, ref: 0 } ); + _annotationsById.set( id, { annotation, ref: 0 } ); + points.add( annotation ); added.add( annotation ); } else { // refine the position of the canonical annotation if the new one is from a // higher LoD. - const canonicalAnnotation = annotations.get( id ).annotation; + const canonicalAnnotation = _annotationsById.get( id ).annotation; if ( annotation.lodLevel > canonicalAnnotation.lodLevel ) { canonicalAnnotation.lodLevel = annotation.lodLevel; @@ -34,15 +37,15 @@ export class PointAnnotationManager { } - annotations.get( id ).ref ++; + _annotationsById.get( id ).ref ++; } delete( annotation ) { - const { annotations } = this; + const { _annotationsById } = this; const { id } = annotation; - const info = annotations.get( id ); + const info = _annotationsById.get( id ); info.ref --; @@ -50,13 +53,14 @@ export class PointAnnotationManager { update() { - const { removed, annotations } = this; - annotations.forEach( ( info, id ) => { + const { removed, points, _annotationsById } = this; + _annotationsById.forEach( ( info, id ) => { if ( info.ref === 0 ) { removed.add( info.annotation ); - annotations.delete( id ); + points.delete( info.annotation ); + _annotationsById.delete( id ); } diff --git a/src/three/plugins/mvt/debug/LineAnnotationOverlay.js b/src/three/plugins/mvt/debug/LineAnnotationOverlay.js index 7d3ecbac4..a406f9879 100644 --- a/src/three/plugins/mvt/debug/LineAnnotationOverlay.js +++ b/src/three/plugins/mvt/debug/LineAnnotationOverlay.js @@ -122,7 +122,7 @@ export class LineAnnotationOverlay { } // settled line paths → segment buffer - const lineItems = anchorManager.getLines().filter( item => item instanceof LineAnnotation && item.ready ); + const lineItems = Array.from( anchorManager.lines ).filter( item => item instanceof LineAnnotation && item.ready ); let segmentCount = 0; for ( const line of lineItems ) { @@ -153,7 +153,7 @@ export class LineAnnotationOverlay { } // anchors at their active ( highest-LoD settled ) path → point buffer - const anchorItems = anchorManager.getAnchors().filter( anchor => anchor.ready ); + const anchorItems = Array.from( anchorManager.anchors ).filter( anchor => anchor.ready ); const pointsPosAttr = new BufferAttribute( new Float32Array( anchorItems.length * 3 ), 3 ); const pointsColAttr = new BufferAttribute( new Float32Array( anchorItems.length * 2 * 3 ), 3 );