@driftengine/drft 4.4.2 → 4.6.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/drftRead.ts CHANGED
@@ -31,6 +31,8 @@ import {
31
31
  ATTR_SPECULAR,
32
32
  ATTR_UVS,
33
33
  ATTR_WEIGHTS,
34
+ ATTR_LAYERS,
35
+ ATTR_CHANNEL,
34
36
  CHUNK_ENTRY_BYTES,
35
37
  CHUNK_HEAD,
36
38
  CHUNK_LODM,
@@ -55,8 +57,12 @@ import {
55
57
  CHUNK_ANIM,
56
58
  CHUNK_NODE,
57
59
  CHUNK_INST,
60
+ CHUNK_REGN,
61
+ CHUNK_LVOL,
58
62
  CHUNK_LITE,
59
63
  CHUNK_MSHQ,
64
+ CHUNK_KITS,
65
+ CHUNK_MSHC,
60
66
  CHUNK_SKIN,
61
67
  CHUNK_MORP,
62
68
  CHUNK_NNET,
@@ -74,6 +80,13 @@ import { type NavPolyMesh, readNavm } from './navm.ts';
74
80
  import { type DrftNetwork, type DrftNnet, readNnet } from './nnet.ts';
75
81
  import { type DrftGraph, readNgrf } from './ngrf.ts';
76
82
  import { readColliders } from './drftColliders.ts';
83
+ import { RegionLedger, readRegion } from './drftRegions.ts';
84
+ import { expandAssembly } from './assemble.ts';
85
+ import { checkCopies, readAssembly, readKit } from './drftAssembly.ts';
86
+ import type { DrftAssembly } from './drftAssembly.ts';
87
+ import { readLightVolume } from './drftLightVolume.ts';
88
+ import type { DrftLightVolume } from './drftLightVolume.ts';
89
+ import type { DrftRegion } from './drftRegions.ts';
77
90
  import { readInstances } from './drftInstances.ts';
78
91
  import type { DrftInstanceGroup } from './drftInstances.ts';
79
92
  import { readLights } from './drftLights.ts';
@@ -208,6 +221,24 @@ export interface DrftAsset {
208
221
  * ```
209
222
  */
210
223
  readonly colliders: readonly Float32Array[];
224
+ /**
225
+ * The regions of a streamed world, in the order they were written, or empty. `REGN`, defined at
226
+ * 1.21: each names the meshes it draws at every level, its props, occluders and collision.
227
+ */
228
+ readonly regions: readonly DrftRegion[];
229
+ /** A world's summed lights, or null for a file carrying no `LVOL`. Defined at 1.22. */
230
+ readonly lightVolume: DrftLightVolume | null;
231
+ /**
232
+ * The meshes that are kit pieces, ascending, or empty. `KITS`, defined at 1.23. A piece is in
233
+ * `meshes` like any other, and is drawn only as copies inside the assemblies.
234
+ */
235
+ readonly kit: readonly number[];
236
+ /**
237
+ * Each `MSHC` mesh as the file carried it, by ordinal, or empty. Its slot in `meshes` holds it
238
+ * expanded — which for a world built from a kit is most of a gigabyte, so a consumer of such a
239
+ * file streams it (`DrftStream.onAssembly`) rather than reading it whole.
240
+ */
241
+ readonly assemblies: ReadonlyMap<number, DrftAssembly>;
211
242
  /** Optional chunks this reader did not understand, in file order. Diagnostics only. */
212
243
  readonly skipped: readonly string[];
213
244
  readonly versionMajor: number;
@@ -290,6 +321,10 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
290
321
  writer set only one bit is rejected rather than half-skinned. See ATTR_JOINTS. */
291
322
  const joints = (attributes & ATTR_JOINTS) !== 0 ? floats(vertices * 4, 'joints') : undefined;
292
323
  const weights = (attributes & ATTR_WEIGHTS) !== 0 ? floats(vertices * 4, 'weights') : undefined;
324
+ /* Texture-array layers, 1.20, one float a vertex and last. See ATTR_LAYERS. */
325
+ const layers = (attributes & ATTR_LAYERS) !== 0 ? floats(vertices, 'layers') : undefined;
326
+ /* Sway, sky and opacity, 1.23, four floats a vertex and last. See ATTR_CHANNEL. */
327
+ const channel = (attributes & ATTR_CHANNEL) !== 0 ? floats(vertices * 4, 'channel') : undefined;
293
328
 
294
329
  const indexBytes = indexCount * 4;
295
330
  if (at + indexBytes > limit) {
@@ -312,6 +347,8 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
312
347
  ...(tangents === undefined ? {} : { tangents }),
313
348
  ...(joints === undefined ? {} : { joints }),
314
349
  ...(weights === undefined ? {} : { weights }),
350
+ ...(layers === undefined ? {} : { layers }),
351
+ ...(channel === undefined ? {} : { channel }),
315
352
  };
316
353
 
317
354
  /*
@@ -324,6 +361,15 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
324
361
  }
325
362
 
326
363
  /** Decode a `.drft` from a buffer. Throws `DrftError` on anything it cannot read. */
364
+ /** A slot held for an assembly until its pieces are in hand. Never returned. */
365
+ const EMPTY_MESH: MeshData = {
366
+ positions: new Float32Array(0),
367
+ normals: new Float32Array(0),
368
+ colors: new Float32Array(0),
369
+ emissive: new Float32Array(0),
370
+ indices: new Uint32Array(0),
371
+ };
372
+
327
373
  export function readDrft(buffer: ArrayBuffer): DrftAsset {
328
374
  if (buffer.byteLength < HEADER_BYTES) {
329
375
  throw new DrftError(`a file is at least ${HEADER_BYTES} bytes, got ${buffer.byteLength}`);
@@ -382,6 +428,10 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
382
428
  let colliders: readonly Float32Array[] = [];
383
429
  /* Read once every mesh is counted, because a group is checked against the mesh count. */
384
430
  let instanceChunk: { offset: number; byteLength: number } | null = null;
431
+ const regionChunks: { offset: number; byteLength: number }[] = [];
432
+ let lightVolume: DrftLightVolume | null = null;
433
+ let kitChunk: { offset: number; byteLength: number } | null = null;
434
+ const assemblies = new Map<number, DrftAssembly>();
385
435
  let lights: DrftLight[] = [];
386
436
  const skipped: string[] = [];
387
437
  let head: DrftHead | null = null;
@@ -427,6 +477,11 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
427
477
  /* A mesh like any other, in the same ordinal sequence: see `drftQuantised.ts`. */
428
478
  else if (chunk.code === CHUNK_MSHQ)
429
479
  meshes.push(decodeQuantisedMesh(buffer, chunk.offset, chunk.byteLength));
480
+ else if (chunk.code === CHUNK_MSHC) {
481
+ /* Its slot held, and filled once every piece is in hand. */
482
+ assemblies.set(meshes.length, readAssembly(buffer, chunk.offset, chunk.byteLength));
483
+ meshes.push(EMPTY_MESH);
484
+ } else if (chunk.code === CHUNK_KITS) kitChunk = chunk;
430
485
  /* A level of detail is a mesh payload under another code, so it takes the same reader. */
431
486
  else if (chunk.code === CHUNK_LODM)
432
487
  levels.push({ level: chunk.index, mesh: readMesh(buffer, chunk) });
@@ -443,6 +498,10 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
443
498
  else if (chunk.code === CHUNK_ENTS) entities = readEnts(buffer, chunk.offset, chunk.byteLength);
444
499
  else if (chunk.code === CHUNK_NODE) nodes = readNodes(buffer, chunk.offset, chunk.byteLength);
445
500
  else if (chunk.code === CHUNK_INST) instanceChunk = chunk;
501
+ /* Read once every mesh is counted, since a region names meshes that follow it. */
502
+ else if (chunk.code === CHUNK_REGN) regionChunks.push(chunk);
503
+ else if (chunk.code === CHUNK_LVOL)
504
+ lightVolume = readLightVolume(buffer, chunk.offset, chunk.byteLength);
446
505
  else if (chunk.code === CHUNK_LITE) lights = readLights(buffer, chunk.offset, chunk.byteLength);
447
506
  else if (chunk.code === CHUNK_SKIN)
448
507
  skins.push(readSkin(buffer, chunk.offset, chunk.byteLength));
@@ -530,6 +589,39 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
530
589
  };
531
590
  }
532
591
 
592
+ const kit =
593
+ kitChunk === null ? [] : readKit(buffer, kitChunk.offset, kitChunk.byteLength, meshes.length);
594
+ const pieces = new Set(kit);
595
+ for (const o of kit) {
596
+ if (assemblies.has(o)) throw new DrftError(`mesh ${o} is a kit piece and an assembly`);
597
+ }
598
+ for (const [ordinal, assembly] of assemblies) {
599
+ checkCopies(assembly, ordinal, pieces, (o) => meshes[o]);
600
+ meshes[ordinal] = expandAssembly(assembly, (o) => meshes[o] as MeshData);
601
+ }
602
+
603
+ const ledger = new RegionLedger();
604
+ const regions = regionChunks.map((chunk) => {
605
+ const region = readRegion(buffer, chunk.offset, chunk.byteLength, meshes.length);
606
+ ledger.admit(region);
607
+ return region;
608
+ });
609
+ const instances =
610
+ instanceChunk === null
611
+ ? []
612
+ : readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length);
613
+ /* A mesh placed by `INST` across the whole file and also a region's would be drawn twice. */
614
+ for (const group of instances) {
615
+ if (ledger.names(group.mesh)) {
616
+ throw new DrftError(`mesh ${group.mesh} is placed by INST and is also a region's`);
617
+ }
618
+ if (pieces.has(group.mesh))
619
+ throw new DrftError(`mesh ${group.mesh} is a kit piece and is placed by INST`);
620
+ }
621
+ for (const o of kit) {
622
+ if (ledger.names(o)) throw new DrftError(`mesh ${o} is a kit piece and a region's`);
623
+ }
624
+
533
625
  /* By the ordinal the file gave them rather than by where they sat, coarsest first. */
534
626
  levels.sort((a, b) => a.level - b.level);
535
627
  const lods = levels.map((entry) => entry.mesh);
@@ -540,10 +632,7 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
540
632
  lods,
541
633
  levels: discreteLevels,
542
634
  nodes,
543
- instances:
544
- instanceChunk === null
545
- ? []
546
- : readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length),
635
+ instances,
547
636
  lights,
