@driftengine/drft 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 (67) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/animationData.d.ts +53 -0
  5. package/dist/animationData.js +13 -0
  6. package/dist/coarseFirst.d.ts +27 -0
  7. package/dist/coarseFirst.js +118 -0
  8. package/dist/drftColliders.d.ts +53 -0
  9. package/dist/drftColliders.js +137 -0
  10. package/dist/drftFormat.d.ts +454 -0
  11. package/dist/drftFormat.js +350 -0
  12. package/dist/drftRead.d.ts +121 -0
  13. package/dist/drftRead.js +487 -0
  14. package/dist/drftSkin.d.ts +40 -0
  15. package/dist/drftSkin.js +270 -0
  16. package/dist/drftStream.d.ts +170 -0
  17. package/dist/drftStream.js +315 -0
  18. package/dist/drftSubs.d.ts +18 -0
  19. package/dist/drftSubs.js +70 -0
  20. package/dist/drftWrite.d.ts +103 -0
  21. package/dist/drftWrite.js +478 -0
  22. package/dist/fixtures/v1-0.d.ts +9 -0
  23. package/dist/fixtures/v1-0.js +9 -0
  24. package/dist/fixtures/v1-1.d.ts +16 -0
  25. package/dist/fixtures/v1-1.js +16 -0
  26. package/dist/fixtures/v1-11.d.ts +14 -0
  27. package/dist/fixtures/v1-11.js +14 -0
  28. package/dist/fixtures/v1-2.d.ts +19 -0
  29. package/dist/fixtures/v1-2.js +19 -0
  30. package/dist/fixtures/v1-3.d.ts +18 -0
  31. package/dist/fixtures/v1-3.js +18 -0
  32. package/dist/fixtures/v1-4.d.ts +13 -0
  33. package/dist/fixtures/v1-4.js +13 -0
  34. package/dist/fixtures/v1-5.d.ts +14 -0
  35. package/dist/fixtures/v1-5.js +14 -0
  36. package/dist/fixtures/v1-6.d.ts +9 -0
  37. package/dist/fixtures/v1-6.js +9 -0
  38. package/dist/fixtures/v1-7.d.ts +9 -0
  39. package/dist/fixtures/v1-7.js +9 -0
  40. package/dist/fixtures/v1-9.d.ts +9 -0
  41. package/dist/fixtures/v1-9.js +9 -0
  42. package/dist/index.d.ts +26 -0
  43. package/dist/index.js +29 -0
  44. package/dist/meshData.d.ts +210 -0
  45. package/dist/meshData.js +105 -0
  46. package/package.json +57 -0
  47. package/src/animationData.ts +58 -0
  48. package/src/coarseFirst.ts +113 -0
  49. package/src/drftColliders.ts +153 -0
  50. package/src/drftFormat.ts +537 -0
  51. package/src/drftRead.ts +652 -0
  52. package/src/drftSkin.ts +326 -0
  53. package/src/drftStream.ts +436 -0
  54. package/src/drftSubs.ts +86 -0
  55. package/src/drftWrite.ts +613 -0
  56. package/src/fixtures/v1-0.ts +10 -0
  57. package/src/fixtures/v1-1.ts +17 -0
  58. package/src/fixtures/v1-11.ts +15 -0
  59. package/src/fixtures/v1-2.ts +20 -0
  60. package/src/fixtures/v1-3.ts +19 -0
  61. package/src/fixtures/v1-4.ts +14 -0
  62. package/src/fixtures/v1-5.ts +15 -0
  63. package/src/fixtures/v1-6.ts +10 -0
  64. package/src/fixtures/v1-7.ts +10 -0
  65. package/src/fixtures/v1-9.ts +10 -0
  66. package/src/index.ts +56 -0
  67. package/src/meshData.ts +301 -0
