rayzee 7.31.1 → 8.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 +164 -0
- package/dist/rayzee.es.js +6728 -4735
- package/dist/rayzee.umd.js +121 -73
- package/package.json +2 -2
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +5 -0
- package/src/PathTracerApp.js +151 -25
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +265 -23
- package/src/Processor/BVHBuilder.js +48 -32
- package/src/Processor/BVHRefitter.js +244 -99
- package/src/Processor/ChunkedRecords.js +302 -0
- package/src/Processor/EmissiveTriangleBuilder.js +48 -21
- package/src/Processor/GeometryExtractor.js +580 -93
- package/src/Processor/HostMemory.js +222 -0
- package/src/Processor/InstanceTable.js +548 -97
- package/src/Processor/PBRT/EqualAreaOctahedral.js +32 -8
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +19 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +257 -50
- package/src/Processor/PackedRayBuffer.js +29 -18
- package/src/Processor/ParallelBVHBuilder.js +9 -8
- package/src/Processor/QueueManager.js +5 -3
- package/src/Processor/SceneProcessor.js +1046 -365
- package/src/Processor/TLASBuilder.js +546 -201
- package/src/Processor/TextureCreator.js +11 -9
- package/src/Processor/VRAMTracker.js +12 -3
- package/src/Processor/Workers/BVHRefitWorker.js +16 -11
- package/src/Processor/Workers/BVHWorker.js +12 -12
- package/src/Processor/Workers/TLASWorker.js +37 -0
- package/src/Processor/utils.js +8 -20
- package/src/Stages/NormalDepth.js +6 -2
- package/src/Stages/PathTracer.js +5 -5
- package/src/Stages/PathTracerStage.js +163 -41
- package/src/TSL/BVHTraversal.js +208 -90
- package/src/TSL/Common.js +135 -2
- package/src/TSL/Displacement.js +21 -11
- package/src/TSL/EmissiveSampling.js +49 -25
- package/src/TSL/ExtendKernel.js +3 -2
- package/src/TSL/LightBVHSampling.js +7 -4
- package/src/TSL/LightsDirect.js +30 -29
- package/src/TSL/ShadeKernel.js +8 -6
- package/src/TSL/SortGlobalKernels.js +3 -4
- package/src/TSL/Struct.js +3 -0
- package/src/TSL/TextureSampling.js +24 -9
- package/src/TSL/patches.js +149 -84
- package/src/index.js +3 -0
- package/src/managers/AnimationManager.js +86 -76
- package/src/managers/MaterialDataManager.js +29 -23
- package/src/managers/TransformManager.js +9 -192
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming reader for .tar.gz / .tgz / .tar archives.
|
|
3
|
+
*
|
|
4
|
+
* ZIP is random-access and fflate's unzipSync is fine for it, but a gzipped tar is a
|
|
5
|
+
* single compressed stream: the only way to reach the last entry is to decompress
|
|
6
|
+
* everything before it. Holding that in memory is not an option — the pbrt-v4 Moana
|
|
7
|
+
* archive is 5.9 GB compressed and 29 GB unpacked — so entries are decoded one at a
|
|
8
|
+
* time and only the ones a filter asks for are kept.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Gunzip } from 'three/addons/libs/fflate.module.js';
|
|
12
|
+
|
|
13
|
+
const BLOCK = 512;
|
|
14
|
+
const SLICE_BYTES = 4 << 20;
|
|
15
|
+
const DEFAULT_BYTE_BUDGET = 1_500_000_000;
|
|
16
|
+
|
|
17
|
+
/** V8 caps a string at 2^29-24 chars, so a larger entry can never be decoded as text. */
|
|
18
|
+
export const MAX_TEXT_ENTRY_BYTES = 536_870_888;
|
|
19
|
+
|
|
20
|
+
const textDecoder = new TextDecoder();
|
|
21
|
+
|
|
22
|
+
export function isGzip( head ) {
|
|
23
|
+
|
|
24
|
+
return head.length >= 2 && head[ 0 ] === 0x1f && head[ 1 ] === 0x8b;
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isZip( head ) {
|
|
29
|
+
|
|
30
|
+
return head.length >= 4 && head[ 0 ] === 0x50 && head[ 1 ] === 0x4b
|
|
31
|
+
&& ( head[ 2 ] === 3 || head[ 2 ] === 5 || head[ 2 ] === 7 );
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isTar( head ) {
|
|
36
|
+
|
|
37
|
+
return head.length >= 263 && String.fromCharCode( ...head.subarray( 257, 262 ) ) === 'ustar';
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function detectArchiveKind( head ) {
|
|
42
|
+
|
|
43
|
+
if ( isGzip( head ) ) return 'gzip';
|
|
44
|
+
if ( isZip( head ) ) return 'zip';
|
|
45
|
+
if ( isTar( head ) ) return 'tar';
|
|
46
|
+
return null;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fieldString( b, off, len ) {
|
|
51
|
+
|
|
52
|
+
let end = off;
|
|
53
|
+
const max = off + len;
|
|
54
|
+
while ( end < max && b[ end ] !== 0 ) end ++;
|
|
55
|
+
return textDecoder.decode( b.subarray( off, end ) );
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fieldNumber( b, off, len ) {
|
|
60
|
+
|
|
61
|
+
// GNU writes sizes that do not fit in 11 octal digits as base-256 with the high bit set.
|
|
62
|
+
if ( b[ off ] & 0x80 ) {
|
|
63
|
+
|
|
64
|
+
let v = b[ off ] & 0x7f;
|
|
65
|
+
for ( let i = off + 1; i < off + len; i ++ ) v = v * 256 + b[ i ];
|
|
66
|
+
return v;
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const s = fieldString( b, off, len ).trim();
|
|
71
|
+
if ( ! s ) return 0;
|
|
72
|
+
const v = parseInt( s, 8 );
|
|
73
|
+
return Number.isFinite( v ) ? v : 0;
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeTarPath( p ) {
|
|
78
|
+
|
|
79
|
+
const out = [];
|
|
80
|
+
for ( const part of p.replace( /\\/g, '/' ).split( '/' ) ) {
|
|
81
|
+
|
|
82
|
+
if ( part === '' || part === '.' ) continue;
|
|
83
|
+
if ( part === '..' ) out.pop();
|
|
84
|
+
else out.push( part );
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return out.join( '/' );
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function paxPath( bytes ) {
|
|
93
|
+
|
|
94
|
+
const text = textDecoder.decode( bytes );
|
|
95
|
+
const m = text.match( /^\d+ path=(.*)$/m );
|
|
96
|
+
return m ? m[ 1 ] : null;
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Pull-free tar parser: chunks are pushed in as they decompress, and entry bodies are
|
|
102
|
+
* copied straight out of them so a skipped entry never allocates.
|
|
103
|
+
*/
|
|
104
|
+
class TarStream {
|
|
105
|
+
|
|
106
|
+
constructor( { filter = null, retain = null, byteBudget = DEFAULT_BYTE_BUDGET, onEntry = null } = {} ) {
|
|
107
|
+
|
|
108
|
+
this.filter = filter;
|
|
109
|
+
// Entries that pass `filter` but fail `retain` are indexed, not copied: the listing
|
|
110
|
+
// records where their bytes live so a caller with a seekable source can read them later.
|
|
111
|
+
// An uncompressed tar over a File is seekable, which is how a 7 GB archive loads without
|
|
112
|
+
// ever holding itself in memory.
|
|
113
|
+
this.retain = retain;
|
|
114
|
+
this.byteBudget = byteBudget;
|
|
115
|
+
this.onEntry = onEntry;
|
|
116
|
+
|
|
117
|
+
this.entries = Object.create( null );
|
|
118
|
+
this.listing = [];
|
|
119
|
+
this.retainedBytes = 0;
|
|
120
|
+
this.truncated = false;
|
|
121
|
+
this.finished = false;
|
|
122
|
+
|
|
123
|
+
this._header = new Uint8Array( BLOCK );
|
|
124
|
+
this._headerLen = 0;
|
|
125
|
+
this._state = 'header';
|
|
126
|
+
this._remaining = 0;
|
|
127
|
+
this._size = 0;
|
|
128
|
+
this._copied = 0;
|
|
129
|
+
this._dst = null;
|
|
130
|
+
this._special = null;
|
|
131
|
+
this._pendingName = null;
|
|
132
|
+
this._abs = 0; // absolute byte offset of the next byte `push` will consume
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
push( chunk ) {
|
|
137
|
+
|
|
138
|
+
const base = this._abs;
|
|
139
|
+
this._abs += chunk.length;
|
|
140
|
+
|
|
141
|
+
let i = 0;
|
|
142
|
+
while ( i < chunk.length && ! this.finished ) {
|
|
143
|
+
|
|
144
|
+
if ( this._state === 'header' ) {
|
|
145
|
+
|
|
146
|
+
const n = Math.min( BLOCK - this._headerLen, chunk.length - i );
|
|
147
|
+
this._header.set( chunk.subarray( i, i + n ), this._headerLen );
|
|
148
|
+
this._headerLen += n;
|
|
149
|
+
i += n;
|
|
150
|
+
if ( this._headerLen === BLOCK ) {
|
|
151
|
+
|
|
152
|
+
this._headerLen = 0;
|
|
153
|
+
this._readHeader();
|
|
154
|
+
this._bodyStart = base + i; // body begins right after the header block
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
} else {
|
|
159
|
+
|
|
160
|
+
const n = Math.min( this._remaining, chunk.length - i );
|
|
161
|
+
if ( this._dst && this._copied < this._size ) {
|
|
162
|
+
|
|
163
|
+
const take = Math.min( n, this._size - this._copied );
|
|
164
|
+
this._dst.set( chunk.subarray( i, i + take ), this._copied );
|
|
165
|
+
this._copied += take;
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._remaining -= n;
|
|
170
|
+
i += n;
|
|
171
|
+
if ( this._remaining === 0 ) this._endBody();
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
end() {
|
|
180
|
+
|
|
181
|
+
this.finished = true;
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
result() {
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
entries: this.entries,
|
|
189
|
+
listing: this.listing,
|
|
190
|
+
retainedBytes: this.retainedBytes,
|
|
191
|
+
truncated: this.truncated
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_readHeader() {
|
|
197
|
+
|
|
198
|
+
const h = this._header;
|
|
199
|
+
let zero = true;
|
|
200
|
+
for ( let i = 0; i < BLOCK; i ++ ) if ( h[ i ] !== 0 ) {
|
|
201
|
+
|
|
202
|
+
zero = false; break;
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if ( zero ) {
|
|
207
|
+
|
|
208
|
+
this.finished = true;
|
|
209
|
+
return;
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const type = String.fromCharCode( h[ 156 ] || 0x30 );
|
|
214
|
+
const size = fieldNumber( h, 124, 12 );
|
|
215
|
+
const prefix = fieldString( h, 345, 155 );
|
|
216
|
+
const raw = fieldString( h, 0, 100 );
|
|
217
|
+
const name = this._pendingName ?? ( prefix ? `${prefix}/${raw}` : raw );
|
|
218
|
+
this._pendingName = null;
|
|
219
|
+
|
|
220
|
+
this._size = size;
|
|
221
|
+
this._copied = 0;
|
|
222
|
+
this._remaining = size + ( ( BLOCK - ( size % BLOCK ) ) % BLOCK );
|
|
223
|
+
this._dst = null;
|
|
224
|
+
this._special = null;
|
|
225
|
+
this._state = this._remaining > 0 ? 'body' : 'header';
|
|
226
|
+
|
|
227
|
+
if ( type === 'L' || type === 'x' || type === 'X' ) {
|
|
228
|
+
|
|
229
|
+
this._special = type === 'L' ? 'longname' : 'pax';
|
|
230
|
+
this._dst = new Uint8Array( size );
|
|
231
|
+
return;
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if ( type === '5' || type === 'g' || type === 'K' || name.endsWith( '/' ) ) return;
|
|
236
|
+
if ( type !== '0' && type !== ' ' ) return;
|
|
237
|
+
|
|
238
|
+
const path = normalizeTarPath( name );
|
|
239
|
+
if ( ! path ) return;
|
|
240
|
+
|
|
241
|
+
this.listing.push( { path, size } );
|
|
242
|
+
|
|
243
|
+
const wanted = this.filter ? this.filter( path, size ) : true;
|
|
244
|
+
if ( ! wanted ) return;
|
|
245
|
+
|
|
246
|
+
this._indexed = true;
|
|
247
|
+
|
|
248
|
+
if ( this.retain && ! this.retain( path, size ) ) return;
|
|
249
|
+
|
|
250
|
+
if ( this.retainedBytes + size > this.byteBudget ) {
|
|
251
|
+
|
|
252
|
+
this.truncated = true;
|
|
253
|
+
return;
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this._dst = new Uint8Array( size );
|
|
258
|
+
this.retainedBytes += size;
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_endBody() {
|
|
263
|
+
|
|
264
|
+
this._state = 'header';
|
|
265
|
+
|
|
266
|
+
if ( this._special === 'longname' ) {
|
|
267
|
+
|
|
268
|
+
this._pendingName = fieldString( this._dst, 0, this._dst.length );
|
|
269
|
+
this._dst = null;
|
|
270
|
+
return;
|
|
271
|
+
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if ( this._special === 'pax' ) {
|
|
275
|
+
|
|
276
|
+
const p = paxPath( this._dst );
|
|
277
|
+
if ( p ) this._pendingName = p;
|
|
278
|
+
this._dst = null;
|
|
279
|
+
return;
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const entry = this.listing[ this.listing.length - 1 ];
|
|
284
|
+
|
|
285
|
+
if ( this._indexed && entry ) {
|
|
286
|
+
|
|
287
|
+
entry.offset = this._bodyStart;
|
|
288
|
+
this._indexed = false;
|
|
289
|
+
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if ( this._dst ) {
|
|
293
|
+
|
|
294
|
+
this.entries[ entry.path ] = this._dst;
|
|
295
|
+
this.onEntry?.( entry.path, this._dst );
|
|
296
|
+
this._dst = null;
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function streamGunzip( source, onChunk ) {
|
|
305
|
+
|
|
306
|
+
if ( typeof DecompressionStream !== 'undefined' && typeof source?.stream === 'function' ) {
|
|
307
|
+
|
|
308
|
+
const reader = source.stream().pipeThrough( new DecompressionStream( 'gzip' ) ).getReader();
|
|
309
|
+
for ( ;; ) {
|
|
310
|
+
|
|
311
|
+
const { done, value } = await reader.read();
|
|
312
|
+
if ( done ) break;
|
|
313
|
+
onChunk( value );
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return;
|
|
318
|
+
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let failure = null;
|
|
322
|
+
const gunzip = new Gunzip( ( chunk, final ) => {
|
|
323
|
+
|
|
324
|
+
if ( chunk instanceof Error ) failure = chunk;
|
|
325
|
+
else if ( chunk && chunk.length ) onChunk( chunk );
|
|
326
|
+
void final;
|
|
327
|
+
|
|
328
|
+
} );
|
|
329
|
+
|
|
330
|
+
await forEachSlice( source, ( slice, last ) => {
|
|
331
|
+
|
|
332
|
+
gunzip.push( slice, last );
|
|
333
|
+
if ( failure ) throw failure;
|
|
334
|
+
|
|
335
|
+
} );
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function forEachSlice( source, fn ) {
|
|
340
|
+
|
|
341
|
+
if ( source instanceof Uint8Array ) {
|
|
342
|
+
|
|
343
|
+
for ( let off = 0; off < source.length; off += SLICE_BYTES ) {
|
|
344
|
+
|
|
345
|
+
const end = Math.min( off + SLICE_BYTES, source.length );
|
|
346
|
+
fn( source.subarray( off, end ), end === source.length );
|
|
347
|
+
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if ( source.length === 0 ) fn( source, true );
|
|
351
|
+
return;
|
|
352
|
+
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const size = source.size;
|
|
356
|
+
for ( let off = 0; off < size; off += SLICE_BYTES ) {
|
|
357
|
+
|
|
358
|
+
const end = Math.min( off + SLICE_BYTES, size );
|
|
359
|
+
fn( new Uint8Array( await source.slice( off, end ).arrayBuffer() ), end === size );
|
|
360
|
+
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if ( size === 0 ) fn( new Uint8Array( 0 ), true );
|
|
364
|
+
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {Blob|File|Uint8Array} source
|
|
369
|
+
* @param {object} [options]
|
|
370
|
+
* @param {(path:string, size:number)=>boolean} [options.filter] - keep this entry's bytes
|
|
371
|
+
* @param {number} [options.byteBudget] - stop retaining past this many bytes; sets `truncated`
|
|
372
|
+
* @param {(progress:{entries:number, bytes:number})=>void} [options.onProgress]
|
|
373
|
+
* @returns {Promise<{entries:Object<string,Uint8Array>, listing:Array<{path:string,size:number}>,
|
|
374
|
+
* retainedBytes:number, truncated:boolean}>}
|
|
375
|
+
*/
|
|
376
|
+
export async function readTarGz( source, options = {} ) {
|
|
377
|
+
|
|
378
|
+
const { onProgress, ...rest } = options;
|
|
379
|
+
const tar = new TarStream( rest );
|
|
380
|
+
|
|
381
|
+
let bytes = 0;
|
|
382
|
+
let nextReport = 0;
|
|
383
|
+
const consume = chunk => {
|
|
384
|
+
|
|
385
|
+
tar.push( chunk );
|
|
386
|
+
bytes += chunk.length;
|
|
387
|
+
if ( onProgress && bytes >= nextReport ) {
|
|
388
|
+
|
|
389
|
+
nextReport = bytes + ( 64 << 20 );
|
|
390
|
+
onProgress( { entries: tar.listing.length, bytes } );
|
|
391
|
+
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
await streamGunzip( source, consume );
|
|
397
|
+
tar.end();
|
|
398
|
+
return tar.result();
|
|
399
|
+
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* A seekable view over an uncompressed .tar: entries are indexed on one streaming pass and read
|
|
404
|
+
* back on demand from the source, so the archive is never resident.
|
|
405
|
+
*
|
|
406
|
+
* Only for `.tar` over a Blob/File — a gzip stream cannot be seeked, and a zip is already read
|
|
407
|
+
* whole by fflate.
|
|
408
|
+
*
|
|
409
|
+
* @param {Blob|File} source
|
|
410
|
+
* @param {object} [options] - `filter` and `retain` as in readTar
|
|
411
|
+
* @returns {Promise<{entries, listing, read: (path:string)=>Promise<Uint8Array|null>, truncated}>}
|
|
412
|
+
*/
|
|
413
|
+
export async function openTar( source, options = {} ) {
|
|
414
|
+
|
|
415
|
+
const tar = new TarStream( { ...options } );
|
|
416
|
+
await forEachSlice( source, slice => tar.push( slice ) );
|
|
417
|
+
tar.end();
|
|
418
|
+
const { entries, listing, retainedBytes, truncated } = tar.result();
|
|
419
|
+
|
|
420
|
+
const byPath = new Map();
|
|
421
|
+
for ( const e of listing ) if ( e.offset !== undefined ) byPath.set( e.path, e );
|
|
422
|
+
|
|
423
|
+
const read = async ( path ) => {
|
|
424
|
+
|
|
425
|
+
if ( entries[ path ] ) return entries[ path ];
|
|
426
|
+
const e = byPath.get( path );
|
|
427
|
+
if ( ! e ) return null;
|
|
428
|
+
return new Uint8Array( await source.slice( e.offset, e.offset + e.size ).arrayBuffer() );
|
|
429
|
+
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
return { entries, listing, read, retainedBytes, truncated, indexed: byPath.size };
|
|
433
|
+
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Same contract as readTarGz for an uncompressed .tar. */
|
|
437
|
+
export async function readTar( source, options = {} ) {
|
|
438
|
+
|
|
439
|
+
const { onProgress, ...rest } = options;
|
|
440
|
+
const tar = new TarStream( rest );
|
|
441
|
+
await forEachSlice( source, slice => tar.push( slice ) );
|
|
442
|
+
tar.end();
|
|
443
|
+
void onProgress;
|
|
444
|
+
return tar.result();
|
|
445
|
+
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Retain-predicate for one or several subtrees of a scene archive.
|
|
450
|
+
*
|
|
451
|
+
* Keeps each chosen element plus everything above it — the top-level scene file, the shared
|
|
452
|
+
* material library, and any `textures` folder hanging off an ancestor — so the entry .pbrt
|
|
453
|
+
* still parses with its siblings absent. An Include pointing at a subtree that was left out
|
|
454
|
+
* only warns, which is what makes a partial load work at all.
|
|
455
|
+
*
|
|
456
|
+
* @param {string|string[]} prefixes - one element path, or several to load together
|
|
457
|
+
*/
|
|
458
|
+
export function elementFilter( prefixes ) {
|
|
459
|
+
|
|
460
|
+
const wanted = ( Array.isArray( prefixes ) ? prefixes : [ prefixes ] )
|
|
461
|
+
.filter( Boolean )
|
|
462
|
+
.map( prefix => {
|
|
463
|
+
|
|
464
|
+
const p = normalizeTarPath( prefix );
|
|
465
|
+
return { p, inside: p + '/' };
|
|
466
|
+
|
|
467
|
+
} );
|
|
468
|
+
|
|
469
|
+
if ( wanted.length === 0 ) return () => true;
|
|
470
|
+
|
|
471
|
+
return path => {
|
|
472
|
+
|
|
473
|
+
const cut = path.lastIndexOf( '/' );
|
|
474
|
+
const dir = cut < 0 ? '' : path.slice( 0, cut );
|
|
475
|
+
if ( dir === '' ) return true;
|
|
476
|
+
|
|
477
|
+
const tex = dir.lastIndexOf( '/textures' );
|
|
478
|
+
const texRoot = tex >= 0 && tex === dir.length - 9 ? dir.slice( 0, tex ) + '/' : null;
|
|
479
|
+
|
|
480
|
+
for ( const { p, inside } of wanted ) {
|
|
481
|
+
|
|
482
|
+
if ( path === p || path.startsWith( inside ) ) return true;
|
|
483
|
+
if ( inside.startsWith( dir + '/' ) ) return true;
|
|
484
|
+
if ( texRoot !== null && inside.startsWith( texRoot ) ) return true;
|
|
485
|
+
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return false;
|
|
489
|
+
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Group a listing into loadable subtrees.
|
|
496
|
+
*
|
|
497
|
+
* The scene root is the directory of the shallowest .pbrt; each of its child directories
|
|
498
|
+
* that holds a .pbrt of its own is an element the caller can load on its own.
|
|
499
|
+
*/
|
|
500
|
+
export function listArchiveElements( listing ) {
|
|
501
|
+
|
|
502
|
+
const scenes = listing
|
|
503
|
+
.filter( e => e.path.toLowerCase().endsWith( '.pbrt' ) )
|
|
504
|
+
.sort( ( a, b ) => a.path.split( '/' ).length - b.path.split( '/' ).length
|
|
505
|
+
|| a.path.length - b.path.length
|
|
506
|
+
|| ( a.path < b.path ? - 1 : 1 ) );
|
|
507
|
+
|
|
508
|
+
if ( ! scenes.length ) return { root: null, elements: [] };
|
|
509
|
+
|
|
510
|
+
const top = scenes[ 0 ].path;
|
|
511
|
+
const cut = top.lastIndexOf( '/' );
|
|
512
|
+
const root = cut < 0 ? '' : top.slice( 0, cut );
|
|
513
|
+
const under = root ? root + '/' : '';
|
|
514
|
+
|
|
515
|
+
const byName = new Map();
|
|
516
|
+
for ( const entry of listing ) {
|
|
517
|
+
|
|
518
|
+
if ( ! entry.path.startsWith( under ) ) continue;
|
|
519
|
+
const rest = entry.path.slice( under.length );
|
|
520
|
+
const slash = rest.indexOf( '/' );
|
|
521
|
+
if ( slash < 0 ) continue;
|
|
522
|
+
|
|
523
|
+
const name = rest.slice( 0, slash );
|
|
524
|
+
let el = byName.get( name );
|
|
525
|
+
if ( ! el ) byName.set( name, el = { name, prefix: under + name, files: 0, bytes: 0, scenes: 0 } );
|
|
526
|
+
el.files ++;
|
|
527
|
+
el.bytes += entry.size;
|
|
528
|
+
if ( entry.path.toLowerCase().endsWith( '.pbrt' ) ) el.scenes ++;
|
|
529
|
+
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const elements = [ ...byName.values() ]
|
|
533
|
+
.filter( el => el.scenes > 0 )
|
|
534
|
+
.sort( ( a, b ) => a.bytes - b.bytes );
|
|
535
|
+
|
|
536
|
+
return { root, elements };
|
|
537
|
+
|
|
538
|
+
}
|