UNPKG

@threepipe/plugin-3d-tiles-renderer

Version:
1,447 lines (1,446 loc) 397 kB
(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