feat(All):Initial
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
export class EnforceNonZeroErrorPlugin {}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
export class EnforceNonZeroErrorPlugin {
|
||||
|
||||
constructor() {
|
||||
|
||||
this.name = 'ENFORCE_NONZERO_ERROR';
|
||||
this.priority = - Infinity;
|
||||
this.originalError = new Map();
|
||||
|
||||
}
|
||||
|
||||
preprocessNode( tile ) {
|
||||
|
||||
// if a tile has zero error then traverse the parents and find some geometric error value in
|
||||
// the parent hierarchy to use for calculating a pseudo geometric error for this tile.
|
||||
if ( tile.geometricError === 0 ) {
|
||||
|
||||
let parent = tile.parent;
|
||||
let depth = 1;
|
||||
|
||||
let targetDepth = - 1;
|
||||
let targetError = Infinity;
|
||||
while ( parent !== null ) {
|
||||
|
||||
if ( parent.geometricError !== 0 && parent.geometricError < targetError ) {
|
||||
|
||||
targetError = parent.geometricError;
|
||||
targetDepth = depth;
|
||||
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
depth ++;
|
||||
|
||||
}
|
||||
|
||||
// find the smallest error in the parent list to avoid grabbing artificially inflated error values
|
||||
// for the sake of forced refinement. Then scale the error by the depth.
|
||||
if ( targetDepth !== - 1 ) {
|
||||
|
||||
tile.geometricError = targetError * ( 2 ** - depth );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export class ImplicitTilingPlugin {}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { SUBTREELoader } from './SUBTREELoader.js';
|
||||
|
||||
export class ImplicitTilingPlugin {
|
||||
|
||||
constructor() {
|
||||
|
||||
this.name = 'IMPLICIT_TILING_PLUGIN';
|
||||
|
||||
}
|
||||
|
||||
init( tiles ) {
|
||||
|
||||
this.tiles = tiles;
|
||||
|
||||
}
|
||||
|
||||
preprocessNode( tile, tileSetDir, parentTile ) {
|
||||
|
||||
if ( tile.implicitTiling ) {
|
||||
|
||||
tile.__hasUnrenderableContent = true;
|
||||
tile.__hasRenderableContent = false;
|
||||
|
||||
// Declare some properties
|
||||
tile.__subtreeIdx = 0; // Idx of the tile in its subtree
|
||||
tile.__implicitRoot = tile; // Keep this tile as an Implicit Root Tile
|
||||
|
||||
// Coords of the tile
|
||||
tile.__x = 0;
|
||||
tile.__y = 0;
|
||||
tile.__z = 0;
|
||||
tile.__level = 0;
|
||||
|
||||
} else if ( /.subtree$/i.test( tile.content?.uri ) ) {
|
||||
|
||||
// Handling content uri pointing to a subtree file
|
||||
tile.__hasUnrenderableContent = true;
|
||||
tile.__hasRenderableContent = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
parseTile( buffer, tile, extension ) {
|
||||
|
||||
if ( /^subtree$/i.test( extension ) ) {
|
||||
|
||||
const loader = new SUBTREELoader( tile );
|
||||
loader.workingPath = tile.__basePath;
|
||||
loader.fetchOptions = this.tiles.fetchOptions;
|
||||
return loader.parse( buffer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
preprocessURL( url, tile ) {
|
||||
|
||||
if ( tile && tile.implicitTiling ) {
|
||||
|
||||
const implicitUri = tile.implicitTiling.subtrees.uri
|
||||
.replace( '{level}', tile.__level )
|
||||
.replace( '{x}', tile.__x )
|
||||
.replace( '{y}', tile.__y )
|
||||
.replace( '{z}', tile.__z );
|
||||
|
||||
return new URL( implicitUri, tile.__basePath + '/' ).toString();
|
||||
|
||||
}
|
||||
|
||||
return url;
|
||||
|
||||
}
|
||||
|
||||
disposeTile( tile ) {
|
||||
|
||||
if ( /.subtree$/i.test( tile.content?.uri ) ) {
|
||||
|
||||
// TODO: ideally the plugin doesn't need to know about children being processed
|
||||
tile.children.forEach( child => {
|
||||
|
||||
// TODO: there should be a reliable way for removing children like this.
|
||||
this.tiles.processNodeQueue.remove( child );
|
||||
|
||||
} );
|
||||
tile.children.length = 0;
|
||||
tile.__childrenProcessed = 0;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,867 @@
|
||||
/**
|
||||
* Structure almost identical to Cesium, also the comments and the names are kept
|
||||
* https://github.com/CesiumGS/cesium/blob/0a69f67b393ba194eefb7254600811c4b712ddc0/packages/engine/Source/Scene/Implicit3DTileContent.js
|
||||
*/
|
||||
import { LoaderBase, LoaderUtils } from '3d-tiles-renderer/core';
|
||||
|
||||
function isOctreeSubdivision( tile ) {
|
||||
|
||||
return tile.__implicitRoot.implicitTiling.subdivisionScheme === 'OCTREE';
|
||||
|
||||
}
|
||||
|
||||
function getBoundsDivider( tile ) {
|
||||
|
||||
return isOctreeSubdivision( tile ) ? 8 : 4;
|
||||
|
||||
}
|
||||
|
||||
function getSubtreeCoordinates( tile, parentTile ) {
|
||||
|
||||
if ( ! parentTile ) {
|
||||
|
||||
return [ 0, 0, 0 ];
|
||||
|
||||
}
|
||||
const x = 2 * parentTile.__x + ( tile.__subtreeIdx % 2 );
|
||||
const y = 2 * parentTile.__y + ( Math.floor( tile.__subtreeIdx / 2 ) % 2 );
|
||||
const z = isOctreeSubdivision( tile ) ?
|
||||
2 * parentTile.__z + ( Math.floor( tile.__subtreeIdx / 4 ) % 2 ) : 0;
|
||||
return [ x, y, z ];
|
||||
|
||||
}
|
||||
|
||||
class SubtreeTile {
|
||||
|
||||
constructor( parentTile, childMortonIndex ) {
|
||||
|
||||
this.parent = parentTile;
|
||||
this.children = [];
|
||||
this.__level = parentTile.__level + 1;
|
||||
this.__implicitRoot = parentTile.__implicitRoot;
|
||||
// Index inside the tree
|
||||
this.__subtreeIdx = childMortonIndex;
|
||||
[ this.__x, this.__y, this.__z ] = getSubtreeCoordinates( this, parentTile );
|
||||
|
||||
}
|
||||
|
||||
static copy( tile ) {
|
||||
|
||||
const copyTile = {};
|
||||
copyTile.children = [];
|
||||
copyTile.__level = tile.__level;
|
||||
copyTile.__implicitRoot = tile.__implicitRoot;
|
||||
// Index inside the tree
|
||||
copyTile.__subtreeIdx = tile.__subtreeIdx;
|
||||
[ copyTile.__x, copyTile.__y, copyTile.__z ] = [ tile.__x, tile.__y, tile.__z ];
|
||||
copyTile.boundingVolume = tile.boundingVolume;
|
||||
copyTile.geometricError = tile.geometricError;
|
||||
return copyTile;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class SUBTREELoader extends LoaderBase {
|
||||
|
||||
constructor( tile ) {
|
||||
|
||||
super();
|
||||
this.tile = tile;
|
||||
this.rootTile = tile.__implicitRoot; // The implicit root tile
|
||||
this.workingPath = null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper object for storing the two parts of the subtree binary
|
||||
*
|
||||
* @typedef {object} Subtree
|
||||
* @property {number} version
|
||||
* @property {JSON} subtreeJson
|
||||
* @property {ArrayBuffer} subtreeByte
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param buffer
|
||||
* @return {Subtree}
|
||||
*/
|
||||
parseBuffer( buffer ) {
|
||||
|
||||
const dataView = new DataView( buffer );
|
||||
let offset = 0;
|
||||
// 16-byte header
|
||||
// 4 bytes
|
||||
const magic = LoaderUtils.readMagicBytes( dataView );
|
||||
console.assert( magic === 'subt', 'SUBTREELoader: The magic bytes equal "subt".' );
|
||||
offset += 4;
|
||||
// 4 bytes
|
||||
const version = dataView.getUint32( offset, true );
|
||||
console.assert( version === 1, 'SUBTREELoader: The version listed in the header is "1".' );
|
||||
offset += 4;
|
||||
// From Cesium
|
||||
// Read the bottom 32 bits of the 64-bit byte length.
|
||||
// This is ok for now because:
|
||||
// 1) not all browsers have native 64-bit operations
|
||||
// 2) the data is well under 4GB
|
||||
// 8 bytes
|
||||
const jsonLength = dataView.getUint32( offset, true );
|
||||
offset += 8;
|
||||
// 8 bytes
|
||||
const byteLength = dataView.getUint32( offset, true );
|
||||
offset += 8;
|
||||
const subtreeJson = JSON.parse( LoaderUtils.arrayToString( new Uint8Array( buffer, offset, jsonLength ) ) );
|
||||
offset += jsonLength;
|
||||
const subtreeByte = buffer.slice( offset, offset + byteLength );
|
||||
return {
|
||||
version,
|
||||
subtreeJson,
|
||||
subtreeByte
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
async parse( buffer ) {
|
||||
|
||||
// todo here : handle json
|
||||
const subtree = this.parseBuffer( buffer );
|
||||
const subtreeJson = subtree.subtreeJson;
|
||||
|
||||
// TODO Handle metadata
|
||||
/*
|
||||
const subtreeMetadata = subtreeJson.subtreeMetadata;
|
||||
subtree._metadata = subtreeMetadata;
|
||||
*/
|
||||
|
||||
/*
|
||||
Tile availability indicates which tiles exist within the subtree
|
||||
Content availability indicates which tiles have associated content resources
|
||||
Child subtree availability indicates what subtrees are reachable from this subtree
|
||||
*/
|
||||
|
||||
// After identifying how availability is stored, put the results in this new array for consistent processing later
|
||||
subtreeJson.contentAvailabilityHeaders = [].concat( subtreeJson.contentAvailability );
|
||||
const bufferHeaders = this.preprocessBuffers( subtreeJson.buffers );
|
||||
const bufferViewHeaders = this.preprocessBufferViews(
|
||||
subtreeJson.bufferViews,
|
||||
bufferHeaders
|
||||
);
|
||||
|
||||
// Buffers and buffer views are inactive until explicitly marked active.
|
||||
// This way we can avoid fetching buffers that will not be used.
|
||||
this.markActiveBufferViews( subtreeJson, bufferViewHeaders );
|
||||
|
||||
// Await the active buffers. If a buffer is external (isExternal === true),
|
||||
// fetch it from its URI.
|
||||
const buffersU8 = await this.requestActiveBuffers(
|
||||
bufferHeaders,
|
||||
subtree.subtreeByte
|
||||
);
|
||||
const bufferViewsU8 = this.parseActiveBufferViews( bufferViewHeaders, buffersU8 );
|
||||
this.parseAvailability( subtree, subtreeJson, bufferViewsU8 );
|
||||
this.expandSubtree( this.tile, subtree );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which buffer views need to be loaded into memory. This includes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The tile availability bitstream (if a bitstream is defined)</li>
|
||||
* <li>The content availability bitstream(s) (if a bitstream is defined)</li>
|
||||
* <li>The child subtree availability bitstream (if a bitstream is defined)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* This function modifies the buffer view headers' isActive flags in place.
|
||||
* </p>
|
||||
*
|
||||
* @param {JSON} subtreeJson The JSON chunk from the subtree
|
||||
* @param {BufferViewHeader[]} bufferViewHeaders The preprocessed buffer view headers
|
||||
* @private
|
||||
*/
|
||||
markActiveBufferViews( subtreeJson, bufferViewHeaders ) {
|
||||
|
||||
let header;
|
||||
const tileAvailabilityHeader = subtreeJson.tileAvailability;
|
||||
// Check for bitstream first, which is part of the current schema.
|
||||
// bufferView is the name of the bitstream from an older schema.
|
||||
if ( ! isNaN( tileAvailabilityHeader.bitstream ) ) {
|
||||
|
||||
header = bufferViewHeaders[ tileAvailabilityHeader.bitstream ];
|
||||
|
||||
} else if ( ! isNaN( tileAvailabilityHeader.bufferView ) ) {
|
||||
|
||||
header = bufferViewHeaders[ tileAvailabilityHeader.bufferView ];
|
||||
|
||||
}
|
||||
if ( header ) {
|
||||
|
||||
header.isActive = true;
|
||||
header.bufferHeader.isActive = true;
|
||||
|
||||
}
|
||||
const contentAvailabilityHeaders = subtreeJson.contentAvailabilityHeaders;
|
||||
for ( let i = 0; i < contentAvailabilityHeaders.length; i ++ ) {
|
||||
|
||||
header = undefined;
|
||||
if ( ! isNaN( contentAvailabilityHeaders[ i ].bitstream ) ) {
|
||||
|
||||
header = bufferViewHeaders[ contentAvailabilityHeaders[ i ].bitstream ];
|
||||
|
||||
} else if ( ! isNaN( contentAvailabilityHeaders[ i ].bufferView ) ) {
|
||||
|
||||
header = bufferViewHeaders[ contentAvailabilityHeaders[ i ].bufferView ];
|
||||
|
||||
}
|
||||
if ( header ) {
|
||||
|
||||
header.isActive = true;
|
||||
header.bufferHeader.isActive = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
header = undefined;
|
||||
const childSubtreeAvailabilityHeader = subtreeJson.childSubtreeAvailability;
|
||||
if ( ! isNaN( childSubtreeAvailabilityHeader.bitstream ) ) {
|
||||
|
||||
header = bufferViewHeaders[ childSubtreeAvailabilityHeader.bitstream ];
|
||||
|
||||
} else if ( ! isNaN( childSubtreeAvailabilityHeader.bufferView ) ) {
|
||||
|
||||
header = bufferViewHeaders[ childSubtreeAvailabilityHeader.bufferView ];
|
||||
|
||||
}
|
||||
if ( header ) {
|
||||
|
||||
header.isActive = true;
|
||||
header.bufferHeader.isActive = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the list of buffers and gather all the active ones into
|
||||
* a dictionary.
|
||||
* <p>
|
||||
* The results are put into a dictionary object. The keys are indices of
|
||||
* buffers, and the values are Uint8Arrays of the contents. Only buffers
|
||||
* marked with the isActive flag are fetched.
|
||||
* </p>
|
||||
* <p>
|
||||
* The internal buffer (the subtree's binary chunk) is also stored in this
|
||||
* dictionary if it is marked active.
|
||||
* </p>
|
||||
* @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers
|
||||
* @param {ArrayBuffer} internalBuffer The binary chunk of the subtree file
|
||||
* @returns {object} buffersU8 A dictionary of buffer index to a Uint8Array of its contents.
|
||||
* @private
|
||||
*/
|
||||
async requestActiveBuffers( bufferHeaders, internalBuffer ) {
|
||||
|
||||
const promises = [];
|
||||
for ( let i = 0; i < bufferHeaders.length; i ++ ) {
|
||||
|
||||
const bufferHeader = bufferHeaders[ i ];
|
||||
// If the buffer is not active, resolve with undefined.
|
||||
if ( ! bufferHeader.isActive ) {
|
||||
|
||||
promises.push( Promise.resolve( ) );
|
||||
|
||||
} else if ( bufferHeader.isExternal ) {
|
||||
|
||||
// Get the absolute URI of the external buffer.
|
||||
const url = this.parseImplicitURIBuffer(
|
||||
this.tile,
|
||||
this.rootTile.implicitTiling.subtrees.uri,
|
||||
bufferHeader.uri
|
||||
);
|
||||
|
||||
const fetchPromise = fetch( url, this.fetchOptions )
|
||||
.then( response => {
|
||||
|
||||
if ( ! response.ok ) {
|
||||
|
||||
throw new Error( `SUBTREELoader: Failed to load external buffer from ${ bufferHeader.uri } with error code ${ response.status }.` );
|
||||
|
||||
}
|
||||
return response.arrayBuffer();
|
||||
|
||||
} )
|
||||
.then( arrayBuffer => new Uint8Array( arrayBuffer ) );
|
||||
|
||||
promises.push( fetchPromise );
|
||||
|
||||
} else {
|
||||
|
||||
promises.push( Promise.resolve( new Uint8Array( internalBuffer ) ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
const bufferResults = await Promise.all( promises );
|
||||
const buffersU8 = {};
|
||||
for ( let i = 0; i < bufferResults.length; i ++ ) {
|
||||
|
||||
const result = bufferResults[ i ];
|
||||
if ( result ) {
|
||||
|
||||
buffersU8[ i ] = result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return buffersU8;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the list of buffer views, and if they are marked as active,
|
||||
* extract a subarray from one of the active buffers.
|
||||
*
|
||||
* @param {BufferViewHeader[]} bufferViewHeaders
|
||||
* @param {object} buffersU8 A dictionary of buffer index to a Uint8Array of its contents.
|
||||
* @returns {object} A dictionary of buffer view index to a Uint8Array of its contents.
|
||||
* @private
|
||||
*/
|
||||
parseActiveBufferViews( bufferViewHeaders, buffersU8 ) {
|
||||
|
||||
const bufferViewsU8 = {};
|
||||
for ( let i = 0; i < bufferViewHeaders.length; i ++ ) {
|
||||
|
||||
const bufferViewHeader = bufferViewHeaders[ i ];
|
||||
if ( ! bufferViewHeader.isActive ) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
const start = bufferViewHeader.byteOffset;
|
||||
const end = start + bufferViewHeader.byteLength;
|
||||
const buffer = buffersU8[ bufferViewHeader.buffer ];
|
||||
bufferViewsU8[ i ] = buffer.slice( start, end );
|
||||
|
||||
}
|
||||
return bufferViewsU8;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A buffer header is the JSON header from the subtree JSON chunk plus
|
||||
* a couple extra boolean flags for easy reference.
|
||||
*
|
||||
* Buffers are assumed inactive until explicitly marked active. This is used
|
||||
* to avoid fetching unneeded buffers.
|
||||
*
|
||||
* @typedef {object} BufferHeader
|
||||
* @property {boolean} isActive Whether this buffer is currently used.
|
||||
* @property {string} [uri] The URI of the buffer (external buffers only)
|
||||
* @property {number} byteLength The byte length of the buffer, including any padding contained within.
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* Iterate over the list of buffers from the subtree JSON and add the isActive field for easier parsing later.
|
||||
* This modifies the objects in place.
|
||||
* @param {Object[]} [bufferHeaders=[]] The JSON from subtreeJson.buffers.
|
||||
* @returns {BufferHeader[]} The same array of headers with additional fields.
|
||||
* @private
|
||||
*/
|
||||
preprocessBuffers( bufferHeaders = [] ) {
|
||||
|
||||
for ( let i = 0; i < bufferHeaders.length; i ++ ) {
|
||||
|
||||
const bufferHeader = bufferHeaders[ i ];
|
||||
bufferHeader.isActive = false;
|
||||
bufferHeader.isExternal = !! bufferHeader.uri;
|
||||
|
||||
}
|
||||
return bufferHeaders;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A buffer view header is the JSON header from the subtree JSON chunk plus
|
||||
* the isActive flag and a reference to the header for the underlying buffer.
|
||||
*
|
||||
* @typedef {object} BufferViewHeader
|
||||
* @property {BufferHeader} bufferHeader A reference to the header for the underlying buffer
|
||||
* @property {boolean} isActive Whether this bufferView is currently used.
|
||||
* @property {number} buffer The index of the underlying buffer.
|
||||
* @property {number} byteOffset The start byte of the bufferView within the buffer.
|
||||
* @property {number} byteLength The length of the bufferView. No padding is included in this length.
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* Iterate the list of buffer views from the subtree JSON and add the
|
||||
* isActive flag. Also save a reference to the bufferHeader.
|
||||
*
|
||||
* @param {Object[]} [bufferViewHeaders=[]] The JSON from subtree.bufferViews.
|
||||
* @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers.
|
||||
* @returns {BufferViewHeader[]} The same array of bufferView headers with additional fields.
|
||||
* @private
|
||||
*/
|
||||
preprocessBufferViews( bufferViewHeaders = [], bufferHeaders ) {
|
||||
|
||||
for ( let i = 0; i < bufferViewHeaders.length; i ++ ) {
|
||||
|
||||
const bufferViewHeader = bufferViewHeaders[ i ];
|
||||
bufferViewHeader.bufferHeader = bufferHeaders[ bufferViewHeader.buffer ];
|
||||
bufferViewHeader.isActive = false;
|
||||
// Keep the external flag for potential use in requestActiveBuffers
|
||||
bufferViewHeader.isExternal = bufferViewHeader.bufferHeader.isExternal;
|
||||
|
||||
}
|
||||
return bufferViewHeaders;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the three availability bitstreams and store them in the subtree.
|
||||
*
|
||||
* @param {Subtree} subtree The subtree to modify.
|
||||
* @param {Object} subtreeJson The subtree JSON.
|
||||
* @param {Object} bufferViewsU8 A dictionary of buffer view index to a Uint8Array of its contents.
|
||||
* @private
|
||||
*/
|
||||
parseAvailability( subtree, subtreeJson, bufferViewsU8 ) {
|
||||
|
||||
const branchingFactor = getBoundsDivider( this.rootTile );
|
||||
const subtreeLevels = this.rootTile.implicitTiling.subtreeLevels;
|
||||
const tileAvailabilityBits =
|
||||
( Math.pow( branchingFactor, subtreeLevels ) - 1 ) / ( branchingFactor - 1 );
|
||||
const childSubtreeBits = Math.pow( branchingFactor, subtreeLevels );
|
||||
subtree._tileAvailability = this.parseAvailabilityBitstream(
|
||||
subtreeJson.tileAvailability,
|
||||
bufferViewsU8,
|
||||
tileAvailabilityBits
|
||||
);
|
||||
subtree._contentAvailabilityBitstreams = [];
|
||||
for ( let i = 0; i < subtreeJson.contentAvailabilityHeaders.length; i ++ ) {
|
||||
|
||||
const bitstream = this.parseAvailabilityBitstream(
|
||||
subtreeJson.contentAvailabilityHeaders[ i ],
|
||||
bufferViewsU8,
|
||||
// content availability has the same length as tile availability.
|
||||
tileAvailabilityBits
|
||||
);
|
||||
subtree._contentAvailabilityBitstreams.push( bitstream );
|
||||
|
||||
}
|
||||
subtree._childSubtreeAvailability = this.parseAvailabilityBitstream(
|
||||
subtreeJson.childSubtreeAvailability,
|
||||
bufferViewsU8,
|
||||
childSubtreeBits
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the JSON describing an availability bitstream, turn it into an
|
||||
* in-memory representation using an object. This handles bitstreams from a bufferView.
|
||||
*
|
||||
* @param {Object} availabilityJson A JSON object representing the availability.
|
||||
* @param {Object} bufferViewsU8 A dictionary of buffer view index to its Uint8Array contents.
|
||||
* @param {number} lengthBits The length of the availability bitstream in bits.
|
||||
* @returns {object}
|
||||
* @private
|
||||
*/
|
||||
parseAvailabilityBitstream(
|
||||
availabilityJson,
|
||||
bufferViewsU8,
|
||||
lengthBits,
|
||||
) {
|
||||
|
||||
if ( ! isNaN( availabilityJson.constant ) ) {
|
||||
|
||||
return {
|
||||
constant: Boolean( availabilityJson.constant ),
|
||||
lengthBits: lengthBits,
|
||||
};
|
||||
|
||||
}
|
||||
let bufferView;
|
||||
// Check for bitstream first, which is part of the current schema.
|
||||
// bufferView is the name of the bitstream from an older schema.
|
||||
if ( ! isNaN( availabilityJson.bitstream ) ) {
|
||||
|
||||
bufferView = bufferViewsU8[ availabilityJson.bitstream ];
|
||||
|
||||
} else if ( ! isNaN( availabilityJson.bufferView ) ) {
|
||||
|
||||
bufferView = bufferViewsU8[ availabilityJson.bufferView ];
|
||||
|
||||
}
|
||||
return {
|
||||
bitstream: bufferView,
|
||||
lengthBits: lengthBits
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a single subtree tile. This transcodes the subtree into
|
||||
* a tree of {@link SubtreeTile}. The root of this tree is stored in
|
||||
* the placeholder tile's children array. This method also creates
|
||||
* tiles for the child subtrees to be lazily expanded as needed.
|
||||
*
|
||||
* @param {Object | SubtreeTile} subtreeRoot The first node of the subtree.
|
||||
* @param {Subtree} subtree The parsed subtree.
|
||||
* @private
|
||||
*/
|
||||
expandSubtree( subtreeRoot, subtree ) {
|
||||
|
||||
// TODO If multiple contents were supported then this tile could contain both renderable and un renderable content.
|
||||
const contentTile = SubtreeTile.copy( subtreeRoot );
|
||||
// If the subtree root tile has content, then create a placeholder child with cloned parameters
|
||||
// Todo Multiple contents not handled, keep the first content found
|
||||
for ( let i = 0; subtree && i < subtree._contentAvailabilityBitstreams.length; i ++ ) {
|
||||
|
||||
if ( subtree && this.getBit( subtree._contentAvailabilityBitstreams[ i ], 0 ) ) {
|
||||
|
||||
// Create a child holding the content uri, this child is similar to its parent and doesn't have any children.
|
||||
contentTile.content = { uri: this.parseImplicitURI( subtreeRoot, this.rootTile.content.uri ) };
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
subtreeRoot.children.push( contentTile );
|
||||
// Creating each leaf inside the current subtree.
|
||||
const bottomRow = this.transcodeSubtreeTiles(
|
||||
contentTile,
|
||||
subtree
|
||||
);
|
||||
// For each child subtree, create a tile containing the uri of the next subtree to fetch.
|
||||
const childSubtrees = this.listChildSubtrees( subtree, bottomRow );
|
||||
for ( let i = 0; i < childSubtrees.length; i ++ ) {
|
||||
|
||||
const subtreeLocator = childSubtrees[ i ];
|
||||
const leafTile = subtreeLocator.tile;
|
||||
const subtreeTile = this.deriveChildTile(
|
||||
null,
|
||||
leafTile,
|
||||
null,
|
||||
subtreeLocator.childMortonIndex
|
||||
);
|
||||
// Assign subtree uri as content.
|
||||
subtreeTile.content = { uri: this.parseImplicitURI( subtreeTile, this.rootTile.implicitTiling.subtrees.uri ) };
|
||||
leafTile.children.push( subtreeTile );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcode the implicitly defined tiles within this subtree and generate
|
||||
* explicit {@link SubtreeTile} objects. This function only transcodes tiles,
|
||||
* child subtrees are handled separately.
|
||||
*
|
||||
* @param {Object | SubtreeTile} subtreeRoot The root of the current subtree.
|
||||
* @param {Subtree} subtree The subtree to get availability information.
|
||||
* @returns {Array} The bottom row of transcoded tiles. This is helpful for processing child subtrees.
|
||||
* @private
|
||||
*/
|
||||
transcodeSubtreeTiles( subtreeRoot, subtree ) {
|
||||
|
||||
// Sliding window over the levels of the tree.
|
||||
// Each row is branchingFactor * length of previous row.
|
||||
// Tiles within a row are ordered by Morton index.
|
||||
let parentRow = [ subtreeRoot ];
|
||||
let currentRow = [];
|
||||
for ( let level = 1; level < this.rootTile.implicitTiling.subtreeLevels; level ++ ) {
|
||||
|
||||
const branchingFactor = getBoundsDivider( this.rootTile );
|
||||
const levelOffset = ( Math.pow( branchingFactor, level ) - 1 ) / ( branchingFactor - 1 );
|
||||
const numberOfChildren = branchingFactor * parentRow.length;
|
||||
for ( let childMortonIndex = 0; childMortonIndex < numberOfChildren; childMortonIndex ++ ) {
|
||||
|
||||
const childBitIndex = levelOffset + childMortonIndex;
|
||||
const parentMortonIndex = childMortonIndex >> Math.log2( branchingFactor );
|
||||
const parentTile = parentRow[ parentMortonIndex ];
|
||||
// Check if tile is available.
|
||||
if ( ! this.getBit( subtree._tileAvailability, childBitIndex ) ) {
|
||||
|
||||
currentRow.push( undefined );
|
||||
continue;
|
||||
|
||||
}
|
||||
// Create a tile and add it as a child.
|
||||
const childTile = this.deriveChildTile(
|
||||
subtree,
|
||||
parentTile,
|
||||
childBitIndex,
|
||||
childMortonIndex
|
||||
);
|
||||
parentTile.children.push( childTile );
|
||||
currentRow.push( childTile );
|
||||
|
||||
}
|
||||
parentRow = currentRow;
|
||||
currentRow = [];
|
||||
|
||||
}
|
||||
return parentRow;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a parent tile and information about which child to create, derive
|
||||
* the properties of the child tile implicitly.
|
||||
* <p>
|
||||
* This creates a real tile for rendering.
|
||||
* </p>
|
||||
*
|
||||
* @param {Subtree} subtree The subtree the child tile belongs to.
|
||||
* @param {Object | SubtreeTile} parentTile The parent of the new child tile.
|
||||
* @param {number} childBitIndex The index of the child tile within the tile's availability information.
|
||||
* @param {number} childMortonIndex The morton index of the child tile relative to its parent.
|
||||
* @returns {SubtreeTile} The new child tile.
|
||||
* @private
|
||||
*/
|
||||
deriveChildTile(
|
||||
subtree,
|
||||
parentTile,
|
||||
childBitIndex,
|
||||
childMortonIndex
|
||||
) {
|
||||
|
||||
const subtreeTile = new SubtreeTile( parentTile, childMortonIndex );
|
||||
subtreeTile.boundingVolume = this.getTileBoundingVolume( subtreeTile );
|
||||
subtreeTile.geometricError = this.getGeometricError( subtreeTile );
|
||||
// Todo Multiple contents not handled, keep the first found content.
|
||||
for ( let i = 0; subtree && i < subtree._contentAvailabilityBitstreams.length; i ++ ) {
|
||||
|
||||
if ( subtree && this.getBit( subtree._contentAvailabilityBitstreams[ i ], childBitIndex ) ) {
|
||||
|
||||
subtreeTile.content = { uri: this.parseImplicitURI( subtreeTile, this.rootTile.content.uri ) };
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return subtreeTile;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bit from the bitstream as a Boolean. If the bitstream
|
||||
* is a constant, the constant value is returned instead.
|
||||
*
|
||||
* @param {ParsedBitstream} object
|
||||
* @param {number} index The integer index of the bit.
|
||||
* @returns {boolean} The value of the bit.
|
||||
* @private
|
||||
*/
|
||||
getBit( object, index ) {
|
||||
|
||||
if ( index < 0 || index >= object.lengthBits ) {
|
||||
|
||||
throw new Error( 'Bit index out of bounds.' );
|
||||
|
||||
}
|
||||
if ( object.constant !== undefined ) {
|
||||
|
||||
return object.constant;
|
||||
|
||||
}
|
||||
// byteIndex is floor(index / 8)
|
||||
const byteIndex = index >> 3;
|
||||
const bitIndex = index % 8;
|
||||
return ( ( new Uint8Array( object.bitstream )[ byteIndex ] >> bitIndex ) & 1 ) === 1;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* //TODO Adapt for Sphere
|
||||
* To maintain numerical stability during this subdivision process,
|
||||
* the actual bounding volumes should not be computed progressively by subdividing a non-root tile volume.
|
||||
* Instead, the exact bounding volumes are computed directly for a given level.
|
||||
* @param {Object | SubtreeTile} tile
|
||||
* @return {Object} object containing the bounding volume.
|
||||
*/
|
||||
getTileBoundingVolume( tile ) {
|
||||
|
||||
const boundingVolume = {};
|
||||
if ( this.rootTile.boundingVolume.region ) {
|
||||
|
||||
const region = [ ...this.rootTile.boundingVolume.region ];
|
||||
const minX = region[ 0 ];
|
||||
const maxX = region[ 2 ];
|
||||
const minY = region[ 1 ];
|
||||
const maxY = region[ 3 ];
|
||||
const sizeX = ( maxX - minX ) / Math.pow( 2, tile.__level );
|
||||
const sizeY = ( maxY - minY ) / Math.pow( 2, tile.__level );
|
||||
region[ 0 ] = minX + sizeX * tile.__x; //west
|
||||
region[ 2 ] = minX + sizeX * ( tile.__x + 1 ); //east
|
||||
region[ 1 ] = minY + sizeY * tile.__y; //south
|
||||
region[ 3 ] = minY + sizeY * ( tile.__y + 1 ); //north
|
||||
for ( let k = 0; k < 4; k ++ ) {
|
||||
|
||||
const coord = region[ k ];
|
||||
if ( coord < - Math.PI ) {
|
||||
|
||||
region[ k ] += 2 * Math.PI;
|
||||
|
||||
} else if ( coord > Math.PI ) {
|
||||
|
||||
region[ k ] -= 2 * Math.PI;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
//Also divide the height in the case of octree.
|
||||
if ( isOctreeSubdivision( tile ) ) {
|
||||
|
||||
const minZ = region[ 4 ];
|
||||
const maxZ = region[ 5 ];
|
||||
const sizeZ = ( maxZ - minZ ) / Math.pow( 2, tile.__level );
|
||||
region[ 4 ] = minZ + sizeZ * tile.__z; //minimum height
|
||||
region[ 5 ] = minZ + sizeZ * ( tile.__z + 1 ); //maximum height
|
||||
|
||||
}
|
||||
boundingVolume.region = region;
|
||||
|
||||
}
|
||||
if ( this.rootTile.boundingVolume.box ) {
|
||||
|
||||
// 0-2: center of the box
|
||||
// 3-5: x axis direction and half length
|
||||
// 6-8: y axis direction and half length
|
||||
// 9-11: z axis direction and half length
|
||||
const box = [ ...this.rootTile.boundingVolume.box ];
|
||||
const cellSteps = 2 ** tile.__level - 1;
|
||||
const scale = Math.pow( 2, - tile.__level );
|
||||
const axisNumber = isOctreeSubdivision( tile ) ? 3 : 2;
|
||||
for ( let i = 0; i < axisNumber; i ++ ) {
|
||||
|
||||
// scale the bounds axes
|
||||
box[ 3 + i * 3 + 0 ] *= scale;
|
||||
box[ 3 + i * 3 + 1 ] *= scale;
|
||||
box[ 3 + i * 3 + 2 ] *= scale;
|
||||
// axis vector
|
||||
const x = box[ 3 + i * 3 + 0 ];
|
||||
const y = box[ 3 + i * 3 + 1 ];
|
||||
const z = box[ 3 + i * 3 + 2 ];
|
||||
// adjust the center by the x, y and z axes
|
||||
const axisOffset = i === 0 ? tile.__x : ( i === 1 ? tile.__y : tile.__z );
|
||||
box[ 0 ] += 2 * x * ( - 0.5 * cellSteps + axisOffset );
|
||||
box[ 1 ] += 2 * y * ( - 0.5 * cellSteps + axisOffset );
|
||||
box[ 2 ] += 2 * z * ( - 0.5 * cellSteps + axisOffset );
|
||||
|
||||
}
|
||||
boundingVolume.box = box;
|
||||
|
||||
}
|
||||
return boundingVolume;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Each child’s geometricError is half of its parent’s geometricError.
|
||||
* @param {Object | SubtreeTile} tile
|
||||
* @return {number}
|
||||
*/
|
||||
getGeometricError( tile ) {
|
||||
|
||||
return this.rootTile.geometricError / Math.pow( 2, tile.__level );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what child subtrees exist and return a list of information.
|
||||
*
|
||||
* @param {Object} subtree The subtree for looking up availability.
|
||||
* @param {Array} bottomRow The bottom row of tiles in a transcoded subtree.
|
||||
* @returns {[]} A list of identifiers for the child subtrees.
|
||||
* @private
|
||||
*/
|
||||
listChildSubtrees( subtree, bottomRow ) {
|
||||
|
||||
const results = [];
|
||||
const branchingFactor = getBoundsDivider( this.rootTile );
|
||||
for ( let i = 0; i < bottomRow.length; i ++ ) {
|
||||
|
||||
const leafTile = bottomRow[ i ];
|
||||
if ( leafTile === undefined ) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
for ( let j = 0; j < branchingFactor; j ++ ) {
|
||||
|
||||
const index = i * branchingFactor + j;
|
||||
if ( this.getBit( subtree._childSubtreeAvailability, index ) ) {
|
||||
|
||||
results.push( {
|
||||
tile: leafTile,
|
||||
childMortonIndex: index
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return results;
|
||||
|
||||
}
|
||||
/**
|
||||
* Replaces placeholder tokens in a URI template with the corresponding tile properties.
|
||||
*
|
||||
* The URI template should contain the tokens:
|
||||
* - `{level}` for the tile's subdivision level.
|
||||
* - `{x}` for the tile's x-coordinate.
|
||||
* - `{y}` for the tile's y-coordinate.
|
||||
* - `{z}` for the tile's z-coordinate.
|
||||
*
|
||||
* @param {Object} tile - The tile object containing properties __level, __x, __y, and __z.
|
||||
* @param {string} uri - The URI template string with placeholders.
|
||||
* @returns {string} The URI with placeholders replaced by the tile's properties.
|
||||
*/
|
||||
parseImplicitURI( tile, uri ) {
|
||||
|
||||
uri = uri.replace( '{level}', tile.__level );
|
||||
uri = uri.replace( '{x}', tile.__x );
|
||||
uri = uri.replace( '{y}', tile.__y );
|
||||
uri = uri.replace( '{z}', tile.__z );
|
||||
return uri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the full external buffer URI for a tile by combining an implicit URI with a buffer URI.
|
||||
*
|
||||
* First, it parses the implicit URI using the tile properties and the provided template. Then, it creates a new URL
|
||||
* relative to the tile's base path, removes the last path segment, and appends the buffer URI.
|
||||
*
|
||||
* @param {Object} tile - The tile object that contains properties:
|
||||
* - __level: the subdivision level,
|
||||
* - __x, __y, __z: the tile coordinates,
|
||||
* @param {string} uri - The URI template string with placeholders for the tile (e.g., `{level}`, `{x}`, `{y}`, `{z}`).
|
||||
* @param {string} bufUri - The buffer file name to append (e.g., "0_1.bin").
|
||||
* @returns {string} The full external buffer URI.
|
||||
*/
|
||||
parseImplicitURIBuffer( tile, uri, bufUri ) {
|
||||
|
||||
// Generate the base tile URI by replacing placeholders
|
||||
const subUri = this.parseImplicitURI( tile, uri );
|
||||
|
||||
// Create a URL object relative to the tile's base path
|
||||
const url = new URL( subUri, this.workingPath + '/' );
|
||||
|
||||
// Remove the last path segment
|
||||
url.pathname = url.pathname.substring( 0, url.pathname.lastIndexOf( '/' ) );
|
||||
|
||||
// Construct the final URL with the buffer URI appended
|
||||
return new URL( url.pathname + '/' + bufUri, this.workingPath + '/' ).toString();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Class for making fetches to Cesium Ion, refreshing the token if needed.
|
||||
export class CesiumIonAuth {
|
||||
|
||||
constructor( options = {} ) {
|
||||
|
||||
const { apiToken, autoRefreshToken = false } = options;
|
||||
this.apiToken = apiToken;
|
||||
this.autoRefreshToken = autoRefreshToken;
|
||||
this.authURL = null;
|
||||
this._tokenRefreshPromise = null;
|
||||
this._bearerToken = null;
|
||||
|
||||
}
|
||||
|
||||
async fetch( url, options ) {
|
||||
|
||||
await this._tokenRefreshPromise;
|
||||
|
||||
// insert the authorization token
|
||||
const fetchOptions = { ...options };
|
||||
fetchOptions.headers = fetchOptions.headers || {};
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
Authorization: this._bearerToken,
|
||||
};
|
||||
|
||||
// try to refresh the token if we failed to load the tile data
|
||||
const res = await fetch( url, fetchOptions );
|
||||
if ( res.status >= 400 && res.status <= 499 && this.autoRefreshToken ) {
|
||||
|
||||
// refresh the bearer token
|
||||
await this.refreshToken( options );
|
||||
fetchOptions.headers.Authorization = this._bearerToken;
|
||||
|
||||
return fetch( url, fetchOptions );
|
||||
|
||||
} else {
|
||||
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refreshToken( options ) {
|
||||
|
||||
if ( this._tokenRefreshPromise === null ) {
|
||||
|
||||
// construct the url to fetch the endpoint
|
||||
const url = new URL( this.authURL );
|
||||
url.searchParams.set( 'access_token', this.apiToken );
|
||||
|
||||
this._tokenRefreshPromise = fetch( url, options )
|
||||
.then( res => {
|
||||
|
||||
if ( ! res.ok ) {
|
||||
|
||||
throw new Error( `CesiumIonAuthPlugin: Failed to load data with error code ${ res.status }` );
|
||||
|
||||
}
|
||||
|
||||
return res.json();
|
||||
|
||||
} )
|
||||
.then( json => {
|
||||
|
||||
this._bearerToken = `Bearer ${ json.accessToken }`;
|
||||
this._tokenRefreshPromise = null;
|
||||
|
||||
return json;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
return this._tokenRefreshPromise;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { TraversalUtils } from '3d-tiles-renderer/core';
|
||||
|
||||
const TILES_MAP_URL = 'https://tile.googleapis.com/v1/createSession';
|
||||
|
||||
// Class for making fetches to Google Cloud, refreshing the token if needed.
|
||||
// Supports both the 2d map tiles API in addition to 3d tiles.
|
||||
export class GoogleCloudAuth {
|
||||
|
||||
get isMapTilesSession() {
|
||||
|
||||
return this.authURL === TILES_MAP_URL;
|
||||
|
||||
}
|
||||
|
||||
constructor( options = {} ) {
|
||||
|
||||
const { apiToken, sessionOptions = null, autoRefreshToken = false } = options;
|
||||
this.apiToken = apiToken;
|
||||
this.autoRefreshToken = autoRefreshToken;
|
||||
this.authURL = TILES_MAP_URL;
|
||||
this.sessionToken = null;
|
||||
this.sessionOptions = sessionOptions;
|
||||
this._tokenRefreshPromise = null;
|
||||
|
||||
}
|
||||
|
||||
async fetch( url, options ) {
|
||||
|
||||
// if we're using a map tiles session then we have to refresh the token separately
|
||||
if ( this.sessionToken === null && this.isMapTilesSession ) {
|
||||
|
||||
this.refreshToken( options );
|
||||
|
||||
}
|
||||
|
||||
await this._tokenRefreshPromise;
|
||||
|
||||
// construct the url
|
||||
const fetchUrl = new URL( url );
|
||||
fetchUrl.searchParams.set( 'key', this.apiToken );
|
||||
if ( this.sessionToken ) {
|
||||
|
||||
fetchUrl.searchParams.set( 'session', this.sessionToken );
|
||||
|
||||
}
|
||||
|
||||
// try to refresh the session token if we failed to load it
|
||||
let res = await fetch( fetchUrl, options );
|
||||
if ( res.status >= 400 && res.status <= 499 && this.autoRefreshToken ) {
|
||||
|
||||
// refresh the session token
|
||||
await this.refreshToken( options );
|
||||
if ( this.sessionToken ) {
|
||||
|
||||
fetchUrl.searchParams.set( 'session', this.sessionToken );
|
||||
|
||||
}
|
||||
|
||||
res = await fetch( fetchUrl, options );
|
||||
|
||||
}
|
||||
|
||||
if ( this.sessionToken === null && ! this.isMapTilesSession ) {
|
||||
|
||||
// if we're using a 3d tiles session then we get the session key in the first request
|
||||
return res
|
||||
.json()
|
||||
.then( json => {
|
||||
|
||||
this.sessionToken = getSessionToken( json );
|
||||
return json;
|
||||
|
||||
} );
|
||||
|
||||
} else {
|
||||
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refreshToken( options ) {
|
||||
|
||||
if ( this._tokenRefreshPromise === null ) {
|
||||
|
||||
// construct the url to fetch the endpoint
|
||||
const url = new URL( this.authURL );
|
||||
url.searchParams.set( 'key', this.apiToken );
|
||||
|
||||
// initialize options for map tiles
|
||||
const fetchOptions = { ...options };
|
||||
if ( this.isMapTilesSession ) {
|
||||
|
||||
fetchOptions.method = 'POST';
|
||||
fetchOptions.body = JSON.stringify( this.sessionOptions );
|
||||
fetchOptions.headers = fetchOptions.headers || {};
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
this._tokenRefreshPromise = fetch( url, fetchOptions )
|
||||
.then( res => {
|
||||
|
||||
if ( ! res.ok ) {
|
||||
|
||||
throw new Error( `GoogleCloudAuth: Failed to load data with error code ${ res.status }` );
|
||||
|
||||
}
|
||||
|
||||
return res.json();
|
||||
|
||||
} )
|
||||
.then( json => {
|
||||
|
||||
this.sessionToken = getSessionToken( json );
|
||||
this._tokenRefreshPromise = null;
|
||||
|
||||
return json;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
return this._tokenRefreshPromise;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Takes a json response from the auth url and extracts the session token
|
||||
function getSessionToken( json ) {
|
||||
|
||||
if ( 'session' in json ) {
|
||||
|
||||
// if using the 2d maps api
|
||||
return json.session;
|
||||
|
||||
} else {
|
||||
|
||||
// is using the 3d tiles api
|
||||
let sessionToken = null;
|
||||
const root = json.root;
|
||||
TraversalUtils.traverseSet( root, tile => {
|
||||
|
||||
if ( tile.content && tile.content.uri ) {
|
||||
|
||||
const [ , params ] = tile.content.uri.split( '?' );
|
||||
sessionToken = new URLSearchParams( params ).get( 'session' );
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} );
|
||||
|
||||
return sessionToken;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ImplicitTilingPlugin.js';
|
||||
export * from './EnforceNonZeroErrorPlugin.js';
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './ImplicitTilingPlugin.js';
|
||||
export * from './EnforceNonZeroErrorPlugin.js';
|
||||
export * from './auth/GoogleCloudAuth.js';
|
||||
export * from './auth/CesiumIonAuth.js';
|
||||
export * from './loaders/QuantizedMeshLoaderBase.js';
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import { LoaderBase } from '3d-tiles-renderer/core';
|
||||
|
||||
export function zigZagDecode( value ) {
|
||||
|
||||
return ( value >> 1 ) ^ ( - ( value & 1 ) );
|
||||
|
||||
}
|
||||
|
||||
export class QuantizedMeshLoaderBase extends LoaderBase {
|
||||
|
||||
constructor( ...args ) {
|
||||
|
||||
super( ...args );
|
||||
|
||||
this.fetchOptions.header = {
|
||||
Accept: 'application/vnd.quantized-mesh,application/octet-stream;q=0.9',
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
loadAsync( ...args ) {
|
||||
|
||||
const { fetchOptions } = this;
|
||||
fetchOptions.header = fetchOptions.header || {};
|
||||
fetchOptions.header[ 'Accept' ] = 'application/vnd.quantized-mesh,application/octet-stream;q=0.9';
|
||||
fetchOptions.header[ 'Accept' ] += ';extensions=octvertexnormals-watermask-metadata';
|
||||
|
||||
return super.loadAsync( ...args );
|
||||
|
||||
}
|
||||
|
||||
parse( buffer ) {
|
||||
|
||||
let pointer = 0;
|
||||
const view = new DataView( buffer );
|
||||
const readFloat64 = () => {
|
||||
|
||||
const result = view.getFloat64( pointer, true );
|
||||
pointer += 8;
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
const readFloat32 = () => {
|
||||
|
||||
const result = view.getFloat32( pointer, true );
|
||||
pointer += 4;
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
const readInt = () => {
|
||||
|
||||
const result = view.getUint32( pointer, true );
|
||||
pointer += 4;
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
const readByte = () => {
|
||||
|
||||
const result = view.getUint8( pointer );
|
||||
pointer += 1;
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
const readBuffer = ( count, type ) => {
|
||||
|
||||
const result = new type( buffer, pointer, count );
|
||||
pointer += count * type.BYTES_PER_ELEMENT;
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
// extract header
|
||||
const header = {
|
||||
center: [ readFloat64(), readFloat64(), readFloat64() ],
|
||||
minHeight: readFloat32(),
|
||||
maxHeight: readFloat32(),
|
||||
sphereCenter: [ readFloat64(), readFloat64(), readFloat64() ],
|
||||
sphereRadius: readFloat64(),
|
||||
horizonOcclusionPoint: [ readFloat64(), readFloat64(), readFloat64() ],
|
||||
};
|
||||
|
||||
// extract vertex data
|
||||
const vertexCount = readInt();
|
||||
const uBuffer = readBuffer( vertexCount, Uint16Array );
|
||||
const vBuffer = readBuffer( vertexCount, Uint16Array );
|
||||
const hBuffer = readBuffer( vertexCount, Uint16Array );
|
||||
|
||||
const uResult = new Float32Array( vertexCount );
|
||||
const vResult = new Float32Array( vertexCount );
|
||||
const hResult = new Float32Array( vertexCount );
|
||||
|
||||
// decode vertex data
|
||||
let u = 0;
|
||||
let v = 0;
|
||||
let h = 0;
|
||||
const MAX_VALUE = 32767;
|
||||
for ( let i = 0; i < vertexCount; ++ i ) {
|
||||
|
||||
u += zigZagDecode( uBuffer[ i ] );
|
||||
v += zigZagDecode( vBuffer[ i ] );
|
||||
h += zigZagDecode( hBuffer[ i ] );
|
||||
|
||||
uResult[ i ] = u / MAX_VALUE;
|
||||
vResult[ i ] = v / MAX_VALUE;
|
||||
hResult[ i ] = h / MAX_VALUE;
|
||||
|
||||
}
|
||||
|
||||
// align pointer for index data
|
||||
const is32 = vertexCount > 65536;
|
||||
const bufferType = is32 ? Uint32Array : Uint16Array;
|
||||
if ( is32 ) {
|
||||
|
||||
pointer = Math.ceil( pointer / 4 ) * 4;
|
||||
|
||||
} else {
|
||||
|
||||
pointer = Math.ceil( pointer / 2 ) * 2;
|
||||
|
||||
}
|
||||
|
||||
// extract index data
|
||||
const triangleCount = readInt();
|
||||
const indices = readBuffer( triangleCount * 3, bufferType );
|
||||
|
||||
// decode the index data
|
||||
let highest = 0;
|
||||
for ( var i = 0; i < indices.length; ++ i ) {
|
||||
|
||||
const code = indices[ i ];
|
||||
indices[ i ] = highest - code;
|
||||
if ( code === 0 ) {
|
||||
|
||||
++ highest;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sort functions for the edges since they are not pre-sorted
|
||||
const vSort = ( a, b ) => vResult[ b ] - vResult[ a ];
|
||||
const vSortReverse = ( a, b ) => - vSort( a, b );
|
||||
|
||||
const uSort = ( a, b ) => uResult[ a ] - uResult[ b ];
|
||||
const uSortReverse = ( a, b ) => - uSort( a, b );
|
||||
|
||||
// get edge indices
|
||||
const westVertexCount = readInt();
|
||||
const westIndices = readBuffer( westVertexCount, bufferType );
|
||||
westIndices.sort( vSort );
|
||||
|
||||
const southVertexCount = readInt();
|
||||
const southIndices = readBuffer( southVertexCount, bufferType );
|
||||
southIndices.sort( uSort );
|
||||
|
||||
const eastVertexCount = readInt();
|
||||
const eastIndices = readBuffer( eastVertexCount, bufferType );
|
||||
eastIndices.sort( vSortReverse );
|
||||
|
||||
const northVertexCount = readInt();
|
||||
const northIndices = readBuffer( northVertexCount, bufferType );
|
||||
northIndices.sort( uSortReverse );
|
||||
|
||||
const edgeIndices = {
|
||||
westIndices,
|
||||
southIndices,
|
||||
eastIndices,
|
||||
northIndices,
|
||||
};
|
||||
|
||||
// parse extensions
|
||||
const extensions = {};
|
||||
while ( pointer < view.byteLength ) {
|
||||
|
||||
const extensionId = readByte();
|
||||
const extensionLength = readInt();
|
||||
|
||||
if ( extensionId === 1 ) {
|
||||
|
||||
// oct encoded normals
|
||||
const xy = readBuffer( vertexCount * 2, Uint8Array );
|
||||
const normals = new Float32Array( vertexCount * 3 );
|
||||
|
||||
// https://github.com/CesiumGS/cesium/blob/baaabaa49058067c855ad050be73a9cdfe9b6ac7/packages/engine/Source/Core/AttributeCompression.js#L119-L140
|
||||
for ( let i = 0; i < vertexCount; i ++ ) {
|
||||
|
||||
let x = ( xy[ 2 * i + 0 ] / 255 ) * 2 - 1;
|
||||
let y = ( xy[ 2 * i + 1 ] / 255 ) * 2 - 1;
|
||||
const z = 1.0 - ( Math.abs( x ) + Math.abs( y ) );
|
||||
|
||||
if ( z < 0.0 ) {
|
||||
|
||||
const oldVX = x;
|
||||
x = ( 1.0 - Math.abs( y ) ) * signNotZero( oldVX );
|
||||
y = ( 1.0 - Math.abs( oldVX ) ) * signNotZero( y );
|
||||
|
||||
}
|
||||
|
||||
const len = Math.sqrt( x * x + y * y + z * z );
|
||||
normals[ 3 * i + 0 ] = x / len;
|
||||
normals[ 3 * i + 1 ] = y / len;
|
||||
normals[ 3 * i + 2 ] = z / len;
|
||||
|
||||
}
|
||||
|
||||
extensions[ 'octvertexnormals' ] = {
|
||||
extensionId,
|
||||
normals,
|
||||
};
|
||||
|
||||
} else if ( extensionId === 2 ) {
|
||||
|
||||
// water mask
|
||||
const size = extensionLength === 1 ? 1 : 256;
|
||||
const mask = readBuffer( size * size, Uint8Array );
|
||||
extensions[ 'watermask' ] = {
|
||||
extensionId,
|
||||
mask,
|
||||
size,
|
||||
};
|
||||
|
||||
} else if ( extensionId === 4 ) {
|
||||
|
||||
// metadata
|
||||
const jsonLength = readInt();
|
||||
const jsonBuffer = readBuffer( jsonLength, Uint8Array );
|
||||
const json = new TextDecoder().decode( jsonBuffer );
|
||||
extensions[ 'metadata' ] = {
|
||||
extensionId,
|
||||
json: JSON.parse( json ),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
header,
|
||||
indices,
|
||||
vertexData: {
|
||||
u: uResult,
|
||||
v: vResult,
|
||||
height: hResult,
|
||||
},
|
||||
edgeIndices,
|
||||
extensions,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function signNotZero( v ) {
|
||||
|
||||
return v < 0.0 ? - 1.0 : 1.0;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const WGS84_RADIUS: number;
|
||||
export const WGS84_FLATTENING: number;
|
||||
export const WGS84_HEIGHT: number;
|
||||
@@ -0,0 +1,12 @@
|
||||
// FAILED is negative so lru cache priority sorting will unload it first
|
||||
export const FAILED = - 1;
|
||||
export const UNLOADED = 0;
|
||||
export const LOADING = 1;
|
||||
export const PARSING = 2;
|
||||
export const LOADED = 3;
|
||||
|
||||
// https://en.wikipedia.org/wiki/World_Geodetic_System
|
||||
// https://en.wikipedia.org/wiki/Flattening
|
||||
export const WGS84_RADIUS = 6378137;
|
||||
export const WGS84_FLATTENING = 1 / 298.257223563;
|
||||
export const WGS84_HEIGHT = - ( WGS84_FLATTENING * WGS84_RADIUS - WGS84_RADIUS );
|
||||
@@ -0,0 +1,16 @@
|
||||
// common
|
||||
export { TilesRendererBase } from './tiles/TilesRendererBase.js';
|
||||
export { Tile } from './tiles/Tile.js';
|
||||
export { TileBase } from './tiles/TileBase.js';
|
||||
export { Tileset } from './tiles/Tileset.js';
|
||||
export * from './loaders/B3DMLoaderBase.js';
|
||||
export * from './loaders/I3DMLoaderBase.js';
|
||||
export * from './loaders/PNTSLoaderBase.js';
|
||||
export * from './loaders/CMPTLoaderBase.js';
|
||||
export * from './loaders/LoaderBase.js';
|
||||
export * from './constants.js';
|
||||
|
||||
export { LRUCache } from './utilities/LRUCache.js';
|
||||
export { PriorityQueue } from './utilities/PriorityQueue.js';
|
||||
export { BatchTable } from './utilities/BatchTable.js';
|
||||
export { FeatureTable } from './utilities/FeatureTable.js';
|
||||
@@ -0,0 +1,13 @@
|
||||
// common
|
||||
export { TilesRendererBase } from './tiles/TilesRendererBase.js';
|
||||
export { LoaderBase } from './loaders/LoaderBase.js';
|
||||
export * from './loaders/B3DMLoaderBase.js';
|
||||
export * from './loaders/I3DMLoaderBase.js';
|
||||
export * from './loaders/PNTSLoaderBase.js';
|
||||
export * from './loaders/CMPTLoaderBase.js';
|
||||
export * from './constants.js';
|
||||
|
||||
export { LRUCache } from './utilities/LRUCache.js';
|
||||
export { PriorityQueue } from './utilities/PriorityQueue.js';
|
||||
export * as TraversalUtils from './utilities/TraversalUtils.js';
|
||||
export * as LoaderUtils from './utilities/LoaderUtils.js';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { BatchTable } from '../utilities/BatchTable.js';
|
||||
import { FeatureTable } from '../utilities/FeatureTable.js';
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
|
||||
export interface B3DMBaseResult {
|
||||
|
||||
version : string;
|
||||
featureTable: FeatureTable;
|
||||
batchTable : BatchTable;
|
||||
glbBytes : Uint8Array;
|
||||
|
||||
}
|
||||
|
||||
export class B3DMLoaderBase<Result = B3DMBaseResult, ParseResult = Result>
|
||||
extends LoaderBase<Result, ParseResult> {
|
||||
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// B3DM File Format
|
||||
// https://github.com/CesiumGS/3d-tiles/blob/master/specification/TileFormats/Batched3DModel/README.md
|
||||
|
||||
import { BatchTable } from '../utilities/BatchTable.js';
|
||||
import { FeatureTable } from '../utilities/FeatureTable.js';
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
import { readMagicBytes } from '../utilities/LoaderUtils.js';
|
||||
|
||||
export class B3DMLoaderBase extends LoaderBase {
|
||||
|
||||
parse( buffer ) {
|
||||
|
||||
// TODO: this should be able to take a uint8array with an offset and length
|
||||
const dataView = new DataView( buffer );
|
||||
|
||||
// 28-byte header
|
||||
|
||||
// 4 bytes
|
||||
const magic = readMagicBytes( dataView );
|
||||
|
||||
console.assert( magic === 'b3dm' );
|
||||
|
||||
// 4 bytes
|
||||
const version = dataView.getUint32( 4, true );
|
||||
|
||||
console.assert( version === 1 );
|
||||
|
||||
// 4 bytes
|
||||
const byteLength = dataView.getUint32( 8, true );
|
||||
|
||||
console.assert( byteLength === buffer.byteLength );
|
||||
|
||||
// 4 bytes
|
||||
const featureTableJSONByteLength = dataView.getUint32( 12, true );
|
||||
|
||||
// 4 bytes
|
||||
const featureTableBinaryByteLength = dataView.getUint32( 16, true );
|
||||
|
||||
// 4 bytes
|
||||
const batchTableJSONByteLength = dataView.getUint32( 20, true );
|
||||
|
||||
// 4 bytes
|
||||
const batchTableBinaryByteLength = dataView.getUint32( 24, true );
|
||||
|
||||
// Feature Table
|
||||
const featureTableStart = 28;
|
||||
const featureTableBuffer = buffer.slice(
|
||||
featureTableStart,
|
||||
featureTableStart + featureTableJSONByteLength + featureTableBinaryByteLength,
|
||||
);
|
||||
const featureTable = new FeatureTable(
|
||||
featureTableBuffer,
|
||||
0,
|
||||
featureTableJSONByteLength,
|
||||
featureTableBinaryByteLength,
|
||||
);
|
||||
|
||||
// Batch Table
|
||||
const batchTableStart = featureTableStart + featureTableJSONByteLength + featureTableBinaryByteLength;
|
||||
const batchTableBuffer = buffer.slice(
|
||||
batchTableStart,
|
||||
batchTableStart + batchTableJSONByteLength + batchTableBinaryByteLength,
|
||||
);
|
||||
const batchTable = new BatchTable(
|
||||
batchTableBuffer,
|
||||
featureTable.getData( 'BATCH_LENGTH' ),
|
||||
0,
|
||||
batchTableJSONByteLength,
|
||||
batchTableBinaryByteLength,
|
||||
);
|
||||
|
||||
const glbStart = batchTableStart + batchTableJSONByteLength + batchTableBinaryByteLength;
|
||||
const glbBytes = new Uint8Array( buffer, glbStart, byteLength - glbStart );
|
||||
|
||||
return {
|
||||
version,
|
||||
featureTable,
|
||||
batchTable,
|
||||
glbBytes,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
|
||||
interface TileInfo {
|
||||
|
||||
type : string;
|
||||
buffer : Uint8Array;
|
||||
version : string;
|
||||
|
||||
}
|
||||
|
||||
export interface CMPTBaseResult {
|
||||
|
||||
version : string;
|
||||
tiles : Array< TileInfo >;
|
||||
|
||||
}
|
||||
|
||||
export class CMPTLoaderBase<Result = CMPTBaseResult, ParseResult = Result>
|
||||
extends LoaderBase<Result, ParseResult> {
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// CMPT File Format
|
||||
// https://github.com/CesiumGS/3d-tiles/blob/master/specification/TileFormats/Composite/README.md
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
import { readMagicBytes } from '../utilities/LoaderUtils.js';
|
||||
|
||||
export class CMPTLoaderBase extends LoaderBase {
|
||||
|
||||
parse( buffer ) {
|
||||
|
||||
const dataView = new DataView( buffer );
|
||||
|
||||
// 16-byte header
|
||||
|
||||
// 4 bytes
|
||||
const magic = readMagicBytes( dataView );
|
||||
|
||||
console.assert( magic === 'cmpt', 'CMPTLoader: The magic bytes equal "cmpt".' );
|
||||
|
||||
// 4 bytes
|
||||
const version = dataView.getUint32( 4, true );
|
||||
|
||||
console.assert( version === 1, 'CMPTLoader: The version listed in the header is "1".' );
|
||||
|
||||
// 4 bytes
|
||||
const byteLength = dataView.getUint32( 8, true );
|
||||
|
||||
console.assert( byteLength === buffer.byteLength, 'CMPTLoader: The contents buffer length listed in the header matches the file.' );
|
||||
|
||||
// 4 bytes
|
||||
const tilesLength = dataView.getUint32( 12, true );
|
||||
|
||||
const tiles = [];
|
||||
let offset = 16;
|
||||
for ( let i = 0; i < tilesLength; i ++ ) {
|
||||
|
||||
const tileView = new DataView( buffer, offset, 12 );
|
||||
const tileMagic = readMagicBytes( tileView );
|
||||
const tileVersion = tileView.getUint32( 4, true );
|
||||
const byteLength = tileView.getUint32( 8, true );
|
||||
|
||||
const tileBuffer = new Uint8Array( buffer, offset, byteLength );
|
||||
tiles.push( {
|
||||
|
||||
type: tileMagic,
|
||||
buffer: tileBuffer,
|
||||
version: tileVersion,
|
||||
|
||||
} );
|
||||
offset += byteLength;
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
tiles,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { BatchTable } from '../utilities/BatchTable.js';
|
||||
import { FeatureTable } from '../utilities/FeatureTable.js';
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
|
||||
export interface I3DMBaseResult {
|
||||
|
||||
version : string;
|
||||
featureTable: FeatureTable;
|
||||
batchTable : BatchTable;
|
||||
glbBytes : Uint8Array;
|
||||
|
||||
}
|
||||
|
||||
export class I3DMLoaderBase<Result = I3DMBaseResult, ParseResult = Result>
|
||||
extends LoaderBase<Result, ParseResult> {
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// I3DM File Format
|
||||
// https://github.com/CesiumGS/3d-tiles/blob/master/specification/TileFormats/Instanced3DModel/README.md
|
||||
|
||||
import { BatchTable } from '../utilities/BatchTable.js';
|
||||
import { FeatureTable } from '../utilities/FeatureTable.js';
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
import { readMagicBytes, arrayToString, getWorkingPath } from '../utilities/LoaderUtils.js';
|
||||
|
||||
export class I3DMLoaderBase extends LoaderBase {
|
||||
|
||||
parse( buffer ) {
|
||||
|
||||
const dataView = new DataView( buffer );
|
||||
|
||||
// 32-byte header
|
||||
|
||||
// 4 bytes
|
||||
const magic = readMagicBytes( dataView );
|
||||
|
||||
console.assert( magic === 'i3dm' );
|
||||
|
||||
// 4 bytes
|
||||
const version = dataView.getUint32( 4, true );
|
||||
|
||||
console.assert( version === 1 );
|
||||
|
||||
// 4 bytes
|
||||
const byteLength = dataView.getUint32( 8, true );
|
||||
|
||||
console.assert( byteLength === buffer.byteLength );
|
||||
|
||||
// 4 bytes
|
||||
const featureTableJSONByteLength = dataView.getUint32( 12, true );
|
||||
|
||||
// 4 bytes
|
||||
const featureTableBinaryByteLength = dataView.getUint32( 16, true );
|
||||
|
||||
// 4 bytes
|
||||
const batchTableJSONByteLength = dataView.getUint32( 20, true );
|
||||
|
||||
// 4 bytes
|
||||
const batchTableBinaryByteLength = dataView.getUint32( 24, true );
|
||||
|
||||
// 4 bytes
|
||||
const gltfFormat = dataView.getUint32( 28, true );
|
||||
|
||||
// Feature Table
|
||||
const featureTableStart = 32;
|
||||
const featureTableBuffer = buffer.slice(
|
||||
featureTableStart,
|
||||
featureTableStart + featureTableJSONByteLength + featureTableBinaryByteLength,
|
||||
);
|
||||
const featureTable = new FeatureTable(
|
||||
featureTableBuffer,
|
||||
0,
|
||||
featureTableJSONByteLength,
|
||||
featureTableBinaryByteLength,
|
||||
);
|
||||
|
||||
// Batch Table
|
||||
const batchTableStart = featureTableStart + featureTableJSONByteLength + featureTableBinaryByteLength;
|
||||
const batchTableBuffer = buffer.slice(
|
||||
batchTableStart,
|
||||
batchTableStart + batchTableJSONByteLength + batchTableBinaryByteLength,
|
||||
);
|
||||
const batchTable = new BatchTable(
|
||||
batchTableBuffer,
|
||||
featureTable.getData( 'INSTANCES_LENGTH' ),
|
||||
0,
|
||||
batchTableJSONByteLength,
|
||||
batchTableBinaryByteLength,
|
||||
);
|
||||
|
||||
const glbStart = batchTableStart + batchTableJSONByteLength + batchTableBinaryByteLength;
|
||||
const bodyBytes = new Uint8Array( buffer, glbStart, byteLength - glbStart );
|
||||
|
||||
let glbBytes = null;
|
||||
let promise = null;
|
||||
let gltfWorkingPath = null;
|
||||
if ( gltfFormat ) {
|
||||
|
||||
glbBytes = bodyBytes;
|
||||
promise = Promise.resolve();
|
||||
|
||||
} else {
|
||||
|
||||
const externalUri = this.resolveExternalURL( arrayToString( bodyBytes ) );
|
||||
|
||||
//Store the gltf working path
|
||||
gltfWorkingPath = getWorkingPath( externalUri );
|
||||
|
||||
promise = fetch( externalUri, this.fetchOptions )
|
||||
.then( res => {
|
||||
|
||||
if ( ! res.ok ) {
|
||||
|
||||
throw new Error( `I3DMLoaderBase : Failed to load file "${ externalUri }" with status ${ res.status } : ${ res.statusText }` );
|
||||
|
||||
}
|
||||
|
||||
return res.arrayBuffer();
|
||||
|
||||
} )
|
||||
.then( buffer => {
|
||||
|
||||
glbBytes = new Uint8Array( buffer );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
return promise.then( () => {
|
||||
|
||||
return {
|
||||
version,
|
||||
featureTable,
|
||||
batchTable,
|
||||
glbBytes,
|
||||
gltfWorkingPath
|
||||
};
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export class LoaderBase<Result = any, ParseResult = Promise< Result >> {
|
||||
|
||||
fetchOptions: any;
|
||||
workingPath: string;
|
||||
load( url: string ): Promise< Result >;
|
||||
resolveExternalURL( url: string ): string;
|
||||
parse( buffer: ArrayBuffer ): ParseResult;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { getWorkingPath } from '../utilities/LoaderUtils.js';
|
||||
|
||||
export class LoaderBase {
|
||||
|
||||
constructor() {
|
||||
|
||||
this.fetchOptions = {};
|
||||
this.workingPath = '';
|
||||
|
||||
}
|
||||
|
||||
load( ...args ) {
|
||||
|
||||
console.warn( 'Loader: "load" function has been deprecated in favor of "loadAsync".' );
|
||||
return this.loadAsync( ...args );
|
||||
|
||||
}
|
||||
|
||||
loadAsync( url ) {
|
||||
|
||||
return fetch( url, this.fetchOptions )
|
||||
.then( res => {
|
||||
|
||||
if ( ! res.ok ) {
|
||||
|
||||
throw new Error( `Failed to load file "${ url }" with status ${ res.status } : ${ res.statusText }` );
|
||||
|
||||
}
|
||||
return res.arrayBuffer();
|
||||
|
||||
} )
|
||||
.then( buffer => {
|
||||
|
||||
if ( this.workingPath === '' ) {
|
||||
|
||||
this.workingPath = getWorkingPath( url );
|
||||
|
||||
}
|
||||
|
||||
return this.parse( buffer );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
resolveExternalURL( url ) {
|
||||
|
||||
return new URL( url, this.workingPath ).href;
|
||||
|
||||
}
|
||||
|
||||
parse( buffer ) {
|
||||
|
||||
throw new Error( 'LoaderBase: Parse not implemented.' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { BatchTable } from '../utilities/BatchTable.js';
|
||||
import { FeatureTable } from '../utilities/FeatureTable.js';
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
|
||||
export interface PNTSBaseResult {
|
||||
|
||||
version : string;
|
||||
featureTable: FeatureTable;
|
||||
batchTable : BatchTable;
|
||||
|
||||
}
|
||||
|
||||
export class PNTSLoaderBase<Result = PNTSBaseResult, ParseResult = Result>
|
||||
extends LoaderBase<Result, ParseResult> {
|
||||
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// PNTS File Format
|
||||
// https://github.com/CesiumGS/3d-tiles/blob/master/specification/TileFormats/PointCloud/README.md
|
||||
|
||||
import { BatchTable } from '../utilities/BatchTable.js';
|
||||
import { FeatureTable } from '../utilities/FeatureTable.js';
|
||||
import { readMagicBytes } from '../utilities/LoaderUtils.js';
|
||||
import { LoaderBase } from './LoaderBase.js';
|
||||
|
||||
export class PNTSLoaderBase extends LoaderBase {
|
||||
|
||||
parse( buffer ) {
|
||||
|
||||
const dataView = new DataView( buffer );
|
||||
|
||||
// 28-byte header
|
||||
|
||||
// 4 bytes
|
||||
const magic = readMagicBytes( dataView );
|
||||
|
||||
console.assert( magic === 'pnts' );
|
||||
|
||||
// 4 bytes
|
||||
const version = dataView.getUint32( 4, true );
|
||||
|
||||
console.assert( version === 1 );
|
||||
|
||||
// 4 bytes
|
||||
const byteLength = dataView.getUint32( 8, true );
|
||||
|
||||
console.assert( byteLength === buffer.byteLength );
|
||||
|
||||
// 4 bytes
|
||||
const featureTableJSONByteLength = dataView.getUint32( 12, true );
|
||||
|
||||
// 4 bytes
|
||||
const featureTableBinaryByteLength = dataView.getUint32( 16, true );
|
||||
|
||||
// 4 bytes
|
||||
const batchTableJSONByteLength = dataView.getUint32( 20, true );
|
||||
|
||||
// 4 bytes
|
||||
const batchTableBinaryByteLength = dataView.getUint32( 24, true );
|
||||
|
||||
// Feature Table
|
||||
const featureTableStart = 28;
|
||||
const featureTableBuffer = buffer.slice(
|
||||
featureTableStart,
|
||||
featureTableStart + featureTableJSONByteLength + featureTableBinaryByteLength,
|
||||
);
|
||||
const featureTable = new FeatureTable(
|
||||
featureTableBuffer,
|
||||
0,
|
||||
featureTableJSONByteLength,
|
||||
featureTableBinaryByteLength,
|
||||
);
|
||||
|
||||
// Batch Table
|
||||
const batchTableStart = featureTableStart + featureTableJSONByteLength + featureTableBinaryByteLength;
|
||||
const batchTableBuffer = buffer.slice(
|
||||
batchTableStart,
|
||||
batchTableStart + batchTableJSONByteLength + batchTableBinaryByteLength,
|
||||
);
|
||||
const batchTable = new BatchTable(
|
||||
batchTableBuffer,
|
||||
featureTable.getData( 'BATCH_LENGTH' ) || featureTable.getData( 'POINTS_LENGTH' ),
|
||||
0,
|
||||
batchTableJSONByteLength,
|
||||
batchTableBinaryByteLength,
|
||||
);
|
||||
|
||||
return Promise.resolve( {
|
||||
|
||||
version,
|
||||
featureTable,
|
||||
batchTable,
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { TileBase } from './TileBase.js';
|
||||
|
||||
/**
|
||||
* Documented 3d-tile state managed by the TilesRenderer* / used/usable in priority / traverseFunctions!
|
||||
*/
|
||||
export interface Tile extends TileBase {
|
||||
|
||||
parent: Tile;
|
||||
|
||||
/**
|
||||
* Hierarchy Depth from the TileGroup
|
||||
*/
|
||||
__depth : number;
|
||||
/**
|
||||
* The screen space error for this tile
|
||||
*/
|
||||
__error : number;
|
||||
/**
|
||||
* How far is this tiles bounds from the nearest active Camera.
|
||||
* Expected to be filled in during calculateError implementations.
|
||||
*/
|
||||
__distanceFromCamera : number;
|
||||
/**
|
||||
* This tile is currently active if:
|
||||
* 1: Tile content is loaded and ready to be made visible if needed
|
||||
*/
|
||||
__active : boolean;
|
||||
/**
|
||||
* This tile is currently visible if:
|
||||
* 1: Tile content is loaded
|
||||
* 2: Tile is within a camera frustum
|
||||
* 3: Tile meets the SSE requirements
|
||||
*/
|
||||
__visible : boolean;
|
||||
/**
|
||||
* Whether or not the tile was visited during the last update run.
|
||||
*/
|
||||
__used : boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the tile was within the frustum on the last update run.
|
||||
*/
|
||||
__inFrustum : boolean;
|
||||
|
||||
/**
|
||||
* The depth of the tiles that increments only when a child with geometry content is encountered
|
||||
*/
|
||||
__depthFromRenderedParent : number;
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 3d-tiles Tile object per spec:
|
||||
* (incomplete, expanding as features become supported by this package.)
|
||||
*
|
||||
* See spec for full schema: https://github.com/CesiumGS/3d-tiles/blob/master/specification/schema/tile.schema.json
|
||||
*/
|
||||
export interface TileBase {
|
||||
|
||||
boundingVolume: {
|
||||
|
||||
/**
|
||||
* An array of 12 numbers that define an oriented bounding box. The first three elements define the x, y, and z
|
||||
* values for the center of the box. The next three elements (with indices 3, 4, and 5) define the x axis
|
||||
* direction and half-length. The next three elements (indices 6, 7, and 8) define the y axis direction and
|
||||
* half-length. The last three elements (indices 9, 10, and 11) define the z axis direction and half-length.
|
||||
*/
|
||||
box?: number[];
|
||||
|
||||
/**
|
||||
* An array of four numbers that define a bounding sphere. The first three elements define the x, y, and z
|
||||
* values for the center of the sphere. The last element (with index 3) defines the radius in meters.
|
||||
*/
|
||||
sphere?: number[];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The error, in meters, introduced if this tileset is not rendered. At runtime, the geometric error is used to compute screen space error (SSE), i.e., the error measured in pixels.
|
||||
*/
|
||||
geometricError: number;
|
||||
|
||||
// optional properties
|
||||
|
||||
children?: TileBase[];
|
||||
|
||||
content?: {
|
||||
|
||||
uri: string;
|
||||
|
||||
/**
|
||||
* Dictionary object with content specific extension objects.
|
||||
*/
|
||||
extensions?: Record<string, any>;
|
||||
|
||||
extras?: Record<string, any>;
|
||||
|
||||
// Non standard, noted here as it exists in the code in this package to support old pre-1.0 tilesets
|
||||
url?: string;
|
||||
|
||||
};
|
||||
// An object that describes the implicit subdivision of this tile.
|
||||
implicitTiling: {
|
||||
// A string describing the subdivision scheme used within the tileset.
|
||||
subdivisionScheme: 'QUADTREE' | 'OCTREE';
|
||||
subtreeLevels: number;
|
||||
availableLevels: number;
|
||||
// An object describing the location of subtree files.
|
||||
subtrees: {
|
||||
// A template URI pointing to subtree files
|
||||
uri: string;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Dictionary object with tile specific extension objects.
|
||||
*/
|
||||
extensions?: Record<string, any>;
|
||||
|
||||
extras?: Record<string, any>;
|
||||
|
||||
refine?: 'REPLACE' | 'ADD';
|
||||
|
||||
transform?: number[];
|
||||
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { Tile } from './Tile.js';
|
||||
|
||||
/**
|
||||
* Internal state used/set by the package.
|
||||
*/
|
||||
|
||||
export interface TileInternal extends Tile {
|
||||
|
||||
// tile description
|
||||
__isLeaf: boolean;
|
||||
__hasContent: boolean;
|
||||
__hasRenderableContent: boolean;
|
||||
__hasUnrenderableContent: boolean;
|
||||
|
||||
// resource tracking
|
||||
__usedLastFrame: boolean;
|
||||
__used: boolean;
|
||||
|
||||
// Visibility tracking
|
||||
__allChildrenLoaded: boolean;
|
||||
__inFrustum: boolean;
|
||||
__wasSetVisible: boolean;
|
||||
|
||||
// download state tracking
|
||||
/**
|
||||
* This tile is currently active if:
|
||||
* 1: Tile content is loaded and ready to be made visible if needed
|
||||
*/
|
||||
__active: boolean;
|
||||
__loadIndex: number;
|
||||
__loadAbort: AbortController | null;
|
||||
__loadingState: number;
|
||||
__wasSetActive: boolean;
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { LRUCache } from '../utilities/LRUCache.js';
|
||||
import { PriorityQueue } from '../utilities/PriorityQueue.js';
|
||||
|
||||
export class TilesRendererBase {
|
||||
|
||||
readonly rootTileSet : object | null;
|
||||
readonly root : object | null;
|
||||
|
||||
errorTarget : number;
|
||||
errorThreshold : number;
|
||||
displayActiveTiles : boolean;
|
||||
maxDepth : number;
|
||||
|
||||
loadProgress: number;
|
||||
|
||||
fetchOptions : RequestInit;
|
||||
preprocessURL : ( ( uri: string | URL ) => string ) | null;
|
||||
|
||||
lruCache : LRUCache;
|
||||
parseQueue : PriorityQueue;
|
||||
downloadQueue : PriorityQueue;
|
||||
processNodeQueue: PriorityQueue;
|
||||
|
||||
constructor( url?: string );
|
||||
update() : void;
|
||||
registerPlugin( plugin: object ) : void;
|
||||
unregisterPlugin( plugin: object | string ) : boolean;
|
||||
getPluginByName( plugin: object | string ) : object;
|
||||
traverse(
|
||||
beforeCb : ( ( tile : object, parent : object, depth : number ) => boolean ) | null,
|
||||
afterCb : ( ( tile : object, parent : object, depth : number ) => boolean ) | null
|
||||
) : void;
|
||||
getAttributions( target? : Array<{ type: string, value: any }> ) : Array<{ type: string, value: any }>;
|
||||
|
||||
dispose() : void;
|
||||
resetFailedTiles() : void;
|
||||
|
||||
}
|
||||
+1077
File diff suppressed because it is too large
Load Diff
+66
@@ -0,0 +1,66 @@
|
||||
import { TileBase } from './TileBase.js';
|
||||
|
||||
/**
|
||||
* A 3d-tiles tileset.
|
||||
*
|
||||
* Schema, see: https://github.com/CesiumGS/3d-tiles/blob/main/specification/schema/tileset.schema.json
|
||||
*/
|
||||
export interface Tileset {
|
||||
|
||||
/**
|
||||
* Metadata about the entire tileset.
|
||||
*/
|
||||
asset: {
|
||||
|
||||
/**
|
||||
* 3d-tiles version
|
||||
*/
|
||||
version: string,
|
||||
|
||||
/**
|
||||
* Application specific version
|
||||
*/
|
||||
tilesetVersion?: string,
|
||||
|
||||
/**
|
||||
* Dictionary object with extension-specific objects.
|
||||
*/
|
||||
extensions? : Record<string, any>,
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The error, in meters, introduced if this tileset is not rendered. At runtime, the geometric error is used to compute screen space error (SSE), i.e., the error measured in pixels.
|
||||
*/
|
||||
geometricError: number;
|
||||
|
||||
/**
|
||||
* The root tile.
|
||||
*/
|
||||
root: TileBase;
|
||||
|
||||
// optional properties
|
||||
|
||||
/**
|
||||
* Names of 3D Tiles extensions used somewhere in this tileset.
|
||||
*/
|
||||
extensionsUsed?: string[];
|
||||
|
||||
/**
|
||||
* Names of 3D Tiles extensions required to properly load this tileset.
|
||||
*/
|
||||
extensionsRequired?: string[];
|
||||
|
||||
/**
|
||||
* A dictionary object of metadata about per-feature properties.
|
||||
*/
|
||||
properties?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* Dictionary object with extension-specific objects.
|
||||
*/
|
||||
extensions? : Record<string, any>;
|
||||
|
||||
extras? : Record<string, any>;
|
||||
|
||||
}
|
||||
+454
@@ -0,0 +1,454 @@
|
||||
import { LOADED, FAILED } from '../constants.js';
|
||||
|
||||
const viewErrorTarget = {
|
||||
inView: false,
|
||||
error: Infinity,
|
||||
distanceFromCamera: Infinity,
|
||||
};
|
||||
|
||||
// flag guiding the behavior of the traversal to load the siblings at the root of the
|
||||
// tile set or not. The spec seems to indicate "true" when using REPLACE define but
|
||||
// Cesium's behavior is "false".
|
||||
// See CesiumGS/3d-tiles#776
|
||||
const LOAD_ROOT_SIBLINGS = true;
|
||||
|
||||
function isDownloadFinished( value ) {
|
||||
|
||||
return value === LOADED || value === FAILED;
|
||||
|
||||
}
|
||||
|
||||
// Checks whether this tile was last used on the given frame.
|
||||
function isUsedThisFrame( tile, frameCount ) {
|
||||
|
||||
return tile.__lastFrameVisited === frameCount && tile.__used;
|
||||
|
||||
}
|
||||
|
||||
function areChildrenProcessed( tile ) {
|
||||
|
||||
return tile.__childrenProcessed === tile.children.length;
|
||||
|
||||
}
|
||||
|
||||
// Resets the frame frame information for the given tile
|
||||
function resetFrameState( tile, renderer ) {
|
||||
|
||||
if ( tile.__lastFrameVisited !== renderer.frameCount ) {
|
||||
|
||||
tile.__lastFrameVisited = renderer.frameCount;
|
||||
tile.__used = false;
|
||||
tile.__inFrustum = false;
|
||||
tile.__isLeaf = false;
|
||||
tile.__visible = false;
|
||||
tile.__active = false;
|
||||
tile.__error = Infinity;
|
||||
tile.__distanceFromCamera = Infinity;
|
||||
tile.__allChildrenLoaded = false;
|
||||
|
||||
// update tile frustum and error state
|
||||
renderer.calculateTileViewError( tile, viewErrorTarget );
|
||||
tile.__inFrustum = viewErrorTarget.inView;
|
||||
tile.__error = viewErrorTarget.error;
|
||||
tile.__distanceFromCamera = viewErrorTarget.distanceFromCamera;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Recursively mark tiles used down to the next layer, skipping external tile sets
|
||||
function recursivelyMarkUsed( tile, renderer ) {
|
||||
|
||||
renderer.ensureChildrenArePreprocessed( tile );
|
||||
|
||||
resetFrameState( tile, renderer );
|
||||
markUsed( tile, renderer );
|
||||
|
||||
// don't traverse if the children have not been processed, yet but tile set content
|
||||
// should be considered to be "replaced" by the loaded children so await that here.
|
||||
if ( tile.__hasUnrenderableContent && areChildrenProcessed( tile ) ) {
|
||||
|
||||
const children = tile.children;
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
recursivelyMarkUsed( children[ i ], renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Recursively traverses to the next tiles with unloaded renderable content to load them
|
||||
function recursivelyLoadNextRenderableTiles( tile, renderer ) {
|
||||
|
||||
renderer.ensureChildrenArePreprocessed( tile );
|
||||
|
||||
// exit the recursion if the tile hasn't been used this frame
|
||||
if ( isUsedThisFrame( tile, renderer.frameCount ) ) {
|
||||
|
||||
// queue this tile to download content
|
||||
if ( tile.__hasContent ) {
|
||||
|
||||
renderer.queueTileForDownload( tile );
|
||||
|
||||
}
|
||||
|
||||
if ( areChildrenProcessed( tile ) ) {
|
||||
|
||||
// queue any used child tiles
|
||||
const children = tile.children;
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
recursivelyLoadNextRenderableTiles( children[ i ], renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Mark a tile as being used by current view
|
||||
function markUsed( tile, renderer ) {
|
||||
|
||||
if ( tile.__used ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
tile.__used = true;
|
||||
renderer.markTileUsed( tile );
|
||||
renderer.stats.used ++;
|
||||
|
||||
if ( tile.__inFrustum === true ) {
|
||||
|
||||
renderer.stats.inFrustum ++;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Returns whether the tile can be traversed to the next layer of children by checking the tile metrics
|
||||
function canTraverse( tile, renderer ) {
|
||||
|
||||
// If we've met the error requirements then don't load further - if an external tile set is encountered,
|
||||
// though, then continue to refine.
|
||||
if ( tile.__error <= renderer.errorTarget && ! tile.__hasUnrenderableContent ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// Early out if we've reached the maximum allowed depth.
|
||||
if ( renderer.maxDepth > 0 && tile.__depth + 1 >= renderer.maxDepth ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// Early out if the children haven't been processed, yet
|
||||
if ( ! areChildrenProcessed( tile ) ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// Determine which tiles are used by the renderer given the current camera configuration
|
||||
export function markUsedTiles( tile, renderer ) {
|
||||
|
||||
// determine frustum set is run first so we can ensure the preprocessing of all the necessary
|
||||
// child tiles has happened here.
|
||||
renderer.ensureChildrenArePreprocessed( tile );
|
||||
|
||||
resetFrameState( tile, renderer );
|
||||
|
||||
if ( ! tile.__inFrustum ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if ( ! canTraverse( tile, renderer ) ) {
|
||||
|
||||
markUsed( tile, renderer );
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Traverse children and see if any children are in view.
|
||||
let anyChildrenUsed = false;
|
||||
let anyChildrenInFrustum = false;
|
||||
const children = tile.children;
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
const c = children[ i ];
|
||||
markUsedTiles( c, renderer );
|
||||
anyChildrenUsed = anyChildrenUsed || isUsedThisFrame( c, renderer.frameCount );
|
||||
anyChildrenInFrustum = anyChildrenInFrustum || c.__inFrustum;
|
||||
|
||||
}
|
||||
|
||||
// Disabled for now because this will cause otherwise unused children to be added to the lru cache
|
||||
// if none of the children are in the frustum then this tile shouldn't be displayed.
|
||||
// Otherwise this can cause load oscillation as parents are traversed and loaded and then determined
|
||||
// to not be used because children aren't visible. See #1165.
|
||||
// if ( tile.refine === 'REPLACE' && ! anyChildrenInFrustum && children.length !== 0 && ! tile.__hasUnrenderableContent ) {
|
||||
|
||||
// // TODO: we're not checking tiles with unrenderable content here since external tile sets might look like they're in the frustum,
|
||||
// // load the children, then the children indicate that it's not visible, causing it to be unloaded. Then it will be loaded again.
|
||||
// // The impact when including external tile set roots in the check is more significant but can't be used unless we keep external tile
|
||||
// // sets around even when they're not needed. See issue #741.
|
||||
|
||||
// // TODO: what if we mark the tile as not in the frustum but we _do_ mark it as used? Then we can stop frustum traversal and at least
|
||||
// // prevent tiles from rendering unless they're needed.
|
||||
// console.log('FAILED')
|
||||
// tile.__inFrustum = false;
|
||||
// return;
|
||||
|
||||
// }
|
||||
|
||||
// wait until after the above condition to mark the traversed tile as used or not
|
||||
markUsed( tile, renderer );
|
||||
|
||||
// If this is a tile that needs children loaded to refine then recursively load child
|
||||
// tiles until error is met
|
||||
if ( anyChildrenUsed && tile.refine === 'REPLACE' && ( tile.__depth !== 0 || LOAD_ROOT_SIBLINGS ) ) {
|
||||
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
const c = children[ i ];
|
||||
recursivelyMarkUsed( c, renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Traverse and mark the tiles that are at the leaf nodes of the "used" tree.
|
||||
export function markUsedSetLeaves( tile, renderer ) {
|
||||
|
||||
const frameCount = renderer.frameCount;
|
||||
if ( ! isUsedThisFrame( tile, frameCount ) ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// This tile is a leaf if none of the children had been used.
|
||||
const children = tile.children;
|
||||
let anyChildrenUsed = false;
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
const c = children[ i ];
|
||||
anyChildrenUsed = anyChildrenUsed || isUsedThisFrame( c, frameCount );
|
||||
|
||||
}
|
||||
|
||||
if ( ! anyChildrenUsed ) {
|
||||
|
||||
tile.__isLeaf = true;
|
||||
|
||||
} else {
|
||||
|
||||
let allChildrenLoaded = true;
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
const c = children[ i ];
|
||||
markUsedSetLeaves( c, renderer );
|
||||
|
||||
if ( isUsedThisFrame( c, frameCount ) ) {
|
||||
|
||||
// consider a child to be loaded if
|
||||
// - the children's children have been loaded
|
||||
// - the tile content has loaded
|
||||
// - the tile is completely empty - ie has no children and no content
|
||||
// - the child tile set has tried to load but failed
|
||||
const childLoaded =
|
||||
c.__allChildrenLoaded ||
|
||||
! c.__hasContent ||
|
||||
( c.__hasRenderableContent && isDownloadFinished( c.__loadingState ) ) ||
|
||||
( c.__hasUnrenderableContent && c.__loadingState === FAILED );
|
||||
allChildrenLoaded = allChildrenLoaded && childLoaded;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tile.__allChildrenLoaded = allChildrenLoaded;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: revisit implementation
|
||||
// Skip past tiles we consider unrenderable because they are outside the error threshold.
|
||||
export function markVisibleTiles( tile, renderer ) {
|
||||
|
||||
const stats = renderer.stats;
|
||||
if ( ! isUsedThisFrame( tile, renderer.frameCount ) ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Request the tile contents or mark it as visible if we've found a leaf.
|
||||
if ( tile.__isLeaf ) {
|
||||
|
||||
if ( tile.__loadingState === LOADED ) {
|
||||
|
||||
if ( tile.__inFrustum ) {
|
||||
|
||||
tile.__visible = true;
|
||||
stats.visible ++;
|
||||
|
||||
}
|
||||
tile.__active = true;
|
||||
stats.active ++;
|
||||
|
||||
} else if ( tile.__hasContent ) {
|
||||
|
||||
renderer.queueTileForDownload( tile );
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
const children = tile.children;
|
||||
const hasContent = tile.__hasContent;
|
||||
const loadedContent = isDownloadFinished( tile.__loadingState ) && hasContent;
|
||||
const errorRequirement = ( renderer.errorTarget + 1 ) * renderer.errorThreshold;
|
||||
const meetsSSE = tile.__error <= errorRequirement;
|
||||
const isAdditiveRefine = tile.refine === 'ADD';
|
||||
|
||||
// TODO: the "meetsSSE" field can be removed when the "errorThreshold" field has been removed
|
||||
|
||||
// Don't wait for all children tiles to load if this tile set has empty tiles at the root in order
|
||||
// to match Cesium's behavior
|
||||
const allChildrenLoaded = tile.__allChildrenLoaded || ( tile.__depth === 0 && ! LOAD_ROOT_SIBLINGS );
|
||||
|
||||
// If we've met the SSE requirements and we can load content then fire a fetch.
|
||||
if ( hasContent && ( meetsSSE || isAdditiveRefine ) ) {
|
||||
|
||||
renderer.queueTileForDownload( tile );
|
||||
|
||||
}
|
||||
|
||||
// By this time only tiles that meet the screen space error requirements will be traversed. Only mark this
|
||||
// as visible if it's been loaded and not all children have loaded yet or it's an additive tile, meaning it needs
|
||||
// to display in addition to the children.
|
||||
|
||||
// Skip the tile entirely if there's no content to load
|
||||
if ( meetsSSE && loadedContent && ! allChildrenLoaded || loadedContent && isAdditiveRefine ) {
|
||||
|
||||
if ( tile.__inFrustum ) {
|
||||
|
||||
tile.__visible = true;
|
||||
stats.visible ++;
|
||||
|
||||
}
|
||||
tile.__active = true;
|
||||
stats.active ++;
|
||||
|
||||
}
|
||||
|
||||
// If we're additive then don't stop the traversal here because it doesn't matter whether the children load in
|
||||
// at the same rate.
|
||||
if ( ! isAdditiveRefine && meetsSSE && ! allChildrenLoaded ) {
|
||||
|
||||
// load the child content if we've found that we've been loaded so we can move down to the next tile
|
||||
// layer when the data has loaded.
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
const c = children[ i ];
|
||||
if ( isUsedThisFrame( c, renderer.frameCount ) ) {
|
||||
|
||||
recursivelyLoadNextRenderableTiles( c, renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
markVisibleTiles( children[ i ], renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Final traverse to toggle tile visibility.
|
||||
export function toggleTiles( tile, renderer ) {
|
||||
|
||||
const isUsed = isUsedThisFrame( tile, renderer.frameCount );
|
||||
if ( isUsed || tile.__usedLastFrame ) {
|
||||
|
||||
let setActive = false;
|
||||
let setVisible = false;
|
||||
if ( isUsed ) {
|
||||
|
||||
// enable visibility if active due to shadows
|
||||
setActive = tile.__active;
|
||||
if ( renderer.displayActiveTiles ) {
|
||||
|
||||
setVisible = tile.__active || tile.__visible;
|
||||
|
||||
} else {
|
||||
|
||||
setVisible = tile.__visible;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// if the tile was used last frame but not this one then there's potential for the tile
|
||||
// to not have been visited during the traversal, meaning it hasn't been reset and has
|
||||
// stale values. This ensures the values are not stale.
|
||||
resetFrameState( tile, renderer );
|
||||
|
||||
}
|
||||
|
||||
// If the active or visible state changed then call the functions.
|
||||
if ( tile.__hasRenderableContent && tile.__loadingState === LOADED ) {
|
||||
|
||||
if ( tile.__wasSetActive !== setActive ) {
|
||||
|
||||
renderer.invokeOnePlugin( plugin => plugin.setTileActive && plugin.setTileActive( tile, setActive ) );
|
||||
|
||||
}
|
||||
|
||||
if ( tile.__wasSetVisible !== setVisible ) {
|
||||
|
||||
renderer.invokeOnePlugin( plugin => plugin.setTileVisible && plugin.setTileVisible( tile, setVisible ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
tile.__wasSetActive = setActive;
|
||||
tile.__wasSetVisible = setVisible;
|
||||
tile.__usedLastFrame = isUsed;
|
||||
|
||||
const children = tile.children;
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
const c = children[ i ];
|
||||
toggleTiles( c, renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
export class BatchTable {
|
||||
|
||||
count : number;
|
||||
|
||||
constructor(
|
||||
buffer : ArrayBuffer,
|
||||
count : number,
|
||||
start : number,
|
||||
headerLength : number,
|
||||
binLength : number
|
||||
);
|
||||
|
||||
getKeys() : Array< string >;
|
||||
|
||||
getDataFromId(
|
||||
id: number,
|
||||
target?: object
|
||||
) : object;
|
||||
|
||||
getPropertyArray(
|
||||
key: string,
|
||||
) : number | string | ArrayBufferView;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { BatchTableHierarchyExtension } from './BatchTableHierarchyExtension.js';
|
||||
import { FeatureTable } from './FeatureTable.js';
|
||||
|
||||
export class BatchTable extends FeatureTable {
|
||||
|
||||
get batchSize() {
|
||||
|
||||
console.warn( 'BatchTable.batchSize has been deprecated and replaced with BatchTable.count.' );
|
||||
return this.count;
|
||||
|
||||
}
|
||||
|
||||
constructor( buffer, count, start, headerLength, binLength ) {
|
||||
|
||||
super( buffer, start, headerLength, binLength );
|
||||
this.count = count;
|
||||
|
||||
this.extensions = {};
|
||||
const extensions = this.header.extensions;
|
||||
if ( extensions ) {
|
||||
|
||||
if ( extensions[ '3DTILES_batch_table_hierarchy' ] ) {
|
||||
|
||||
this.extensions[ '3DTILES_batch_table_hierarchy' ] = new BatchTableHierarchyExtension( this );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getData( key, componentType = null, type = null ) {
|
||||
|
||||
console.warn( 'BatchTable: BatchTable.getData is deprecated. Use BatchTable.getDataFromId to get all' +
|
||||
'properties for an id or BatchTable.getPropertyArray for getting an array of value for a property.' );
|
||||
return super.getData( key, this.count, componentType, type );
|
||||
|
||||
}
|
||||
|
||||
getDataFromId( id, target = {} ) {
|
||||
|
||||
if ( id < 0 || id >= this.count ) {
|
||||
|
||||
throw new Error( `BatchTable: id value "${ id }" out of bounds for "${ this.count }" features number.` );
|
||||
|
||||
}
|
||||
|
||||
for ( const key of this.getKeys() ) {
|
||||
|
||||
target[ key ] = super.getData( key, this.count )[ id ];
|
||||
|
||||
}
|
||||
|
||||
for ( const extensionName in this.extensions ) {
|
||||
|
||||
const extension = this.extensions[ extensionName ];
|
||||
|
||||
if ( extension.getDataFromId instanceof Function ) {
|
||||
|
||||
target[ extensionName ] = target[ extensionName ] || {};
|
||||
extension.getDataFromId( id, target[ extensionName ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return target;
|
||||
|
||||
}
|
||||
|
||||
getPropertyArray( key ) {
|
||||
|
||||
return super.getData( key, this.count );
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { parseBinArray } from './FeatureTable.js';
|
||||
|
||||
export class BatchTableHierarchyExtension {
|
||||
|
||||
constructor( batchTable ) {
|
||||
|
||||
this.batchTable = batchTable;
|
||||
|
||||
const extensionHeader = batchTable.header.extensions[ '3DTILES_batch_table_hierarchy' ];
|
||||
|
||||
this.classes = extensionHeader.classes;
|
||||
for ( const classDef of this.classes ) {
|
||||
|
||||
const instances = classDef.instances;
|
||||
for ( const property in instances ) {
|
||||
|
||||
classDef.instances[ property ] = this._parseProperty( instances[ property ], classDef.length, property );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.instancesLength = extensionHeader.instancesLength;
|
||||
|
||||
this.classIds = this._parseProperty( extensionHeader.classIds, this.instancesLength, 'classIds' );
|
||||
|
||||
if ( extensionHeader.parentCounts ) {
|
||||
|
||||
this.parentCounts = this._parseProperty( extensionHeader.parentCounts, this.instancesLength, 'parentCounts' );
|
||||
|
||||
} else {
|
||||
|
||||
this.parentCounts = new Array( this.instancesLength ).fill( 1 );
|
||||
|
||||
}
|
||||
|
||||
if ( extensionHeader.parentIds ) {
|
||||
|
||||
const parentIdsLength = this.parentCounts.reduce( ( a, b ) => a + b, 0 );
|
||||
this.parentIds = this._parseProperty( extensionHeader.parentIds, parentIdsLength, 'parentIds' );
|
||||
|
||||
} else {
|
||||
|
||||
this.parentIds = null;
|
||||
|
||||
}
|
||||
|
||||
this.instancesIds = [];
|
||||
const classCounter = {};
|
||||
for ( const classId of this.classIds ) {
|
||||
|
||||
classCounter[ classId ] = classCounter[ classId ] ?? 0;
|
||||
this.instancesIds.push( classCounter[ classId ] );
|
||||
classCounter[ classId ] ++;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_parseProperty( property, propertyLength, propertyName ) {
|
||||
|
||||
if ( Array.isArray( property ) ) {
|
||||
|
||||
return property;
|
||||
|
||||
} else {
|
||||
|
||||
const { buffer, binOffset } = this.batchTable;
|
||||
|
||||
const byteOffset = property.byteOffset;
|
||||
const componentType = property.componentType || 'UNSIGNED_SHORT';
|
||||
|
||||
const arrayStart = binOffset + byteOffset;
|
||||
|
||||
return parseBinArray( buffer, arrayStart, propertyLength, 'SCALAR', componentType, propertyName );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getDataFromId( id, target = {} ) {
|
||||
|
||||
// Get properties inherited from parents
|
||||
|
||||
const parentCount = this.parentCounts[ id ];
|
||||
|
||||
if ( this.parentIds && parentCount > 0 ) {
|
||||
|
||||
let parentIdsOffset = 0;
|
||||
for ( let i = 0; i < id; i ++ ) {
|
||||
|
||||
parentIdsOffset += this.parentCounts[ i ];
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < parentCount; i ++ ) {
|
||||
|
||||
const parentId = this.parentIds[ parentIdsOffset + i ];
|
||||
if ( parentId !== id ) {
|
||||
|
||||
this.getDataFromId( parentId, target );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Get properties proper to this instance
|
||||
|
||||
const classId = this.classIds[ id ];
|
||||
const instances = this.classes[ classId ].instances;
|
||||
const className = this.classes[ classId ].name;
|
||||
const instanceId = this.instancesIds[ id ];
|
||||
|
||||
for ( const key in instances ) {
|
||||
|
||||
target[ className ] = target[ className ] || {};
|
||||
target[ className ][ key ] = instances[ key ][ instanceId ];
|
||||
|
||||
}
|
||||
|
||||
return target;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
interface FeatureTableHeader {
|
||||
|
||||
extensions?: object;
|
||||
extras?: any;
|
||||
|
||||
}
|
||||
|
||||
export class FeatureTable {
|
||||
|
||||
header: FeatureTableHeader;
|
||||
|
||||
constructor(
|
||||
buffer : ArrayBuffer,
|
||||
start : number,
|
||||
headerLength : number,
|
||||
binLength : number
|
||||
);
|
||||
|
||||
getKeys() : Array< string >;
|
||||
|
||||
getData(
|
||||
key : string,
|
||||
count : number,
|
||||
defaultComponentType? : string | null,
|
||||
defaultType? : string | null
|
||||
) : number | string | ArrayBufferView;
|
||||
|
||||
getBuffer( byteOffset : number, byteLength : number ) : ArrayBuffer;
|
||||
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { arrayToString } from './LoaderUtils.js';
|
||||
|
||||
export function parseBinArray( buffer, arrayStart, count, type, componentType, propertyName ) {
|
||||
|
||||
let stride;
|
||||
switch ( type ) {
|
||||
|
||||
case 'SCALAR':
|
||||
stride = 1;
|
||||
break;
|
||||
|
||||
case 'VEC2':
|
||||
stride = 2;
|
||||
break;
|
||||
|
||||
case 'VEC3':
|
||||
stride = 3;
|
||||
break;
|
||||
|
||||
case 'VEC4':
|
||||
stride = 4;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error( `FeatureTable : Feature type not provided for "${ propertyName }".` );
|
||||
|
||||
}
|
||||
|
||||
let data;
|
||||
const arrayLength = count * stride;
|
||||
|
||||
switch ( componentType ) {
|
||||
|
||||
case 'BYTE':
|
||||
data = new Int8Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
case 'UNSIGNED_BYTE':
|
||||
data = new Uint8Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
case 'SHORT':
|
||||
data = new Int16Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
case 'UNSIGNED_SHORT':
|
||||
data = new Uint16Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
case 'INT':
|
||||
data = new Int32Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
case 'UNSIGNED_INT':
|
||||
data = new Uint32Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
case 'FLOAT':
|
||||
data = new Float32Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
case 'DOUBLE':
|
||||
data = new Float64Array( buffer, arrayStart, arrayLength );
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error( `FeatureTable : Feature component type not provided for "${ propertyName }".` );
|
||||
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
export class FeatureTable {
|
||||
|
||||
constructor( buffer, start, headerLength, binLength ) {
|
||||
|
||||
this.buffer = buffer;
|
||||
this.binOffset = start + headerLength;
|
||||
this.binLength = binLength;
|
||||
|
||||
let header = null;
|
||||
if ( headerLength !== 0 ) {
|
||||
|
||||
const headerData = new Uint8Array( buffer, start, headerLength );
|
||||
header = JSON.parse( arrayToString( headerData ) );
|
||||
|
||||
} else {
|
||||
|
||||
header = {};
|
||||
|
||||
}
|
||||
this.header = header;
|
||||
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
|
||||
return Object.keys( this.header ).filter( key => key !== 'extensions' );
|
||||
|
||||
}
|
||||
|
||||
getData( key, count, defaultComponentType = null, defaultType = null ) {
|
||||
|
||||
const header = this.header;
|
||||
|
||||
if ( ! ( key in header ) ) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
const feature = header[ key ];
|
||||
if ( ! ( feature instanceof Object ) ) {
|
||||
|
||||
return feature;
|
||||
|
||||
} else if ( Array.isArray( feature ) ) {
|
||||
|
||||
return feature;
|
||||
|
||||
} else {
|
||||
|
||||
const { buffer, binOffset, binLength } = this;
|
||||
const byteOffset = feature.byteOffset || 0;
|
||||
const featureType = feature.type || defaultType;
|
||||
const featureComponentType = feature.componentType || defaultComponentType;
|
||||
|
||||
if ( 'type' in feature && defaultType && feature.type !== defaultType ) {
|
||||
|
||||
throw new Error( 'FeatureTable: Specified type does not match expected type.' );
|
||||
|
||||
}
|
||||
|
||||
const arrayStart = binOffset + byteOffset;
|
||||
const data = parseBinArray( buffer, arrayStart, count, featureType, featureComponentType, key );
|
||||
|
||||
const dataEnd = arrayStart + data.byteLength;
|
||||
if ( dataEnd > binOffset + binLength ) {
|
||||
|
||||
throw new Error( 'FeatureTable: Feature data read outside binary body length.' );
|
||||
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getBuffer( byteOffset, byteLength ) {
|
||||
|
||||
const { buffer, binOffset } = this;
|
||||
return buffer.slice( binOffset + byteOffset, binOffset + byteOffset + byteLength );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export class LRUCache {
|
||||
|
||||
minSize: number;
|
||||
maxSize: number;
|
||||
minBytesSize: number;
|
||||
maxBytesSize: number;
|
||||
unloadPercent: number;
|
||||
autoMarkUnused: boolean;
|
||||
|
||||
unloadPriorityCallback: ( item: any ) => number;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
const GIGABYTE_BYTES = 2 ** 30;
|
||||
|
||||
class LRUCache {
|
||||
|
||||
get unloadPriorityCallback() {
|
||||
|
||||
return this._unloadPriorityCallback;
|
||||
|
||||
}
|
||||
|
||||
set unloadPriorityCallback( cb ) {
|
||||
|
||||
if ( cb.length === 1 ) {
|
||||
|
||||
console.warn( 'LRUCache: "unloadPriorityCallback" function has been changed to take two arguments.' );
|
||||
this._unloadPriorityCallback = ( a, b ) => {
|
||||
|
||||
const valA = cb( a );
|
||||
const valB = cb( b );
|
||||
|
||||
if ( valA < valB ) return - 1;
|
||||
if ( valA > valB ) return 1;
|
||||
return 0;
|
||||
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
this._unloadPriorityCallback = cb;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
||||
// options
|
||||
this.minSize = 6000;
|
||||
this.maxSize = 8000;
|
||||
this.minBytesSize = 0.3 * GIGABYTE_BYTES;
|
||||
this.maxBytesSize = 0.4 * GIGABYTE_BYTES;
|
||||
this.unloadPercent = 0.05;
|
||||
this.autoMarkUnused = true;
|
||||
|
||||
// "itemSet" doubles as both the list of the full set of items currently
|
||||
// stored in the cache (keys) as well as a map to the time the item was last
|
||||
// used so it can be sorted appropriately.
|
||||
this.itemSet = new Map();
|
||||
this.itemList = [];
|
||||
this.usedSet = new Set();
|
||||
this.callbacks = new Map();
|
||||
this.unloadingHandle = - 1;
|
||||
this.cachedBytes = 0;
|
||||
this.bytesMap = new Map();
|
||||
this.loadedSet = new Set();
|
||||
|
||||
this._unloadPriorityCallback = null;
|
||||
|
||||
const itemSet = this.itemSet;
|
||||
this.defaultPriorityCallback = item => itemSet.get( item );
|
||||
|
||||
}
|
||||
|
||||
// Returns whether or not the cache has reached the maximum size
|
||||
isFull() {
|
||||
|
||||
return this.itemSet.size >= this.maxSize || this.cachedBytes >= this.maxBytesSize;
|
||||
|
||||
}
|
||||
|
||||
getMemoryUsage( item ) {
|
||||
|
||||
return this.bytesMap.get( item ) || 0;
|
||||
|
||||
}
|
||||
|
||||
setMemoryUsage( item, bytes ) {
|
||||
|
||||
const { bytesMap, itemSet } = this;
|
||||
if ( ! itemSet.has( item ) ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
this.cachedBytes -= bytesMap.get( item ) || 0;
|
||||
bytesMap.set( item, bytes );
|
||||
this.cachedBytes += bytes;
|
||||
|
||||
}
|
||||
|
||||
add( item, removeCb ) {
|
||||
|
||||
const itemSet = this.itemSet;
|
||||
if ( itemSet.has( item ) ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
if ( this.isFull() ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
const usedSet = this.usedSet;
|
||||
const itemList = this.itemList;
|
||||
const callbacks = this.callbacks;
|
||||
itemList.push( item );
|
||||
usedSet.add( item );
|
||||
itemSet.set( item, Date.now() );
|
||||
callbacks.set( item, removeCb );
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
has( item ) {
|
||||
|
||||
return this.itemSet.has( item );
|
||||
|
||||
}
|
||||
|
||||
remove( item ) {
|
||||
|
||||
const usedSet = this.usedSet;
|
||||
const itemSet = this.itemSet;
|
||||
const itemList = this.itemList;
|
||||
const bytesMap = this.bytesMap;
|
||||
const callbacks = this.callbacks;
|
||||
const loadedSet = this.loadedSet;
|
||||
|
||||
if ( itemSet.has( item ) ) {
|
||||
|
||||
this.cachedBytes -= bytesMap.get( item ) || 0;
|
||||
bytesMap.delete( item );
|
||||
|
||||
callbacks.get( item )( item );
|
||||
|
||||
const index = itemList.indexOf( item );
|
||||
itemList.splice( index, 1 );
|
||||
usedSet.delete( item );
|
||||
itemSet.delete( item );
|
||||
callbacks.delete( item );
|
||||
loadedSet.delete( item );
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// Marks whether tiles in the cache have been completely loaded or not. Tiles that have not been completely
|
||||
// loaded are subject to being disposed early if the cache is full above its max size limits, even if they
|
||||
// are marked as used.
|
||||
setLoaded( item, value ) {
|
||||
|
||||
const { itemSet, loadedSet } = this;
|
||||
if ( itemSet.has( item ) ) {
|
||||
|
||||
if ( value === true ) {
|
||||
|
||||
loadedSet.add( item );
|
||||
|
||||
} else {
|
||||
|
||||
loadedSet.delete( item );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
markUsed( item ) {
|
||||
|
||||
const itemSet = this.itemSet;
|
||||
const usedSet = this.usedSet;
|
||||
if ( itemSet.has( item ) && ! usedSet.has( item ) ) {
|
||||
|
||||
itemSet.set( item, Date.now() );
|
||||
usedSet.add( item );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
markUnused( item ) {
|
||||
|
||||
this.usedSet.delete( item );
|
||||
|
||||
}
|
||||
|
||||
markAllUnused() {
|
||||
|
||||
this.usedSet.clear();
|
||||
|
||||
}
|
||||
|
||||
// TODO: this should be renamed because it's not necessarily unloading all unused content
|
||||
// Maybe call it "cleanup" or "unloadToMinSize"
|
||||
unloadUnusedContent() {
|
||||
|
||||
const {
|
||||
unloadPercent,
|
||||
minSize,
|
||||
maxSize,
|
||||
itemList,
|
||||
itemSet,
|
||||
usedSet,
|
||||
loadedSet,
|
||||
callbacks,
|
||||
bytesMap,
|
||||
minBytesSize,
|
||||
maxBytesSize,
|
||||
} = this;
|
||||
|
||||
const unused = itemList.length - usedSet.size;
|
||||
const unloaded = itemList.length - loadedSet.size;
|
||||
const excessNodes = Math.max( Math.min( itemList.length - minSize, unused ), 0 );
|
||||
const excessBytes = this.cachedBytes - minBytesSize;
|
||||
const unloadPriorityCallback = this.unloadPriorityCallback || this.defaultPriorityCallback;
|
||||
let needsRerun = false;
|
||||
|
||||
const hasNodesToUnload = excessNodes > 0 && unused > 0 || unloaded && itemList.length > maxSize;
|
||||
const hasBytesToUnload = unused && this.cachedBytes > minBytesSize || unloaded && this.cachedBytes > maxBytesSize;
|
||||
if ( hasBytesToUnload || hasNodesToUnload ) {
|
||||
|
||||
// used items should be at the end of the array, "unloaded" items in the middle of the array
|
||||
itemList.sort( ( a, b ) => {
|
||||
|
||||
const usedA = usedSet.has( a );
|
||||
const usedB = usedSet.has( b );
|
||||
if ( usedA === usedB ) {
|
||||
|
||||
const loadedA = loadedSet.has( a );
|
||||
const loadedB = loadedSet.has( b );
|
||||
if ( loadedA === loadedB ) {
|
||||
|
||||
// Use the sort function otherwise
|
||||
// higher priority should be further to the left
|
||||
return - unloadPriorityCallback( a, b );
|
||||
|
||||
} else {
|
||||
|
||||
return loadedA ? 1 : - 1;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// If one is used and the other is not move the used one towards the end of the array
|
||||
return usedA ? 1 : - 1;
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// address corner cases where the minSize might be zero or smaller than maxSize - minSize,
|
||||
// which would result in a very small or no items being unloaded.
|
||||
const maxUnload = Math.max( minSize * unloadPercent, excessNodes * unloadPercent );
|
||||
const nodesToUnload = Math.ceil( Math.min( maxUnload, unused, excessNodes ) );
|
||||
const maxBytesUnload = Math.max( unloadPercent * excessBytes, unloadPercent * minBytesSize );
|
||||
const bytesToUnload = Math.min( maxBytesUnload, excessBytes );
|
||||
|
||||
let removedNodes = 0;
|
||||
let removedBytes = 0;
|
||||
|
||||
// evict up to the max node or bytes size, keeping one more item over the max bytes limit
|
||||
// so the "full" function behaves correctly.
|
||||
while (
|
||||
this.cachedBytes - removedBytes > maxBytesSize ||
|
||||
itemList.length - removedNodes > maxSize
|
||||
) {
|
||||
|
||||
const item = itemList[ removedNodes ];
|
||||
const bytes = bytesMap.get( item ) || 0;
|
||||
if (
|
||||
usedSet.has( item ) && loadedSet.has( item ) ||
|
||||
this.cachedBytes - removedBytes - bytes < maxBytesSize &&
|
||||
itemList.length - removedNodes <= maxSize
|
||||
) {
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
removedBytes += bytes;
|
||||
removedNodes ++;
|
||||
|
||||
}
|
||||
|
||||
// evict up to the min node or bytes size, keeping one more item over the min bytes limit
|
||||
// so we're meeting it
|
||||
while (
|
||||
removedBytes < bytesToUnload ||
|
||||
removedNodes < nodesToUnload
|
||||
) {
|
||||
|
||||
const item = itemList[ removedNodes ];
|
||||
const bytes = bytesMap.get( item ) || 0;
|
||||
if (
|
||||
usedSet.has( item ) ||
|
||||
this.cachedBytes - removedBytes - bytes < minBytesSize &&
|
||||
removedNodes >= nodesToUnload
|
||||
) {
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
removedBytes += bytes;
|
||||
removedNodes ++;
|
||||
|
||||
}
|
||||
|
||||
// remove the nodes
|
||||
itemList.splice( 0, removedNodes ).forEach( item => {
|
||||
|
||||
this.cachedBytes -= bytesMap.get( item ) || 0;
|
||||
|
||||
callbacks.get( item )( item );
|
||||
bytesMap.delete( item );
|
||||
itemSet.delete( item );
|
||||
callbacks.delete( item );
|
||||
loadedSet.delete( item );
|
||||
usedSet.delete( item );
|
||||
|
||||
} );
|
||||
|
||||
// if we didn't remove enough nodes or we still have excess bytes and there are nodes to removed
|
||||
// then we want to fire another round of unloading
|
||||
needsRerun = removedNodes < excessNodes || removedBytes < excessBytes && removedNodes < unused;
|
||||
needsRerun = needsRerun && removedNodes > 0;
|
||||
|
||||
}
|
||||
|
||||
if ( needsRerun ) {
|
||||
|
||||
this.unloadingHandle = requestAnimationFrame( () => this.scheduleUnload() );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
scheduleUnload() {
|
||||
|
||||
cancelAnimationFrame( this.unloadingHandle );
|
||||
|
||||
if ( ! this.scheduled ) {
|
||||
|
||||
this.scheduled = true;
|
||||
queueMicrotask( () => {
|
||||
|
||||
this.scheduled = false;
|
||||
this.unloadUnusedContent();
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { LRUCache };
|
||||
@@ -0,0 +1,49 @@
|
||||
export function readMagicBytes( bufferOrDataView ) {
|
||||
|
||||
if ( bufferOrDataView === null || bufferOrDataView.byteLength < 4 ) {
|
||||
|
||||
return '';
|
||||
|
||||
}
|
||||
|
||||
let view;
|
||||
if ( bufferOrDataView instanceof DataView ) {
|
||||
|
||||
view = bufferOrDataView;
|
||||
|
||||
} else {
|
||||
|
||||
view = new DataView( bufferOrDataView );
|
||||
|
||||
}
|
||||
|
||||
if ( String.fromCharCode( view.getUint8( 0 ) ) === '{' ) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
let magicBytes = '';
|
||||
for ( let i = 0; i < 4; i ++ ) {
|
||||
|
||||
magicBytes += String.fromCharCode( view.getUint8( i ) );
|
||||
|
||||
}
|
||||
|
||||
return magicBytes;
|
||||
|
||||
}
|
||||
|
||||
const utf8decoder = new TextDecoder();
|
||||
export function arrayToString( array ) {
|
||||
|
||||
return utf8decoder.decode( array );
|
||||
|
||||
}
|
||||
|
||||
// Returns a working path with a trailing slash
|
||||
export function getWorkingPath( url ) {
|
||||
|
||||
return url.replace( /[\\/][^\\/]+$/, '' ) + '/';
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export class PriorityQueue {
|
||||
|
||||
maxJobs : number;
|
||||
autoUpdate : boolean;
|
||||
priorityCallback : ( itemA : any, itemB : any ) => number;
|
||||
|
||||
schedulingCallback : ( func : Function ) => void;
|
||||
|
||||
sort() : void;
|
||||
add( item : any, callback : ( item : any ) => any ) : Promise< any >;
|
||||
remove( item : any ) : void;
|
||||
removeByFilter( filter : ( item : any ) => boolean ) : void;
|
||||
|
||||
tryRunJobs() : void;
|
||||
scheduleJobRun() : void;
|
||||
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
class PriorityQueue {
|
||||
|
||||
// returns whether tasks are queued or actively running
|
||||
get running() {
|
||||
|
||||
return this.items.length !== 0 || this.currJobs !== 0;
|
||||
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
||||
// options
|
||||
this.maxJobs = 6;
|
||||
|
||||
this.items = [];
|
||||
this.callbacks = new Map();
|
||||
this.currJobs = 0;
|
||||
this.scheduled = false;
|
||||
this.autoUpdate = true;
|
||||
|
||||
this.priorityCallback = null;
|
||||
|
||||
// Customizable scheduling callback. Default using requestAnimationFrame()
|
||||
this.schedulingCallback = func => {
|
||||
|
||||
requestAnimationFrame( func );
|
||||
|
||||
};
|
||||
|
||||
this._runjobs = () => {
|
||||
|
||||
this.scheduled = false;
|
||||
this.tryRunJobs();
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
sort() {
|
||||
|
||||
const priorityCallback = this.priorityCallback;
|
||||
const items = this.items;
|
||||
if ( priorityCallback !== null ) {
|
||||
|
||||
items.sort( priorityCallback );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
has( item ) {
|
||||
|
||||
return this.callbacks.has( item );
|
||||
|
||||
}
|
||||
|
||||
add( item, callback ) {
|
||||
|
||||
const data = {
|
||||
callback,
|
||||
reject: null,
|
||||
resolve: null,
|
||||
promise: null,
|
||||
};
|
||||
|
||||
data.promise = new Promise( ( resolve, reject ) => {
|
||||
|
||||
const items = this.items;
|
||||
const callbacks = this.callbacks;
|
||||
|
||||
data.resolve = resolve;
|
||||
data.reject = reject;
|
||||
|
||||
items.unshift( item );
|
||||
callbacks.set( item, data );
|
||||
|
||||
if ( this.autoUpdate ) {
|
||||
|
||||
this.scheduleJobRun();
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
return data.promise;
|
||||
|
||||
}
|
||||
|
||||
remove( item ) {
|
||||
|
||||
const items = this.items;
|
||||
const callbacks = this.callbacks;
|
||||
|
||||
const index = items.indexOf( item );
|
||||
if ( index !== - 1 ) {
|
||||
|
||||
// reject the promise to ensure there are no dangling promises - add a
|
||||
// catch here to handle the case where the promise was never used anywhere
|
||||
// else.
|
||||
const info = callbacks.get( item );
|
||||
info.promise.catch( () => {} );
|
||||
info.reject( new Error( 'PriorityQueue: Item removed.' ) );
|
||||
|
||||
items.splice( index, 1 );
|
||||
callbacks.delete( item );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
removeByFilter( filter ) {
|
||||
|
||||
const { items } = this;
|
||||
for ( let i = 0; i < items.length; i ++ ) {
|
||||
|
||||
const item = items[ i ];
|
||||
if ( filter( item ) ) {
|
||||
|
||||
this.remove( item );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tryRunJobs() {
|
||||
|
||||
this.sort();
|
||||
|
||||
const items = this.items;
|
||||
const callbacks = this.callbacks;
|
||||
const maxJobs = this.maxJobs;
|
||||
let iterated = 0;
|
||||
|
||||
const completedCallback = () => {
|
||||
|
||||
this.currJobs --;
|
||||
|
||||
if ( this.autoUpdate ) {
|
||||
|
||||
this.scheduleJobRun();
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
while ( maxJobs > this.currJobs && items.length > 0 && iterated < maxJobs ) {
|
||||
|
||||
this.currJobs ++;
|
||||
iterated ++;
|
||||
const item = items.pop();
|
||||
const { callback, resolve, reject } = callbacks.get( item );
|
||||
callbacks.delete( item );
|
||||
|
||||
let result;
|
||||
try {
|
||||
|
||||
result = callback( item );
|
||||
|
||||
} catch ( err ) {
|
||||
|
||||
reject( err );
|
||||
completedCallback();
|
||||
|
||||
}
|
||||
|
||||
if ( result instanceof Promise ) {
|
||||
|
||||
result
|
||||
.then( resolve )
|
||||
.catch( reject )
|
||||
.finally( completedCallback );
|
||||
|
||||
} else {
|
||||
|
||||
resolve( result );
|
||||
completedCallback();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
scheduleJobRun() {
|
||||
|
||||
if ( ! this.scheduled ) {
|
||||
|
||||
this.schedulingCallback( this._runjobs );
|
||||
|
||||
this.scheduled = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { PriorityQueue };
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// Helper function for traversing a tile set. If `beforeCb` returns `true` then the
|
||||
// traversal will end early.
|
||||
export function traverseSet( tile, beforeCb = null, afterCb = null ) {
|
||||
|
||||
const stack = [];
|
||||
|
||||
// A stack-based, depth-first traversal, storing
|
||||
// triplets (tile, parent, depth) in the stack array.
|
||||
|
||||
stack.push( tile );
|
||||
stack.push( null );
|
||||
stack.push( 0 );
|
||||
|
||||
while ( stack.length > 0 ) {
|
||||
|
||||
const depth = stack.pop();
|
||||
const parent = stack.pop();
|
||||
const tile = stack.pop();
|
||||
|
||||
if ( beforeCb && beforeCb( tile, parent, depth ) ) {
|
||||
|
||||
if ( afterCb ) {
|
||||
|
||||
afterCb( tile, parent, depth );
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
const children = tile.children;
|
||||
|
||||
// Children might be undefined if the tile has not been preprocessed yet
|
||||
if ( children ) {
|
||||
|
||||
for ( let i = children.length - 1; i >= 0; i -- ) {
|
||||
|
||||
stack.push( children[ i ] );
|
||||
stack.push( tile );
|
||||
stack.push( depth + 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( afterCb ) {
|
||||
|
||||
afterCb( tile, parent, depth );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Traverses the ancestry of the tile up to the root tile.
|
||||
export function traverseAncestors( tile, callback = null ) {
|
||||
|
||||
let current = tile;
|
||||
|
||||
while ( current ) {
|
||||
|
||||
const depth = current.__depth;
|
||||
const parent = current.parent;
|
||||
|
||||
if ( callback ) {
|
||||
|
||||
callback( current, parent, depth );
|
||||
|
||||
}
|
||||
|
||||
current = parent;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// function that rate limits the amount of time a function can be called to once
|
||||
// per frame, initially queuing a new call for the next frame.
|
||||
export function throttle( callback ) {
|
||||
|
||||
let handle = null;
|
||||
return () => {
|
||||
|
||||
if ( handle === null ) {
|
||||
|
||||
handle = requestAnimationFrame( () => {
|
||||
|
||||
handle = null;
|
||||
callback();
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Returns the file extension of the path component of a URL
|
||||
* @param {string} url
|
||||
* @returns {string} null if no extension found
|
||||
*/
|
||||
export function getUrlExtension( url ) {
|
||||
|
||||
if ( ! url ) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
// Find the last occurrence of '?' and '#' to handle query params and fragments
|
||||
let endIndex = url.length;
|
||||
const queryIndex = url.indexOf( '?' );
|
||||
const fragmentIndex = url.indexOf( '#' );
|
||||
if ( queryIndex !== - 1 ) {
|
||||
|
||||
endIndex = Math.min( endIndex, queryIndex );
|
||||
|
||||
}
|
||||
|
||||
if ( fragmentIndex !== - 1 ) {
|
||||
|
||||
endIndex = Math.min( endIndex, fragmentIndex );
|
||||
|
||||
}
|
||||
|
||||
// Check if the string is just a hostname or whether the path does not end in an extension
|
||||
const lastPeriodIndex = url.lastIndexOf( '.', endIndex );
|
||||
const lastSlashIndex = url.lastIndexOf( '/', endIndex );
|
||||
const protocolIndex = url.indexOf( '://' );
|
||||
const isHostOnly = protocolIndex !== - 1 && protocolIndex + 2 === lastSlashIndex;
|
||||
if ( isHostOnly || lastPeriodIndex === - 1 || lastPeriodIndex < lastSlashIndex ) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
return url.substring( lastPeriodIndex + 1, endIndex ) || null;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '3d-tiles-renderer/core';
|
||||
export * from '3d-tiles-renderer/three';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '3d-tiles-renderer/core';
|
||||
export * from '3d-tiles-renderer/three';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '3d-tiles-renderer/core/plugins';
|
||||
export * from '3d-tiles-renderer/three/plugins';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '3d-tiles-renderer/core/plugins';
|
||||
export * from '3d-tiles-renderer/three/plugins';
|
||||
@@ -0,0 +1,250 @@
|
||||
# 3D Tiles React Components
|
||||
|
||||
Set of components for loading and rendering 3D Tiles in [@react-three/fiber](https://r3f.docs.pmnd.rs/).
|
||||
|
||||
**Examples**
|
||||
|
||||
[Basic example](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/r3f/basic.html)
|
||||
|
||||
[Cesium Ion example](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/r3f/ion.html)
|
||||
|
||||
[Google Photorealistic Tiles example](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/r3f/globe.html)
|
||||
|
||||
# Use
|
||||
|
||||
## Simple
|
||||
|
||||
```jsx
|
||||
import { TilesRenderer } from '3d-tiles-renderer/r3f';
|
||||
|
||||
const TILESET_URL = /* your tile set url */;
|
||||
const cameraPosition = [ x, y, z ]; // Set the camera position so the tiles are visible
|
||||
export default function App() {
|
||||
return (
|
||||
<Canvas camera={ { position: cameraPosition } }>
|
||||
<TilesRenderer url={ TILESET_URL } />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## With Plugins, Controls, & Attribution
|
||||
|
||||
Basic set up for Google Photorealistic tiles, Globe controls, and an overlay for displaying data set attributions.
|
||||
|
||||
```jsx
|
||||
import { TilesRenderer, TilesPlugin, GlobeControls, TilesAttributionOverlay } from '3d-tiles-renderer/r3f';
|
||||
import { DebugTilesPlugin, GoogleCloudAuthPlugin } from '3d-tiles-renderer/plugins';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Canvas camera={ { position: [ 0, 0, 1e8 ] } }>
|
||||
<TilesRenderer>
|
||||
<TilesPlugin plugin={ DebugTilesPlugin } displayBoxBounds={ true } />
|
||||
<TilesPlugin plugin={ GoogleCloudAuthPlugin } args={ { apiToken: /* your api token here */ } } />
|
||||
<GlobeControls />
|
||||
<TilesAttributionOverlay />
|
||||
</TilesRenderer>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Cesium Ion & Google Cloud
|
||||
|
||||
Simplified wrappers for using the TilesRenderer with Cesium Ion and Google Cloud for Photorealistic Tiles. Use the `TilesAttributionOverlay` to display appropriate credits for the data sets.
|
||||
|
||||
```jsx
|
||||
import { TilesRenderer, TilesPlugin } from '3d-tiles-renderer/r3f';
|
||||
import { CesiumIonAuthPlugin, GoogleCloudAuthPlugin } from '3d-tiles-renderer/plugins';
|
||||
|
||||
function GoogleTilesRenderer( { children, apiToken, ...rest } ) {
|
||||
return (
|
||||
<TilesRenderer { ...rest } key={ apiToken }>
|
||||
<TilesPlugin plugin={ GoogleCloudAuthPlugin } args={ { apiToken } } />
|
||||
{ children }
|
||||
</TilesRenderer>
|
||||
);
|
||||
}
|
||||
|
||||
function CesiumIonTilesRenderer( { children, apiToken, assetId, ...rest } ) {
|
||||
return (
|
||||
<TilesRenderer { ...rest } key={ apiToken + assetId }>
|
||||
<TilesPlugin plugin={ CesiumIonAuthPlugin } args={ { apiToken, assetId } } />
|
||||
{ children }
|
||||
</TilesRenderer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
# Components
|
||||
|
||||
## TilesRenderer
|
||||
|
||||
Wrapper for the three.js `TilesRenderer` class. Listening for events are specified with a camel-case property prefixed with `on`, such as `onLoadModel`, and all other properties are specified as individual properties with dashes being used to indicate nested properties. For example, `lruCache-minSize` is used to set `lruCache.minSize`.
|
||||
|
||||
```jsx
|
||||
<TilesRenderer
|
||||
url={ tilesetUrl }
|
||||
|
||||
// if false then "update" is not called
|
||||
enabled={ true }
|
||||
|
||||
// pass properties to apply to the tile set root object
|
||||
group={ {
|
||||
position: [ 0, 10, 0 ],
|
||||
rotation: [ Math.PI / 2, 0, 0 ],
|
||||
} }
|
||||
|
||||
// set options to the TilesRenderer object
|
||||
errorTarget={ 6 }
|
||||
errorThreshold={ 10 }
|
||||
|
||||
// set nested object options of the TilesRenderer
|
||||
parseQueue-maxJobs={ 30 }
|
||||
downloadQueue-maxJobs={ 10 }
|
||||
lruCache-minBytesSize={ 0.25 * 1e6 }
|
||||
lruCache-maxBytesSize={ 0.5 * 1e6 }
|
||||
|
||||
// event registration
|
||||
onLoadTileSet={ onLoadTileSetCallback }
|
||||
onLoadModel={ onLoadModelCallback }
|
||||
/>
|
||||
```
|
||||
|
||||
## TilesPlugin
|
||||
|
||||
Plugins can be set as children of the TilesRenderer component to add additional functionality. TilePlugin components must be nested inside a TilesRenderer component. Constructor arguments are passed via the `args` parameter while local members can be passed via the regular properties. But note that depending on the plugin some properties cannot be changed after construction and initialization.
|
||||
|
||||
See the [PLUGINS documentation](https://github.com/NASA-AMMOS/3DTilesRendererJS/blob/master/PLUGINS.md) for docs on all avilable plugins.
|
||||
|
||||
```jsx
|
||||
<TilesRenderer url={ tilesetUrl }>
|
||||
<TilesPlugin
|
||||
plugin={ PluginClassName }
|
||||
args={ /* constructor arguments as array or object */ }
|
||||
{ ...pluginProps }
|
||||
/>
|
||||
</TilesRenderer>
|
||||
```
|
||||
|
||||
And a practical example of creating and using a plugin:
|
||||
|
||||
```jsx
|
||||
<TilesRenderer url={ tilesetUrl } >
|
||||
<TilesPlugin plugin={ GLTFExtensionsPlugin }
|
||||
dracoLoader={ dracoLoader }
|
||||
ktxLoader={ ktx2Loader }
|
||||
autoDispose={ false }
|
||||
{ /*
|
||||
// alternatively the options can be passed via constructor arguments
|
||||
// or a mix of both can be used.
|
||||
args = { {
|
||||
dracoLoader,
|
||||
ktxLoader,
|
||||
autoDispose: false,
|
||||
} }
|
||||
*/ }
|
||||
/>
|
||||
</TilesRenderer>
|
||||
```
|
||||
|
||||
## Controls
|
||||
|
||||
These `EnvironmentControls` and `GlobeControls` classes have been wrapped as components to handle user-interaction. They will both be set to the `controls` react three fiber state field when in use. All properties on the original classes can be passed as properties:
|
||||
|
||||
```jsx
|
||||
<>
|
||||
<TilesRenderer url={ url } { ...props } />
|
||||
<EnvironmentControls enableDamping={ true } enabled={ true } />
|
||||
</>
|
||||
```
|
||||
|
||||
The `GlobeControls` component must be set as a child of the `TilesRenderer` component that is providing the ellipsoid to orbit around.
|
||||
|
||||
```jsx
|
||||
<TilesRenderer url={ url } { ...props }>
|
||||
<GlobeControls enableDamping={ true } />
|
||||
</TilesRenderer>
|
||||
```
|
||||
|
||||
## EastNorthUpFrame
|
||||
|
||||
The `EastNorthUpFrame` creates a root object that is centered on the provided point relative to the tile sets ellipsoid, specified via lat/lon/height and euler angle props and is used to place 3D objects relative to that point. It does not rotate the original tile set and must be a child of a `TilesRenderer` component.
|
||||
|
||||
It can be used to place markers on the surface of the ellipsoid, such as a cone for pointing to a location:
|
||||
|
||||
```jsx
|
||||
<TilesRenderer url={ url } { ...props }>
|
||||
{ /* ... */ }
|
||||
<EastNorthUpFrame
|
||||
{/* The latitude and longitude to place the frame at in radians */}
|
||||
lat={ lat }
|
||||
lon={ lon }
|
||||
|
||||
{/* The height above the ellipsoid to place the frame at in meters */}
|
||||
height={ 100 }
|
||||
|
||||
{/*
|
||||
The azimuth, elevation, and roll around the "north" axis, applied
|
||||
in that order intrinsicly, in radians
|
||||
*/}
|
||||
az={ 0 }
|
||||
el={ 0 }
|
||||
roll={ 0 }
|
||||
>
|
||||
{/* Children are position relative to the east, north, up frame */}
|
||||
<mesh rotation-x={ - Math.PI / 2 } scale={ 100 } position-z={ 50 }>
|
||||
<coneGeometry args={ [ 0.5 ] } />
|
||||
<meshStandardMaterial color={ 'red' } />
|
||||
</mesh>
|
||||
</EastNorthUpFrame>
|
||||
</TilesRenderer>
|
||||
```
|
||||
|
||||
## TilesAttributionOverlay
|
||||
|
||||
The `TilesAttributionOverlay` component must be embedded in a tile set and will automatically display the credits associated with the loaded data set.
|
||||
|
||||
```jsx
|
||||
<TilesRenderer url={ url } { ...props }>
|
||||
<TilesAttributionOverlay
|
||||
|
||||
{ /*
|
||||
Callback function for generating attribution elements from credit info.
|
||||
Takes the list of attributions and a unique "id" assigned to the overlay dom element.
|
||||
*/ }
|
||||
generateAttributions={ null }
|
||||
|
||||
{ /* remaining properties are assigned to the root overlay element */ }
|
||||
/>
|
||||
</TilesRenderer>
|
||||
```
|
||||
|
||||
## CompassGizmo
|
||||
|
||||
Adds a compass to the bottom right of the page that orients to "north" based on the camera position and orientation. Must be nested in a `TilesRenderer` component.
|
||||
|
||||
Any children passed into the class will replace the default red and white compass design with +Y pointing north and +X pointing east. The graphic children should fit within a volume from - 0.5 to 0.5 along all axes.
|
||||
|
||||
```jsx
|
||||
<CompassGizmo
|
||||
{/* Specifies whether the compass will render in '2d' or '3d' */}
|
||||
mode={ '3d' }
|
||||
|
||||
{/* The size of the compass in pixels */}
|
||||
scale={ 35 }
|
||||
|
||||
{/* The number pixels in margin to add relative to the bottom right of the screen */}
|
||||
margin={ 10 }
|
||||
|
||||
{/* Whether to render the main scene */}
|
||||
overrideRenderLoop={ true }
|
||||
|
||||
{/* Whether the gizmo is visible and rendering */}
|
||||
visible={ true }
|
||||
|
||||
{/* Any remaining props including click events are passed through to the parent group */}
|
||||
onClick={ () => console.log( 'compass clicked!' ) }
|
||||
/>
|
||||
```
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import type { ForwardRefExoticComponent, RefAttributes } from 'react';
|
||||
import type { Camera, Object3D } from 'three';
|
||||
import type { EnvironmentControls as EnvironmentControlsImpl, GlobeControls as GlobeControlsImpl } from '3d-tiles-renderer/three';
|
||||
import type { TilesRenderer } from './TilesRenderer.jsx';
|
||||
|
||||
interface ControlsBaseProps {
|
||||
domElement?: HTMLCanvasElement | null;
|
||||
scene?: Object3D | null;
|
||||
camera?: Camera | null;
|
||||
tilesRenderer?: typeof TilesRenderer | null;
|
||||
}
|
||||
|
||||
type EnvironmentControlsProps = Partial<
|
||||
InstanceType<typeof EnvironmentControlsImpl>
|
||||
> &
|
||||
ControlsBaseProps;
|
||||
|
||||
type GlobeControlsProps = Partial<InstanceType<typeof GlobeControlsImpl>> &
|
||||
ControlsBaseProps;
|
||||
|
||||
export declare const EnvironmentControls: ForwardRefExoticComponent<
|
||||
EnvironmentControlsProps & RefAttributes<EnvironmentControlsImpl>
|
||||
>;
|
||||
|
||||
export declare const GlobeControls: ForwardRefExoticComponent<
|
||||
GlobeControlsProps & RefAttributes<GlobeControlsImpl>
|
||||
>;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { forwardRef, useMemo, useEffect, useContext } from 'react';
|
||||
import { useThree, useFrame } from '@react-three/fiber';
|
||||
import { EnvironmentControls as EnvironmentControlsImpl, GlobeControls as GlobeControlsImpl } from '3d-tiles-renderer/three';
|
||||
import { useShallowOptions } from '../utilities/useOptions.js';
|
||||
import { TilesRendererContext } from './TilesRenderer.jsx';
|
||||
import { useApplyRefs } from '../utilities/useApplyRefs.js';
|
||||
|
||||
// Add a base component implementation for both EnvironmentControls and GlobeControls
|
||||
const ControlsBaseComponent = forwardRef( function ControlsBaseComponent( props, ref ) {
|
||||
|
||||
const { controlsConstructor, domElement, scene, camera, ellipsoid, ellipsoidFrame, tilesRenderer, ...rest } = props;
|
||||
|
||||
const [ defaultCamera ] = useThree( state => [ state.camera ] );
|
||||
const [ gl ] = useThree( state => [ state.gl ] );
|
||||
const [ defaultScene ] = useThree( state => [ state.scene ] );
|
||||
const [ invalidate ] = useThree( state => [ state.invalidate ] );
|
||||
const [ get ] = useThree( state => [ state.get ] );
|
||||
const [ set ] = useThree( state => [ state.set ] );
|
||||
|
||||
const contextTilesRenderer = useContext( TilesRendererContext );
|
||||
const appliedTilesRenderer = tilesRenderer || contextTilesRenderer;
|
||||
const appliedCamera = camera || defaultCamera || null;
|
||||
const appliedScene = scene || defaultScene || null;
|
||||
const appliedDomElement = domElement || gl.domElement || null;
|
||||
const appliedEllipsoid = ellipsoid || appliedTilesRenderer?.ellipsoid || null;
|
||||
const appliedEllipsoidFrame = ellipsoidFrame || appliedTilesRenderer?.group || null;
|
||||
|
||||
// create a controls instance
|
||||
const controls = useMemo( () => {
|
||||
|
||||
return new controlsConstructor();
|
||||
|
||||
}, [ controlsConstructor ] );
|
||||
|
||||
// assign / call the reference
|
||||
useApplyRefs( controls, ref );
|
||||
|
||||
// fire invalidate callbacks
|
||||
useEffect( () => {
|
||||
|
||||
const callback = () => invalidate();
|
||||
controls.addEventListener( 'change', callback );
|
||||
controls.addEventListener( 'start', callback );
|
||||
controls.addEventListener( 'end', callback );
|
||||
return () => {
|
||||
|
||||
controls.removeEventListener( 'change', callback );
|
||||
controls.removeEventListener( 'start', callback );
|
||||
controls.removeEventListener( 'end', callback );
|
||||
|
||||
};
|
||||
|
||||
}, [ controls, invalidate ] );
|
||||
|
||||
// assign the camera
|
||||
useEffect( () => {
|
||||
|
||||
controls.setCamera( appliedCamera );
|
||||
|
||||
}, [ controls, appliedCamera ] );
|
||||
|
||||
// assign the scene
|
||||
useEffect( () => {
|
||||
|
||||
controls.setScene( appliedScene );
|
||||
|
||||
}, [ controls, appliedScene ] );
|
||||
|
||||
// assign the tiles renderer
|
||||
useEffect( () => {
|
||||
|
||||
if ( controls.isGlobeControls ) {
|
||||
|
||||
controls.setEllipsoid( appliedEllipsoid, appliedEllipsoidFrame );
|
||||
|
||||
}
|
||||
|
||||
}, [ controls, appliedEllipsoid, appliedEllipsoidFrame ] );
|
||||
|
||||
// attach to the dom element
|
||||
useEffect( () => {
|
||||
|
||||
controls.attach( appliedDomElement );
|
||||
return () => {
|
||||
|
||||
controls.detach();
|
||||
|
||||
};
|
||||
|
||||
}, [ controls, appliedDomElement ] );
|
||||
|
||||
// set the controls for global use
|
||||
useEffect( () => {
|
||||
|
||||
const old = get().controls;
|
||||
set( { controls } );
|
||||
return () => set( { controls: old } );
|
||||
|
||||
}, [ controls, get, set ] );
|
||||
|
||||
// update the controls with a priority of - 1 so it happens before tiles renderer update
|
||||
useFrame( () => {
|
||||
|
||||
controls.update();
|
||||
|
||||
}, - 1 );
|
||||
|
||||
useShallowOptions( controls, rest );
|
||||
|
||||
} );
|
||||
|
||||
export const EnvironmentControls = forwardRef( function EnvironmentControls( props, ref ) {
|
||||
|
||||
return <ControlsBaseComponent { ...props } ref={ ref } controlsConstructor={ EnvironmentControlsImpl } />;
|
||||
|
||||
} );
|
||||
|
||||
export const GlobeControls = forwardRef( function GlobeControls( props, ref ) {
|
||||
|
||||
return <ControlsBaseComponent { ...props } ref={ ref } controlsConstructor={ GlobeControlsImpl } />;
|
||||
|
||||
} );
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Camera } from 'three';
|
||||
import { CameraTransitionManager } from '3d-tiles-renderer/three';
|
||||
|
||||
interface CameraTransitionProps {
|
||||
mode?: 'perspective' | 'orthographic';
|
||||
onBeforeToggle?: ( manager: CameraTransitionManager, targetCamera: Camera ) => void;
|
||||
perspectiveCamera?: Camera;
|
||||
orthographicCamera?: Camera;
|
||||
}
|
||||
|
||||
export declare const CameraTransition: React.ForwardRefExoticComponent<
|
||||
CameraTransitionProps & React.RefAttributes<CameraTransitionManager>
|
||||
>;
|
||||
@@ -0,0 +1,194 @@
|
||||
import { forwardRef, useEffect, useMemo } from 'react';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { CameraTransitionManager } from '3d-tiles-renderer/three';
|
||||
import { useDeepOptions } from '../utilities/useOptions.js';
|
||||
import { useApplyRefs } from '../utilities/useApplyRefs.js';
|
||||
|
||||
export const CameraTransition = forwardRef( function CameraTransition( props, ref ) {
|
||||
|
||||
const {
|
||||
mode = 'perspective',
|
||||
onBeforeToggle,
|
||||
perspectiveCamera,
|
||||
orthographicCamera,
|
||||
...options
|
||||
} = props;
|
||||
|
||||
const [ set, get, invalidate, controls, camera, size ] = useThree( state => [ state.set, state.get, state.invalidate, state.controls, state.camera, state.size ] );
|
||||
|
||||
// create the manager
|
||||
const manager = useMemo( () => {
|
||||
|
||||
const manager = new CameraTransitionManager();
|
||||
manager.autoSync = false;
|
||||
|
||||
if ( camera.isOrthographicCamera ) {
|
||||
|
||||
manager.orthographicCamera.copy( camera );
|
||||
manager.mode = 'orthographic';
|
||||
|
||||
} else {
|
||||
|
||||
manager.perspectiveCamera.copy( camera );
|
||||
|
||||
}
|
||||
|
||||
manager.syncCameras();
|
||||
manager.mode = mode;
|
||||
|
||||
return manager;
|
||||
|
||||
// only respect the camera initially so the default camera settings are automatically used
|
||||
|
||||
}, [] ); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
const { perspectiveCamera, orthographicCamera } = manager;
|
||||
const aspect = size.width / size.height;
|
||||
perspectiveCamera.aspect = aspect;
|
||||
perspectiveCamera.updateProjectionMatrix();
|
||||
|
||||
orthographicCamera.left = - orthographicCamera.top * aspect;
|
||||
orthographicCamera.right = - orthographicCamera.left;
|
||||
perspectiveCamera.updateProjectionMatrix();
|
||||
|
||||
}, [ manager, size ] );
|
||||
|
||||
// assign ref
|
||||
useApplyRefs( manager, ref );
|
||||
|
||||
// set the camera
|
||||
useEffect( () => {
|
||||
|
||||
const cameraCallback = ( { camera } ) => {
|
||||
|
||||
set( () => ( { camera } ) );
|
||||
|
||||
};
|
||||
|
||||
set( () => ( { camera: manager.camera } ) );
|
||||
manager.addEventListener( 'camera-change', cameraCallback );
|
||||
return () => {
|
||||
|
||||
manager.removeEventListener( 'camera-change', cameraCallback );
|
||||
|
||||
};
|
||||
|
||||
}, [ manager, set ] );
|
||||
|
||||
// assign cameras
|
||||
useEffect( () => {
|
||||
|
||||
const oldPerspectiveCamera = manager.perspectiveCamera;
|
||||
const oldOrthographicCamera = manager.orthographicCamera;
|
||||
manager.perspectiveCamera = perspectiveCamera || oldPerspectiveCamera;
|
||||
manager.orthographicCamera = orthographicCamera || oldOrthographicCamera;
|
||||
|
||||
set( () => ( { camera: manager.camera } ) );
|
||||
|
||||
return () => {
|
||||
|
||||
manager.perspectiveCamera = oldPerspectiveCamera;
|
||||
manager.orthographicCamera = oldOrthographicCamera;
|
||||
|
||||
};
|
||||
|
||||
}, [ perspectiveCamera, orthographicCamera, manager, set ] );
|
||||
|
||||
// toggle
|
||||
useEffect( () => {
|
||||
|
||||
if ( mode !== manager.mode ) {
|
||||
|
||||
// calculate the camera being toggled to. Because "toggle" has not yet been
|
||||
// called this will select the camera that is being transitioned to.
|
||||
const targetCamera = mode === 'orthographic' ? manager.orthographicCamera : manager.perspectiveCamera;
|
||||
if ( onBeforeToggle ) {
|
||||
|
||||
onBeforeToggle( manager, targetCamera );
|
||||
|
||||
} else if ( controls && controls.isEnvironmentControls ) {
|
||||
|
||||
controls.getPivotPoint( manager.fixedPoint );
|
||||
manager.syncCameras();
|
||||
|
||||
controls.adjustCamera( manager.perspectiveCamera );
|
||||
controls.adjustCamera( manager.orthographicCamera );
|
||||
|
||||
} else {
|
||||
|
||||
manager.fixedPoint
|
||||
.set( 0, 0, - 1 )
|
||||
.transformDirection( manager.camera.matrixWorld )
|
||||
.multiplyScalar( 50 )
|
||||
.add( manager.camera.position );
|
||||
manager.syncCameras();
|
||||
|
||||
}
|
||||
|
||||
manager.toggle();
|
||||
invalidate();
|
||||
|
||||
}
|
||||
|
||||
}, [ mode, manager, invalidate, controls, onBeforeToggle ] );
|
||||
|
||||
// rerender the frame when the transition animates
|
||||
useEffect( () => {
|
||||
|
||||
const callback = () => invalidate();
|
||||
manager.addEventListener( 'transition-start', callback );
|
||||
manager.addEventListener( 'change', callback );
|
||||
manager.addEventListener( 'transition-end', callback );
|
||||
|
||||
return () => {
|
||||
|
||||
manager.removeEventListener( 'transition-start', callback );
|
||||
manager.removeEventListener( 'change', callback );
|
||||
manager.removeEventListener( 'transition-end', callback );
|
||||
|
||||
};
|
||||
|
||||
}, [ manager, invalidate ] );
|
||||
|
||||
useDeepOptions( manager, options );
|
||||
|
||||
// update animation
|
||||
useFrame( () => {
|
||||
|
||||
manager.update();
|
||||
if ( controls ) {
|
||||
|
||||
controls.enabled = ! manager.animating;
|
||||
|
||||
}
|
||||
|
||||
// ensure the orthographic camera size is resized correctly if the user is not
|
||||
// providing their own camera.
|
||||
const { camera, size } = get();
|
||||
if ( ! orthographicCamera && camera === manager.orthographicCamera ) {
|
||||
|
||||
const aspect = size.width / size.height;
|
||||
const camera = manager.orthographicCamera;
|
||||
if ( aspect !== camera.right ) {
|
||||
|
||||
camera.bottom = - 1;
|
||||
camera.top = 1;
|
||||
camera.left = - aspect;
|
||||
camera.right = aspect;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( manager.animating ) {
|
||||
|
||||
invalidate();
|
||||
|
||||
}
|
||||
|
||||
}, - 1 );
|
||||
|
||||
} );
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
export interface CanvasDOMOverlayProps extends ComponentPropsWithoutRef<'div'> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useMemo, useEffect, StrictMode, forwardRef, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
|
||||
// Utility class for overlaying dom elements on top of the canvas
|
||||
export const CanvasDOMOverlay = forwardRef( function CanvasDOMOverlay( { children, ...rest }, ref ) {
|
||||
|
||||
// create the dom element and react root
|
||||
const [ gl ] = useThree( state => [ state.gl ] );
|
||||
const [ root, setRoot ] = useState( null );
|
||||
const container = useMemo( () => document.createElement( 'div' ), [] );
|
||||
|
||||
// position the container
|
||||
useEffect( () => {
|
||||
|
||||
container.style.pointerEvents = 'none';
|
||||
container.style.position = 'absolute';
|
||||
container.style.width = '100%';
|
||||
container.style.height = '100%';
|
||||
container.style.left = 0;
|
||||
container.style.top = 0;
|
||||
gl.domElement.parentNode.appendChild( container );
|
||||
|
||||
return () => {
|
||||
|
||||
container.remove();
|
||||
|
||||
};
|
||||
|
||||
}, [ container, gl.domElement.parentNode ] );
|
||||
|
||||
// create the react render root
|
||||
useEffect( () => {
|
||||
|
||||
const root = createRoot( container );
|
||||
setRoot( root );
|
||||
return () => {
|
||||
|
||||
root.unmount();
|
||||
|
||||
};
|
||||
|
||||
}, [ container ] );
|
||||
|
||||
// render the children into the container
|
||||
if ( root !== null ) {
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<div { ...rest } ref={ ref }>
|
||||
{ children }
|
||||
</div>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import type {
|
||||
ReactNode,
|
||||
ForwardRefExoticComponent,
|
||||
RefAttributes,
|
||||
} from 'react';
|
||||
import type { Group } from 'three';
|
||||
|
||||
interface CompassGizmoProps {
|
||||
children?: ReactNode;
|
||||
mode?: '3d' | '2d';
|
||||
visible?: boolean;
|
||||
scale?: number;
|
||||
margin?: number | [number, number];
|
||||
overrideRenderLoop?: boolean;
|
||||
}
|
||||
|
||||
export declare const CompassGizmo: ForwardRefExoticComponent<
|
||||
CompassGizmoProps & RefAttributes<Group>
|
||||
>;
|
||||
@@ -0,0 +1,268 @@
|
||||
import { createPortal, useFrame, useThree } from '@react-three/fiber';
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BackSide, Matrix4, OrthographicCamera, Ray, Scene, Vector3 } from 'three';
|
||||
import { TilesRendererContext } from './TilesRenderer.jsx';
|
||||
|
||||
// Based in part on @pmndrs/drei's Gizmo component
|
||||
|
||||
const _vec = /*@__PURE__*/ new Vector3();
|
||||
const _axis = /*@__PURE__*/ new Vector3();
|
||||
const _pos = /*@__PURE__*/ new Vector3();
|
||||
const _matrix = /*@__PURE__*/ new Matrix4();
|
||||
const _enuMatrix = /*@__PURE__*/ new Matrix4();
|
||||
const _ray = /*@__PURE__*/ new Ray();
|
||||
const _cart = {};
|
||||
|
||||
// Returns the "focus" point that the camera is facing based on the closest point to the ellipsoid.
|
||||
// Used for determining the compass orientation.
|
||||
function getCameraFocusPoint( camera, ellipsoid, tilesGroup, target ) {
|
||||
|
||||
// get ray in globe coordinate frame
|
||||
_ray.origin.copy( camera.position );
|
||||
_ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
|
||||
_ray.applyMatrix4( tilesGroup.matrixWorldInverse );
|
||||
|
||||
// get the closest point to the ray on the globe in the global coordinate frame
|
||||
ellipsoid.closestPointToRayEstimate( _ray, _pos );
|
||||
_pos.applyMatrix4( tilesGroup.matrixWorld );
|
||||
|
||||
// get ortho camera info
|
||||
_axis.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
|
||||
|
||||
// ensure we move the camera exactly along the forward vector to avoid shifting
|
||||
// the camera in other directions due to floating point error
|
||||
const dist = _pos.sub( camera.position ).dot( _axis );
|
||||
target.copy( camera.position ).addScaledVector( _axis, dist );
|
||||
return target;
|
||||
|
||||
}
|
||||
|
||||
// Renders the portal with an orthographic camera
|
||||
function RenderPortal( props ) {
|
||||
|
||||
const { defaultScene, defaultCamera, overrideRenderLoop = true, renderPriority = 1 } = props;
|
||||
const camera = useMemo( () => new OrthographicCamera(), [] );
|
||||
const [ set, size, gl, scene ] = useThree( state => [ state.set, state.size, state.gl, state.scene ] );
|
||||
useEffect( () => {
|
||||
|
||||
set( { camera } );
|
||||
|
||||
}, [ set, camera ] );
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
camera.left = - size.width / 2;
|
||||
camera.right = size.width / 2;
|
||||
camera.top = size.height / 2;
|
||||
camera.bottom = - size.height / 2;
|
||||
camera.near = 0;
|
||||
camera.far = 2000;
|
||||
camera.position.z = camera.far / 2;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
}, [ camera, size ] );
|
||||
|
||||
useFrame( () => {
|
||||
|
||||
if ( overrideRenderLoop ) {
|
||||
|
||||
gl.render( defaultScene, defaultCamera );
|
||||
|
||||
}
|
||||
|
||||
const currentAutoClear = gl.autoClear;
|
||||
gl.autoClear = false;
|
||||
|
||||
gl.clearDepth();
|
||||
gl.render( scene, camera );
|
||||
|
||||
gl.autoClear = currentAutoClear;
|
||||
|
||||
}, renderPriority );
|
||||
|
||||
}
|
||||
|
||||
// generates an extruded box geometry
|
||||
function TriangleGeometry() {
|
||||
|
||||
const ref = useRef();
|
||||
useEffect( () => {
|
||||
|
||||
const geometry = ref.current;
|
||||
const position = geometry.attributes.position;
|
||||
for ( let i = 0, l = position.count; i < l; i ++ ) {
|
||||
|
||||
_vec.fromBufferAttribute( position, i );
|
||||
if ( _vec.y > 0 ) {
|
||||
|
||||
_vec.x = 0;
|
||||
position.setXYZ( i, ..._vec );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
return <boxGeometry ref={ ref } />;
|
||||
|
||||
}
|
||||
|
||||
// renders a typical compass graphic with red north triangle, white south, and a tinted circular background
|
||||
function CompassGraphic( { northColor = 0xEF5350, southColor = 0xFFFFFF } ) {
|
||||
|
||||
const [ lightTarget, setLightTarget ] = useState();
|
||||
const groupRef = useRef();
|
||||
useEffect( () => {
|
||||
|
||||
setLightTarget( groupRef.current );
|
||||
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<group scale={ 0.5 } ref={ groupRef }>
|
||||
|
||||
{/* Lights */}
|
||||
<ambientLight intensity={ 1 } />
|
||||
<directionalLight position={ [ 0, 2, 3 ] } intensity={ 3 } target={ lightTarget } />
|
||||
<directionalLight position={ [ 0, - 2, - 3 ] } intensity={ 3 } target={ lightTarget } />
|
||||
|
||||
{/* Background */}
|
||||
<mesh>
|
||||
<sphereGeometry />
|
||||
<meshBasicMaterial color={ 0 } opacity={ 0.3 } transparent={ true } side={ BackSide } />
|
||||
</mesh>
|
||||
|
||||
{/* Compass shape */}
|
||||
<group scale={ [ 0.5, 1, 0.15 ] }>
|
||||
<mesh position-y={ 0.5 }>
|
||||
<TriangleGeometry />
|
||||
<meshStandardMaterial color={ northColor } />
|
||||
</mesh>
|
||||
<mesh position-y={ - 0.5 } rotation-x={ Math.PI }>
|
||||
<TriangleGeometry />
|
||||
<meshStandardMaterial color={ southColor } />
|
||||
</mesh>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export function CompassGizmo( { children, overrideRenderLoop, mode = '3d', margin = 10, scale = 35, visible = true, ...rest } ) {
|
||||
|
||||
const [ defaultCamera, defaultScene, size ] = useThree( state => [ state.camera, state.scene, state.size ] );
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const groupRef = useRef( null );
|
||||
const scene = useMemo( () => {
|
||||
|
||||
return new Scene();
|
||||
|
||||
}, [] );
|
||||
|
||||
let marginX, marginY;
|
||||
if ( Array.isArray( margin ) ) {
|
||||
|
||||
marginX = margin[ 0 ];
|
||||
marginY = margin[ 1 ];
|
||||
|
||||
} else {
|
||||
|
||||
marginX = margin;
|
||||
marginY = margin;
|
||||
|
||||
}
|
||||
|
||||
useFrame( () => {
|
||||
|
||||
if ( tiles === null || groupRef.current === null ) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
const { ellipsoid } = tiles;
|
||||
const group = groupRef.current;
|
||||
|
||||
// get the ENU frame in world space
|
||||
getCameraFocusPoint( defaultCamera, ellipsoid, tiles.group, _pos ).applyMatrix4( tiles.group.matrixWorldInverse );
|
||||
ellipsoid.getPositionToCartographic( _pos, _cart );
|
||||
|
||||
ellipsoid
|
||||
.getEastNorthUpFrame( _cart.lat, _cart.lon, 0, _enuMatrix )
|
||||
.premultiply( tiles.group.matrixWorld );
|
||||
|
||||
// get the camera orientation in the local ENU frame
|
||||
_enuMatrix.invert();
|
||||
_matrix.copy( defaultCamera.matrixWorld ).premultiply( _enuMatrix );
|
||||
|
||||
if ( mode.toLowerCase() === '3d' ) {
|
||||
|
||||
group.quaternion.setFromRotationMatrix( _matrix ).invert();
|
||||
|
||||
} else {
|
||||
|
||||
// get the projected facing direction of the camera
|
||||
_vec.set( 0, 1, 0 ).transformDirection( _matrix ).normalize();
|
||||
_vec.z = 0;
|
||||
_vec.normalize();
|
||||
|
||||
if ( _vec.length() === 0 ) {
|
||||
|
||||
// if we're looking exactly top-down
|
||||
group.quaternion.identity();
|
||||
|
||||
} else {
|
||||
|
||||
// compute the 2d looking direction
|
||||
const angle = _axis.set( 0, 1, 0 ).angleTo( _vec );
|
||||
_axis.cross( _vec ).normalize();
|
||||
group.quaternion.setFromAxisAngle( _axis, - angle );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// default to the compass graphic
|
||||
if ( ! children ) {
|
||||
|
||||
children = <CompassGraphic />;
|
||||
|
||||
}
|
||||
|
||||
// remove the portal rendering if not present
|
||||
if ( ! visible ) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
createPortal(
|
||||
<>
|
||||
<group
|
||||
ref={ groupRef }
|
||||
scale={ scale }
|
||||
position={ [
|
||||
size.width / 2 - marginX - scale / 2,
|
||||
- size.height / 2 + marginY + scale / 2,
|
||||
0,
|
||||
] }
|
||||
|
||||
{ ...rest }
|
||||
>{ children }</group>
|
||||
<RenderPortal
|
||||
defaultCamera={ defaultCamera }
|
||||
defaultScene={ defaultScene }
|
||||
overrideRenderLoop={ overrideRenderLoop }
|
||||
renderPriority={ 10 }
|
||||
/>
|
||||
</>,
|
||||
scene,
|
||||
{ events: { priority: 10 } },
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Vector3, Ray } from 'three';
|
||||
import { Camera } from '@react-three/fiber';
|
||||
|
||||
interface SettledObjectProps {
|
||||
component?: ReactNode;
|
||||
lat?: number | null;
|
||||
lon?: number | null;
|
||||
rayorigin?: Vector3 | null;
|
||||
raydirection?: Vector3 | null;
|
||||
onQueryUpdate?: ( hit: any ) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface AnimatedSettledObjectProps extends SettledObjectProps {
|
||||
interpolationFactor?: number;
|
||||
}
|
||||
|
||||
interface SettledObjectsProps {
|
||||
scene?: any;
|
||||
children?: ReactNode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface QueryManagerContextType {
|
||||
registerLatLonQuery( lat: number, lon: number, callback: Function ): number;
|
||||
unregisterQuery( index: number ): void;
|
||||
registerRayQuery( ray: Ray, callback: Function ): number;
|
||||
setScene( scene: any[] ): void;
|
||||
addCamera( camera: Camera ): void;
|
||||
setEllipsoidFromTilesRenderer( tiles: any ): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export const QueryManagerContext: React.Context<QueryManagerContextType | null>;
|
||||
|
||||
export const AnimatedSettledObject: React.ForwardRefExoticComponent<
|
||||
AnimatedSettledObjectProps & React.RefAttributes<AnimatedSettledObjectProps>
|
||||
>;
|
||||
|
||||
export const SettledObject: React.ForwardRefExoticComponent<
|
||||
SettledObjectProps & React.RefAttributes<SettledObjectProps>
|
||||
>;
|
||||
|
||||
export const SettledObjects: React.ForwardRefExoticComponent<
|
||||
SettledObjectsProps & React.RefAttributes<SettledObjectsProps>
|
||||
>;
|
||||
@@ -0,0 +1,241 @@
|
||||
import { cloneElement, createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { OBJECT_FRAME } from '3d-tiles-renderer/three';
|
||||
import { Matrix4, Ray, Vector3 } from 'three';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { useMultipleRefs } from '../utilities/useMultipleRefs.js';
|
||||
import { TilesRendererContext } from './TilesRenderer.jsx';
|
||||
import { QueryManager } from '../utilities/QueryManager.js';
|
||||
import { useDeepOptions } from '../utilities/useOptions.js';
|
||||
import { useApplyRefs } from '../utilities/useApplyRefs.js';
|
||||
|
||||
const QueryManagerContext = createContext( null );
|
||||
const _matrix = /* @__PURE__ */ new Matrix4();
|
||||
const _ray = /* @__PURE__ */ new Ray();
|
||||
|
||||
export const AnimatedSettledObject = forwardRef( function AnimatedSettledObject( props, ref ) {
|
||||
|
||||
const {
|
||||
interpolationFactor = 0.025,
|
||||
onQueryUpdate = null,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const queries = useContext( QueryManagerContext );
|
||||
const invalidate = useThree( ( { invalidate } ) => invalidate );
|
||||
const target = useMemo( () => new Vector3(), [] );
|
||||
const isInitialized = useMemo( () => ( { value: false } ), [] );
|
||||
const isTargetSet = useMemo( () => ( { value: false } ), [] );
|
||||
const objectRef = useRef( null );
|
||||
|
||||
const queryCallback = useCallback( hit => {
|
||||
|
||||
if ( tiles === null || hit === null || objectRef.current === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
const { lat, lon, rayorigin, raydirection } = rest;
|
||||
if ( lat !== null && lon !== null ) {
|
||||
|
||||
target.copy( hit.point );
|
||||
isTargetSet.value = true;
|
||||
|
||||
queries.ellipsoid.getObjectFrame( lat, lon, 0, 0, 0, 0, _matrix, OBJECT_FRAME ).premultiply( tiles.group.matrixWorld );
|
||||
objectRef.current.quaternion.setFromRotationMatrix( _matrix );
|
||||
invalidate();
|
||||
|
||||
} else if ( rayorigin !== null && raydirection !== null ) {
|
||||
|
||||
target.copy( hit.point );
|
||||
isTargetSet.value = true;
|
||||
|
||||
objectRef.current.quaternion.identity();
|
||||
invalidate();
|
||||
|
||||
}
|
||||
|
||||
if ( onQueryUpdate ) {
|
||||
|
||||
onQueryUpdate( hit );
|
||||
|
||||
}
|
||||
|
||||
}, [ invalidate, isTargetSet, queries.ellipsoid, rest, target, tiles, onQueryUpdate ] );
|
||||
|
||||
// interpolate the point position
|
||||
useFrame( ( state, delta ) => {
|
||||
|
||||
if ( objectRef.current ) {
|
||||
|
||||
objectRef.current.visible = isInitialized.value;
|
||||
|
||||
}
|
||||
|
||||
if ( objectRef.current && isTargetSet.value ) {
|
||||
|
||||
// jump the point to the target if it's being set for the first time
|
||||
if ( isInitialized.value === false ) {
|
||||
|
||||
isInitialized.value = true;
|
||||
objectRef.current.position.copy( target );
|
||||
|
||||
} else {
|
||||
|
||||
// framerate independent lerp by Freya Holmer
|
||||
const factor = 1 - 2 ** ( - delta / interpolationFactor );
|
||||
if ( objectRef.current.position.distanceToSquared( target ) > 1e-6 ) {
|
||||
|
||||
objectRef.current.position.lerp(
|
||||
target, interpolationFactor === 0 ? 1 : factor
|
||||
);
|
||||
|
||||
invalidate();
|
||||
|
||||
} else {
|
||||
|
||||
objectRef.current.position.copy( target );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
return (
|
||||
<SettledObject
|
||||
ref={ useMultipleRefs( objectRef, ref ) }
|
||||
onQueryUpdate={ queryCallback }
|
||||
{ ...rest }
|
||||
/>
|
||||
);
|
||||
|
||||
} );
|
||||
|
||||
// Object that updates its "settled" state
|
||||
export const SettledObject = forwardRef( function SettledObject( props, ref ) {
|
||||
|
||||
const {
|
||||
component = <group />,
|
||||
lat = null,
|
||||
lon = null,
|
||||
rayorigin = null,
|
||||
raydirection = null,
|
||||
onQueryUpdate = null,
|
||||
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const objectRef = useRef( null );
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const queries = useContext( QueryManagerContext );
|
||||
const invalidate = useThree( ( { invalidate } ) => invalidate );
|
||||
const target = useMemo( () => new Vector3(), [] );
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
const callback = hit => {
|
||||
|
||||
if ( onQueryUpdate ) {
|
||||
|
||||
onQueryUpdate( hit );
|
||||
|
||||
} else if ( tiles && hit !== null && objectRef.current !== null ) {
|
||||
|
||||
if ( lat !== null && lon !== null ) {
|
||||
|
||||
objectRef.current.position.copy( hit.point );
|
||||
queries.ellipsoid.getObjectFrame( lat, lon, 0, 0, 0, 0, _matrix, OBJECT_FRAME ).premultiply( tiles.group.matrixWorld );
|
||||
objectRef.current.quaternion.setFromRotationMatrix( _matrix );
|
||||
invalidate();
|
||||
|
||||
} else if ( rayorigin !== null && raydirection !== null ) {
|
||||
|
||||
objectRef.current.position.copy( hit.point );
|
||||
objectRef.current.quaternion.identity();
|
||||
invalidate();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if ( lat !== null && lon !== null ) {
|
||||
|
||||
const index = queries.registerLatLonQuery( lat, lon, callback );
|
||||
return () => queries.unregisterQuery( index );
|
||||
|
||||
} else if ( rayorigin !== null && raydirection !== null ) {
|
||||
|
||||
_ray.origin.copy( rayorigin );
|
||||
_ray.direction.copy( raydirection );
|
||||
const index = queries.registerRayQuery( _ray, callback );
|
||||
return () => queries.unregisterQuery( index );
|
||||
|
||||
}
|
||||
|
||||
}, [ lat, lon, rayorigin, raydirection, queries, tiles, invalidate, target, onQueryUpdate ] );
|
||||
|
||||
return cloneElement( component, { ...rest, ref: useMultipleRefs( objectRef, ref ), raycast: () => false } );
|
||||
|
||||
} );
|
||||
|
||||
export const SettledObjects = forwardRef( function SettledObjects( props, ref ) {
|
||||
|
||||
const threeScene = useThree( ( { scene } ) => scene );
|
||||
const {
|
||||
scene = threeScene,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const queries = useMemo( () => new QueryManager(), [] );
|
||||
const camera = useThree( ( { camera } ) => camera );
|
||||
|
||||
useDeepOptions( queries, rest );
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
return () => queries.dispose();
|
||||
|
||||
}, [ queries ] );
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
queries.setScene( ...( Array.isArray( scene ) ? scene : [ scene ] ) );
|
||||
|
||||
}, [ queries, scene ] );
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
queries.addCamera( camera );
|
||||
|
||||
}, [ queries, camera ] );
|
||||
|
||||
useFrame( () => {
|
||||
|
||||
if ( tiles ) {
|
||||
|
||||
queries.setEllipsoidFromTilesRenderer( tiles );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// assign ref
|
||||
useApplyRefs( queries, ref );
|
||||
|
||||
return (
|
||||
<QueryManagerContext.Provider value={ queries }>
|
||||
<group matrixAutoUpdate={ false } matrixWorldAutoUpdate={ false }>
|
||||
{ children }
|
||||
</group>
|
||||
</QueryManagerContext.Provider>
|
||||
);
|
||||
|
||||
} );
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import type { CanvasDOMOverlayProps } from './CanvasDOMOverlay.jsx';
|
||||
import type { ReactNode, ForwardRefExoticComponent, RefAttributes, CSSProperties } from 'react';
|
||||
|
||||
type Attribution = {
|
||||
type: 'string' | 'html' | 'image';
|
||||
value: any;
|
||||
};
|
||||
|
||||
interface TilesAttributionOverlayProps extends CanvasDOMOverlayProps {
|
||||
style?: CSSProperties;
|
||||
generateAttributions?: ( ( attributions: Attribution[], classId: string ) => ReactNode ) | null;
|
||||
}
|
||||
|
||||
export declare const TilesAttributionOverlay: ForwardRefExoticComponent<
|
||||
TilesAttributionOverlayProps & RefAttributes<TilesAttributionOverlayProps>
|
||||
>;
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { useContext, useState, useEffect, useMemo } from 'react';
|
||||
import { TilesRendererContext } from './TilesRenderer.jsx';
|
||||
import { CanvasDOMOverlay } from './CanvasDOMOverlay.jsx';
|
||||
|
||||
function randomID() {
|
||||
|
||||
return crypto.getRandomValues( new Uint32Array( 1 ) )[ 0 ].toString( 16 );
|
||||
|
||||
}
|
||||
|
||||
// Overlay for displaying tile data set attributions
|
||||
export function TilesAttributionOverlay( { children, style, generateAttributions, ...rest } ) {
|
||||
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const [ attributions, setAttributions ] = useState( [] );
|
||||
|
||||
// Add events for checking when attributions may be updated
|
||||
useEffect( () => {
|
||||
|
||||
if ( ! tiles ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
let queued = false;
|
||||
const callback = () => {
|
||||
|
||||
if ( ! queued ) {
|
||||
|
||||
queued = true;
|
||||
queueMicrotask( () => {
|
||||
|
||||
setAttributions( tiles.getAttributions() );
|
||||
queued = false;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
tiles.addEventListener( 'tile-visibility-change', callback );
|
||||
tiles.addEventListener( 'load-tile-set', callback );
|
||||
|
||||
return () => {
|
||||
|
||||
tiles.removeEventListener( 'tile-visibility-change', callback );
|
||||
tiles.removeEventListener( 'load-tile-set', callback );
|
||||
|
||||
};
|
||||
|
||||
}, [ tiles ] );
|
||||
|
||||
// Generate CSS for modifying child elements implicit to the html attributions
|
||||
const classId = useMemo( () => 'class_' + randomID(), [] );
|
||||
const styles = useMemo( () => `
|
||||
#${ classId } a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#${ classId } img {
|
||||
max-width: 125px;
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
}
|
||||
`, [ classId ] );
|
||||
|
||||
let output;
|
||||
if ( generateAttributions ) {
|
||||
|
||||
output = generateAttributions( attributions, classId );
|
||||
|
||||
} else {
|
||||
|
||||
// generate elements for each type of attribution
|
||||
const elements = [];
|
||||
attributions.forEach( ( att, i ) => {
|
||||
|
||||
let element = null;
|
||||
if ( att.type === 'string' ) {
|
||||
|
||||
element = <div key={ i }>{ att.value }</div>;
|
||||
|
||||
} else if ( att.type === 'html' ) {
|
||||
|
||||
element = <div key={ i } dangerouslySetInnerHTML={ { __html: att.value } } style={ { pointerEvents: 'all' } }/>;
|
||||
|
||||
} else if ( att.type === 'image' ) {
|
||||
|
||||
element = <div key={ i }><img src={ att.value } /></div>;
|
||||
|
||||
}
|
||||
|
||||
if ( element ) {
|
||||
|
||||
elements.push( element );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
output = (
|
||||
<>
|
||||
<style>{ styles }</style>
|
||||
{ elements }
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<CanvasDOMOverlay
|
||||
id={ classId }
|
||||
style={ {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
padding: '10px',
|
||||
color: 'rgba( 255, 255, 255, 0.75 )',
|
||||
fontSize: '10px',
|
||||
...style,
|
||||
} }
|
||||
{ ...rest }
|
||||
>
|
||||
{ children }
|
||||
{ output }
|
||||
</CanvasDOMOverlay>
|
||||
);
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import type { TilesRenderer as TilesRendererImpl, TilesRendererEventMap } from '3d-tiles-renderer/three';
|
||||
import type {
|
||||
ReactNode,
|
||||
Context,
|
||||
ForwardRefExoticComponent,
|
||||
RefAttributes,
|
||||
JSX,
|
||||
} from 'react';
|
||||
|
||||
export declare const TilesRendererContext: Context<TilesRendererImpl | null>;
|
||||
|
||||
interface EastNorthUpFrameProps {
|
||||
lat?: number;
|
||||
lon?: number;
|
||||
height?: number;
|
||||
az?: number;
|
||||
el?: number;
|
||||
roll?: number;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export declare const EastNorthUpFrame: ForwardRefExoticComponent<
|
||||
EastNorthUpFrameProps & RefAttributes<any>
|
||||
>;
|
||||
|
||||
interface TilesPluginProps<
|
||||
Plugin extends new ( ...args: any[] ) => void,
|
||||
Params extends any[] = ConstructorParameters<Plugin>,
|
||||
> {
|
||||
plugin: Plugin;
|
||||
args?: Params;
|
||||
[key: string]: Params | Plugin | any;
|
||||
}
|
||||
|
||||
export declare const TilesPlugin: <
|
||||
Plugin extends new ( ...args: any[] ) => void,
|
||||
Params extends any[] = ConstructorParameters<Plugin>,
|
||||
>(
|
||||
props: TilesPluginProps<Plugin, Params> & RefAttributes<Plugin>,
|
||||
) => JSX.Element;
|
||||
|
||||
// dynamically mapping keys of TilesRendererEventMap to onCamelCased
|
||||
type CamelCase<S extends string> = S extends `${infer T}-${infer U}`
|
||||
? `${T}${Capitalize<CamelCase<U>>}`
|
||||
: S;
|
||||
|
||||
type EventHandler<K extends keyof TilesRendererEventMap> = (
|
||||
event: TilesRendererEventMap[K],
|
||||
) => void;
|
||||
|
||||
type TilesRendererEventMapForR3f = {
|
||||
[K in keyof TilesRendererEventMap as `on${Capitalize<CamelCase<K>>}`]?: EventHandler<K>;
|
||||
};
|
||||
|
||||
interface TilesRendererProps
|
||||
extends Partial<TilesRendererImpl>,
|
||||
TilesRendererEventMapForR3f {
|
||||
url?: string;
|
||||
enabled?: boolean;
|
||||
dispose?: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export declare const TilesRenderer: ForwardRefExoticComponent<
|
||||
TilesRendererProps & RefAttributes<TilesRendererImpl>
|
||||
>;
|
||||
@@ -0,0 +1,304 @@
|
||||
import { createContext, useContext, useEffect, useRef, forwardRef, useCallback, useState, useLayoutEffect, useReducer } from 'react';
|
||||
import { useThree, useFrame } from '@react-three/fiber';
|
||||
import { Object3D } from 'three';
|
||||
import { TilesRenderer as TilesRendererImpl, WGS84_ELLIPSOID } from '3d-tiles-renderer/three';
|
||||
import { useDeepOptions } from '../utilities/useOptions.js';
|
||||
import { useObjectDep } from '../utilities/useObjectDep.js';
|
||||
import { useApplyRefs } from '../utilities/useApplyRefs.js';
|
||||
|
||||
// context for accessing the tile set
|
||||
export const TilesRendererContext = createContext( null );
|
||||
export const TilesPluginContext = createContext( null );
|
||||
|
||||
// group that matches the transform of the tile set root group
|
||||
function TileSetRoot( { children } ) {
|
||||
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const ref = useRef();
|
||||
useEffect( () => {
|
||||
|
||||
if ( tiles ) {
|
||||
|
||||
ref.current.matrixWorld = tiles.group.matrixWorld;
|
||||
|
||||
}
|
||||
|
||||
}, [ tiles ] );
|
||||
|
||||
return <group ref={ ref } matrixWorldAutoUpdate={ false } matrixAutoUpdate={ false }>{ children }</group>;
|
||||
|
||||
}
|
||||
|
||||
export function EastNorthUpFrame( props ) {
|
||||
|
||||
const {
|
||||
lat = 0,
|
||||
lon = 0,
|
||||
height = 0,
|
||||
az = 0,
|
||||
el = 0,
|
||||
roll = 0,
|
||||
ellipsoid = WGS84_ELLIPSOID.clone(),
|
||||
children,
|
||||
} = props;
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const invalidate = useThree( state => state.invalidate );
|
||||
const [ group, setGroup ] = useState( null );
|
||||
const updateCallback = useCallback( () => {
|
||||
|
||||
if ( group === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// hide the group if the tiles aren't loaded yet
|
||||
const localEllipsoid = tiles && tiles.ellipsoid || ellipsoid || null;
|
||||
group.matrix.identity();
|
||||
group.visible = Boolean( tiles && tiles.root || ellipsoid );
|
||||
|
||||
if ( localEllipsoid === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
localEllipsoid.getOrientedEastNorthUpFrame( lat, lon, height, az, el, roll, group.matrix );
|
||||
group.matrix.decompose( group.position, group.quaternion, group.scale );
|
||||
group.updateMatrixWorld();
|
||||
invalidate();
|
||||
|
||||
}, [ invalidate, tiles, lat, lon, height, az, el, roll, ellipsoid, group, useObjectDep( ellipsoid.radius ) ] ); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// adjust the matrix world update logic if a tile set is present so that we can position the frame
|
||||
// correctly regardless of the parent.
|
||||
useEffect( () => {
|
||||
|
||||
if ( tiles !== null && group !== null ) {
|
||||
|
||||
group.updateMatrixWorld = function ( force ) {
|
||||
|
||||
if ( this.matrixAutoUpdate ) {
|
||||
|
||||
this.updateMatrix();
|
||||
|
||||
}
|
||||
|
||||
if ( this.matrixWorldNeedsUpdate || force ) {
|
||||
|
||||
this.matrixWorld.multiplyMatrices( tiles.group.matrixWorld, this.matrix );
|
||||
force = true;
|
||||
|
||||
}
|
||||
|
||||
const children = this.children;
|
||||
for ( let i = 0, l = children.length; i < l; i ++ ) {
|
||||
|
||||
const child = children[ i ];
|
||||
child.updateMatrixWorld( force );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return () => {
|
||||
|
||||
group.updateMatrixWorld = Object3D.prototype.updateMatrixWorld;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}, [ tiles, group ] );
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
updateCallback();
|
||||
|
||||
}, [ updateCallback ] );
|
||||
|
||||
// update the position when a tile set is loaded since it may modify the ellipsoid
|
||||
useEffect( () => {
|
||||
|
||||
if ( tiles === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
tiles.addEventListener( 'load-tile-set', updateCallback );
|
||||
return () => {
|
||||
|
||||
tiles.removeEventListener( 'load-tile-set', updateCallback );
|
||||
|
||||
};
|
||||
|
||||
}, [ tiles, updateCallback ] );
|
||||
|
||||
return <group ref={ setGroup }>{ children }</group>;
|
||||
|
||||
}
|
||||
|
||||
// component for registering a plugin
|
||||
export const TilesPlugin = forwardRef( function TilesPlugin( props, ref ) {
|
||||
|
||||
const { plugin, args, children, ...options } = props;
|
||||
const tiles = useContext( TilesRendererContext );
|
||||
const [ instance, setInstance ] = useState( null );
|
||||
const [ , forceUpdate ] = useReducer( x => x + 1, 0 );
|
||||
|
||||
useLayoutEffect( () => {
|
||||
|
||||
if ( tiles === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
let instance;
|
||||
if ( Array.isArray( args ) ) {
|
||||
|
||||
instance = new plugin( ...args );
|
||||
|
||||
} else {
|
||||
|
||||
instance = new plugin( args );
|
||||
|
||||
}
|
||||
|
||||
setInstance( instance );
|
||||
|
||||
return () => {
|
||||
|
||||
setInstance( null );
|
||||
|
||||
};
|
||||
|
||||
}, [ plugin, tiles, useObjectDep( args ) ] ); // eslint-disable-line
|
||||
|
||||
// assigns any provided options to the plugin
|
||||
useDeepOptions( instance, options );
|
||||
|
||||
// register the plugin
|
||||
useLayoutEffect( () => {
|
||||
|
||||
if ( instance === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// force the component to rerender after registering the plugin because we don't
|
||||
// include the children until the plugin is added.
|
||||
tiles.registerPlugin( instance );
|
||||
forceUpdate();
|
||||
|
||||
return () => {
|
||||
|
||||
tiles.unregisterPlugin( instance );
|
||||
|
||||
};
|
||||
|
||||
// "tiles" is excluded from the dependencies since this would otherwise run once with the
|
||||
// new tiles renderer, resulting in an error when the instance is added to a second renderer.
|
||||
|
||||
}, [ instance ] ); // eslint-disable-line
|
||||
|
||||
// assign ref
|
||||
useApplyRefs( instance, ref );
|
||||
|
||||
// only render out the plugin once the instance and context are ready and registered
|
||||
if ( ! instance || ! tiles.plugins.includes( instance ) ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
return <TilesPluginContext.Provider value={ instance }>{ children }</TilesPluginContext.Provider>;
|
||||
|
||||
} );
|
||||
|
||||
// component for adding a TilesRenderer to the scene
|
||||
export const TilesRenderer = forwardRef( function TilesRenderer( props, ref ) {
|
||||
|
||||
const { url, group = {}, enabled = true, children, ...options } = props;
|
||||
const [ camera, gl, invalidate ] = useThree( state => [ state.camera, state.gl, state.invalidate ] );
|
||||
const [ tiles, setTiles ] = useState( null );
|
||||
|
||||
// create the tile set
|
||||
useEffect( () => {
|
||||
|
||||
const needsRender = () => invalidate();
|
||||
|
||||
const tiles = new TilesRendererImpl( url );
|
||||
tiles.addEventListener( 'needs-render', needsRender );
|
||||
tiles.addEventListener( 'needs-update', needsRender );
|
||||
setTiles( tiles );
|
||||
|
||||
return () => {
|
||||
|
||||
tiles.removeEventListener( 'needs-render', needsRender );
|
||||
tiles.removeEventListener( 'needs-update', needsRender );
|
||||
tiles.dispose();
|
||||
setTiles( null );
|
||||
|
||||
};
|
||||
|
||||
}, [ url, invalidate ] );
|
||||
|
||||
// update the resolution for the camera
|
||||
useFrame( () => {
|
||||
|
||||
if ( tiles === null || ! enabled ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
camera.updateMatrixWorld();
|
||||
tiles.setResolutionFromRenderer( camera, gl );
|
||||
tiles.update();
|
||||
|
||||
} );
|
||||
|
||||
// add the camera
|
||||
useLayoutEffect( () => {
|
||||
|
||||
if ( tiles === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
tiles.setCamera( camera );
|
||||
return () => {
|
||||
|
||||
tiles.deleteCamera( camera );
|
||||
|
||||
};
|
||||
|
||||
}, [ tiles, camera ] );
|
||||
|
||||
// assign ref
|
||||
useApplyRefs( tiles, ref );
|
||||
|
||||
// assign options recursively
|
||||
useDeepOptions( tiles, options );
|
||||
|
||||
// only render out the tiles once the instance and context are ready
|
||||
if ( ! tiles ) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<primitive object={ tiles.group } { ...group } />
|
||||
<TilesRendererContext.Provider value={ tiles }>
|
||||
<TileSetRoot>
|
||||
{ children }
|
||||
</TileSetRoot>
|
||||
</TilesRendererContext.Provider>
|
||||
</>;
|
||||
|
||||
} );
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './components/CameraControls.jsx';
|
||||
export * from './components/CameraTransition.jsx';
|
||||
export * from './components/CanvasDOMOverlay.jsx';
|
||||
export * from './components/CompassGizmo.jsx';
|
||||
export * from './components/SettledObjects.jsx';
|
||||
export * from './components/TilesAttributionOverlay.jsx';
|
||||
export * from './components/TilesRenderer.jsx';
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './components/TilesRenderer.jsx';
|
||||
export * from './components/TilesAttributionOverlay.jsx';
|
||||
export * from './components/CanvasDOMOverlay.jsx';
|
||||
export * from './components/CameraControls.jsx';
|
||||
export * from './components/CompassGizmo.jsx';
|
||||
export * from './components/CameraTransition.jsx';
|
||||
export * from './components/SettledObjects.jsx';
|
||||
@@ -0,0 +1,437 @@
|
||||
import {
|
||||
Raycaster,
|
||||
Matrix4,
|
||||
EventDispatcher,
|
||||
Vector3,
|
||||
Ray,
|
||||
Line3,
|
||||
Vector2,
|
||||
} from 'three';
|
||||
import { SceneObserver } from './SceneObserver.js';
|
||||
import { Ellipsoid } from '3d-tiles-renderer/three';
|
||||
|
||||
const _raycaster = /* @__PURE__ */ new Raycaster();
|
||||
const _line0 = /* @__PURE__ */ new Line3();
|
||||
const _line1 = /* @__PURE__ */ new Line3();
|
||||
const _params = /* @__PURE__ */ new Vector2();
|
||||
const _direction = /* @__PURE__ */ new Vector3();
|
||||
const _matrix = /* @__PURE__ */ new Matrix4();
|
||||
export class QueryManager extends EventDispatcher {
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
// settings
|
||||
this.autoRun = true;
|
||||
|
||||
// queries
|
||||
this.queryMap = new Map();
|
||||
this.index = 0;
|
||||
|
||||
// jobs
|
||||
this.queued = [];
|
||||
this.scheduled = false;
|
||||
this.duration = 1;
|
||||
|
||||
// scene
|
||||
this.objects = [];
|
||||
this.observer = new SceneObserver();
|
||||
this.ellipsoid = new Ellipsoid();
|
||||
this.frame = new Matrix4();
|
||||
|
||||
// cameras for sorting
|
||||
this.cameras = new Set();
|
||||
|
||||
// register to mark items as dirty
|
||||
const queueAll = ( () => {
|
||||
|
||||
let queued = false;
|
||||
return () => {
|
||||
|
||||
if ( ! queued ) {
|
||||
|
||||
queued = true;
|
||||
queueMicrotask( () => {
|
||||
|
||||
this.queryMap.forEach( item => this._enqueue( item ) );
|
||||
queued = false;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} )();
|
||||
|
||||
this.observer.addEventListener( 'childadded', queueAll );
|
||||
this.observer.addEventListener( 'childremoved', queueAll );
|
||||
|
||||
}
|
||||
|
||||
// job runner
|
||||
_enqueue( info ) {
|
||||
|
||||
if ( ! info.queued ) {
|
||||
|
||||
this.queued.push( info );
|
||||
info.queued = true;
|
||||
this._scheduleRun();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_runJobs() {
|
||||
|
||||
const { queued, cameras, duration } = this;
|
||||
const start = performance.now();
|
||||
|
||||
// Iterate over all cameras
|
||||
cameras.forEach( ( camera, c ) => {
|
||||
|
||||
_matrix.copy( camera.matrixWorldInverse ).premultiply( camera.projectionMatrix );
|
||||
_direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
|
||||
|
||||
_line0.start.setFromMatrixPosition( camera.matrixWorld );
|
||||
_line0.end.addVectors( _direction, _line0.start );
|
||||
|
||||
for ( let i = 0, l = queued.length; i < l; i ++ ) {
|
||||
|
||||
const info = queued[ i ];
|
||||
const { ray } = info;
|
||||
|
||||
// save the values for sorting
|
||||
let distance;
|
||||
let inFrustum;
|
||||
if ( info.point === null ) {
|
||||
|
||||
// prioritize displaying points that are from rays pointing in the same direction as
|
||||
// the camera. Find the distance between camera ray and projection ray:
|
||||
_line1.start.copy( ray.origin );
|
||||
ray.at( 1, _line1.end );
|
||||
closestPointLineToLine( _line0, _line1, _params );
|
||||
|
||||
info.distance = _params.x * ( 1.0 - Math.abs( _direction.dot( ray.direction ) ) );
|
||||
info.inFrustum = true;
|
||||
|
||||
} else {
|
||||
|
||||
// if the point is within the frustum then prioritize it
|
||||
const p = _line1.start;
|
||||
p.copy( info.point ).applyMatrix4( _matrix );
|
||||
if ( p.x > - 1 && p.x < 1 && p.y > - 1 && p.y < 1 && p.z > - 1 && p.z < 1 ) {
|
||||
|
||||
// calculate the distance to the last hit point
|
||||
info.distance = p.subVectors( info.point, _line0.start ).dot( _direction );
|
||||
info.inFrustum = true;
|
||||
|
||||
} else {
|
||||
|
||||
info.distance = 0;
|
||||
info.inFrustum = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( c === 0 ) {
|
||||
|
||||
info.distance = distance;
|
||||
info.inFrustum = inFrustum;
|
||||
|
||||
} else {
|
||||
|
||||
info.inFrustum = info.inFrustum || inFrustum;
|
||||
info.distance = Math.min( info.distance, distance );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// sort the items if necessary
|
||||
if ( cameras.length !== 0 ) {
|
||||
|
||||
queued.sort( ( a, b ) => {
|
||||
|
||||
if ( ( a.point === null ) !== ( b.point === null ) ) {
|
||||
|
||||
return a.point === null ? 1 : - 1;
|
||||
|
||||
} else if ( a.inFrustum !== b.inFrustum ) {
|
||||
|
||||
return a.inFrustum ? 1 : - 1;
|
||||
|
||||
} else if ( ( a.distance < 0 ) !== ( b.distance < 0 ) ) {
|
||||
|
||||
return a.distance < 0 ? - 1 : 1;
|
||||
|
||||
} else {
|
||||
|
||||
return b.distance - a.distance;
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
// update all the positions
|
||||
while ( queued.length !== 0 && performance.now() - start < duration ) {
|
||||
|
||||
const item = queued.pop();
|
||||
item.queued = false;
|
||||
this._updateQuery( item );
|
||||
|
||||
}
|
||||
|
||||
if ( queued.length !== 0 ) {
|
||||
|
||||
this._scheduleRun();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_scheduleRun() {
|
||||
|
||||
if ( this.autoRun && ! this.scheduled ) {
|
||||
|
||||
this.scheduled = true;
|
||||
requestAnimationFrame( () => {
|
||||
|
||||
this.scheduled = false;
|
||||
this._runJobs();
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_updateQuery( item ) {
|
||||
|
||||
_raycaster.ray.copy( item.ray );
|
||||
_raycaster.far = 'lat' in item ? 1e4 + Math.max( ...this.ellipsoid.radius ) : Infinity;
|
||||
|
||||
// save the last hit point for sorting
|
||||
const hit = _raycaster.intersectObjects( this.objects )[ 0 ] || null;
|
||||
if ( hit !== null ) {
|
||||
|
||||
if ( item.point === null ) {
|
||||
|
||||
item.point = hit.point.clone();
|
||||
|
||||
} else {
|
||||
|
||||
item.point.copy( hit.point );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
item.callback( hit );
|
||||
|
||||
}
|
||||
|
||||
// add and remove cameras used for sorting
|
||||
addCamera( camera ) {
|
||||
|
||||
const { queryMap, cameras } = this;
|
||||
cameras.add( camera );
|
||||
queryMap.forEach( o => this._enqueue( o ) );
|
||||
|
||||
}
|
||||
|
||||
deleteCamera( camera ) {
|
||||
|
||||
const { cameras } = this;
|
||||
cameras.delete( camera );
|
||||
|
||||
}
|
||||
|
||||
// run the given item index if possible
|
||||
runIfNeeded( index ) {
|
||||
|
||||
const { queryMap, queued } = this;
|
||||
const item = queryMap.get( index );
|
||||
if ( item.queued ) {
|
||||
|
||||
this._updateQuery( item );
|
||||
item.queued = false;
|
||||
queued.splice( queued.indexOf( item ), 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// set the scene used for query
|
||||
setScene( ...objects ) {
|
||||
|
||||
const { observer } = this;
|
||||
observer.dispose();
|
||||
objects.forEach( o => observer.observe( o ) );
|
||||
this.objects = objects;
|
||||
this._scheduleRun();
|
||||
|
||||
}
|
||||
|
||||
// update the ellipsoid and frame based on a tiles renderer, updating the item rays only if necessary
|
||||
setEllipsoidFromTilesRenderer( tilesRenderer ) {
|
||||
|
||||
const { queryMap, ellipsoid, frame } = this;
|
||||
if (
|
||||
! ellipsoid.radius.equals( tilesRenderer.ellipsoid.radius ) ||
|
||||
! frame.equals( tilesRenderer.group.matrixWorld )
|
||||
) {
|
||||
|
||||
ellipsoid.copy( tilesRenderer.ellipsoid );
|
||||
frame.copy( tilesRenderer.group.matrixWorld );
|
||||
|
||||
// update the query rays for any item specified via lat / lon
|
||||
queryMap.forEach( o => {
|
||||
|
||||
if ( 'lat' in o ) {
|
||||
|
||||
const { lat, lon, ray } = o;
|
||||
ellipsoid.getCartographicToPosition( lat, lon, 1e4, ray.origin ).applyMatrix4( frame );
|
||||
ellipsoid.getCartographicToNormal( lat, lon, ray.direction ).transformDirection( frame ).multiplyScalar( - 1 );
|
||||
|
||||
}
|
||||
|
||||
this._enqueue( o );
|
||||
|
||||
} );
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// register query callbacks
|
||||
registerRayQuery( ray, callback ) {
|
||||
|
||||
const index = this.index ++;
|
||||
const item = {
|
||||
ray: ray.clone(),
|
||||
callback,
|
||||
queued: false,
|
||||
distance: - 1,
|
||||
point: null,
|
||||
};
|
||||
|
||||
this.queryMap.set( index, item );
|
||||
this._enqueue( item );
|
||||
return index;
|
||||
|
||||
}
|
||||
|
||||
registerLatLonQuery( lat, lon, callback ) {
|
||||
|
||||
const { ellipsoid, frame } = this;
|
||||
const index = this.index ++;
|
||||
|
||||
const ray = new Ray();
|
||||
ellipsoid.getCartographicToPosition( lat, lon, 1e4, ray.origin ).applyMatrix4( frame );
|
||||
ellipsoid.getCartographicToNormal( lat, lon, ray.direction ).transformDirection( frame ).multiplyScalar( - 1 );
|
||||
|
||||
const item = {
|
||||
ray: ray.clone(),
|
||||
lat, lon,
|
||||
callback,
|
||||
queued: false,
|
||||
distance: - 1,
|
||||
point: null,
|
||||
};
|
||||
|
||||
this.queryMap.set( index, item );
|
||||
this._enqueue( item );
|
||||
return index;
|
||||
|
||||
}
|
||||
|
||||
unregisterQuery( index ) {
|
||||
|
||||
const { queued, queryMap } = this;
|
||||
const item = queryMap.get( index );
|
||||
queryMap.delete( index );
|
||||
|
||||
if ( item && item.queued ) {
|
||||
|
||||
item.queued = false;
|
||||
queued.splice( queued.indexOf( item ), 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// dispose of everything
|
||||
dispose() {
|
||||
|
||||
this.queryMap.clear();
|
||||
this.queued.length = 0;
|
||||
this.objects.length = 0;
|
||||
this.observer.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// copied from three-mesh-bvh
|
||||
const closestPointLineToLine = ( function () {
|
||||
|
||||
// https://github.com/juj/MathGeoLib/blob/master/src/Geometry/Line.cpp#L56
|
||||
const dir1 = new Vector3();
|
||||
const dir2 = new Vector3();
|
||||
const v02 = new Vector3();
|
||||
return function closestPointLineToLine( l1, l2, result ) {
|
||||
|
||||
const v0 = l1.start;
|
||||
const v10 = dir1;
|
||||
const v2 = l2.start;
|
||||
const v32 = dir2;
|
||||
|
||||
v02.subVectors( v0, v2 );
|
||||
dir1.subVectors( l1.end, l1.start );
|
||||
dir2.subVectors( l2.end, l2.start );
|
||||
|
||||
// float d0232 = v02.Dot(v32);
|
||||
const d0232 = v02.dot( v32 );
|
||||
|
||||
// float d3210 = v32.Dot(v10);
|
||||
const d3210 = v32.dot( v10 );
|
||||
|
||||
// float d3232 = v32.Dot(v32);
|
||||
const d3232 = v32.dot( v32 );
|
||||
|
||||
// float d0210 = v02.Dot(v10);
|
||||
const d0210 = v02.dot( v10 );
|
||||
|
||||
// float d1010 = v10.Dot(v10);
|
||||
const d1010 = v10.dot( v10 );
|
||||
|
||||
// float denom = d1010*d3232 - d3210*d3210;
|
||||
const denom = d1010 * d3232 - d3210 * d3210;
|
||||
|
||||
let d, d2;
|
||||
if ( denom !== 0 ) {
|
||||
|
||||
d = ( d0232 * d3210 - d0210 * d3232 ) / denom;
|
||||
|
||||
} else {
|
||||
|
||||
d = 0;
|
||||
|
||||
}
|
||||
|
||||
d2 = ( d0232 + d * d3210 ) / d3232; // eslint-disable-line
|
||||
|
||||
result.x = d;
|
||||
result.y = d2;
|
||||
|
||||
};
|
||||
|
||||
} )();
|
||||
@@ -0,0 +1,99 @@
|
||||
import { EventDispatcher } from 'three';
|
||||
|
||||
function traverse( root, callback ) {
|
||||
|
||||
if ( callback( root ) ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
root.children.forEach( c => {
|
||||
|
||||
traverse( c, callback );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
export class SceneObserver extends EventDispatcher {
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
this.objects = new Set();
|
||||
this.observed = new Set();
|
||||
this._addedCallback = ( { child } ) => {
|
||||
|
||||
traverse( child, c => {
|
||||
|
||||
if ( this.observed.has( c ) ) {
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
|
||||
this.objects.add( c );
|
||||
c.addEventListener( 'childadded', this._addedCallback );
|
||||
c.addEventListener( 'childremoved', this._removedCallback );
|
||||
this.dispatchEvent( { type: 'childadded', child } );
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
};
|
||||
|
||||
this._removedCallback = ( { child } ) => {
|
||||
|
||||
traverse( child, c => {
|
||||
|
||||
if ( this.observed.has( c ) ) {
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
|
||||
this.objects.delete( c );
|
||||
c.removeEventListener( 'childadded', this._addedCallback );
|
||||
c.removeEventListener( 'childremoved', this._removedCallback );
|
||||
this.dispatchEvent( { type: 'childremoved', child } );
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
observe( root ) {
|
||||
|
||||
const { observed } = this;
|
||||
this._addedCallback( { child: root } );
|
||||
observed.add( root );
|
||||
|
||||
}
|
||||
|
||||
unobserve( root ) {
|
||||
|
||||
const { observed } = this;
|
||||
observed.delete( root );
|
||||
this._removedCallback( { child: root } );
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this.observed.forEach( root => {
|
||||
|
||||
this.unobserve( root );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// assign a give target to the set of refs
|
||||
export function useApplyRefs( target, ...refs ) {
|
||||
|
||||
useEffect( () => {
|
||||
|
||||
refs.forEach( ref => {
|
||||
|
||||
if ( ref ) {
|
||||
|
||||
if ( ref instanceof Function ) {
|
||||
|
||||
ref( target );
|
||||
|
||||
} else {
|
||||
|
||||
ref.current = target;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}, [ target, ...refs ] ); // eslint-disable-line
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export function useMultipleRefs( ...refs ) {
|
||||
|
||||
return useCallback( target => {
|
||||
|
||||
refs.forEach( ref => {
|
||||
|
||||
if ( ref ) {
|
||||
|
||||
if ( typeof ref === 'function' ) {
|
||||
|
||||
ref( target );
|
||||
|
||||
} else {
|
||||
|
||||
ref.current = target;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}, refs ); // eslint-disable-line
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
// checks if the first level of object key-values are equal
|
||||
function areObjectsEqual( a, b ) {
|
||||
|
||||
// early check for equivalence
|
||||
if ( a === b ) {
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// if either of the objects is null or undefined, then perform a simple check
|
||||
if ( ! a || ! b ) {
|
||||
|
||||
return a === b;
|
||||
|
||||
}
|
||||
|
||||
// check all keys and values in the first object
|
||||
for ( const key in a ) {
|
||||
|
||||
if ( a[ key ] !== b[ key ] ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check all keys and values in the second object
|
||||
for ( const key in b ) {
|
||||
|
||||
if ( a[ key ] !== b[ key ] ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// Helper for using an object as a dependency in a useEffect or useMemo array
|
||||
export function useObjectDep( object ) {
|
||||
|
||||
// only modify the returned object reference if it has changed
|
||||
const ref = useRef();
|
||||
if ( ! areObjectsEqual( ref.current, object ) ) {
|
||||
|
||||
ref.current = object;
|
||||
|
||||
}
|
||||
|
||||
return ref.current;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useObjectDep } from './useObjectDep.js';
|
||||
|
||||
// return true if the given key is for registering an event
|
||||
function isEventName( key ) {
|
||||
|
||||
return /^on/g.test( key );
|
||||
|
||||
}
|
||||
|
||||
// returns the event name to register for the given key
|
||||
function getEventName( key ) {
|
||||
|
||||
return key
|
||||
.replace( /^on/, '' )
|
||||
.replace( /[a-z][A-Z]/g, match => `${ match[ 0 ] }-${ match[ 1 ] }` )
|
||||
.toLowerCase();
|
||||
|
||||
}
|
||||
|
||||
// returns a dash-separated key as a list of tokens
|
||||
function getPath( key ) {
|
||||
|
||||
return key.split( '-' );
|
||||
|
||||
}
|
||||
|
||||
// gets the value from the object at the given path
|
||||
function getValueAtPath( object, path ) {
|
||||
|
||||
let curr = object;
|
||||
const tokens = [ ...path ];
|
||||
while ( tokens.length !== 0 ) {
|
||||
|
||||
const key = tokens.shift();
|
||||
curr = curr[ key ];
|
||||
|
||||
}
|
||||
|
||||
return curr;
|
||||
|
||||
}
|
||||
|
||||
// sets the value of the object at the given path
|
||||
function setValueAtPath( object, path, value ) {
|
||||
|
||||
const tokens = [ ...path ];
|
||||
const finalKey = tokens.pop();
|
||||
getValueAtPath( object, tokens )[ finalKey ] = value;
|
||||
|
||||
}
|
||||
|
||||
// Recursively assigns a set of options to an object, interpreting dashes as periods
|
||||
export function useDeepOptions( target, options, shallow = false ) {
|
||||
|
||||
// assign options recursively
|
||||
useLayoutEffect( () => {
|
||||
|
||||
if ( target === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
const previousState = {};
|
||||
const events = {};
|
||||
for ( const key in options ) {
|
||||
|
||||
if ( isEventName( key ) && target.addEventListener && ! ( key in target ) ) {
|
||||
|
||||
const eventName = getEventName( key );
|
||||
events[ eventName ] = options[ key ];
|
||||
target.addEventListener( eventName, options[ key ] );
|
||||
|
||||
} else {
|
||||
|
||||
const path = shallow ? [ key ] : getPath( key );
|
||||
previousState[ key ] = getValueAtPath( target, path );
|
||||
setValueAtPath( target, path, options[ key ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
for ( const key in events ) {
|
||||
|
||||
target.removeEventListener( key, events[ key ] );
|
||||
|
||||
}
|
||||
|
||||
for ( const key in previousState ) {
|
||||
|
||||
const path = shallow ? [ key ] : getPath( key );
|
||||
setValueAtPath( target, path, previousState[ key ] );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}, [ target, useObjectDep( options ) ] ); // eslint-disable-line
|
||||
|
||||
}
|
||||
|
||||
// Assigns a set of options to an object shallowly, interpreting dashes as periods
|
||||
export function useShallowOptions( instance, options ) {
|
||||
|
||||
useDeepOptions( instance, options, true );
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// bundle entry point
|
||||
|
||||
export * from './timeline';
|
||||
export * from './timelineEventsEmitter';
|
||||
export * from './settings/timelineConsts';
|
||||
// @ public timeline models.
|
||||
export * from './models/timelineRanged';
|
||||
export * from './models/timelineModel';
|
||||
export * from './models/timelineRow';
|
||||
export * from './models/timelineKeyframe';
|
||||
|
||||
// @public styles
|
||||
export * from './settings/timelineOptions';
|
||||
export * from './settings/styles/timelineKeyframeStyle';
|
||||
export * from './settings/styles/timelineRowStyle';
|
||||
export * from './settings/styles/timelineStyle';
|
||||
export * from './settings/styles/timelineGroupStyle';
|
||||
|
||||
export * from './utils/timelineStyleUtils';
|
||||
export * from './utils/timelineUtils';
|
||||
export * from './utils/timelineElement';
|
||||
|
||||
// @private helper containers.
|
||||
export * from './utils/timelineSelectable';
|
||||
export * from './utils/timelineCutBoundsRectResults';
|
||||
export * from './utils/timelineSelectionResults';
|
||||
export * from './utils/timelinePoint';
|
||||
export * from './utils/timelineMouseData';
|
||||
export * from './utils/timelineElementDragState';
|
||||
export * from './utils/timelineDraggableData';
|
||||
|
||||
// @private virtual model
|
||||
export * from './viewModels/timelineGroupViewModel';
|
||||
export * from './viewModels/timelineKeyframeViewModel';
|
||||
export * from './viewModels/timelineRowViewModel';
|
||||
export * from './viewModels/timelineViewModel';
|
||||
|
||||
// @public events
|
||||
export * from './utils/events/timelineKeyframeChangedEvent';
|
||||
export * from './utils/events/timelineTimeChangedEvent';
|
||||
export * from './utils/events/timelineSelectedEvent';
|
||||
export * from './utils/events/timelineScrollEvent';
|
||||
export * from './utils/events/timelineClickEvent';
|
||||
export * from './utils/events/timelineDragEvent';
|
||||
|
||||
// @public enums
|
||||
export * from './enums/timelineKeyframeShape';
|
||||
export * from './enums/timelineInteractionMode';
|
||||
export * from './enums/timelineScrollSource';
|
||||
export * from './enums/timelineElementType';
|
||||
export * from './enums/timelineCursorType';
|
||||
export * from './enums/timelineCapShape';
|
||||
export * from './enums/timelineEventSource';
|
||||
export * from './enums/timelineSelectionMode';
|
||||
export * from './enums/timelineEvents';
|
||||
// @private defaults are exposed:
|
||||
export * from './settings/defaults/defaultTimelineStyle';
|
||||
export * from './settings/defaults/defaultTimelineRowStyle';
|
||||
export * from './settings/defaults/defaultTimelineOptions';
|
||||
export * from './settings/defaults/defaultTimelineKeyframeStyle';
|
||||
export * from './settings/defaults/defaultTimelineConsts';
|
||||
export * from './settings/defaults/defaultGroupStyle';
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum TimelineCapShape {
|
||||
None = 'none',
|
||||
Triangle = 'triangle',
|
||||
Rect = 'rect',
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export enum TimelineCursorType {
|
||||
Alias = 'alias',
|
||||
AllScroll = 'all-scroll',
|
||||
Auto = 'auto',
|
||||
Cell = 'cell',
|
||||
ContextMenu = 'context-menu',
|
||||
ColResize = 'col-resize',
|
||||
Copy = 'copy',
|
||||
Crosshair = 'crosshair',
|
||||
Default = 'default',
|
||||
EResize = 'e-resize',
|
||||
EWResize = 'ew-resize',
|
||||
Grab = 'grab',
|
||||
Grabbing = 'grabbing',
|
||||
Help = 'help',
|
||||
Move = 'move',
|
||||
NResize = 'n-resize',
|
||||
NEResize = 'ne-resize',
|
||||
NESWResize = 'nesw-resize',
|
||||
NSResize = 'ns-resize',
|
||||
NWResize = 'nw-resize',
|
||||
NWSEResize = 'nwse-resize',
|
||||
NoDrop = 'no-drop',
|
||||
None = 'none',
|
||||
NotAllowed = 'not-allowed',
|
||||
Pointer = 'pointer',
|
||||
Progress = 'progress',
|
||||
RowResize = 'row-resize',
|
||||
SResize = 's-resize',
|
||||
SEResize = 'se-resize',
|
||||
SWResize = 'sw-resize',
|
||||
Text = 'text',
|
||||
WResize = 'w-resize',
|
||||
Wait = 'wait',
|
||||
ZoomIn = 'zoom-in',
|
||||
ZoomOut = 'zoom-out',
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Internal element type.
|
||||
*/
|
||||
export enum TimelineElementType {
|
||||
/**
|
||||
* Timeline
|
||||
*/
|
||||
Timeline = 'timeline',
|
||||
/**
|
||||
* Keyframes
|
||||
*/
|
||||
Keyframe = 'keyframe',
|
||||
/**
|
||||
* Keyframes connected and presenting one group.
|
||||
*/
|
||||
Group = 'group',
|
||||
/**
|
||||
* Timeline row.
|
||||
*/
|
||||
Row = 'row',
|
||||
None = 'none',
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export enum TimelineEventSource {
|
||||
/**
|
||||
* Changed by user interaction events.
|
||||
*/
|
||||
User = 'user',
|
||||
/**
|
||||
* Changed programmatically.
|
||||
*/
|
||||
Programmatically = 'programmatically',
|
||||
/**
|
||||
* Changed by the set time function.
|
||||
*/
|
||||
SetTimeMethod = 'setTimeMethod',
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Event names of the component.
|
||||
*/
|
||||
export enum TimelineEvents {
|
||||
Selected = 'selected',
|
||||
TimeChanged = 'timechanged',
|
||||
KeyframeChanged = 'keyframeChanged',
|
||||
DragStarted = 'dragStarted',
|
||||
Drag = 'drag',
|
||||
DragFinished = 'dragFinished',
|
||||
Scroll = 'scroll',
|
||||
ScrollFinished = 'scrollFinished',
|
||||
ContextMenu = 'onContextMenu',
|
||||
DoubleClick = 'doubleClick',
|
||||
MouseDown = 'mouseDown',
|
||||
Zoom = 'zoom',
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export enum TimelineInteractionMode {
|
||||
/**
|
||||
* Keyframe selection tool selecting single or group of keyframes.
|
||||
*/
|
||||
Selection = 'selection',
|
||||
/**
|
||||
* Pan tool with the possibility to select keyframes.
|
||||
*/
|
||||
Pan = 'pan',
|
||||
/**
|
||||
* Allow only pan without any keyframes interaction.
|
||||
* Timeline still can be moved and controlled by option 'timelineDraggable'.
|
||||
*/
|
||||
NonInteractivePan = 'nonInteractivePan',
|
||||
/**
|
||||
* Zoom tool.
|
||||
*/
|
||||
Zoom = 'zoom',
|
||||
|
||||
/**
|
||||
* No iteraction, except moving a timeline.
|
||||
* Timeline still can be moved and controlled by option 'timelineDraggable'.
|
||||
*/
|
||||
None = 'none',
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum TimelineKeyframeShape {
|
||||
None = 'none',
|
||||
Rhomb = 'rhomb',
|
||||
Circle = 'circle',
|
||||
Rect = 'rect',
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum TimelineScrollSource {
|
||||
DefaultMode = 'none',
|
||||
ZoomMode = 'zoom',
|
||||
ScrollBySelection = 'scrollBySelection',
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Timeline selection event type.
|
||||
*/
|
||||
export enum TimelineSelectionEventSource {
|
||||
/**
|
||||
* Keyframe selection is performed.
|
||||
*/
|
||||
Keyframes = 'keyframes',
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Timeline selection mode.
|
||||
*/
|
||||
export enum TimelineSelectionMode {
|
||||
/**
|
||||
* Select new items. deselect changed.
|
||||
*/
|
||||
Normal = 'normal',
|
||||
/**
|
||||
* Append current selection.
|
||||
*/
|
||||
Append = 'append',
|
||||
/**
|
||||
* Revert selection of a specified nodes.
|
||||
*/
|
||||
Revert = 'revert',
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { TimelineKeyframeStyle } from '../settings/styles/timelineKeyframeStyle';
|
||||
import { TimelineGroupStyle } from '../settings/styles/timelineGroupStyle';
|
||||
/**
|
||||
* Timeline group view model.
|
||||
*/
|
||||
export interface TimelineGroup {
|
||||
/**
|
||||
* Group style.
|
||||
*/
|
||||
style: TimelineGroupStyle;
|
||||
/**
|
||||
* Child keyframes style.
|
||||
*/
|
||||
keyframesStyle?: TimelineKeyframeStyle;
|
||||
/**
|
||||
* Whether group is draggable.
|
||||
* Considered to be false when really set as false.
|
||||
*/
|
||||
draggable?: boolean;
|
||||
/**
|
||||
* Whether group keyframes are draggable.
|
||||
*/
|
||||
keyframesDraggable?: boolean;
|
||||
/**
|
||||
* Whether group is hidden.
|
||||
*/
|
||||
hidden?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TimelineKeyframeStyle } from '../settings/styles/timelineKeyframeStyle';
|
||||
import { TimelineSelectable } from '../utils/timelineSelectable';
|
||||
import { TimelineRanged } from './timelineRanged';
|
||||
import { TimelineGroup } from './timelineGroup';
|
||||
|
||||
export interface TimelineKeyframe extends TimelineSelectable, TimelineRanged {
|
||||
/**
|
||||
* Keyframe value.
|
||||
*/
|
||||
val: number;
|
||||
/**
|
||||
* Related keyframe group.
|
||||
* Timeline keyframes groups are rendered as one instance.
|
||||
*/
|
||||
group?: string | TimelineGroup;
|
||||
/**
|
||||
* Keyframe style.
|
||||
*/
|
||||
style?: TimelineKeyframeStyle;
|
||||
/**
|
||||
* Whether keyframe is hidden.
|
||||
*/
|
||||
hidden?: boolean;
|
||||
/**
|
||||
* Whether group is draggable.
|
||||
* Considered to be false when really set as false.
|
||||
*/
|
||||
draggable?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { TimelineRow } from './timelineRow';
|
||||
export interface TimelineModel {
|
||||
rows: TimelineRow[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface TimelineRanged {
|
||||
/**
|
||||
* min
|
||||
*/
|
||||
min?: number | null;
|
||||
/**
|
||||
* max.
|
||||
*/
|
||||
max?: number | null;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { TimelineKeyframe } from './timelineKeyframe';
|
||||
import { TimelineRowStyle } from '../settings/styles/timelineRowStyle';
|
||||
import { TimelineRanged } from './timelineRanged';
|
||||
|
||||
export interface TimelineRow extends TimelineRanged {
|
||||
style?: TimelineRowStyle;
|
||||
keyframes?: TimelineKeyframe[] | null;
|
||||
hidden?: boolean;
|
||||
/**
|
||||
* Whether group keyframes are draggable.
|
||||
*/
|
||||
keyframesDraggable?: boolean;
|
||||
groupsDraggable?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { TimelineGroupStyle } from '../styles/timelineGroupStyle';
|
||||
export const defaultRowHeight = 24;
|
||||
const margin = 4;
|
||||
export const defaultGroupStyle = {
|
||||
height: 'auto',
|
||||
marginTop: margin,
|
||||
/**
|
||||
* Default group fill color.
|
||||
*/
|
||||
fillColor: '#094771',
|
||||
/**
|
||||
* 组默认文本设置
|
||||
*/
|
||||
text: {
|
||||
label: "",
|
||||
isStroke:false,
|
||||
font: '10px sans-serif',
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
direction:"inherit",
|
||||
fillColor: '#fff'
|
||||
}
|
||||
} as TimelineGroupStyle;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { TimelineConsts } from '../timelineConsts';
|
||||
|
||||
export const defaultTimelineConsts: TimelineConsts = {
|
||||
/**
|
||||
* Private. Auto pan speed.
|
||||
*/
|
||||
autoPanSpeed: 50,
|
||||
/**
|
||||
* Private. scroll speed when mouse drag is used (from 0 to 1)
|
||||
*/
|
||||
scrollByDragSpeed: 0.12,
|
||||
/**
|
||||
* Private. Determine whether item was clicked.
|
||||
*/
|
||||
clickDetectionMs: 120,
|
||||
/**
|
||||
* Private. Timeout to detect double click.
|
||||
*/
|
||||
doubleClickTimeoutMs: 400,
|
||||
/**
|
||||
* Private. Time in ms used to refresh scrollbars when pan is finished.
|
||||
*/
|
||||
scrollFinishedTimeoutMs: 500,
|
||||
/**
|
||||
* Private. Auto pan padding
|
||||
*/
|
||||
autoPanByScrollPadding: 10,
|
||||
/**
|
||||
* Private. Click threshold
|
||||
*/
|
||||
clickThreshold: 3,
|
||||
/**
|
||||
* Private. Private.Click min radius for the elements detection.
|
||||
*/
|
||||
clickDetectionMinRadius: 2,
|
||||
/**
|
||||
* Private. Skip some auto pan/scroll actions if they are executed more rapid than this value.
|
||||
*/
|
||||
autoPanSpeedLimit: 10,
|
||||
/**
|
||||
* Private. Default auto size for the group. It's percents.
|
||||
*/
|
||||
defaultGroupHeight: 0.7,
|
||||
} as TimelineConsts;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { TimelineKeyframeShape } from '../../enums/timelineKeyframeShape';
|
||||
import { TimelineKeyframeStyle } from '../styles/timelineKeyframeStyle';
|
||||
|
||||
export const defaultTimelineKeyframeStyle = {
|
||||
/**
|
||||
* keyframe fill color.
|
||||
*/
|
||||
fillColor: 'DarkOrange',
|
||||
shape: TimelineKeyframeShape.Rhomb,
|
||||
/**
|
||||
* Selected keyframe fill color.
|
||||
*/
|
||||
selectedFillColor: 'red',
|
||||
strokeColor: 'black',
|
||||
selectedStrokeColor: 'black',
|
||||
strokeThickness: 0.2,
|
||||
height: 'auto',
|
||||
width: 'auto',
|
||||
} as TimelineKeyframeStyle;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { TimelineOptions } from '../timelineOptions';
|
||||
import { defaultGroupStyle } from './defaultGroupStyle';
|
||||
import { defaultTimelineKeyframeStyle } from './defaultTimelineKeyframeStyle';
|
||||
import { defaultTimelineRowStyle } from './defaultTimelineRowStyle';
|
||||
import { defaultTimelineStyle } from './defaultTimelineStyle';
|
||||
|
||||
export const defaultTimelineOptions = {
|
||||
/**
|
||||
* Snap all selected keyframes as a bundle during the drag.
|
||||
*/
|
||||
snapAllKeyframesOnMove: false,
|
||||
|
||||
/**
|
||||
* Check whether snapping is enabled.
|
||||
*/
|
||||
snapEnabled: true,
|
||||
|
||||
/**
|
||||
* Timeline style.
|
||||
*/
|
||||
timelineStyle: defaultTimelineStyle,
|
||||
/**
|
||||
* approximate step for the timeline in pixels for 1 second
|
||||
*/
|
||||
stepPx: 120,
|
||||
/**
|
||||
* Number of units that should fit into one stepPx. (1 second by a default)
|
||||
*/
|
||||
stepVal: 1000,
|
||||
stepSmallPx: 30,
|
||||
/**
|
||||
* Snap step in units. from 0 to stepVal
|
||||
*/
|
||||
snapStep: 200,
|
||||
/**
|
||||
* additional left margin in pixels to start the line gauge from.
|
||||
*/
|
||||
leftMargin: 25,
|
||||
headerFillColor: '#101011',
|
||||
fillColor: '#101011',
|
||||
|
||||
labelsColor: '#D5D5D5',
|
||||
/**
|
||||
* Header gauge tick color.
|
||||
*/
|
||||
tickColor: '#D5D5D5',
|
||||
/**
|
||||
* Selection rectangle color.
|
||||
*/
|
||||
selectionColor: 'White',
|
||||
|
||||
/**
|
||||
* Default rows style.
|
||||
* Can be overridden by setting style individually for each row.
|
||||
*/
|
||||
rowsStyle: defaultTimelineRowStyle,
|
||||
|
||||
/**
|
||||
* Style for the all keyframes in a current row.
|
||||
* Individual keyframe can have own style.
|
||||
*/
|
||||
keyframesStyle: defaultTimelineKeyframeStyle,
|
||||
/**
|
||||
* Style of the groups.
|
||||
*/
|
||||
groupsStyle: defaultGroupStyle,
|
||||
/**
|
||||
* Header height in pixels
|
||||
*/
|
||||
headerHeight: 30,
|
||||
font: '11px sans-serif',
|
||||
/**
|
||||
* Default zoom level = 1. where screen pixels are equals to the corresponding stepVal stepPx.
|
||||
*/
|
||||
zoom: 1,
|
||||
/**
|
||||
* Default zoom speed.
|
||||
*/
|
||||
zoomSpeed: 0.1,
|
||||
/**
|
||||
* Max zoom value.
|
||||
*/
|
||||
zoomMin: 0.1,
|
||||
/**
|
||||
* Min zoom value.
|
||||
*/
|
||||
zoomMax: 8,
|
||||
/**
|
||||
* Set this to true in a MAC OS environment: The Meta key will be used instead of the Ctrl key.
|
||||
*/
|
||||
controlKeyIsMetaKey: false,
|
||||
/**
|
||||
* Access the scroll container via this class for e.g. scroll bar styling.
|
||||
*/
|
||||
scrollContainerClass: 'scroll-container',
|
||||
/**
|
||||
* keyframes group is draggable.
|
||||
*/
|
||||
groupsDraggable: true,
|
||||
/**
|
||||
* keyframes are draggable.
|
||||
*/
|
||||
keyframesDraggable: true,
|
||||
/**
|
||||
* Timeline can be dragged or position can be changed by user interaction. Default: true
|
||||
*/
|
||||
timelineDraggable: true,
|
||||
/**
|
||||
* Start drawing timeline from this min point.
|
||||
* Bounds for the keyframe dragging.
|
||||
*/
|
||||
min: 0,
|
||||
/**
|
||||
* Max bounds timeline can navigate to.
|
||||
*/
|
||||
max: Number.MAX_VALUE,
|
||||
} as TimelineOptions;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { TimelineRowStyle } from '../styles/timelineRowStyle';
|
||||
import { defaultGroupStyle, defaultRowHeight } from './defaultGroupStyle';
|
||||
import { defaultTimelineKeyframeStyle } from './defaultTimelineKeyframeStyle';
|
||||
|
||||
export const defaultTimelineRowStyle = {
|
||||
/**
|
||||
* Row height in pixels.
|
||||
*/
|
||||
height: defaultRowHeight,
|
||||
marginBottom: 2,
|
||||
fillColor: '#252526',
|
||||
/**
|
||||
* Style for the all keyframes in a current row.
|
||||
* Individual keyframe can have own style.
|
||||
*/
|
||||
keyframesStyle: defaultTimelineKeyframeStyle,
|
||||
/**
|
||||
* Style of the groups.
|
||||
*/
|
||||
groupsStyle: defaultGroupStyle,
|
||||
} as TimelineRowStyle;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TimelineCapShape } from '../../enums/timelineCapShape';
|
||||
import { TimelineCursorType } from '../../enums/timelineCursorType';
|
||||
import { TimelineStyle } from '../styles/timelineStyle';
|
||||
|
||||
export const defaultTimelineStyle = {
|
||||
width: 2,
|
||||
marginTop: 15,
|
||||
marginBottom: 0,
|
||||
strokeColor: 'DarkOrange',
|
||||
fillColor: 'DarkOrange',
|
||||
capStyle: {
|
||||
width: 4,
|
||||
height: 10,
|
||||
/**
|
||||
* Draw timeline rectangular cap.
|
||||
*/
|
||||
capType: TimelineCapShape.Rect,
|
||||
strokeColor: 'DarkOrange',
|
||||
fillColor: 'DarkOrange',
|
||||
},
|
||||
cursor: TimelineCursorType.EWResize,
|
||||
} as TimelineStyle;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { TimelineCapShape } from '../../enums/timelineCapShape';
|
||||
|
||||
/**
|
||||
* Timeline active/current value indicator style.
|
||||
*/
|
||||
export interface TimelineCapStyle {
|
||||
/**
|
||||
* Cap style width in pixels.
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* Cap style height in pixels.
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* Cap stroke color.
|
||||
*/
|
||||
strokeColor?: string;
|
||||
/**
|
||||
* Cap fill color.
|
||||
*/
|
||||
fillColor?: string;
|
||||
/**
|
||||
* Cap type
|
||||
*/
|
||||
capType?: TimelineCapShape;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { TimelineCursorType } from '../../enums/timelineCursorType';
|
||||
import { TimelineKeyframeStyle } from '../styles/timelineKeyframeStyle';
|
||||
|
||||
/**
|
||||
* Timeline group style.
|
||||
*/
|
||||
export interface TimelineGroupStyle {
|
||||
/**
|
||||
* Keyframes style height in pixels.
|
||||
* 'auto' to automatically calculate.
|
||||
*/
|
||||
height?: number | string;
|
||||
/**
|
||||
* Group stroke color.
|
||||
*/
|
||||
strokeColor?: string;
|
||||
/**
|
||||
* Group stroke thickness.
|
||||
*/
|
||||
strokeThickness?: number | null;
|
||||
|
||||
/**
|
||||
* Group border radius. See canvas roundRect official documentation.
|
||||
*/
|
||||
radii?: number | DOMPointInit | Iterable<number | DOMPointInit>;
|
||||
|
||||
/**
|
||||
* Group fill color.
|
||||
*/
|
||||
fillColor?: string;
|
||||
/**
|
||||
* Group mouse over cursor style.
|
||||
*/
|
||||
cursor?: TimelineCursorType;
|
||||
/**
|
||||
* Margin top in px or 'auto' to center element.
|
||||
*/
|
||||
marginTop?: number | string;
|
||||
/**
|
||||
* Style for all the keyframes in the current group.
|
||||
*/
|
||||
keyframesStyle?: TimelineKeyframeStyle;
|
||||
|
||||
/**
|
||||
* 组默认文本设置
|
||||
*/
|
||||
text?: {
|
||||
label?: string,
|
||||
isStroke?: boolean,
|
||||
font?: string,
|
||||
textAlign?: "start" | "end" | "left" | "right" | "center",
|
||||
textBaseline?: "top" | "hanging" | "middle" | "alphabetic" | "ideographic" | "bottom",
|
||||
direction?: "ltr" | "rtl" | "inherit",
|
||||
fillColor?: string,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TimelineCursorType } from '../../enums/timelineCursorType';
|
||||
import { TimelineKeyframeShape } from '../../enums/timelineKeyframeShape';
|
||||
|
||||
export interface TimelineKeyframeStyle {
|
||||
/**
|
||||
* Timeline cursor style.
|
||||
*/
|
||||
cursor?: TimelineCursorType;
|
||||
/**
|
||||
* Timeline keyframe shape
|
||||
*/
|
||||
shape?: TimelineKeyframeShape;
|
||||
/**
|
||||
* keyframe size, number or text 'auto'
|
||||
*/
|
||||
height?: number | string;
|
||||
/**
|
||||
* keyframe size, number or text 'auto'
|
||||
*/
|
||||
width?: number | string;
|
||||
/**
|
||||
* Keyframe fill color
|
||||
*/
|
||||
fillColor?: string | null;
|
||||
/**
|
||||
* Keyframe selected fill color.
|
||||
*/
|
||||
selectedFillColor?: string | null;
|
||||
/**
|
||||
* Keyframe stroke color.
|
||||
*/
|
||||
strokeColor?: string | null;
|
||||
/**
|
||||
* Keyframe selected stroke color.
|
||||
*/
|
||||
selectedStrokeColor?: string | null;
|
||||
/**
|
||||
* Keyframe stroke Thickness.
|
||||
*/
|
||||
strokeThickness?: number | null;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TimelineGroupStyle } from './timelineGroupStyle';
|
||||
import { TimelineKeyframeStyle } from './timelineKeyframeStyle';
|
||||
|
||||
/**
|
||||
* Style of the row.
|
||||
*/
|
||||
export interface TimelineRowStyle {
|
||||
/**
|
||||
* Size of the row in pixels.
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* Track fill color.
|
||||
*/
|
||||
fillColor?: string;
|
||||
/**
|
||||
* Row margin bottom in pixels between tracks/rows.
|
||||
*/
|
||||
marginBottom?: number;
|
||||
/**
|
||||
* Style for the all keyframes in a current row.
|
||||
* Individual keyframe can have own style.
|
||||
*/
|
||||
keyframesStyle?: TimelineKeyframeStyle;
|
||||
/**
|
||||
* Style of the groups. Keyframe groups can be also styles separately.
|
||||
*/
|
||||
groupsStyle?: TimelineGroupStyle;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TimelineCursorType } from '../../enums/timelineCursorType';
|
||||
import { TimelineCapStyle } from './timelineCapStyle';
|
||||
|
||||
/**
|
||||
* Timeline active/current value indicator style.
|
||||
*/
|
||||
export interface TimelineStyle {
|
||||
width?: number;
|
||||
/**
|
||||
* Margin top in pixels.
|
||||
*/
|
||||
marginTop?: number;
|
||||
/**
|
||||
* Margin bottom in pixels.
|
||||
*/
|
||||
marginBottom?: number;
|
||||
/**
|
||||
* Timeline top cap style.
|
||||
*/
|
||||
capStyle?: TimelineCapStyle;
|
||||
/**
|
||||
* Timeline indicator stroke color.
|
||||
*/
|
||||
strokeColor?: string;
|
||||
/**
|
||||
* Timeline fill color.
|
||||
*/
|
||||
fillColor?: string;
|
||||
/**
|
||||
* Timeline cursor.
|
||||
*/
|
||||
cursor?: TimelineCursorType;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Internal components consts.
|
||||
* Still can be changed thru private property _consts but this is discouraged.
|
||||
*/
|
||||
export interface TimelineConsts {
|
||||
/**
|
||||
* Auto pan speed.
|
||||
*/
|
||||
autoPanSpeed: number;
|
||||
/**
|
||||
* scroll speed when mouse drag is used (from 0 to 1)
|
||||
*/
|
||||
scrollByDragSpeed: number;
|
||||
/**
|
||||
* Determine whether item was clicked.
|
||||
*/
|
||||
clickDetectionMs: number;
|
||||
/**
|
||||
* Timeout to detect double click.
|
||||
*/
|
||||
doubleClickTimeoutMs: number;
|
||||
/**
|
||||
* Time in ms used to refresh scrollbars when pan is finished.
|
||||
*/
|
||||
scrollFinishedTimeoutMs: number;
|
||||
/**
|
||||
* Auto pan padding
|
||||
*/
|
||||
autoPanByScrollPadding: number;
|
||||
/**
|
||||
* Click threshold
|
||||
*/
|
||||
clickThreshold: number;
|
||||
|
||||
/**
|
||||
* Click min radius for the elements detection.
|
||||
*/
|
||||
clickDetectionMinRadius: number;
|
||||
/**
|
||||
* Default auto size for the group. It's percents.
|
||||
*/
|
||||
autoPanSpeedLimit: number;
|
||||
/**
|
||||
* Default auto size for the group. It's percents.
|
||||
*/
|
||||
defaultGroupHeight: number;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { TimelineRowStyle } from './styles/timelineRowStyle';
|
||||
import { TimelineStyle } from './styles/timelineStyle';
|
||||
import { TimelineRanged } from '../models/timelineRanged';
|
||||
|
||||
export interface TimelineOptions extends TimelineRanged {
|
||||
/**
|
||||
* Id 或时间轴容器的 HTMLElement
|
||||
*/
|
||||
id?: string | HTMLElement | null;
|
||||
/**
|
||||
* 检查是否启用了捕捉
|
||||
*/
|
||||
snapEnabled?: boolean;
|
||||
/**
|
||||
* 拖动时将所有选定的关键帧合并为一个包.
|
||||
*/
|
||||
snapAllKeyframesOnMove?: boolean;
|
||||
/**
|
||||
* 以像素为单位的时间轴的近似步长为1秒
|
||||
*/
|
||||
stepPx?: number;
|
||||
/**
|
||||
* 一个步骤中应该包含的点的个数。
|
||||
*/
|
||||
stepVal?: number;
|
||||
stepSmallPx?: number;
|
||||
/**
|
||||
* 以单位表示。从0到stepVal
|
||||
*/
|
||||
snapStep?: number;
|
||||
/**
|
||||
* 以像素为单位增加左页边距,从该值开始计算行距.
|
||||
*/
|
||||
leftMargin?: number;
|
||||
/**
|
||||
* 组件header背景颜色.
|
||||
*/
|
||||
headerFillColor?: string;
|
||||
/**
|
||||
* 组件背景颜色.
|
||||
*/
|
||||
fillColor?: string;
|
||||
/**
|
||||
* 标题标签颜色.
|
||||
*/
|
||||
labelsColor?: string;
|
||||
/**
|
||||
* 标题刻度条颜色.
|
||||
*/
|
||||
tickColor?: string;
|
||||
/**
|
||||
* 选择矩形颜色.
|
||||
*/
|
||||
selectionColor?: string;
|
||||
|
||||
/**
|
||||
* 标题高度(以像素为单位)。
|
||||
*/
|
||||
headerHeight?: number;
|
||||
/**
|
||||
* Header ticks font
|
||||
*/
|
||||
font?: string;
|
||||
/**
|
||||
* Default zoom level = 1. where screen pixels are equals to the corresponding stepVal stepPx.
|
||||
*/
|
||||
zoom?: number;
|
||||
/**
|
||||
* Default zoom speed.
|
||||
*/
|
||||
zoomSpeed?: number;
|
||||
/**
|
||||
* Max zoom value.
|
||||
*/
|
||||
zoomMin?: number;
|
||||
/**
|
||||
* Min zoom value.
|
||||
*/
|
||||
zoomMax?: number;
|
||||
/**
|
||||
* Set this to true in a MAC OS environment: The Meta key will be used instead of the Ctrl key.
|
||||
*/
|
||||
controlKeyIsMetaKey?: boolean;
|
||||
/**
|
||||
* Access the scroll container via this class for e.g. scroll bar styling.
|
||||
*/
|
||||
scrollContainerClass?: string;
|
||||
/**
|
||||
* Default rows style.
|
||||
* Can be overridden by setting style individually for each row.
|
||||
*/
|
||||
rowsStyle?: TimelineRowStyle;
|
||||
/**
|
||||
* Timeline indicator style.
|
||||
*/
|
||||
timelineStyle?: TimelineStyle;
|
||||
|
||||
/**
|
||||
* keyframes group is draggable. Default: true
|
||||
*/
|
||||
groupsDraggable?: boolean;
|
||||
/**
|
||||
* keyframes group is draggable. Default: true
|
||||
*/
|
||||
keyframesDraggable?: boolean;
|
||||
/**
|
||||
* Timeline can be dragged or position can be changed by user interaction. Default: true
|
||||
*/
|
||||
timelineDraggable?: boolean;
|
||||
|
||||
/**
|
||||
* Array of the denominators used to determine 'beautiful' numbers to be rendered for the gauge.
|
||||
* Default: [1, 2, 5, 10];
|
||||
*/
|
||||
denominators?: number[];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user