@threenative/assets 0.3.2 → 0.3.5

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/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Document, Material } from '@gltf-transform/core';
2
+
1
3
  /**
2
4
  * Encodes compiled textures to KTX2/Basis so the GPU stores them compressed instead of as
3
5
  * decoded RGBA (a 2048x2048 PNG is 16 MB in VRAM; BC7/ASTC hold it in a quarter of that).
@@ -46,6 +48,20 @@ interface IAssetBudget {
46
48
  readonly uncooked: number | "none";
47
49
  readonly total: number | "none";
48
50
  }
51
+ /** Runtime decoder capabilities that decide which emitted bytes can be compressed on-device. */
52
+ interface IAssetRuntimeDecoderCapabilities {
53
+ readonly ktx2: boolean;
54
+ readonly meshopt: boolean;
55
+ }
56
+
57
+ interface IModelDedupeSummary {
58
+ /** Distinct cooked outputs among the model sources that took part. */
59
+ readonly cooked: number;
60
+ /** Cooked bytes the duplicates would have written on their own. */
61
+ readonly savedBytes: number;
62
+ /** Model sources that took part, canonicals and duplicates alike. */
63
+ readonly sources: number;
64
+ }
49
65
 
50
66
  type AssetFindingGrade = "fail" | "ok" | "warn";
