@driftengine/drft 3.63.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dtex.ts ADDED
@@ -0,0 +1,404 @@
1
+ import { DrftError, align } from './drftFormat.ts';
2
+
3
+ /**
4
+ * `DTEX`: a material as a decode program over a latent, rather than as pictures.
5
+ *
6
+ * **The chunk carries no semantics and that is deliberate.** `@driftengine/drft` is the container
7
+ * and has no dependencies — it does not know what a `DecodeGraph` means, only what shape it is. So
8
+ * the types here are plain arrays that `@driftengine/texture`'s own types satisfy structurally,
9
+ * the same arrangement `Terrain` has with `Heightfield` across the physics boundary: a rule that
10
+ * cannot be shared as code is still shared as a test.
11
+ *
12
+ * ```
13
+ * u32 material which MATL entry this decodes
14
+ * u32 latentWidth
15
+ * u32 latentHeight
16
+ * u32 latentComponents
17
+ * u32 channelCount
18
+ * u32 nodeCount decode graph nodes, four words each
19
+ * u32 resultRegister
20
+ * u32 addressMode 0 and 1 lattice, 2 and 3 texel-centre; clamp, then wrap
21
+ * u32 networkInputs
22
+ * u32 networkOutputs
23
+ * u32 hiddenCount
24
+ * u32 weightCount
25
+ * u32 tileCount
26
+ * u32 tileSize texels on a tile's edge, or 0 for a payload that is one image
27
+ * u32 hidden[hiddenCount]
28
+ * u32 channels[channelCount] (semanticIndex << 4) | component, the packing REMAP_CHANNEL uses
29
+ * u32 nodes[nodeCount * 4]
30
+ * u32 tileOffset[tileCount] byte offset into the latent payload
31
+ * u32 tileLength[tileCount]
32
+ * u32 tileHash[tileCount * 2] `hashTile`'s 64 bits, high word first
33
+ * f32 weights[weightCount]
34
+ * u8 latent[...] padded to the container's alignment
35
+ * ```
36
+ *
37
+ * **No per-chunk version field, which is where this departs from the plan.** The plan's layout
38
+ * opened with a header and a version; every chunk in this container already answers to the file's
39
+ * own version, and `MSHL` states the reason in one line — a second version is a second thing to
40
+ * keep in step, and the one that drifts is the one nobody reads.
41
+ *
42
+ * **The tile table is the streaming unit, and until 2026-09-20 it could not be.** It was a flat
43
+ * list of offsets into a row-major image with a 32-bit hash that nothing wrote — and a tile of a
44
+ * row-major image is not contiguous, so no offset and length could name one. Three things make it
45
+ * real, and they are one decision rather than three:
46
+ *
47
+ * - **`tileSize` is in the header**, so a reader can work out the grid. Without it the table is a
48
+ * list of byte ranges nobody can address by tile.
49
+ * - **The payload is tile-major when there are tiles**: each tile's texels run together, in the
50
+ * grid's own order — row-major across, then down — which is exactly the order and the extent
51
+ * `@driftengine/texture`'s `latentTileGrid` cuts and `tilesForView` asks for. `tileCount` of 0
52
+ * means the payload is one row-major image, which is what every material small enough not to
53
+ * stream still is.
54
+ * - **The hash is `hashTile`'s 64 bits**, the same number residency addresses a tile by, so a tile
55
+ * that arrived for one material is already resident for every other material sharing it. Thirty
56
+ * two bits is about a one in ten thousand collision over sixty thousand tiles, and a collision
57
+ * here is one surface wearing another's texture.
58
+ *
59
+ * **What it gives up**: a reader that wants the whole image must put the tiles back together, which
60
+ * is a copy the row-major form does not need. That is why `tileCount` of 0 stays a legal chunk
61
+ * rather than being tidied away. **What would make it wrong** is a mip chain, which this chunk does
62
+ * not carry — `latentTileGrid` cuts one level per mip and a `DTEX` has one level, so the table is
63
+ * that level's tiles and nothing is implied about the others.
64
+ *
65
+ * **What is validated here is containment, not meaning.** A tile whose bytes run past the payload
66
+ * and a result register no node ever writes are refused, because those make a reader hand back
67
+ * arbitrary memory or an uninitialised value. Whether the program is one an interpreter can *run*
68
+ * is `validateDecodeGraph`'s question, in the package that knows what an operation is.
69
+ *
70
+ * Additive, and additive is what keeps `FORMAT.md`'s freeze intact: a reader that does not know
71
+ * this code skips it by its length and loses only the material, which it could not have decoded.
72
+ */
73
+
74
+ /** Registers the decode vocabulary has. Mirrors `MAX_REGISTERS`; asserted equal by a test. */
75
+ export const DTEX_REGISTERS = 16;
76
+
77
+ /** Address modes the decode vocabulary has. Mirrors `ADDRESS_MODE_COUNT`; asserted equal by a test. */
78
+ export const DTEX_ADDRESS_MODES = 4;
79
+
80
+ /** How many tiles one material may carry. Well past what a streaming budget permits. */
81
+ export const DTEX_MAX_TILES = 1 << 16;
82
+
83
+ /** A material's decode program, and which `MATL` entry it belongs to. */
84
+ export interface DtexEntry {
85
+ /** The index of the `MATL` entry this decodes. */
86
+ readonly material: number;
87
+ readonly texture: DtexMaterial;
88
+ }
89
+
90
+ export interface DtexMaterial {
91
+ latentWidth: number;
92
+ latentHeight: number;
93
+ latentComponents: number;
94
+ /** `(semanticIndex << 4) | component` per channel. */
95
+ channels: Uint32Array;
96
+ /** Four words a node: op, a, b, out. */
97
+ nodes: Uint32Array;
98
+ resultRegister: number;
99
+ addressMode: number;
100
+ networkInputs: number;
101
+ networkOutputs: number;
102
+ hidden: Uint32Array;
103
+ weights: Float32Array;
104
+ tileOffset: Uint32Array;
105
+ tileLength: Uint32Array;
106
+ /** Two words a tile, high first, as `hashTile` splits its 64 bits. */
107
+ tileHash: Uint32Array;
108
+ /**
109
+ * Texels on a tile's edge, or 0 where the payload is one row-major image.
110
+ *
111
+ * A grid rather than a count, because what streams is a rectangle of texels and a reader has to
112
+ * be able to say which one it is looking at.
113
+ */
114
+ tileSize: number;
115
+ latent: Uint8Array;
116
+ }
117
+
118
+ /*
119
+ * Fourteen: the thirteen the layout above lists after `material`, and `material` itself.
120
+ *
121
+ * **The pairing is in the chunk rather than in the order the chunks appear**, because a file may
122
+ * carry a decode program for some of its materials and not others — a scene where one surface came
123
+ * from a capture and the rest were authored — and a reader that paired by position would give the
124
+ * wrong material the wrong texture without anything failing.
125
+ */
126
+ const HEADER_WORDS = 14;
127
+ const HEADER = HEADER_WORDS * 4;
128
+
129
+ interface Layout {
130
+ hidden: number;
131
+ channels: number;
132
+ nodes: number;
133
+ tileOffset: number;
134
+ tileLength: number;
135
+ tileHash: number;
136
+ weights: number;
137
+ latent: number;
138
+ size: number;
139
+ }
140
+
141
+ function layoutOf(material: DtexMaterial): Layout {
142
+ const hiddenCount = material.hidden.length;
143
+ const channelCount = material.channels.length;
144
+ const nodeWords = material.nodes.length;
145
+ const tiles = material.tileOffset.length;
146
+ let at = HEADER;
147
+ const hidden = at;
148
+ at += hiddenCount * 4;
149
+ const channels = at;
150
+ at += channelCount * 4;
151
+ const nodes = at;
152
+ at += nodeWords * 4;
153
+ const tileOffset = at;
154
+ at += tiles * 4;
155
+ const tileLength = at;
156
+ at += tiles * 4;
157
+ const tileHash = at;
158
+ /* Two words a tile: `hashTile` is 64 bits and JavaScript has no integer that wide. */
159
+ at += tiles * 8;
160
+ const weights = at;
161
+ at += material.weights.length * 4;
162
+ const latent = at;
163
+ at += material.latent.length;
164
+ return { hidden, channels, nodes, tileOffset, tileLength, tileHash, weights, latent, size: at };
165
+ }
166
+
167
+ /** How many tiles across a latent of `width` texels is cut into at this tile size. */
168
+ export function dtexTilesAcross(width: number, tileSize: number): number {
169
+ return tileSize > 0 ? Math.ceil(width / tileSize) : 0;
170
+ }
171
+
172
+ /**
173
+ * How many tiles the grid holds, which is what the table's length has to be.
174
+ *
175
+ * **The order is the grid's own**: across a row, then down, the same order
176
+ * `@driftengine/texture`'s `latentTileGrid` cuts in — so a reader with a tile coordinate reads
177
+ * `ty * dtexTilesAcross(width, tileSize) + tx` and gets the entry for it.
178
+ */
179
+ export function dtexTileCount(width: number, height: number, tileSize: number): number {
180
+ return dtexTilesAcross(width, tileSize) * dtexTilesAcross(height, tileSize);
181
+ }
182
+
183
+ /** A tile table has to be the grid, or nothing can be addressed by where it is. */
184
+ function refuseStrayGrid(material: DtexMaterial, tiles: number): void {
185
+ const { tileSize, latentWidth, latentHeight } = material;
186
+ if (tiles === 0) {
187
+ if (tileSize !== 0) {
188
+ throw new DrftError(`DTEX declares a tile edge of ${tileSize} texels and carries no tiles`);
189
+ }
190
+ return;
191
+ }
192
+ if (!Number.isInteger(tileSize) || tileSize < 1) {
193
+ throw new DrftError(`DTEX carries ${tiles} tiles and a tile edge of ${tileSize} texels`);
194
+ }
195
+ const wanted = dtexTileCount(latentWidth, latentHeight, tileSize);
196
+ if (tiles !== wanted) {
197
+ throw new DrftError(
198
+ `DTEX carries ${tiles} tiles where a ${latentWidth} by ${latentHeight} latent cut at ` +
199
+ `${tileSize} is ${wanted}`,
200
+ );
201
+ }
202
+ }
203
+
204
+ export function buildDtex(entry: DtexEntry): Uint8Array {
205
+ const { texture: material } = entry;
206
+ if (!Number.isInteger(entry.material) || entry.material < 0) {
207
+ throw new DrftError(`DTEX names material ${entry.material}, which is not an index`);
208
+ }
209
+ const tiles = material.tileOffset.length;
210
+ if (tiles > DTEX_MAX_TILES) {
211
+ throw new DrftError(`DTEX carries ${tiles} tiles, past the ${DTEX_MAX_TILES} cap`);
212
+ }
213
+ if (material.tileLength.length !== tiles || material.tileHash.length !== tiles * 2) {
214
+ throw new DrftError('DTEX was given a tile table whose three columns are different lengths');
215
+ }
216
+ refuseStrayGrid(material, tiles);
217
+ if (material.nodes.length === 0 || material.nodes.length % 4 !== 0) {
218
+ throw new DrftError(
219
+ `DTEX was given ${material.nodes.length} node words, which is not whole nodes`,
220
+ );
221
+ }
222
+ if (material.latentComponents < 1 || material.latentComponents > 4) {
223
+ throw new DrftError(`DTEX latent has ${material.latentComponents} components, not one to four`);
224
+ }
225
+ /*
226
+ * **Only where the payload is one image.** With tiles it is tile-major and two identical tiles
227
+ * share one run, so a chunk whose payload is *smaller* than its grid is a chunk that deduplicated
228
+ * — which is the point. What holds there is the per-tile containment check below.
229
+ */
230
+ if (
231
+ tiles === 0 &&
232
+ material.latent.length <
233
+ material.latentWidth * material.latentHeight * material.latentComponents
234
+ ) {
235
+ throw new DrftError('DTEX latent payload is smaller than the grid it declares');
236
+ }
237
+
238
+ /*
239
+ * Every tile is checked to lie inside the payload before anything is written. A tile running
240
+ * past the end is a subarray of arbitrary memory on the way back in, which is exactly what
241
+ * `MSHL` checks its cluster table for.
242
+ */
243
+ for (let t = 0; t < tiles; t += 1) {
244
+ const start = material.tileOffset[t] as number;
245
+ const length = material.tileLength[t] as number;
246
+ if (start + length > material.latent.length) {
247
+ throw new DrftError(`DTEX tile ${t} runs past the ${material.latent.length}-byte payload`);
248
+ }
249
+ }
250
+
251
+ /* A result register nothing writes hands a reader whatever that register happened to hold. */
252
+ let written = false;
253
+ for (let n = 0; n * 4 < material.nodes.length; n += 1) {
254
+ const out = material.nodes[n * 4 + 3] as number;
255
+ if (out >= DTEX_REGISTERS) {
256
+ throw new DrftError(
257
+ `DTEX node ${n} writes register ${out}, past the ${DTEX_REGISTERS} there are`,
258
+ );
259
+ }
260
+ if (out === material.resultRegister) written = true;
261
+ }
262
+ if (!written) {
263
+ throw new DrftError(
264
+ `DTEX names register ${material.resultRegister} as its result, and no node writes it`,
265
+ );
266
+ }
267
+
268
+ const layout = layoutOf(material);
269
+ const bytes = new Uint8Array(align(layout.size));
270
+ const view = new DataView(bytes.buffer);
271
+ view.setUint32(0, entry.material, true);
272
+ view.setUint32(4, material.latentWidth, true);
273
+ view.setUint32(8, material.latentHeight, true);
274
+ view.setUint32(12, material.latentComponents, true);
275
+ view.setUint32(16, material.channels.length, true);
276
+ view.setUint32(20, material.nodes.length / 4, true);
277
+ view.setUint32(24, material.resultRegister, true);
278
+ view.setUint32(28, material.addressMode, true);
279
+ view.setUint32(32, material.networkInputs, true);
280
+ view.setUint32(36, material.networkOutputs, true);
281
+ view.setUint32(40, material.hidden.length, true);
282
+ view.setUint32(44, material.weights.length, true);
283
+ view.setUint32(48, tiles, true);
284
+ view.setUint32(52, material.tileSize, true);
285
+
286
+ new Uint32Array(bytes.buffer, layout.hidden, material.hidden.length).set(material.hidden);
287
+ new Uint32Array(bytes.buffer, layout.channels, material.channels.length).set(material.channels);
288
+ new Uint32Array(bytes.buffer, layout.nodes, material.nodes.length).set(material.nodes);
289
+ new Uint32Array(bytes.buffer, layout.tileOffset, tiles).set(material.tileOffset);
290
+ new Uint32Array(bytes.buffer, layout.tileLength, tiles).set(material.tileLength);
291
+ new Uint32Array(bytes.buffer, layout.tileHash, tiles * 2).set(material.tileHash);
292
+ new Float32Array(bytes.buffer, layout.weights, material.weights.length).set(material.weights);
293
+ bytes.set(material.latent, layout.latent);
294
+ return bytes;
295
+ }
296
+
297
+ /**
298
+ * The material a `DTEX` chunk carries, as views over the fetched buffer.
299
+ *
300
+ * Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
301
+ * object holding them.
302
+ */
303
+ export function readDtex(buffer: ArrayBuffer, offset: number, byteLength: number): DtexEntry {
304
+ if (byteLength < HEADER) throw new DrftError('DTEX is too short to hold its header');
305
+ const view = new DataView(buffer, offset, byteLength);
306
+ const materialIndex = view.getUint32(0, true);
307
+ const latentWidth = view.getUint32(4, true);
308
+ const latentHeight = view.getUint32(8, true);
309
+ const latentComponents = view.getUint32(12, true);
310
+ const channelCount = view.getUint32(16, true);
311
+ const nodeCount = view.getUint32(20, true);
312
+ const resultRegister = view.getUint32(24, true);
313
+ const addressMode = view.getUint32(28, true);
314
+ const networkInputs = view.getUint32(32, true);
315
+ const networkOutputs = view.getUint32(36, true);
316
+ const hiddenCount = view.getUint32(40, true);
317
+ const weightCount = view.getUint32(44, true);
318
+ const tiles = view.getUint32(48, true);
319
+ const tileSize = view.getUint32(52, true);
320
+
321
+ if (nodeCount === 0) throw new DrftError('DTEX carries no decode program');
322
+ if (latentComponents < 1 || latentComponents > 4) {
323
+ throw new DrftError(`DTEX latent declares ${latentComponents} components, not one to four`);
324
+ }
325
+ if (addressMode >= DTEX_ADDRESS_MODES) {
326
+ throw new DrftError(
327
+ `DTEX declares address mode ${String(addressMode)}; this reader knows 0 to ${String(DTEX_ADDRESS_MODES - 1)}`,
328
+ );
329
+ }
330
+
331
+ const shape: DtexMaterial = {
332
+ latentWidth,
333
+ latentHeight,
334
+ latentComponents,
335
+ channels: new Uint32Array(channelCount),
336
+ nodes: new Uint32Array(nodeCount * 4),
337
+ resultRegister,
338
+ addressMode,
339
+ networkInputs,
340
+ networkOutputs,
341
+ hidden: new Uint32Array(hiddenCount),
342
+ weights: new Float32Array(weightCount),
343
+ tileOffset: new Uint32Array(tiles),
344
+ tileLength: new Uint32Array(tiles),
345
+ tileHash: new Uint32Array(tiles * 2),
346
+ tileSize,
347
+ latent: new Uint8Array(0),
348
+ };
349
+ const layout = layoutOf(shape);
350
+ const latentBytes = byteLength - layout.latent;
351
+ if (latentBytes < 0) {
352
+ throw new DrftError(
353
+ `DTEX declares ${String(layout.latent)} bytes of tables in a ${String(byteLength)}-byte chunk`,
354
+ );
355
+ }
356
+
357
+ const base = offset;
358
+ const material: DtexMaterial = {
359
+ latentWidth,
360
+ latentHeight,
361
+ latentComponents,
362
+ channels: new Uint32Array(buffer, base + layout.channels, channelCount),
363
+ nodes: new Uint32Array(buffer, base + layout.nodes, nodeCount * 4),
364
+ resultRegister,
365
+ addressMode,
366
+ networkInputs,
367
+ networkOutputs,
368
+ hidden: new Uint32Array(buffer, base + layout.hidden, hiddenCount),
369
+ weights: new Float32Array(buffer, base + layout.weights, weightCount),
370
+ tileOffset: new Uint32Array(buffer, base + layout.tileOffset, tiles),
371
+ tileLength: new Uint32Array(buffer, base + layout.tileLength, tiles),
372
+ tileHash: new Uint32Array(buffer, base + layout.tileHash, tiles * 2),
373
+ tileSize,
374
+ latent: new Uint8Array(buffer, base + layout.latent, latentBytes),
375
+ };
376
+
377
+ refuseStrayGrid(material, tiles);
378
+ if (tiles === 0 && latentBytes < latentWidth * latentHeight * latentComponents) {
379
+ throw new DrftError('DTEX latent payload is smaller than the grid it declares');
380
+ }
381
+ for (let t = 0; t < tiles; t += 1) {
382
+ const start = material.tileOffset[t] as number;
383
+ const length = material.tileLength[t] as number;
384
+ if (start + length > latentBytes) {
385
+ throw new DrftError(`DTEX tile ${t} runs past the ${String(latentBytes)}-byte payload`);
386
+ }
387
+ }
388
+ let written = false;
389
+ for (let n = 0; n < nodeCount; n += 1) {
390
+ const out = material.nodes[n * 4 + 3] as number;
391
+ if (out >= DTEX_REGISTERS) {
392
+ throw new DrftError(
393
+ `DTEX node ${n} writes register ${out}, past the ${DTEX_REGISTERS} there are`,
394
+ );
395
+ }
396
+ if (out === resultRegister) written = true;
397
+ }
398
+ if (!written) {
399
+ throw new DrftError(
400
+ `DTEX names register ${resultRegister} as its result, and no node writes it`,
401
+ );
402
+ }
403
+ return { material: materialIndex, texture: material };
404
+ }
package/src/ents.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { DrftError, align } from './drftFormat.ts';
2
+
3
+ /**
4
+ * `ENTS`: the things in a scene, as the entity model wrote them.
5
+ *
6
+ * ```
7
+ * u32 byteLength of the text that follows
8
+ * u8 scene[byteLength] UTF-8 JSON, padded to the container's alignment
9
+ * ```
10
+ *
11
+ * **Text, in a binary container, on purpose.** Every other chunk here holds numbers whose meaning
12
+ * this package knows — positions, indices, weights. A scene holds a *consumer's* component fields,
13
+ * whose types are declared in that consumer's own code and in its `.drs` files. A binary encoding
14
+ * for them would be an encoding of somebody else's type system, extended every time they add a
15
+ * field, and wrong in the direction that reads as working: a field the container did not know how
16
+ * to write would silently not be there. What `serializeWorld` produces is already a value; this
17
+ * carries it unchanged.
18
+ *
19
+ * **The scene's own version is inside it and is not repeated.** `MSHL` states the reason in one
20
+ * line — a second version is a second thing to keep in step, and the one that drifts is the one
21
+ * nobody reads.
22
+ *
23
+ * **What is checked is that this is a scene, not that it is a scene anybody can load.** Whether a
24
+ * component named here exists, and whether its schema still matches, is `deserializeWorld`'s
25
+ * question and it answers in words. Refusing text that is not a scene at all belongs here, so a
26
+ * consumer meets a sentence rather than `undefined.entities` three calls later.
27
+ *
28
+ * Additive on the same terms as `DTEX`: a reader that does not know this code skips it by its
29
+ * length and loses only the scene, which it had no entity model to load into anyway.
30
+ */
31
+
32
+ /** A world as data. `@driftengine/entities`' `SerializedScene` is one of these. */
33
+ export interface EntsScene {
34
+ readonly version: number;
35
+ /** What each component type looked like when this was written. */
36
+ readonly schemas: Readonly<Record<string, unknown>>;
37
+ readonly entities: readonly {
38
+ /** Component name → field id → value. */
39
+ readonly components: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
40
+ }[];
41
+ }
42
+
43
+ const HEADER = 4;
44
+ const encoder = new TextEncoder();
45
+ const decoder = new TextDecoder();
46
+
47
+ export function buildEnts(scene: EntsScene): Uint8Array {
48
+ refuseNonScene(scene);
49
+ const text = encoder.encode(JSON.stringify(scene));
50
+ const bytes = new Uint8Array(align(HEADER + text.length));
51
+ new DataView(bytes.buffer).setUint32(0, text.length, true);
52
+ bytes.set(text, HEADER);
53
+ return bytes;
54
+ }
55
+
56
+ /**
57
+ * The scene an `ENTS` chunk carries.
58
+ *
59
+ * **A copy rather than a view, and it is the only chunk here that has to be.** Text is not a typed
60
+ * array over the buffer; parsing it is what makes it a scene. A consumer that loads one is about
61
+ * to create every entity in it anyway, so the copy is not the cost that matters.
62
+ */
63
+ export function readEnts(buffer: ArrayBuffer, offset: number, byteLength: number): EntsScene {
64
+ if (byteLength < HEADER) throw new DrftError('ENTS is too short to hold its header');
65
+ const view = new DataView(buffer, offset, byteLength);
66
+ const length = view.getUint32(0, true);
67
+ if (HEADER + length > byteLength) {
68
+ throw new DrftError(
69
+ `ENTS declares ${String(length)} bytes of scene in a ${String(byteLength)}-byte chunk`,
70
+ );
71
+ }
72
+
73
+ const text = decoder.decode(new Uint8Array(buffer, offset + HEADER, length));
74
+ let parsed: unknown;
75
+ try {
76
+ parsed = JSON.parse(text);
77
+ } catch (error) {
78
+ throw new DrftError(`ENTS does not hold a scene: ${(error as Error).message}`);
79
+ }
80
+ refuseNonScene(parsed);
81
+ return parsed;
82
+ }
83
+
84
+ /** The shape a scene has, checked on both sides so neither end can write one the other refuses. */
85
+ function refuseNonScene(scene: unknown): asserts scene is EntsScene {
86
+ if (typeof scene !== 'object' || scene === null) {
87
+ throw new DrftError('ENTS holds no scene at all');
88
+ }
89
+ const held = scene as Partial<EntsScene>;
90
+ if (!Number.isInteger(held.version) || (held.version as number) < 1) {
91
+ throw new DrftError(`ENTS holds a scene at version ${String(held.version)}`);
92
+ }
93
+ if (typeof held.schemas !== 'object' || held.schemas === null) {
94
+ throw new DrftError('ENTS holds a scene with no schemas, so nothing in it could be matched up');
95
+ }
96
+ if (!Array.isArray(held.entities)) {
97
+ throw new DrftError('ENTS holds a scene whose entities are not a list');
98
+ }
99
+ for (let at = 0; at < held.entities.length; at += 1) {
100
+ const entity = held.entities[at] as { components?: unknown } | null;
101
+ if (typeof entity !== 'object' || entity === null || typeof entity.components !== 'object') {
102
+ throw new DrftError(`ENTS holds an entity at ${at} with no components`);
103
+ }
104
+ }
105
+ }
package/src/index.ts CHANGED
@@ -16,6 +16,25 @@ export type { AnimationClip, DrftSkin, Joint, JointTrack, TrackPath } from './an
16
16
  export type { DrftMorph, DrftNode } from './drftSkin.ts';
