@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.
Files changed (100) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/assetPath.d.ts +51 -0
  5. package/dist/assetPath.js +59 -0
  6. package/dist/coarseLevel.d.ts +94 -0
  7. package/dist/coarseLevel.js +606 -0
  8. package/dist/dds.d.ts +41 -0
  9. package/dist/dds.js +425 -0
  10. package/dist/drawKey.d.ts +64 -0
  11. package/dist/drawKey.js +42 -0
  12. package/dist/drftLoader.d.ts +420 -0
  13. package/dist/drftLoader.js +996 -0
  14. package/dist/drftTextures.d.ts +87 -0
  15. package/dist/drftTextures.js +138 -0
  16. package/dist/fbx.d.ts +86 -0
  17. package/dist/fbx.js +792 -0
  18. package/dist/fbxHarness.d.ts +68 -0
  19. package/dist/fbxHarness.js +185 -0
  20. package/dist/fbxInflate.d.ts +60 -0
  21. package/dist/fbxInflate.js +204 -0
  22. package/dist/fbxMaterial.d.ts +87 -0
  23. package/dist/fbxMaterial.js +345 -0
  24. package/dist/fbxRig.d.ts +130 -0
  25. package/dist/fbxRig.js +601 -0
  26. package/dist/gltf.d.ts +242 -0
  27. package/dist/gltf.js +904 -0
  28. package/dist/gltfAccessor.d.ts +2 -0
  29. package/dist/gltfAccessor.js +106 -0
  30. package/dist/gltfSkin.d.ts +32 -0
  31. package/dist/gltfSkin.js +144 -0
  32. package/dist/iesPhotometry.d.ts +44 -0
  33. package/dist/iesPhotometry.js +94 -0
  34. package/dist/imageInfo.d.ts +31 -0
  35. package/dist/imageInfo.js +119 -0
  36. package/dist/index.d.ts +58 -0
  37. package/dist/index.js +132 -0
  38. package/dist/kn5.d.ts +158 -0
  39. package/dist/kn5.js +557 -0
  40. package/dist/loadProgress.d.ts +118 -0
  41. package/dist/loadProgress.js +1 -0
  42. package/dist/localise.d.ts +35 -0
  43. package/dist/localise.js +253 -0
  44. package/dist/obj.d.ts +54 -0
  45. package/dist/obj.js +335 -0
  46. package/dist/orient.d.ts +104 -0
  47. package/dist/orient.js +241 -0
  48. package/dist/radianceHdr.d.ts +36 -0
  49. package/dist/radianceHdr.js +172 -0
  50. package/dist/readModel.d.ts +153 -0
  51. package/dist/readModel.js +386 -0
  52. package/dist/recognise.d.ts +38 -0
  53. package/dist/recognise.js +97 -0
  54. package/dist/stl.d.ts +22 -0
  55. package/dist/stl.js +131 -0
  56. package/dist/tangentOrder.d.ts +24 -0
  57. package/dist/tangentOrder.js +31 -0
  58. package/dist/threemf.d.ts +46 -0
  59. package/dist/threemf.js +254 -0
  60. package/dist/uploadBudget.d.ts +82 -0
  61. package/dist/uploadBudget.js +86 -0
  62. package/dist/usd.d.ts +49 -0
  63. package/dist/usd.js +542 -0
  64. package/dist/weld.d.ts +44 -0
  65. package/dist/weld.js +237 -0
  66. package/dist/zip.d.ts +35 -0
  67. package/dist/zip.js +110 -0
  68. package/package.json +61 -0
  69. package/src/assetPath.ts +79 -0
  70. package/src/coarseLevel.ts +720 -0
  71. package/src/dds.ts +507 -0
  72. package/src/drawKey.ts +91 -0
  73. package/src/drftLoader.ts +1197 -0
  74. package/src/drftTextures.ts +153 -0
  75. package/src/fbx.ts +934 -0
  76. package/src/fbxHarness.ts +215 -0
  77. package/src/fbxInflate.ts +258 -0
  78. package/src/fbxMaterial.ts +410 -0
  79. package/src/fbxRig.ts +652 -0
  80. package/src/gltf.ts +1161 -0
  81. package/src/gltfAccessor.ts +127 -0
  82. package/src/gltfSkin.ts +210 -0
  83. package/src/iesPhotometry.ts +136 -0
  84. package/src/imageInfo.ts +130 -0
  85. package/src/index.ts +162 -0
  86. package/src/kn5.ts +705 -0
  87. package/src/loadProgress.ts +131 -0
  88. package/src/localise.ts +275 -0
  89. package/src/obj.ts +386 -0
  90. package/src/orient.ts +262 -0
  91. package/src/radianceHdr.ts +195 -0
  92. package/src/readModel.ts +515 -0
  93. package/src/recognise.ts +124 -0
  94. package/src/stl.ts +147 -0
  95. package/src/tangentOrder.ts +32 -0
  96. package/src/threemf.ts +303 -0
  97. package/src/uploadBudget.ts +90 -0
  98. package/src/usd.ts +642 -0
  99. package/src/weld.ts +264 -0
  100. package/src/zip.ts +130 -0
