spoint 0.1.44 → 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 -24
- package/client/ModelCache.js +76 -0
- package/client/app.js +17 -3
- 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,30 +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 reader = response.body.getReader()
|
|
83
|
-
const chunks = []
|
|
84
|
-
let receivedLength = 0
|
|
85
|
-
|
|
86
|
-
while (true) {
|
|
87
|
-
const { done, value } = await reader.read()
|
|
88
|
-
if (done) break
|
|
89
|
-
chunks.push(value)
|
|
90
|
-
receivedLength += value.length
|
|
91
|
-
if (contentLength > 0) {
|
|
92
|
-
this.updateProgress(receivedLength, contentLength)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const result = new Uint8Array(receivedLength)
|
|
97
|
-
let position = 0
|
|
98
|
-
for (const chunk of chunks) {
|
|
99
|
-
result.set(chunk, position)
|
|
100
|
-
position += chunk.length
|
|
101
|
-
}
|
|
102
|
-
return result
|
|
81
|
+
return await fetchCached(url, (received, total) => this.updateProgress(received, total))
|
|
103
82
|
} catch (error) {
|
|
104
83
|
console.error('[loading] fetch failed:', url, error)
|
|
105
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])
|
|
@@ -977,8 +978,13 @@ function loadEntityModel(entityId, entityState) {
|
|
|
977
978
|
fitShadowFrustum()
|
|
978
979
|
pendingLoads.delete(entityId)
|
|
979
980
|
if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
|
|
981
|
+
if (firstSnapshotEntityPending.has(entityId)) { firstSnapshotEntityPending.delete(entityId); if (firstSnapshotEntityPending.size === 0) checkAllLoaded() }
|
|
980
982
|
if (loadingScreenHidden) renderer.compileAsync(scene, camera).catch(() => renderer.compile(scene, camera))
|
|
981
|
-
}
|
|
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)
|
|
982
988
|
}
|
|
983
989
|
|
|
984
990
|
function renderAppUI(state) {
|
|
@@ -1026,7 +1032,13 @@ const client = new PhysicsNetworkClient({
|
|
|
1026
1032
|
}
|
|
1027
1033
|
rebuildEntityHierarchy(smoothState.entities)
|
|
1028
1034
|
latestState = state
|
|
1029
|
-
if (!firstSnapshotReceived) {
|
|
1035
|
+
if (!firstSnapshotReceived) {
|
|
1036
|
+
firstSnapshotReceived = true
|
|
1037
|
+
for (const e of smoothState.entities) {
|
|
1038
|
+
if (e.model && !entityMeshes.has(e.id)) firstSnapshotEntityPending.add(e.id)
|
|
1039
|
+
}
|
|
1040
|
+
checkAllLoaded()
|
|
1041
|
+
}
|
|
1030
1042
|
},
|
|
1031
1043
|
onPlayerJoined: (id) => { if (!playerMeshes.has(id)) createPlayerVRM(id) },
|
|
1032
1044
|
onPlayerLeft: (id) => removePlayerMesh(id),
|
|
@@ -1091,6 +1103,7 @@ let inputLoopId = null
|
|
|
1091
1103
|
let loadingScreenHidden = false
|
|
1092
1104
|
let environmentLoaded = false
|
|
1093
1105
|
let firstSnapshotReceived = false
|
|
1106
|
+
const firstSnapshotEntityPending = new Set()
|
|
1094
1107
|
let lastShootState = false
|
|
1095
1108
|
let lastHealth = 100
|
|
1096
1109
|
|
|
@@ -1123,6 +1136,7 @@ function checkAllLoaded() {
|
|
|
1123
1136
|
if (!assetsLoaded) return
|
|
1124
1137
|
if (!environmentLoaded) return
|
|
1125
1138
|
if (!firstSnapshotReceived) return
|
|
1139
|
+
if (firstSnapshotEntityPending.size > 0) return
|
|
1126
1140
|
loadingMgr.setStage('INIT')
|
|
1127
1141
|
loadingMgr.complete()
|
|
1128
1142
|
loadingScreenHidden = true
|
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
|