diff --git a/src/Viewer.js b/src/Viewer.js index 79a1f462..5da0c8b6 100644 --- a/src/Viewer.js +++ b/src/Viewer.js @@ -26,6 +26,8 @@ import { RenderMode } from './RenderMode.js'; import { LogLevel } from './LogLevel.js'; import { SceneRevealMode } from './SceneRevealMode.js'; import { SplatRenderMode } from './SplatRenderMode.js'; +import { SplatBuffer } from './loaders/SplatBuffer.js'; +import { openDB } from '../util/idb.js'; const THREE_CAMERA_FOV = 50; const MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT = .75; @@ -283,6 +285,9 @@ export class Viewer { this.disposing = false; this.disposed = false; this.disposePromise = null; + this.dbPromise = null; + // Whether to use indexedDB for caching splat buffers + this.cacheEnabled = options.cacheEnabled === true; if (!this.dropInMode) this.init(); } @@ -1009,9 +1014,11 @@ export class Viewer { for (let i = 0; i < sceneOptions.length; i++) { const options = sceneOptions[i]; const format = (options.format !== undefined && options.format !== null) ? options.format : sceneFormatFromPath(options.path); - const baseDownloadPromise = this.downloadSplatSceneToSplatBuffer(options.path, options.splatAlphaRemovalThreshold, - onLoadProgress.bind(this, i), false, undefined, - format, options.headers); + // add indexedDB when caching is enabled + const baseDownloadPromise = { + promise: this.downloadOrCacheSplatBuffer(options.path, options.splatAlphaRemovalThreshold, + onLoadProgress.bind(this, i), false, undefined, format, options.headers) + }; baseDownloadPromises.push(baseDownloadPromise); nativeDownloadPromises.push(baseDownloadPromise.promise); } @@ -2096,4 +2103,59 @@ export class Viewer { isMobile() { return navigator.userAgent.includes('Mobi'); } + + async getDb() { + if (!this.dbPromise) { + this.dbPromise = openDB('SplatBufferCacheDB', 1, { + upgrade(db) { + if (!db.objectStoreNames.contains('buffers')) { + db.createObjectStore('buffers'); + } + } + }); + } + return this.dbPromise; + } + + async getCachedSplatBuffer(key) { + if (!this.cacheEnabled) return null; + const db = await this.getDb(); + const bufferData = await db.get('buffers', key); + if (!bufferData) return null; + return new SplatBuffer(bufferData); + } + + async setCachedSplatBuffer(key, buffer) { + if (!this.cacheEnabled) return; + const db = await this.getDb(); + await db.put('buffers', buffer.bufferData, key); + } + + async downloadOrCacheSplatBuffer( + path, + splatAlphaRemovalThreshold = 1, + onProgress = undefined, + progressiveBuild = false, + onSectionBuilt = undefined, + format, + headers + ) { + const key = `${path}_${format}_${splatAlphaRemovalThreshold}`; + let cached = null; + if (this.cacheEnabled) { + cached = await this.getCachedSplatBuffer(key); + if (cached) return cached; + } + const buffer = await this.downloadSplatSceneToSplatBuffer( + path, + splatAlphaRemovalThreshold, + onProgress, + progressiveBuild, + onSectionBuilt, + format, + headers + ); + if (this.cacheEnabled) await this.setCachedSplatBuffer(key, buffer); + return buffer; + } } diff --git a/util/idb.js b/util/idb.js new file mode 100644 index 00000000..dc1114ad --- /dev/null +++ b/util/idb.js @@ -0,0 +1,8 @@ +/** + * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0. + * Original file: /npm/idb@7.1.1/build/index.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +let e,t;const n=new WeakMap,r=new WeakMap,o=new WeakMap,s=new WeakMap,a=new WeakMap;let i={get(e,t,n){if(e instanceof IDBTransaction){if("done"===t)return r.get(e);if("objectStoreNames"===t)return e.objectStoreNames||o.get(e);if("store"===t)return n.objectStoreNames[1]?void 0:n.objectStore(n.objectStoreNames[0])}return u(e[t])},set:(e,t,n)=>(e[t]=n,!0),has:(e,t)=>e instanceof IDBTransaction&&("done"===t||"store"===t)||t in e};function c(e){return e!==IDBDatabase.prototype.transaction||"objectStoreNames"in IDBTransaction.prototype?(t||(t=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(e)?function(...t){return e.apply(l(this),t),u(n.get(this))}:function(...t){return u(e.apply(l(this),t))}:function(t,...n){const r=e.call(l(this),t,...n);return o.set(r,t.sort?t.sort():[t]),u(r)}}function d(t){return"function"==typeof t?c(t):(t instanceof IDBTransaction&&function(e){if(r.has(e))return;const t=new Promise(((t,n)=>{const r=()=>{e.removeEventListener("complete",o),e.removeEventListener("error",s),e.removeEventListener("abort",s)},o=()=>{t(),r()},s=()=>{n(e.error||new DOMException("AbortError","AbortError")),r()};e.addEventListener("complete",o),e.addEventListener("error",s),e.addEventListener("abort",s)}));r.set(e,t)}(t),n=t,(e||(e=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])).some((e=>n instanceof e))?new Proxy(t,i):t);var n}function u(e){if(e instanceof IDBRequest)return function(e){const t=new Promise(((t,n)=>{const r=()=>{e.removeEventListener("success",o),e.removeEventListener("error",s)},o=()=>{t(u(e.result)),r()},s=()=>{n(e.error),r()};e.addEventListener("success",o),e.addEventListener("error",s)}));return t.then((t=>{t instanceof IDBCursor&&n.set(t,e)})).catch((()=>{})),a.set(t,e),t}(e);if(s.has(e))return s.get(e);const t=d(e);return t!==e&&(s.set(e,t),a.set(t,e)),t}const l=e=>a.get(e);function f(e,t,{blocked:n,upgrade:r,blocking:o,terminated:s}={}){const a=indexedDB.open(e,t),i=u(a);return r&&a.addEventListener("upgradeneeded",(e=>{r(u(a.result),e.oldVersion,e.newVersion,u(a.transaction),e)})),n&&a.addEventListener("blocked",(e=>n(e.oldVersion,e.newVersion,e))),i.then((e=>{s&&e.addEventListener("close",(()=>s())),o&&e.addEventListener("versionchange",(e=>o(e.oldVersion,e.newVersion,e)))})).catch((()=>{})),i}function p(e,{blocked:t}={}){const n=indexedDB.deleteDatabase(e);return t&&n.addEventListener("blocked",(e=>t(e.oldVersion,e))),u(n).then((()=>{}))}const D=["get","getKey","getAll","getAllKeys","count"],v=["put","add","delete","clear"],b=new Map;function I(e,t){if(!(e instanceof IDBDatabase)||t in e||"string"!=typeof t)return;if(b.get(t))return b.get(t);const n=t.replace(/FromIndex$/,""),r=t!==n,o=v.includes(n);if(!(n in(r?IDBIndex:IDBObjectStore).prototype)||!o&&!D.includes(n))return;const s=async function(e,...t){const s=this.transaction(e,o?"readwrite":"readonly");let a=s.store;return r&&(a=a.index(t.shift())),(await Promise.all([a[n](...t),o&&s.done]))[0]};return b.set(t,s),s}i=(e=>({...e,get:(t,n,r)=>I(t,n)||e.get(t,n,r),has:(t,n)=>!!I(t,n)||e.has(t,n)}))(i);export{p as deleteDB,f as openDB,l as unwrap,u as wrap};export default null; +//# sourceMappingURL=/sm/0ecc1ee212c4ceef77427983aa99400261b08fb9b998f490bf0d417faadfc3ab.map \ No newline at end of file