548
637
  skins,
549
638
  clips,
@@ -554,6 +643,10 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
554
643
  networks: nnet?.networks ?? [],
555
644
  graphs,
556
645
  colliders,
646
+ regions,
647
+ lightVolume,
648
+ kit,
649
+ assemblies,
557
650
  dtex,
558
651
  navigation,
559
652
  entities,
@@ -758,6 +851,16 @@ export function readMaterials(buffer: ArrayBuffer, chunk: DrftChunk): DrftMateri
758
851
  blend: has(76, 4) ? (view.getUint32(at + 76, true) & 1) !== 0 : false,
759
852
  /* One-sided unless it says otherwise, which is how every file before the bit was drawn. */
760
853
  doubleSided: has(76, 4) ? (view.getUint32(at + 76, true) & 2) !== 0 : false,
854
+ /* Not glass, which is what every file before 1.19 meant. */
855
+ transmission: has(80, 4) ? view.getFloat32(at + 80, true) : 0,
856
+ frost: has(84, 4) ? view.getFloat32(at + 84, true) : 0,
857
+ tint: has(88, 12)
858
+ ? [
859
+ view.getFloat32(at + 88, true),
860
+ view.getFloat32(at + 92, true),
861
+ view.getFloat32(at + 96, true),
862
+ ]
863
+ : [1, 1, 1],
761
864
  });
