@driftengine/assets 4.5.0 → 4.6.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Model readers for seven formats, and the streaming loader that uploads them.
4
4
 
5
- **Cost: 15.9 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
5
+ **Cost: 22.5 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
7
7
 
8
8
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
@@ -7,7 +7,9 @@ import type { DrftFieldPlacement } from './fieldPlacement.ts';
7
7
  import { TextureSet } from './drftTextures.ts';
8
8
  import type { DrawSurfaceOverride } from './drawKey.ts';
9
9
  import type { AnimationClip, DrftLight, DrftMorph, DrftNode, DrftSkin } from '@driftengine/drft';
10
+ import type { DrftLightVolume, EntsScene, NavPolyMesh } from '@driftengine/drft';
10
11
  import type { DrftLoadProgress, DrftPart } from './loadProgress.ts';
12
+ import type { LoadedRegion } from './regionStore.ts';
11
13
  /**
12
14
  * How a loaded model is placed: fitted to a size, or left exactly where its file puts it.
13
15
  *
@@ -152,6 +154,23 @@ export interface DrftLoaderOptions {
152
154
  * of the file — which is why it is an option a caller weighs rather than a default.
153
155
  */
154
156
  readonly texturePreview?: number;
157
+ /**
158
+ * Take each image as it decodes, by the name its `TEXS` chunk gives it, and upload none: for a
159
+ * consumer that builds its own textures — a world packing every picture into a layer of an
160
+ * array — where a surface texture of each as well would spend the memory twice. On the same
161
+ * frame clock as an upload, one a frame; `null` for an image that did not decode. Materials in
162
+ * the file that name an image then draw untextured, which is the consumer's to answer.
163
+ */
164
+ readonly onImage?: (name: string, image: ImageBitmap | null) => void;
165
+ /**
166
+ * Offered each mesh no region holds as it arrives, by its ordinal in the file; return true to
167
+ * take it, and it is neither uploaded as a part nor merged. For a consumer that draws some of a
168
+ * file's meshes its own way — a crowd of kinds it instances and moves every frame — which a part
169
+ * cannot be, since the merge bakes a part into a static group by the image it wears. Counted as
170
+ * arrived, so a loading bar still finishes. What it gives up: the loader's upload budget and
171
+ * fade, which a taken mesh leaves to the consumer.
172
+ */
173
+ readonly onMesh?: (mesh: MeshData, ordinal: number) => boolean;
155
174
  }
156
175
  /**
157
176
  * A `.drft` loaded progressively: the model builds up on screen instead of appearing.
@@ -185,6 +204,11 @@ export declare class DrftLoader {
185
204
  private readonly revealed;
186
205
  /** Arrived, not yet uploaded. Drained by `update` at the budget. */
187
206
  private readonly queue;
207
+ /**
208
+ * Paged region meshes asked for and not yet up, drained after `queue` on the same clock: each is
209
+ * an expansion from the copies the file carried, then an upload.
210
+ */
211
+ private pageQueue;
188
212
  /** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
189
213
  private readonly loadedInstances;
190
214
  /**
@@ -253,6 +277,10 @@ export declare class DrftLoader {
253
277
  */
254
278
  private loadedNodes;
255
279
  private loadedLights;
280
+ private loadedColliders;
281
+ private loadedNavigation;
282
+ private loadedEntities;
283
+ private loadedLightVolume;
256
284
  private loadedFields;
257
285
  /** The fields placed, built once the fit and the fields are both known. See `fields`. */
258
286
  private placedFields;