17
17
  export type { DrftSubs, DrftSubstanceEntry } from './drftSubs.ts';
18
18
  export { buildSubs, readSubs } from './drftSubs.ts';
19
+ export type { DrftSdfv, DrftSdfvEntry } from './sdfv.ts';
20
+ export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES } from './sdfv.ts';
21
+ export {
22
+ NNET_ENTRY_BYTES,
23
+ NNET_MAX_HIDDEN,
24
+ NNET_MAX_WIDTH,
25
+ buildNnet,
26
+ nnetWeightCount,
27
+ readNnet,
28
+ } from './nnet.ts';
29
+ export type { DrftNetwork, DrftNnet } from './nnet.ts';
30
+ export { buildNgrf, readNgrf } from './ngrf.ts';
31
+ export type {
32
+ DrftAttribute,
33
+ DrftGraph,
34
+ DrftGraphNode,
35
+ DrftGraphTensor,
36
+ DrftGraphValue,
37
+ } from './ngrf.ts';
19
38
  export {
20
39
  buildClip,
21
40
  buildMorph,
@@ -40,6 +59,27 @@ export {
40
59
  buildColliders,
41
60
  readColliders,
42
61
  } from './drftColliders.ts';
62
+ /*
63
+ * **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
64
+ * carries no semantics — this package is the container and does not know what a channel means —
65
+ * so a channel crosses as `(semanticIndex << 4) | component` and `@driftengine/texture` owns the
66
+ * numbering.
67
+ */
68
+ export { buildDtex, readDtex, DTEX_ADDRESS_MODES, DTEX_MAX_TILES, DTEX_REGISTERS } from './dtex.ts';
69
+ export type { DtexEntry, DtexMaterial } from './dtex.ts';
70
+ /*
71
+ * **`NAVM`: a way across the scene, with the placement it was built at.** The numbers in a polygon
72
+ * mesh are cell indices, so the origin and the cell size travel inside the chunk — a mesh that
73
+ * arrives without them is in the wrong units at the wrong place, and looks loaded.
74
+ *
75
+ * **`ENTS`: the things in the scene, as the entity model wrote them.** Text, in a binary container,
76
+ * because the values are a consumer's own component fields and this package does not know their
77
+ * types — see `ents.ts` for why encoding them would be worse than carrying them.
78
+ */
79
+ export { buildNavm, readNavm } from './navm.ts';
80
+ export type { NavPolyMesh } from './navm.ts';
81
+ export { buildEnts, readEnts } from './ents.ts';
82
+ export type { EntsScene } from './ents.ts';
43
83
  export { readDrft } from './drftRead.ts';
44
84
  export type { DrftAsset, DrftTexture } from './drftRead.ts';
45
85
  /* Reading one while it is still arriving, for a scene that would rather show a model build. */
@@ -54,3 +94,6 @@ export type { DrftHead, DrftMaterial, DrftSplatBlock, DrftSplats } from './drftF
54
94
  /* The order a capture is written in, so a baker outside this package can lay one out the same
55
95
  way — it is the whole reason a capture streams into a recognisable place. */
56
96
  export { coarseFirstOrder } from './coarseFirst.ts';
97
+
98
+ export { MAX_CLUSTERS, buildMeshlets, readMeshlets } from './drftMeshlets.ts';
99
+ export type { MeshletLevel } from './drftMeshlets.ts';