Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 112 additions & 79 deletions example/three/annotationsExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
CesiumIonAuthPlugin,
PMTilesOverlay,
MVTAnnotationsPlugin,
MVTAnnotationsDriver,
UpdateOnChangePlugin,
} from '3d-tiles-renderer/plugins';
import { LoadRegionPlugin } from '3d-tiles-renderer/three/plugins';
Expand Down Expand Up @@ -110,42 +111,136 @@ const params = {
};

let controls, scene, renderer, camera, tiles;
let annotationsPoints = null;
let characterPoints = null;
let driver = null;

// raycasting
const pointer = new Vector2();
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 ) {
Expand All @@ -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
Expand All @@ -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 );

Expand Down Expand Up @@ -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 ];
Expand Down
92 changes: 73 additions & 19 deletions example/three/src/plugins/mvt/CharacterPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { BufferAttribute } from 'three';
import { GlyphMaterial } from './GlyphMaterial.js';
import { GlyphPoints } from './GlyphPoints.js';

const _uvTarget = {};

export class CharacterPoints extends GlyphPoints {

constructor( options = {} ) {

const {
size = 16,
glyphSize = 16 * window.devicePixelRatio,
slotCount = 256,
slotCount = 64,
font = null,
strokeStyle = 'black',
strokeWidth = 0,
Expand All @@ -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 );

}
Expand All @@ -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;
Expand Down Expand Up @@ -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 );
Expand Down
Loading
Loading