762
865
  }
763
866
  return out;
@@ -0,0 +1,350 @@
1
+ /**
2
+ * `REGN`: one region of a streamed world — its geometry at every level of detail, its prop
3
+ * batches, its occluders and its collision — as one chunk, written just ahead of its meshes.
4
+ *
5
+ * **Required, because a reader that skipped it would draw every level of every region at once.**
6
+ * A region's levels are ordinary meshes, so an old reader would open a city as each district drawn
7
+ * three times over itself, and every prop it places as one copy at the origin — the `INST`
8
+ * argument. A refusal naming `REGN` is the honest answer. What that gives up is opening such a file
9
+ * at all in a reader before 1.21.
10
+ *
11
+ * **Levels are meshes named by ordinal, finest first, each with a geometric error**: how far, in
12
+ * metres, that level's surface may stand from the real one. The runtime turns an error into a
13
+ * switch distance against its own screen, which a file cannot know. A mesh belongs to at most one
14
+ * region's levels. An instance group's prototype may be shared by any number of regions — a lamp
15
+ * post is one mesh however many districts stand it — but is never also a level mesh.
16
+ *
17
+ * Layout, little-endian:
18
+ * - u32 id, then f32 × 6 bounds (min xyz, max xyz);
19
+ * - u32 level count, then per level: f32 error, u32 mesh count, that many u32 mesh ordinals;
20
+ * - u32 group count, then per group: u32 mesh ordinal, u32 copies, that many 16-float matrices;
21
+ * - u32 occluder count, then f32 × 6 a box (min xyz, max xyz);
22
+ * - u32 vertex count, u32 index count, f32 × 3 a vertex, u32 an index: the region's collision.
23
+ */
24
+ import { DrftError, align } from './drftFormat.ts';
25
+ import type { DrftInstanceGroup } from './drftInstances.ts';
26
+
27
+ export interface DrftRegionLevel {
28
+ /** How far, in metres, this level's surface may stand from the real one. 0 for exact. */
29
+ readonly error: number;
30
+ /** The ordinals of the meshes this level draws. */
31
+ readonly meshes: readonly number[];
32
+ }
33
+
34
+ export interface DrftRegionCollision {
35
+ readonly positions: Float32Array;
36
+ readonly indices: Uint32Array;
37
+ }
38
+
39
+ export interface DrftRegion {
40
+ readonly id: number;
41
+ /** Everything the region holds, min xyz then max xyz. */
42
+ readonly bounds: ArrayLike<number>;
43
+ /** Finest first; at least one. */
44
+ readonly levels: readonly DrftRegionLevel[];
45
+ /** Its props, drawn at its finest level. */
46
+ readonly instances: readonly DrftInstanceGroup[];
47
+ /** Boxes that hide what stands behind them, six floats each. */
48
+ readonly occluders: Float32Array;
49
+ /** One triangle mesh the region collides as, or null. */
50
+ readonly collision: DrftRegionCollision | null;
51
+ }
52
+
53
+ /** The most levels a region may carry; the runtime keeps one value of a byte for "none". */
54
+ export const REGION_MAX_LEVELS = 254;
55
+
56
+ export function buildRegion(region: DrftRegion): Uint8Array {
57
+ checkRegion(region, Number.POSITIVE_INFINITY);
58
+ let bytes = 4 + 24 + 4 + 4 + 4 + 8;
59
+ for (const level of region.levels) bytes += 8 + level.meshes.length * 4;
60
+ for (const group of region.instances) bytes += 8 + group.transforms.length * 4;
61
+ bytes += region.occluders.length * 4;
62
+ const vertices = region.collision?.positions.length ?? 0;
63
+ const indices = region.collision?.indices.length ?? 0;
64
+ bytes += vertices * 4 + indices * 4;
65
+ const out = new Uint8Array(align(bytes));
66
+ const view = new DataView(out.buffer);
67
+ let at = 0;
68
+ const u32 = (value: number): void => {
69
+ view.setUint32(at, value, true);
70
+ at += 4;
71
+ };
72
+ const f32 = (value: number): void => {
73
+ view.setFloat32(at, value, true);
74
+ at += 4;
75
+ };
76
+ u32(region.id);
77
+ for (let i = 0; i < 6; i++) f32(region.bounds[i] as number);
78
+ u32(region.levels.length);
79
+ for (const level of region.levels) {
80
+ f32(level.error);
81
+ u32(level.meshes.length);
82
+ for (const mesh of level.meshes) u32(mesh);
83
+ }
84
+ u32(region.instances.length);
85
+ for (const group of region.instances) {
86
+ u32(group.mesh);
87
+ u32(group.transforms.length / 16);
88
+ for (let i = 0; i < group.transforms.length; i++) f32(group.transforms[i] as number);
89
+ }
90
+ u32(region.occluders.length / 6);
91
+ for (let i = 0; i < region.occluders.length; i++) f32(region.occluders[i] as number);
92
+ u32(vertices / 3);
93
+ u32(indices);
94
+ for (let i = 0; i < vertices; i++) f32(region.collision?.positions[i] as number);
95
+ for (let i = 0; i < indices; i++) u32(region.collision?.indices[i] as number);
96
+ /* The one check the builder owns: it wrote exactly what it measured. */
97
+ if (at !== bytes) throw new DrftError(`internal: REGN wrote ${at} of ${bytes} bytes`);
98
+ return out;
99
+ }
100
+
101
+ /** One `REGN` payload, checked on its own against the file's mesh count. */
102
+ export function readRegion(
103
+ buffer: ArrayBufferLike,
104
+ offset: number,
105
+ byteLength: number,
106
+ meshCount: number,
107
+ ): DrftRegion {
108
+ const view = new DataView(buffer, offset, byteLength);
109
+ let at = 0;
110
+ const need = (bytes: number, what: string): void => {
111
+ if (at + bytes > byteLength) throw new DrftError(`REGN ends inside ${what}`);
112
+ };
113
+ const u32 = (): number => {
114
+ const value = view.getUint32(at, true);
115
+ at += 4;
116
+ return value;
117
+ };
118
+ const floats = (count: number): Float32Array => {
119
+ const out = new Float32Array(count);
120
+ for (let i = 0; i < count; i++) out[i] = view.getFloat32(at + i * 4, true);
121
+ at += count * 4;
122
+ return out;
123
+ };
124
+ need(32, 'its header');
125
+ const id = u32();
126
+ const bounds = floats(6);
127
+ const levelCount = u32();
128
+ const levels: DrftRegionLevel[] = [];
129
+ for (let l = 0; l < levelCount; l++) {
130
+ need(8, `region ${id}'s level ${l}`);
131
+ const error = view.getFloat32(at, true);
132
+ at += 4;
133
+ const count = u32();
134
+ need(count * 4, `region ${id}'s level ${l}`);
135
+ const meshes: number[] = [];
136
+ for (let m = 0; m < count; m++) meshes.push(u32());
137
+ levels.push({ error, meshes });
138
+ }
139
+ need(4, `region ${id}'s groups`);
140
+ const groupCount = u32();
141
+ const instances: DrftInstanceGroup[] = [];
142
+ for (let g = 0; g < groupCount; g++) {
143
+ need(8, `region ${id}'s group ${g}`);
144
+ const mesh = u32();
145
+ const copies = u32();
146
+ need(copies * 64, `region ${id}'s group ${g}`);
147
+ instances.push({ mesh, transforms: floats(copies * 16) });
148
+ }
149
+ need(4, `region ${id}'s occluders`);
150
+ const occluderCount = u32();
151
+ need(occluderCount * 24, `region ${id}'s occluders`);
152
+ const occluders = floats(occluderCount * 6);
153
+ need(8, `region ${id}'s collision`);
154
+ const vertices = u32();
155
+ const indexCount = u32();
156
+ need(vertices * 12 + indexCount * 4, `region ${id}'s collision`);
157
+ const positions = floats(vertices * 3);
158
+ const indices = new Uint32Array(indexCount);
159
+ for (let i = 0; i < indexCount; i++) indices[i] = u32();
160
+ const region: DrftRegion = {
161
+ id,
162
+ bounds,
163
+ levels,
164
+ instances,
165
+ occluders,
166
+ collision: vertices === 0 && indexCount === 0 ? null : { positions, indices },
167
+ };
168
+ checkRegion(region, meshCount);
169
+ return region;
170
+ }
171
+
172
+ /** What one region must be on its own, whoever is asking. */
173
+ function checkRegion(region: DrftRegion, meshCount: number): void {
174
+ const name = `region ${region.id}`;
175
+ const b = region.bounds;
176
+ for (let axis = 0; axis < 3; axis++) {
177
+ if (!((b[axis] as number) <= (b[axis + 3] as number))) {
178
+ throw new DrftError(`${name}'s bounds are inside out on axis ${axis}`);
179
+ }
180
+ }
181
+ if (region.levels.length < 1 || region.levels.length > REGION_MAX_LEVELS) {
182
+ throw new DrftError(
183
+ `${name} has ${region.levels.length} levels; 1 to ${REGION_MAX_LEVELS} are allowed`,
184
+ );
185
+ }
186
+ let previous = 0;
187
+ for (let l = 0; l < region.levels.length; l++) {
188
+ const level = region.levels[l] as DrftRegionLevel;
189
+ if (!(level.error >= previous) || !Number.isFinite(level.error)) {
190
+ throw new DrftError(
191
+ `${name}'s level ${l} has error ${level.error}, finer than the one before`,
192
+ );
193
+ }
194
+ previous = level.error;
195
+ for (const mesh of level.meshes) {
196
+ if (mesh >= meshCount) {
197
+ throw new DrftError(`${name} draws mesh ${mesh}, and the file carries ${meshCount}`);
198
+ }
199
+ }
200
+ }
201
+ const grouped = new Set<number>();
202
+ for (const group of region.instances) {
203
+ if (group.mesh >= meshCount) {
204
+ throw new DrftError(`${name} places mesh ${group.mesh}, and the file carries ${meshCount}`);
205
+ }
206
+ if (group.transforms.length === 0 || group.transforms.length % 16 !== 0) {
207
+ throw new DrftError(`${name}'s group for mesh ${group.mesh} is not whole matrices`);
208
+ }
209
+ if (grouped.has(group.mesh)) throw new DrftError(`${name} groups mesh ${group.mesh} twice`);
210
+ grouped.add(group.mesh);
211
+ }
212
+ if (region.occluders.length % 6 !== 0) {
213
+ throw new DrftError(`${name}'s occluders are not whole boxes`);
214
+ }
215
+ const collision = region.collision;
216
+ if (collision !== null) {
217
+ const vertices = collision.positions.length / 3;
218
+ if (collision.positions.length % 3 !== 0 || collision.indices.length % 3 !== 0) {
219
+ throw new DrftError(`${name}'s collision is not whole vertices and triangles`);
220
+ }
221
+ for (let i = 0; i < collision.indices.length; i++) {
222
+ if ((collision.indices[i] as number) >= vertices) {
223
+ throw new DrftError(
224
+ `${name}'s collision names vertex ${collision.indices[i]} of ${vertices}`,
225
+ );
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * What regions must be to each other: an id once, a level mesh in one region's one level, and a
233
+ * group's prototype never also a level mesh. Kept across chunks by the writer, the whole-file reader
234
+ * and the stream alike, so a file one of them refuses the others refuse too.
235
+ */
236
+ export class RegionLedger {
237
+ private readonly ids = new Set<number>();
238
+ private readonly levelMeshes = new Set<number>();
239
+ private readonly prototypes = new Set<number>();
240
+
241
+ admit(region: DrftRegion): void {
242
+ if (this.ids.has(region.id)) throw new DrftError(`region ${region.id} appears twice`);
243
+ this.ids.add(region.id);
244
+ for (const level of region.levels) {
245
+ for (const mesh of level.meshes) {
246
+ if (this.levelMeshes.has(mesh) || this.prototypes.has(mesh)) {
247
+ throw new DrftError(
248
+ `mesh ${mesh} is region ${region.id}'s and something else's; a level mesh belongs to one region's one level`,
249
+ );
250
+ }
251
+ this.levelMeshes.add(mesh);
252
+ }
253
+ }
254
+ for (const group of region.instances) {
255
+ if (this.levelMeshes.has(group.mesh)) {
256
+ throw new DrftError(
257
+ `mesh ${group.mesh} is both a level mesh and a prop region ${region.id} places`,
258
+ );
259
+ }
260
+ this.prototypes.add(group.mesh);
261
+ }
262
+ }
263
+
264
+ /** Whether a mesh is any region's, as a level or a prop. */
265
+ names(mesh: number): boolean {
266
+ return this.levelMeshes.has(mesh) || this.prototypes.has(mesh);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Where each region's chunk goes among the meshes: `before[m]` is the regions written just ahead of
272
+ * mesh `m`, and `before[meshCount]` those after the last. A streaming reader then always has a
273
+ * region before the meshes it introduces.
274
+ *
275
+ * The meshes no region introduces come first, then each region's new meshes, in the order the
276
+ * regions are given and ascending. **A region introduces the kit pieces its assemblies copy, where
277
+ * no earlier region did** (`piecesOf` names them), so a piece travels with the first region that
278
+ * needs it rather than at the head of the file: a world's kit is most of its bytes, and a walker's
279
+ * first regions need a sliver of it. A piece ahead of the first mesh any region names is the
280
+ * file's own, as every piece was before a region could carry one, so either layout is valid. That
281
+ * a piece comes ahead of every assembly copying it is the kit's own rule (`checkCopies`). Anything
282
+ * else is refused by name rather than reordered, because the ordinals are what the regions and the
283
+ * materials already point at.
284
+ */
285
+ export function planRegions(
286
+ regions: readonly DrftRegion[],
287
+ meshCount: number,
288
+ piecesOf: (mesh: number) => ArrayLike<number> | undefined = () => undefined,
289
+ ): DrftRegion[][] {
290
+ const ledger = new RegionLedger();
291
+ for (const region of regions) {
292
+ checkRegion(region, meshCount);
293
+ ledger.admit(region);
294
+ }
295
+ /* What each region introduces: its levels' meshes and its props, then the pieces its level
296
+ meshes copy that nobody introduced before it — those past the first mesh any region names;
297
+ a piece ahead of it is the file's own, written first as every piece once was. */
298
+ let firstNamed = meshCount;
299
+ for (let m = 0; m < meshCount; m++) {
300
+ if (ledger.names(m)) {
301
+ firstNamed = m;
302
+ break;
303
+ }
304
+ }
305
+ const introduced = new Set<number>();
306
+ const fresh = regions.map((region) => {
307
+ const out: number[] = [];
308
+ const take = (mesh: number): void => {
309
+ if (introduced.has(mesh)) return;
310
+ introduced.add(mesh);
311
+ out.push(mesh);
312
+ };
313
+ for (const level of region.levels) for (const mesh of level.meshes) take(mesh);
314
+ for (const group of region.instances) take(group.mesh);
315
+ for (const level of region.levels) {
316
+ for (const mesh of level.meshes) {
317
+ const pieces = piecesOf(mesh);
318
+ for (let i = 0; i < (pieces?.length ?? 0); i++) {
319
+ const piece = pieces?.[i] as number;
320
+ if (piece > firstNamed && !ledger.names(piece)) take(piece);
321
+ }
322
+ }
323
+ }
324
+ return out.sort((a, b) => a - b);
325
+ });
326
+ let next = meshCount - introduced.size;
327
+ for (let m = 0; m < next; m++) {
328
+ if (introduced.has(m)) {
329
+ throw new DrftError(
330
+ `mesh ${m} is a region's and comes before mesh ${next - 1}, which no region names; ` +
331
+ 'a file with regions writes the meshes no region names first',
332
+ );
333
+ }
334
+ }
335
+ const before: DrftRegion[][] = [];
336
+ for (let m = 0; m <= meshCount; m++) before.push([]);
337
+ regions.forEach((region, r) => {
338
+ (before[next] as DrftRegion[]).push(region);
339
+ for (const mesh of fresh[r] as number[]) {
340
+ if (mesh !== next) {
341
+ throw new DrftError(
342
+ `region ${region.id} introduces mesh ${mesh} where mesh ${next} is next; ` +
343
+ "write each region's new meshes in the order the regions are written",
344
+ );
345
+ }
346
+ next++;
347
+ }
348
+ });
349
+ return before;
350
+ }