spoint 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/LoadingManager.js +3 -26
- package/client/ModelCache.js +76 -0
- package/client/app.js +7 -2
- package/package.json +1 -1
- package/src/sdk/StaticHandler.js +10 -1
package/client/LoadingManager.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fetchCached } from './ModelCache.js'
|
|
2
|
+
|
|
1
3
|
export class LoadingManager extends EventTarget {
|
|
2
4
|
static STAGES = {
|
|
3
5
|
CONNECTING: { name: 'CONNECTING', label: 'Connecting...', range: [0, 10] },
|
|
@@ -76,32 +78,7 @@ export class LoadingManager extends EventTarget {
|
|
|
76
78
|
|
|
77
79
|
async fetchWithProgress(url) {
|
|
78
80
|
try {
|
|
79
|
-
|
|
80
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
81
|
-
const contentLength = parseInt(response.headers.get('content-length') || '0', 10)
|
|
82
|
-
const isGzip = (response.headers.get('content-encoding') || '').includes('gzip')
|
|
83
|
-
const useTotal = contentLength > 0 && !isGzip
|
|
84
|
-
const reader = response.body.getReader()
|
|
85
|
-
const chunks = []
|
|
86
|
-
let receivedLength = 0
|
|
87
|
-
|
|
88
|
-
while (true) {
|
|
89
|
-
const { done, value } = await reader.read()
|
|
90
|
-
if (done) break
|
|
91
|
-
chunks.push(value)
|
|
92
|
-
receivedLength += value.length
|
|
93
|
-
if (useTotal) {
|
|
94
|
-
this.updateProgress(receivedLength, contentLength)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const result = new Uint8Array(receivedLength)
|
|
99
|
-
let position = 0
|
|
100
|
-
for (const chunk of chunks) {
|
|
101
|
-
result.set(chunk, position)
|
|
102
|
-
position += chunk.length
|
|
103
|
-
}
|
|
104
|
-
return result
|
|
81
|
+
return await fetchCached(url, (received, total) => this.updateProgress(received, total))
|
|
105
82
|
} catch (error) {
|
|
106
83
|
console.error('[loading] fetch failed:', url, error)
|
|
107
84
|
throw error
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const DB_NAME = 'spawnpoint-model-cache'
|
|
2
|
+
const DB_VERSION = 1
|
|
3
|
+
const STORE = 'models'
|
|
4
|
+
|
|
5
|
+
let _db = null
|
|
6
|
+
|
|
7
|
+
async function openDB() {
|
|
8
|
+
if (_db) return _db
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION)
|
|
11
|
+
req.onupgradeneeded = e => e.target.result.createObjectStore(STORE)
|
|
12
|
+
req.onsuccess = e => { _db = e.target.result; resolve(_db) }
|
|
13
|
+
req.onerror = () => reject(req.error)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function dbGet(key) {
|
|
18
|
+
const db = await openDB()
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const tx = db.transaction(STORE, 'readonly')
|
|
21
|
+
const req = tx.objectStore(STORE).get(key)
|
|
22
|
+
req.onsuccess = () => resolve(req.result)
|
|
23
|
+
req.onerror = () => reject(req.error)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function dbPut(key, value) {
|
|
28
|
+
const db = await openDB()
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const tx = db.transaction(STORE, 'readwrite')
|
|
31
|
+
const req = tx.objectStore(STORE).put(value, key)
|
|
32
|
+
req.onsuccess = () => resolve()
|
|
33
|
+
req.onerror = () => reject(req.error)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function fetchCached(url, onProgress) {
|
|
38
|
+
let cached = null
|
|
39
|
+
try { cached = await dbGet(url) } catch {}
|
|
40
|
+
|
|
41
|
+
if (cached?.etag) {
|
|
42
|
+
const head = await fetch(url, { method: 'HEAD' }).catch(() => null)
|
|
43
|
+
const serverEtag = head?.headers?.get('etag')
|
|
44
|
+
if (serverEtag && serverEtag === cached.etag) {
|
|
45
|
+
return new Uint8Array(cached.buffer)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const response = await fetch(url)
|
|
50
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
51
|
+
const etag = response.headers.get('etag') || ''
|
|
52
|
+
const contentLength = parseInt(response.headers.get('content-length') || '0', 10)
|
|
53
|
+
const isGzip = (response.headers.get('content-encoding') || '').includes('gzip')
|
|
54
|
+
const useTotal = contentLength > 0 && !isGzip
|
|
55
|
+
const reader = response.body.getReader()
|
|
56
|
+
const chunks = []
|
|
57
|
+
let received = 0
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
const { done, value } = await reader.read()
|
|
61
|
+
if (done) break
|
|
62
|
+
chunks.push(value)
|
|
63
|
+
received += value.length
|
|
64
|
+
if (useTotal && onProgress) onProgress(received, contentLength)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = new Uint8Array(received)
|
|
68
|
+
let pos = 0
|
|
69
|
+
for (const chunk of chunks) { result.set(chunk, pos); pos += chunk.length }
|
|
70
|
+
|
|
71
|
+
if (etag) {
|
|
72
|
+
try { await dbPut(url, { etag, buffer: result.buffer }) } catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
}
|
package/client/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { VRButton } from 'three/addons/webxr/VRButton.js'
|
|
|
15
15
|
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'
|
|
16
16
|
import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js'
|
|
17
17
|
import { LoadingManager } from './LoadingManager.js'
|
|
18
|
+
import { fetchCached } from './ModelCache.js'
|
|
18
19
|
import { createLoadingScreen } from './createLoadingScreen.js'
|
|
19
20
|
import { MobileControls, detectDevice } from './MobileControls.js'
|
|
20
21
|
import { ARControls, createARButton } from './ARControls.js'
|
|
@@ -956,7 +957,7 @@ function loadEntityModel(entityId, entityState) {
|
|
|
956
957
|
}
|
|
957
958
|
loadingMgr.setStage('RESOURCES')
|
|
958
959
|
const url = entityState.model.startsWith('./') ? '/' + entityState.model.slice(2) : entityState.model
|
|
959
|
-
|
|
960
|
+
const onGltfLoad = (gltf) => {
|
|
960
961
|
const model = gltf.scene
|
|
961
962
|
const mp = entityState.position; model.position.set(mp[0], mp[1], mp[2])
|
|
962
963
|
const mr = entityState.rotation; if (mr) model.quaternion.set(mr[0], mr[1], mr[2], mr[3])
|
|
@@ -979,7 +980,11 @@ function loadEntityModel(entityId, entityState) {
|
|
|
979
980
|
if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
|
|
980
981
|
if (firstSnapshotEntityPending.has(entityId)) { firstSnapshotEntityPending.delete(entityId); if (firstSnapshotEntityPending.size === 0) checkAllLoaded() }
|
|
981
982
|
if (loadingScreenHidden) renderer.compileAsync(scene, camera).catch(() => renderer.compile(scene, camera))
|
|
982
|
-
}
|
|
983
|
+
}
|
|
984
|
+
const onGltfErr = (err) => { console.error('[gltf]', url, err); pendingLoads.delete(entityId); if (firstSnapshotEntityPending.has(entityId)) { firstSnapshotEntityPending.delete(entityId); if (firstSnapshotEntityPending.size === 0) checkAllLoaded() } }
|
|
985
|
+
fetchCached(url).then(buf => {
|
|
986
|
+
gltfLoader.parse(buf.buffer.slice(0), '', onGltfLoad, onGltfErr)
|
|
987
|
+
}).catch(onGltfErr)
|
|
983
988
|
}
|
|
984
989
|
|
|
985
990
|
function renderAppUI(state) {
|
package/package.json
CHANGED
package/src/sdk/StaticHandler.js
CHANGED
|
@@ -45,9 +45,18 @@ export function createStaticHandler(dirs) {
|
|
|
45
45
|
} else if (ext === '.glb' || ext === '.vrm' || ext === '.gltf') {
|
|
46
46
|
headers['Cache-Control'] = 'public, max-age=86400, immutable'
|
|
47
47
|
}
|
|
48
|
-
const { content, gzipped } = getCached(fp, ext)
|
|
48
|
+
const { content, gzipped, mtime } = getCached(fp, ext)
|
|
49
49
|
if (gzipped) headers['Content-Encoding'] = 'gzip'
|
|
50
50
|
headers['Content-Length'] = content.length
|
|
51
|
+
if (ext === '.glb' || ext === '.vrm' || ext === '.gltf') {
|
|
52
|
+
headers['ETag'] = `"${mtime.toString(16)}"`
|
|
53
|
+
const ifNoneMatch = req.headers['if-none-match']
|
|
54
|
+
if (ifNoneMatch === headers['ETag']) {
|
|
55
|
+
res.writeHead(304, { 'ETag': headers['ETag'], 'Cache-Control': headers['Cache-Control'] })
|
|
56
|
+
res.end()
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
}
|
|
51
60
|
res.writeHead(200, headers)
|
|
52
61
|
res.end(content)
|
|
53
62
|
return
|