@threenative/assets 0.3.3 → 0.3.6

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,12 +1,123 @@
1
1
  import { Document, Material } from '@gltf-transform/core';
2
2
 
3
+ interface IImageQualityFloor {
4
+ readonly ssim: number;
5
+ readonly meanDeltaE00: number;
6
+ }
7
+ interface IImageQuality {
8
+ readonly version: string;
9
+ readonly width: number;
10
+ readonly height: number;
11
+ readonly ssim: number;
12
+ readonly meanDeltaE00: number | null;
13
+ readonly slots: readonly string[];
14
+ readonly floor: IImageQualityFloor;
15
+ readonly status: "pass" | "below-floor" | "unvalidated-slots";
16
+ readonly alpha: {
17
+ readonly ssim: number;
18
+ readonly meanAbsoluteError: number;
19
+ readonly coverage: readonly {
20
+ readonly threshold: number;
21
+ readonly source: number;
22
+ readonly decoded: number;
23
+ readonly changedPixels: number;
24
+ }[];
25
+ };
26
+ }
27
+ interface ITextureQuality extends IImageQuality {
28
+ readonly rung?: string;
29
+ readonly compressionSkipped?: "block-size" | "not-smaller" | "below-floor";
30
+ readonly sourceWidth: number;
31
+ readonly sourceHeight: number;
32
+ readonly codec: string;
33
+ }
34
+
35
+ /**
36
+ * Compresses the images embedded *inside* a `.glb` — the half of a model the geometry passes
37
+ * never touched. A prop carrying three 2048x2048 JPEGs is a small file and 67 MB of VRAM once
38
+ * the driver decodes it; meshopt shrinks the other, cheaper half. This stage transcodes each
39
+ * embedded image to KTX2/Basis and declares `KHR_texture_basisu`, so the GPU stores blocks
40
+ * instead of RGBA, and caps the resolution so a 4K texture an image model produced cannot
41
+ * silently eat a phone's whole budget.
42
+ *
43
+ * Slot constraints feed one measured ladder: normal/data slots retain UASTC/none, while
44
+ * colour escalates from ETC1S through RDO and plain UASTC. Alpha coverage is checked separately;
45
+ * a forced codec must still meet the floor. Colour space comes from the slot too — `getTextureColorSpace`
46
+ * knows base colour and emissive are sRGB and that metallic-roughness and normals are data —
47
+ * so a linear map is never encoded through a perceptual metric.
48
+ *
49
+ * The encoder is the same in-process Basis WASM the standalone texture pass uses, so no new
50
+ * dependency and nothing extra to install. Everything fails closed: an image no decoder reads,
51
+ * an image whose dimensions are not a whole number of 4x4 blocks, and a texture whose binding
52
+ * did not survive the round trip all throw naming the model and the image.
53
+ */
54
+ interface IModelTextureOverride {
55
+ readonly codec: TextureCodec;
56
+ /** glTF slot the override applies to, e.g. `normalTexture` or `baseColorTexture`. */
57
+ readonly slot: string;
58
+ }
59
+ type TextureMaxSize = number | Readonly<Record<string, number>>;
60
+ interface IModelTexturesOptions {
61
+ /** Internal target-selected raw ASTC output. */
62
+ readonly astc?: boolean;
63
+ readonly floor?: Partial<IImageQualityFloor>;
64
+ /** Internal reporting switch; selection still measures and enforces the floor. */
65
+ readonly measureQuality?: boolean;
66
+ /**
67
+ * Resolved by the compiler from the target's runtime capabilities, never by a project: the
68
+ * target has no KTX2 decoder, so an over-cap embedded image is box-resampled and re-emitted as
69
+ * a PNG instead of being encoded. Everything else — authored-byte retention under the cap,
70
+ * per-image deduplication, colour space — is unchanged.
71
+ */
72
+ readonly decoderFree?: boolean;
73
+ /**
74
+ * Longest edge an embedded image may keep, default 2048. Larger images are box-resampled
75
+ * down preserving aspect ratio; smaller ones are never upscaled.
76
+ */
77
+ readonly maxSize?: TextureMaxSize;
78
+ readonly overrides?: readonly IModelTextureOverride[];
79
+ /** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
80
+ readonly quality?: number;
81
+ }
82
+ /** What the stage did, recorded in the manifest and printed by the size report. */
83
+ interface IEmbeddedTextureSummary {
84
+ readonly quality: Readonly<Record<string, ITextureQuality>>;
85
+ readonly bytesAfter: number;
86
+ readonly bytesBefore: number;
87
+ readonly count: number;
88
+ /** Chosen codec per embedded image, keyed by texture name. */
89
+ readonly formats: Readonly<Record<string, string>>;
90
+ readonly gpuBytesAfter: number;
91
+ readonly gpuBytesBefore: number;
92
+ /** How many images the resolution cap actually downsampled. */
93
+ readonly resized: number;
94
+ /** Images retained at their authored bytes, by name, with the reason each one was not cooked. */
95
+ readonly skippedCompression: Readonly<Record<string, TextureSkipReason>>;
96
+ }
97
+ /** One embedded image as the verification compares it, source against re-read output. */
98
+ interface IModelTextureBinding {
99
+ readonly height: number;
100
+ readonly key: string;
101
+ /** KTX2 mip levels; 1 for a still-uncompressed source image. */
102
+ readonly levels: number;
103
+ readonly mimeType: string;
104
+ readonly slots: readonly string[];
105
+ readonly texCoords: readonly number[];
106
+ readonly width: number;
107
+ }
108
+ interface IModelTextureBindings {
109
+ /** `material -> slot=textureIndex@uvN` for the five core glTF slots, in document order. */
110
+ readonly materials: readonly string[];
111
+ readonly textures: readonly IModelTextureBinding[];
112
+ }
113
+
3
114
  /**
4
115
  * Encodes compiled textures to KTX2/Basis so the GPU stores them compressed instead of as
5
116
  * decoded RGBA (a 2048x2048 PNG is 16 MB in VRAM; BC7/ASTC hold it in a quarter of that).
6
117
  *
7
118
  * Codec choice is a declared property of the asset, never a guess: a config override wins,
8
- * then the `*_normal.*` / `_nrm.*` filename convention, then alpha presence (UASTC keeps
9
- * exact alpha and survives normal-map data; ETC1S does not). Mip chains are generated at
119
+ * then the `*_normal.*` / `_nrm.*` filename convention constrains the shared measured ladder.
120
+ * Colour escalates through ETC1S/RDO/UASTC/none; unknown alpha cutoffs require exact alpha. Mip chains are generated at
10
121
  * encode time — always — because an uploaded compressed texture without mips looks worse
11
122
  * than the PNG it replaced.
12
123
  *
@@ -18,10 +129,10 @@ type TextureCodec = "etc1s" | "none" | "uastc";
18
129
  /**
19
130
  * Why a source shipped uncompressed although its pass ran. `not-smaller` means encoding would
20
131
  * have grown the download; `block-size` means no block codec can address the source's
21
- * dimensions. Both keep the authored bytes; neither is a silent decision — the manifest carries
132
+ * dimensions; `below-floor` means no compressed rung passed. A capped source retains its resized pixels as PNG; none is a silent decision — the manifest carries
22
133
  * the reason and the report prints it.
23
134
  */
24
- type TextureSkipReason = "block-size" | "not-smaller";
135
+ type TextureSkipReason = "block-size" | "not-smaller" | "below-floor";
25
136
  interface ITextureOverride {
26
137
  readonly codec: TextureCodec;
27
138
  /** First matching override wins; matched against the logical path, e.g. "ui/x.png". */
@@ -30,8 +141,13 @@ interface ITextureOverride {
30
141
  readonly quality?: number;
31
142
  }
32
143
  interface ITexturePassOptions {
144
+ /** Compiler-selected mobile output; the quality ladder measures final ASTC bytes. */
145
+ readonly astc?: boolean;
146
+ readonly floor?: Partial<IImageQualityFloor>;
147
+ /** Internal reporting control; selection still measures and enforces the floor. */
148
+ readonly measureQuality?: boolean;
33
149
  /** Longest edge to retain; larger sources are downsampled without upscaling. */
34
- readonly maxSize?: number;
150
+ readonly maxSize?: TextureMaxSize;
35
151
  readonly overrides?: readonly ITextureOverride[];
36
152
  /** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
37
153
  readonly quality?: number;
@@ -54,6 +170,15 @@ interface IAssetRuntimeDecoderCapabilities {
54
170
  readonly meshopt: boolean;
55
171
  }
56
172
 
173
+ interface IModelDedupeSummary {
174
+ /** Distinct cooked outputs among the model sources that took part. */
175
+ readonly cooked: number;
176
+ /** Cooked bytes the duplicates would have written on their own. */
177
+ readonly savedBytes: number;
178
+ /** Model sources that took part, canonicals and duplicates alike. */
179
+ readonly sources: number;
180
+ }
181
+
57
182
  type AssetFindingGrade = "fail" | "ok" | "warn";
58
183
  interface IAssetFinding {
59
184
  readonly asset: string;
@@ -164,6 +289,7 @@ interface IModelLodRuntimeOptions {
164
289
  }
165
290
  /** A partial override for one asset; nested objects overlay, they never replace. */
166
291
  interface IModelLodOverride {
292
+ readonly cutout?: boolean;
167
293
  readonly enabled?: boolean;
168
294
  readonly generation?: IModelLodGenerationOptions;
169
295
  readonly preset?: LodPreset;
@@ -175,6 +301,12 @@ interface IModelLodOverride {
175
301
  * re-enable. Resolution is per asset and happens where the asset is known (PRD-377 §3.2, §5).
176
302
  */
177
303
  interface IModelLodOptions {
304
+ /**
305
+ * Convert eligible `BLEND` foliage to alpha-tested in the cook (PRD-458 §4). Default `true`,
306
+ * because a needle card exported as `BLEND` is refused a chain by rule and the tree then draws
307
+ * LOD0 forever. `false` keeps the authored blending; per asset, `cutout` in an override entry.
308
+ */
309
+ readonly cutout?: boolean;
178
310
  readonly enabled?: boolean;
179
311
  readonly generation?: IModelLodGenerationOptions;
180
312
  readonly overrides?: Readonly<Record<string, boolean | IModelLodOverride>>;
@@ -275,6 +407,74 @@ interface IAudioPassOptions {
275
407
  /** `"none"` drops the pass; an object configures it; absent means the defaults run. */
276
408
  declare function parseAudioConfig(raw: unknown): IAudioPassOptions | undefined;
277
409
 
410
+ interface IModelCompactInstanceOptions {
411
+ /**
412
+ * Minimum number of nodes that must share one mesh before it is instanced, default 2.
413
+ * gltf-transform's own default is 5; the lower floor is what lets a paired prop instance.
414
+ */
415
+ readonly min?: number;
416
+ }
417
+ interface IModelCompactOptions {
418
+ readonly flatten?: boolean;
419
+ readonly instance?: boolean | IModelCompactInstanceOptions;
420
+ readonly join?: boolean;
421
+ /** Extra exact node names to protect, on top of the regex and the structural rules. */
422
+ readonly protectedNames?: readonly string[];
423
+ /**
424
+ * Case-insensitive node-name regex source for nodes the game looks up or moves. Defaults to
425
+ * {@link DEFAULT_PROTECTED_PATTERN}; an invalid source throws rather than silently protecting nothing.
426
+ */
427
+ readonly protectedPattern?: string;
428
+ }
429
+ /** Why a node was kept out of join/instance. */
430
+ type TModelProtectedRule = "allow-list" | "animation-ancestor" | "animation-target" | "regex" | "skin-joint";
431
+ interface IModelProtectedNode {
432
+ readonly name: string;
433
+ readonly rule: TModelProtectedRule;
434
+ }
435
+ interface IModelCompactSummary {
436
+ /** Whether each pass ran and how many primitives/nodes it removed. */
437
+ readonly flatten: {
438
+ readonly enabled: boolean;
439
+ /** Nodes reparented toward the scene root; a node left in place is not counted. */
440
+ readonly reparented: number;
441
+ };
442
+ readonly instance: {
443
+ readonly batches: number;
444
+ readonly enabled: boolean;
445
+ readonly instances: number;
446
+ /** Why no batch was created, when none was. */
447
+ readonly reason?: string;
448
+ };
449
+ readonly join: {
450
+ readonly enabled: boolean;
451
+ readonly primitivesAfter: number;
452
+ readonly primitivesBefore: number;
453
+ };
454
+ readonly nodesAfter: number;
455
+ readonly nodesBefore: number;
456
+ readonly primitivesAfter: number;
457
+ readonly primitivesBefore: number;
458
+ /**
459
+ * Named scene nodes that existed before compaction and are gone after it. Empty is the
460
+ * expected result of a run whose protected set covered every name the game addresses; a
461
+ * non-empty list is the report's warning that a `getObjectByName` can now return undefined.
462
+ */
463
+ readonly removed: readonly string[];
464
+ /** Every node kept out of compaction, with the rule that protected it, sorted by name. */
465
+ readonly protected: readonly IModelProtectedNode[];
466
+ }
467
+ /** The compaction policy with every default resolved, so it is a stable cache key. */
468
+ interface IResolvedCompactOptions {
469
+ readonly flatten: boolean;
470
+ readonly instance: false | IModelCompactInstanceOptions;
471
+ readonly join: boolean;
472
+ readonly protectedNames: readonly string[];
473
+ readonly protectedPattern: string;
474
+ /** Algorithm identity, so an upgraded pass never re-serves a cache from an older build. */
475
+ readonly version: number;
476
+ }
477
+
278
478
  /**
279
479
  * Atlas integration follows the mesh → xatlas → remapped-attribute split documented by
280
480
  * xatlas-three 0.2.1 (MIT, copyright 2022 Palash Bansal). This implementation uses xatlasjs's
@@ -325,71 +525,6 @@ interface IModelVirtualOptions {
325
525
  readonly simplifyRatio?: number;
326
526
  }
327
527
 
328
- /**
329
- * Compresses the images embedded *inside* a `.glb` — the half of a model the geometry passes
330
- * never touched. A prop carrying three 2048x2048 JPEGs is a small file and 67 MB of VRAM once
331
- * the driver decodes it; meshopt shrinks the other, cheaper half. This stage transcodes each
332
- * embedded image to KTX2/Basis and declares `KHR_texture_basisu`, so the GPU stores blocks
333
- * instead of RGBA, and caps the resolution so a 4K texture an image model produced cannot
334
- * silently eat a phone's whole budget.
335
- *
336
- * Codec choice follows the declared use of the image, never a guess: a config override on the
337
- * glTF slot wins, then normal-map slots (UASTC survives normal data; ETC1S does not), then
338
- * alpha in the decoded pixels. Colour space comes from the slot too — `getTextureColorSpace`
339
- * knows base colour and emissive are sRGB and that metallic-roughness and normals are data —
340
- * so a linear map is never encoded through a perceptual metric.
341
- *
342
- * The encoder is the same in-process Basis WASM the standalone texture pass uses, so no new
343
- * dependency and nothing extra to install. Everything fails closed: an image no decoder reads,
344
- * an image whose dimensions are not a whole number of 4x4 blocks, and a texture whose binding
345
- * did not survive the round trip all throw naming the model and the image.
346
- */
347
- interface IModelTextureOverride {
348
- readonly codec: TextureCodec;
349
- /** glTF slot the override applies to, e.g. `normalTexture` or `baseColorTexture`. */
350
- readonly slot: string;
351
- }
352
- interface IModelTexturesOptions {
353
- /**
354
- * Longest edge an embedded image may keep, default 2048. Larger images are box-resampled
355
- * down preserving aspect ratio; smaller ones are never upscaled.
356
- */
357
- readonly maxSize?: number;
358
- readonly overrides?: readonly IModelTextureOverride[];
359
- /** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
360
- readonly quality?: number;
361
- }
362
- /** What the stage did, recorded in the manifest and printed by the size report. */
363
- interface IEmbeddedTextureSummary {
364
- readonly bytesAfter: number;
365
- readonly bytesBefore: number;
366
- readonly count: number;
367
- /** Chosen codec per embedded image, keyed by texture name. */
368
- readonly formats: Readonly<Record<string, string>>;
369
- readonly gpuBytesAfter: number;
370
- readonly gpuBytesBefore: number;
371
- /** How many images the resolution cap actually downsampled. */
372
- readonly resized: number;
373
- /** Images retained at their authored bytes, by name, with the reason each one was not cooked. */
374
- readonly skippedCompression: Readonly<Record<string, TextureSkipReason>>;
375
- }
376
- /** One embedded image as the verification compares it, source against re-read output. */
377
- interface IModelTextureBinding {
378
- readonly height: number;
379
- readonly key: string;
380
- /** KTX2 mip levels; 1 for a still-uncompressed source image. */
381
- readonly levels: number;
382
- readonly mimeType: string;
383
- readonly slots: readonly string[];
384
- readonly texCoords: readonly number[];
385
- readonly width: number;
386
- }
387
- interface IModelTextureBindings {
388
- /** `material -> slot=textureIndex@uvN` for the five core glTF slots, in document order. */
389
- readonly materials: readonly string[];
390
- readonly textures: readonly IModelTextureBinding[];
391
- }
392
-
393
528
  /**
394
529
  * Images that models share, written once and referenced from every model that carries them.
395
530
  *
@@ -404,6 +539,7 @@ interface IModelTextureBindings {
404
539
  * bytes resolve to one URL and the browser cache, the bundle and the delete-test all see one file.
405
540
  */
406
541
  interface ISharedImage {
542
+ readonly quality?: ITextureQuality;
407
543
  readonly buffer: Buffer;
408
544
  /** `uastc`, `etc1s` or `none`, spelled into the filename so a cache hit knows what it found. */
409
545
  readonly codec: string;
@@ -470,6 +606,12 @@ interface IModelSimplifyOptions {
470
606
  }
471
607
  interface IModelPassOptions {
472
608
  readonly passes?: IModelPassesOptions;
609
+ /**
610
+ * Lossless scene-graph compaction (flatten → instance → join) against one protected-node set.
611
+ * Absent means on with defaults; `false` is the kill switch; an object configures the
612
+ * sub-passes, the protected-name allow-list and the name regex. It never decimates geometry.
613
+ */
614
+ readonly compact?: boolean | IModelCompactOptions;
473
615
  /** Native WebGPU rejects interleaved vertex attributes; mobile output uses separate buffers. */
474
616
  readonly vertexLayout?: "interleaved" | "separate";
475
617
  /** Preserve generated TEXCOORD_1 data that is consumed by a runtime-attached lightmap. */
@@ -527,6 +669,7 @@ declare function modelPass(options?: IModelPassOptions): IAssetPass;
527
669
  * so tests can pin the exact lines a build prints.
528
670
  */
529
671
  interface ITextureSizeRow {
672
+ readonly quality?: ITextureQuality;
530
673
  readonly after: number;
531
674
  readonly before: number;
532
675
  /** Set when the bytes shipped as authored; printed so a flat row is never left unexplained. */
@@ -537,6 +680,7 @@ interface ITextureSizeRow {
537
680
  declare function formatTextureSizes(rows: readonly ITextureSizeRow[]): readonly string[];
538
681
  /** What the model pass did to the images inside one `.glb`. */
539
682
  interface IEmbeddedTextureRow {
683
+ readonly quality?: Readonly<Record<string, ITextureQuality>>;
540
684
  readonly skippedCompression?: Readonly<Record<string, TextureSkipReason>>;
541
685
  readonly bytesAfter: number;
542
686
  readonly bytesBefore: number;
@@ -551,6 +695,8 @@ interface IEmbeddedTextureRow {
551
695
  interface IVirtualRow {
552
696
  readonly bakeSeconds: number;
553
697
  readonly clusters: number;
698
+ /** Primitives handed back to the discrete ladder because their DAG reduced nothing. */
699
+ readonly declined: number;
554
700
  readonly levels: number;
555
701
  readonly payloadBytes: number;
556
702
  readonly primitives: number;
@@ -583,6 +729,17 @@ interface ILodJoinedRow {
583
729
  interface ILodRow {
584
730
  /** Bytes the derived index buffers add, before compression. */
585
731
  readonly byteOverhead: number;
732
+ /**
733
+ * The foliage cutout conversion the cook ran before generating (PRD-458 §4): the `BLEND`
734
+ * materials that became alpha-tested, and the ones that kept blending and why.
735
+ */
736
+ readonly cutout?: {
737
+ readonly converted: readonly string[];
738
+ readonly kept: readonly {
739
+ readonly name: string;
740
+ readonly reason: string;
741
+ }[];
742
+ };
586
743
  /** Migration/legacy notes the resolver raised, by code. */
587
744
  readonly diagnostics: readonly string[];
588
745
  /** The resolved increasing geometric-error targets, as the bake consumed them. */
@@ -717,6 +874,19 @@ interface IAudioRow {
717
874
  */
718
875
  declare function formatAudioSizes(rows: readonly IAudioRow[]): readonly string[];
719
876
  /** One compiled model plus a total, before against after the optimization pass. */
877
+ /** The cook's material merge (PRD-458 §5): what collapsed, and the counts either side of it. */
878
+ interface IMaterialsRow {
879
+ /** Distinct signatures — the count a draw call is bound by. */
880
+ readonly distinct: {
881
+ readonly after: number;
882
+ readonly before: number;
883
+ };
884
+ readonly materials: {
885
+ readonly after: number;
886
+ readonly before: number;
887
+ };
888
+ readonly merged: readonly string[];
889
+ }
720
890
  interface IModelSizeRow {
721
891
  readonly after: number;
722
892
  readonly before: number;
@@ -739,8 +909,12 @@ interface IModelSizeRow {
739
909
  readonly simplify?: ISimplifyRow;
740
910
  /** Automatic discrete LOD generation (PRD-377), when the effective policy ran. */
741
911
  readonly lod?: ILodRow;
912
+ /** Lossless scene-graph compaction (PRD-443), when it ran. */
913
+ readonly compact?: IModelCompactSummary;
742
914
  /** The cluster-DAG bake, when it was configured for this model. */
743
915
  readonly virtual?: IVirtualRow;
916
+ /** The material merge the cook ran unconditionally, before any other geometry stage. */
917
+ readonly materials?: IMaterialsRow;
744
918
  /** Triangle count of the compiled output, recorded in the manifest. */
745
919
  readonly triangles?: number;
746
920
  }
@@ -804,6 +978,13 @@ interface IAssetPass {
804
978
  /** True when this pass's emitted output requires a runtime decoder unavailable on mobile. */
805
979
  readonly needsRuntimeDecoder?: boolean;
806
980
  readonly name: string;
981
+ /**
982
+ * Input kinds whose bytes this pass can change. The compile hashes only the passes that can
983
+ * change an asset into that asset's digest, so a model-only option edit does not rename every
984
+ * texture. `undefined` (the default for a caller-supplied pass) means any kind, which stays
985
+ * conservative: an unknown pass can change every asset.
986
+ */
987
+ readonly appliesTo?: readonly AssetKind[];
807
988
  /**
808
989
  * JSON-serializable snapshot of every option that changes this pass's output. Part of the
809
990
  * compile cache key: without it, editing a config value (texture quality, quantize bits, an
@@ -825,7 +1006,7 @@ interface IAssetSourceConfig {
825
1006
  readonly audio?: IAudioConfig | "none";
826
1007
  /**
827
1008
  * The bound on how many workers a bake may use. A direct `concurrency` option overrides it;
828
- * absent means the driver's default (`min(4, cores - 1)`). CI boxes and laptops differ, and
1009
+ * 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
829
1010
  * a 6.8 GB pack does not get to decide the machine's fate.
830
1011
  */
831
1012
  readonly concurrency?: number;
@@ -840,6 +1021,14 @@ interface IAssetSourceConfig {
840
1021
  * default-on decision is a later phase (PRD-377 §8).
841
1022
  */
842
1023
  readonly lod?: boolean | IModelLodOptions | "none";
1024
+ /**
1025
+ * Also writes the native cooked package (`<output>/native/assets.tnpk`, TNPK v1) beside the
1026
+ * existing outputs: one Buffer entry per cooked binary buffer and one Texture entry per cooked
1027
+ * PNG or JPEG (decoded to RGBA8). Mesh, material and scene entries are out of scope until their
1028
+ * native loaders land.
1029
+ * Absent means off, so a web-only project pays nothing.
1030
+ */
1031
+ readonly nativePackage?: boolean;
843
1032
  readonly output?: string;
844
1033
  readonly source?: string;
845
1034
  readonly targets?: IAssetTargets;
@@ -850,6 +1039,11 @@ interface IAssetSourceConfig {
850
1039
  readonly textures?: ITexturesConfig | "none";
851
1040
  }
852
1041
  interface IModelsConfig {
1042
+ /**
1043
+ * Lossless scene-graph compaction (flatten → instance → join) against one protected-node set.
1044
+ * Default true ({@link IModelCompactOptions}); `false` ships the scene graph as authored.
1045
+ */
1046
+ readonly compact?: boolean | IModelCompactOptions;
853
1047
  /** Generate standard TEXCOORD_1 lightmap UVs. Absent means no lightmap pass. */
854
1048
  readonly lightmap?: ILightmapPassOptions;
855
1049
  readonly passes?: IModelPassesOptions;
@@ -884,7 +1078,8 @@ interface IAudioConfig {
884
1078
  readonly seamMaxRatio?: number;
885
1079
  }
886
1080
  interface ITexturesConfig {
887
- readonly maxSize?: number;
1081
+ readonly floor?: ITexturePassOptions["floor"];
1082
+ readonly maxSize?: TextureMaxSize;
888
1083
  readonly overrides?: readonly ITextureOverride[];
889
1084
  readonly quality?: number;
890
1085
  }
@@ -909,31 +1104,16 @@ interface IAssetCompileOptions {
909
1104
  */
910
1105
  readonly processingOrder?: "reversed" | "sorted";
911
1106
  /**
912
- * The platform this bake is for, which decides whether compression can ship at all.
913
- *
914
- * Android and iOS run the native host without WebAssembly, so they carry no Basis transcoder
915
- * and no Meshopt decoder: a `.ktx2` texture or a meshopt-compressed mesh in a mobile bundle is
916
- * a black screen, and `threenative build` refuses one with `TN_NATIVE_KTX2_UNSUPPORTED`. Web
917
- * and desktop decode both.
918
- *
919
- * Absent means web — a direct `compileAssets` call, or a project that compiles once and serves
920
- * the result. `threenative build` always names its `--target`, so the passes that a platform
921
- * cannot decode drop for that build and stay on for every other one. This is the whole reason
922
- * `assets.textures: "none"` used to be pinned in the scaffolded config: the author was asked to
923
- * choose one constant for four targets, and every game that wanted Android shipped its web
924
- * build uncompressed too. The build knows its target; it decides.
1107
+ * The conservative default decoder policy: web/desktop keep the existing loaders;
1108
+ * Android/iOS stay decoder-free until their packaged loaders are qualified. A V8 engine
1109
+ * alone does not establish a working Basis, Meshopt or Draco path. Absent means web.
1110
+ * Native builds provide runtimeDecoders from their selected artifact instead.
925
1111
  */
926
1112
  readonly platform?: "android" | "desktop" | "ios" | "web";
927
- /**
928
- * What the runtime that will execute this bake can decode, when the caller has probed it.
929
- *
930
- * `platform` is a proxy: it answers "android and iOS have no WebAssembly" for the targets this
931
- * repository ships. A desktop host can also lack WebAssembly — the Linux arm64 lane builds
932
- * QuickJS over wgpu-native — and then the same meshopt/KTX2 passes it would keep produce an
933
- * asset nothing can decode. `threenative build` reads the engine from the runtime binary it will
934
- * package and passes the real capability here; absent, the platform-derived default stands.
935
- */
1113
+ /** Which compressed outputs the selected runtime's bundle can decode, independently of WASM. */
936
1114
  readonly runtimeDecoders?: IAssetRuntimeDecoderCapabilities;
1115
+ /** Exact selected runtime/cohort identity; changing it invalidates cooked output. */
1116
+ readonly runtimeIdentity?: string;
937
1117
  readonly source?: string;
938
1118
  /** Overrides resolution of three's Basis transcoder for the copy into the output root. */
939
1119
  readonly transcoder?: IBasisTranscoder;
@@ -955,6 +1135,11 @@ interface IAssetCompileResult {
955
1135
  readonly skippedCompression: readonly ISkippedReportRow[];
956
1136
  readonly receipt?: IBakeReceipt;
957
1137
  readonly report?: IAssetHealthReport;
1138
+ /**
1139
+ * What the model cook saved by cooking content-identical sources once, when this project has
1140
+ * any model source. `cooked` is the number of distinct outputs those sources produced.
1141
+ */
1142
+ readonly dedupe?: IModelDedupeSummary;
958
1143
  readonly skipped: number;
959
1144
  readonly written: number;
960
1145
  }
@@ -1317,4 +1502,4 @@ declare function censusDocument(model: string, document: Document, pageSize?: nu
1317
1502
  /** Totals across a set of models, so a scene's census is one line rather than twenty-nine. */
1318
1503
  declare function totalCensus(entries: readonly IContentCensus[]): Omit<IContentCensus, "model">;
1319
1504
 
1320
- 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 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, 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 };
1505
+ 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 };