rayzee 9.0.0 → 9.1.0
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/README.md +139 -8
- package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
- package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
- package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
- package/dist/rayzee.es.js +10344 -5334
- package/dist/rayzee.umd.js +265 -265
- package/package.json +1 -1
- package/src/AssetConfig.js +4 -0
- package/src/EngineDefaults.js +38 -56
- package/src/EngineEvents.js +3 -0
- package/src/EngineIssues.js +6 -0
- package/src/Headless.js +3 -1
- package/src/Passes/AIUpscaler.js +2 -1
- package/src/Passes/OIDNDenoiser.js +3 -6
- package/src/PathTracerApp.js +290 -19
- package/src/Processor/ArchiveCache.js +120 -0
- package/src/Processor/ArchiveReader.js +179 -32
- package/src/Processor/AssetLoader.js +514 -76
- package/src/Processor/AtmosphereModel.js +465 -0
- package/src/Processor/BVHBuilder.js +4 -1
- package/src/Processor/BVHLeafFold.js +100 -0
- package/src/Processor/BVHRefitter.js +98 -214
- package/src/Processor/ChunkedRecords.js +189 -4
- package/src/Processor/EmissiveTriangleBuilder.js +8 -0
- package/src/Processor/GeometryExtractor.js +238 -40
- package/src/Processor/InstanceTable.js +55 -37
- package/src/Processor/PBRT/PBRTCurves.js +104 -27
- package/src/Processor/PBRT/PBRTParser.js +132 -22
- package/src/Processor/PBRT/PBRTSceneBuilder.js +192 -49
- package/src/Processor/PBRT/buffers.js +27 -0
- package/src/Processor/PBRT/index.js +55 -15
- package/src/Processor/PackedRayBuffer.js +10 -13
- package/src/Processor/ParallelBVHBuilder.js +36 -15
- package/src/Processor/PhysicalSky.js +308 -0
- package/src/Processor/QueueManager.js +1 -0
- package/src/Processor/SceneProcessor.js +1132 -181
- package/src/Processor/SplitBLAS.js +249 -0
- package/src/Processor/SunPosition.js +41 -0
- package/src/Processor/TextureCreator.js +21 -0
- package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
- package/src/Processor/Workers/BVHWorker.js +17 -13
- package/src/Processor/ZipReader.js +207 -0
- package/src/RenderSettings.js +38 -0
- package/src/SceneState/SceneState.js +367 -0
- package/src/SceneState/portable.js +52 -0
- package/src/Stages/NormalDepth.js +7 -6
- package/src/Stages/PathTracer.js +108 -4
- package/src/Stages/PathTracerStage.js +251 -104
- package/src/Storage/BLASCache.js +190 -0
- package/src/Storage/CDFCache.js +77 -0
- package/src/Storage/DownloadCache.js +266 -0
- package/src/Storage/GeometrySpill.js +217 -0
- package/src/Storage/SceneGraphCodec.js +503 -0
- package/src/Storage/SpillStore.js +112 -0
- package/src/Storage/StorageManager.js +934 -0
- package/src/Storage/StorageOps.js +137 -0
- package/src/Storage/StorageWorker.js +37 -0
- package/src/Storage/events.js +29 -0
- package/src/Storage/identity.js +77 -0
- package/src/Storage/index.js +9 -0
- package/src/Storage/inlineTransport.js +24 -0
- package/src/Storage/locks.js +99 -0
- package/src/Storage/openStorage.js +87 -0
- package/src/Storage/sceneCachePolicy.js +12 -0
- package/src/Storage/shared.js +9 -0
- package/src/Storage/transport.js +62 -0
- package/src/TSL/Atmosphere.js +684 -0
- package/src/TSL/BVHTraversal.js +288 -239
- package/src/TSL/Common.js +11 -2
- package/src/TSL/Displacement.js +6 -6
- package/src/TSL/EmissiveSampling.js +5 -5
- package/src/TSL/EnvironmentCDF.js +176 -0
- package/src/TSL/GenerateKernel.js +3 -3
- package/src/TSL/HitFacet.js +4 -4
- package/src/TSL/LightsDirect.js +8 -8
- package/src/TSL/LightsSampling.js +55 -37
- package/src/TSL/Random.js +1 -0
- package/src/TSL/ShadeKernel.js +75 -19
- package/src/TSL/Sun.js +40 -0
- package/src/TSL/TextureSampling.js +6 -7
- package/src/index.js +18 -0
- package/src/managers/CameraManager.js +169 -1
- package/src/managers/EnvironmentManager.js +263 -33
- package/src/managers/GoboManager.js +19 -8
- package/src/managers/IESManager.js +3 -3
- package/src/managers/LightManager.js +98 -5
- package/src/managers/MaterialDataManager.js +22 -2
- package/src/managers/TransformManager.js +26 -0
- package/src/managers/UniformManager.js +5 -3
- package/src/managers/VideoRenderManager.js +3 -1
- package/src/managers/timeline/CameraTrack.js +22 -0
- package/src/managers/timeline/TimelineManager.js +14 -0
- package/src/version.js +3 -0
- package/src/Processor/ProceduralSky.js +0 -291
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stored per-mesh BVHs (BLASes), so reopening a scene skips building them. Keyed by the scene's
|
|
3
|
+
* source and the builder settings; every template's triangles are checksummed on both sides, so a
|
|
4
|
+
* stored BLAS is only ever used for exactly the triangles it was built over.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ENGINE_AREAS } from './StorageManager.js';
|
|
8
|
+
|
|
9
|
+
export const BLAS_CACHE_FORMAT = 2;
|
|
10
|
+
|
|
11
|
+
const PIECE_RECORDS = 1 << 20;
|
|
12
|
+
const YIELD_EVERY = 1 << 21;
|
|
13
|
+
|
|
14
|
+
const pause = () => new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
15
|
+
|
|
16
|
+
// A BLAS depends on vertex positions alone: lanes 0-2, 4-6 and 8-10 of a triangle record.
|
|
17
|
+
const POSITION_LANES = [ 0, 1, 2, 4, 5, 6, 8, 9, 10 ];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sum of per-record hashes over records [start, start + count): the same whatever order the
|
|
21
|
+
* records are in, so it compares triangles before and after a BLAS build reorders them.
|
|
22
|
+
* @param {number[]} [lanes] - which lanes to hash; the triangle positions by default
|
|
23
|
+
*/
|
|
24
|
+
export function rangeChecksum( records, start, count, lanes = POSITION_LANES ) {
|
|
25
|
+
|
|
26
|
+
const stride = records.lanesPerRecord;
|
|
27
|
+
const used = lanes.length;
|
|
28
|
+
let a = 0;
|
|
29
|
+
let b = 0;
|
|
30
|
+
let done = 0;
|
|
31
|
+
|
|
32
|
+
while ( done < count ) {
|
|
33
|
+
|
|
34
|
+
const rec = start + done;
|
|
35
|
+
const chunk = records.chunkFor( rec );
|
|
36
|
+
const n = Math.min( records.recordsPerChunk - ( rec % records.recordsPerChunk ), count - done );
|
|
37
|
+
let o = records.baseOf( rec );
|
|
38
|
+
|
|
39
|
+
for ( let i = 0; i < n; i ++, o += stride ) {
|
|
40
|
+
|
|
41
|
+
let h = 0x811c9dc5;
|
|
42
|
+
let g = 0x9e3779b9;
|
|
43
|
+
for ( let l = 0; l < used; l ++ ) {
|
|
44
|
+
|
|
45
|
+
const v = chunk[ o + lanes[ l ] ];
|
|
46
|
+
h = Math.imul( h ^ v, 0x01000193 );
|
|
47
|
+
g = Math.imul( ( g + v ) | 0, 0x85ebca6b ) ^ ( g >>> 13 );
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
a = ( a + h ) >>> 0;
|
|
52
|
+
b = ( b + g ) >>> 0;
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
done += n;
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return `${a.toString( 16 )}.${b.toString( 16 )}`;
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Checksums every owning template, yielding now and then so a big scene does not freeze the page. */
|
|
65
|
+
export async function templateChecksums( records, templates ) {
|
|
66
|
+
|
|
67
|
+
const out = [];
|
|
68
|
+
let since = 0;
|
|
69
|
+
for ( const { triOffset, triCount } of templates ) {
|
|
70
|
+
|
|
71
|
+
out.push( rangeChecksum( records, triOffset, triCount ) );
|
|
72
|
+
since += triCount;
|
|
73
|
+
if ( since >= YIELD_EVERY ) {
|
|
74
|
+
|
|
75
|
+
since = 0;
|
|
76
|
+
await pause();
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return out;
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {import('./StorageManager.js').StorageManager} storage
|
|
88
|
+
* @returns {Promise<?{index: Object, nodes: File, order: File, release: function(): void}>}
|
|
89
|
+
*/
|
|
90
|
+
export async function openBLASCache( storage, key ) {
|
|
91
|
+
|
|
92
|
+
const entry = await storage?.area( ENGINE_AREAS.SCENES ).open( key );
|
|
93
|
+
if ( ! entry ) return null;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
|
|
97
|
+
const index = await entry.json( 'index.json' );
|
|
98
|
+
const nodes = await entry.file( 'nodes.f32' );
|
|
99
|
+
const order = await entry.file( 'order.u32' );
|
|
100
|
+
if ( index?.v !== BLAS_CACHE_FORMAT || ! nodes || ! order ) {
|
|
101
|
+
|
|
102
|
+
entry.release();
|
|
103
|
+
return null;
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { index, nodes, order, release: () => entry.release() };
|
|
108
|
+
|
|
109
|
+
} catch {
|
|
110
|
+
|
|
111
|
+
entry.release();
|
|
112
|
+
return null;
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {{index: Object, bvh: import('../Processor/ChunkedRecords.js').ChunkedRecords,
|
|
120
|
+
* orders: Array<?Uint32Array>, isStale: function(): boolean, label?: string}} build
|
|
121
|
+
* @returns {Promise<boolean>}
|
|
122
|
+
*/
|
|
123
|
+
export async function saveBLASCache( storage, key, { index, bvh, orders, isStale, label = '' } ) {
|
|
124
|
+
|
|
125
|
+
const area = storage?.area( ENGINE_AREAS.SCENES );
|
|
126
|
+
if ( ! area ) return false;
|
|
127
|
+
|
|
128
|
+
const nodeRecords = index.totalNodes - index.tlasNodeCount;
|
|
129
|
+
const orderBytes = orders.reduce( ( n, o ) => n + ( o?.byteLength ?? 0 ), 0 );
|
|
130
|
+
const writer = await area.create( key, { label: `${label} (BVH)`, expectedBytes: nodeRecords * 64 + orderBytes } );
|
|
131
|
+
if ( ! writer ) return false;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
|
|
135
|
+
for ( let rec = index.tlasNodeCount; rec < index.totalNodes; ) {
|
|
136
|
+
|
|
137
|
+
if ( isStale() ) throw new Error( 'scene changed while it was being stored' );
|
|
138
|
+
const chunk = bvh.chunkFor( rec );
|
|
139
|
+
const room = bvh.recordsPerChunk - ( rec % bvh.recordsPerChunk );
|
|
140
|
+
const n = Math.min( room, index.totalNodes - rec, PIECE_RECORDS );
|
|
141
|
+
const base = bvh.baseOf( rec );
|
|
142
|
+
await writer.write( 'nodes.f32', chunk.subarray( base, base + n * 16 ) );
|
|
143
|
+
rec += n;
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for ( const order of orders ) {
|
|
148
|
+
|
|
149
|
+
if ( ! order ) continue;
|
|
150
|
+
for ( let at = 0; at < order.length; at += PIECE_RECORDS * 4 ) {
|
|
151
|
+
|
|
152
|
+
await writer.write( 'order.u32', order.subarray( at, Math.min( order.length, at + PIECE_RECORDS * 4 ) ) );
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if ( orderBytes === 0 ) await writer.write( 'order.u32', new Uint8Array( 0 ) );
|
|
159
|
+
if ( isStale() ) throw new Error( 'scene changed while it was being stored' );
|
|
160
|
+
await writer.writeJSON( 'index.json', index );
|
|
161
|
+
await writer.commit();
|
|
162
|
+
return true;
|
|
163
|
+
|
|
164
|
+
} catch {
|
|
165
|
+
|
|
166
|
+
await writer.abort();
|
|
167
|
+
return false;
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Reads records [from, to) of a stored node file into `bvh` at the same indices + `offset`. */
|
|
174
|
+
export async function readNodesInto( file, bvh, firstRecord, recordCount ) {
|
|
175
|
+
|
|
176
|
+
for ( let done = 0; done < recordCount; done += PIECE_RECORDS ) {
|
|
177
|
+
|
|
178
|
+
const n = Math.min( PIECE_RECORDS, recordCount - done );
|
|
179
|
+
const buffer = await file.slice( done * 64, ( done + n ) * 64 ).arrayBuffer();
|
|
180
|
+
bvh.setRecords( firstRecord + done, new Float32Array( buffer ) );
|
|
181
|
+
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function readOrder( file, offset, count ) {
|
|
187
|
+
|
|
188
|
+
return new Uint32Array( await file.slice( offset * 4, ( offset + count ) * 4 ).arrayBuffer() );
|
|
189
|
+
|
|
190
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ENGINE_AREAS } from './StorageManager.js';
|
|
2
|
+
import { sharedStorage } from './shared.js';
|
|
3
|
+
import { getAssetConfig } from '../AssetConfig.js';
|
|
4
|
+
|
|
5
|
+
const FORMAT = 1;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where an environment texture came from, recorded by the loader so its sampling tables can be
|
|
9
|
+
* found again. Pixels edited in place (skies, colour conversion) change the key or clear it.
|
|
10
|
+
*/
|
|
11
|
+
export function setEnvironmentSource( texture, source ) {
|
|
12
|
+
|
|
13
|
+
if ( texture && source ) texture.userData.__rayzeeSource = source;
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function keyFor( texture ) {
|
|
18
|
+
|
|
19
|
+
const source = texture?.userData?.__rayzeeSource;
|
|
20
|
+
if ( ! source ) return null;
|
|
21
|
+
const { width = 0, height = 0 } = texture.image ?? {};
|
|
22
|
+
return `cdf:${FORMAT}:${source}|${width}x${height}|${texture.userData.__rayzeeColorSpace ?? 'native'}`;
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const area = () => sharedStorage( getAssetConfig().cacheNamespace )?.area( ENGINE_AREAS.CDF ) ?? null;
|
|
27
|
+
|
|
28
|
+
/** @returns {Promise<?{marginalData: Float32Array, conditionalData: Float32Array, totalSum: number, compensationDelta: number, width: number, height: number}>} */
|
|
29
|
+
export async function loadCDF( texture ) {
|
|
30
|
+
|
|
31
|
+
const key = keyFor( texture );
|
|
32
|
+
const entry = key ? await area()?.open( key ) : null;
|
|
33
|
+
if ( ! entry ) return null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
|
|
37
|
+
const [ marginal, conditional ] = await Promise.all( [ entry.file( 'marginal.f32' ), entry.file( 'conditional.f32' ) ] );
|
|
38
|
+
if ( ! marginal || ! conditional ) return null;
|
|
39
|
+
const { totalSum, compensationDelta, width, height } = entry.extra;
|
|
40
|
+
return {
|
|
41
|
+
marginalData: new Float32Array( await marginal.arrayBuffer() ),
|
|
42
|
+
conditionalData: new Float32Array( await conditional.arrayBuffer() ),
|
|
43
|
+
totalSum, compensationDelta, width, height,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
} finally {
|
|
47
|
+
|
|
48
|
+
entry.release();
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function saveCDF( texture, { marginalData, conditionalData, totalSum, compensationDelta, width, height } ) {
|
|
55
|
+
|
|
56
|
+
const key = keyFor( texture );
|
|
57
|
+
const target = key ? area() : null;
|
|
58
|
+
if ( ! target || ! marginalData || ! conditionalData ) return false;
|
|
59
|
+
|
|
60
|
+
const writer = await target.create( key, { label: texture.name || 'environment', expectedBytes: marginalData.byteLength + conditionalData.byteLength } );
|
|
61
|
+
if ( ! writer ) return false;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
|
|
65
|
+
await writer.write( 'marginal.f32', marginalData );
|
|
66
|
+
await writer.write( 'conditional.f32', conditionalData );
|
|
67
|
+
await writer.commit( { totalSum, compensationDelta, width, height } );
|
|
68
|
+
return true;
|
|
69
|
+
|
|
70
|
+
} catch {
|
|
71
|
+
|
|
72
|
+
await writer.abort();
|
|
73
|
+
return false;
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { ENGINE_AREAS } from './StorageManager.js';
|
|
2
|
+
import { sharedStorage } from './shared.js';
|
|
3
|
+
import { getAssetConfig } from '../AssetConfig.js';
|
|
4
|
+
|
|
5
|
+
const DAY_MS = 86_400_000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `immutable`: a cached copy is always right (versioned paths). `revalidate`: use the cached copy
|
|
9
|
+
* at once, then check the server in the background at most once per `maxAgeMs`. `network`: skip
|
|
10
|
+
* the cache.
|
|
11
|
+
*/
|
|
12
|
+
export const DOWNLOAD_POLICY = Object.freeze( { IMMUTABLE: 'immutable', REVALIDATE: 'revalidate', NETWORK: 'network' } );
|
|
13
|
+
|
|
14
|
+
const progressEvent = ( loaded, total ) => ( { loaded, total, lengthComputable: total > 0 && loaded <= total } );
|
|
15
|
+
|
|
16
|
+
const header = ( response, name ) => response.headers?.get?.( name ) ?? null;
|
|
17
|
+
|
|
18
|
+
const abortError = ( signal ) => signal.reason ?? new DOMException( 'aborted', 'AbortError' );
|
|
19
|
+
|
|
20
|
+
export function nameFromUrl( url ) {
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
|
|
24
|
+
const path = new URL( url, globalThis.location?.href ?? 'http://localhost/' ).pathname;
|
|
25
|
+
return decodeURIComponent( path.split( '/' ).pop() ) || 'download';
|
|
26
|
+
|
|
27
|
+
} catch {
|
|
28
|
+
|
|
29
|
+
return 'download';
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readBody( response, { onProgress, signal } ) {
|
|
36
|
+
|
|
37
|
+
const total = Number( header( response, 'content-length' ) ) || 0;
|
|
38
|
+
const type = header( response, 'content-type' ) ?? '';
|
|
39
|
+
if ( ! response.body ) return new Blob( [ await response.arrayBuffer() ], { type } );
|
|
40
|
+
|
|
41
|
+
const reader = response.body.getReader();
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let loaded = 0;
|
|
44
|
+
for ( ;; ) {
|
|
45
|
+
|
|
46
|
+
if ( signal?.aborted ) {
|
|
47
|
+
|
|
48
|
+
reader.cancel().catch( () => {} );
|
|
49
|
+
throw abortError( signal );
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { done, value } = await reader.read();
|
|
54
|
+
if ( done ) break;
|
|
55
|
+
chunks.push( value );
|
|
56
|
+
loaded += value.length;
|
|
57
|
+
onProgress?.( progressEvent( loaded, total ) );
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Blob( chunks, { type } );
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const asFile = ( blob, name, type ) => new File( [ blob ], name, { type: type || blob.type, lastModified: blob.lastModified ?? Date.now() } );
|
|
66
|
+
|
|
67
|
+
const noop = () => {};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Downloads that survive reloads. Without storage (or when it is full) a download lands in
|
|
71
|
+
* memory exactly as before, so callers never branch on it.
|
|
72
|
+
*/
|
|
73
|
+
export class DownloadCache {
|
|
74
|
+
|
|
75
|
+
/** @param {?import('./StorageManager.js').StorageManager} storage */
|
|
76
|
+
constructor( storage ) {
|
|
77
|
+
|
|
78
|
+
this._storage = storage;
|
|
79
|
+
this._revalidating = new Set();
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get _area() {
|
|
84
|
+
|
|
85
|
+
return this._storage?.area( ENGINE_AREAS.DOWNLOADS ) ?? null;
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hold `release()` until done reading the File: it keeps another tab from replacing the entry.
|
|
91
|
+
* @returns {Promise<{file: File, release: function(): void, fromCache: boolean}>}
|
|
92
|
+
*/
|
|
93
|
+
async fetch( url, {
|
|
94
|
+
key = url,
|
|
95
|
+
name = nameFromUrl( url ),
|
|
96
|
+
policy = DOWNLOAD_POLICY.REVALIDATE,
|
|
97
|
+
maxAgeMs = DAY_MS,
|
|
98
|
+
onProgress = null,
|
|
99
|
+
signal = null,
|
|
100
|
+
fetchOptions = {},
|
|
101
|
+
} = {} ) {
|
|
102
|
+
|
|
103
|
+
const area = this._area;
|
|
104
|
+
|
|
105
|
+
if ( area && policy !== DOWNLOAD_POLICY.NETWORK ) {
|
|
106
|
+
|
|
107
|
+
const entry = await area.open( key );
|
|
108
|
+
if ( entry ) {
|
|
109
|
+
|
|
110
|
+
const blob = await entry.file( 'data' );
|
|
111
|
+
if ( blob ) {
|
|
112
|
+
|
|
113
|
+
if ( policy === DOWNLOAD_POLICY.REVALIDATE && Date.now() - ( entry.extra.checkedAt ?? 0 ) >= maxAgeMs ) {
|
|
114
|
+
|
|
115
|
+
this._revalidate( url, key, name, entry.extra, fetchOptions );
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onProgress?.( progressEvent( blob.size, blob.size ) );
|
|
120
|
+
return { file: asFile( blob, name, entry.extra.contentType ), release: () => entry.release(), fromCache: true };
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
entry.release();
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return this._download( url, key, name, { onProgress, signal, fetchOptions, cache: !! area } );
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Whether a copy is stored, without opening it for reading. */
|
|
135
|
+
async has( key ) {
|
|
136
|
+
|
|
137
|
+
return ( await this._area?.has( key ) ) === true;
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async _download( url, key, name, { onProgress, signal, fetchOptions, cache } ) {
|
|
142
|
+
|
|
143
|
+
if ( signal?.aborted ) throw abortError( signal );
|
|
144
|
+
const response = await fetch( url, { ...fetchOptions, signal } );
|
|
145
|
+
if ( ! response.ok ) throw new Error( `HTTP ${response.status} fetching ${url}` );
|
|
146
|
+
|
|
147
|
+
const total = Number( header( response, 'content-length' ) ) || 0;
|
|
148
|
+
const info = {
|
|
149
|
+
url,
|
|
150
|
+
contentType: header( response, 'content-type' ) ?? '',
|
|
151
|
+
lastModified: header( response, 'last-modified' ),
|
|
152
|
+
contentLength: total || null,
|
|
153
|
+
checkedAt: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const writer = cache && response.body ? await this._area.create( key, { label: name, expectedBytes: total } ) : null;
|
|
157
|
+
if ( ! writer ) {
|
|
158
|
+
|
|
159
|
+
return { file: asFile( await readBody( response, { onProgress, signal } ), name, info.contentType ), release: noop, fromCache: false };
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
|
|
165
|
+
await writer.writeStream( 'data', response.body, { signal, onProgress: ( loaded ) => onProgress?.( progressEvent( loaded, total ) ) } );
|
|
166
|
+
await writer.commit( info );
|
|
167
|
+
|
|
168
|
+
} catch ( error ) {
|
|
169
|
+
|
|
170
|
+
await writer.abort();
|
|
171
|
+
if ( error?.name !== 'QuotaExceededError' ) throw error;
|
|
172
|
+
return this._download( url, key, name, { onProgress, signal, fetchOptions, cache: false } );
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const entry = await this._area.open( key, { wait: true } );
|
|
177
|
+
const blob = entry && await entry.file( 'data' );
|
|
178
|
+
if ( ! blob ) {
|
|
179
|
+
|
|
180
|
+
entry?.release();
|
|
181
|
+
throw new Error( `storage: ${name} vanished right after it was saved` );
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { file: asFile( blob, name, info.contentType ), release: () => entry.release(), fromCache: false };
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async _revalidate( url, key, name, extra, fetchOptions ) {
|
|
190
|
+
|
|
191
|
+
if ( this._revalidating.has( key ) ) return;
|
|
192
|
+
this._revalidating.add( key );
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
|
|
196
|
+
const head = await fetch( url, { ...fetchOptions, method: 'HEAD' } );
|
|
197
|
+
if ( ! head.ok ) return;
|
|
198
|
+
|
|
199
|
+
const lastModified = header( head, 'last-modified' );
|
|
200
|
+
const length = Number( header( head, 'content-length' ) ) || null;
|
|
201
|
+
const changed = ( lastModified && extra.lastModified && lastModified !== extra.lastModified )
|
|
202
|
+
|| ( length && extra.contentLength && length !== extra.contentLength );
|
|
203
|
+
|
|
204
|
+
if ( changed ) {
|
|
205
|
+
|
|
206
|
+
const { release } = await this._download( url, key, name, { fetchOptions, cache: true } );
|
|
207
|
+
release();
|
|
208
|
+
|
|
209
|
+
} else {
|
|
210
|
+
|
|
211
|
+
await this._area.patchMeta( key, { extra: { ...extra, checkedAt: Date.now() } } );
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
} catch {
|
|
216
|
+
|
|
217
|
+
// Offline, or the server refuses HEAD: keep serving the stored copy.
|
|
218
|
+
|
|
219
|
+
} finally {
|
|
220
|
+
|
|
221
|
+
this._revalidating.delete( key );
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One-off download into memory or storage, for code that holds no DownloadCache. */
|
|
230
|
+
export function fetchFile( url, storage, options ) {
|
|
231
|
+
|
|
232
|
+
return new DownloadCache( storage ).fetch( url, options );
|
|
233
|
+
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Downloads through whatever storage this page's apps opened; plain fetches when none. */
|
|
237
|
+
export function sharedDownloads() {
|
|
238
|
+
|
|
239
|
+
return new DownloadCache( sharedStorage( getAssetConfig().cacheNamespace ) );
|
|
240
|
+
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* A URL a three.js loader can take in place of `url`: an object URL of the stored copy when there
|
|
245
|
+
* is storage, the network URL untouched otherwise (so the loader's own progress still works).
|
|
246
|
+
* Call `release()` once the loader has read it.
|
|
247
|
+
* @returns {Promise<{url: string, release: function(): void, cached: boolean}>}
|
|
248
|
+
*/
|
|
249
|
+
export async function cachedObjectURL( url, { downloads = sharedDownloads(), ...options } = {} ) {
|
|
250
|
+
|
|
251
|
+
if ( ! downloads._storage || ! /^https?:/i.test( url ) ) return { url, release: noop, cached: false };
|
|
252
|
+
|
|
253
|
+
const { file, release } = await downloads.fetch( url, options );
|
|
254
|
+
const objectUrl = URL.createObjectURL( file );
|
|
255
|
+
return {
|
|
256
|
+
url: objectUrl,
|
|
257
|
+
cached: true,
|
|
258
|
+
release: () => {
|
|
259
|
+
|
|
260
|
+
URL.revokeObjectURL( objectUrl );
|
|
261
|
+
release();
|
|
262
|
+
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
}
|