@@ -0,0 +1,315 @@
1
+ /** Reading a `.drft` while it is still arriving, chunk by chunk. */
2
+ import { CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_SPLT, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_TEXS, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, KNOWN_CHUNKS, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_SKIN, CHUNK_MORP, } from './drftFormat.js';
3
+ import { readHead, readMaterials, readMesh, readSplatBlock, readTexture } from './drftRead.js';
4
+ import { readClip, readMorph, readNodes, readSkin } from './drftSkin.js';
5
+ /**
6
+ * An incremental `.drft` reader: feed it bytes, it reports each chunk as that chunk completes.
7
+ *
8
+ * **Its own entry point rather than a flag on `readDrft`**, which is what docs/FORMAT.md §4.6
9
+ * asks for and the reason is the strictness. `readDrft` requires a `HEAD` and at least one
10
+ * `MESH` and refuses anything partial, which is right for a file on disk and wrong for a
11
+ * stream. The rules do not relax here, they *move*: a chunk is validated when its last byte
12
+ * lands, the table's own rules apply the moment the table is readable, and the whole-asset
13
+ * rules apply at `end`. A malformed file still throws, naming the chunk.
14
+ *
15
+ * **Zero-copy survives, and that is not obvious.** The header carries `totalBytes`, so this
16
+ * allocates one buffer of the final size the moment it knows that number and writes arriving
17
+ * bytes into it. Every completed chunk is then viewed in place, exactly as a whole-file read
18
+ * views it. Accumulating chunks into their own buffers instead would cost a copy per chunk and
19
+ * give up the one property this format exists for.
20
+ *
21
+ * Sequential, because that is the property worth designing for: with the payloads laid out in
22
+ * priority order by the baker, a *plain* fetch refines the model as bytes arrive, with no range
23
+ * requests and no server support beyond serving a file. Range requests would be an
24
+ * optimisation on top, and would need an offset per push rather than a different reader.
25
+ */
26
+ export class DrftStream {
27
+ handlers;
28
+ /** The final buffer, once `totalBytes` is known. Written into at the arrival offset. */
29
+ buffer = null;
30
+ bytes = null;
31
+ /** The header and table, before the real buffer exists. At most a few arrivals. */
32
+ prelude = [];
33
+ preludeBytes = 0;
34
+ received = 0;
35
+ chunks = [];
36
+ /** How many table entries have been reported complete, in table order. */
37
+ settled = 0;
38
+ meshOrdinal = 0;
39
+ textureOrdinal = 0;
40
+ head = null;
41
+ meshCount = 0;
42
+ materialCount = 0;
43
+ textureCount = 0;
44
+ splatBlockCount = 0;
45
+ manifestSent = false;
46
+ versionMajor = 0;
47
+ versionMinor = 0;
48
+ ended = false;
49
+ constructor(handlers = {}) {
50
+ this.handlers = handlers;
51
+ }
52
+ /** Bytes arrived so far, and how many the file says there are. Zero until the header lands. */
53
+ get progress() {
54
+ return { received: this.received, total: this.buffer?.byteLength ?? 0 };
55
+ }
56
+ /**
57
+ * Hand over the next bytes of the file, in order.
58
+ *
59
+ * Reports whatever those bytes completed before returning, so a caller that draws between
60
+ * pushes sees every intermediate state.
61
+ */
62
+ push(part) {
63
+ if (this.ended)
64
+ throw new DrftError('bytes pushed after the stream ended');
65
+ if (this.bytes === null) {
66
+ /*
67
+ * Before the buffer exists there is nowhere to write, so the first arrivals are held as
68
+ * they came and joined once.
69
+ *
70
+ * **Held as slices rather than as bytes**, which is not a micro-optimisation: the first
71
+ * thing a fetch hands over is tens of kilobytes, and pushing that a byte at a time into a
72
+ * plain array is a five-figure loop before a single pixel of the model exists. It was
73
+ * measurable as a hitch at the very start of a load, which is the worst place to have one
74
+ * because nothing is on screen yet to explain it.
75
+ */
76
+ this.prelude.push(part);
77
+ this.preludeBytes += part.length;
78
+ if (this.preludeBytes < HEADER_BYTES)
79
+ return;
80
+ this.openBuffer();
81
+ if (this.bytes === null)
82
+ return;
83
+ }
84
+ else {
85
+ const target = this.bytes;
86
+ if (this.received + part.length > target.length) {
87
+ throw new DrftError(`the file said it was ${target.length} bytes and more than that has arrived`);
88
+ }
89
+ target.set(part, this.received);
90
+ this.received += part.length;
91
+ }
92
+ this.readTable();
93
+ this.settleChunks();
94
+ }
95
+ /**
96
+ * No more bytes. Applies the rules that are about the asset as a whole rather than about one
97
+ * chunk, which is where `readDrft`'s refusals live.
98
+ */
99
+ end() {
100
+ if (this.ended)
101
+ return;
102
+ this.ended = true;
103
+ if (this.buffer === null) {
104
+ throw new DrftError(`a file is at least ${HEADER_BYTES} bytes, got ${this.preludeBytes}`);
105
+ }
106
+ if (this.received < this.buffer.byteLength) {
107
+ throw new DrftError(`the file said it was ${this.buffer.byteLength} bytes and ${this.received} arrived`);
108
+ }
109
+ if (this.head === null)
110
+ throw new DrftError('no HEAD chunk — every asset must describe itself');
111
+ /* Geometry *or* a capture, since 1.5 — the same rule `readDrft` and `writeDrft` apply, kept
112
+ in step here because a stream that refused what a whole-file read accepts would fail only
113
+ for the consumer that had chosen to show a load. */
114
+ if (this.meshCount === 0 && this.splatBlockCount === 0) {
115
+ throw new DrftError('no MESH and no SPLT chunk — an asset with neither geometry nor a capture');
116
+ }
117
+ /*
118
+ * The pairing check `readDrft` makes, and for the same reason: materials pair with meshes
119
+ * by ordinal, so a count mismatch means every surface describes a different mesh. That is
120
+ * silent, and it draws as a scene whose materials have been shuffled.
121
+ */
122
+ if (this.materialCount > 0 && this.materialCount !== this.meshCount) {
123
+ throw new DrftError(`MATL carries ${this.materialCount} materials for ${this.meshCount} meshes; ` +
124
+ `they pair by ordinal`);
125
+ }
126
+ }
127
+ /** Everything the header and the table say, once they have arrived. */
128
+ openBuffer() {
129
+ const prelude = new Uint8Array(this.preludeBytes);
130
+ let at = 0;
131
+ for (const part of this.prelude) {
132
+ prelude.set(part, at);
133
+ at += part.length;
134
+ }
135
+ const view = new DataView(prelude.buffer, prelude.byteOffset, prelude.byteLength);
136
+ if (view.getUint32(0, true) !== DRFT_MAGIC) {
137
+ throw new DrftError('not a drft file — the magic does not match');
138
+ }
139
+ this.versionMajor = view.getUint16(4, true);
140
+ this.versionMinor = view.getUint16(6, true);
141
+ const minReaderMajor = view.getUint16(8, true);
142
+ if (minReaderMajor > DRFT_VERSION_MAJOR) {
143
+ throw new DrftError(`this file needs a reader of version ${minReaderMajor} or newer and this one is ` +
144
+ `${DRFT_VERSION_MAJOR}. Its own version is ${this.versionMajor}.${this.versionMinor}.`);
145
+ }
146
+ const chunkCount = view.getUint32(12, true);
147
+ const totalBytes = view.getUint32(16, true);
148
+ const tableEnd = HEADER_BYTES + chunkCount * CHUNK_ENTRY_BYTES;
149
+ if (tableEnd > totalBytes) {
150
+ throw new DrftError(`a table of ${chunkCount} chunks runs past the end of the file`);
151
+ }
152
+ this.buffer = new ArrayBuffer(totalBytes);
153
+ this.bytes = new Uint8Array(this.buffer);
154
+ this.bytes.set(prelude.subarray(0, Math.min(prelude.length, totalBytes)));
155
+ this.received = Math.min(prelude.length, totalBytes);
156
+ this.prelude = [];
157
+ this.preludeBytes = 0;
158
+ }
159
+ /** Parse the table once all of it has arrived, and announce what is coming. */
160
+ readTable() {
161
+ if (this.manifestSent || this.buffer === null)
162
+ return;
163
+ const view = new DataView(this.buffer);
164
+ const chunkCount = view.getUint32(12, true);
165
+ const tableEnd = HEADER_BYTES + chunkCount * CHUNK_ENTRY_BYTES;
166
+ if (this.received < tableEnd)
167
+ return;
168
+ let meshBytes = 0;
169
+ let textureBytes = 0;
170
+ let splatBytes = 0;
171
+ for (let i = 0; i < chunkCount; i++) {
172
+ const entry = HEADER_BYTES + i * CHUNK_ENTRY_BYTES;
173
+ const chunk = {
174
+ code: view.getUint32(entry, true),
175
+ offset: view.getUint32(entry + 4, true),
176
+ byteLength: view.getUint32(entry + 8, true),
177
+ flags: view.getUint16(entry + 12, true),
178
+ index: view.getUint16(entry + 14, true),
179
+ };
180
+ if (chunk.offset + chunk.byteLength > this.buffer.byteLength) {
181
+ throw new DrftError(`chunk ${i} (${fourCCName(chunk.code)}) spans ${chunk.offset}..` +
182
+ `${chunk.offset + chunk.byteLength} past the end of a ${this.buffer.byteLength} ` +
183
+ `byte file`);
184
+ }
185
+ if (chunk.offset % 4 !== 0) {
186
+ throw new DrftError(`chunk ${i} (${fourCCName(chunk.code)}) is not 4-byte aligned`);
187
+ }
188
+ if (!KNOWN_CHUNKS.has(chunk.code) && (chunk.flags & CHUNK_REQUIRED) !== 0) {
189
+ throw new DrftError(`this file requires chunk "${fourCCName(chunk.code)}", which this reader does not ` +
190
+ `understand. It was written by version ${this.versionMajor}.${this.versionMinor}.`);
191
+ }
192
+ /*
193
+ * A level of detail counts as geometry for the weighting, because that is what it is:
194
+ * the bar is weighted by what the file is made of, and a few hundred kilobytes of
195
+ * outline is part of the geometry a viewer is waiting for. It is deliberately *not*
196
+ * counted in `meshCount`, which is the number of parts — an outline is not a part, and
197
+ * a readout saying "1 of 188 parts" would be counting the model twice.
198
+ */
199
+ if (chunk.code === CHUNK_MESH || chunk.code === CHUNK_LODM)
200
+ meshBytes += chunk.byteLength;
201
+ if (chunk.code === CHUNK_TEXS)
202
+ textureBytes += chunk.byteLength;
203
+ /* Its own weight rather than geometry's: a capture is not made of parts, so folding it into
204
+ `meshBytes` would make a "parts" readout weigh something that is not one. */
205
+ if (chunk.code === CHUNK_SPLT)
206
+ splatBytes += chunk.byteLength;
207
+ this.chunks.push(chunk);
208
+ }
209
+ /*
210
+ * Sorted by where the payload sits rather than by table order, because completion is
211
+ * decided by arriving bytes and the two orders are allowed to differ: the table is a
212
+ * manifest and the payload order is a bake-time priority decision. Sorting here means the
213
+ * settle loop can stop at the first chunk that is not yet complete.
214
+ */
215
+ this.chunks.sort((a, b) => a.offset - b.offset);
216
+ this.manifestSent = true;
217
+ this.handlers.onManifest?.({
218
+ totalBytes: this.buffer.byteLength,
219
+ meshCount: this.chunks.filter((c) => c.code === CHUNK_MESH).length,
220
+ textureCount: this.chunks.filter((c) => c.code === CHUNK_TEXS).length,
221
+ lodCount: this.chunks.filter((c) => c.code === CHUNK_LODM).length,
222
+ splatBlockCount: this.chunks.filter((c) => c.code === CHUNK_SPLT).length,
223
+ meshBytes,
224
+ textureBytes,
225
+ splatBytes,
226
+ });
227
+ }
228
+ /** Report every chunk whose last byte has now landed, in file order. */
229
+ settleChunks() {
230
+ if (!this.manifestSent || this.buffer === null)
231
+ return;
232
+ while (this.settled < this.chunks.length) {
233
+ const chunk = this.chunks[this.settled];
234
+ if (chunk === undefined)
235
+ break;
236
+ if (chunk.offset + chunk.byteLength > this.received)
237
+ break;
238
+ this.settled++;
239
+ if (!KNOWN_CHUNKS.has(chunk.code))
240
+ continue;
241
+ if (chunk.code === CHUNK_HEAD) {
242
+ this.head = readHead(this.buffer, chunk);
243
+ this.handlers.onHead?.(this.head);
244
+ }
245
+ else if (chunk.code === CHUNK_MESH) {
246
+ const mesh = readMesh(this.buffer, chunk);
247
+ this.meshCount++;
248
+ this.handlers.onMesh?.(mesh, this.meshOrdinal++);
249
+ }
250
+ else if (chunk.code === CHUNK_SPLT) {
251
+ /* The ordinal the file gave it, so blocks append in the writer's order however they
252
+ arrived — which for a sequential fetch is the same order, and for a range-fetching
253
+ caller need not be. */
254
+ this.splatBlockCount++;
255
+ this.handlers.onSplats?.(readSplatBlock(this.buffer, chunk), chunk.index);
256
+ }
257
+ else if (chunk.code === CHUNK_MORP) {
258
+ /*
259
+ * Reported as its own event rather than folded into the mesh, because a `MORP` chunk can
260
+ * land before or after the `MESH` it names — the writer puts it after, a range-fetching
261
+ * caller need not — and a stream cannot rewrite a mesh it has already handed over.
262
+ */
263
+ this.handlers.onMorph?.(readMorph(this.buffer, chunk.offset, chunk.byteLength));
264
+ }
265
+ else if (chunk.code === CHUNK_NODE) {
266
+ this.handlers.onNodes?.(readNodes(this.buffer, chunk.offset, chunk.byteLength));
267
+ }
268
+ else if (chunk.code === CHUNK_SKIN) {
269
+ this.handlers.onSkin?.(readSkin(this.buffer, chunk.offset, chunk.byteLength), chunk.index);
270
+ }
271
+ else if (chunk.code === CHUNK_ANIM) {
272
+ this.handlers.onClip?.(readClip(this.buffer, chunk.offset, chunk.byteLength), chunk.index);
273
+ }
274
+ else if (chunk.code === CHUNK_LODM) {
275
+ /* The level the file gave it, not the order it happened to arrive in. */
276
+ this.handlers.onLod?.(readMesh(this.buffer, chunk), chunk.index);
277
+ }
278
+ else if (chunk.code === CHUNK_MATL) {
279
+ const materials = readMaterials(this.buffer, chunk);
280
+ this.materialCount = materials.length;
281
+ this.handlers.onMaterials?.(materials);
282
+ }
283
+ else if (chunk.code === CHUNK_TEXS) {
284
+ const texture = readTexture(this.buffer, chunk);
285
+ this.textureCount++;
286
+ this.handlers.onTexture?.(texture, this.textureOrdinal++);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ /**
292
+ * Read a `.drft` from a fetch response as it arrives.
293
+ *
294
+ * The whole point of the sequential design in one function: a plain `fetch` of a plain file,
295
+ * and the model improves as the bytes land. A response with no body — an error page, a
296
+ * cache miss served as a redirect — throws rather than resolving to an empty asset.
297
+ */
298
+ export async function streamDrft(response, handlers = {}) {
299
+ if (!response.ok) {
300
+ throw new DrftError(`fetching the asset returned ${response.status} ${response.statusText}`);
301
+ }
302
+ const body = response.body;
303
+ if (body === null)
304
+ throw new DrftError('the response carried no body to stream');
305
+ const stream = new DrftStream(handlers);
306
+ const reader = body.getReader();
307
+ for (;;) {
308
+ const next = await reader.read();
309
+ if (next.done)
310
+ break;
311
+ if (next.value !== undefined)
312
+ stream.push(next.value);
313
+ }
314
+ stream.end();
315
+ }
@@ -0,0 +1,18 @@
1
+ export interface DrftSubstanceEntry {
2
+ /** The material ordinal this labels, as `MATL` numbers them. */
3
+ readonly material: number;
4
+ /** The substance id, matched **exactly** — see `installChemistry`, which never guesses. */
5
+ readonly substance: string;
6
+ }
7
+ export interface DrftSubs {
8
+ readonly entries: readonly DrftSubstanceEntry[];
9
+ }
10
+ /**
11
+ * `u32` count, then per entry: `u32` material, `u32` name bytes, then the UTF-8 name.
12
+ *
13
+ * Byte length rather than character count, because a name is text and text is not one byte a
14
+ * character — `chêne` is five characters and six bytes, and a reader counting characters would
15
+ * walk off the end of one entry into the next.
16
+ */
17
+ export declare function buildSubs(subs: DrftSubs): Uint8Array;
18
+ export declare function readSubs(buffer: ArrayBuffer, offset: number, byteLength: number): DrftSubs;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `SUBS` — which substance each material is made of. Container 1.8.
3
+ *
4
+ * **`§16`'s second mechanism, and the whole point of it is that there is no code in between**: the
5
+ * baker reads `extras.substance` off a glTF material, this writes it, a consumer reads it back and
6
+ * hands the ids to `installChemistry().match`. An artist labels the oak in Blender and the log
7
+ * burns like oak in the game.
8
+ *
9
+ * **Additive, so it is free.** `FORMAT.md` rule 2: an optional chunk a reader does not know is
10
+ * skipped in silence, so a 1.7 reader opens a 1.8 file as the geometry it also holds. A game with
11
+ * no chemistry in it pays nothing for a file that carries labels.
12
+ *
13
+ * **One chunk for the whole file rather than one per material.** The chunk table's `index` is the
14
+ * ordinal *within a FourCC*, which `MORP` already found the hard way — a file where only the fourth
15
+ * material is labelled would carry a chunk at index 0, and a reader pairing by it would set the
16
+ * wrong material on fire. Carrying the ordinal per entry makes the pairing explicit and makes one
17
+ * chunk enough.
18
+ */
19
+ import { DrftError, align } from './drftFormat.js';
20
+ const encoder = new TextEncoder();
21
+ const decoder = new TextDecoder();
22
+ /**
23
+ * `u32` count, then per entry: `u32` material, `u32` name bytes, then the UTF-8 name.
24
+ *
25
+ * Byte length rather than character count, because a name is text and text is not one byte a
26
+ * character — `chêne` is five characters and six bytes, and a reader counting characters would
27
+ * walk off the end of one entry into the next.
28
+ */
29
+ export function buildSubs(subs) {
30
+ const names = subs.entries.map((entry) => encoder.encode(entry.substance));
31
+ let size = 4;
32
+ for (const name of names)
33
+ size += 8 + name.byteLength;
34
+ const bytes = new Uint8Array(align(size));
35
+ const view = new DataView(bytes.buffer);
36
+ view.setUint32(0, subs.entries.length, true);
37
+ let at = 4;
38
+ for (let i = 0; i < subs.entries.length; i++) {
39
+ const name = names[i];
40
+ view.setUint32(at, subs.entries[i].material, true);
41
+ view.setUint32(at + 4, name.byteLength, true);
42
+ bytes.set(name, at + 8);
43
+ at += 8 + name.byteLength;
44
+ }
45
+ return bytes;
46
+ }
47
+ export function readSubs(buffer, offset, byteLength) {
48
+ if (byteLength < 4)
49
+ throw new DrftError('SUBS is too short to hold its count');
50
+ const view = new DataView(buffer, offset, byteLength);
51
+ const count = view.getUint32(0, true);
52
+ const entries = [];
53
+ let at = 4;
54
+ for (let i = 0; i < count; i++) {
55
+ if (at + 8 > byteLength) {
56
+ throw new DrftError(`SUBS declares ${count} entries and ends inside entry ${i}`);
57
+ }
58
+ const material = view.getUint32(at, true);
59
+ const length = view.getUint32(at + 4, true);
60
+ if (at + 8 + length > byteLength) {
61
+ throw new DrftError(`SUBS entry ${i} names ${length} bytes and the chunk has ${byteLength - at - 8} left`);
62
+ }
63
+ entries.push({
64
+ material,
65
+ substance: decoder.decode(new Uint8Array(buffer, offset + at + 8, length)),
66
+ });
67
+ at += 8 + length;
68
+ }
69
+ return { entries };
70
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Writing a `.drft`.
3
+ *
4
+ * The writer is the smaller half and the stricter one: it validates what it is given
5
+ * before anything reaches a file, because a malformed asset caught here costs a failed
6
+ * bake, and the same asset caught later costs somebody a night on a device they cannot
7
+ * attach a debugger to. That is not hypothetical — see `validateMeshData`.
8
+ *
9
+ * Runs offline in the baker, so it may allocate freely. Nothing here is on a frame path.
10
+ */
11
+ import type { AnimationClip, DrftSkin } from './animationData.ts';
12
+ import type { DrftNode } from './drftSkin.ts';
13
+ import type { MeshData } from './meshData.ts';
14
+ import type { DrftHead, DrftMaterial, DrftSplats } from './drftFormat.ts';
15
+ import { type DrftSubstanceEntry } from './drftSubs.ts';
16
+ /** An image to embed, already compressed, with what its own header said about it. */
17
+ export interface DrftTextureSource {
18
+ /**
19
+ * What the source model called this image, normally its declared relative path.
20
+ *
21
+ * Carried so a texture can be addressed by name instead of by an ordinal. An ordinal is a
22
+ * position in whatever order a reader happened to meet its records, which is not something
23
+ * a consumer should have to know or a format should ask it to depend on.
24
+ */
25
+ readonly name: string;
26
+ readonly codec: number;
27
+ readonly width: number;
28
+ readonly height: number;
29
+ readonly bytes: Uint8Array;
30
+ }
31
+ /** What a caller hands over to be baked. */
32
+ export interface DrftSource {
33
+ readonly head?: Partial<DrftHead>;
34
+ readonly meshes: readonly MeshData[];
35
+ /**
36
+ * One material per mesh, by ordinal, or absent for an asset that names none.
37
+ *
38
+ * All or nothing: a partial list would make material *n* mean a different mesh depending
39
+ * on how many came before it, which is the kind of off-by-one that shows up as one wrong
40
+ * surface in a scene and is looked for everywhere except here.
41
+ */
42
+ readonly materials?: readonly DrftMaterial[];
43
+ readonly textures?: readonly DrftTextureSource[];
44
+ /**
45
+ * Which substance each material is made of, or absent for a file that labels none.
46
+ *
47
+ * Written as one `SUBS` chunk at 1.8. It pairs by the material ordinal carried per entry rather
48
+ * than by position, so a file labelling only its fourth material says so.
49
+ */
50
+ readonly substances?: readonly DrftSubstanceEntry[];
51
+ /**
52
+ * The convex hulls this asset collides as, or absent for a file that carries none.
53
+ *
54
+ * Each entry is xyz-packed points in the asset's own space, at most 64 of them, which is what
55
+ * `hullShape` takes. Written as one `COLL` chunk at 1.12. See `drftColliders.ts` for why the
56
+ * format carries points rather than shapes.
57
+ */
58
+ readonly colliders?: readonly Float32Array[];
59
+ /**
60
+ * Coarse whole-asset levels of detail, **coarsest first**, or absent for a file with none.
61
+ *
62
+ * Each one stands in for the entire model rather than for one part of it, which is the
63
+ * argument docs/FORMAT.md §4.6 makes: a single decimated body reads as a car, and 187 coarse
64
+ * fragments read as a mess. They are written first so a sequential fetch has an outline on
65
+ * screen within the first few kilobytes, and they carry no material entry because they carry
66
+ * their colour per vertex — `MATL` pairs with `MESH` by ordinal and nothing else.
67
+ */
68
+ readonly lods?: readonly MeshData[];
69
+ /**
70
+ * Discrete levels of detail, **finest first**, each a complete `.drft` of its own.
71
+ *
72
+ * Distinct from `lods` above in every way that matters: those are one merged outline per level
73
+ * for a progressive load, these are whole alternative models a source authored by hand, each
74
+ * with its own materials and hierarchy. See `CHUNK_LODF`.
75
+ */
76
+ readonly levels?: readonly Uint8Array[];
77
+ /**
78
+ * The asset's own hierarchy, or absent for one mesh at the origin.
79
+ *
80
+ * What `NODE` carries, and what rigid TRS animation drives. A reader that skips the chunk gets
81
+ * the meshes exactly as it always did, which is why this is additive.
82
+ */
83
+ readonly nodes?: readonly DrftNode[];
84
+ /** Skins, one `SKIN` chunk each, or absent for an asset that deforms nothing. */
85
+ readonly skins?: readonly DrftSkin[];
86
+ /** Clips, one `ANIM` chunk each, or absent for an asset that animates nothing. */
87
+ readonly clips?: readonly AnimationClip[];
88
+ /**
89
+ * A Gaussian splat capture, or absent for a file carrying none.
90
+ *
91
+ * Written as several `SPLT` blocks, interleaved across the whole capture so any prefix of the
92
+ * file is a complete sparse version of it. See `coarseFirst.ts` for the ordering and
93
+ * `CHUNK_SPLT` for why it is several chunks rather than one.
94
+ */
95
+ readonly splats?: DrftSplats;
96
+ }
97
+ /**
98
+ * Bake a `.drft` into one `ArrayBuffer`.
99
+ *
100
+ * Every payload lands 4-byte aligned, which is what lets the reader view the vertex data
101
+ * in place rather than copying it out.
102
+ */
103
+ export declare function writeDrft(source: DrftSource): ArrayBuffer;