diff --git a/example/three/annotationsExample.js b/example/three/annotationsExample.js index c3961b5d9..c4d1af7ac 100644 --- a/example/three/annotationsExample.js +++ b/example/three/annotationsExample.js @@ -11,6 +11,7 @@ import { CesiumIonAuthPlugin, PMTilesOverlay, MVTAnnotationsPlugin, + MVTAnnotationsDriver, UpdateOnChangePlugin, } from '3d-tiles-renderer/plugins'; import { LoadRegionPlugin } from '3d-tiles-renderer/three/plugins'; @@ -110,8 +111,7 @@ const params = { }; let controls, scene, renderer, camera, tiles; -let annotationsPoints = null; -let characterPoints = null; +let driver = null; // raycasting const pointer = new Vector2(); @@ -119,33 +119,128 @@ const raycaster = new Raycaster(); const tooltip = document.getElementById( 'tooltip' ); let lastClientX = 0, lastClientY = 0; -init(); -animate(); +// supplies the plugin's annotation callbacks and owns its render objects ( added to the driver's +// group, which the plugin mounts under the tile group ): point annotations -> `annotationPoints`, +// text anchors -> `characterPoints` +class ExampleAnnotationsDriver extends MVTAnnotationsDriver { + + constructor() { + + super(); -// route occupancy annotations to the right renderer: point annotations expose a `position`, -// text-anchor annotations expose `characterPositions` -function splitAnnotations( set ) { + const dpr = renderer.getPixelRatio(); - const points = []; - const text = []; - for ( const item of set ) { + // icons for point annotations + const annotationPoints = new AnnotationPoints( { + getKind: ( layer, properties ) => { - if ( item.characterPositions !== undefined ) { + return KIND_TO_ICON[ properties.kind ] || 'point'; + + } + } ); + + // fallback dot + maki icons drawn into the atlas + annotationPoints.glyphAtlas.drawChar( 'point', '●', { + fillStyle: 'white', + strokeStyle: '#3f3e4c', + strokeWidth: 3 * dpr, + font: '30px sans-serif', + } ); + + MAKI_ICONS.forEach( icon => + fetch( MAKI_BASE + icon + '.svg' ) + .then( r => r.text() ) + .then( svgText => { + + annotationPoints.glyphAtlas.drawSVG( icon, svgText, { + fillStyle: 'white', + strokeStyle: '#3f3e4c', + strokeWidth: 3 * dpr, + iconScale: 0.9, + } ); + + } ) + .catch( () => null ) + + ); + + // glyphs for text ( road ) annotations + const characterPoints = new CharacterPoints( { + strokeStyle: '#3f3e4c', + strokeWidth: 3 * dpr, + } ); + + this.group.add( annotationPoints, characterPoints ); + this.annotationPoints = annotationPoints; + this.characterPoints = characterPoints; + + } - text.push( item ); + filterAnnotation( layer, properties, type ) { + + if ( type === 1 ) { + + return properties.kind in KIND_TO_ICON; } else { - points.push( item ); + return 'name' in properties; + + } + + } + + measureChar( char ) { + + return this.characterPoints.measureChar( char ); + + } + + onAnnotationsUpdate( added, removed ) { + + const a = splitAnnotations( added ); + const r = splitAnnotations( removed ); + this.annotationPoints.update( a.points, r.points ); + this.characterPoints.update( a.text, r.text ); + + // route occupancy annotations to the right renderer: point annotations expose a `position`, + // text-anchor annotations expose `characterPositions` + function splitAnnotations( set ) { + + const points = []; + const text = []; + for ( const item of set ) { + + if ( item.characterPositions !== undefined ) { + + text.push( item ); + + } else { + + points.push( item ); + + } + + } + + return { points, text }; } } - return { points, text }; + dispose() { + + this.annotationPoints.dispose(); + this.characterPoints.dispose(); + + } } +init(); +animate(); + function reinstantiateTiles() { if ( tiles ) { @@ -168,32 +263,11 @@ function reinstantiateTiles() { } ) ); tiles.registerPlugin( new TilesFadePlugin() ); tiles.registerPlugin( new MeshBVHPlugin() ); + driver = new ExampleAnnotationsDriver(); tiles.registerPlugin( new MVTAnnotationsPlugin( { overlay, camera, - filterAnnotation: ( layer, properties, type ) => { - - if ( type === 1 ) { - - return properties.kind in KIND_TO_ICON; - - } else { - - return 'name' in properties; - - } - - }, - onAnnotationsUpdate: ( added, removed ) => { - - const a = splitAnnotations( added ); - const r = splitAnnotations( removed ); - annotationsPoints.update( a.points, r.points ); - characterPoints.update( a.text, r.text ); - - }, - // deferred: characterPoints is created below, but this only runs at update time - measureChar: char => characterPoints.measureChar( char ), + driver, } ) ); // use the camera cartographic region plugin to prevent particularly low-lod @@ -208,47 +282,6 @@ function reinstantiateTiles() { regions: [ cameraRegion ], } ) ); - annotationsPoints = new AnnotationPoints( { - getKind: ( layer, properties ) => { - - return KIND_TO_ICON[ properties.kind ] || 'point'; - - } - } ); - - // load and draw all the icons - annotationsPoints.glyphAtlas.drawChar( 'point', '●', { - fillStyle: 'white', - strokeStyle: '#3f3e4c', - strokeWidth: 3 * renderer.getPixelRatio(), - font: '30px sans-serif', - } ); - - MAKI_ICONS.forEach( icon => - fetch( MAKI_BASE + icon + '.svg' ) - .then( r => r.text() ) - .then( svgText => { - - annotationsPoints.glyphAtlas.drawSVG( icon, svgText, { - fillStyle: 'white', - strokeStyle: '#3f3e4c', - strokeWidth: 3 * renderer.getPixelRatio(), - iconScale: 0.9, - } ); - - } ) - .catch( () => null ) - - ); - - tiles.group.add( annotationsPoints ); - - characterPoints = new CharacterPoints( { - strokeStyle: '#3f3e4c', - strokeWidth: 3 * renderer.getPixelRatio(), - } ); - tiles.group.add( characterPoints ); - tiles.group.rotation.x = - Math.PI / 2; scene.add( tiles.group ); @@ -332,7 +365,7 @@ function updateTooltip() { raycaster.setFromCamera( pointer, camera ); - const hits = raycaster.intersectObject( annotationsPoints ); + const hits = raycaster.intersectObject( driver.annotationPoints ); if ( hits.length > 0 ) { const { properties } = hits[ 0 ]; diff --git a/example/three/src/plugins/mvt/CharacterPoints.js b/example/three/src/plugins/mvt/CharacterPoints.js index 5aa9b8245..2dbac18b8 100644 --- a/example/three/src/plugins/mvt/CharacterPoints.js +++ b/example/three/src/plugins/mvt/CharacterPoints.js @@ -2,6 +2,8 @@ import { BufferAttribute } from 'three'; import { GlyphMaterial } from './GlyphMaterial.js'; import { GlyphPoints } from './GlyphPoints.js'; +const _uvTarget = {}; + export class CharacterPoints extends GlyphPoints { constructor( options = {} ) { @@ -9,7 +11,7 @@ export class CharacterPoints extends GlyphPoints { const { size = 16, glyphSize = 16 * window.devicePixelRatio, - slotCount = 256, + slotCount = 64, font = null, strokeStyle = 'black', strokeWidth = 0, @@ -19,15 +21,19 @@ export class CharacterPoints extends GlyphPoints { // CSS font used to rasterize glyphs, sized to fit the atlas slot const fontSize = Math.round( glyphSize * 0.7 ); - this._font = font ?? `400 ${ fontSize }px Arial`; + this._font = font ?? `400 ${ fontSize }px sans-serif`; // canvas context for measuring advance widths, normalized to em units ( width / fontSize ) this._advanceCache = new Map(); - // black halo so glyphs read over the imagery + // styling this._strokeStyle = strokeStyle; this._strokeWidth = strokeWidth; + // the refs and unused list of the character glyphs + this._refs = {}; + this._unused = []; + this.glyphAtlas.resize( slotCount, glyphSize ); } @@ -50,30 +56,78 @@ export class CharacterPoints extends GlyphPoints { } - // uv bounds of the glyph for `char`, rasterizing it into the atlas on first use - _glyphUV( char ) { + update( added, removed ) { - // TODO: we need to use a smart counter to determine when - // to free a slot - const { glyphAtlas } = this; - if ( ! glyphAtlas.has( char ) ) { + super.update( added, removed ); + const { _refs, _unused, _removed, glyphAtlas } = this; - glyphAtlas.drawChar( char, char, { - font: this._font, - color: 'white', - strokeStyle: this._strokeStyle, - strokeWidth: this._strokeWidth, - } ); + // iterate over all the added annotations and increment the ref, drawing the + // glyph to the atlas if needed. + added.forEach( ( { text } ) => { - } + for ( let i = 0, l = text.length; i < l; i ++ ) { + + const char = text[ i ]; + if ( ! ( char in _refs ) ) { + + // if we've reached our capacity + if ( glyphAtlas.capacity === glyphAtlas.count ) { + + if ( _unused.length > 0 ) { + + // if there are unused slots then free up one of those + const unusedChar = _unused.pop(); + glyphAtlas.release( unusedChar ); + delete _refs[ unusedChar ]; + + } else { + + // otherwise resize the atlas to fit more glyphs + glyphAtlas.resize( glyphAtlas.capacity * 2 ); + + } + + } + + // draw the glyph + _refs[ char ] = 0; + glyphAtlas.drawChar( char, char, { + font: this._font, + color: 'white', + strokeStyle: this._strokeStyle, + strokeWidth: this._strokeWidth, + } ); + + } + + _refs[ 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 ++ ) { + + const char = text[ i ]; + _refs[ char ] --; + if ( _refs[ char ] === 0 ) { + + _unused.push( char ); + + } + + } - return glyphAtlas.getUV( char ); + } ); } _updateGeometry() { - const { _orderedEntries, geometry, position } = this; + const { _orderedEntries, geometry, position, glyphAtlas } = this; // total character count across all entries ( including fading ones ) let count = 0; @@ -117,7 +171,7 @@ export class CharacterPoints extends GlyphPoints { const p = positions[ c ]; posAttr.setXYZ( i, p.x - position.x, p.y - position.y, p.z - position.z ); - const uv = this._glyphUV( text[ c ] ); + const uv = glyphAtlas.getUV( text[ c ], _uvTarget ); if ( uv !== null ) { glyphUVAttr.setXY( i, uv.x, uv.y ); diff --git a/example/three/src/plugins/mvt/GlyphPoints.js b/example/three/src/plugins/mvt/GlyphPoints.js index 75b888bbc..57a3ce844 100644 --- a/example/three/src/plugins/mvt/GlyphPoints.js +++ b/example/three/src/plugins/mvt/GlyphPoints.js @@ -38,6 +38,15 @@ export class GlyphPoints extends Points { this._entryMap = new Map(); this._orderedEntries = []; this._lastUpdateTime = - 1; + this._removed = []; + + } + + dispose() { + + this.glyphAtlas.dispose(); + this.geometry.dispose(); + this.material.dispose(); } @@ -48,7 +57,9 @@ export class GlyphPoints extends Points { const dt = this._lastUpdateTime < 0 ? 0 : Math.min( now - this._lastUpdateTime, 0.1 ); this._lastUpdateTime = now; - const { _entryMap, _orderedEntries, fadeInDuration, fadeOutDuration } = this; + const { _entryMap, _orderedEntries, _removed, fadeInDuration, fadeOutDuration } = this; + _removed.length = 0; + for ( const item of added ) { const existing = _entryMap.get( item.id ); @@ -79,7 +90,6 @@ export class GlyphPoints extends Points { } - const toRemove = []; for ( const [ id, entry ] of _entryMap ) { if ( entry.state === 'in' ) { @@ -96,7 +106,8 @@ export class GlyphPoints extends Points { entry.fade = Math.max( 0, entry.fade - dt / fadeOutDuration ); if ( entry.fade <= 0 ) { - toRemove.push( id ); + _entryMap.delete( id ); + _removed.push( entry.item ); } @@ -104,13 +115,7 @@ export class GlyphPoints extends Points { } - if ( toRemove.length > 0 ) { - - for ( const id of toRemove ) { - - _entryMap.delete( id ); - - } + if ( _removed.length > 0 ) { this._orderedEntries = _orderedEntries.filter( e => _entryMap.has( e.item.id ) ); @@ -118,6 +123,8 @@ export class GlyphPoints extends Points { this._updateGeometry(); + return _removed; + } onAfterRender( renderer, scene, camera ) { diff --git a/src/three/plugins/API.md b/src/three/plugins/API.md index f9cc232a0..9a78a94e5 100644 --- a/src/three/plugins/API.md +++ b/src/three/plugins/API.md @@ -920,12 +920,30 @@ Slots are addressed by string key and can be drawn with text, images, or paths. ### .isFull ```js -isFull: +isFull: boolean ``` Returns true when all slots are allocated. +### .capacity + +```js +capacity: number +``` + +Returns the total number of icons that can be added to the atlas. + + +### .count + +```js +count: number +``` + +Returns the number of icons currently used. + + ### .constructor ```js @@ -1230,11 +1248,84 @@ dispose(): void Disposes all textures used by this instance. +## MVTAnnotationsDriver + +Bundles the callbacks the "MVTAnnotationsPlugin" needs into a single object. Subclass and override +the methods to customize which features become annotations, their placement priority, per-character +sizing, the displayed text, and how visibility changes are rendered. + + +### .group + +```js +group: Group +``` + +Render group for the driver's own three.js objects. The plugin mounts it under +`tiles.group` on `init` and removes it on `dispose`; add any objects the driver draws to it. + + +### .filterAnnotation + +```js +filterAnnotation( layer: string, properties: Object, type: number ): boolean +``` + +Whether an MVT feature should be included as an annotation. + + +### .sortAnnotations + +```js +sortAnnotations( a: Object, b: Object ): number +``` + +Relative placement priority between two annotations, following the `Array.prototype.sort` +contract. Lower values sort first, are placed first, and win collisions. + + +### .measureChar + +```js +measureChar( char: string ): number +``` + +Advance width of a single character, in pixels, used to space glyphs along text labels. + + +### .getText + +```js +getText( properties: Object ): string +``` + +The string a line / road annotation should display for the given feature. + + +### .onAnnotationsUpdate + +```js +onAnnotationsUpdate( added: Set, removed: Set ): void +``` + +Called each frame with the annotations whose visibility changed, for the caller to render. + + +### .dispose + +```js +dispose(): void +``` + +Releases any resources the driver created (geometries, materials, textures, etc.). Called by +the plugin from its own `dispose`. + + ## MVTAnnotationsPlugin Plugin that extracts point features from an MVT overlay and manages their screen-space occupation, preventing label crowding via a hierarchical lock system and raycasted depth -placement. Rendering is left entirely to the caller via `onAnnotationsUpdate`. +placement. Rendering is left entirely to the caller via the driver's `onAnnotationsUpdate`. ### .constructor @@ -1249,12 +1340,10 @@ constructor( // Initial camera. Can be updated with `setCamera()`. camera = null: Camera, - // Three.js scene reference (stored for caller use). - scene = null: Scene, - - // Takes a character nd returns a per-character advance width - // used for text label spacing. - measureChar?: function, + // Supplies the annotation callbacks: feature filtering, + // placement priority, per-character sizing, and render + // updates. + driver?: MVTAnnotationsDriver, } ) ``` diff --git a/src/three/plugins/mvt/GlyphAtlasTexture.js b/src/three/plugins/mvt/GlyphAtlasTexture.js index cc80216c7..855e30095 100644 --- a/src/three/plugins/mvt/GlyphAtlasTexture.js +++ b/src/three/plugins/mvt/GlyphAtlasTexture.js @@ -9,13 +9,34 @@ export class GlyphAtlasTexture extends CanvasTexture { /** * Returns true when all slots are allocated. - * @returns {boolean} */ + * @type {boolean} + **/ get isFull() { return this._freeList.length === 0 && this._nextIndex >= this._capacity; } + /** + * Returns the total number of icons that can be added to the atlas. + * @type {number} + **/ + get capacity() { + + return this._capacity; + + } + + /** + * Returns the number of icons currently used. + * @type {number} + **/ + get count() { + + return this._slots.size; + + } + /** * @param {number} slotCount - Maximum number of slots in the atlas. * @param {number} slotSize - Width and height of each slot in pixels. @@ -27,6 +48,8 @@ export class GlyphAtlasTexture extends CanvasTexture { this.generateMipmaps = false; this.slotSize = 0; + this._columns = - 1; + this._capacity = - 1; // key -> slot index this._slots = new Map(); @@ -88,18 +111,18 @@ export class GlyphAtlasTexture extends CanvasTexture { * @param {string} key * @returns {{ x: number, y: number, w: number, h: number } | null} */ - getUV( key ) { + getUV( key, target = {} ) { + target = {}; const slot = this.get( key ); if ( slot === null ) return null; - const { width, height } = this.image; - return { - x: slot.x / width, - y: ( height - slot.y ) / height, - w: this.slotSize / width, - h: this.slotSize / height, - }; + const { width, height, slotSize } = this.image; + target.x = slot.x / width; + target.y = ( height - slot.y ) / height; + target.w = slotSize / width; + target.h = slotSize / height; + return target; } @@ -358,6 +381,8 @@ export class GlyphAtlasTexture extends CanvasTexture { } + this.dispose(); + this.image = canvas; this.ctx = ctx; this.slotSize = slotSize; @@ -421,6 +446,8 @@ export class GlyphAtlasTexture extends CanvasTexture { ctx.beginPath(); ctx.rect( slot.x, slot.y, slot.w, slot.h ); ctx.clip(); + + ctx.clearRect( slot.x, slot.y, slot.w, slot.h ); callback( ctx, slot.x, slot.y, slot.w, slot.h ); ctx.restore(); diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index c06ede8ae..92f6c83a8 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -1,5 +1,5 @@ /** @import { Camera, Scene } from 'three' */ -import { Matrix4 } from 'three'; +import { Group, Matrix4 } from 'three'; import { MVTHierarchy } from './MVTHierarchy.js'; import { DelayedScreenOccupationManager } from './DelayedScreenOccupationManager.js'; import { SettlingManager } from './SettlingManager.js'; @@ -45,17 +45,101 @@ function collectMeshes( object ) { * @param {Set} removed - `PointAnnotationItem` instances that became hidden this frame. */ +/** + * Bundles the callbacks the "MVTAnnotationsPlugin" needs into a single object. Subclass and override + * the methods to customize which features become annotations, their placement priority, per-character + * sizing, the displayed text, and how visibility changes are rendered. + */ +export class MVTAnnotationsDriver { + + constructor() { + + /** + * Render group for the driver's own three.js objects. The plugin mounts it under + * `tiles.group` on `init` and removes it on `dispose`; add any objects the driver draws to it. + * @type {Group} + */ + this.group = new Group(); + + } + + /** + * Whether an MVT feature should be included as an annotation. + * @param {string} layer - The MVT layer name the feature belongs to. + * @param {Object} properties - The feature's property map. + * @param {number} type - The MVT geometry type: `1` = point, `2` = line. + * @returns {boolean} True to include the feature as an annotation. + */ + filterAnnotation( layer, properties, type ) { + + return false; + + } + + /** + * Relative placement priority between two annotations, following the `Array.prototype.sort` + * contract. Lower values sort first, are placed first, and win collisions. + * @param {Object} a - The first annotation. + * @param {Object} b - The second annotation. + * @returns {number} Negative if `a` precedes `b`, positive if it follows, `0` if equal. + */ + sortAnnotations( a, b ) { + + const rankA = a.properties[ 'rank' ] ?? 1e10; + const rankB = b.properties[ 'rank' ] ?? 1e10; + return rankA - rankB; + + } + + /** + * Advance width of a single character, in pixels, used to space glyphs along text labels. + * @param {string} char - The character to measure. + * @returns {number} The advance width in pixels. + */ + measureChar( char ) { + + return 1; + + } + + /** + * The string a line / road annotation should display for the given feature. + * @param {Object} properties - The feature's property map. + * @returns {string} The label text, or an empty string to render nothing. + */ + getText( properties ) { + + return properties.name ?? ''; + + } + + /** + * Called each frame with the annotations whose visibility changed, for the caller to render. + * @param {Set} added - Annotations that became visible this frame. + * @param {Set} removed - Annotations that became hidden this frame. + * @returns {void} + */ + onAnnotationsUpdate( added, removed ) {} + + /** + * Releases any resources the driver created (geometries, materials, textures, etc.). Called by + * the plugin from its own `dispose`. + * @returns {void} + */ + dispose() {} + +} + /** * Plugin that extracts point features from an MVT overlay and manages their screen-space * occupation, preventing label crowding via a hierarchical lock system and raycasted depth - * placement. Rendering is left entirely to the caller via `onAnnotationsUpdate`. + * placement. Rendering is left entirely to the caller via the driver's `onAnnotationsUpdate`. * @param {Object} options * @param {Object} options.overlay - The `PMTilesOverlay` (or compatible overlay) whose tile * content is parsed for point features. * @param {Camera} [options.camera=null] - Initial camera. Can be updated with `setCamera()`. - * @param {Scene} [options.scene=null] - Three.js scene reference (stored for caller use). - * @param {Function} [options.measureChar] - Takes a character nd returns a per-character - * advance width used for text label spacing. + * @param {MVTAnnotationsDriver} [options.driver] - Supplies the annotation callbacks: feature + * filtering, placement priority, per-character sizing, and render updates. */ export class MVTAnnotationsPlugin { @@ -73,26 +157,19 @@ export class MVTAnnotationsPlugin { const { overlay, - sortCallback = ( a, b ) => { - - const rankA = a.properties[ 'rank' ] ?? 1e10; - const rankB = b.properties[ 'rank' ] ?? 1e10; - return rankA - rankB; - - }, - filterAnnotation = () => false, - onAnnotationsUpdate = () => {}, - measureChar = () => 1, camera = null, + driver = new MVTAnnotationsDriver(), } = options; // user settings this.overlay = overlay; this.camera = camera; - this.sortCallback = sortCallback; - this.filterAnnotation = filterAnnotation; - this.onAnnotationsUpdate = onAnnotationsUpdate; - this.measureChar = measureChar; + this.driver = driver; + + // 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(); @@ -117,6 +194,10 @@ export class MVTAnnotationsPlugin { // init this.tiles = tiles; + // mount the driver's render group under the tile group + tiles.group.add( this.driver.group ); + this.driver.group.updateMatrixWorld(); + const { overlay, occupancy, @@ -161,7 +242,7 @@ export class MVTAnnotationsPlugin { } - const sort = this.sortCallback( a, b ); + const sort = this.driver.sortAnnotations( a, b ); if ( sort !== 0 ) { // user sort @@ -242,7 +323,8 @@ export class MVTAnnotationsPlugin { anchorManager.update(); anchorManager.added.forEach( item => { - item.measureChar = this.measureChar; + item.measureChar = this._measureChar; + item.getText = this._getText; occupancy.register( item ); } ); @@ -260,7 +342,7 @@ export class MVTAnnotationsPlugin { // occupancy occupancy.camera = camera; occupancy.update(); - this.onAnnotationsUpdate( occupancy.added, occupancy.removed ); + this.driver.onAnnotationsUpdate( occupancy.added, occupancy.removed ); if ( occupancy.added.size > 0 || occupancy.removed.size > 0 ) { @@ -288,7 +370,7 @@ export class MVTAnnotationsPlugin { const { contentCache, - filterAnnotation, + _filterAnnotation, vectorTileInfo, settlingManager, anchorManager, @@ -310,8 +392,8 @@ export class MVTAnnotationsPlugin { // parse the icon annotations const annotations = []; - parsePointAnnotations( vectorTile, x, y, level, tiling, filterAnnotation, annotations ); - parseLineAnnotations( vectorTile, x, y, level, tiling, filterAnnotation, annotations ); + parsePointAnnotations( vectorTile, x, y, level, tiling, _filterAnnotation, annotations ); + parseLineAnnotations( vectorTile, x, y, level, tiling, _filterAnnotation, annotations ); vectorTileInfo.set( key, { annotations } ); for ( const ann of annotations ) { @@ -391,6 +473,10 @@ export class MVTAnnotationsPlugin { debug.occupancy.dispose(); debug.paths.dispose(); + // unmount and dispose the driver's render group + tiles.group.remove( this.driver.group ); + this.driver.dispose(); + hierarchy.removeEventListener( 'toggle', this._onVectorTileToggle ); tiles.removeEventListener( 'update-after', this._onUpdateAfter ); tiles.removeEventListener( 'tile-visibility-change', this._onVisibilityChange ); diff --git a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js index f8e27fc0d..25c2017c5 100644 --- a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js +++ b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js @@ -29,8 +29,7 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { get text() { - // TODO: this should be derivable from the user specified driver - return this.properties.name || ''; + return this.getText( this.properties ); } @@ -70,6 +69,9 @@ 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 ?? ''; + // 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;