@@ -0,0 +1,68 @@
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
+ /** A property, tagged with the type letter the container stores it under. */
21
+ export type FbxProperty = {
22
+ readonly kind: 'I';
23
+ readonly value: number;
24
+ } | {
25
+ readonly kind: 'D';
26
+ readonly value: number;
27
+ } | {
28
+ readonly kind: 'L';
29
+ readonly value: number;
30
+ } | {
31
+ readonly kind: 'S';
32
+ readonly value: string;
33
+ } | {
34
+ readonly kind: 'd';
35
+ readonly value: readonly number[];
36
+ } | {
37
+ readonly kind: 'i';
38
+ readonly value: readonly number[];
39
+ } | {
40
+ readonly kind: 'l';
41
+ readonly value: readonly number[];
42
+ };
43
+ export declare const I: (value: number) => FbxProperty;
44
+ export declare const D: (value: number) => FbxProperty;
45
+ export declare const L: (value: number) => FbxProperty;
46
+ export declare const S: (value: string) => FbxProperty;
47
+ export declare const doubles: (value: readonly number[]) => FbxProperty;
48
+ export declare const ints: (value: readonly number[]) => FbxProperty;
49
+ export declare const longs: (value: readonly number[]) => FbxProperty;
50
+ export interface FbxWritable {
51
+ readonly name: string;
52
+ readonly properties?: readonly FbxProperty[];
53
+ readonly children?: readonly FbxWritable[];
54
+ }
55
+ export declare const node: (name: string, properties?: readonly FbxProperty[], children?: readonly FbxWritable[]) => FbxWritable;
56
+ /** A `Properties70` entry, which is a `P` record of name, type, subtype, flags, then values. */
57
+ export declare const p: (name: string, type: string, ...values: readonly FbxProperty[]) => FbxWritable;
58
+ /** One `C` record. `OO` is object-to-object; `OP` names the property it drives. */
59
+ export declare const connect: (child: number, parent: number, property?: string) => FbxWritable;
60
+ /**
61
+ * A whole file: the magic, the version, the records, and the terminator.
62
+ *
63
+ * Version is a parameter because the container's one structural break is at 7500, where record
64
+ * offsets widen to 64 bits. This writer emits the narrow form, so it writes files below that —
65
+ * which is what every exporter in the wild still writes, and what the reader's own `wide` branch
66
+ * exists to tell apart.
67
+ */
68
+ export declare function writeFbx(children: readonly FbxWritable[], version?: number): ArrayBuffer;
@@ -0,0 +1,185 @@
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
+ export const I = (value) => ({ kind: 'I', value });
21
+ export const D = (value) => ({ kind: 'D', value });
22
+ export const L = (value) => ({ kind: 'L', value });
23
+ export const S = (value) => ({ kind: 'S', value });
24
+ export const doubles = (value) => ({ kind: 'd', value });
25
+ export const ints = (value) => ({ kind: 'i', value });
26
+ export const longs = (value) => ({ kind: 'l', value });
27
+ export const node = (name, properties = [], children = []) => ({ name, properties, children });
28
+ /** A `Properties70` entry, which is a `P` record of name, type, subtype, flags, then values. */
29
+ export const p = (name, type, ...values) => node('P', [S(name), S(type), S(''), S(''), ...values]);
30
+ /** One `C` record. `OO` is object-to-object; `OP` names the property it drives. */
31
+ export const connect = (child, parent, property) => property === undefined
32
+ ? node('C', [S('OO'), L(child), L(parent)])
33
+ : node('C', [S('OP'), L(child), L(parent), S(property)]);
34
+ class Bytes {
35
+ buffer = new Uint8Array(1024);
36
+ view = new DataView(this.buffer.buffer);
37
+ length = 0;
38
+ room(extra) {
39
+ if (this.length + extra <= this.buffer.length)
40
+ return;
41
+ let size = this.buffer.length;
42
+ while (size < this.length + extra)
43
+ size *= 2;
44
+ const grown = new Uint8Array(size);
45
+ grown.set(this.buffer.subarray(0, this.length));
46
+ this.buffer = grown;
47
+ this.view = new DataView(grown.buffer);
48
+ }
49
+ u8(value) {
50
+ this.room(1);
51
+ this.view.setUint8(this.length, value);
52
+ this.length += 1;
53
+ }
54
+ u32(value) {
55
+ this.room(4);
56
+ this.view.setUint32(this.length, value, true);
57
+ this.length += 4;
58
+ }
59
+ i32(value) {
60
+ this.room(4);
61
+ this.view.setInt32(this.length, value, true);
62
+ this.length += 4;
63
+ }
64
+ f32(value) {
65
+ this.room(4);
66
+ this.view.setFloat32(this.length, value, true);
67
+ this.length += 4;
68
+ }
69
+ f64(value) {
70
+ this.room(8);
71
+ this.view.setFloat64(this.length, value, true);
72
+ this.length += 8;
73
+ }
74
+ i64(value) {
75
+ this.room(8);
76
+ this.view.setBigInt64(this.length, BigInt(Math.trunc(value)), true);
77
+ this.length += 8;
78
+ }
79
+ raw(value) {
80
+ this.room(value.length);
81
+ this.buffer.set(value, this.length);
82
+ this.length += value.length;
83
+ }
84
+ /** Overwrite a `u32` already written, which is how a record learns where it ends. */
85
+ patch(at, value) {
86
+ this.view.setUint32(at, value, true);
87
+ }
88
+ done() {
89
+ return this.buffer.subarray(0, this.length);
90
+ }
91
+ }
92
+ const latin1 = (text) => {
93
+ const out = new Uint8Array(text.length);
94
+ for (let i = 0; i < text.length; i++)
95
+ out[i] = text.charCodeAt(i) & 0xff;
96
+ return out;
97
+ };
98
+ function writeProperty(out, property) {
99
+ out.u8(property.kind.charCodeAt(0));
100
+ switch (property.kind) {
101
+ case 'I':
102
+ out.i32(property.value);
103
+ return;
104
+ case 'D':
105
+ out.f64(property.value);
106
+ return;
107
+ case 'L':
108
+ out.i64(property.value);
109
+ return;
110
+ case 'S': {
111
+ const text = latin1(property.value);
112
+ out.u32(text.length);
113
+ out.raw(text);
114
+ return;
115
+ }
116
+ default: {
117
+ /* count, encoding, then the byte length of the payload. Encoding 0 is uncompressed. */
118
+ const values = property.value;
119
+ out.u32(values.length);
120
+ out.u32(0);
121
+ const width = property.kind === 'd' || property.kind === 'l' ? 8 : 4;
122
+ out.u32(values.length * width);
123
+ for (const value of values) {
124
+ if (property.kind === 'd')
125
+ out.f64(value);
126
+ else if (property.kind === 'l')
127
+ out.i64(value);
128
+ else
129
+ out.i32(value);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ function writeNode(out, subject) {
135
+ const endAt = out.length;
136
+ out.u32(0);
137
+ const properties = subject.properties ?? [];
138
+ const children = subject.children ?? [];
139
+ out.u32(properties.length);
140
+ const lengthAt = out.length;
141
+ out.u32(0);
142
+ const name = latin1(subject.name);
143
+ out.u8(name.length);
144
+ out.raw(name);
145
+ const propertiesFrom = out.length;
146
+ for (const property of properties)
147
+ writeProperty(out, property);
148
+ out.patch(lengthAt, out.length - propertiesFrom);
149
+ if (children.length > 0) {
150
+ for (const child of children)
151
+ writeNode(out, child);
152
+ /* The null record that closes a nested list: thirteen zero bytes at this version. */
153
+ for (let i = 0; i < 13; i++)
154
+ out.u8(0);
155
+ }
156
+ out.patch(endAt, out.length);
157
+ }
158
+ /**
159
+ * A whole file: the magic, the version, the records, and the terminator.
160
+ *
161
+ * Version is a parameter because the container's one structural break is at 7500, where record
162
+ * offsets widen to 64 bits. This writer emits the narrow form, so it writes files below that —
163
+ * which is what every exporter in the wild still writes, and what the reader's own `wide` branch
164
+ * exists to tell apart.
165
+ */
166
+ export function writeFbx(children, version = 7400) {
167
+ if (version >= 7500)
168
+ throw new Error('fbxHarness: this writer emits the pre-7500 narrow form');
169
+ const out = new Bytes();
170
+ out.raw(latin1('Kaydara FBX Binary '));
171
+ out.u8(0x00);
172
+ out.u8(0x1a);
173
+ out.u8(0x00);
174
+ out.u32(version);
175
+ for (const child of children)
176
+ writeNode(out, child);
177
+ /* A null record to end the top-level list, then the footer's slack: the reader stops sixteen
178
+ bytes from the end, which is what a real file's footer occupies. */
179
+ for (let i = 0; i < 13; i++)
180
+ out.u8(0);
181
+ for (let i = 0; i < 16; i++)
182
+ out.u8(0);
183
+ const bytes = out.done();
184
+ return bytes.slice().buffer;
185
+ }
@@ -0,0 +1,60 @@
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
+ import type { Inflate } from './fbx.ts';
27
+ /** Turn a deflate stream into bytes, eventually. What a browser can actually offer. */
28
+ export type AsyncInflate = (compressed: Uint8Array, expectedBytes: number) => Promise<Uint8Array>;
29
+ /**
30
+ * `DecompressionStream`, which every target browser has and which needs no dependency.
31
+ *
32
+ * FBX writes zlib-wrapped deflate, so `'deflate'` rather than `'deflate-raw'`. Getting that
33
+ * wrong fails on the first two bytes rather than producing something subtly wrong, which is
34
+ * the good kind of mistake to make.
35
+ */
36
+ export declare const browserInflate: AsyncInflate;
37
+ export declare function prepareFbxInflate(buffer: ArrayBuffer, inflate: AsyncInflate): Promise<Inflate>;
38
+ /**
39
+ * The same trick for a zip, which is how a `.3mf` and a bought archive both arrive.
40
+ *
41
+ * **Promoted from a consumer's private copy.** Every piece needed to open an archive was already
42
+ * exported — `readZip`, `readerFor`, `MODEL_FORMATS`, `assetCandidates`, `ModelSource.beside` — and
43
+ * a second consumer still spent about sixty lines assembling them, most of it rediscovering this
44
+ * function. The engine's own model worker had implemented it privately for `.3mf` at the same time,
45
+ * which is two copies of one idea and the point at which it belongs here.
46
+ *
47
+ * **A zip stores raw deflate and FBX stores zlib-wrapped**, so this takes its own decompressor and
48
+ * is deliberately a separate name from `prepareFbxInflate`. Passing either where the other belongs
49
+ * fails on the first bytes, which is the good kind of mistake.
50
+ *
51
+ * Two passes for the same reason: `readZip` expands each entry as it walks and the only
52
+ * decompressor a browser has is asynchronous, so entries are recorded first and answered second.
53
+ * Spans are keyed by `byteOffset`, which identifies them exactly, since each payload is a subarray
54
+ * of the one buffer being read.
55
+ */
56
+ export declare function prepareZipInflate(buffer: ArrayBuffer, inflate: AsyncInflate): Promise<Inflate>;
57
+ /**
58
+ * Raw deflate through the browser, which is what a zip needs and what FBX must not be given.
59
+ */
60
+ export declare const browserInflateRaw: AsyncInflate;
@@ -0,0 +1,204 @@
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
+ import { parseFbxTree } from './fbx.js';
27
+ import { readZip } from './zip.js';
28
+ import { DrftError } from '@driftengine/drft';
29
+ /**
30
+ * Read a decompression stream to the end, and decide what a failure at the end means.
31
+ *
32
+ * **Written because `new Response(stream).arrayBuffer()` throws away the reason.** Measured in
33
+ * Chrome 141 over the four ways a deflate stream can fail — bytes after the end, a truncated
34
+ * payload, a flipped byte, and something that is not deflate at all — and the Response reports
35
+ * exactly `Failed to fetch` for every one of them. That is the message a consumer saw for a
36
+ * model that would not load, and it names the network for a fault that has nothing to do with
37
+ * it. A reader keeps what the platform actually said: *Junk found after end of compressed
38
+ * data*, *Compressed input was truncated*, *incorrect header check*.
39
+ *
40
+ * **And bytes after the end of the stream are not a failure**, which is the other half and the
41
+ * one that changes what loads. Measured on both sides: Node's `zlib.inflateSync` returns all
42
+ * 4,096 bytes of a stream with sixteen zeros appended, and `DecompressionStream` errors on the
43
+ * same input *after* delivering all 4,096. So an exporter that writes a padded length produces a
44
+ * file the baker reads and a browser refuses, which is a difference between two hosts reading one
45
+ * format rather than anything about the file. The bytes are complete either way, so they are
46
+ * kept, and the error is only raised when the array actually came up short.
47
+ *
48
+ * What would make this wrong: a stream that errors early *and* whose header happens to promise
49
+ * fewer bytes than arrived. `expectedBytes` comes from the record around the payload rather than
50
+ * from the payload itself, so that would need the file to be wrong about its own array, which is
51
+ * a different failure and one the reader above this catches by shape.
52
+ */
53
+ async function readToEnd(stream, expectedBytes) {
54
+ const reader = stream.getReader();
55
+ const chunks = [];
56
+ let got = 0;
57
+ try {
58
+ for (;;) {
59
+ const { done, value } = await reader.read();
60
+ if (done)
61
+ break;
62
+ chunks.push(value);
63
+ got += value.length;
64
+ }
65
+ }
66
+ catch (error) {
67
+ /* Short, so the array is genuinely missing bytes and the reason belongs to the caller. */
68
+ if (got === 0 || got < expectedBytes) {
69
+ const said = error instanceof Error ? error.message : String(error);
70
+ throw new Error(said === '' ? 'the compressed data could not be read' : said);
71
+ }
72
+ }
73
+ const out = new Uint8Array(got);
74
+ let at = 0;
75
+ for (const chunk of chunks) {
76
+ out.set(chunk, at);
77
+ at += chunk.length;
78
+ }
79
+ return out;
80
+ }
81
+ /**
82
+ * `DecompressionStream`, which every target browser has and which needs no dependency.
83
+ *
84
+ * FBX writes zlib-wrapped deflate, so `'deflate'` rather than `'deflate-raw'`. Getting that
85
+ * wrong fails on the first two bytes rather than producing something subtly wrong, which is
86
+ * the good kind of mistake to make.
87
+ */
88
+ export const browserInflate = async (compressed, expectedBytes) => {
89
+ const stream = new Blob([compressed.slice()])
90
+ .stream()
91
+ .pipeThrough(new DecompressionStream('deflate'));
92
+ return readToEnd(stream, expectedBytes);
93
+ };
94
+ /**
95
+ * Expand every recorded span, and give a failure a second chance on its own.
96
+ *
97
+ * **The retry is for a fan-out and not for the data.** Both callers hand `Promise.all` every
98
+ * span a file has at once, which for one bought model measured 1,586 concurrent Blobs, streams
99
+ * and stream readers, on top of the 32 the archive around it had already run. A file this shape
100
+ * was reported as failing at one span while the same span, decompressed on its own in the same
101
+ * browser, was measured as completely sound: the compressed length is 1,064 bytes, the array is
102
+ * 1,680, and it expands to exactly that. So the one thing left standing is the company it was
103
+ * decompressed in, which is a resource failure rather than a file.
104
+ *
105
+ * The cost is one extra attempt, on the failure path only, after the fan-out has drained. What
106
+ * would make it wrong is a file that fails deterministically: then the second attempt fails
107
+ * identically, the throw carries the platform's own reason, and the only thing lost is the time
108
+ * of a single span. That is the trade, and it is why the retry is not a loop.
109
+ */
110
+ async function expandSpans(spans, inflate, format) {
111
+ const decoded = new Map();
112
+ const failed = [];
113
+ await Promise.all(spans.map(async (span) => {
114
+ try {
115
+ decoded.set(span.at, await inflate(span.compressed, span.expected));
116
+ }
117
+ catch {
118
+ failed.push(span);
119
+ }
120
+ }));
121
+ for (const span of failed) {
122
+ try {
123
+ decoded.set(span.at, await inflate(span.compressed, span.expected));
124
+ }
125
+ catch (error) {
126
+ throw new DrftError(`${format}: an array at byte ${span.at} would not decompress: ` +
127
+ `${error instanceof Error ? error.message : String(error)}`);
128
+ }
129
+ }
130
+ return decoded;
131
+ }
132
+ export async function prepareFbxInflate(buffer, inflate) {
133
+ const spans = [];
134
+ /*
135
+ * Pass one records rather than expands. The stand-in has to be the *expected* length,
136
+ * because the caller immediately builds a typed array over it and a short one would throw
137
+ * here rather than at the point the shape is actually wrong.
138
+ */
139
+ parseFbxTree(buffer, (compressed, expected) => {
140
+ spans.push({ at: compressed.byteOffset, compressed, expected });
141
+ return new Uint8Array(expected);
142
+ });
143
+ const decoded = await expandSpans(spans, inflate, 'fbx');
144
+ return (compressed, expectedBytes) => {
145
+ const found = decoded.get(compressed.byteOffset);
146
+ if (found === undefined) {
147
+ /*
148
+ * Only reachable if the two passes disagreed about the file, which would mean the
149
+ * parse is not deterministic. Loud, because silently returning empty bytes would draw
150
+ * a model with a hole in it and send somebody looking at the geometry reader.
151
+ */
152
+ throw new DrftError(`fbx: no decompressed array was prepared for byte ${compressed.byteOffset}`);
153
+ }
154
+ if (found.length < expectedBytes) {
155
+ throw new DrftError(`fbx: an array at byte ${compressed.byteOffset} decompressed to ${found.length} bytes, ` +
156
+ `and its own header says ${expectedBytes}`);
157
+ }
158
+ return found;
159
+ };
160
+ }
161
+ /**
162
+ * The same trick for a zip, which is how a `.3mf` and a bought archive both arrive.
163
+ *
164
+ * **Promoted from a consumer's private copy.** Every piece needed to open an archive was already
165
+ * exported — `readZip`, `readerFor`, `MODEL_FORMATS`, `assetCandidates`, `ModelSource.beside` — and
166
+ * a second consumer still spent about sixty lines assembling them, most of it rediscovering this
167
+ * function. The engine's own model worker had implemented it privately for `.3mf` at the same time,
168
+ * which is two copies of one idea and the point at which it belongs here.
169
+ *
170
+ * **A zip stores raw deflate and FBX stores zlib-wrapped**, so this takes its own decompressor and
171
+ * is deliberately a separate name from `prepareFbxInflate`. Passing either where the other belongs
172
+ * fails on the first bytes, which is the good kind of mistake.
173
+ *
174
+ * Two passes for the same reason: `readZip` expands each entry as it walks and the only
175
+ * decompressor a browser has is asynchronous, so entries are recorded first and answered second.
176
+ * Spans are keyed by `byteOffset`, which identifies them exactly, since each payload is a subarray
177
+ * of the one buffer being read.
178
+ */
179
+ export async function prepareZipInflate(buffer, inflate) {
180
+ const spans = [];
181
+ readZip(buffer, (compressed, expected) => {
182
+ spans.push({ at: compressed.byteOffset, compressed, expected });
183
+ return new Uint8Array(expected);
184
+ });
185
+ /* The same expansion the FBX path takes, which this one used to do without the retry and
186
+ without saying which entry failed: a bare rejection out of `Promise.all` named neither. */
187
+ const decoded = await expandSpans(spans, inflate, 'zip');
188
+ return (compressed) => {
189
+ const found = decoded.get(compressed.byteOffset);
190
+ if (found === undefined) {
191
+ throw new DrftError(`zip: no decompressed entry was prepared for byte ${compressed.byteOffset}`);
192
+ }
193
+ return found;
194
+ };
195
+ }
196
+ /**
197
+ * Raw deflate through the browser, which is what a zip needs and what FBX must not be given.
198
+ */
199
+ export const browserInflateRaw = async (compressed, expectedBytes) => {
200
+ const stream = new Blob([compressed.slice()])
201
+ .stream()
202
+ .pipeThrough(new DecompressionStream('deflate-raw'));
203
+ return readToEnd(stream, expectedBytes);
204
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * FBX materials and the textures they name. One responsibility: what a surface looks like.
3
+ *
4
+ * Split from `fbx.ts` because reading a record tree and interpreting a shading model are
5
+ * different jobs with different ways of being wrong. The container either parses or it does
6
+ * not; a material silently comes out the wrong colour.
7
+ *
8
+ * **This resolves nothing from disk.** A texture is reported as the string the file wrote,
9
+ * and turning that into bytes belongs to the caller, which is the only party that knows
10
+ * whether it is reading a folder, a zip or a URL. The same reasoning as the injected
11
+ * `Inflate` in `fbx.ts`, for the same reason: this module is part of an engine that runs in
12
+ * a browser and must not reach for a filesystem.
13
+ *
14
+ * **Why the declared path is the only one worth keeping.** These records carry an absolute
15
+ * `FileName` as well, and it is the author's own machine: the asset this was built against
16
+ * says `G:\SketchfabRipper_v1.18.0-b2\tools\blender-292\_sfTemp\textures\carbon.jpg`. That
17
+ * path is dead everywhere except one computer that no longer has the file either. The
18
+ * `RelativeFilename` beside it says `textures\carbon.jpg`, which is true relative to the
19
+ * model and is what every other reader uses.
20
+ */
21
+ import type { FbxNode } from './fbx.ts';
22
+ import type { AssetReference } from './assetPath.ts';
23
+ /** A surface, in the terms this engine's vertex attributes speak. */
24
+ export interface FbxMaterial {
25
+ readonly name: string;
26
+ readonly color: readonly [number, number, number];
27
+ /** Highlight strength, 0 to 1. */
28
+ readonly specular: number;
29
+ /** Highlight width, 0 to 1, where 0 is a mirror. */
30
+ readonly roughness: number;
31
+ /** 1 is opaque. FBX states the inverse, when it states it at all. */
32
+ readonly opacity: number;
33
+ /** Self-illumination, 0 to 1. */
34
+ readonly emissive: number;
35
+ readonly emissiveColor: readonly [number, number, number];
36
+ /** Index into `FbxMaterials.textures` for the albedo map, or -1 for an untextured surface. */
37
+ readonly albedo: number;
38
+ /** How much of the environment this surface mirrors, from `ReflectionFactor`. */
39
+ readonly reflectivity: number;
40
+ /**
41
+ * The three map indices `DrftMaterial` carries, all -1 here.
42
+ *
43
+ * This reader resolves no maps but a normal, ORM or emissive one; an FBX that embeds them names
44
+ * them in its own texture connections and wiring that up is its own piece of work. The fields
45
+ * exist because `fbx.ts` pushes an `FbxMaterial` straight into a `DrftMaterial[]`, so the two
46
+ * shapes have to agree.
47
+ */
48
+ readonly normalMap: number;
49
+ readonly ormMap: number;
50
+ readonly emissiveMap: number;
51
+ /** The scales that go with an `ormMap`. Identity here, since there is never one. */
52
+ readonly roughnessScale: number;
53
+ readonly metallicScale: number;
54
+ readonly occlusionStrength: number;
55
+ /** Zero: an FBX material states no alpha test, so nothing is discarded. */
56
+ readonly cutout: number;
57
+ }
58
+ export interface FbxMaterials {
59
+ readonly byId: ReadonlyMap<number, FbxMaterial>;
60
+ /**
61
+ * Model id to its materials, in the order the file connected them.
62
+ *
63
+ * That order is load-bearing: `LayerElementMaterial` stores an *index into this list*,
64
+ * not an object id, so reordering it repaints the model.
65
+ */
66
+ readonly ofModel: ReadonlyMap<number, readonly number[]>;
67
+ /** Every texture the file names, with bytes where it embedded them itself. */
68
+ readonly textures: readonly AssetReference[];
69
+ }
70
+ /**
71
+ * What a surface with no material at all is shaded as.
72
+ *
73
+ * Exported so the geometry reader has no null to branch on. A geometry can genuinely reach
74
+ * the reader wearing nothing, and a fallback stated once here is better than the same three
75
+ * defaults spelled out at each use, which is how two of them end up disagreeing.
76
+ */
77
+ export declare const FALLBACK_MATERIAL: FbxMaterial;
78
+ /**
79
+ * Materials, their textures, and which model wears which.
80
+ *
81
+ * Textures reach a material through an `OP` connection, which is FBX for "this object feeds
82
+ * that object's *named property*" — the property being `DiffuseColor`, `NormalMap`,
83
+ * `SpecularColor` and so on. Only the albedo is used, because it is the only one this
84
+ * renderer has a slot for; the rest are counted and reported rather than dropped in silence,
85
+ * so a model that looks flatter than it should says why.
86
+ */
87
+ export declare function readMaterials(root: FbxNode, warnings: string[]): FbxMaterials;