@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/animationData.d.ts +53 -0
- package/dist/animationData.js +13 -0
- package/dist/coarseFirst.d.ts +27 -0
- package/dist/coarseFirst.js +118 -0
- package/dist/drftColliders.d.ts +53 -0
- package/dist/drftColliders.js +137 -0
- package/dist/drftFormat.d.ts +454 -0
- package/dist/drftFormat.js +350 -0
- package/dist/drftRead.d.ts +121 -0
- package/dist/drftRead.js +487 -0
- package/dist/drftSkin.d.ts +40 -0
- package/dist/drftSkin.js +270 -0
- package/dist/drftStream.d.ts +170 -0
- package/dist/drftStream.js +315 -0
- package/dist/drftSubs.d.ts +18 -0
- package/dist/drftSubs.js +70 -0
- package/dist/drftWrite.d.ts +103 -0
- package/dist/drftWrite.js +478 -0
- package/dist/fixtures/v1-0.d.ts +9 -0
- package/dist/fixtures/v1-0.js +9 -0
- package/dist/fixtures/v1-1.d.ts +16 -0
- package/dist/fixtures/v1-1.js +16 -0
- package/dist/fixtures/v1-11.d.ts +14 -0
- package/dist/fixtures/v1-11.js +14 -0
- package/dist/fixtures/v1-2.d.ts +19 -0
- package/dist/fixtures/v1-2.js +19 -0
- package/dist/fixtures/v1-3.d.ts +18 -0
- package/dist/fixtures/v1-3.js +18 -0
- package/dist/fixtures/v1-4.d.ts +13 -0
- package/dist/fixtures/v1-4.js +13 -0
- package/dist/fixtures/v1-5.d.ts +14 -0
- package/dist/fixtures/v1-5.js +14 -0
- package/dist/fixtures/v1-6.d.ts +9 -0
- package/dist/fixtures/v1-6.js +9 -0
- package/dist/fixtures/v1-7.d.ts +9 -0
- package/dist/fixtures/v1-7.js +9 -0
- package/dist/fixtures/v1-9.d.ts +9 -0
- package/dist/fixtures/v1-9.js +9 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +29 -0
- package/dist/meshData.d.ts +210 -0
- package/dist/meshData.js +105 -0
- package/package.json +57 -0
- package/src/animationData.ts +58 -0
- package/src/coarseFirst.ts +113 -0
- package/src/drftColliders.ts +153 -0
- package/src/drftFormat.ts +537 -0
- package/src/drftRead.ts +652 -0
- package/src/drftSkin.ts +326 -0
- package/src/drftStream.ts +436 -0
- package/src/drftSubs.ts +86 -0
- package/src/drftWrite.ts +613 -0
- package/src/fixtures/v1-0.ts +10 -0
- package/src/fixtures/v1-1.ts +17 -0
- package/src/fixtures/v1-11.ts +15 -0
- package/src/fixtures/v1-2.ts +20 -0
- package/src/fixtures/v1-3.ts +19 -0
- package/src/fixtures/v1-4.ts +14 -0
- package/src/fixtures/v1-5.ts +15 -0
- package/src/fixtures/v1-6.ts +10 -0
- package/src/fixtures/v1-7.ts +10 -0
- package/src/fixtures/v1-9.ts +10 -0
- package/src/index.ts +56 -0
- package/src/meshData.ts +301 -0
package/dist/drftSkin.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { align, DrftError } from './drftFormat.js';
|
|
2
|
+
/** i32 parent, 3 floats, 4 floats, 3 floats, i32 mesh. */
|
|
3
|
+
const NODE_ENTRY_BYTES = 4 + 12 + 16 + 12 + 4;
|
|
4
|
+
/** u32 joint, u32 path, u32 keyCount. */
|
|
5
|
+
const TRACK_ENTRY_BYTES = 12;
|
|
6
|
+
const PATHS = ['translation', 'rotation', 'scale'];
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const decoder = new TextDecoder();
|
|
9
|
+
/** Length-prefixed and padded to four, so the block that follows a name stays aligned. */
|
|
10
|
+
function encodeString(value) {
|
|
11
|
+
const text = encoder.encode(value);
|
|
12
|
+
const bytes = new Uint8Array(align(4 + text.length));
|
|
13
|
+
new DataView(bytes.buffer).setUint32(0, text.length, true);
|
|
14
|
+
bytes.set(text, 4);
|
|
15
|
+
return bytes;
|
|
16
|
+
}
|
|
17
|
+
/** Reads one, and answers where the next begins. */
|
|
18
|
+
function decodeString(view, at) {
|
|
19
|
+
const length = view.getUint32(at, true);
|
|
20
|
+
const start = view.byteOffset + at + 4;
|
|
21
|
+
if (start + length > view.byteOffset + view.byteLength) {
|
|
22
|
+
throw new DrftError('a name runs past the end of its chunk');
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
value: decoder.decode(new Uint8Array(view.buffer, start, length)),
|
|
26
|
+
next: at + align(4 + length),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/* --- NODE --- */
|
|
30
|
+
export function buildNodes(nodes) {
|
|
31
|
+
const names = nodes.map((node) => encodeString(node.name));
|
|
32
|
+
let nameBytes = 0;
|
|
33
|
+
for (const name of names)
|
|
34
|
+
nameBytes += name.length;
|
|
35
|
+
const bytes = new Uint8Array(align(8 + nodes.length * NODE_ENTRY_BYTES + nameBytes));
|
|
36
|
+
const view = new DataView(bytes.buffer);
|
|
37
|
+
view.setUint32(0, nodes.length, true);
|
|
38
|
+
view.setUint32(4, NODE_ENTRY_BYTES, true);
|
|
39
|
+
let at = 8;
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
view.setInt32(at, node.parent, true);
|
|
42
|
+
for (let c = 0; c < 3; c++)
|
|
43
|
+
view.setFloat32(at + 4 + c * 4, node.translation[c], true);
|
|
44
|
+
for (let c = 0; c < 4; c++)
|
|
45
|
+
view.setFloat32(at + 16 + c * 4, node.rotation[c], true);
|
|
46
|
+
for (let c = 0; c < 3; c++)
|
|
47
|
+
view.setFloat32(at + 32 + c * 4, node.scale[c], true);
|
|
48
|
+
view.setInt32(at + 44, node.mesh, true);
|
|
49
|
+
at += NODE_ENTRY_BYTES;
|
|
50
|
+
}
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
bytes.set(name, at);
|
|
53
|
+
at += name.length;
|
|
54
|
+
}
|
|
55
|
+
return bytes;
|
|
56
|
+
}
|
|
57
|
+
export function readNodes(buffer, offset, byteLength) {
|
|
58
|
+
if (byteLength < 8)
|
|
59
|
+
throw new DrftError('NODE is too short to hold its header');
|
|
60
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
61
|
+
const count = view.getUint32(0, true);
|
|
62
|
+
/*
|
|
63
|
+
* The stride is read rather than assumed, exactly as `MATL` reads its own: a later minor version
|
|
64
|
+
* may widen an entry, and a reader that assumed the old width would walk into the middle of the
|
|
65
|
+
* next one and produce a hierarchy of noise rather than a refusal.
|
|
66
|
+
*/
|
|
67
|
+
const stride = view.getUint32(4, true);
|
|
68
|
+
if (stride < NODE_ENTRY_BYTES) {
|
|
69
|
+
throw new DrftError(`NODE entry stride ${stride} is shorter than this reader's ${NODE_ENTRY_BYTES}`);
|
|
70
|
+
}
|
|
71
|
+
if (8 + count * stride > byteLength)
|
|
72
|
+
throw new DrftError('NODE names more nodes than it holds');
|
|
73
|
+
const nodes = [];
|
|
74
|
+
let namesAt = 8 + count * stride;
|
|
75
|
+
for (let i = 0; i < count; i++) {
|
|
76
|
+
const at = 8 + i * stride;
|
|
77
|
+
const name = decodeString(view, namesAt);
|
|
78
|
+
namesAt = name.next;
|
|
79
|
+
nodes.push({
|
|
80
|
+
parent: view.getInt32(at, true),
|
|
81
|
+
translation: [
|
|
82
|
+
view.getFloat32(at + 4, true),
|
|
83
|
+
view.getFloat32(at + 8, true),
|
|
84
|
+
view.getFloat32(at + 12, true),
|
|
85
|
+
],
|
|
86
|
+
rotation: [
|
|
87
|
+
view.getFloat32(at + 16, true),
|
|
88
|
+
view.getFloat32(at + 20, true),
|
|
89
|
+
view.getFloat32(at + 24, true),
|
|
90
|
+
view.getFloat32(at + 28, true),
|
|
91
|
+
],
|
|
92
|
+
scale: [
|
|
93
|
+
view.getFloat32(at + 32, true),
|
|
94
|
+
view.getFloat32(at + 36, true),
|
|
95
|
+
view.getFloat32(at + 40, true),
|
|
96
|
+
],
|
|
97
|
+
mesh: view.getInt32(at + 44, true),
|
|
98
|
+
name: name.value,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return nodes;
|
|
102
|
+
}
|
|
103
|
+
/* --- SKIN --- */
|
|
104
|
+
export function buildSkin(skin) {
|
|
105
|
+
const count = skin.joints.length;
|
|
106
|
+
if (skin.inverseBind.length !== count * 16) {
|
|
107
|
+
throw new DrftError(`SKIN: ${count} joints but ${skin.inverseBind.length} inverse-bind floats; expected ${count * 16}`);
|
|
108
|
+
}
|
|
109
|
+
const names = skin.joints.map((joint) => encodeString(joint.name));
|
|
110
|
+
let nameBytes = 0;
|
|
111
|
+
for (const name of names)
|
|
112
|
+
nameBytes += name.length;
|
|
113
|
+
const parentsAt = 8;
|
|
114
|
+
const matricesAt = parentsAt + count * 4;
|
|
115
|
+
const namesStart = matricesAt + count * 64;
|
|
116
|
+
const bytes = new Uint8Array(align(namesStart + nameBytes));
|
|
117
|
+
const view = new DataView(bytes.buffer);
|
|
118
|
+
view.setUint32(0, count, true);
|
|
119
|
+
/* Reserved, and it must be ignored on read — rule 6 of §4.4, so it can become a field later. */
|
|
120
|
+
view.setUint32(4, 0, true);
|
|
121
|
+
skin.joints.forEach((joint, i) => view.setInt32(parentsAt + i * 4, joint.parent, true));
|
|
122
|
+
for (let f = 0; f < count * 16; f++) {
|
|
123
|
+
view.setFloat32(matricesAt + f * 4, skin.inverseBind[f], true);
|
|
124
|
+
}
|
|
125
|
+
let at = namesStart;
|
|
126
|
+
for (const name of names) {
|
|
127
|
+
bytes.set(name, at);
|
|
128
|
+
at += name.length;
|
|
129
|
+
}
|
|
130
|
+
return bytes;
|
|
131
|
+
}
|
|
132
|
+
export function readSkin(buffer, offset, byteLength) {
|
|
133
|
+
if (byteLength < 8)
|
|
134
|
+
throw new DrftError('SKIN is too short to hold its header');
|
|
135
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
136
|
+
const count = view.getUint32(0, true);
|
|
137
|
+
const parentsAt = 8;
|
|
138
|
+
const matricesAt = parentsAt + count * 4;
|
|
139
|
+
const namesStart = matricesAt + count * 64;
|
|
140
|
+
if (namesStart > byteLength)
|
|
141
|
+
throw new DrftError('SKIN names more joints than it holds');
|
|
142
|
+
const inverseBind = new Float32Array(count * 16);
|
|
143
|
+
for (let f = 0; f < count * 16; f++)
|
|
144
|
+
inverseBind[f] = view.getFloat32(matricesAt + f * 4, true);
|
|
145
|
+
const joints = [];
|
|
146
|
+
let namesAt = namesStart;
|
|
147
|
+
for (let i = 0; i < count; i++) {
|
|
148
|
+
const name = decodeString(view, namesAt);
|
|
149
|
+
namesAt = name.next;
|
|
150
|
+
const parent = view.getInt32(parentsAt + i * 4, true);
|
|
151
|
+
/*
|
|
152
|
+
* Checked on the way in rather than trusted. A palette is resolved in index order, so a joint
|
|
153
|
+
* whose parent is not already resolved produces a rig wrong in one limb — which reads as a bad
|
|
154
|
+
* animation rather than as a bad file, and is exactly the failure a reader can turn loud.
|
|
155
|
+
*/
|
|
156
|
+
if (parent >= i) {
|
|
157
|
+
throw new DrftError(`SKIN: joint ${i} ("${name.value}") names parent ${parent}, which is not before it. ` +
|
|
158
|
+
`Joints are written parents-first.`);
|
|
159
|
+
}
|
|
160
|
+
joints.push({ parent, name: name.value });
|
|
161
|
+
}
|
|
162
|
+
return { joints, inverseBind };
|
|
163
|
+
}
|
|
164
|
+
/* --- ANIM --- */
|
|
165
|
+
export function buildClip(clip) {
|
|
166
|
+
const name = encodeString(clip.name);
|
|
167
|
+
const tracks = clip.tracks;
|
|
168
|
+
let floats = 0;
|
|
169
|
+
for (const track of tracks)
|
|
170
|
+
floats += track.times.length + track.values.length;
|
|
171
|
+
const headerAt = 16 + name.length;
|
|
172
|
+
const dataAt = headerAt + tracks.length * TRACK_ENTRY_BYTES;
|
|
173
|
+
const bytes = new Uint8Array(align(dataAt + floats * 4));
|
|
174
|
+
const view = new DataView(bytes.buffer);
|
|
175
|
+
view.setUint32(0, tracks.length, true);
|
|
176
|
+
view.setFloat32(4, clip.durationSec, true);
|
|
177
|
+
view.setUint32(8, name.length, true);
|
|
178
|
+
view.setUint32(12, 0, true);
|
|
179
|
+
bytes.set(name, 16);
|
|
180
|
+
let at = headerAt;
|
|
181
|
+
let data = dataAt;
|
|
182
|
+
for (const track of tracks) {
|
|
183
|
+
const path = PATHS.indexOf(track.path);
|
|
184
|
+
if (path < 0)
|
|
185
|
+
throw new DrftError(`ANIM: unknown track path "${track.path}"`);
|
|
186
|
+
view.setUint32(at, track.joint, true);
|
|
187
|
+
view.setUint32(at + 4, path, true);
|
|
188
|
+
view.setUint32(at + 8, track.times.length, true);
|
|
189
|
+
at += TRACK_ENTRY_BYTES;
|
|
190
|
+
for (const value of track.times) {
|
|
191
|
+
view.setFloat32(data, value, true);
|
|
192
|
+
data += 4;
|
|
193
|
+
}
|
|
194
|
+
for (const value of track.values) {
|
|
195
|
+
view.setFloat32(data, value, true);
|
|
196
|
+
data += 4;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return bytes;
|
|
200
|
+
}
|
|
201
|
+
export function readClip(buffer, offset, byteLength) {
|
|
202
|
+
if (byteLength < 16)
|
|
203
|
+
throw new DrftError('ANIM is too short to hold its header');
|
|
204
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
205
|
+
const count = view.getUint32(0, true);
|
|
206
|
+
const durationSec = view.getFloat32(4, true);
|
|
207
|
+
const nameBytes = view.getUint32(8, true);
|
|
208
|
+
const name = decodeString(view, 16).value;
|
|
209
|
+
const headerAt = 16 + nameBytes;
|
|
210
|
+
let at = headerAt;
|
|
211
|
+
let data = headerAt + count * TRACK_ENTRY_BYTES;
|
|
212
|
+
const tracks = [];
|
|
213
|
+
for (let i = 0; i < count; i++) {
|
|
214
|
+
const joint = view.getUint32(at, true);
|
|
215
|
+
const pathIndex = view.getUint32(at + 4, true);
|
|
216
|
+
const keys = view.getUint32(at + 8, true);
|
|
217
|
+
at += TRACK_ENTRY_BYTES;
|
|
218
|
+
const path = PATHS[pathIndex];
|
|
219
|
+
if (path === undefined)
|
|
220
|
+
throw new DrftError(`ANIM: track ${i} names path ${pathIndex}`);
|
|
221
|
+
const components = path === 'rotation' ? 4 : 3;
|
|
222
|
+
if (data + keys * 4 + keys * components * 4 > byteLength) {
|
|
223
|
+
throw new DrftError(`ANIM: track ${i} reads past the end of its chunk`);
|
|
224
|
+
}
|
|
225
|
+
const times = new Float32Array(keys);
|
|
226
|
+
for (let k = 0; k < keys; k++) {
|
|
227
|
+
times[k] = view.getFloat32(data, true);
|
|
228
|
+
data += 4;
|
|
229
|
+
}
|
|
230
|
+
const values = new Float32Array(keys * components);
|
|
231
|
+
for (let v = 0; v < keys * components; v++) {
|
|
232
|
+
values[v] = view.getFloat32(data, true);
|
|
233
|
+
data += 4;
|
|
234
|
+
}
|
|
235
|
+
tracks.push({ joint, path, times, values });
|
|
236
|
+
}
|
|
237
|
+
return { name, durationSec, tracks };
|
|
238
|
+
}
|
|
239
|
+
export function buildMorph(morph) {
|
|
240
|
+
const bytes = new Uint8Array(align(8 + morph.deltas.length * 4));
|
|
241
|
+
const view = new DataView(bytes.buffer);
|
|
242
|
+
view.setUint32(0, morph.targetCount, true);
|
|
243
|
+
/*
|
|
244
|
+
* The mesh this deforms, **in the payload rather than in the chunk table's `index`**.
|
|
245
|
+
*
|
|
246
|
+
* That field is the ordinal *within a FourCC* — the writer derives it and a caller cannot set
|
|
247
|
+
* it — so for a file where only the second and fifth meshes have targets, the two `MORP` chunks
|
|
248
|
+
* would carry 0 and 1 and a reader pairing by it would deform the wrong geometry. The table's
|
|
249
|
+
* own convention is right and this is the field that has to move.
|
|
250
|
+
*/
|
|
251
|
+
view.setUint32(4, morph.mesh, true);
|
|
252
|
+
for (let f = 0; f < morph.deltas.length; f++) {
|
|
253
|
+
view.setFloat32(8 + f * 4, morph.deltas[f], true);
|
|
254
|
+
}
|
|
255
|
+
return bytes;
|
|
256
|
+
}
|
|
257
|
+
export function readMorph(buffer, offset, byteLength) {
|
|
258
|
+
if (byteLength < 8)
|
|
259
|
+
throw new DrftError('MORP is too short to hold its header');
|
|
260
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
261
|
+
const targetCount = view.getUint32(0, true);
|
|
262
|
+
const mesh = view.getUint32(4, true);
|
|
263
|
+
if (targetCount < 1)
|
|
264
|
+
throw new DrftError(`MORP for mesh ${mesh} declares ${targetCount} targets`);
|
|
265
|
+
const floats = Math.floor((byteLength - 8) / 4);
|
|
266
|
+
const deltas = new Float32Array(floats);
|
|
267
|
+
for (let f = 0; f < floats; f++)
|
|
268
|
+
deltas[f] = view.getFloat32(8 + f * 4, true);
|
|
269
|
+
return { mesh, targetCount, deltas };
|
|
270
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Reading a `.drft` while it is still arriving, chunk by chunk. */
|
|
2
|
+
import type { MeshData } from './meshData.ts';
|
|
3
|
+
import type { DrftHead, DrftMaterial, DrftSplatBlock } from './drftFormat.ts';
|
|
4
|
+
import type { DrftTexture } from './drftRead.ts';
|
|
5
|
+
import type { AnimationClip, DrftSkin } from './animationData.ts';
|
|
6
|
+
import type { DrftNode } from './drftSkin.ts';
|
|
7
|
+
import type { DrftMorph } from './drftSkin.ts';
|
|
8
|
+
/**
|
|
9
|
+
* What the file says is coming, known from the first few kilobytes.
|
|
10
|
+
*
|
|
11
|
+
* **The chunk table is a manifest and always was**: a fixed 32-byte header, then
|
|
12
|
+
* `chunkCount x 16` bytes naming every chunk's kind, offset and length. For a 187-mesh car
|
|
13
|
+
* that is about 3 KB, so a reader holding the first three kilobytes of a 74 MB file knows
|
|
14
|
+
* exactly what the rest of it contains and how big each piece is. That is what lets a loading
|
|
15
|
+
* bar say "142 of 187 parts" rather than counting bytes and hoping.
|
|
16
|
+
*/
|
|
17
|
+
export interface DrftManifest {
|
|
18
|
+
readonly totalBytes: number;
|
|
19
|
+
readonly meshCount: number;
|
|
20
|
+
readonly textureCount: number;
|
|
21
|
+
/** Bytes of geometry, of images, and of everything else, from the table alone. */
|
|
22
|
+
readonly meshBytes: number;
|
|
23
|
+
readonly textureBytes: number;
|
|
24
|
+
/**
|
|
25
|
+
* How many coarse levels of detail this file carries, which is normally one or none.
|
|
26
|
+
*
|
|
27
|
+
* Worth reporting rather than leaving to be discovered, because it is the difference between
|
|
28
|
+
* a load that opens on an outline and one that opens on an empty room, and a caller composing
|
|
29
|
+
* words for a progress readout wants to know that before the first stage rather than after it.
|
|
30
|
+
*/
|
|
31
|
+
readonly lodCount: number;
|
|
32
|
+
/**
|
|
33
|
+
* How many `SPLT` blocks this file carries, and how many bytes they are between them.
|
|
34
|
+
*
|
|
35
|
+
* **Blocks, not splats**, because the number a progress readout wants is how many refinements
|
|
36
|
+
* are still coming — the splat count is in every block's own header and a caller has it from
|
|
37
|
+
* the first one. Zero for a file with no capture, which is most of them.
|
|
38
|
+
*/
|
|
39
|
+
readonly splatBlockCount: number;
|
|
40
|
+
readonly splatBytes: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Called as each kind of thing finishes arriving. Every one is optional.
|
|
44
|
+
*
|
|
45
|
+
* A consumer that wants the whole asset at once should use `readDrft`; these exist so a scene
|
|
46
|
+
* can put a coarse thing on screen and improve it, which is the point of streaming at all.
|
|
47
|
+
*/
|
|
48
|
+
export interface DrftStreamHandlers {
|
|
49
|
+
/** The table has arrived, so what is coming is now known. Fires once, before anything else. */
|
|
50
|
+
readonly onManifest?: (manifest: DrftManifest) => void;
|
|
51
|
+
readonly onHead?: (head: DrftHead) => void;
|
|
52
|
+
/**
|
|
53
|
+
* A coarse whole-model level, with its level ordinal: 0 is the coarsest.
|
|
54
|
+
*
|
|
55
|
+
* Fires before any part on a file the baker laid out, which is the whole point of it — a
|
|
56
|
+
* complete object on screen while the parts are still arriving. A caller that draws it must
|
|
57
|
+
* stop drawing it in the same frame it starts drawing the model, or the two are both there.
|
|
58
|
+
*/
|
|
59
|
+
readonly onLod?: (mesh: MeshData, level: number) => void;
|
|
60
|
+
/** One mesh, in the order the file lays them out, with its ordinal in that order. */
|
|
61
|
+
readonly onMesh?: (mesh: MeshData, ordinal: number) => void;
|
|
62
|
+
/**
|
|
63
|
+
* One block of a Gaussian splat capture, with its ordinal.
|
|
64
|
+
*
|
|
65
|
+
* **Every block is a sparse version of the whole capture, so the first one is already worth
|
|
66
|
+
* drawing** — that is what the writer's coarse-first ordering buys and it is the reason splats
|
|
67
|
+
* are in this container rather than in a file of their own. A consumer allocates from
|
|
68
|
+
* `block.totalCount` on the first call and appends after that; the drawn count rises and
|
|
69
|
+
* nothing is re-allocated or re-sorted.
|
|
70
|
+
*/
|
|
71
|
+
readonly onSplats?: (block: DrftSplatBlock, ordinal: number) => void;
|
|
72
|
+
/**
|
|
73
|
+
* The asset's hierarchy, once, when its `NODE` chunk lands.
|
|
74
|
+
*
|
|
75
|
+
* Handed over whole rather than a node at a time: a hierarchy is one chunk and a partial one is
|
|
76
|
+
* not useful — a child whose parent has not arrived cannot be placed.
|
|
77
|
+
*/
|
|
78
|
+
readonly onNodes?: (nodes: readonly DrftNode[]) => void;
|
|
79
|
+
/** One mesh's morph deltas, naming the mesh ordinal they belong to. */
|
|
80
|
+
readonly onMorph?: (morph: DrftMorph) => void;
|
|
81
|
+
/** One skin, as its `SKIN` chunk lands. Several arrive for an asset with several. */
|
|
82
|
+
readonly onSkin?: (skin: DrftSkin, ordinal: number) => void;
|
|
83
|
+
/**
|
|
84
|
+
* One clip, as its `ANIM` chunk lands.
|
|
85
|
+
*
|
|
86
|
+
* Per chunk rather than collected, for the reason `onSplats` is: a consumer that can start a
|
|
87
|
+
* character on the first clip should not wait for the last. The writer puts all three ahead of
|
|
88
|
+
* the geometry so this is usually possible.
|
|
89
|
+
*/
|
|
90
|
+
readonly onClip?: (clip: AnimationClip, ordinal: number) => void;
|
|
91
|
+
readonly onMaterials?: (materials: readonly DrftMaterial[]) => void;
|
|
92
|
+
readonly onTexture?: (texture: DrftTexture, ordinal: number) => void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* An incremental `.drft` reader: feed it bytes, it reports each chunk as that chunk completes.
|
|
96
|
+
*
|
|
97
|
+
* **Its own entry point rather than a flag on `readDrft`**, which is what docs/FORMAT.md §4.6
|
|
98
|
+
* asks for and the reason is the strictness. `readDrft` requires a `HEAD` and at least one
|
|
99
|
+
* `MESH` and refuses anything partial, which is right for a file on disk and wrong for a
|
|
100
|
+
* stream. The rules do not relax here, they *move*: a chunk is validated when its last byte
|
|
101
|
+
* lands, the table's own rules apply the moment the table is readable, and the whole-asset
|
|
102
|
+
* rules apply at `end`. A malformed file still throws, naming the chunk.
|
|
103
|
+
*
|
|
104
|
+
* **Zero-copy survives, and that is not obvious.** The header carries `totalBytes`, so this
|
|
105
|
+
* allocates one buffer of the final size the moment it knows that number and writes arriving
|
|
106
|
+
* bytes into it. Every completed chunk is then viewed in place, exactly as a whole-file read
|
|
107
|
+
* views it. Accumulating chunks into their own buffers instead would cost a copy per chunk and
|
|
108
|
+
* give up the one property this format exists for.
|
|
109
|
+
*
|
|
110
|
+
* Sequential, because that is the property worth designing for: with the payloads laid out in
|
|
111
|
+
* priority order by the baker, a *plain* fetch refines the model as bytes arrive, with no range
|
|
112
|
+
* requests and no server support beyond serving a file. Range requests would be an
|
|
113
|
+
* optimisation on top, and would need an offset per push rather than a different reader.
|
|
114
|
+
*/
|
|
115
|
+
export declare class DrftStream {
|
|
116
|
+
private readonly handlers;
|
|
117
|
+
/** The final buffer, once `totalBytes` is known. Written into at the arrival offset. */
|
|
118
|
+
private buffer;
|
|
119
|
+
private bytes;
|
|
120
|
+
/** The header and table, before the real buffer exists. At most a few arrivals. */
|
|
121
|
+
private prelude;
|
|
122
|
+
private preludeBytes;
|
|
123
|
+
private received;
|
|
124
|
+
private chunks;
|
|
125
|
+
/** How many table entries have been reported complete, in table order. */
|
|
126
|
+
private settled;
|
|
127
|
+
private meshOrdinal;
|
|
128
|
+
private textureOrdinal;
|
|
129
|
+
private head;
|
|
130
|
+
private meshCount;
|
|
131
|
+
private materialCount;
|
|
132
|
+
private textureCount;
|
|
133
|
+
private splatBlockCount;
|
|
134
|
+
private manifestSent;
|
|
135
|
+
private versionMajor;
|
|
136
|
+
private versionMinor;
|
|
137
|
+
private ended;
|
|
138
|
+
constructor(handlers?: DrftStreamHandlers);
|
|
139
|
+
/** Bytes arrived so far, and how many the file says there are. Zero until the header lands. */
|
|
140
|
+
get progress(): {
|
|
141
|
+
readonly received: number;
|
|
142
|
+
readonly total: number;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Hand over the next bytes of the file, in order.
|
|
146
|
+
*
|
|
147
|
+
* Reports whatever those bytes completed before returning, so a caller that draws between
|
|
148
|
+
* pushes sees every intermediate state.
|
|
149
|
+
*/
|
|
150
|
+
push(part: Uint8Array): void;
|
|
151
|
+
/**
|
|
152
|
+
* No more bytes. Applies the rules that are about the asset as a whole rather than about one
|
|
153
|
+
* chunk, which is where `readDrft`'s refusals live.
|
|
154
|
+
*/
|
|
155
|
+
end(): void;
|
|
156
|
+
/** Everything the header and the table say, once they have arrived. */
|
|
157
|
+
private openBuffer;
|
|
158
|
+
/** Parse the table once all of it has arrived, and announce what is coming. */
|
|
159
|
+
private readTable;
|
|
160
|
+
/** Report every chunk whose last byte has now landed, in file order. */
|
|
161
|
+
private settleChunks;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read a `.drft` from a fetch response as it arrives.
|
|
165
|
+
*
|
|
166
|
+
* The whole point of the sequential design in one function: a plain `fetch` of a plain file,
|
|
167
|
+
* and the model improves as the bytes land. A response with no body — an error page, a
|
|
168
|
+
* cache miss served as a redirect — throws rather than resolving to an empty asset.
|
|
169
|
+
*/
|
|
170
|
+
export declare function streamDrft(response: Response, handlers?: DrftStreamHandlers): Promise<void>;
|