@driftengine/assets 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/assetPath.d.ts +51 -0
- package/dist/assetPath.js +59 -0
- package/dist/coarseLevel.d.ts +94 -0
- package/dist/coarseLevel.js +606 -0
- package/dist/dds.d.ts +41 -0
- package/dist/dds.js +425 -0
- package/dist/drawKey.d.ts +64 -0
- package/dist/drawKey.js +42 -0
- package/dist/drftLoader.d.ts +420 -0
- package/dist/drftLoader.js +996 -0
- package/dist/drftTextures.d.ts +87 -0
- package/dist/drftTextures.js +138 -0
- package/dist/fbx.d.ts +86 -0
- package/dist/fbx.js +792 -0
- package/dist/fbxHarness.d.ts +68 -0
- package/dist/fbxHarness.js +185 -0
- package/dist/fbxInflate.d.ts +60 -0
- package/dist/fbxInflate.js +204 -0
- package/dist/fbxMaterial.d.ts +87 -0
- package/dist/fbxMaterial.js +345 -0
- package/dist/fbxRig.d.ts +130 -0
- package/dist/fbxRig.js +601 -0
- package/dist/gltf.d.ts +242 -0
- package/dist/gltf.js +904 -0
- package/dist/gltfAccessor.d.ts +2 -0
- package/dist/gltfAccessor.js +106 -0
- package/dist/gltfSkin.d.ts +32 -0
- package/dist/gltfSkin.js +144 -0
- package/dist/iesPhotometry.d.ts +44 -0
- package/dist/iesPhotometry.js +94 -0
- package/dist/imageInfo.d.ts +31 -0
- package/dist/imageInfo.js +119 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +132 -0
- package/dist/kn5.d.ts +158 -0
- package/dist/kn5.js +557 -0
- package/dist/loadProgress.d.ts +118 -0
- package/dist/loadProgress.js +1 -0
- package/dist/localise.d.ts +35 -0
- package/dist/localise.js +253 -0
- package/dist/obj.d.ts +54 -0
- package/dist/obj.js +335 -0
- package/dist/orient.d.ts +104 -0
- package/dist/orient.js +241 -0
- package/dist/radianceHdr.d.ts +36 -0
- package/dist/radianceHdr.js +172 -0
- package/dist/readModel.d.ts +153 -0
- package/dist/readModel.js +386 -0
- package/dist/recognise.d.ts +38 -0
- package/dist/recognise.js +97 -0
- package/dist/stl.d.ts +22 -0
- package/dist/stl.js +131 -0
- package/dist/tangentOrder.d.ts +24 -0
- package/dist/tangentOrder.js +31 -0
- package/dist/threemf.d.ts +46 -0
- package/dist/threemf.js +254 -0
- package/dist/uploadBudget.d.ts +82 -0
- package/dist/uploadBudget.js +86 -0
- package/dist/usd.d.ts +49 -0
- package/dist/usd.js +542 -0
- package/dist/weld.d.ts +44 -0
- package/dist/weld.js +237 -0
- package/dist/zip.d.ts +35 -0
- package/dist/zip.js +110 -0
- package/package.json +61 -0
- package/src/assetPath.ts +79 -0
- package/src/coarseLevel.ts +720 -0
- package/src/dds.ts +507 -0
- package/src/drawKey.ts +91 -0
- package/src/drftLoader.ts +1197 -0
- package/src/drftTextures.ts +153 -0
- package/src/fbx.ts +934 -0
- package/src/fbxHarness.ts +215 -0
- package/src/fbxInflate.ts +258 -0
- package/src/fbxMaterial.ts +410 -0
- package/src/fbxRig.ts +652 -0
- package/src/gltf.ts +1161 -0
- package/src/gltfAccessor.ts +127 -0
- package/src/gltfSkin.ts +210 -0
- package/src/iesPhotometry.ts +136 -0
- package/src/imageInfo.ts +130 -0
- package/src/index.ts +162 -0
- package/src/kn5.ts +705 -0
- package/src/loadProgress.ts +131 -0
- package/src/localise.ts +275 -0
- package/src/obj.ts +386 -0
- package/src/orient.ts +262 -0
- package/src/radianceHdr.ts +195 -0
- package/src/readModel.ts +515 -0
- package/src/recognise.ts +124 -0
- package/src/stl.ts +147 -0
- package/src/tangentOrder.ts +32 -0
- package/src/threemf.ts +303 -0
- package/src/uploadBudget.ts +90 -0
- package/src/usd.ts +642 -0
- package/src/weld.ts +264 -0
- package/src/zip.ts +130 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A binary FBX, written, so a reader can be tested without a licence and without a fixture.
|
|
3
|
+
*
|
|
4
|
+
* **Not a `.test.ts`, for the reason `rendererHarness.ts` gives one level down**: Vitest registers
|
|
5
|
+
* a test when the file declaring it is imported, so a second test file importing this from beside
|
|
6
|
+
* its own tests would re-run every test in that file too. Split out so importing the harness costs
|
|
7
|
+
* nothing but the harness.
|
|
8
|
+
*
|
|
9
|
+
* **Why write one rather than check one in.** The rig-carrying files this reader exists for are
|
|
10
|
+
* bought character assets: they are tens of megabytes, they are licensed, and none of them can be
|
|
11
|
+
* committed here. A fixture would also answer only the questions that one file happens to ask. A
|
|
12
|
+
* writer answers the ones a test asks — a cluster whose weights do not sum to one, a bone whose
|
|
13
|
+
* only connection is to its cluster, a curve with one key — and each is three lines rather than a
|
|
14
|
+
* hunt for a file that happens to contain it.
|
|
15
|
+
*
|
|
16
|
+
* **What it is not**: a general FBX writer. It emits the records this reader reads, in the shape
|
|
17
|
+
* Autodesk's own exporters emit them, and nothing else. Arrays are written uncompressed, which is
|
|
18
|
+
* legal — `encoding` 0 — and keeps the harness free of a deflate implementation.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** A property, tagged with the type letter the container stores it under. */
|
|
22
|
+
export type FbxProperty =
|
|
23
|
+
| { readonly kind: 'I'; readonly value: number }
|
|
24
|
+
| { readonly kind: 'D'; readonly value: number }
|
|
25
|
+
| { readonly kind: 'L'; readonly value: number }
|
|
26
|
+
| { readonly kind: 'S'; readonly value: string }
|
|
27
|
+
| { readonly kind: 'd'; readonly value: readonly number[] }
|
|
28
|
+
| { readonly kind: 'i'; readonly value: readonly number[] }
|
|
29
|
+
| { readonly kind: 'l'; readonly value: readonly number[] };
|
|
30
|
+
|
|
31
|
+
export const I = (value: number): FbxProperty => ({ kind: 'I', value });
|
|
32
|
+
export const D = (value: number): FbxProperty => ({ kind: 'D', value });
|
|
33
|
+
export const L = (value: number): FbxProperty => ({ kind: 'L', value });
|
|
34
|
+
export const S = (value: string): FbxProperty => ({ kind: 'S', value });
|
|
35
|
+
export const doubles = (value: readonly number[]): FbxProperty => ({ kind: 'd', value });
|
|
36
|
+
export const ints = (value: readonly number[]): FbxProperty => ({ kind: 'i', value });
|
|
37
|
+
export const longs = (value: readonly number[]): FbxProperty => ({ kind: 'l', value });
|
|
38
|
+
|
|
39
|
+
export interface FbxWritable {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly properties?: readonly FbxProperty[];
|
|
42
|
+
readonly children?: readonly FbxWritable[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const node = (
|
|
46
|
+
name: string,
|
|
47
|
+
properties: readonly FbxProperty[] = [],
|
|
48
|
+
children: readonly FbxWritable[] = [],
|
|
49
|
+
): FbxWritable => ({ name, properties, children });
|
|
50
|
+
|
|
51
|
+
/** A `Properties70` entry, which is a `P` record of name, type, subtype, flags, then values. */
|
|
52
|
+
export const p = (name: string, type: string, ...values: readonly FbxProperty[]): FbxWritable =>
|
|
53
|
+
node('P', [S(name), S(type), S(''), S(''), ...values]);
|
|
54
|
+
|
|
55
|
+
/** One `C` record. `OO` is object-to-object; `OP` names the property it drives. */
|
|
56
|
+
export const connect = (child: number, parent: number, property?: string): FbxWritable =>
|
|
57
|
+
property === undefined
|
|
58
|
+
? node('C', [S('OO'), L(child), L(parent)])
|
|
59
|
+
: node('C', [S('OP'), L(child), L(parent), S(property)]);
|
|
60
|
+
|
|
61
|
+
class Bytes {
|
|
62
|
+
private buffer = new Uint8Array(1024);
|
|
63
|
+
private view = new DataView(this.buffer.buffer);
|
|
64
|
+
length = 0;
|
|
65
|
+
|
|
66
|
+
private room(extra: number): void {
|
|
67
|
+
if (this.length + extra <= this.buffer.length) return;
|
|
68
|
+
let size = this.buffer.length;
|
|
69
|
+
while (size < this.length + extra) size *= 2;
|
|
70
|
+
const grown = new Uint8Array(size);
|
|
71
|
+
grown.set(this.buffer.subarray(0, this.length));
|
|
72
|
+
this.buffer = grown;
|
|
73
|
+
this.view = new DataView(grown.buffer);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
u8(value: number): void {
|
|
77
|
+
this.room(1);
|
|
78
|
+
this.view.setUint8(this.length, value);
|
|
79
|
+
this.length += 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
u32(value: number): void {
|
|
83
|
+
this.room(4);
|
|
84
|
+
this.view.setUint32(this.length, value, true);
|
|
85
|
+
this.length += 4;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
i32(value: number): void {
|
|
89
|
+
this.room(4);
|
|
90
|
+
this.view.setInt32(this.length, value, true);
|
|
91
|
+
this.length += 4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
f32(value: number): void {
|
|
95
|
+
this.room(4);
|
|
96
|
+
this.view.setFloat32(this.length, value, true);
|
|
97
|
+
this.length += 4;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
f64(value: number): void {
|
|
101
|
+
this.room(8);
|
|
102
|
+
this.view.setFloat64(this.length, value, true);
|
|
103
|
+
this.length += 8;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
i64(value: number): void {
|
|
107
|
+
this.room(8);
|
|
108
|
+
this.view.setBigInt64(this.length, BigInt(Math.trunc(value)), true);
|
|
109
|
+
this.length += 8;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
raw(value: Uint8Array): void {
|
|
113
|
+
this.room(value.length);
|
|
114
|
+
this.buffer.set(value, this.length);
|
|
115
|
+
this.length += value.length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Overwrite a `u32` already written, which is how a record learns where it ends. */
|
|
119
|
+
patch(at: number, value: number): void {
|
|
120
|
+
this.view.setUint32(at, value, true);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
done(): Uint8Array {
|
|
124
|
+
return this.buffer.subarray(0, this.length);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const latin1 = (text: string): Uint8Array => {
|
|
129
|
+
const out = new Uint8Array(text.length);
|
|
130
|
+
for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i) & 0xff;
|
|
131
|
+
return out;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function writeProperty(out: Bytes, property: FbxProperty): void {
|
|
135
|
+
out.u8(property.kind.charCodeAt(0));
|
|
136
|
+
switch (property.kind) {
|
|
137
|
+
case 'I':
|
|
138
|
+
out.i32(property.value);
|
|
139
|
+
return;
|
|
140
|
+
case 'D':
|
|
141
|
+
out.f64(property.value);
|
|
142
|
+
return;
|
|
143
|
+
case 'L':
|
|
144
|
+
out.i64(property.value);
|
|
145
|
+
return;
|
|
146
|
+
case 'S': {
|
|
147
|
+
const text = latin1(property.value);
|
|
148
|
+
out.u32(text.length);
|
|
149
|
+
out.raw(text);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
default: {
|
|
153
|
+
/* count, encoding, then the byte length of the payload. Encoding 0 is uncompressed. */
|
|
154
|
+
const values = property.value;
|
|
155
|
+
out.u32(values.length);
|
|
156
|
+
out.u32(0);
|
|
157
|
+
const width = property.kind === 'd' || property.kind === 'l' ? 8 : 4;
|
|
158
|
+
out.u32(values.length * width);
|
|
159
|
+
for (const value of values) {
|
|
160
|
+
if (property.kind === 'd') out.f64(value);
|
|
161
|
+
else if (property.kind === 'l') out.i64(value);
|
|
162
|
+
else out.i32(value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeNode(out: Bytes, subject: FbxWritable): void {
|
|
169
|
+
const endAt = out.length;
|
|
170
|
+
out.u32(0);
|
|
171
|
+
const properties = subject.properties ?? [];
|
|
172
|
+
const children = subject.children ?? [];
|
|
173
|
+
out.u32(properties.length);
|
|
174
|
+
const lengthAt = out.length;
|
|
175
|
+
out.u32(0);
|
|
176
|
+
const name = latin1(subject.name);
|
|
177
|
+
out.u8(name.length);
|
|
178
|
+
out.raw(name);
|
|
179
|
+
|
|
180
|
+
const propertiesFrom = out.length;
|
|
181
|
+
for (const property of properties) writeProperty(out, property);
|
|
182
|
+
out.patch(lengthAt, out.length - propertiesFrom);
|
|
183
|
+
|
|
184
|
+
if (children.length > 0) {
|
|
185
|
+
for (const child of children) writeNode(out, child);
|
|
186
|
+
/* The null record that closes a nested list: thirteen zero bytes at this version. */
|
|
187
|
+
for (let i = 0; i < 13; i++) out.u8(0);
|
|
188
|
+
}
|
|
189
|
+
out.patch(endAt, out.length);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A whole file: the magic, the version, the records, and the terminator.
|
|
194
|
+
*
|
|
195
|
+
* Version is a parameter because the container's one structural break is at 7500, where record
|
|
196
|
+
* offsets widen to 64 bits. This writer emits the narrow form, so it writes files below that —
|
|
197
|
+
* which is what every exporter in the wild still writes, and what the reader's own `wide` branch
|
|
198
|
+
* exists to tell apart.
|
|
199
|
+
*/
|
|
200
|
+
export function writeFbx(children: readonly FbxWritable[], version = 7400): ArrayBuffer {
|
|
201
|
+
if (version >= 7500) throw new Error('fbxHarness: this writer emits the pre-7500 narrow form');
|
|
202
|
+
const out = new Bytes();
|
|
203
|
+
out.raw(latin1('Kaydara FBX Binary '));
|
|
204
|
+
out.u8(0x00);
|
|
205
|
+
out.u8(0x1a);
|
|
206
|
+
out.u8(0x00);
|
|
207
|
+
out.u32(version);
|
|
208
|
+
for (const child of children) writeNode(out, child);
|
|
209
|
+
/* A null record to end the top-level list, then the footer's slack: the reader stops sixteen
|
|
210
|
+
bytes from the end, which is what a real file's footer occupies. */
|
|
211
|
+
for (let i = 0; i < 13; i++) out.u8(0);
|
|
212
|
+
for (let i = 0; i < 16; i++) out.u8(0);
|
|
213
|
+
const bytes = out.done();
|
|
214
|
+
return bytes.slice().buffer as ArrayBuffer;
|
|
215
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decompressing an FBX's arrays in a browser. One responsibility: make an async
|
|
3
|
+
* decompressor usable by a synchronous parser.
|
|
4
|
+
*
|
|
5
|
+
* **The problem.** FBX stores its vertex arrays deflate-compressed, and `fbxToMeshes` is
|
|
6
|
+
* synchronous: it walks a recursive record tree and decodes each array where it meets it.
|
|
7
|
+
* The only decompressor a browser has built in is `DecompressionStream`, which is
|
|
8
|
+
* asynchronous. So the reader ran everywhere except the one place it was designed to be able
|
|
9
|
+
* to run, and only for files that happen to store their arrays uncompressed.
|
|
10
|
+
*
|
|
11
|
+
* **Why not make the reader async.** Every array decode sits inside a recursive descent over
|
|
12
|
+
* the tree, so awaiting there turns the whole parser inside out for a cost paid once per
|
|
13
|
+
* file. It would also make the baker's path async for no reason, since Node has had a
|
|
14
|
+
* synchronous inflate all along.
|
|
15
|
+
*
|
|
16
|
+
* **Why not ship a decompressor.** Writing a DEFLATE decoder is a couple of hundred lines of
|
|
17
|
+
* Huffman tables and back-references. It is well specified and it is also exactly the sort of
|
|
18
|
+
* thing the platform already does, faster, in code somebody else maintains. Adding one would
|
|
19
|
+
* be choosing to own a correctness risk for nothing.
|
|
20
|
+
*
|
|
21
|
+
* **So: decompress everything first, then parse.** The tree is walked once with an inflate
|
|
22
|
+
* that *records* each compressed span instead of expanding it, every span is decompressed
|
|
23
|
+
* concurrently, and the real parse then runs synchronously against the results. Two structural
|
|
24
|
+
* walks rather than one, and the expensive half happens exactly once.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { parseFbxTree } from './fbx.ts';
|
|
28
|
+
import { readZip } from './zip.ts';
|
|
29
|
+
import type { Inflate } from './fbx.ts';
|
|
30
|
+
import { DrftError } from '@driftengine/drft';
|
|
31
|
+
|
|
32
|
+
/** Turn a deflate stream into bytes, eventually. What a browser can actually offer. */
|
|
33
|
+
export type AsyncInflate = (compressed: Uint8Array, expectedBytes: number) => Promise<Uint8Array>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read a decompression stream to the end, and decide what a failure at the end means.
|
|
37
|
+
*
|
|
38
|
+
* **Written because `new Response(stream).arrayBuffer()` throws away the reason.** Measured in
|
|
39
|
+
* Chrome 141 over the four ways a deflate stream can fail — bytes after the end, a truncated
|
|
40
|
+
* payload, a flipped byte, and something that is not deflate at all — and the Response reports
|
|
41
|
+
* exactly `Failed to fetch` for every one of them. That is the message a consumer saw for a
|
|
42
|
+
* model that would not load, and it names the network for a fault that has nothing to do with
|
|
43
|
+
* it. A reader keeps what the platform actually said: *Junk found after end of compressed
|
|
44
|
+
* data*, *Compressed input was truncated*, *incorrect header check*.
|
|
45
|
+
*
|
|
46
|
+
* **And bytes after the end of the stream are not a failure**, which is the other half and the
|
|
47
|
+
* one that changes what loads. Measured on both sides: Node's `zlib.inflateSync` returns all
|
|
48
|
+
* 4,096 bytes of a stream with sixteen zeros appended, and `DecompressionStream` errors on the
|
|
49
|
+
* same input *after* delivering all 4,096. So an exporter that writes a padded length produces a
|
|
50
|
+
* file the baker reads and a browser refuses, which is a difference between two hosts reading one
|
|
51
|
+
* format rather than anything about the file. The bytes are complete either way, so they are
|
|
52
|
+
* kept, and the error is only raised when the array actually came up short.
|
|
53
|
+
*
|
|
54
|
+
* What would make this wrong: a stream that errors early *and* whose header happens to promise
|
|
55
|
+
* fewer bytes than arrived. `expectedBytes` comes from the record around the payload rather than
|
|
56
|
+
* from the payload itself, so that would need the file to be wrong about its own array, which is
|
|
57
|
+
* a different failure and one the reader above this catches by shape.
|
|
58
|
+
*/
|
|
59
|
+
async function readToEnd(
|
|
60
|
+
stream: ReadableStream<Uint8Array>,
|
|
61
|
+
expectedBytes: number,
|
|
62
|
+
): Promise<Uint8Array> {
|
|
63
|
+
const reader = stream.getReader();
|
|
64
|
+
const chunks: Uint8Array[] = [];
|
|
65
|
+
let got = 0;
|
|
66
|
+
try {
|
|
67
|
+
for (;;) {
|
|
68
|
+
const { done, value } = await reader.read();
|
|
69
|
+
if (done) break;
|
|
70
|
+
chunks.push(value);
|
|
71
|
+
got += value.length;
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
/* Short, so the array is genuinely missing bytes and the reason belongs to the caller. */
|
|
75
|
+
if (got === 0 || got < expectedBytes) {
|
|
76
|
+
const said = error instanceof Error ? error.message : String(error);
|
|
77
|
+
throw new Error(said === '' ? 'the compressed data could not be read' : said);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const out = new Uint8Array(got);
|
|
81
|
+
let at = 0;
|
|
82
|
+
for (const chunk of chunks) {
|
|
83
|
+
out.set(chunk, at);
|
|
84
|
+
at += chunk.length;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `DecompressionStream`, which every target browser has and which needs no dependency.
|
|
91
|
+
*
|
|
92
|
+
* FBX writes zlib-wrapped deflate, so `'deflate'` rather than `'deflate-raw'`. Getting that
|
|
93
|
+
* wrong fails on the first two bytes rather than producing something subtly wrong, which is
|
|
94
|
+
* the good kind of mistake to make.
|
|
95
|
+
*/
|
|
96
|
+
export const browserInflate: AsyncInflate = async (compressed, expectedBytes) => {
|
|
97
|
+
const stream = new Blob([compressed.slice()])
|
|
98
|
+
.stream()
|
|
99
|
+
.pipeThrough(new DecompressionStream('deflate'));
|
|
100
|
+
return readToEnd(stream, expectedBytes);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decompress every array in a file, then hand back a synchronous `Inflate` over the results.
|
|
105
|
+
*
|
|
106
|
+
* The returned function is what `fbxToMeshes` wants, so the reader itself needs no change and
|
|
107
|
+
* the baker keeps using Node's synchronous inflate directly.
|
|
108
|
+
*
|
|
109
|
+
* Spans are keyed by `byteOffset`, which identifies them exactly: each compressed payload is
|
|
110
|
+
* a subarray of the one buffer being read, so its offset within that buffer is unique and is
|
|
111
|
+
* the same value the parser will present on the second pass.
|
|
112
|
+
*/
|
|
113
|
+
interface Span {
|
|
114
|
+
readonly at: number;
|
|
115
|
+
readonly compressed: Uint8Array;
|
|
116
|
+
readonly expected: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Expand every recorded span, and give a failure a second chance on its own.
|
|
121
|
+
*
|
|
122
|
+
* **The retry is for a fan-out and not for the data.** Both callers hand `Promise.all` every
|
|
123
|
+
* span a file has at once, which for one bought model measured 1,586 concurrent Blobs, streams
|
|
124
|
+
* and stream readers, on top of the 32 the archive around it had already run. A file this shape
|
|
125
|
+
* was reported as failing at one span while the same span, decompressed on its own in the same
|
|
126
|
+
* browser, was measured as completely sound: the compressed length is 1,064 bytes, the array is
|
|
127
|
+
* 1,680, and it expands to exactly that. So the one thing left standing is the company it was
|
|
128
|
+
* decompressed in, which is a resource failure rather than a file.
|
|
129
|
+
*
|
|
130
|
+
* The cost is one extra attempt, on the failure path only, after the fan-out has drained. What
|
|
131
|
+
* would make it wrong is a file that fails deterministically: then the second attempt fails
|
|
132
|
+
* identically, the throw carries the platform's own reason, and the only thing lost is the time
|
|
133
|
+
* of a single span. That is the trade, and it is why the retry is not a loop.
|
|
134
|
+
*/
|
|
135
|
+
async function expandSpans(
|
|
136
|
+
spans: readonly Span[],
|
|
137
|
+
inflate: AsyncInflate,
|
|
138
|
+
format: 'fbx' | 'zip',
|
|
139
|
+
): Promise<Map<number, Uint8Array>> {
|
|
140
|
+
const decoded = new Map<number, Uint8Array>();
|
|
141
|
+
const failed: Span[] = [];
|
|
142
|
+
|
|
143
|
+
await Promise.all(
|
|
144
|
+
spans.map(async (span) => {
|
|
145
|
+
try {
|
|
146
|
+
decoded.set(span.at, await inflate(span.compressed, span.expected));
|
|
147
|
+
} catch {
|
|
148
|
+
failed.push(span);
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
for (const span of failed) {
|
|
154
|
+
try {
|
|
155
|
+
decoded.set(span.at, await inflate(span.compressed, span.expected));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new DrftError(
|
|
158
|
+
`${format}: an array at byte ${span.at} would not decompress: ` +
|
|
159
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return decoded;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function prepareFbxInflate(
|
|
168
|
+
buffer: ArrayBuffer,
|
|
169
|
+
inflate: AsyncInflate,
|
|
170
|
+
): Promise<Inflate> {
|
|
171
|
+
const spans: Span[] = [];
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* Pass one records rather than expands. The stand-in has to be the *expected* length,
|
|
175
|
+
* because the caller immediately builds a typed array over it and a short one would throw
|
|
176
|
+
* here rather than at the point the shape is actually wrong.
|
|
177
|
+
*/
|
|
178
|
+
parseFbxTree(buffer, (compressed, expected) => {
|
|
179
|
+
spans.push({ at: compressed.byteOffset, compressed, expected });
|
|
180
|
+
return new Uint8Array(expected);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const decoded = await expandSpans(spans, inflate, 'fbx');
|
|
184
|
+
|
|
185
|
+
return (compressed: Uint8Array, expectedBytes: number): Uint8Array => {
|
|
186
|
+
const found = decoded.get(compressed.byteOffset);
|
|
187
|
+
if (found === undefined) {
|
|
188
|
+
/*
|
|
189
|
+
* Only reachable if the two passes disagreed about the file, which would mean the
|
|
190
|
+
* parse is not deterministic. Loud, because silently returning empty bytes would draw
|
|
191
|
+
* a model with a hole in it and send somebody looking at the geometry reader.
|
|
192
|
+
*/
|
|
193
|
+
throw new DrftError(
|
|
194
|
+
`fbx: no decompressed array was prepared for byte ${compressed.byteOffset}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (found.length < expectedBytes) {
|
|
198
|
+
throw new DrftError(
|
|
199
|
+
`fbx: an array at byte ${compressed.byteOffset} decompressed to ${found.length} bytes, ` +
|
|
200
|
+
`and its own header says ${expectedBytes}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return found;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The same trick for a zip, which is how a `.3mf` and a bought archive both arrive.
|
|
209
|
+
*
|
|
210
|
+
* **Promoted from a consumer's private copy.** Every piece needed to open an archive was already
|
|
211
|
+
* exported — `readZip`, `readerFor`, `MODEL_FORMATS`, `assetCandidates`, `ModelSource.beside` — and
|
|
212
|
+
* a second consumer still spent about sixty lines assembling them, most of it rediscovering this
|
|
213
|
+
* function. The engine's own model worker had implemented it privately for `.3mf` at the same time,
|
|
214
|
+
* which is two copies of one idea and the point at which it belongs here.
|
|
215
|
+
*
|
|
216
|
+
* **A zip stores raw deflate and FBX stores zlib-wrapped**, so this takes its own decompressor and
|
|
217
|
+
* is deliberately a separate name from `prepareFbxInflate`. Passing either where the other belongs
|
|
218
|
+
* fails on the first bytes, which is the good kind of mistake.
|
|
219
|
+
*
|
|
220
|
+
* Two passes for the same reason: `readZip` expands each entry as it walks and the only
|
|
221
|
+
* decompressor a browser has is asynchronous, so entries are recorded first and answered second.
|
|
222
|
+
* Spans are keyed by `byteOffset`, which identifies them exactly, since each payload is a subarray
|
|
223
|
+
* of the one buffer being read.
|
|
224
|
+
*/
|
|
225
|
+
export async function prepareZipInflate(
|
|
226
|
+
buffer: ArrayBuffer,
|
|
227
|
+
inflate: AsyncInflate,
|
|
228
|
+
): Promise<Inflate> {
|
|
229
|
+
const spans: Span[] = [];
|
|
230
|
+
readZip(buffer, (compressed, expected) => {
|
|
231
|
+
spans.push({ at: compressed.byteOffset, compressed, expected });
|
|
232
|
+
return new Uint8Array(expected);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/* The same expansion the FBX path takes, which this one used to do without the retry and
|
|
236
|
+
without saying which entry failed: a bare rejection out of `Promise.all` named neither. */
|
|
237
|
+
const decoded = await expandSpans(spans, inflate, 'zip');
|
|
238
|
+
|
|
239
|
+
return (compressed: Uint8Array): Uint8Array => {
|
|
240
|
+
const found = decoded.get(compressed.byteOffset);
|
|
241
|
+
if (found === undefined) {
|
|
242
|
+
throw new DrftError(
|
|
243
|
+
`zip: no decompressed entry was prepared for byte ${compressed.byteOffset}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return found;
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Raw deflate through the browser, which is what a zip needs and what FBX must not be given.
|
|
252
|
+
*/
|
|
253
|
+
export const browserInflateRaw: AsyncInflate = async (compressed, expectedBytes) => {
|
|
254
|
+
const stream = new Blob([compressed.slice()])
|
|
255
|
+
.stream()
|
|
256
|
+
.pipeThrough(new DecompressionStream('deflate-raw'));
|
|
257
|
+
return readToEnd(stream, expectedBytes);
|
|
258
|
+
};
|