@threepipe/plugin-3d-tiles-renderer
Version:
Interface for 3d-tiles-renderer
1,447 lines (1,446 loc) • 397 kB
JavaScript
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("threepipe")) : typeof define === "function" && define.amd ? define(["exports", "threepipe"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["@threepipe/plugin-3d-tiles-renderer"] = {}, global.threepipe));
})(this, function(exports2, THREE) {
"use strict";/**
* @license
* @threepipe/plugin-3d-tiles-renderer v0.3.2
* Copyright 2022-2025 repalash <palash@shaders.app>
* Apache-2.0 License
* See ./dependencies.txt for any bundled third-party dependencies and licenses.
*/
function _interopNamespaceDefault(e) {
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
if (e) {
for (const k in e) {
if (k !== "default") {
const d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: () => e[k]
});
}
}
}
n.default = e;
return Object.freeze(n);
}
const THREE__namespace = /* @__PURE__ */ _interopNamespaceDefault(THREE);
function getUrlExtension(url) {
if (!url) {
return null;
}
const filename = url.replace(/[a-z]+:\/\/[^/]+/i, "").replace(/\?.*$/i, "").replace(/.*\//g, "");
const lastPeriod = filename.lastIndexOf(".");
if (lastPeriod === -1) {
return null;
}
return filename.substring(lastPeriod + 1) || null;
}
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() {
this.minSize = 6e3;
this.maxSize = 8e3;
this.minBytesSize = 0.3 * GIGABYTE_BYTES;
this.maxBytesSize = 0.4 * GIGABYTE_BYTES;
this.unloadPercent = 0.05;
this.autoMarkUnused = true;
this.itemSet = /* @__PURE__ */ new Map();
this.itemList = [];
this.usedSet = /* @__PURE__ */ new Set();
this.callbacks = /* @__PURE__ */ new Map();
this.unloadingHandle = -1;
this.cachedBytes = 0;
this.bytesMap = /* @__PURE__ */ new Map();
this.loadedSet = /* @__PURE__ */ new Set();
this._unloadPriorityCallback = null;
this.computeMemoryUsageCallback = () => 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) ?? null;
}
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;
const bytesMap = this.bytesMap;
itemList.push(item);
usedSet.add(item);
itemSet.set(item, Date.now());
callbacks.set(item, removeCb);
const bytes = this.computeMemoryUsageCallback(item);
this.cachedBytes += bytes || 0;
bytesMap.set(item, bytes);
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);
}
}
}
updateMemoryUsage(item) {
const itemSet = this.itemSet;
const bytesMap = this.bytesMap;
if (!itemSet.has(item)) {
return;
}
this.cachedBytes -= bytesMap.get(item) || 0;
const bytes = this.computeMemoryUsageCallback(item);
bytesMap.set(item, bytes);
this.cachedBytes += bytes;
}
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) {
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) {
return -unloadPriorityCallback(a, b);
} else {
return loadedA ? 1 : -1;
}
} else {
return usedA ? 1 : -1;
}
});
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;
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++;
}
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++;
}
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);
});
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();
});
}
}
}
class PriorityQueue {
// returns whether tasks are queued or actively running
get running() {
return this.items.length !== 0 || this.currJobs !== 0;
}
constructor() {
this.maxJobs = 6;
this.items = [];
this.callbacks = /* @__PURE__ */ new Map();
this.currJobs = 0;
this.scheduled = false;
this.autoUpdate = true;
this.priorityCallback = () => {
throw new Error("PriorityQueue: PriorityCallback function not defined.");
};
this.schedulingCallback = (func) => {
requestAnimationFrame(func);
};
this._runjobs = () => {
this.scheduled = false;
this.tryRunJobs();
};
}
sort() {
const priorityCallback2 = this.priorityCallback;
const items = this.items;
items.sort(priorityCallback2);
}
has(item) {
return this.callbacks.has(item);
}
add(item, callback) {
return new Promise((resolve, reject) => {
const items = this.items;
const callbacks = this.callbacks;
items.push(item);
callbacks.set(item, {
callback,
resolve,
reject
});
if (this.autoUpdate) {
this.scheduleJobRun();
}
});
}
remove(item) {
const items = this.items;
const callbacks = this.callbacks;
const index = items.indexOf(item);
if (index !== -1) {
items.splice(index, 1);
callbacks.delete(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;
}
}
}
const FAILED = -1;
const UNLOADED = 0;
const LOADING = 1;
const PARSING = 2;
const LOADED = 3;
const WGS84_RADIUS = 6378137;
const WGS84_HEIGHT = 6356752314245179e-9;
const viewErrorTarget$1 = {
inView: false,
error: Infinity,
distance: Infinity
};
function isDownloadFinished(value) {
return value === LOADED || value === FAILED;
}
function isUsedThisFrame(tile, frameCount) {
return tile.__lastFrameVisited === frameCount && tile.__used;
}
function areChildrenProcessed(tile) {
return tile.__childrenProcessed === tile.children.length;
}
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.__childrenWereVisible = false;
tile.__allChildrenLoaded = false;
renderer.calculateTileViewError(tile, viewErrorTarget$1);
tile.__inFrustum = viewErrorTarget$1.inView;
tile.__error = viewErrorTarget$1.error;
tile.__distanceFromCamera = viewErrorTarget$1.distance;
}
}
function recursivelyMarkUsed(tile, renderer) {
renderer.ensureChildrenArePreprocessed(tile);
resetFrameState(tile, renderer);
markUsed(tile, renderer);
if (!tile.__hasRenderableContent && areChildrenProcessed(tile)) {
const children = tile.children;
for (let i = 0, l = children.length; i < l; i++) {
recursivelyMarkUsed(children[i], renderer);
}
}
}
function recursivelyLoadNextRenderableTiles(tile, renderer) {
renderer.ensureChildrenArePreprocessed(tile);
if (isUsedThisFrame(tile, renderer.frameCount)) {
if (tile.__hasContent && tile.__loadingState === UNLOADED && !renderer.lruCache.isFull()) {
renderer.queueTileForDownload(tile);
}
if (areChildrenProcessed(tile)) {
const children = tile.children;
for (let i = 0, l = children.length; i < l; i++) {
recursivelyLoadNextRenderableTiles(children[i], renderer);
}
}
}
}
function markUsed(tile, renderer) {
if (tile.__used) {
return;
}
tile.__used = true;
renderer.markTileUsed(tile);
renderer.stats.used++;
if (tile.__inFrustum === true) {
renderer.stats.inFrustum++;
}
}
function canTraverse(tile, renderer) {
if (tile.__error <= renderer.errorTarget) {
return false;
}
if (renderer.maxDepth > 0 && tile.__depth + 1 >= renderer.maxDepth) {
return false;
}
if (!areChildrenProcessed(tile)) {
return false;
}
return true;
}
function traverseSet(tile, beforeCb = null, afterCb = null) {
const stack = [];
stack.push(tile);
stack.push(null);
stack.push(0);
while (stack.length > 0) {
const depth = stack.pop();
const parent = stack.pop();
const tile2 = stack.pop();
if (beforeCb && beforeCb(tile2, parent, depth)) {
if (afterCb) {
afterCb(tile2, parent, depth);
}
return;
}
const children = tile2.children;
if (children) {
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i]);
stack.push(tile2);
stack.push(depth + 1);
}
}
if (afterCb) {
afterCb(tile2, parent, depth);
}
}
}
function markUsedTiles(tile, renderer) {
renderer.ensureChildrenArePreprocessed(tile);
resetFrameState(tile, renderer);
if (!tile.__inFrustum) {
return;
}
if (!canTraverse(tile, renderer)) {
markUsed(tile, renderer);
return;
}
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;
}
if (tile.refine === "REPLACE" && !anyChildrenInFrustum && children.length !== 0 && !tile.__hasUnrenderableContent) {
tile.__inFrustum = false;
return;
}
markUsed(tile, renderer);
if (anyChildrenUsed && tile.refine === "REPLACE") {
for (let i = 0, l = children.length; i < l; i++) {
const c = children[i];
recursivelyMarkUsed(c, renderer);
}
}
}
function markUsedSetLeaves(tile, renderer) {
const frameCount = renderer.frameCount;
if (!isUsedThisFrame(tile, frameCount)) {
return;
}
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 childrenWereVisible = false;
let allChildrenLoaded = true;
for (let i = 0, l = children.length; i < l; i++) {
const c = children[i];
markUsedSetLeaves(c, renderer);
childrenWereVisible = childrenWereVisible || c.__wasSetVisible || c.__childrenWereVisible;
if (isUsedThisFrame(c, frameCount)) {
const childLoaded = c.__allChildrenLoaded || c.__hasRenderableContent && isDownloadFinished(c.__loadingState) || !c.__hasContent && c.children.length === 0 || c.__hasUnrenderableContent && c.__loadingState === FAILED;
allChildrenLoaded = allChildrenLoaded && childLoaded;
}
}
tile.__childrenWereVisible = childrenWereVisible;
tile.__allChildrenLoaded = allChildrenLoaded;
}
}
function markVisibleTiles(tile, renderer) {
const stats = renderer.stats;
if (!isUsedThisFrame(tile, renderer.frameCount)) {
return;
}
const lruCache = renderer.lruCache;
if (tile.__isLeaf) {
if (tile.__loadingState === LOADED) {
if (tile.__inFrustum) {
tile.__visible = true;
stats.visible++;
}
tile.__active = true;
stats.active++;
} else if (!lruCache.isFull() && 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 childrenWereVisible = tile.__childrenWereVisible;
const allChildrenLoaded = tile.__allChildrenLoaded;
const includeTile = meetsSSE || tile.refine === "ADD";
if (includeTile && !loadedContent && !lruCache.isFull() && hasContent) {
renderer.queueTileForDownload(tile);
}
if (meetsSSE && !allChildrenLoaded && !childrenWereVisible && loadedContent || tile.refine === "ADD" && loadedContent) {
if (tile.__inFrustum) {
tile.__visible = true;
stats.visible++;
}
tile.__active = true;
stats.active++;
}
if (tile.refine === "REPLACE" && meetsSSE && !allChildrenLoaded) {
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);
}
}
}
function toggleTiles(tile, renderer) {
const isUsed = isUsedThisFrame(tile, renderer.frameCount);
if (isUsed || tile.__usedLastFrame) {
let setActive = false;
let setVisible = false;
if (isUsed) {
setActive = tile.__active;
if (renderer.displayActiveTiles) {
setVisible = tile.__active || tile.__visible;
} else {
setVisible = tile.__visible;
}
} else {
resetFrameState(tile, renderer);
}
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);
}
}
}
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;
}
}
function throttle(callback) {
let handle = null;
return () => {
if (handle === null) {
handle = requestAnimationFrame(() => {
handle = null;
callback();
});
}
};
}
const PLUGIN_REGISTERED = Symbol("PLUGIN_REGISTERED");
const priorityCallback = (a, b) => {
if (a.__depthFromRenderedParent !== b.__depthFromRenderedParent) {
return a.__depthFromRenderedParent > b.__depthFromRenderedParent ? -1 : 1;
} else if (a.__inFrustum !== b.__inFrustum) {
return a.__inFrustum ? 1 : -1;
} else if (a.__used !== b.__used) {
return a.__used ? 1 : -1;
} else if (a.__error !== b.__error) {
return a.__error > b.__error ? 1 : -1;
} else if (a.__distanceFromCamera !== b.__distanceFromCamera) {
return a.__distanceFromCamera > b.__distanceFromCamera ? -1 : 1;
}
return 0;
};
const lruPriorityCallback = (a, b) => {
if (a.__depthFromRenderedParent !== b.__depthFromRenderedParent) {
return a.__depthFromRenderedParent > b.__depthFromRenderedParent ? 1 : -1;
} else if (a.__loadingState !== b.__loadingState) {
return a.__loadingState > b.__loadingState ? -1 : 1;
} else if (a.__lastFrameVisited !== b.__lastFrameVisited) {
return a.__lastFrameVisited > b.__lastFrameVisited ? -1 : 1;
} else if (a.__hasUnrenderableContent !== b.__hasUnrenderableContent) {
return a.__hasUnrenderableContent ? -1 : 1;
} else if (a.__error !== b.__error) {
return a.__error > b.__error ? -1 : 1;
}
return 0;
};
class TilesRendererBase {
get root() {
const tileSet = this.rootTileSet;
return tileSet ? tileSet.root : null;
}
get loadProgress() {
const { stats, isLoading } = this;
const loading = stats.downloading + stats.parsing;
const total = stats.inCacheSinceLoad + (isLoading ? 1 : 0);
return total === 0 ? 1 : 1 - loading / total;
}
get errorThreshold() {
return this._errorThreshold;
}
set errorThreshold(v) {
console.warn('TilesRenderer: The "errorThreshold" option has been deprecated.');
this._errorThreshold = v;
}
constructor(url = null) {
this.rootLoadingState = UNLOADED;
this.rootTileSet = null;
this.rootURL = url;
this.fetchOptions = {};
this.plugins = [];
this.queuedTiles = [];
this.cachedSinceLoadComplete = /* @__PURE__ */ new Set();
this.isLoading = false;
const lruCache = new LRUCache();
lruCache.unloadPriorityCallback = lruPriorityCallback;
const downloadQueue = new PriorityQueue();
downloadQueue.maxJobs = 10;
downloadQueue.priorityCallback = priorityCallback;
const parseQueue = new PriorityQueue();
parseQueue.maxJobs = 1;
parseQueue.priorityCallback = priorityCallback;
const processNodeQueue = new PriorityQueue();
processNodeQueue.maxJobs = 25;
processNodeQueue.priorityCallback = priorityCallback;
processNodeQueue.log = true;
this.visibleTiles = /* @__PURE__ */ new Set();
this.activeTiles = /* @__PURE__ */ new Set();
this.usedSet = /* @__PURE__ */ new Set();
this.lruCache = lruCache;
this.downloadQueue = downloadQueue;
this.parseQueue = parseQueue;
this.processNodeQueue = processNodeQueue;
this.stats = {
inCacheSinceLoad: 0,
inCache: 0,
parsing: 0,
downloading: 0,
failed: 0,
inFrustum: 0,
used: 0,
active: 0,
visible: 0
};
this.frameCount = 0;
this._dispatchNeedsUpdateEvent = throttle(() => {
this.dispatchEvent({ type: "needs-update" });
});
this.errorTarget = 16;
this._errorThreshold = Infinity;
this.displayActiveTiles = false;
this.maxDepth = Infinity;
}
// Plugins
registerPlugin(plugin) {
if (plugin[PLUGIN_REGISTERED] === true) {
throw new Error("TilesRendererBase: A plugin can only be registered to a single tile set");
}
const plugins = this.plugins;
const priority = plugin.priority || 0;
let insertionPoint = plugins.length;
for (let i = 0; i < plugins.length; i++) {
const otherPriority = plugins[i].priority || 0;
if (otherPriority > priority) {
insertionPoint = i;
break;
}
}
plugins.splice(insertionPoint, 0, plugin);
plugin[PLUGIN_REGISTERED] = true;
if (plugin.init) {
plugin.init(this);
}
}
unregisterPlugin(plugin) {
const plugins = this.plugins;
if (typeof plugin === "string") {
plugin = this.getPluginByName(name);
}
if (plugins.includes(plugin)) {
const index = plugins.indexOf(plugin);
plugins.splice(index, 1);
if (plugin.dispose) {
plugin.dispose();
}
return true;
}
return false;
}
getPluginByName(name2) {
return this.plugins.find((p) => p.name === name2) || null;
}
traverse(beforecb, aftercb, ensureFullyProcessed = true) {
if (!this.root) return;
traverseSet(this.root, (tile, ...args) => {
if (ensureFullyProcessed) {
this.ensureChildrenArePreprocessed(tile, true);
}
return beforecb ? beforecb(tile, ...args) : false;
}, aftercb);
}
queueTileForDownload(tile) {
if (tile.__loadingState !== UNLOADED) {
return;
}
this.queuedTiles.push(tile);
}
markTileUsed(tile) {
this.usedSet.add(tile);
this.lruCache.markUsed(tile);
}
// Public API
update() {
const { lruCache, usedSet, stats, root, downloadQueue, parseQueue, processNodeQueue } = this;
if (this.rootLoadingState === UNLOADED) {
this.rootLoadingState = LOADING;
this.invokeOnePlugin((plugin) => plugin.loadRootTileSet && plugin.loadRootTileSet()).then((root2) => {
this.rootLoadingState = LOADED;
this.rootTileSet = root2;
this.dispatchEvent({ type: "needs-update" });
this.dispatchEvent({ type: "load-content" });
this.dispatchEvent({
type: "load-tile-set",
tileSet: root2
});
}).catch((error) => {
this.rootLoadingState = FAILED;
console.error(error);
this.rootTileSet = null;
this.dispatchEvent({
type: "load-error",
tile: null,
error
});
});
}
if (!root) {
return;
}
stats.inFrustum = 0;
stats.used = 0;
stats.active = 0;
stats.visible = 0;
this.frameCount++;
usedSet.forEach((tile) => lruCache.markUnused(tile));
usedSet.clear();
markUsedTiles(root, this);
markUsedSetLeaves(root, this);
markVisibleTiles(root, this);
toggleTiles(root, this);
const queuedTiles = this.queuedTiles;
queuedTiles.sort(lruCache.unloadPriorityCallback);
for (let i = 0, l = queuedTiles.length; i < l && !lruCache.isFull(); i++) {
this.requestTileContents(queuedTiles[i]);
}
queuedTiles.length = 0;
lruCache.scheduleUnload();
const runningTasks = downloadQueue.running || parseQueue.running || processNodeQueue.running;
if (runningTasks === false && this.isLoading === true) {
this.cachedSinceLoadComplete.clear();
stats.inCacheSinceLoad = 0;
this.dispatchEvent({ type: "tiles-load-end" });
this.isLoading = false;
}
}
resetFailedTiles() {
if (this.rootLoadingState === FAILED) {
this.rootLoadingState = UNLOADED;
}
const stats = this.stats;
if (stats.failed === 0) {
return;
}
this.traverse((tile) => {
if (tile.__loadingState === FAILED) {
tile.__loadingState = UNLOADED;
}
}, null, false);
stats.failed = 0;
}
dispose() {
this.plugins.forEach((plugin) => {
this.unregisterPlugin(plugin);
});
const lruCache = this.lruCache;
const toRemove = [];
this.traverse((t) => {
toRemove.push(t);
return false;
}, null, false);
for (let i = 0, l = toRemove.length; i < l; i++) {
lruCache.remove(toRemove[i]);
}
this.stats = {
parsing: 0,
downloading: 0,
failed: 0,
inFrustum: 0,
used: 0,
active: 0,
visible: 0
};
this.frameCount = 0;
}
// Overrideable
dispatchEvent(e) {
}
fetchData(url, options) {
return fetch(url, options);
}
parseTile(buffer, tile, extension) {
return null;
}
disposeTile(tile) {
if (tile.__visible) {
this.invokeOnePlugin((plugin) => plugin.setTileVisible && plugin.setTileVisible(tile, false));
tile.__visible = false;
}
if (tile.__active) {
this.invokeOnePlugin((plugin) => plugin.setTileActive && plugin.setTileActive(tile, false));
tile.__active = false;
}
}
preprocessNode(tile, tileSetDir, parentTile = null) {
var _a;
if (tile.content) {
if (!("uri" in tile.content) && "url" in tile.content) {
tile.content.uri = tile.content.url;
delete tile.content.url;
}
if (tile.content.boundingVolume && !("box" in tile.content.boundingVolume || "sphere" in tile.content.boundingVolume || "region" in tile.content.boundingVolume)) {
delete tile.content.boundingVolume;
}
}
tile.parent = parentTile;
tile.children = tile.children || [];
if ((_a = tile.content) == null ? void 0 : _a.uri) {
const extension = getUrlExtension(tile.content.uri);
tile.__hasContent = true;
tile.__hasUnrenderableContent = Boolean(extension && /json$/.test(extension));
tile.__hasRenderableContent = !tile.__hasUnrenderableContent;
} else {
tile.__hasContent = false;
tile.__hasUnrenderableContent = false;
tile.__hasRenderableContent = false;
}
tile.__childrenProcessed = 0;
if (parentTile) {
parentTile.__childrenProcessed++;
}
tile.__distanceFromCamera = Infinity;
tile.__error = Infinity;
tile.__inFrustum = false;
tile.__isLeaf = false;
tile.__usedLastFrame = false;
tile.__used = false;
tile.__wasSetVisible = false;
tile.__visible = false;
tile.__childrenWereVisible = false;
tile.__allChildrenLoaded = false;
tile.__wasSetActive = false;
tile.__active = false;
tile.__loadingState = UNLOADED;
if (parentTile === null) {
tile.__depth = 0;
tile.__depthFromRenderedParent = tile.__hasRenderableContent ? 1 : 0;
tile.refine = tile.refine || "REPLACE";
} else {
tile.__depth = parentTile.__depth + 1;
tile.__depthFromRenderedParent = parentTile.__depthFromRenderedParent + (tile.__hasRenderableContent ? 1 : 0);
tile.refine = tile.refine || parentTile.refine;
}
tile.__basePath = tileSetDir;
tile.__lastFrameVisited = -1;
this.invokeAllPlugins((plugin) => {
plugin !== this && plugin.preprocessNode && plugin.preprocessNode(tile, tileSetDir, parentTile);
});
}
setTileActive(tile, active) {
active ? this.activeTiles.add(tile) : this.activeTiles.delete(tile);
}
setTileVisible(tile, visible) {
visible ? this.visibleTiles.add(tile) : this.visibleTiles.delete(tile);
}
calculateTileViewError(tile, target) {
}
ensureChildrenArePreprocessed(tile, immediate = false) {
const children = tile.children;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if ("__depth" in child) {
break;
} else if (immediate) {
this.processNodeQueue.remove(child);
this.preprocessNode(child, tile.__basePath, tile);
} else {
if (!this.processNodeQueue.has(child)) {
this.processNodeQueue.add(child, (child2) => {
this.preprocessNode(child2, tile.__basePath, tile);
this._dispatchNeedsUpdateEvent();
});
}
}
}
}
// Private Functions
preprocessTileSet(json, url, parent = null) {
const version = json.asset.version;
const [major, minor] = version.split(".").map((v) => parseInt(v));
console.assert(
major <= 1,
"TilesRenderer: asset.version is expected to be a 1.x or a compatible version."
);
if (major === 1 && minor > 0) {
console.warn("TilesRenderer: tiles versions at 1.1 or higher have limited support. Some new extensions and features may not be supported.");
}
let basePath = url.replace(/\/[^/]*$/, "");
basePath = new URL(basePath, window.location.href).toString();
this.preprocessNode(json.root, basePath, parent);
}
loadRootTileSet() {
let processedUrl = this.rootURL;
this.invokeAllPlugins((plugin) => processedUrl = plugin.preprocessURL ? plugin.preprocessURL(processedUrl, null) : processedUrl);
const pr = this.invokeOnePlugin((plugin) => plugin.fetchData && plugin.fetchData(processedUrl, this.fetchOptions)).then((res) => {
if (res.ok) {
return res.json();
} else {
throw new Error(`TilesRenderer: Failed to load tileset "${processedUrl}" with status ${res.status} : ${res.statusText}`);
}
}).then((root) => {
this.preprocessTileSet(root, processedUrl);
return root;
});
return pr;
}
requestTileContents(tile) {
if (tile.__loadingState !== UNLOADED) {
return;
}
let isExternalTileSet = false;
let uri = new URL(tile.content.uri, tile.__basePath + "/").toString();
this.invokeAllPlugins((plugin) => uri = plugin.preprocessURL ? plugin.preprocessURL(uri, tile) : uri);
const stats = this.stats;
const lruCache = this.lruCache;
const downloadQueue = this.downloadQueue;
const parseQueue = this.parseQueue;
const extension = getUrlExtension(uri);
const controller = new AbortController();
const signal = controller.signal;
const addedSuccessfully = lruCache.add(tile, (t) => {
controller.abort();
if (isExternalTileSet) {
t.children.length = 0;
t.__childrenProcessed = 0;
} else {
this.invokeAllPlugins((plugin) => {
plugin.disposeTile && plugin.disposeTile(t);
});
}
stats.inCache--;
if (this.cachedSinceLoadComplete.has(tile)) {
this.cachedSinceLoadComplete.delete(tile);
stats.inCacheSinceLoad--;
}
if (t.__loadingState === LOADING) {
stats.downloading--;
} else if (t.__loadingState === PARSING) {
stats.parsing--;
}
t.__loadingState = UNLOADED;
parseQueue.remove(t);
downloadQueue.remove(t);
});
if (!addedSuccessfully) {
return;
}
if (!this.isLoading) {
this.isLoading = true;
this.dispatchEvent({ type: "tiles-load-start" });
}
this.cachedSinceLoadComplete.add(tile);
stats.inCacheSinceLoad++;
stats.inCache++;
stats.downloading++;
tile.__loadingState = LOADING;
return downloadQueue.add(tile, (downloadTile) => {
if (signal.aborted) {
return Promise.resolve();
}
return this.invokeOnePlugin((plugin) => plugin.fetchData && plugin.fetchData(uri, { ...this.fetchOptions, signal }));
}).then((res) => {
if (signal.aborted) {
return;
}
if (res.ok) {
return extension === "json" ? res.json() : res.arrayBuffer();
} else {
throw new Error(`Failed to load model with error code ${res.status}`);
}
}).then((content) => {
if (signal.aborted) {
return;
}
stats.downloading--;
stats.parsing++;
tile.__loadingState = PARSING;
return parseQueue.add(tile, (parseTile) => {
if (signal.aborted) {
return Promise.resolve();
}
if (extension === "json" && content.root) {
this.preprocessTileSet(content, uri, tile);
tile.children.push(content.root);
isExternalTileSet = true;
return Promise.resolve();
} else {
return this.invokeOnePlugin((plugin) => plugin.parseTile && plugin.parseTile(content, parseTile, extension, uri, signal));
}
});
}).then(() => {
if (signal.aborted) {
return;
}
stats.parsing--;
tile.__loadingState = LOADED;
lruCache.setLoaded(tile, true);
if (lruCache.getMemoryUsage(tile) === null) {
if (lruCache.isFull() && lruCache.computeMemoryUsageCallback(tile) > 0) {
lruCache.remove(tile);
} else {
lruCache.updateMemoryUsage(tile);
}
}
this.dispatchEvent({ type: "needs-update" });
this.dispatchEvent({ type: "load-content" });
if (tile.cached.scene) {
this.dispatchEvent({
type: "load-model",
scene: tile.cached.scene,
tile
});
}
}).catch((error) => {
if (signal.aborted) {
return;
}
if (error.name !== "AbortError") {
parseQueue.remove(tile);
downloadQueue.remove(tile);
if (tile.__loadingState === PARSING) {
stats.parsing--;
} else if (tile.__loadingState === LOADING) {
stats.downloading--;
}
stats.failed++;
console.error(`TilesRenderer : Failed to load tile at url "${tile.content.uri}".`);
console.error(error);
tile.__loadingState = FAILED;
lruCache.setLoaded(tile, true);
this.dispatchEvent({
type: "load-error",
tile,
error,
uri
});
} else {
lruCache.remove(tile);
}
});
}
getAttributions(target = []) {
this.invokeAllPlugins((plugin) => plugin !== this && plugin.getAttributions && plugin.getAttributions(target));
return target;
}
invokeOnePlugin(func) {
const plugins = [...this.plugins, this];
for (let i = 0; i < plugins.length; i++) {
const result = func(plugins[i]);
if (result) {
return result;
}
}
return null;
}
invokeAllPlugins(func) {
const plugins = [...this.plugins, this];
const pending = [];
for (let i = 0; i < plugins.length; i++) {
const result = func(plugins[i]);
if (result) {
pending.push(result);
}
}
return pending.length === 0 ? null : Promise.all(pending);
}
}
const utf8decoder = new TextDecoder();
function arrayToString(array) {
return utf8decoder.decode(array);
}
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;
}
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);
}
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);
}
}
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 = {}) {
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);
}
}
}
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;
}
}
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 allproperties 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()) {
if (key !== "extensions") {
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);
}
}
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 = this.workingPathForURL(url);
}
return this.parse(buffer);
});
}
resolveExternalURL(url) {
if (/^[^\\/]/.test(url) && !/^http/.test(url)) {
return this.workingPath + "/" + url;
} else {
return url;
}
}
workingPathForURL(url) {
const splits = url.split(/[\\/]/g);
splits.pop();
const workingPath = splits.join("/");
return workingPath + "/";
}
parse(buffer) {
throw new Error("LoaderBase: Parse not implemented.");
}
}
function readMagicBytes(bufferOrDataView) {
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;
}
class B3DMLoaderBase extends LoaderBase {
parse(buffer) {
const dataView = new DataView(buffer);
const magic = readMagicBytes(dataView);
console.assert(magic === "b3dm");
const version = dataView.getUint32(4, t