51
67
  interface IAssetFinding {
@@ -99,6 +115,89 @@ declare function runHealthReport(inputs: readonly IAssetHealthInput[], targets?:
99
115
  /** The person-readable form printed by the compile step, one line per finding plus a summary. */
100
116
  declare function formatHealthReport(report: IAssetHealthReport): readonly string[];
101
117
 
118
+ /**
119
+ * What the `minTriangles` pre-filter is measured against.
120
+ *
121
+ * `"primitive"` compares each primitive on its own; `"asset"` compares the whole source asset's
122
+ * total. The asset total is the evidence-backed default: a carrier can be 347,497 triangles split
123
+ * over 300 primitives of ~1,200, where an absolute per-primitive floor measures how the artist
124
+ * happened to split the mesh rather than whether simplification would pay. Whichever is chosen, the
125
+ * floor is only a cheap pre-filter — the measured saving rule below is the gate.
126
+ */
127
+ type LodMinTrianglesScope = "primitive" | "asset";
128
+
129
+ /** Generation knobs, shared by every preset (PRD-377 §3.2); both are ceilings, not promises. */
130
+ interface IModelLodGenerationOptions {
131
+ /**
132
+ * Versioned increasing geometric-error targets, in normalized mesh-extent units. Each target is
133
+ * simplified from LOD0 independently; a target that cannot reduce is dropped. Default
134
+ * {@link LOD_ERROR_TARGETS}.
135
+ */
136
+ readonly errorTargets?: readonly number[];
137
+ /** Levels in the chain including LOD0; integer 1–8. Default {@link DEFAULT_LOD_MAX_LEVELS}. */
138
+ readonly maxLevels?: number;
139
+ /**
140
+ * Fraction of its predecessor's triangles a derived level must save to be kept. Default
141
+ * {@link LOD_MIN_SAVING}. This is the benefit gate: an inability to reach it is a normal skip.
142
+ */
143
+ readonly minSaving?: number;
144
+ /**
145
+ * Cheap pre-filter floor in triangles. It is not the benefit gate — {@link minSaving} is — and
146
+ * only skips work where the simplifier's fixed per-call cost would dominate. Default
147
+ * {@link DEFAULT_LOD_MIN_TRIANGLES}.
148
+ */
149
+ readonly minTriangles?: number;
150
+ /**
151
+ * What {@link minTriangles} is measured against. Default
152
+ * {@link DEFAULT_LOD_MIN_TRIANGLES_SCOPE}, which floors the whole asset rather than each
153
+ * primitive, so a model split into many small primitives is still measured by its total.
154
+ */
155
+ readonly minTrianglesScope?: LodMinTrianglesScope;
156
+ /**
157
+ * Opt-in far rung that joins a mesh's same-material primitives into one draw per material group
158
+ * (PRD-377 §4.4 extension). **Default `false`: with no option the cook is byte-identical to
159
+ * today.** A join never crosses a material, never touches a skinned, morph-target or animated
160
+ * node, and never leaves the mesh's own node, so authored LOD0, node identity and picking are
161
+ * untouched; the joined geometry is an additional far rung beside LOD0.
162
+ */
163
+ readonly join?: boolean;
164
+ }
165
+ /** Quality policy for automatic LOD; it selects the projected pixel-error budget (PRD-377 §3.2). */
166
+ type LodPreset = "aggressive" | "balanced" | "quality";
167
+ /** Screen-space selection knobs; they change what the runtime picks, never what is baked. */
168
+ interface IModelLodRuntimeOptions {
169
+ /** Projected geometric-error budget in raster pixels; positive finite. */
170
+ readonly maxPixelError?: number;
171
+ /** Fraction in `[0, 0.5)`, default 0.15, that stabilizes coarsening at a boundary. */
172
+ readonly hysteresis?: number;
173
+ }
174
+ /** A partial override for one asset; nested objects overlay, they never replace. */
175
+ interface IModelLodOverride {
176
+ readonly cutout?: boolean;
177
+ readonly enabled?: boolean;
178
+ readonly generation?: IModelLodGenerationOptions;
179
+ readonly preset?: LodPreset;
180
+ readonly runtime?: IModelLodRuntimeOptions;
181
+ }
182
+ /**
183
+ * `assets.lod`, as the compile step reads it. Absent or `{}` resolves to enabled/balanced; `false`
184
+ * and `{ enabled: false }` are equivalent absolute kill switches that no per-asset override can
185
+ * re-enable. Resolution is per asset and happens where the asset is known (PRD-377 §3.2, §5).
186
+ */
187
+ interface IModelLodOptions {
188
+ /**
189
+ * Convert eligible `BLEND` foliage to alpha-tested in the cook (PRD-458 §4). Default `true`,
190
+ * because a needle card exported as `BLEND` is refused a chain by rule and the tree then draws
191
+ * LOD0 forever. `false` keeps the authored blending; per asset, `cutout` in an override entry.
192
+ */
193
+ readonly cutout?: boolean;
194
+ readonly enabled?: boolean;
195
+ readonly generation?: IModelLodGenerationOptions;
196
+ readonly overrides?: Readonly<Record<string, boolean | IModelLodOverride>>;
197
+ readonly preset?: LodPreset;
198
+ readonly runtime?: IModelLodRuntimeOptions;
199
+ }
200
+
102
201
  /**
103
202
  * The five bands a clip's content is judged in, contiguous and gapless up to Nyquist.
104
203
  *
@@ -192,6 +291,74 @@ interface IAudioPassOptions {
192
291
  /** `"none"` drops the pass; an object configures it; absent means the defaults run. */
193
292
  declare function parseAudioConfig(raw: unknown): IAudioPassOptions | undefined;
194
293
 
294
+ interface IModelCompactInstanceOptions {
295
+ /**
296
+ * Minimum number of nodes that must share one mesh before it is instanced, default 2.
297
+ * gltf-transform's own default is 5; the lower floor is what lets a paired prop instance.
298
+ */
299
+ readonly min?: number;
300
+ }
301
+ interface IModelCompactOptions {
302
+ readonly flatten?: boolean;
303
+ readonly instance?: boolean | IModelCompactInstanceOptions;
304
+ readonly join?: boolean;
305
+ /** Extra exact node names to protect, on top of the regex and the structural rules. */
306
+ readonly protectedNames?: readonly string[];
307
+ /**
308
+ * Case-insensitive node-name regex source for nodes the game looks up or moves. Defaults to
309
+ * {@link DEFAULT_PROTECTED_PATTERN}; an invalid source throws rather than silently protecting nothing.
310
+ */
311
+ readonly protectedPattern?: string;
312
+ }
313
+ /** Why a node was kept out of join/instance. */
314
+ type TModelProtectedRule = "allow-list" | "animation-ancestor" | "animation-target" | "regex" | "skin-joint";
315
+ interface IModelProtectedNode {
316
+ readonly name: string;
317
+ readonly rule: TModelProtectedRule;
318
+ }
319
+ interface IModelCompactSummary {
320
+ /** Whether each pass ran and how many primitives/nodes it removed. */
321
+ readonly flatten: {
322
+ readonly enabled: boolean;
323
+ /** Nodes reparented toward the scene root; a node left in place is not counted. */
324
+ readonly reparented: number;
325
+ };
326
+ readonly instance: {
327
+ readonly batches: number;
328
+ readonly enabled: boolean;
329
+ readonly instances: number;
330
+ /** Why no batch was created, when none was. */
331
+ readonly reason?: string;
332
+ };
333
+ readonly join: {
334
+ readonly enabled: boolean;
335
+ readonly primitivesAfter: number;
336
+ readonly primitivesBefore: number;
337
+ };
338
+ readonly nodesAfter: number;
339
+ readonly nodesBefore: number;
340
+ readonly primitivesAfter: number;
341
+ readonly primitivesBefore: number;
342
+ /**
343
+ * Named scene nodes that existed before compaction and are gone after it. Empty is the
344
+ * expected result of a run whose protected set covered every name the game addresses; a
345
+ * non-empty list is the report's warning that a `getObjectByName` can now return undefined.
346
+ */
347
+ readonly removed: readonly string[];
348
+ /** Every node kept out of compaction, with the rule that protected it, sorted by name. */
349
+ readonly protected: readonly IModelProtectedNode[];
350
+ }
351
+ /** The compaction policy with every default resolved, so it is a stable cache key. */
352
+ interface IResolvedCompactOptions {
353
+ readonly flatten: boolean;
354
+ readonly instance: false | IModelCompactInstanceOptions;
355
+ readonly join: boolean;
356
+ readonly protectedNames: readonly string[];
357
+ readonly protectedPattern: string;
358
+ /** Algorithm identity, so an upgraded pass never re-serves a cache from an older build. */
359
+ readonly version: number;
360
+ }
361
+
195
362
  /**
196
363
  * Atlas integration follows the mesh → xatlas → remapped-attribute split documented by
197
364
  * xatlas-three 0.2.1 (MIT, copyright 2022 Palash Bansal). This implementation uses xatlasjs's
@@ -267,6 +434,13 @@ interface IModelTextureOverride {
267
434
  readonly slot: string;
268
435
  }
269
436
  interface IModelTexturesOptions {
437
+ /**
438
+ * Resolved by the compiler from the target's runtime capabilities, never by a project: the
439
+ * target has no KTX2 decoder, so an over-cap embedded image is box-resampled and re-emitted as
440
+ * a PNG instead of being encoded. Everything else — authored-byte retention under the cap,
441
+ * per-image deduplication, colour space — is unchanged.
442
+ */
443
+ readonly decoderFree?: boolean;
270
444
  /**
271
445
  * Longest edge an embedded image may keep, default 2048. Larger images are box-resampled
272
446
  * down preserving aspect ratio; smaller ones are never upscaled.
@@ -387,6 +561,12 @@ interface IModelSimplifyOptions {
387
561
  }
388
562
  interface IModelPassOptions {
389
563
  readonly passes?: IModelPassesOptions;
564
+ /**
565
+ * Lossless scene-graph compaction (flatten → instance → join) against one protected-node set.
566
+ * Absent means on with defaults; `false` is the kill switch; an object configures the
567
+ * sub-passes, the protected-name allow-list and the name regex. It never decimates geometry.
568
+ */
569
+ readonly compact?: boolean | IModelCompactOptions;
390
570
  /** Native WebGPU rejects interleaved vertex attributes; mobile output uses separate buffers. */
391
571
  readonly vertexLayout?: "interleaved" | "separate";
392
572
  /** Preserve generated TEXCOORD_1 data that is consumed by a runtime-attached lightmap. */
@@ -414,6 +594,13 @@ interface IModelPassOptions {
414
594
  * would hide that.
415
595
  */
416
596
  readonly virtual?: IModelVirtualOptions | "none";
597
+ /**
598
+ * Automatic discrete LOD (`assets.lod`). Absent means no automatic generation in this phase;
599
+ * `false` / `"none"` is the kill switch, and an object carries the resolved policy. Per-asset
600
+ * overrides key off the canonical source asset path, which is why the pass resolves them here
601
+ * rather than at registry construction.
602
+ */
603
+ readonly lod?: boolean | IModelLodOptions | "none";
417
604
  }
418
605
 
419
606
  /**
@@ -475,6 +662,63 @@ interface ISimplifyRow {
475
662
  readonly trianglesAfter: number;
476
663
  readonly trianglesBefore: number;
477
664
  }
665
+ /** What automatic discrete generation produced for one model (PRD-377 §4.3). */
666
+ /** One material group inside an opt-in joined far rung: what it collapsed and how much it draws. */
667
+ interface ILodJoinedGroupRow {
668
+ readonly material: string;
669
+ readonly primitives: number;
670
+ readonly triangles: number;
671
+ }
672
+ /** The opt-in joined far rung as the manifest carries it: draws and the primitives collapsed. */
673
+ interface ILodJoinedRow {
674
+ readonly draws: number;
675
+ readonly groups: readonly ILodJoinedGroupRow[];
676
+ readonly primitives: number;
677
+ readonly triangles: number;
678
+ readonly trianglesBefore: number;
679
+ }
680
+ interface ILodRow {
681
+ /** Bytes the derived index buffers add, before compression. */
682
+ readonly byteOverhead: number;
683
+ /**
684
+ * The foliage cutout conversion the cook ran before generating (PRD-458 §4): the `BLEND`
685
+ * materials that became alpha-tested, and the ones that kept blending and why.
686
+ */
687
+ readonly cutout?: {
688
+ readonly converted: readonly string[];
689
+ readonly kept: readonly {
690
+ readonly name: string;
691
+ readonly reason: string;
692
+ }[];
693
+ };
694
+ /** Migration/legacy notes the resolver raised, by code. */
695
+ readonly diagnostics: readonly string[];
696
+ /** The resolved increasing geometric-error targets, as the bake consumed them. */
697
+ readonly errorTargets: readonly number[];
698
+ readonly fingerprint: string;
699
+ readonly generated: number;
700
+ /** Whether the opt-in join was requested; {@link joined} is present only when it did something. */
701
+ readonly join: boolean;
702
+ /** The opt-in joined far rung's honesty record, when one was produced. */
703
+ readonly joined?: ILodJoinedRow;
704
+ readonly levels: number;
705
+ readonly maxLevels: number;
706
+ /** Fraction of its predecessor's triangles a derived level had to save. */
707
+ readonly minSaving: number;
708
+ readonly minTriangles: number;
709
+ /** What `minTriangles` measured against: `"primitive"` or `"asset"`. */
710
+ readonly minTrianglesScope: string;
711
+ readonly preset: string;
712
+ readonly reasons: readonly string[];
713
+ /** The runtime selection budget this asset ships with; the loader reads it from the manifest. */
714
+ readonly runtime: {
715
+ readonly hysteresis: number;
716
+ readonly maxPixelError: number;
717
+ };
718
+ readonly skipped: number;
719
+ readonly trianglesAfter: number;
720
+ readonly trianglesBefore: number;
721
+ }
478
722
  /** Whether the pass executed this bake or every input that reached it was compile-cache-served. */
479
723
  type PassCostStatus = "cached" | "ran";
480
724
  /** What one input cost the pass that processed it, as the pass driver measured it. */
@@ -581,6 +825,19 @@ interface IAudioRow {
581
825
  */
582
826
  declare function formatAudioSizes(rows: readonly IAudioRow[]): readonly string[];
583
827
  /** One compiled model plus a total, before against after the optimization pass. */
828
+ /** The cook's material merge (PRD-458 §5): what collapsed, and the counts either side of it. */
829
+ interface IMaterialsRow {
830
+ /** Distinct signatures — the count a draw call is bound by. */
831
+ readonly distinct: {
832
+ readonly after: number;
833
+ readonly before: number;
834
+ };
835
+ readonly materials: {
836
+ readonly after: number;
837
+ readonly before: number;
838
+ };
839
+ readonly merged: readonly string[];
840
+ }
584
841
  interface IModelSizeRow {
585
842
  readonly after: number;
586
843
  readonly before: number;
@@ -601,8 +858,14 @@ interface IModelSizeRow {
601
858
  };
602
859
  /** LOD simplification, when it was configured for this model. */
603
860
  readonly simplify?: ISimplifyRow;
861
+ /** Automatic discrete LOD generation (PRD-377), when the effective policy ran. */
862
+ readonly lod?: ILodRow;
863
+ /** Lossless scene-graph compaction (PRD-443), when it ran. */
864
+ readonly compact?: IModelCompactSummary;
604
865
  /** The cluster-DAG bake, when it was configured for this model. */
605
866
  readonly virtual?: IVirtualRow;
867
+ /** The material merge the cook ran unconditionally, before any other geometry stage. */
868
+ readonly materials?: IMaterialsRow;
606
869
  /** Triangle count of the compiled output, recorded in the manifest. */
607
870
  readonly triangles?: number;
608
871
  }
@@ -666,6 +929,13 @@ interface IAssetPass {
666
929
  /** True when this pass's emitted output requires a runtime decoder unavailable on mobile. */
667
930
  readonly needsRuntimeDecoder?: boolean;
668
931
  readonly name: string;
932
+ /**
933
+ * Input kinds whose bytes this pass can change. The compile hashes only the passes that can
934
+ * change an asset into that asset's digest, so a model-only option edit does not rename every
935
+ * texture. `undefined` (the default for a caller-supplied pass) means any kind, which stays
936
+ * conservative: an unknown pass can change every asset.
937
+ */
938
+ readonly appliesTo?: readonly AssetKind[];
669
939
  /**
670
940
  * JSON-serializable snapshot of every option that changes this pass's output. Part of the
671
941
  * compile cache key: without it, editing a config value (texture quality, quantize bits, an
@@ -687,7 +957,7 @@ interface IAssetSourceConfig {
687
957
  readonly audio?: IAudioConfig | "none";
688
958
  /**
689
959
  * The bound on how many workers a bake may use. A direct `concurrency` option overrides it;
690
- * absent means the driver's default (`min(4, cores - 1)`). CI boxes and laptops differ, and
960
+ * absent means the driver's default (cores - 1, bounded by ~2 GB of memory a worker, at most 12). CI boxes and laptops differ, and
691
961
  * a 6.8 GB pack does not get to decide the machine's fate.
692
962
  */
693
963
  readonly concurrency?: number;
@@ -696,6 +966,12 @@ interface IAssetSourceConfig {
696
966
  * exactly as committed. Absent means optimization runs with defaults.
697
967
  */
698
968
  readonly models?: IModelsConfig | "none";
969
+ /**
970
+ * Automatic discrete LOD (`assets.lod`): a boolean kill switch/enable, an object carrying the
971
+ * resolved generation policy, or `"none"`. Absent means this phase generates nothing; the
972
+ * default-on decision is a later phase (PRD-377 §8).
973
+ */
974
+ readonly lod?: boolean | IModelLodOptions | "none";
699
975
  readonly output?: string;
700
976
  readonly source?: string;
701
977
  readonly targets?: IAssetTargets;
@@ -706,6 +982,11 @@ interface IAssetSourceConfig {
706
982
  readonly textures?: ITexturesConfig | "none";
707
983
  }
708
984
  interface IModelsConfig {
985
+ /**
986
+ * Lossless scene-graph compaction (flatten → instance → join) against one protected-node set.
987
+ * Default true ({@link IModelCompactOptions}); `false` ships the scene graph as authored.
988
+ */
989
+ readonly compact?: boolean | IModelCompactOptions;
709
990
  /** Generate standard TEXCOORD_1 lightmap UVs. Absent means no lightmap pass. */
710
991
  readonly lightmap?: ILightmapPassOptions;
711
992
  readonly passes?: IModelPassesOptions;
@@ -780,6 +1061,16 @@ interface IAssetCompileOptions {
780
1061
  * build uncompressed too. The build knows its target; it decides.
781
1062
  */
782
1063
  readonly platform?: "android" | "desktop" | "ios" | "web";
1064
+ /**
1065
+ * What the runtime that will execute this bake can decode, when the caller has probed it.
1066
+ *
1067
+ * `platform` is a proxy: it answers "android and iOS have no WebAssembly" for the targets this
1068
+ * repository ships. A desktop host can also lack WebAssembly — the Linux arm64 lane builds
1069
+ * QuickJS over wgpu-native — and then the same meshopt/KTX2 passes it would keep produce an
1070
+ * asset nothing can decode. `threenative build` reads the engine from the runtime binary it will
1071
+ * package and passes the real capability here; absent, the platform-derived default stands.
1072
+ */
1073
+ readonly runtimeDecoders?: IAssetRuntimeDecoderCapabilities;
783
1074
  readonly source?: string;
784
1075
  /** Overrides resolution of three's Basis transcoder for the copy into the output root. */
785
1076
  readonly transcoder?: IBasisTranscoder;
@@ -801,6 +1092,11 @@ interface IAssetCompileResult {
801
1092
  readonly skippedCompression: readonly ISkippedReportRow[];
802
1093
  readonly receipt?: IBakeReceipt;
803
1094
  readonly report?: IAssetHealthReport;
1095
+ /**
1096
+ * What the model cook saved by cooking content-identical sources once, when this project has
1097
+ * any model source. `cooked` is the number of distinct outputs those sources produced.
1098
+ */
1099
+ readonly dedupe?: IModelDedupeSummary;
804
1100
  readonly skipped: number;
805
1101
  readonly written: number;
806
1102
  }
@@ -928,4 +1224,239 @@ interface IAssetWatchHandle {
928
1224
  }
929
1225
  declare function watchAssets(options?: IAssetWatchOptions): IAssetWatchHandle;
930
1226
 
931
- export { type AssetFindingGrade, type AssetKind, type AudioBand, type AudioConditioning, type AudioNormalisation, type IAssetCompileOptions, type IAssetCompileResult, type IAssetFinding, type IAssetHealthEntry, type IAssetHealthInput, type IAssetHealthReport, type IAssetPass, type IAssetPassOutput, type IAssetSourceConfig, type IAssetTargets, type IAssetWatchHandle, type IAssetWatchOptions, type IAssetWatchSummary, type IAudioConfig, type IAudioLoopOptions, type IAudioOverride, type IAudioPassOptions, type IAudioRow, type IAudioSpectrumExpectation, type IBakeReceipt, type IBakeReceiptOutput, type IBasisTranscoder, type IEmbeddedTextureRow, type IEmbeddedTextureSummary, type ILightmapPassOptions, type IModelPassOptions, type IModelPassesOptions, type IModelQuantizeOptions, type IModelSimplifyOptions, type IModelSimplifySummary, type IModelSizeRow, type IModelStats, type IModelTextureBinding, type IModelTextureBindings, type IModelTextureOverride, type IModelTexturesOptions, type IModelsConfig, type IPassCostAssetRow, type IPassCostRow, type IPngInfo, type ISimplifyRow, type ITextureOverride, type ITexturePassOptions, type ITextureSizeRow, type ITextureStats, type ITexturesConfig, type PassCostStatus, type TextureCodec, audioPass, compileAssets, formatAudioSizes, formatHealthReport, formatModelSizes, formatPassCosts, formatTextureSizes, lightmapPass, modelPass, parseAudioConfig, parsePng, resolveBasisTranscoder, runHealthReport, texturePass, watchAssets };
1227
+ /**
1228
+ * A deterministic texture atlas packer.
1229
+ *
1230
+ * The reference game's carrier is 95 meshes over 76 materials and **73 distinct texture sources** —
1231
+ * one texture per material, because an imported model carries a material per part. A per-material
1232
+ * merge over content shaped like that has nothing to collapse: it found 274 buckets holding 213
1233
+ * singletons and moved no frame time. That null result is the evidence for this file. Materials
1234
+ * cannot be deduplicated while each one owns a private texture, so the atlas is the prerequisite
1235
+ * and everything downstream — dedupe, merge, instancing — is priced against it.
1236
+ *
1237
+ * Two properties, both load-bearing:
1238
+ *
1239
+ * - **Deterministic.** Two runs over the same inputs place every source at the same pixel. The
1240
+ * order is derived from the sources themselves (taller first, then wider, then the key), never
1241
+ * from map iteration or filesystem order, because a build that packs differently on a rebuild
1242
+ * invalidates every downstream cache and makes a screenshot-parity gate meaningless.
1243
+ * - **It refuses what it cannot do.** A source that tiles is excluded and reported, never packed
1244
+ * and silently clamped: a repeating surface whose UVs leave `[0, 1]` reads neighbouring pages
1245
+ * once it shares one, which is a visible bug that looks like corruption rather than like a
1246
+ * packing decision.
1247
+ *
1248
+ * Shelf packing, not a perfect bin: sorted by height it wastes a few percent of a page against an
1249
+ * optimal solver nobody can read, and the cost of a wasted page is bytes, while the cost of a
1250
+ * wrong one is a visual bug. `ponytail: shelf packer, revisit only if page count is measured to
1251
+ * matter.`
1252
+ */
1253
+ /** One image offered to the packer. */
1254
+ interface IAtlasSource {
1255
+ /** Stable identity — the content hash or the texture's URI. Ties in the sort break on this. */
1256
+ readonly key: string;
1257
+ readonly width: number;
1258
+ readonly height: number;
1259
+ /**
1260
+ * Whether the material samples this source outside `[0, 1]`. A tiling source cannot share a
1261
+ * page, and saying so is the source's job, not a guess the packer makes from pixels.
1262
+ */
1263
+ readonly tiles?: boolean;
1264
+ }
1265
+ /** Where one source landed. */
1266
+ interface IAtlasPlacement {
1267
+ readonly key: string;
1268
+ readonly page: number;
1269
+ readonly x: number;
1270
+ readonly y: number;
1271
+ readonly width: number;
1272
+ readonly height: number;
1273
+ }
1274
+ /** The UV transform for one source: `uv' = uv * scale + offset`. */
1275
+ interface IAtlasTransform {
1276
+ readonly page: number;
1277
+ readonly offsetX: number;
1278
+ readonly offsetY: number;
1279
+ readonly scaleX: number;
1280
+ readonly scaleY: number;
1281
+ }
1282
+ /** Why a source was left out. Every exclusion is reported; none is silent. */
1283
+ type AtlasExclusionReason = "tiles" | "too-large";
1284
+ interface IAtlasExclusion {
1285
+ readonly key: string;
1286
+ readonly reason: AtlasExclusionReason;
1287
+ }
1288
+ interface IAtlasPage {
1289
+ readonly index: number;
1290
+ readonly width: number;
1291
+ readonly height: number;
1292
+ readonly placements: readonly IAtlasPlacement[];
1293
+ }
1294
+ interface IAtlasResult {
1295
+ readonly pages: readonly IAtlasPage[];
1296
+ /** Keyed by source key, in the packer's own deterministic order. */
1297
+ readonly transforms: ReadonlyMap<string, IAtlasTransform>;
1298
+ readonly excluded: readonly IAtlasExclusion[];
1299
+ }
1300
+ interface IAtlasOptions {
1301
+ /** Page edge in pixels. Default 4096. */
1302
+ readonly pageSize?: number;
1303
+ /**
1304
+ * Transparent pixels kept between neighbours, so a mip level or a bilinear tap cannot reach the
1305
+ * next source. Default 4 — two levels of mip bleed at the sizes this packs.
1306
+ */
1307
+ readonly padding?: number;
1308
+ }
1309
+ /**
1310
+ * Packs sources into pages and answers the UV transform for each.
1311
+ *
1312
+ * Fails closed on a malformed source: a zero or non-integer dimension is a decoder that did not
1313
+ * decode, and packing it would put a divide-by-zero in every UV it rewrites.
1314
+ */
1315
+ declare function packAtlas(sources: readonly IAtlasSource[], options?: IAtlasOptions): IAtlasResult;
1316
+ /**
1317
+ * The manifest a build writes beside the pages.
1318
+ *
1319
+ * Sorted by key so two runs produce byte-identical JSON — the same reason the packing order is
1320
+ * derived rather than inherited.
1321
+ */
1322
+ declare function atlasManifest(result: IAtlasResult): string;
1323
+
1324
+ /**
1325
+ * Moving a mesh's UVs onto its atlas page, and deciding which meshes may not go.
1326
+ *
1327
+ * The rewrite itself is two multiplies and two adds per vertex. The decision in front of it is the
1328
+ * part that matters: a surface that samples outside `[0, 1]` tiles, and a tiling surface on a
1329
+ * shared page reads its neighbours. That is a visible bug that looks like corruption, so the
1330
+ * policy here is to detect it from the geometry's own UVs and from the sampler's wrap mode, refuse
1331
+ * to pack the source, and report it — never to clamp, and never to guess from the image.
1332
+ */
1333
+
1334
+ /** glTF sampler wrap modes; `10497` is REPEAT and `33648` is MIRRORED_REPEAT. */
1335
+ declare const WRAP_REPEAT = 10497;
1336
+ declare const WRAP_MIRRORED_REPEAT = 33648;
1337
+ declare const WRAP_CLAMP_TO_EDGE = 33071;
1338
+ /** Whether this UV set samples outside the unit square, and therefore tiles. */
1339
+ declare function uvsTile(uv: ArrayLike<number>): boolean;
1340
+ /** Whether the sampler asks to repeat. A wrap mode the packer does not know is treated as tiling. */
1341
+ declare function wrapTiles(wrapS: number | undefined, wrapT: number | undefined): boolean;
1342
+ /**
1343
+ * Rewrites a UV buffer in place onto its atlas page.
1344
+ *
1345
+ * In place because the caller owns a freshly-decoded accessor and a second buffer per mesh is a
1346
+ * copy of the whole scene's UVs for nothing. Throws on an odd-length buffer rather than rewriting
1347
+ * half a coordinate.
1348
+ */
1349
+ declare function rewriteUvs(uv: Float32Array, transform: IAtlasTransform): Float32Array;
1350
+ /**
1351
+ * The texel a rewritten UV resolves to, in the source image's own pixel space.
1352
+ *
1353
+ * This is the inverse of `rewriteUvs` composed with the page geometry, and it exists so a test can
1354
+ * assert the round trip rather than assert the arithmetic against itself: sample a texel, rewrite,
1355
+ * resolve, and land within a texel of where you started.
1356
+ */
1357
+ declare function resolveSourceTexel(atlasUv: readonly [number, number], transform: IAtlasTransform, source: {
1358
+ readonly width: number;
1359
+ readonly height: number;
1360
+ }): {
1361
+ x: number;
1362
+ y: number;
1363
+ };
1364
+
1365
+ /**
1366
+ * Collapsing materials that became identical once their textures shared a page.
1367
+ *
1368
+ * This is the step the atlas exists to enable, and it is also the step that falsifies the atlas.
1369
+ * The reference game's merge found 274 buckets holding 213 singletons because every material owned
1370
+ * a private texture; if the singleton count does not fall once those textures share pages, the
1371
+ * atlas hypothesis is wrong and everything priced on it has to be repriced. So the signature here
1372
+ * is deliberately conservative: two materials collapse only when every field that can change a
1373
+ * pixel agrees, and anything the signature does not know about keeps them apart.
1374
+ */
1375
+ /** The material state a signature is taken over. Structural, so a glTF or a three material fits. */
1376
+ interface IMaterialState {
1377
+ readonly name?: string;
1378
+ /** Texture identity per slot *after* atlasing — the page, not the original image. */
1379
+ readonly textures: Readonly<Record<string, string | undefined>>;
1380
+ /** Scalar and vector uniforms: base colour, metallic, roughness, emissive, alpha cutoff, … */
1381
+ readonly uniforms: Readonly<Record<string, number | readonly number[] | string | boolean | undefined>>;
1382
+ /** Anything that changes how the surface is drawn rather than what it samples. */
1383
+ readonly flags: Readonly<Record<string, number | string | boolean | undefined>>;
1384
+ }
1385
+ /**
1386
+ * A stable signature over everything that decides how this material looks.
1387
+ *
1388
+ * The name is deliberately **not** in it: an imported pack names a material per part, and letting
1389
+ * the name separate them is exactly the property that made 213 singletons. Every other field is,
1390
+ * including ones this pipeline does not understand, because an unknown field that differs is a
1391
+ * reason not to collapse.
1392
+ */
1393
+ declare function materialSignature(material: IMaterialState): string;
1394
+ /** One group of materials that became the same material. */
1395
+ interface IMaterialBucket {
1396
+ readonly signature: string;
1397
+ /** Names in input order, so a report can say which materials collapsed into which. */
1398
+ readonly members: readonly string[];
1399
+ }
1400
+ interface IDedupeCensus {
1401
+ /** Distinct signatures — the material count after deduplication. */
1402
+ readonly buckets: number;
1403
+ /** Buckets holding exactly one material: the count the atlas has to move. */
1404
+ readonly singletons: number;
1405
+ /** Materials in. */
1406
+ readonly materials: number;
1407
+ }
1408
+ /** Groups materials by signature, in first-seen order so two runs report the same thing. */
1409
+ declare function dedupeMaterials(materials: readonly IMaterialState[]): {
1410
+ buckets: readonly IMaterialBucket[];
1411
+ census: IDedupeCensus;
1412
+ };
1413
+ /**
1414
+ * The same materials with every texture slot repointed at its atlas page.
1415
+ *
1416
+ * A slot whose source was excluded from the atlas keeps its original texture, which is what makes
1417
+ * the census honest: those materials stay singletons, and the report says how many.
1418
+ */
1419
+ declare function withAtlasTextures(materials: readonly IMaterialState[], pageOf: (texture: string) => string | undefined): IMaterialState[];
1420
+
1421
+ /**
1422
+ * The bucket census: what a merge could collapse, before and after an atlas.
1423
+ *
1424
+ * "Fewer objects" was the obvious lever on the reference game and it recovered nothing — a runtime
1425
+ * per-material merge replaced 246 of 1,561 meshes and moved no frame time, because 274 buckets
1426
+ * held 213 singletons. The census is the number to read before promising a merge, and this is the
1427
+ * thing that reads it: it walks a glTF document's materials, takes the same signature the dedupe
1428
+ * takes, and reports the buckets twice — once as authored, once with every atlasable texture
1429
+ * repointed at a page.
1430
+ *
1431
+ * The second number is the falsification point for the whole atlas argument. If singletons do not
1432
+ * fall, the atlas is not the lever and nothing downstream of it should be built.
1433
+ */
1434
+
1435
+ /** One model's census. */
1436
+ interface IContentCensus {
1437
+ readonly model: string;
1438
+ readonly meshes: number;
1439
+ readonly primitives: number;
1440
+ readonly materials: number;
1441
+ /** Distinct texture images the materials reference. */
1442
+ readonly textureSources: number;
1443
+ /** Sources the packer refused, with the reason — tiling surfaces, mostly. */
1444
+ readonly excluded: number;
1445
+ readonly atlasPages: number;
1446
+ readonly before: IDedupeCensus;
1447
+ readonly after: IDedupeCensus;
1448
+ }
1449
+ /** Reads a material into the state the signature is taken over. */
1450
+ declare function materialStateOf(material: Material): IMaterialState;
1451
+ /**
1452
+ * Censuses one document: what its materials are now, and what they would be once their textures
1453
+ * shared atlas pages.
1454
+ *
1455
+ * A texture whose size the document does not state cannot be packed, so it is reported as excluded
1456
+ * rather than assumed square — an assumed size is an assumed page layout.
1457
+ */
1458
+ declare function censusDocument(model: string, document: Document, pageSize?: number): IContentCensus;
1459
+ /** Totals across a set of models, so a scene's census is one line rather than twenty-nine. */
1460
+ declare function totalCensus(entries: readonly IContentCensus[]): Omit<IContentCensus, "model">;
1461
+
1462
+ export { type AssetFindingGrade, type AssetKind, type AtlasExclusionReason, type AudioBand, type AudioConditioning, type AudioNormalisation, type IAssetCompileOptions, type IAssetCompileResult, type IAssetFinding, type IAssetHealthEntry, type IAssetHealthInput, type IAssetHealthReport, type IAssetPass, type IAssetPassOutput, type IAssetSourceConfig, type IAssetTargets, type IAssetWatchHandle, type IAssetWatchOptions, type IAssetWatchSummary, type IAtlasExclusion, type IAtlasOptions, type IAtlasPage, type IAtlasPlacement, type IAtlasResult, type IAtlasSource, type IAudioConfig, type IAudioLoopOptions, type IAudioOverride, type IAudioPassOptions, type IAudioRow, type IAudioSpectrumExpectation, type IBakeReceipt, type IBakeReceiptOutput, type IBasisTranscoder, type IContentCensus, type IDedupeCensus, type IEmbeddedTextureRow, type IEmbeddedTextureSummary, type ILightmapPassOptions, type IMaterialBucket, type IMaterialState, type IModelCompactInstanceOptions, type IModelCompactOptions, type IModelCompactSummary, type IModelPassOptions, type IModelPassesOptions, type IModelProtectedNode, type IModelQuantizeOptions, type IModelSimplifyOptions, type IModelSimplifySummary, type IModelSizeRow, type IModelStats, type IModelTextureBinding, type IModelTextureBindings, type IModelTextureOverride, type IModelTexturesOptions, type IModelsConfig, type IPassCostAssetRow, type IPassCostRow, type IPngInfo, type IResolvedCompactOptions, type ISimplifyRow, type ITextureOverride, type ITexturePassOptions, type ITextureSizeRow, type ITextureStats, type ITexturesConfig, type PassCostStatus, type TModelProtectedRule, type TextureCodec, WRAP_CLAMP_TO_EDGE, WRAP_MIRRORED_REPEAT, WRAP_REPEAT, atlasManifest, audioPass, censusDocument, compileAssets, dedupeMaterials, formatAudioSizes, formatHealthReport, formatModelSizes, formatPassCosts, formatTextureSizes, lightmapPass, materialSignature, materialStateOf, modelPass, packAtlas, parseAudioConfig, parsePng, resolveBasisTranscoder, resolveSourceTexel, rewriteUvs, runHealthReport, texturePass, totalCensus, uvsTile, watchAssets, withAtlasTextures, wrapTiles };