@@ -276,6 +304,8 @@ export declare class DrftLoader {
276
304
  private merged;
277
305
  private disposed;
278
306
  private arrived;
307
+ /** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
308
+ private readonly regionStore;
279
309
  constructor(renderer: RendererApi, options?: DrftLoaderOptions);
280
310
  get progress(): DrftLoadProgress;
281
311
  /**
@@ -306,6 +336,41 @@ export declare class DrftLoader {
306
336
  * geometry. What a candela is in this scene's light units is the caller's decision.
307
337
  */
308
338
  get lights(): readonly DrftLight[];
339
+ /**
340
+ * The convex hulls the asset collides as, in the file's own space and unfitted, or empty. Handed
341
+ * on for the caller to build shapes from, as `hullShape` takes them.
342
+ */
343
+ get colliders(): readonly Float32Array[];
344
+ /** The asset's navigation mesh, unfitted, or null for a file carrying no `NAVM`. */
345
+ get navigation(): NavPolyMesh | null;
346
+ /**
347
+ * The asset's serialised entities, or null for a file carrying no `ENTS` — handed on as
348
+ * `deserializeWorld` takes them, because what an entity is belongs to the caller's schemas.
349
+ */
350
+ get entities(): EntsScene | null;
351
+ /**
352
+ * A streamed world's regions by id, each filling as its meshes and props upload — `pending` is 0
353
+ * once one is whole. Their meshes are never among `parts`: a region's levels are alternatives,
354
+ * drawn one at a time through `HlodSet`, and its batches cull instance by instance.
355
+ */
356
+ get regions(): ReadonlyMap<number, LoadedRegion>;
357
+ /** One mesh to draw, from the stream or expanded from an assembly, queued for upload. */
358
+ private take;
359
+ /**
360
+ * Bring a region's paged level up, or free it. A level that arrived as assemblies (`paged`) is
361
+ * held as the copies the file carried; asked for, each mesh is expanded and uploaded on the
362
+ * loader's clocked queue in later `update`s, and `resident` turns true when the last is up. Freed,
363
+ * its meshes go and the copies stay. A level that is not paged ignores this.
364
+ *
365
+ * The caller decides from what it will draw: page in the levels `HlodSet` is choosing or about
366
+ * to, draw a coarser one until the fine one is resident, and page out what the eye has left.
367
+ */
368
+ pageRegion(id: number, level: number, resident: boolean): void;
369
+ /**
370
+ * A world's summed lights, unfitted, or null until `LVOL` lands and for a file carrying none.
371
+ * Handed straight to `createWorldLightField`, whose volume it is shaped as.
372
+ */
373
+ get lightVolume(): DrftLightVolume | null;
309
374
  /**
310
375
  * The distance fields the file carries, each where it stands: at the loader's fit, and once a
311
376
  * copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
@@ -422,10 +487,9 @@ export declare class DrftLoader {
422
487
  /**
423
488
  * A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
424
489
  *
425
- * **The fit is conjugated into each placement.** The prototype is uploaded with the fit already
426
- * applied, as every part is, so a copy's matrix M becomes F·M·F⁻¹: the same turn, and a translation
427
- * of t − R·t + s·u for the fit's scale s and offset t and the copy's own translation u. A fitted
428
- * model's copies then land where its geometry does, and are not scaled twice.
490
+ * **The fit is conjugated into each placement** by `fitCopies`, which a region's props share: the
491
+ * prototype is uploaded with the fit already applied, so the copies land where its geometry does
492
+ * and are not scaled twice.
429
493
  */
430
494
  private uploadInstanced;
431
495
  private buildGroup;
@@ -1,9 +1,10 @@
1
1
  /** Loading a `.drft` into drawable parts progressively, without stalling the frame. */
2
- import { MeshBuilder, concatMeshes, createMeshInstances, placeMesh } from '@driftengine/core';
2
+ import { MeshBuilder, concatMeshes, placeMesh } from '@driftengine/core';
3
3
  import { placeFields } from './fieldPlacement.js';
4
4
  import { TextureSet, textureColorSpaces } from './drftTextures.js';
5
5
  import { drawKeyOf, resolveDrawGrouping } from './drawKey.js';
6
- import { streamDrft, CODEC_JPEG, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError, } from '@driftengine/drft';
6
+ import { streamDrft, CODEC_JPEG, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError, expandAssembly, } from '@driftengine/drft';
7
+ import { RegionStore, fitCopies } from './regionStore.js';
7
8
  import { DEFAULT_REVEAL_SEC, DEFAULT_UPLOAD_MS_PER_FRAME, DEFAULT_UPLOADS_PER_FRAME, isDocumentResponse, mayBeginMore, } from './uploadBudget.js';
8
9
  /**
9
10
  * A `.drft` loaded progressively: the model builds up on screen instead of appearing.
@@ -37,6 +38,11 @@ export class DrftLoader {
37
38
  revealed = [];
38
39
  /** Arrived, not yet uploaded. Drained by `update` at the budget. */
39
40
  queue = [];
41
+ /**
42
+ * Paged region meshes asked for and not yet up, drained after `queue` on the same clock: each is
43
+ * an expansion from the copies the file carried, then an upload.
44
+ */
45
+ pageQueue = [];
40
46
  /** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
41
47
  loadedInstances = new Map();
42
48
  /**
@@ -105,6 +111,10 @@ export class DrftLoader {
105
111
  */
106
112
  loadedNodes = [];
107
113
  loadedLights = [];
114
+ loadedColliders = [];
115
+ loadedNavigation = null;
116
+ loadedEntities = null;
117
+ loadedLightVolume = null;
108
118
  loadedFields = [];
109
119
  /** The fields placed, built once the fit and the fields are both known. See `fields`. */
110
120
  placedFields = null;
@@ -137,9 +147,12 @@ export class DrftLoader {
137
147
  merged = false;
138
148
  disposed = false;
139
149
  arrived = 0;
150
+ /** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
151
+ regionStore;
140
152
  constructor(renderer, options = {}) {
141
153
  this.renderer = renderer;
142
154
  this.options = options;
155
+ this.regionStore = new RegionStore(renderer, () => this.fit);
143
156
  }
144
157
  get progress() {
145
158
  return this.state;
@@ -178,6 +191,91 @@ export class DrftLoader {
178
191
  get lights() {
179
192
  return this.loadedLights;
180
193
  }
194
+ /**
195
+ * The convex hulls the asset collides as, in the file's own space and unfitted, or empty. Handed
196
+ * on for the caller to build shapes from, as `hullShape` takes them.
197
+ */
198
+ get colliders() {
199
+ return this.loadedColliders;
200
+ }
201
+ /** The asset's navigation mesh, unfitted, or null for a file carrying no `NAVM`. */
202
+ get navigation() {
203
+ return this.loadedNavigation;
204
+ }
205
+ /**
206
+ * The asset's serialised entities, or null for a file carrying no `ENTS` — handed on as
207
+ * `deserializeWorld` takes them, because what an entity is belongs to the caller's schemas.
208
+ */
209
+ get entities() {
210
+ return this.loadedEntities;
211
+ }
212
+ /**
213
+ * A streamed world's regions by id, each filling as its meshes and props upload — `pending` is 0
214
+ * once one is whole. Their meshes are never among `parts`: a region's levels are alternatives,
215
+ * drawn one at a time through `HlodSet`, and its batches cull instance by instance.
216
+ */
217
+ get regions() {
218
+ return this.regionStore.regions;
219
+ }
220
+ /** One mesh to draw, from the stream or expanded from an assembly, queued for upload. */
221
+ take(mesh, ordinal) {
222
+ if (this.disposed)
223
+ return;
224
+ /* A region's own meshes are known by now: a region is written ahead of what it introduces. */
225
+ if (!this.regionStore.owns(ordinal) && this.options.onMesh?.(mesh, ordinal) === true) {
226
+ this.arrived++;
227
+ this.set({ phase: 'geometry', partsDone: this.arrived });
228
+ return;
229
+ }
230
+ const material = this.materials[ordinal];
231
+ /*
232
+ * Deltas onto the mesh they name, before it is queued for upload. The container writes a
233
+ * `MORP` immediately ahead of its `MESH` so this is in hand; a file that puts them the
234
+ * other way round still opens, and that mesh simply does not morph — a defined
235
+ * degradation rather than a wrong picture.
236
+ */
237
+ const morph = this.loadedMorphs.find((entry) => entry.mesh === ordinal);
238
+ const withMorph = morph === undefined
239
+ ? mesh
240
+ : { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
241
+ this.queue.push({
242
+ mesh: withMorph,
243
+ ordinal,
244
+ material,
245
+ instances: this.loadedInstances.get(ordinal) ?? null,
246
+ });
247
+ this.arrived++;
248
+ this.set({ phase: 'geometry', partsDone: this.arrived });
249
+ }
250
+ /**
251
+ * Bring a region's paged level up, or free it. A level that arrived as assemblies (`paged`) is
252
+ * held as the copies the file carried; asked for, each mesh is expanded and uploaded on the
253
+ * loader's clocked queue in later `update`s, and `resident` turns true when the last is up. Freed,
254
+ * its meshes go and the copies stay. A level that is not paged ignores this.
255
+ *
256
+ * The caller decides from what it will draw: page in the levels `HlodSet` is choosing or about
257
+ * to, draw a coarser one until the fine one is resident, and page out what the eye has left.
258
+ */
259
+ pageRegion(id, level, resident) {
260
+ const changes = this.regionStore.page(id, level, resident);
261
+ if (resident) {
262
+ for (const { ordinal, expand } of changes) {
263
+ this.pageQueue.push({ ordinal, material: this.materials[ordinal], expand });
264
+ }
265
+ return;
266
+ }
267
+ if (changes.length === 0)
268
+ return;
269
+ const released = new Set(changes.map((c) => c.ordinal));
270
+ this.pageQueue = this.pageQueue.filter((item) => !released.has(item.ordinal));
271
+ }
272
+ /**
273
+ * A world's summed lights, unfitted, or null until `LVOL` lands and for a file carrying none.
274
+ * Handed straight to `createWorldLightField`, whose volume it is shaped as.
275
+ */
276
+ get lightVolume() {
277
+ return this.loadedLightVolume;
278
+ }
181
279
  /**
182
280
  * The distance fields the file carries, each where it stands: at the loader's fit, and once a
183
281
  * copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
@@ -349,6 +447,15 @@ export class DrftLoader {
349
447
  onLights: (lights) => {
350
448
  this.loadedLights = lights;
351
449
  },
450
+ onColliders: (hulls) => {
451
+ this.loadedColliders = hulls;
452
+ },
453
+ onNavigation: (navigation) => {
454
+ this.loadedNavigation = navigation;
455
+ },
456
+ onEntities: (entities) => {
457
+ this.loadedEntities = entities;
458
+ },
352
459
  onFields: (fields) => {
353
460
  this.loadedFields = fields;
354
461
  this.placedFields = null;
@@ -357,6 +464,14 @@ export class DrftLoader {
357
464
  for (const group of groups)
358
465
  this.loadedInstances.set(group.mesh, group.transforms);
359
466
  },
467
+ onLightVolume: (volume) => {
468
+ this.loadedLightVolume = volume;
469
+ },
470
+ onRegion: (region) => {
471
+ if (this.disposed)
472
+ return;
473
+ this.regionStore.admit(region);
474
+ },
360
475
  onSkin: (skin) => {
361
476
  this.loadedSkins.push(skin);
362
477
  },
@@ -422,27 +537,23 @@ export class DrftLoader {
422
537
  this.materials = list;
423
538
  this.set({ phase: 'materials' });
424
539
  },
425
- onMesh: (mesh, ordinal) => {
540
+ onKit: (pieces) => {
541
+ /* A piece is drawn only as copies: it is not a part, and the count said it was. */
542
+ this.set({ partsTotal: Math.max(0, this.state.partsTotal - pieces.length) });
543
+ },
544
+ onAssembly: (assembly, ordinal, piece) => {
426
545
  if (this.disposed)
427
546
  return;
428
- const material = this.materials[ordinal];
429
- /*
430
- * Deltas onto the mesh they name, before it is queued for upload. The container writes a
431
- * `MORP` immediately ahead of its `MESH` so this is in hand; a file that puts them the
432
- * other way round still opens, and that mesh simply does not morph — a defined
433
- * degradation rather than a wrong picture.
434
- */
435
- const morph = this.loadedMorphs.find((entry) => entry.mesh === ordinal);
436
- const withMorph = morph === undefined
437
- ? mesh
438
- : { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
439
- this.queue.push({
440
- mesh: withMorph,
441
- material,
442
- instances: this.loadedInstances.get(ordinal) ?? null,
443
- });
444
- this.arrived++;
445
- this.set({ phase: 'geometry', partsDone: this.arrived });
547
+ if (this.regionStore.isLevelMesh(ordinal)) {
548
+ this.regionStore.hold(ordinal, assembly, piece);
549
+ this.arrived++;
550
+ this.set({ phase: 'geometry', partsDone: this.arrived });
551
+ return;
552
+ }
553
+ this.take(expandAssembly(assembly, piece), ordinal);
554
+ },
555
+ onMesh: (mesh, ordinal) => {
556
+ this.take(mesh, ordinal);
446
557
  },
447
558
  onTexture: (texture, ordinal) => {
448
559
  if (this.disposed)
@@ -572,10 +683,28 @@ export class DrftLoader {
572
683
  const next = this.queue.shift();
573
684
  if (next === undefined)
574
685
  break;
575
- if (this.uploadOne(next.mesh, next.material, next.instances))
686
+ if (this.uploadOne(next.mesh, next.material, next.instances, next.ordinal))
576
687
  changed = true;
577
688
  begun++;
578
689
  }
690
+ /* Paged region meshes, after the stream's own: an expansion from the copies, then an upload. */
691
+ while (this.pageQueue.length > 0 &&
692
+ mayBeginMore(begun, performance.now() - startedMs, msBudget)) {
693
+ const next = this.pageQueue.shift();
694
+ if (next === undefined)
695
+ break;
696
+ if (this.uploadOne(next.expand(), next.material, null, next.ordinal))
697
+ changed = true;
698
+ begun++;
699
+ }
700
+ /* A region's prop batches, on the same clock: each is an allocation and an upload. Drained
701
+ before the merge below can begin, which is what lets `ready` not wait on them separately. */
702
+ while (this.regionStore.busy &&
703
+ mayBeginMore(begun, performance.now() - startedMs, msBudget) &&
704
+ this.regionStore.buildNext()) {
705
+ changed = true;
706
+ begun++;
707
+ }
579
708
  /* One image a frame, and only into a frame that has room for it. A 2048 square decodes
580
709
  off-thread and then uploads with its mips, which is the single most expensive thing in a
581
710
  load and lands right after the geometry — on top of the parts, until this was clocked. */
@@ -583,7 +712,11 @@ export class DrftLoader {
583
712
  mayBeginMore(begun, performance.now() - startedMs, msBudget)) {
584
713
  const ordinal = this.imageQueue.shift();
585
714
  if (ordinal !== undefined) {
586
- this.rebuildTextures(ordinal);
715
+ const take = this.options.onImage;
716
+ if (take === undefined)
717
+ this.rebuildTextures(ordinal);
718
+ else
719
+ take(this.chunks[ordinal]?.name ?? '', this.images[ordinal] ?? null);
587
720
  /*
588
721
  * Counted here, once, and only for the image itself.
589
722
  *
@@ -655,6 +788,7 @@ export class DrftLoader {
655
788
  if (this.disposed)
656
789
  return;
657
790
  this.disposed = true;
791
+ this.regionStore.dispose();
658
792
  /*
659
793
  * Everything this class ever made, each disposed once.
660
794
  *
@@ -760,7 +894,7 @@ export class DrftLoader {
760
894
  return true;
761
895
  }
762
896
  /** One part, transformed, dressed and uploaded. Returns whether it landed. */
763
- uploadOne(mesh, material, instances) {
897
+ uploadOne(mesh, material, instances, ordinal) {
764
898
  const fit = this.fit;
765
899
  if (fit === null)
766
900
  return false;
@@ -779,6 +913,37 @@ export class DrftLoader {
779
913
  */
780
914
  const g = resolveDrawGrouping(material, override);
781
915
  const { albedo, orm, normal, emissive, opacity, reflectivity, roughnessScale, metallicScale, occlusionStrength, cutout, blend, doubleSided, } = g;
916
+ /*
917
+ * A region's mesh — a level or a prop's prototype — is one part of its own, never grouped: the
918
+ * merge would weld a region's levels together and one region to the next. See `regionStore.ts`.
919
+ */
920
+ if (this.regionStore.owns(ordinal)) {
921
+ try {
922
+ this.regionStore.arrived(ordinal, {
923
+ mesh: this.renderer.createMesh(placed),
924
+ albedo,
925
+ orm,
926
+ normal,
927
+ emissive,
928
+ opacity,
929
+ reflectivity,
930
+ roughnessScale,
931
+ metallicScale,
932
+ occlusionStrength,
933
+ cutout,
934
+ blend,
935
+ doubleSided,
936
+ glass: glassOf(g),
937
+ reveal: 1,
938
+ instances: null,
939
+ });
940
+ return true;
941
+ }
942
+ catch (error) {
943
+ console.warn(`DrftLoader: region mesh ${ordinal} would not upload and was skipped`, error);
944
+ return false;
945
+ }
946
+ }
782
947
  /*
783
948
  * Added to its merged group here, while there is a budget governing how much of this happens
784
949
  * in one frame.
@@ -883,42 +1048,15 @@ export class DrftLoader {
883
1048
  /**
884
1049
  * A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
885
1050
  *
886
- * **The fit is conjugated into each placement.** The prototype is uploaded with the fit already
887
- * applied, as every part is, so a copy's matrix M becomes F·M·F⁻¹: the same turn, and a translation
888
- * of t − R·t + s·u for the fit's scale s and offset t and the copy's own translation u. A fitted
889
- * model's copies then land where its geometry does, and are not scaled twice.
1051
+ * **The fit is conjugated into each placement** by `fitCopies`, which a region's props share: the
1052
+ * prototype is uploaded with the fit already applied, so the copies land where its geometry does
1053
+ * and are not scaled twice.
890
1054
  */
891
1055
  uploadInstanced(prototype, instances, fit, surface) {
892
1056
  try {
893
1057
  const uploaded = this.renderer.createMesh(prototype);
894
- const count = instances.length / 16;
895
- const batch = this.renderer.createInstanced(uploaded, count);
896
- const data = createMeshInstances(count);
897
- const s = fit.scale;
898
- for (let k = 0; k < count; k++) {
899
- const at = k * 16;
900
- for (let i = 0; i < 12; i++)
901
- data.models[at + i] = instances[at + i];
902
- const ux = instances[at + 12];
903
- const uy = instances[at + 13];
904
- const uz = instances[at + 14];
905
- /* R·t, with R the copy's own 3×3 read column-major. */
906
- const rtx = instances[at] * fit.x +
907
- instances[at + 4] * fit.y +
908
- instances[at + 8] * fit.z;
909
- const rty = instances[at + 1] * fit.x +
910
- instances[at + 5] * fit.y +
911
- instances[at + 9] * fit.z;
912
- const rtz = instances[at + 2] * fit.x +
913
- instances[at + 6] * fit.y +
914
- instances[at + 10] * fit.z;
915
- data.models[at + 12] = fit.x - rtx + s * ux;
916
- data.models[at + 13] = fit.y - rty + s * uy;
917
- data.models[at + 14] = fit.z - rtz + s * uz;
918
- data.models[at + 15] = 1;
919
- }
920
- data.tints.fill(1);
921
- data.count = count;
1058
+ const data = fitCopies(instances, fit);
1059
+ const batch = this.renderer.createInstanced(uploaded, data.count);
922
1060
  this.renderer.uploadInstanced(batch, data);
923
1061
  const part = { mesh: uploaded, ...surface, reveal: 0, instances: { batch, data } };
924
1062
  this.revealed.push(part);
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
13
13
  export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
14
14
  export type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
15
15
  export type { DrftLoadPhase, DrftLoadProgress, DrftPart } from './loadProgress.ts';
16
+ export type { LoadedRegion, LoadedRegionLevel } from './regionStore.ts';
16
17
  export type { DrftFieldPlacement } from './fieldPlacement.ts';
17
18
  export { TextureSet, textureColorSpaces } from './drftTextures.ts';
18
19
  export { assetCandidates, basenameOf } from './assetPath.ts';
@@ -0,0 +1,94 @@
1
+ import type { MeshInstances, RendererApi } from '@driftengine/core';
2
+ import type { DrftAssembly, DrftRegion, DrftRegionCollision, PieceLookup } from '@driftengine/drft';
3
+ import type { MeshData } from '@driftengine/drft';
4
+ import type { DrftPart } from './loadProgress.ts';
5
+ /** How the loader placed the model: `p · scale + (x, y, z)`. */
6
+ export interface LoaderFit {
7
+ readonly scale: number;
8
+ readonly x: number;
9
+ readonly y: number;
10
+ readonly z: number;
11
+ }
12
+ export interface LoadedRegionLevel {
13
+ /** How far, in metres of the file, this level may stand from the real surface. */
14
+ readonly error: number;
15
+ /** One part a mesh, filled as they upload. */
16
+ readonly parts: DrftPart[];
17
+ /** Whether any of its meshes arrived as assemblies, kept small until paged in. */
18
+ paged: boolean;
19
+ /** Whether every mesh is up: always, for a level that is not paged. */
20
+ resident: boolean;
21
+ }
22
+ export interface LoadedRegion {
23
+ readonly id: number;
24
+ /** Min xyz then max xyz, placed. */
25
+ readonly bounds: Float32Array;
26
+ /** Finest first. `HlodSet.add` takes the errors, scaled by the fit. */
27
+ readonly levels: readonly LoadedRegionLevel[];
28
+ /** Its props, one instanced part a prototype, culled per instance. */
29
+ readonly batches: DrftPart[];
30
+ /**
31
+ * Which of the region's groups in the file each batch is, index for index. Batches are built as
32
+ * their prototypes arrive, not in the file's order, so a caller pairing something of its own with
33
+ * each group — a material, a blend — pairs through this, never by position.
34
+ */
35
+ readonly batchGroups: number[];
36
+ /** Six floats a box, placed, for `addOccluder`. */
37
+ readonly occluders: Float32Array;
38
+ /** What it collides as, placed, or null. */
39
+ readonly collision: DrftRegionCollision | null;
40
+ /** Meshes and batches still to come; 0 once every level and prop is up. */
41
+ pending: number;
42
+ }
43
+ /**
44
+ * Copies of a fitted prototype, each matrix conjugated by the fit: M becomes F·M·F⁻¹, the same turn
45
+ * and a translation of t − R·t + s·u for the fit's scale s and offset t and the copy's own
46
+ * translation u. The prototype already carries the fit, so the copies land where its geometry does
47
+ * and are not scaled twice.
48
+ */
49
+ export declare function fitCopies(transforms: Float32Array, fit: LoaderFit): MeshInstances;
50
+ export declare class RegionStore {
51
+ private readonly renderer;
52
+ private readonly fit;
53
+ readonly regions: Map<number, LoadedRegion>;
54
+ /** Level meshes by ordinal: whose, and which level. */
55
+ private readonly levelOwners;
56
+ /** Prototypes that have uploaded, by ordinal. */
57
+ private readonly prototypes;
58
+ /** Groups whose prototype has not uploaded yet, by its ordinal. */
59
+ private readonly waiting;
60
+ private readonly jobs;
61
+ private readonly made;
62
+ /** Assemblies held small, by ordinal, and each paged level's ordinals. */
63
+ private readonly compact;
64
+ private readonly levelCompact;
65
+ /** Paged meshes asked for and not yet up, and those that are. */
66
+ private readonly requested;
67
+ private readonly pagedParts;
68
+ constructor(renderer: RendererApi, fit: () => LoaderFit | null);
69
+ /** Whether a mesh is a region's, as a level or a prop, and so is not a part of the model. */
70
+ owns(ordinal: number): boolean;
71
+ /** Whether batches are queued. */
72
+ get busy(): boolean;
73
+ /** A region's chunk has landed, ahead of the meshes it introduces. */
74
+ admit(region: DrftRegion): void;
75
+ /** Whether a mesh is one of a region's levels. */
76
+ isLevelMesh(ordinal: number): boolean;
77
+ /** A level's assembly has arrived: held small, its level paged and not yet up. */
78
+ hold(ordinal: number, assembly: DrftAssembly, piece: PieceLookup): void;
79
+ /**
80
+ * Ask for a paged level's meshes, or free them. Coming up, returns each mesh still to upload with
81
+ * a way to expand it, for the loader's queue; going down, frees what is up and returns the
82
+ * ordinals released, so the loader can drop any still queued.
83
+ */
84
+ page(id: number, level: number, resident: boolean): {
85
+ ordinal: number;
86
+ expand: () => MeshData;
87
+ }[];
88
+ private release;
89
+ /** A region's mesh has uploaded, as the part the loader would have made of it. */
90
+ arrived(ordinal: number, part: DrftPart): void;
91
+ /** Build one queued batch inside the caller's budget; false when none was queued. */
92
+ buildNext(): boolean;
93
+ dispose(): void;
94
+ }