@threenative/assets 0.3.3 → 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.
@@ -4590,12 +4590,12 @@ var WriterContext = class {
4590
4590
  source: this.imageIndexMap.get(texture),
4591
4591
  sampler: this.samplerDefIndexMap.get(samplerKey)
4592
4592
  };
4593
- const textureKey = JSON.stringify(textureDef);
4594
- if (!this.textureDefIndexMap.has(textureKey)) {
4595
- this.textureDefIndexMap.set(textureKey, this.jsonDoc.json.textures.length);
4593
+ const textureKey2 = JSON.stringify(textureDef);
4594
+ if (!this.textureDefIndexMap.has(textureKey2)) {
4595
+ this.textureDefIndexMap.set(textureKey2, this.jsonDoc.json.textures.length);
4596
4596
  this.jsonDoc.json.textures.push(textureDef);
4597
4597
  }
4598
- const textureInfoDef = { index: this.textureDefIndexMap.get(textureKey) };
4598
+ const textureInfoDef = { index: this.textureDefIndexMap.get(textureKey2) };
4599
4599
  if (textureInfo.getTexCoord() !== 0) textureInfoDef.texCoord = textureInfo.getTexCoord();
4600
4600
  if (Object.keys(textureInfo.getExtras()).length > 0) textureInfoDef.extras = textureInfo.getExtras();
4601
4601
  this.textureInfoDefMap.set(textureInfo, textureInfoDef);
@@ -10789,7 +10789,7 @@ var TNDiscreteLod = class extends Extension {
10789
10789
  };
10790
10790
  function attachDiscreteLod(document, extension, primitive, chain) {
10791
10791
  const buffer = document.getRoot().listBuffers()[0] ?? document.createBuffer();
10792
- const property = extension.createDiscreteLod().setStrategy("discrete").setBaselineTriangles(chain.baselineTriangles).setLod0Triangles(chain.lod0Triangles).setErrorScale(chain.errorScale).setCounts([...chain.counts]).setErrors([...chain.errors]).setAbsoluteErrors([...chain.absoluteErrors]).setSharedVertexBuffers(true);
10792
+ const property = extension.createDiscreteLod().setStrategy(chain.strategy ?? "discrete").setBaselineTriangles(chain.baselineTriangles).setLod0Triangles(chain.lod0Triangles).setErrorScale(chain.errorScale).setCounts([...chain.counts]).setErrors([...chain.errors]).setAbsoluteErrors([...chain.absoluteErrors]).setSharedVertexBuffers(true);
10793
10793
  for (const indices of chain.indices) {
10794
10794
  property.addIndices(
10795
10795
  document.createAccessor().setArray(Uint32Array.from(indices)).setType("SCALAR").setBuffer(buffer)
@@ -11324,6 +11324,15 @@ function listNodeScenes(node) {
11324
11324
  }
11325
11325
  return child.listParents().filter((parent2) => parent2 instanceof Scene);
11326
11326
  }
11327
+ function clearNodeParent(node) {
11328
+ const scenes = listNodeScenes(node);
11329
+ const parent = node.getParentNode();
11330
+ if (!parent) return node;
11331
+ node.setMatrix(node.getWorldMatrix());
11332
+ parent.removeChild(node);
11333
+ for (const scene of scenes) scene.addChild(node);
11334
+ return node;
11335
+ }
11327
11336
  var ARRAY_TYPE2 = typeof Float32Array !== "undefined" ? Float32Array : Array;
11328
11337
  function invert$1(out, a) {
11329
11338
  var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
@@ -12191,6 +12200,12 @@ function hasModifier(prop, cache) {
12191
12200
  cache.set(prop, false);
12192
12201
  return false;
12193
12202
  }
12203
+ function dequantizeAttribute(attribute) {
12204
+ const srcArray = attribute.getArray();
12205
+ if (!srcArray) return;
12206
+ const dstArray = dequantizeAttributeArray(srcArray, attribute.getComponentType(), attribute.getNormalized());
12207
+ attribute.setArray(dstArray).setNormalized(false);
12208
+ }
12194
12209
  function dequantizeAttributeArray(srcArray, componentType, normalized) {
12195
12210
  const dstArray = new Float32Array(srcArray.length);
12196
12211
  for (let i = 0, il = srcArray.length; i < il; i++) if (normalized) dstArray[i] = MathUtils.decodeNormalizedInt(srcArray[i], componentType);
@@ -12572,6 +12587,121 @@ async function maybeGetPixels(texture) {
12572
12587
  return null;
12573
12588
  }
12574
12589
  }
12590
+ var NAME$19 = "instance";
12591
+ var INSTANCE_DEFAULTS = { min: 5 };
12592
+ function instance(_options = INSTANCE_DEFAULTS) {
12593
+ const options = assignDefaults(INSTANCE_DEFAULTS, _options);
12594
+ return createTransform(NAME$19, (doc) => {
12595
+ const logger = doc.getLogger();
12596
+ const root = doc.getRoot();
12597
+ if (root.listAnimations().length) {
12598
+ logger.warn(`${NAME$19}: Instancing is not currently supported for animated models.`);
12599
+ logger.debug(`${NAME$19}: Complete.`);
12600
+ return;
12601
+ }
12602
+ const batchExtension = doc.createExtension(EXTMeshGPUInstancing);
12603
+ let numBatches = 0;
12604
+ let numInstances = 0;
12605
+ for (const scene of root.listScenes()) {
12606
+ const meshInstances = /* @__PURE__ */ new Map();
12607
+ scene.traverse((node) => {
12608
+ const mesh = node.getMesh();
12609
+ if (!mesh) return;
12610
+ if (node.getExtension("EXT_mesh_gpu_instancing")) return;
12611
+ meshInstances.set(mesh, (meshInstances.get(mesh) || /* @__PURE__ */ new Set()).add(node));
12612
+ });
12613
+ const modifiedNodes = [];
12614
+ for (const mesh of Array.from(meshInstances.keys())) {
12615
+ const nodes = Array.from(meshInstances.get(mesh));
12616
+ if (nodes.length < options.min) continue;
12617
+ if (nodes.some((node) => node.getSkin())) continue;
12618
+ if (mesh.listPrimitives().some(hasVolume) && nodes.some(hasScale)) continue;
12619
+ const batch = createBatch(doc, batchExtension, mesh, nodes.length);
12620
+ const batchTranslation = batch.getAttribute("TRANSLATION");
12621
+ const batchRotation = batch.getAttribute("ROTATION");
12622
+ const batchScale = batch.getAttribute("SCALE");
12623
+ const batchNode = doc.createNode().setMesh(mesh).setExtension("EXT_mesh_gpu_instancing", batch);
12624
+ scene.addChild(batchNode);
12625
+ let needsTranslation = false;
12626
+ let needsRotation = false;
12627
+ let needsScale = false;
12628
+ for (let i = 0; i < nodes.length; i++) {
12629
+ let t, r, s;
12630
+ const node = nodes[i];
12631
+ batchTranslation.setElement(i, t = node.getWorldTranslation());
12632
+ batchRotation.setElement(i, r = node.getWorldRotation());
12633
+ batchScale.setElement(i, s = node.getWorldScale());
12634
+ if (!MathUtils.eq(t, [
12635
+ 0,
12636
+ 0,
12637
+ 0
12638
+ ])) needsTranslation = true;
12639
+ if (!MathUtils.eq(r, [
12640
+ 0,
12641
+ 0,
12642
+ 0,
12643
+ 1
12644
+ ])) needsRotation = true;
12645
+ if (!MathUtils.eq(s, [
12646
+ 1,
12647
+ 1,
12648
+ 1
12649
+ ])) needsScale = true;
12650
+ }
12651
+ if (!needsTranslation) batchTranslation.dispose();
12652
+ if (!needsRotation) batchRotation.dispose();
12653
+ if (!needsScale) batchScale.dispose();
12654
+ if (!needsTranslation && !needsRotation && !needsScale) {
12655
+ batchNode.dispose();
12656
+ batch.dispose();
12657
+ continue;
12658
+ }
12659
+ for (const node of nodes) {
12660
+ node.setMesh(null);
12661
+ modifiedNodes.push(node);
12662
+ }
12663
+ numBatches++;
12664
+ numInstances += nodes.length;
12665
+ }
12666
+ pruneUnusedNodes(modifiedNodes, logger);
12667
+ }
12668
+ if (numBatches > 0) logger.info(`${NAME$19}: Created ${numBatches} batches, with ${numInstances} total instances.`);
12669
+ else logger.info(`${NAME$19}: No meshes with >=${options.min} parent nodes were found.`);
12670
+ if (batchExtension.listProperties().length === 0) batchExtension.dispose();
12671
+ logger.debug(`${NAME$19}: Complete.`);
12672
+ });
12673
+ }
12674
+ function pruneUnusedNodes(nodes, logger) {
12675
+ let node;
12676
+ let unusedNodes = 0;
12677
+ while (node = nodes.pop()) {
12678
+ if (node.listChildren().length || node.getCamera() || node.getMesh() || node.getSkin() || node.listExtensions().length) continue;
12679
+ const nodeParent = node.getParentNode();
12680
+ if (nodeParent) nodes.push(nodeParent);
12681
+ node.dispose();
12682
+ unusedNodes++;
12683
+ }
12684
+ logger.debug(`${NAME$19}: Removed ${unusedNodes} unused nodes.`);
12685
+ }
12686
+ function hasVolume(prim) {
12687
+ const material = prim.getMaterial();
12688
+ return !!(material && material.getExtension("KHR_materials_volume"));
12689
+ }
12690
+ function hasScale(node) {
12691
+ const scale3 = node.getWorldScale();
12692
+ return !MathUtils.eq(scale3, [
12693
+ 1,
12694
+ 1,
12695
+ 1
12696
+ ]);
12697
+ }
12698
+ function createBatch(doc, batchExtension, mesh, count) {
12699
+ const buffer = mesh.listPrimitives()[0].getAttribute("POSITION").getBuffer();
12700
+ const batchTranslation = doc.createAccessor().setType("VEC3").setArray(new Float32Array(3 * count)).setBuffer(buffer);
12701
+ const batchRotation = doc.createAccessor().setType("VEC4").setArray(new Float32Array(4 * count)).setBuffer(buffer);
12702
+ const batchScale = doc.createAccessor().setType("VEC3").setArray(new Float32Array(3 * count)).setBuffer(buffer);
12703
+ return batchExtension.createInstancedMesh().setAttribute("TRANSLATION", batchTranslation).setAttribute("ROTATION", batchRotation).setAttribute("SCALE", batchScale);
12704
+ }
12575
12705
  var JOIN_PRIMITIVE_DEFAULTS = { skipValidation: false };
12576
12706
  var EMPTY_U32 = 2 ** 32 - 1;
12577
12707
  function joinPrimitives(prims, _options = {}) {
@@ -12656,7 +12786,145 @@ function remapIndices(srcIndices, remap, dstIndices, dstOffset) {
12656
12786
  }
12657
12787
  }
12658
12788
  }
12789
+ var NAME$18 = "join";
12659
12790
  var { ROOT, NODE, MESH, PRIMITIVE, ACCESSOR } = PropertyType;
12791
+ var _matrix = [
12792
+ 0,
12793
+ 0,
12794
+ 0,
12795
+ 0,
12796
+ 0,
12797
+ 0,
12798
+ 0,
12799
+ 0,
12800
+ 0,
12801
+ 0,
12802
+ 0,
12803
+ 0,
12804
+ 0,
12805
+ 0,
12806
+ 0,
12807
+ 0
12808
+ ];
12809
+ var JOIN_DEFAULTS = {
12810
+ keepMeshes: false,
12811
+ keepNamed: false,
12812
+ cleanup: true,
12813
+ filter: () => true
12814
+ };
12815
+ function join(_options = JOIN_DEFAULTS) {
12816
+ const options = assignDefaults(JOIN_DEFAULTS, _options);
12817
+ return createTransform(NAME$18, async (document) => {
12818
+ const root = document.getRoot();
12819
+ const logger = document.getLogger();
12820
+ const srcRestartModeCount = _getPrimRestartModeCount(document);
12821
+ for (const scene of root.listScenes()) {
12822
+ _joinLevel(document, scene, options);
12823
+ scene.traverse((node) => _joinLevel(document, node, options));
12824
+ }
12825
+ if (_getPrimRestartModeCount(document) < srcRestartModeCount) document.createExtension(KHRMeshPrimitiveRestart).setRequired(true);
12826
+ if (options.cleanup) await document.transform(prune({
12827
+ propertyTypes: [
12828
+ NODE,
12829
+ MESH,
12830
+ PRIMITIVE,
12831
+ ACCESSOR
12832
+ ],
12833
+ keepAttributes: true,
12834
+ keepIndices: true,
12835
+ keepLeaves: false
12836
+ }));
12837
+ logger.debug(`${NAME$18}: Complete.`);
12838
+ });
12839
+ }
12840
+ function _joinLevel(document, parent, options) {
12841
+ const logger = document.getLogger();
12842
+ const groups = {};
12843
+ const children = parent.listChildren();
12844
+ for (let nodeIndex = 0; nodeIndex < children.length; nodeIndex++) {
12845
+ const node = children[nodeIndex];
12846
+ if (!options.filter(node)) continue;
12847
+ if (node.listParents().some((p) => p instanceof AnimationChannel)) continue;
12848
+ const mesh = node.getMesh();
12849
+ if (!mesh) continue;
12850
+ if (node.getExtension("EXT_mesh_gpu_instancing")) continue;
12851
+ if (node.getSkin()) continue;
12852
+ for (const prim of mesh.listPrimitives()) {
12853
+ if (prim.listTargets().length > 0) continue;
12854
+ const material = prim.getMaterial();
12855
+ if (material && material.getExtension("KHR_materials_volume")) continue;
12856
+ compactPrimitive(prim);
12857
+ dequantizeTransformableAttributes(prim);
12858
+ let key = createPrimGroupKey(prim);
12859
+ const isNamed = mesh.getName() || node.getName();
12860
+ if (options.keepMeshes || options.keepNamed && isNamed) key += `|${nodeIndex}`;
12861
+ if (!(key in groups)) groups[key] = {
12862
+ prims: [],
12863
+ primMeshes: [],
12864
+ primNodes: [],
12865
+ dstNode: node,
12866
+ dstMesh: void 0
12867
+ };
12868
+ const group = groups[key];
12869
+ group.prims.push(prim);
12870
+ group.primNodes.push(node);
12871
+ }
12872
+ }
12873
+ const joinGroups = Object.values(groups).filter(({ prims }) => prims.length > 1);
12874
+ const srcNodes = new Set(joinGroups.flatMap((group) => group.primNodes));
12875
+ for (const node of srcNodes) {
12876
+ const mesh = node.getMesh();
12877
+ if (mesh.listParents().some((parent2) => {
12878
+ return parent2.propertyType !== ROOT && node !== parent2;
12879
+ })) node.setMesh(mesh.clone());
12880
+ }
12881
+ for (const group of joinGroups) {
12882
+ const { dstNode, primNodes } = group;
12883
+ group.dstMesh = dstNode.getMesh();
12884
+ group.primMeshes = primNodes.map((node) => node.getMesh());
12885
+ }
12886
+ for (const group of joinGroups) {
12887
+ const { prims, primNodes, primMeshes, dstNode, dstMesh } = group;
12888
+ const dstMatrix = dstNode.getMatrix();
12889
+ for (let i = 0; i < prims.length; i++) {
12890
+ const primNode = primNodes[i];
12891
+ const primMesh = primMeshes[i];
12892
+ let prim = prims[i];
12893
+ primMesh.removePrimitive(prim);
12894
+ if (isUsed(prim)) prim = prims[i] = _deepClonePrimitive(prims[i]);
12895
+ if (primNode !== dstNode) {
12896
+ multiply$2(_matrix, invert$1(_matrix, dstMatrix), primNode.getMatrix());
12897
+ transformPrimitive(prim, _matrix);
12898
+ }
12899
+ }
12900
+ const dstPrim = joinPrimitives(prims);
12901
+ const dstVertexCount = dstPrim.listAttributes()[0].getCount();
12902
+ dstMesh.addPrimitive(dstPrim);
12903
+ logger.debug(`${NAME$18}: Joined Primitives (${prims.length}) containing ${formatLong(dstVertexCount)} vertices under Node "${dstNode.getName()}".`);
12904
+ }
12905
+ }
12906
+ function _deepClonePrimitive(src) {
12907
+ const dst = src.clone();
12908
+ for (const semantic of dst.listSemantics()) dst.setAttribute(semantic, dst.getAttribute(semantic).clone());
12909
+ const indices = dst.getIndices();
12910
+ if (indices) dst.setIndices(indices.clone());
12911
+ return dst;
12912
+ }
12913
+ function _getPrimRestartModeCount(document) {
12914
+ let count = 0;
12915
+ for (const mesh of document.getRoot().listMeshes()) for (const prim of mesh.listPrimitives()) if (isPrimitiveRestartMode(prim.getMode())) count++;
12916
+ return count;
12917
+ }
12918
+ function dequantizeTransformableAttributes(prim) {
12919
+ for (const semantic of [
12920
+ "POSITION",
12921
+ "NORMAL",
12922
+ "TANGENT"
12923
+ ]) {
12924
+ const attribute = prim.getAttribute(semantic);
12925
+ if (attribute) dequantizeAttribute(attribute);
12926
+ }
12927
+ }
12660
12928
  function sortPrimitiveWeights(prim, limit = Infinity) {
12661
12929
  if (Number.isFinite(limit) && limit % 4 || limit <= 0) throw new Error(`Limit must be positive multiple of four.`);
12662
12930
  const vertexCount = prim.getAttribute("POSITION").getCount();
@@ -12964,7 +13232,7 @@ function transformBatch(document, batch, nodeTransform) {
12964
13232
  1,
12965
13233
  1
12966
13234
  ];
12967
- const instanceMatrix = [
13235
+ const instanceMatrix2 = [
12968
13236
  1,
12969
13237
  0,
12970
13238
  0,
@@ -12984,9 +13252,9 @@ function transformBatch(document, batch, nodeTransform) {
12984
13252
  ];
12985
13253
  const transformMatrix = fromTransform(nodeTransform);
12986
13254
  for (let i = 0, count = tpl.getCount(); i < count; i++) {
12987
- MathUtils.compose(instanceTranslation ? instanceTranslation.getElement(i, t) : T_IDENTITY, instanceRotation ? instanceRotation.getElement(i, r) : R_IDENTITY, instanceScale ? instanceScale.getElement(i, s) : S_IDENTITY, instanceMatrix);
12988
- multiply$2(instanceMatrix, instanceMatrix, transformMatrix);
12989
- MathUtils.decompose(instanceMatrix, t, r, s);
13255
+ MathUtils.compose(instanceTranslation ? instanceTranslation.getElement(i, t) : T_IDENTITY, instanceRotation ? instanceRotation.getElement(i, r) : R_IDENTITY, instanceScale ? instanceScale.getElement(i, s) : S_IDENTITY, instanceMatrix2);
13256
+ multiply$2(instanceMatrix2, instanceMatrix2, transformMatrix);
13257
+ MathUtils.decompose(instanceMatrix2, t, r, s);
12990
13258
  if (instanceTranslation) instanceTranslation.setElement(i, t);
12991
13259
  if (instanceRotation) instanceRotation.setElement(i, r);
12992
13260
  if (instanceScale) instanceScale.setElement(i, s);
@@ -13371,6 +13639,460 @@ function _simplifyPoints(document, prim, options) {
13371
13639
  return prim;
13372
13640
  }
13373
13641
 
13642
+ // src/content/dedupe-materials.ts
13643
+ function normalise(value) {
13644
+ if (value === void 0) return "\0";
13645
+ if (Array.isArray(value)) return value.map((entry) => normalise(entry)).join(",");
13646
+ if (typeof value === "number") {
13647
+ return Object.is(value, -0) ? "0" : String(value);
13648
+ }
13649
+ return String(value);
13650
+ }
13651
+ function materialSignature(material) {
13652
+ const parts = [];
13653
+ for (const [slot, texture] of Object.entries(material.textures).sort(
13654
+ ([left], [right]) => left < right ? -1 : 1
13655
+ ))
13656
+ parts.push(`t:${slot}=${texture ?? "\0"}`);
13657
+ for (const [name, value] of Object.entries(material.uniforms).sort(
13658
+ ([left], [right]) => left < right ? -1 : 1
13659
+ ))
13660
+ parts.push(`u:${name}=${normalise(value)}`);
13661
+ for (const [name, value] of Object.entries(material.flags).sort(
13662
+ ([left], [right]) => left < right ? -1 : 1
13663
+ ))
13664
+ parts.push(`f:${name}=${normalise(value)}`);
13665
+ return parts.join("|");
13666
+ }
13667
+ function dedupeMaterials(materials) {
13668
+ const bySignature = /* @__PURE__ */ new Map();
13669
+ for (let index = 0; index < materials.length; index += 1) {
13670
+ const material = materials[index];
13671
+ if (material === void 0) continue;
13672
+ const signature = materialSignature(material);
13673
+ const members = bySignature.get(signature);
13674
+ const name = material.name ?? `material-${String(index)}`;
13675
+ if (members === void 0) bySignature.set(signature, [name]);
13676
+ else members.push(name);
13677
+ }
13678
+ const buckets = [...bySignature.entries()].map(([signature, members]) => ({
13679
+ members,
13680
+ signature
13681
+ }));
13682
+ return {
13683
+ buckets,
13684
+ census: {
13685
+ buckets: buckets.length,
13686
+ materials: materials.length,
13687
+ singletons: buckets.filter((bucket) => bucket.members.length === 1).length
13688
+ }
13689
+ };
13690
+ }
13691
+
13692
+ // src/content/census.ts
13693
+ function textureKey(texture) {
13694
+ if (texture === null) return void 0;
13695
+ const uri = texture.getURI();
13696
+ if (uri !== "") return uri;
13697
+ const name = texture.getName();
13698
+ if (name !== "") return `name:${name}`;
13699
+ const image = texture.getImage();
13700
+ return image === null ? void 0 : `bytes:${String(image.byteLength)}:${String(image[0] ?? 0)}`;
13701
+ }
13702
+ function materialStateOf(material) {
13703
+ const textures = {
13704
+ baseColor: textureKey(material.getBaseColorTexture()),
13705
+ emissive: textureKey(material.getEmissiveTexture()),
13706
+ metallicRoughness: textureKey(material.getMetallicRoughnessTexture()),
13707
+ normal: textureKey(material.getNormalTexture()),
13708
+ occlusion: textureKey(material.getOcclusionTexture())
13709
+ };
13710
+ return {
13711
+ flags: {
13712
+ alphaMode: material.getAlphaMode(),
13713
+ doubleSided: material.getDoubleSided()
13714
+ },
13715
+ name: material.getName(),
13716
+ textures,
13717
+ uniforms: {
13718
+ alphaCutoff: material.getAlphaCutoff(),
13719
+ baseColorFactor: [...material.getBaseColorFactor()],
13720
+ emissiveFactor: [...material.getEmissiveFactor()],
13721
+ metallic: material.getMetallicFactor(),
13722
+ normalScale: material.getNormalScale(),
13723
+ occlusionStrength: material.getOcclusionStrength(),
13724
+ roughness: material.getRoughnessFactor()
13725
+ }
13726
+ };
13727
+ }
13728
+
13729
+ // src/foliage.ts
13730
+ var CUTOUT_ALPHA_CUTOFF = 0.5;
13731
+ var VOLUME_EXTENSIONS = ["transmission", "volume", "displacement"];
13732
+ function cutoutSkipReason(material, primitives) {
13733
+ if (material.getBaseColorTexture() === null) return "no-texture-alpha";
13734
+ if ((material.getBaseColorFactor()[3] ?? 1) < 1) return "uniform-transparency";
13735
+ for (const extension of material.listExtensions()) {
13736
+ const name = extension.extensionName.toLowerCase();
13737
+ if (VOLUME_EXTENSIONS.some((needle) => name.includes(needle))) return "volume-or-transmission";
13738
+ }
13739
+ if (primitives.some(vertexAlphaVaries)) return "vertex-colour-alpha";
13740
+ return null;
13741
+ }
13742
+ function vertexAlphaVaries(primitive) {
13743
+ const color = primitive.getAttribute("COLOR_0");
13744
+ if (color === null || color.getElementSize() !== 4) return false;
13745
+ const element = [0, 0, 0, 0];
13746
+ for (let vertex = 0; vertex < color.getCount(); vertex += 1) {
13747
+ color.getElement(vertex, element);
13748
+ if (element[3] < 1) return true;
13749
+ }
13750
+ return false;
13751
+ }
13752
+ function usesMaterial(primitives, material) {
13753
+ return primitives.filter((primitive) => primitive.getMaterial() === material);
13754
+ }
13755
+ function convertFoliageCutout(document) {
13756
+ const root = document.getRoot();
13757
+ const primitives = root.listMeshes().flatMap((mesh) => [...mesh.listPrimitives()]);
13758
+ const converted = [];
13759
+ const kept = [];
13760
+ for (const material of root.listMaterials()) {
13761
+ if (material.getAlphaMode() !== "BLEND") continue;
13762
+ const name = material.getName();
13763
+ const reason = cutoutSkipReason(material, usesMaterial(primitives, material));
13764
+ if (reason !== null) {
13765
+ kept.push({ name, reason });
13766
+ continue;
13767
+ }
13768
+ material.setAlphaMode("MASK").setAlphaCutoff(CUTOUT_ALPHA_CUTOFF);
13769
+ converted.push(name);
13770
+ }
13771
+ return { converted, kept };
13772
+ }
13773
+ function mergeIdenticalMaterials(document) {
13774
+ const root = document.getRoot();
13775
+ const materials = root.listMaterials();
13776
+ const states = materials.map(materialStateOf);
13777
+ const before = dedupeMaterials(states).census;
13778
+ const groups = /* @__PURE__ */ new Map();
13779
+ states.forEach((state, index) => {
13780
+ const signature = materialSignature(state);
13781
+ const group = groups.get(signature);
13782
+ if (group === void 0) groups.set(signature, [index]);
13783
+ else group.push(index);
13784
+ });
13785
+ const merged = [];
13786
+ for (const group of groups.values()) {
13787
+ const [keeper, ...victims] = group;
13788
+ const keep = keeper === void 0 ? void 0 : materials[keeper];
13789
+ if (keep === void 0) continue;
13790
+ for (const index of victims) {
13791
+ const victim = materials[index];
13792
+ if (victim === void 0 || victim === keep) continue;
13793
+ for (const mesh of root.listMeshes()) {
13794
+ for (const primitive of mesh.listPrimitives()) {
13795
+ if (primitive.getMaterial() === victim) primitive.setMaterial(keep);
13796
+ }
13797
+ }
13798
+ merged.push(victim.getName());
13799
+ victim.dispose();
13800
+ }
13801
+ }
13802
+ const after = root.listMaterials();
13803
+ return {
13804
+ distinct: {
13805
+ after: dedupeMaterials(after.map(materialStateOf)).census.buckets,
13806
+ before: before.buckets
13807
+ },
13808
+ materials: { after: after.length, before: before.materials },
13809
+ merged
13810
+ };
13811
+ }
13812
+
13813
+ // src/lod/cards.ts
13814
+ var MAX_CARD_TRIANGLES = 8;
13815
+ var CARD_KEEP_RATIOS = [0.5, 0.25];
13816
+ var TRIANGLE = 3;
13817
+ function findCards(positions, indices) {
13818
+ const vertexCount = Math.floor(positions.length / 3);
13819
+ const parent = new Int32Array(vertexCount);
13820
+ for (let vertex = 0; vertex < vertexCount; vertex += 1) parent[vertex] = vertex;
13821
+ const find = (start) => {
13822
+ let root = start;
13823
+ while (parent[root] !== root) root = parent[root];
13824
+ let walk = start;
13825
+ while (parent[walk] !== root) {
13826
+ const next = parent[walk];
13827
+ parent[walk] = root;
13828
+ walk = next;
13829
+ }
13830
+ return root;
13831
+ };
13832
+ for (let triangle = 0; triangle + 2 < indices.length; triangle += TRIANGLE) {
13833
+ const a = find(indices[triangle]);
13834
+ const b = find(indices[triangle + 1]);
13835
+ const c = find(indices[triangle + 2]);
13836
+ if (a === c) continue;
13837
+ const low = Math.min(a, b, c);
13838
+ for (const root of [a, b, c]) if (root !== low) parent[root] = low;
13839
+ }
13840
+ const grouped = /* @__PURE__ */ new Map();
13841
+ for (let triangle = 0; triangle + 2 < indices.length; triangle += TRIANGLE) {
13842
+ const root = find(indices[triangle]);
13843
+ const group = grouped.get(root);
13844
+ if (group === void 0) grouped.set(root, [triangle]);
13845
+ else group.push(triangle);
13846
+ if ((group?.length ?? 1) > MAX_CARD_TRIANGLES) return null;
13847
+ }
13848
+ const cards = [];
13849
+ for (const triangles of grouped.values()) {
13850
+ if (triangles.length === 0) continue;
13851
+ let area = 0;
13852
+ let sx = 0;
13853
+ let sy = 0;
13854
+ let sz = 0;
13855
+ let vertices = 0;
13856
+ const seen = /* @__PURE__ */ new Set();
13857
+ for (const triangle of triangles) {
13858
+ for (let corner = 0; corner < TRIANGLE; corner += 1) {
13859
+ const vertex = indices[triangle + corner];
13860
+ if (seen.has(vertex)) continue;
13861
+ seen.add(vertex);
13862
+ vertices += 1;
13863
+ sx += positions[vertex * 3];
13864
+ sy += positions[vertex * 3 + 1];
13865
+ sz += positions[vertex * 3 + 2];
13866
+ }
13867
+ const a = indices[triangle] * 3;
13868
+ const b = indices[triangle + 1] * 3;
13869
+ const c = indices[triangle + 2] * 3;
13870
+ const ux = positions[b] - positions[a];
13871
+ const uy = positions[b + 1] - positions[a + 1];
13872
+ const uz = positions[b + 2] - positions[a + 2];
13873
+ const vx = positions[c] - positions[a];
13874
+ const vy = positions[c + 1] - positions[a + 1];
13875
+ const vz = positions[c + 2] - positions[a + 2];
13876
+ const nx = uy * vz - uz * vy;
13877
+ const ny = uz * vx - ux * vz;
13878
+ const nz = ux * vy - uy * vx;
13879
+ area += Math.sqrt(nx * nx + ny * ny + nz * nz) / 2;
13880
+ }
13881
+ cards.push({
13882
+ area,
13883
+ centroid: vertices === 0 ? [0, 0, 0] : [sx / vertices, sy / vertices, sz / vertices],
13884
+ triangles
13885
+ });
13886
+ }
13887
+ return cards.length > 0 ? cards : null;
13888
+ }
13889
+ function positionKey(centroid, quantum) {
13890
+ const q = (value) => Math.round(value / quantum) | 0;
13891
+ let hash = q(centroid[0]) * 73856093 ^ q(centroid[1]) * 19349663 ^ q(centroid[2]) * 83492791;
13892
+ hash = Math.imul(hash ^ hash >>> 15, 739982445);
13893
+ hash = Math.imul(hash ^ hash >>> 12, 695872825);
13894
+ return hash ^ hash >>> 15 | 0;
13895
+ }
13896
+ function placeCards(cards, cellSize) {
13897
+ const placed = cards.map((card, index) => {
13898
+ const [x, y, z] = card.centroid;
13899
+ return {
13900
+ card,
13901
+ // One integer key per cell: the grid is a hash, not a sort order.
13902
+ cell: Math.floor(x / cellSize) * 73856093 ^ Math.floor(y / cellSize) * 19349663 ^ Math.floor(z / cellSize) * 83492791,
13903
+ first: index,
13904
+ key: positionKey(card.centroid, cellSize / 32)
13905
+ };
13906
+ });
13907
+ return { cells: new Set(placed.map((entry) => entry.cell)).size, placed };
13908
+ }
13909
+ function orderPlaced(a, b) {
13910
+ return a.key - b.key || a.card.centroid[0] - b.card.centroid[0] || a.card.centroid[1] - b.card.centroid[1] || a.card.centroid[2] - b.card.centroid[2] || a.first - b.first;
13911
+ }
13912
+ function bucketsOf(placed) {
13913
+ const buckets = /* @__PURE__ */ new Map();
13914
+ for (const entry of placed) {
13915
+ const bucket = buckets.get(entry.cell);
13916
+ if (bucket === void 0) buckets.set(entry.cell, [entry]);
13917
+ else bucket.push(entry);
13918
+ }
13919
+ const cells = [...buckets.keys()].sort((a, b) => a - b);
13920
+ for (const cell of cells) buckets.get(cell).sort(orderPlaced);
13921
+ return { buckets, cells };
13922
+ }
13923
+ function thinLevel(placed, keep, lod0Area, lod0Cells) {
13924
+ const { buckets, cells: order } = bucketsOf(placed);
13925
+ const kept = [];
13926
+ let area = 0;
13927
+ let cells = 0;
13928
+ for (const cell of order) {
13929
+ const bucket = buckets.get(cell);
13930
+ const survivors = Math.max(1, Math.min(bucket.length, Math.round(keep * bucket.length)));
13931
+ for (const entry of bucket.slice(0, survivors)) {
13932
+ kept.push(entry.card);
13933
+ area += entry.card.area;
13934
+ }
13935
+ cells += 1;
13936
+ }
13937
+ return {
13938
+ cards: kept,
13939
+ summary: {
13940
+ areaCoverage: lod0Area === 0 ? 1 : area / lod0Area,
13941
+ cards: kept.length,
13942
+ cellCoverage: lod0Cells === 0 ? 1 : cells / lod0Cells,
13943
+ keep
13944
+ }
13945
+ };
13946
+ }
13947
+ function indicesOf(cards, source) {
13948
+ let length4 = 0;
13949
+ for (const card of cards) length4 += card.triangles.length * TRIANGLE;
13950
+ const out = new Uint32Array(length4);
13951
+ let at = 0;
13952
+ for (const card of cards)
13953
+ for (const triangle of card.triangles) {
13954
+ out[at] = source[triangle];
13955
+ out[at + 1] = source[triangle + 1];
13956
+ out[at + 2] = source[triangle + 2];
13957
+ at += TRIANGLE;
13958
+ }
13959
+ return out;
13960
+ }
13961
+ function cardTriangles(card) {
13962
+ return card.triangles.length;
13963
+ }
13964
+ function terminalCardLevel(cards, lod0Placed, lod0Cells, lod0Area, cellSize, target, previousTriangles) {
13965
+ if (target >= previousTriangles) return null;
13966
+ let factor = 1;
13967
+ let { buckets, cells } = bucketsOf(lod0Placed);
13968
+ const baseTriangles = () => cells.reduce(
13969
+ (total, cell) => total + cardTriangles(buckets.get(cell)[0]?.card),
13970
+ 0
13971
+ );
13972
+ while (baseTriangles() > target && cells.length > 1) {
13973
+ factor *= 2;
13974
+ ({ buckets, cells } = bucketsOf(placeCards(cards, cellSize * factor).placed));
13975
+ }
13976
+ const kept = [];
13977
+ const keptSet = /* @__PURE__ */ new Set();
13978
+ let triangles = 0;
13979
+ for (const cell of cells) {
13980
+ const first = buckets.get(cell)[0];
13981
+ kept.push(first.card);
13982
+ keptSet.add(first.card);
13983
+ triangles += cardTriangles(first.card);
13984
+ }
13985
+ for (const cell of cells) {
13986
+ const bucket = buckets.get(cell);
13987
+ for (let index = 1; index < bucket.length; index += 1) {
13988
+ const card = bucket[index].card;
13989
+ const cost = cardTriangles(card);
13990
+ if (triangles + cost > target) continue;
13991
+ kept.push(card);
13992
+ keptSet.add(card);
13993
+ triangles += cost;
13994
+ }
13995
+ }
13996
+ if (kept.length === 0) return null;
13997
+ const covered = /* @__PURE__ */ new Set();
13998
+ for (const entry of lod0Placed) if (keptSet.has(entry.card)) covered.add(entry.cell);
13999
+ let area = 0;
14000
+ for (const card of kept) area += card.area;
14001
+ return {
14002
+ cards: kept,
14003
+ factor,
14004
+ summary: {
14005
+ areaCoverage: lod0Area === 0 ? 1 : area / lod0Area,
14006
+ cards: kept.length,
14007
+ cellCoverage: lod0Cells === 0 ? 1 : covered.size / lod0Cells,
14008
+ keep: kept.length / cards.length
14009
+ }
14010
+ };
14011
+ }
14012
+ function generateCardChain(positions, indices, maxLevels, minSaving, scale3, keepRatios = CARD_KEEP_RATIOS, terminalTriangles) {
14013
+ const cards = findCards(positions, indices);
14014
+ if (cards === null) return null;
14015
+ const lod0Triangles = Math.floor(indices.length / TRIANGLE);
14016
+ if (lod0Triangles <= 0) return null;
14017
+ let minX = Number.POSITIVE_INFINITY;
14018
+ let minY = Number.POSITIVE_INFINITY;
14019
+ let minZ = Number.POSITIVE_INFINITY;
14020
+ let maxX = Number.NEGATIVE_INFINITY;
14021
+ let maxY = Number.NEGATIVE_INFINITY;
14022
+ let maxZ = Number.NEGATIVE_INFINITY;
14023
+ for (let vertex = 0; vertex + 2 < positions.length; vertex += 3) {
14024
+ const x = positions[vertex];
14025
+ const y = positions[vertex + 1];
14026
+ const z = positions[vertex + 2];
14027
+ if (x < minX) minX = x;
14028
+ if (y < minY) minY = y;
14029
+ if (z < minZ) minZ = z;
14030
+ if (x > maxX) maxX = x;
14031
+ if (y > maxY) maxY = y;
14032
+ if (z > maxZ) maxZ = z;
14033
+ }
14034
+ const diagonal = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ);
14035
+ const cellSize = Math.max(diagonal / Math.cbrt(cards.length), 1e-6);
14036
+ const { cells: lod0Cells, placed } = placeCards(cards, cellSize);
14037
+ const lod0Area = cards.reduce((total, card) => total + card.area, 0);
14038
+ const levels = [];
14039
+ const counts = [];
14040
+ const errors = [];
14041
+ const absoluteErrors = [];
14042
+ const buffers = [];
14043
+ let previousTriangles = lod0Triangles;
14044
+ for (const keep of keepRatios.slice(0, Math.max(0, maxLevels - 1))) {
14045
+ if (levels.length >= Math.max(0, maxLevels - 1)) break;
14046
+ const level = thinLevel(placed, keep, lod0Area, lod0Cells);
14047
+ if (level.cards.length === 0 || level.cards.length === cards.length) continue;
14048
+ const buffer = indicesOf(level.cards, indices);
14049
+ const triangles = Math.floor(buffer.length / TRIANGLE);
14050
+ if (triangles >= previousTriangles) continue;
14051
+ if ((previousTriangles - triangles) / previousTriangles < minSaving) continue;
14052
+ const absoluteError = cellSize * (1 - keep);
14053
+ levels.push(level.summary);
14054
+ counts.push(triangles);
14055
+ absoluteErrors.push(absoluteError);
14056
+ errors.push(absoluteError / Math.max(scale3, 1e-6));
14057
+ buffers.push(buffer);
14058
+ previousTriangles = triangles;
14059
+ }
14060
+ if (levels.length === 0) return null;
14061
+ if (terminalTriangles !== void 0 && terminalTriangles < previousTriangles) {
14062
+ const terminal = terminalCardLevel(
14063
+ cards,
14064
+ placed,
14065
+ lod0Cells,
14066
+ lod0Area,
14067
+ cellSize,
14068
+ terminalTriangles,
14069
+ previousTriangles
14070
+ );
14071
+ if (terminal !== null) {
14072
+ const buffer = indicesOf(terminal.cards, indices);
14073
+ const triangles = Math.floor(buffer.length / TRIANGLE);
14074
+ if (triangles > 0 && triangles < previousTriangles) {
14075
+ const absoluteError = cellSize * terminal.factor;
14076
+ levels.push(terminal.summary);
14077
+ counts.push(triangles);
14078
+ absoluteErrors.push(absoluteError);
14079
+ errors.push(absoluteError / Math.max(scale3, 1e-6));
14080
+ buffers.push(buffer);
14081
+ previousTriangles = triangles;
14082
+ }
14083
+ }
14084
+ }
14085
+ return {
14086
+ absoluteErrors,
14087
+ counts,
14088
+ errors,
14089
+ indices: buffers,
14090
+ levels,
14091
+ lod0Cards: cards.length,
14092
+ lod0Triangles
14093
+ };
14094
+ }
14095
+
13374
14096
  // src/lod/eligibility.ts
13375
14097
  var SUPPORTED_ATTRIBUTES = /* @__PURE__ */ new Set([
13376
14098
  "COLOR_0",
@@ -13380,13 +14102,16 @@ var SUPPORTED_ATTRIBUTES = /* @__PURE__ */ new Set([
13380
14102
  "TEXCOORD_0",
13381
14103
  "TEXCOORD_1"
13382
14104
  ]);
14105
+ function sharedByLevels(semantic) {
14106
+ return SUPPORTED_ATTRIBUTES.has(semantic) || semantic.startsWith("_");
14107
+ }
13383
14108
  var DEFORMING_ATTRIBUTES = /* @__PURE__ */ new Set(["JOINTS_0", "WEIGHTS_0"]);
13384
14109
  var TRIANGLES_MODE = 4;
13385
- var TRIANGLE = 3;
14110
+ var TRIANGLE2 = 3;
13386
14111
  function triangleCount(primitive) {
13387
14112
  const position = primitive.getAttribute("POSITION");
13388
14113
  const drawn = primitive.getIndices()?.getCount() ?? position?.getCount() ?? 0;
13389
- return Math.floor(drawn / TRIANGLE);
14114
+ return Math.floor(drawn / TRIANGLE2);
13390
14115
  }
13391
14116
  function finitePositions(primitive) {
13392
14117
  const position = primitive.getAttribute("POSITION");
@@ -13405,7 +14130,7 @@ function validIndices(primitive) {
13405
14130
  const indices = primitive.getIndices();
13406
14131
  if (indices === null) return position !== null && position.getCount() > 0;
13407
14132
  const array = indices.getArray();
13408
- if (array === null || array.length === 0 || array.length % TRIANGLE !== 0) return false;
14133
+ if (array === null || array.length === 0 || array.length % TRIANGLE2 !== 0) return false;
13409
14134
  const vertices = position?.getCount() ?? 0;
13410
14135
  for (let index = 0; index < array.length; index += 1) {
13411
14136
  const value = array[index];
@@ -13415,7 +14140,8 @@ function validIndices(primitive) {
13415
14140
  }
13416
14141
  function unsupportedMaterial(material) {
13417
14142
  if (material === null) return false;
13418
- if (material.getAlphaMode() !== "OPAQUE") return true;
14143
+ const alphaMode = material.getAlphaMode();
14144
+ if (alphaMode !== "OPAQUE" && alphaMode !== "MASK") return true;
13419
14145
  for (const extension of material.listExtensions()) {
13420
14146
  const name = extension.extensionName.toLowerCase();
13421
14147
  if (name.includes("transmission") || name.includes("displacement") || name.includes("volume"))
@@ -13461,8 +14187,7 @@ function classifyPrimitive(primitive, flags) {
13461
14187
  const semantics = primitive.listSemantics();
13462
14188
  for (const semantic of semantics) {
13463
14189
  if (DEFORMING_ATTRIBUTES.has(semantic)) return { eligible: false, reason: "deforming" };
13464
- if (!SUPPORTED_ATTRIBUTES.has(semantic))
13465
- return { eligible: false, reason: "unsupported-attributes" };
14190
+ if (!sharedByLevels(semantic)) return { eligible: false, reason: "unsupported-attributes" };
13466
14191
  }
13467
14192
  if (!semantics.includes("POSITION")) return { eligible: false, reason: "unsupported-topology" };
13468
14193
  if (!finitePositions(primitive) || !validIndices(primitive))
@@ -13483,8 +14208,7 @@ function classifyJoinCandidate(primitive, flags) {
13483
14208
  const semantics = primitive.listSemantics();
13484
14209
  for (const semantic of semantics) {
13485
14210
  if (DEFORMING_ATTRIBUTES.has(semantic)) return { eligible: false, reason: "deforming" };
13486
- if (!SUPPORTED_ATTRIBUTES.has(semantic))
13487
- return { eligible: false, reason: "unsupported-attributes" };
14211
+ if (!sharedByLevels(semantic)) return { eligible: false, reason: "unsupported-attributes" };
13488
14212
  }
13489
14213
  if (!semantics.includes("POSITION")) return { eligible: false, reason: "unsupported-topology" };
13490
14214
  if (!finitePositions(primitive) || !validIndices(primitive))
@@ -13497,12 +14221,18 @@ function authoredLodName(name) {
13497
14221
 
13498
14222
  // src/lod/generate.ts
13499
14223
  var LOD_GENERATOR = "threenative-discrete-lod";
13500
- var LOD_GENERATOR_VERSION = 1;
14224
+ var LOD_GENERATOR_VERSION = 3;
13501
14225
  var LOD_ARTIFACT_SCHEMA_VERSION = 1;
13502
14226
  var LOD_TOOLCHAIN = "meshoptimizer@1.1.1";
13503
14227
  var LOD_ERROR_TARGETS = [2e-3, 6e-3, 0.02, 0.06];
13504
14228
  var LOD_MIN_SAVING = 0.2;
13505
14229
  var DEFAULT_LOD_MAX_LEVELS = 4;
14230
+ var LOD_TERMINAL_MIN_TRIANGLES = 1500;
14231
+ var LOD_TERMINAL_KEEP_RATIO = 0.05;
14232
+ function terminalLevelTriangles(lod0Triangles) {
14233
+ return Math.max(LOD_TERMINAL_MIN_TRIANGLES, Math.ceil(LOD_TERMINAL_KEEP_RATIO * lod0Triangles));
14234
+ }
14235
+ var TERMINAL_TARGET_ERROR = 1e9;
13506
14236
  var DEFAULT_LOD_MIN_TRIANGLES = 128;
13507
14237
  var DEFAULT_LOD_MIN_TRIANGLES_SCOPE = "asset";
13508
14238
  var DEFAULT_LOD_HYSTERESIS = 0.15;
@@ -13587,8 +14317,10 @@ function resolveLodPolicy(lod, asset, legacy = {}) {
13587
14317
  }
13588
14318
  }
13589
14319
  if (!enabled && reasons.length === 0) reasons.push("disabled");
14320
+ const cutout = !globalOff && (assetBlock?.cutout ?? project?.cutout ?? true);
13590
14321
  const generationFingerprint = lodFingerprint({
13591
14322
  algorithm: `${LOD_GENERATOR}/${String(LOD_GENERATOR_VERSION)}`,
14323
+ cutout,
13592
14324
  enabled,
13593
14325
  errorTargets: generation.errorTargets,
13594
14326
  join: generation.join,
@@ -13600,6 +14332,7 @@ function resolveLodPolicy(lod, asset, legacy = {}) {
13600
14332
  toolchain: LOD_TOOLCHAIN
13601
14333
  });
13602
14334
  return {
14335
+ cutout,
13603
14336
  diagnostics,
13604
14337
  enabled,
13605
14338
  fingerprint: {
@@ -13669,7 +14402,7 @@ function sourceIndices(primitive, vertexCount) {
13669
14402
  return Uint32Array.from(indices.getArray());
13670
14403
  }
13671
14404
  function packAttributes(primitive, vertexCount) {
13672
- const semantics = primitive.listSemantics().filter((semantic) => semantic !== "POSITION");
14405
+ const semantics = primitive.listSemantics().filter((semantic) => semantic !== "POSITION" && !semantic.startsWith("_"));
13673
14406
  const accessors = semantics.map((semantic) => ({ accessor: primitive.getAttribute(semantic), semantic })).filter(
13674
14407
  (entry) => entry.accessor !== null
13675
14408
  );
@@ -13725,6 +14458,16 @@ async function generateChain(primitive, maxLevels, errorTargets, minSaving) {
13725
14458
  if (kept.length === 0) return null;
13726
14459
  const keptSet = new Set(kept);
13727
14460
  const chain = candidates.filter((candidate) => keptSet.has(candidate));
14461
+ const previous = chain[chain.length - 1];
14462
+ const terminal = terminalLevel(
14463
+ positions,
14464
+ indices,
14465
+ attributes,
14466
+ terminalLevelTriangles(lod0Triangles),
14467
+ previous.triangles,
14468
+ previous.error
14469
+ );
14470
+ if (terminal !== null) chain.push(terminal);
13728
14471
  return {
13729
14472
  absoluteErrors: chain.map((level) => level.error * scale3),
13730
14473
  baselineTriangles: lod0Triangles,
@@ -13735,6 +14478,158 @@ async function generateChain(primitive, maxLevels, errorTargets, minSaving) {
13735
14478
  lod0Triangles
13736
14479
  };
13737
14480
  }
14481
+ function weldMesh(positions, indices, attributes) {
14482
+ const vertexCount = Math.floor(positions.length / 3);
14483
+ const seen = /* @__PURE__ */ new Map();
14484
+ const remap = new Uint32Array(vertexCount);
14485
+ const origin = [];
14486
+ for (let vertex = 0; vertex < vertexCount; vertex += 1) {
14487
+ const key = `${positions[vertex * 3]},${positions[vertex * 3 + 1]},${positions[vertex * 3 + 2]}`;
14488
+ const compact = seen.get(key);
14489
+ if (compact === void 0) {
14490
+ seen.set(key, origin.length);
14491
+ remap[vertex] = origin.length;
14492
+ origin.push(vertex);
14493
+ } else remap[vertex] = compact;
14494
+ }
14495
+ const compactPositions = new Float32Array(origin.length * 3);
14496
+ const compactAttributes = new Float32Array(origin.length * attributes.stride);
14497
+ for (let compact = 0; compact < origin.length; compact += 1) {
14498
+ const source = origin[compact];
14499
+ for (let axis = 0; axis < 3; axis += 1)
14500
+ compactPositions[compact * 3 + axis] = positions[source * 3 + axis];
14501
+ for (let slot = 0; slot < attributes.stride; slot += 1)
14502
+ compactAttributes[compact * attributes.stride + slot] = attributes.data[source * attributes.stride + slot];
14503
+ }
14504
+ const compactIndices = new Uint32Array(indices.length);
14505
+ for (let index = 0; index < indices.length; index += 1)
14506
+ compactIndices[index] = remap[indices[index]];
14507
+ return {
14508
+ attributes: compactAttributes,
14509
+ indices: compactIndices,
14510
+ origin: Uint32Array.from(origin),
14511
+ positions: compactPositions
14512
+ };
14513
+ }
14514
+ function terminalLevel(positions, indices, attributes, terminalTriangles, previousTriangles, previousError) {
14515
+ if (terminalTriangles >= previousTriangles) return null;
14516
+ const target = terminalTriangles * 3;
14517
+ if (attributes.stride > 0) {
14518
+ const [pruned, pruneError] = MeshoptSimplifier.simplifyWithAttributes(
14519
+ indices,
14520
+ positions,
14521
+ 3,
14522
+ attributes.data,
14523
+ attributes.stride,
14524
+ attributes.weights,
14525
+ null,
14526
+ target,
14527
+ TERMINAL_TARGET_ERROR,
14528
+ ["Prune"]
14529
+ );
14530
+ const triangles2 = Math.floor(pruned.length / 3);
14531
+ if (triangles2 > 0 && triangles2 <= terminalTriangles && triangles2 < previousTriangles)
14532
+ return {
14533
+ error: Math.max(pruneError, previousError),
14534
+ indices: Uint32Array.from(pruned),
14535
+ triangles: triangles2
14536
+ };
14537
+ }
14538
+ const welded = weldMesh(positions, indices, attributes);
14539
+ let compacted;
14540
+ let weldedError;
14541
+ if (attributes.stride === 0) {
14542
+ [compacted, weldedError] = MeshoptSimplifier.simplify(
14543
+ welded.indices,
14544
+ welded.positions,
14545
+ 3,
14546
+ target,
14547
+ TERMINAL_TARGET_ERROR,
14548
+ []
14549
+ );
14550
+ } else {
14551
+ [compacted, weldedError] = MeshoptSimplifier.simplifyWithAttributes(
14552
+ welded.indices,
14553
+ welded.positions,
14554
+ 3,
14555
+ welded.attributes,
14556
+ attributes.stride,
14557
+ new Array(attributes.stride).fill(0),
14558
+ null,
14559
+ target,
14560
+ TERMINAL_TARGET_ERROR,
14561
+ []
14562
+ );
14563
+ }
14564
+ const triangles = Math.floor(compacted.length / 3);
14565
+ if (triangles <= 0 || triangles >= previousTriangles) return null;
14566
+ const mapped = new Uint32Array(compacted.length);
14567
+ for (let index = 0; index < compacted.length; index += 1)
14568
+ mapped[index] = welded.origin[compacted[index]];
14569
+ return {
14570
+ error: Math.max(weldedError, previousError),
14571
+ indices: mapped,
14572
+ triangles
14573
+ };
14574
+ }
14575
+ function generateTerminalChain(primitive) {
14576
+ const position = primitive.getAttribute("POSITION");
14577
+ if (position === null) return null;
14578
+ const positions = positionsOf(primitive);
14579
+ if (positions === null) return null;
14580
+ const vertexCount = position.getCount();
14581
+ const indices = sourceIndices(primitive, vertexCount);
14582
+ const lod0Triangles = Math.floor(indices.length / 3);
14583
+ if (lod0Triangles <= 0) return null;
14584
+ const scale3 = MeshoptSimplifier.getScale(positions, 3);
14585
+ const attributes = packAttributes(primitive, vertexCount);
14586
+ const terminal = terminalLevel(
14587
+ positions,
14588
+ indices,
14589
+ attributes,
14590
+ terminalLevelTriangles(lod0Triangles),
14591
+ lod0Triangles,
14592
+ 0
14593
+ );
14594
+ if (terminal === null) return null;
14595
+ return {
14596
+ absoluteErrors: [terminal.error * scale3],
14597
+ baselineTriangles: lod0Triangles,
14598
+ counts: [terminal.triangles],
14599
+ errorScale: scale3,
14600
+ errors: [terminal.error],
14601
+ indices: [terminal.indices],
14602
+ lod0Triangles
14603
+ };
14604
+ }
14605
+ function generateCardsChain(primitive, maxLevels, minSaving) {
14606
+ if (primitive.getMaterial()?.getAlphaMode() !== "MASK") return null;
14607
+ const positions = positionsOf(primitive);
14608
+ if (positions === null) return null;
14609
+ const vertexCount = Math.floor(positions.length / 3);
14610
+ const indices = sourceIndices(primitive, vertexCount);
14611
+ const scale3 = MeshoptSimplifier.getScale(positions, 3);
14612
+ const chain = generateCardChain(
14613
+ positions,
14614
+ indices,
14615
+ maxLevels,
14616
+ minSaving,
14617
+ scale3,
14618
+ void 0,
14619
+ terminalLevelTriangles(Math.floor(indices.length / 3))
14620
+ );
14621
+ if (chain === null) return null;
14622
+ return {
14623
+ absoluteErrors: [...chain.absoluteErrors],
14624
+ baselineTriangles: chain.lod0Triangles,
14625
+ cardLevels: chain.levels,
14626
+ counts: [...chain.counts],
14627
+ errorScale: scale3,
14628
+ errors: [...chain.errors],
14629
+ indices: [...chain.indices],
14630
+ lod0Triangles: chain.lod0Triangles
14631
+ };
14632
+ }
13738
14633
  function reachableNodes(document) {
13739
14634
  const visited = /* @__PURE__ */ new Set();
13740
14635
  const nodes = [];
@@ -13770,9 +14665,10 @@ function meshFlags(document, animated) {
13770
14665
  }
13771
14666
  return flags;
13772
14667
  }
13773
- function emptySummary(policy, generatedSeconds) {
14668
+ function emptySummary(policy, generatedSeconds, cutout = { converted: [], kept: [] }) {
13774
14669
  return {
13775
14670
  byteOverhead: 0,
14671
+ cutout,
13776
14672
  diagnostics: [...policy.diagnostics],
13777
14673
  enabled: policy.enabled,
13778
14674
  errorTargets: [...policy.generation.errorTargets],
@@ -13856,6 +14752,10 @@ async function buildJoinedRungs(document, policy, reason) {
13856
14752
  for (const node of nodes) {
13857
14753
  const mesh = node.getMesh();
13858
14754
  if (mesh === null || hasMeshBelow.has(node)) continue;
14755
+ if (node.getExtension("EXT_mesh_gpu_instancing") !== null) {
14756
+ reason("boundary-unsafe");
14757
+ continue;
14758
+ }
13859
14759
  if ((meshRefs.get(mesh) ?? 0) > 1) {
13860
14760
  reason("boundary-unsafe");
13861
14761
  continue;
@@ -14006,10 +14906,11 @@ async function generateDiscreteLod(document, lod, logicalPath, sourceDigest, leg
14006
14906
  const started = now();
14007
14907
  const policy = resolveLodPolicy(lod, logicalPath, legacy);
14008
14908
  const fingerprint = policy.fingerprint.generation;
14009
- if (!policy.enabled) return emptySummary(policy, (now() - started) / 1e3);
14909
+ const cutout = policy.cutout ? convertFoliageCutout(document) : { converted: [], kept: [] };
14910
+ if (!policy.enabled) return emptySummary(policy, (now() - started) / 1e3, cutout);
14010
14911
  const joinRequested = policy.generation.join;
14011
14912
  const discrete = policy.generation.maxLevels > 1;
14012
- if (!discrete && !joinRequested) return emptySummary(policy, (now() - started) / 1e3);
14913
+ if (!discrete && !joinRequested) return emptySummary(policy, (now() - started) / 1e3, cutout);
14013
14914
  await MeshoptSimplifier.ready;
14014
14915
  const animated = animatedNodes(document);
14015
14916
  const flags = meshFlags(document, animated);
@@ -14044,12 +14945,19 @@ async function generateDiscreteLod(document, lod, logicalPath, sourceDigest, leg
14044
14945
  }
14045
14946
  const before = triangleCount(primitive);
14046
14947
  trianglesBefore += before;
14047
- const chain = await generateChain(
14948
+ let chain = await generateChain(
14048
14949
  primitive,
14049
14950
  policy.generation.maxLevels,
14050
14951
  policy.generation.errorTargets,
14051
14952
  policy.generation.minSaving
14052
14953
  );
14954
+ const thinned = chain === null ? generateCardsChain(primitive, policy.generation.maxLevels, policy.generation.minSaving) : null;
14955
+ chain ??= thinned;
14956
+ let strategy = thinned === null ? "discrete" : "cards";
14957
+ if (chain === null) {
14958
+ chain = generateTerminalChain(primitive);
14959
+ strategy = "discrete";
14960
+ }
14053
14961
  if (chain === null) {
14054
14962
  reasons.add("insufficient-reduction");
14055
14963
  skipped.push({
@@ -14061,7 +14969,10 @@ async function generateDiscreteLod(document, lod, logicalPath, sourceDigest, leg
14061
14969
  continue;
14062
14970
  }
14063
14971
  extension ??= document.createExtension(TNDiscreteLod).setRequired(false);
14064
- const property = attachDiscreteLod(document, extension, primitive, chain);
14972
+ const property = attachDiscreteLod(document, extension, primitive, {
14973
+ ...chain,
14974
+ strategy
14975
+ });
14065
14976
  byteOverhead += discreteLodBytes(property);
14066
14977
  levels = Math.max(levels, chain.counts.length);
14067
14978
  const finest = chain.counts[chain.counts.length - 1] ?? before;
@@ -14074,7 +14985,8 @@ async function generateDiscreteLod(document, lod, logicalPath, sourceDigest, leg
14074
14985
  })),
14075
14986
  mesh: mesh.getName(),
14076
14987
  primitive: primitiveIndex,
14077
- strategy: "discrete",
14988
+ ...chain.cardLevels === void 0 ? {} : { cardLevels: chain.cardLevels },
14989
+ strategy,
14078
14990
  trianglesBefore: before
14079
14991
  });
14080
14992
  }
@@ -14092,6 +15004,7 @@ async function generateDiscreteLod(document, lod, logicalPath, sourceDigest, leg
14092
15004
  extension?.setMetadata(metadata);
14093
15005
  return {
14094
15006
  byteOverhead,
15007
+ cutout,
14095
15008
  diagnostics: [...policy.diagnostics],
14096
15009
  enabled: true,
14097
15010
  errorTargets: [...policy.generation.errorTargets],
@@ -14735,6 +15648,7 @@ ${result.stderr.trim().split("\n").slice(-12).join("\n")}`;
14735
15648
  }
14736
15649
  function blenderImportPass(options = {}) {
14737
15650
  return {
15651
+ appliesTo: ["model"],
14738
15652
  configuration: { extensions: [...BLENDER_SOURCE_EXTENSIONS].sort() },
14739
15653
  name: BLENDER_IMPORT_PASS,
14740
15654
  async apply(input, logicalPath) {
@@ -15300,8 +16214,8 @@ async function BASIS(moduleArg = {}) {
15300
16214
  async function instantiateArrayBuffer(binaryFile, imports) {
15301
16215
  try {
15302
16216
  var binary = await getWasmBinary(binaryFile);
15303
- var instance = await WebAssembly.instantiate(binary, imports);
15304
- return instance;
16217
+ var instance2 = await WebAssembly.instantiate(binary, imports);
16218
+ return instance2;
15305
16219
  } catch (reason) {
15306
16220
  err(`failed to asynchronously prepare wasm: ${reason}`);
15307
16221
  abort(reason);
@@ -15324,8 +16238,8 @@ async function BASIS(moduleArg = {}) {
15324
16238
  return { a: wasmImports };
15325
16239
  }
15326
16240
  async function createWasm() {
15327
- function receiveInstance(instance, module) {
15328
- wasmExports = instance.exports;
16241
+ function receiveInstance(instance2, module) {
16242
+ wasmExports = instance2.exports;
15329
16243
  wasmMemory = wasmExports["_"];
15330
16244
  updateMemoryViews();
15331
16245
  wasmTable = wasmExports["ca"];
@@ -19370,6 +20284,7 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
19370
20284
  const root = document.getRoot();
19371
20285
  const textures = root.listTextures();
19372
20286
  if (textures.length === 0) return void 0;
20287
+ const decoderFree = options.decoderFree === true;
19373
20288
  const maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
19374
20289
  const quality = Math.min(255, Math.max(1, Math.round(options.quality ?? DEFAULT_ETC1S_QUALITY)));
19375
20290
  const keys = textureKeys(root);
@@ -19389,6 +20304,16 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
19389
20304
  `TN_ASSETS_MODEL_TEXTURE_MISSING: '${logicalPath}' declares texture '${key}' with no image data.`
19390
20305
  );
19391
20306
  }
20307
+ const recalledFree = decoderFree ? recalled.get(index) : void 0;
20308
+ if (recalledFree !== void 0) {
20309
+ const shape = imageShape(image, texture.getMimeType());
20310
+ bytesBefore += recalledFree.sourceBytes;
20311
+ bytesAfter += image.byteLength;
20312
+ gpuBytesBefore += gpuBytes(shape.width, shape.height, "none");
20313
+ gpuBytesAfter += gpuBytes(shape.width, shape.height, "none");
20314
+ formats[key] = "none";
20315
+ continue;
20316
+ }
19392
20317
  if (texture.getMimeType() === "image/ktx2") {
19393
20318
  const shape = imageShape(image, "image/ktx2");
19394
20319
  const fromStore = recalled.get(index);
@@ -19430,6 +20355,32 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
19430
20355
  formats[key] = "none";
19431
20356
  continue;
19432
20357
  }
20358
+ if (decoderFree) {
20359
+ const target2 = cappedSize(decoded.width, decoded.height, maxSize);
20360
+ if (target2.width === decoded.width && target2.height === decoded.height) {
20361
+ bytesAfter += image.byteLength;
20362
+ gpuBytesAfter += gpuBytes(decoded.width, decoded.height, "none");
20363
+ formats[key] = "none";
20364
+ continue;
20365
+ }
20366
+ const data2 = resampleRgba(
20367
+ decoded.data,
20368
+ decoded.width,
20369
+ decoded.height,
20370
+ target2.width,
20371
+ target2.height,
20372
+ getTextureColorSpace(texture) === "srgb"
20373
+ );
20374
+ const png = new PNG({ height: target2.height, width: target2.width });
20375
+ png.data = Buffer.from(data2);
20376
+ const encoded2 = PNG.sync.write(png);
20377
+ texture.setImage(encoded2).setMimeType("image/png");
20378
+ bytesAfter += encoded2.byteLength;
20379
+ gpuBytesAfter += gpuBytes(target2.width, target2.height, "none");
20380
+ formats[key] = "none";
20381
+ resized += 1;
20382
+ continue;
20383
+ }
19433
20384
  const target = cappedSize(decoded.width, decoded.height, maxSize);
19434
20385
  const srgb = getTextureColorSpace(texture) === "srgb";
19435
20386
  let data = decoded.data;
@@ -19525,6 +20476,7 @@ async function encodeLinearRgbaKtx2(data, width, height) {
19525
20476
  }
19526
20477
  function texturePass(options = {}) {
19527
20478
  return {
20479
+ appliesTo: ["texture"],
19528
20480
  configuration: {
19529
20481
  encoder: KTX2_ENCODER_VERSION,
19530
20482
  keepSmallerSource: true,
@@ -19577,6 +20529,65 @@ function texturePass(options = {}) {
19577
20529
  name: "ktx2"
19578
20530
  };
19579
20531
  }
20532
+ function textureResizePass(options) {
20533
+ const { maxSize } = options;
20534
+ return {
20535
+ appliesTo: ["texture"],
20536
+ // Part of the compile cache key: a different cap — or a different set of excluded globs — must
20537
+ // not re-serve the previous output.
20538
+ configuration: { maxSize, overrides: options.overrides ?? [], resample: "png" },
20539
+ name: "texture-resize",
20540
+ apply: async (input, logicalPath) => {
20541
+ if (classify2(logicalPath) !== "texture") return input;
20542
+ if (matchingOverride(logicalPath, options.overrides)?.codec === "none") return input;
20543
+ const stats = textureStats(input);
20544
+ let decoded;
20545
+ if (stats.width <= 0 || stats.height <= 0) {
20546
+ if (!isPngOrJpeg(input)) return input;
20547
+ decoded = await decodeForResize(input, logicalPath);
20548
+ }
20549
+ const width = decoded?.width ?? stats.width;
20550
+ const height = decoded?.height ?? stats.height;
20551
+ const target = cappedSize(width, height, maxSize);
20552
+ if (target.width === width && target.height === height) return input;
20553
+ const source = decoded ?? await decodeForResize(input, logicalPath);
20554
+ const data = resampleRgba(
20555
+ source.data,
20556
+ source.width,
20557
+ source.height,
20558
+ target.width,
20559
+ target.height,
20560
+ !NORMAL_MAP_BASENAME.test(baseNameOf(logicalPath))
20561
+ );
20562
+ const png = new PNG({ height: target.height, width: target.width });
20563
+ png.data = Buffer.from(data);
20564
+ return {
20565
+ buffer: PNG.sync.write(png),
20566
+ entry: { resizedFrom: `${String(width)}x${String(height)}` },
20567
+ // Emitted as PNG whatever the source extension was: the manifest records the logical
20568
+ // path unchanged and the served output name carries the bytes' real container, so a
20569
+ // `.jpg` logical path resolves to a `.png` output the loader sniffs by content.
20570
+ outputExtension: ".png"
20571
+ };
20572
+ }
20573
+ };
20574
+ }
20575
+ function isPngOrJpeg(bytes) {
20576
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
20577
+ return true;
20578
+ }
20579
+ return bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10;
20580
+ }
20581
+ async function decodeForResize(input, logicalPath) {
20582
+ try {
20583
+ return await decodeImageBytes(input, logicalPath);
20584
+ } catch (error) {
20585
+ const detail = error instanceof Error ? error.message : String(error);
20586
+ throw new Error(
20587
+ `TN_ASSETS_TEXTURE_UNDECODABLE: '${logicalPath}' could not be decoded to apply its size cap: ${detail}`
20588
+ );
20589
+ }
20590
+ }
19580
20591
  function resizeForEncoding(decoded, normalMap, maxSize) {
19581
20592
  const target = maxSize === void 0 ? { height: decoded.height, width: decoded.width } : cappedSize(decoded.width, decoded.height, Math.floor(maxSize / 4) * 4);
19582
20593
  const resized = target.width !== decoded.width || target.height !== decoded.height;
@@ -19610,8 +20621,8 @@ function rgbaHasAlpha2(rgba) {
19610
20621
  function chooseCodec2(logicalPath, alpha, options) {
19611
20622
  const normalMap = NORMAL_MAP_BASENAME.test(baseNameOf(logicalPath));
19612
20623
  const fallbackQuality = options.quality ?? DEFAULT_ETC1S_QUALITY2;
19613
- for (const override of options.overrides ?? []) {
19614
- if (!globMatch(override.glob, logicalPath)) continue;
20624
+ const override = matchingOverride(logicalPath, options.overrides);
20625
+ if (override !== void 0) {
19615
20626
  return {
19616
20627
  codec: override.codec,
19617
20628
  explicit: true,
@@ -19626,6 +20637,9 @@ function chooseCodec2(logicalPath, alpha, options) {
19626
20637
  quality: clampQuality(fallbackQuality)
19627
20638
  };
19628
20639
  }
20640
+ function matchingOverride(logicalPath, overrides) {
20641
+ return overrides?.find((override) => globMatch(override.glob, logicalPath));
20642
+ }
19629
20643
  function encodeSettingsFor(choice) {
19630
20644
  if (choice.codec === "none") return {};
19631
20645
  if (choice.codec === "uastc") {
@@ -19711,7 +20725,7 @@ function chartCount(atlas) {
19711
20725
  parent.set(value, root);
19712
20726
  return root;
19713
20727
  };
19714
- const join = (left, right) => {
20728
+ const join2 = (left, right) => {
19715
20729
  const leftRoot = rootOf(left);
19716
20730
  const rightRoot = rootOf(right);
19717
20731
  parent.set(leftRoot, rightRoot);
@@ -19724,8 +20738,8 @@ function chartCount(atlas) {
19724
20738
  parent.set(first, parent.get(first) ?? first);
19725
20739
  parent.set(second, parent.get(second) ?? second);
19726
20740
  parent.set(third, parent.get(third) ?? third);
19727
- join(first, second);
19728
- join(second, third);
20741
+ join2(first, second);
20742
+ join2(second, third);
19729
20743
  }
19730
20744
  charts += new Set([...parent.keys()].map(rootOf)).size;
19731
20745
  }
@@ -19848,6 +20862,7 @@ function lightmapPass(options) {
19848
20862
  const atlasSize = positiveInteger(options.atlasSize, "atlasSize");
19849
20863
  const padding = positiveInteger(options.padding, "padding");
19850
20864
  return {
20865
+ appliesTo: ["model"],
19851
20866
  cacheKey: JSON.stringify({ atlasSize, padding }),
19852
20867
  name: "lightmap-uv2",
19853
20868
  async apply(input, logicalPath) {
@@ -20347,7 +21362,7 @@ async function bakeVirtualGeometry(document, options, now = () => Date.now()) {
20347
21362
  for (const primitive of mesh.listPrimitives()) {
20348
21363
  const indices = primitive.getIndices();
20349
21364
  const positions = positionsOf2(primitive);
20350
- if (indices === null || positions === null || primitive.getMode() !== 4) {
21365
+ if (indices === null || positions === null || primitive.getMode() !== 4 || primitive.getAttribute("JOINTS_0") !== null) {
20351
21366
  skipped += 1;
20352
21367
  continue;
20353
21368
  }
@@ -20379,6 +21394,454 @@ async function bakeVirtualGeometry(document, options, now = () => Date.now()) {
20379
21394
  stopReason
20380
21395
  };
20381
21396
  }
21397
+ var DEFAULT_PROTECTED_PATTERN = "propeller|aileron|rudder|flap|elevator|gear|wheel|canopy|hook|crew|gunner|threenativepivot|torpedo|wingport|wingstarboard|cockpit controls";
21398
+ var EXT_MESH_GPU_INSTANCING2 = "EXT_mesh_gpu_instancing";
21399
+ function asBatch(node) {
21400
+ return node.getExtension(EXT_MESH_GPU_INSTANCING2);
21401
+ }
21402
+ function sceneNodes(root) {
21403
+ const nodes = [];
21404
+ const seen = /* @__PURE__ */ new Set();
21405
+ for (const scene of root.listScenes()) {
21406
+ scene.traverse((node) => {
21407
+ if (seen.has(node)) return;
21408
+ seen.add(node);
21409
+ nodes.push(node);
21410
+ });
21411
+ }
21412
+ return nodes;
21413
+ }
21414
+ function countPrimitives(nodes) {
21415
+ let primitives = 0;
21416
+ for (const node of nodes) {
21417
+ const mesh = node.getMesh();
21418
+ if (mesh !== null) primitives += mesh.listPrimitives().length;
21419
+ }
21420
+ return primitives;
21421
+ }
21422
+ function countCompactNodes(document) {
21423
+ return sceneNodes(document.getRoot()).length;
21424
+ }
21425
+ function countCompactPrimitives(document) {
21426
+ return countPrimitives(sceneNodes(document.getRoot()));
21427
+ }
21428
+ function sceneNodeNames(document) {
21429
+ return sceneNodes(document.getRoot()).filter((node) => node.getMesh() !== null || node.listChildren().length > 0).map((node) => node.getName()).filter((name) => name !== "").sort();
21430
+ }
21431
+ function removedNames(before, after) {
21432
+ const counts = /* @__PURE__ */ new Map();
21433
+ for (const name of after) counts.set(name, (counts.get(name) ?? 0) + 1);
21434
+ const removed = [];
21435
+ for (const name of before) {
21436
+ const remaining = counts.get(name) ?? 0;
21437
+ if (remaining > 0) counts.set(name, remaining - 1);
21438
+ else removed.push(name);
21439
+ }
21440
+ return removed.sort();
21441
+ }
21442
+ function buildProtectedSet(document, options = {}) {
21443
+ const root = document.getRoot();
21444
+ const rules = /* @__PURE__ */ new Map();
21445
+ const claim = (node, rule) => {
21446
+ if (!rules.has(node)) rules.set(node, rule);
21447
+ };
21448
+ const allowList = new Set(options.protectedNames ?? []);
21449
+ for (const node of sceneNodes(root)) {
21450
+ const name = node.getName();
21451
+ if (name !== "" && allowList.has(name)) claim(node, "allow-list");
21452
+ }
21453
+ for (const skin of root.listSkins()) {
21454
+ for (const joint of skin.listJoints()) claim(joint, "skin-joint");
21455
+ }
21456
+ for (const animation of root.listAnimations()) {
21457
+ for (const channel of animation.listChannels()) {
21458
+ const target = channel.getTargetNode();
21459
+ if (target !== null) claim(target, "animation-target");
21460
+ }
21461
+ }
21462
+ const pattern = new RegExp(options.protectedPattern ?? DEFAULT_PROTECTED_PATTERN, "iu");
21463
+ for (const node of sceneNodes(root)) {
21464
+ const name = node.getName();
21465
+ if (name !== "" && pattern.test(name)) claim(node, "regex");
21466
+ }
21467
+ for (const animation of root.listAnimations()) {
21468
+ for (const channel of animation.listChannels()) {
21469
+ const target = channel.getTargetNode();
21470
+ if (target === null) continue;
21471
+ let ancestor = target.getParentNode();
21472
+ while (ancestor !== null) {
21473
+ claim(ancestor, "animation-ancestor");
21474
+ ancestor = ancestor.getParentNode();
21475
+ }
21476
+ }
21477
+ }
21478
+ const summary = [];
21479
+ for (const [node, rule] of rules) {
21480
+ summary.push({ name: node.getName() === "" ? "(unnamed)" : node.getName(), rule });
21481
+ }
21482
+ summary.sort(
21483
+ (left, right) => left.name.localeCompare(right.name) || left.rule.localeCompare(right.rule)
21484
+ );
21485
+ return { nodes: rules, summary };
21486
+ }
21487
+ function detachProtectedMeshes(document, protectedNodes) {
21488
+ const keep = keepInPlaceNodes(document, protectedNodes);
21489
+ const detached = [];
21490
+ for (const node of keep) {
21491
+ const mesh = node.getMesh();
21492
+ if (mesh === null || !isShared(mesh, node)) continue;
21493
+ const clone = deepCloneMesh(document, mesh);
21494
+ node.setMesh(clone);
21495
+ detached.push({ clone, node, original: mesh });
21496
+ }
21497
+ return detached;
21498
+ }
21499
+ function restoreDetachedMeshes(detached) {
21500
+ for (const { clone, node, original } of detached) {
21501
+ if (node.getMesh() === clone) node.setMesh(original);
21502
+ disposeMeshDeep(clone);
21503
+ }
21504
+ }
21505
+ function disposeMeshDeep(mesh) {
21506
+ for (const primitive of mesh.listPrimitives()) {
21507
+ for (const semantic of primitive.listSemantics()) {
21508
+ const attribute = primitive.getAttribute(semantic);
21509
+ primitive.setAttribute(semantic, null);
21510
+ attribute?.dispose();
21511
+ }
21512
+ const indices = primitive.getIndices();
21513
+ primitive.setIndices(null);
21514
+ indices?.dispose();
21515
+ for (const target of primitive.listTargets()) {
21516
+ for (const semantic of target.listSemantics()) {
21517
+ const attribute = target.getAttribute(semantic);
21518
+ target.setAttribute(semantic, null);
21519
+ attribute?.dispose();
21520
+ }
21521
+ target.dispose();
21522
+ }
21523
+ primitive.dispose();
21524
+ }
21525
+ mesh.dispose();
21526
+ }
21527
+ function isShared(mesh, node) {
21528
+ return mesh.listParents().some((parent) => parent !== node && parent.propertyType === PropertyType.NODE);
21529
+ }
21530
+ function isTrsExact(node) {
21531
+ const world = node.getWorldMatrix();
21532
+ const composed = composeTrsMatrix(
21533
+ node.getWorldTranslation(),
21534
+ node.getWorldRotation(),
21535
+ node.getWorldScale()
21536
+ );
21537
+ for (let index = 0; index < 16; index += 1) {
21538
+ const expected = world[index] ?? 0;
21539
+ const actual = composed[index] ?? 0;
21540
+ if (Math.abs(expected - actual) > 1e-4 * Math.max(1, Math.abs(expected))) return false;
21541
+ }
21542
+ return true;
21543
+ }
21544
+ function composeTrsMatrix(translation, rotation, scale3) {
21545
+ const translationVector = [translation[0] ?? 0, translation[1] ?? 0, translation[2] ?? 0];
21546
+ const rotationVector = [
21547
+ rotation[0] ?? 0,
21548
+ rotation[1] ?? 0,
21549
+ rotation[2] ?? 0,
21550
+ rotation[3] ?? 1
21551
+ ];
21552
+ const scaleVector = [scale3[0] ?? 1, scale3[1] ?? 1, scale3[2] ?? 1];
21553
+ const out = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
21554
+ MathUtils.compose(translationVector, rotationVector, scaleVector, out);
21555
+ return [...out];
21556
+ }
21557
+ function keepInPlaceNodes(document, protectedNodes) {
21558
+ const keepInPlace = new Set(protectedNodes.keys());
21559
+ const spreading = /* @__PURE__ */ new Set();
21560
+ for (const [node, rule] of protectedNodes) {
21561
+ if (rule !== "animation-ancestor") spreading.add(node);
21562
+ }
21563
+ for (const scene of document.getRoot().listScenes()) {
21564
+ scene.traverse((node) => {
21565
+ const parent = node.getParentNode();
21566
+ if (parent === null) {
21567
+ if (!isTrsExact(node)) {
21568
+ keepInPlace.add(node);
21569
+ spreading.add(node);
21570
+ }
21571
+ return;
21572
+ }
21573
+ if (spreading.has(parent) || !isTrsExact(parent) || !isTrsExact(node)) {
21574
+ keepInPlace.add(node);
21575
+ spreading.add(node);
21576
+ }
21577
+ });
21578
+ }
21579
+ return keepInPlace;
21580
+ }
21581
+ function flattenProtected(document, protectedNodes) {
21582
+ const keepInPlace = keepInPlaceNodes(document, protectedNodes);
21583
+ let reparented = 0;
21584
+ for (const scene of document.getRoot().listScenes()) {
21585
+ scene.traverse((node) => {
21586
+ if (keepInPlace.has(node) || node.getParentNode() === null) return;
21587
+ clearNodeParent(node);
21588
+ reparented += 1;
21589
+ });
21590
+ }
21591
+ return reparented;
21592
+ }
21593
+ function deepCloneMesh(document, mesh) {
21594
+ const clone = document.createMesh(mesh.getName());
21595
+ for (const primitive of mesh.listPrimitives()) {
21596
+ const copy = document.createPrimitive();
21597
+ for (const semantic of primitive.listSemantics()) {
21598
+ const attribute = primitive.getAttribute(semantic);
21599
+ if (attribute !== null) copy.setAttribute(semantic, attribute.clone());
21600
+ }
21601
+ const indices = primitive.getIndices();
21602
+ if (indices !== null) copy.setIndices(indices.clone());
21603
+ const material = primitive.getMaterial();
21604
+ if (material !== null) copy.setMaterial(material);
21605
+ for (const target of primitive.listTargets()) {
21606
+ const targetCopy = document.createPrimitiveTarget();
21607
+ for (const semantic of target.listSemantics()) {
21608
+ const attribute = target.getAttribute(semantic);
21609
+ if (attribute !== null) targetCopy.setAttribute(semantic, attribute.clone());
21610
+ }
21611
+ copy.addTarget(targetCopy);
21612
+ }
21613
+ clone.addPrimitive(copy);
21614
+ }
21615
+ return clone;
21616
+ }
21617
+ function instanceCount(batch) {
21618
+ for (const semantic of batch.listSemantics()) {
21619
+ const attribute = batch.getAttribute(semantic);
21620
+ if (attribute !== null) return attribute.getCount();
21621
+ }
21622
+ return 0;
21623
+ }
21624
+ async function compactModel(document, options = {}) {
21625
+ const root = document.getRoot();
21626
+ const namesBefore = sceneNodeNames(document);
21627
+ const nodesBefore = sceneNodes(root).length;
21628
+ const primitivesBefore = countPrimitives(sceneNodes(root));
21629
+ const { nodes: protectedNodes, summary: protectedSummary } = buildProtectedSet(document, options);
21630
+ const flattenEnabled = options.flatten ?? true;
21631
+ const joinEnabled = options.join ?? true;
21632
+ const instanceOption = options.instance ?? true;
21633
+ const instanceEnabled = instanceOption !== false;
21634
+ const instanceMin = typeof instanceOption === "object" && instanceOption.min !== void 0 ? instanceOption.min : 2;
21635
+ const reparented = flattenEnabled ? flattenProtected(document, protectedNodes) : 0;
21636
+ const sharedBefore = /* @__PURE__ */ new Set();
21637
+ for (const node of sceneNodes(root)) {
21638
+ const mesh = node.getMesh();
21639
+ if (mesh !== null && isShared(mesh, node)) sharedBefore.add(node);
21640
+ }
21641
+ let detached = [];
21642
+ if (instanceEnabled && root.listAnimations().length === 0) {
21643
+ detached = detachProtectedMeshes(document, protectedNodes);
21644
+ }
21645
+ let instanceSummary = {
21646
+ batches: 0,
21647
+ enabled: instanceEnabled,
21648
+ instances: 0
21649
+ };
21650
+ if (instanceEnabled) {
21651
+ await instance({ min: instanceMin })(document);
21652
+ let batches = 0;
21653
+ let instances = 0;
21654
+ for (const node of sceneNodes(root)) {
21655
+ const batch = asBatch(node);
21656
+ if (batch === null) continue;
21657
+ batches += 1;
21658
+ instances += instanceCount(batch);
21659
+ }
21660
+ const reason = batches > 0 ? void 0 : root.listAnimations().length > 0 ? "animated model; EXT_mesh_gpu_instancing is not supported for animated models" : `no mesh was shared by >=${String(instanceMin)} nodes`;
21661
+ instanceSummary = {
21662
+ batches,
21663
+ enabled: true,
21664
+ instances,
21665
+ ...reason === void 0 ? {} : { reason }
21666
+ };
21667
+ }
21668
+ restoreDetachedMeshes(detached);
21669
+ const primitivesBeforeJoin = countPrimitives(sceneNodes(root));
21670
+ if (joinEnabled) {
21671
+ const sourcePrimitives = collectPrimitives(root);
21672
+ const exclusions = joinExclusions(root);
21673
+ await join({
21674
+ cleanup: false,
21675
+ filter: (node) => !protectedNodes.has(node) && !sharedBefore.has(node) && !exclusions.has(node)
21676
+ })(document);
21677
+ for (const primitive of sourcePrimitives) {
21678
+ if (primitive.listParents().some((parent) => parent.propertyType !== PropertyType.ROOT)) {
21679
+ continue;
21680
+ }
21681
+ const accessors = accessorsOfPrimitive(primitive);
21682
+ primitive.dispose();
21683
+ for (const accessor of accessors) {
21684
+ if (accessor.listParents().every((parent) => parent.propertyType === PropertyType.ROOT)) {
21685
+ accessor.dispose();
21686
+ }
21687
+ }
21688
+ }
21689
+ for (const mesh of root.listMeshes()) {
21690
+ if (mesh.listPrimitives().length === 0) mesh.dispose();
21691
+ }
21692
+ }
21693
+ const primitivesAfterJoin = countPrimitives(sceneNodes(root));
21694
+ const nodesAfter = sceneNodes(root).length;
21695
+ const summary = {
21696
+ flatten: { enabled: flattenEnabled, reparented },
21697
+ instance: instanceSummary,
21698
+ join: {
21699
+ enabled: joinEnabled,
21700
+ primitivesAfter: primitivesAfterJoin,
21701
+ primitivesBefore: primitivesBeforeJoin
21702
+ },
21703
+ nodesAfter,
21704
+ nodesBefore,
21705
+ primitivesAfter: primitivesAfterJoin,
21706
+ primitivesBefore,
21707
+ removed: removedNames(namesBefore, sceneNodeNames(document)),
21708
+ protected: protectedSummary
21709
+ };
21710
+ return summary;
21711
+ }
21712
+ function collectPrimitives(root) {
21713
+ return root.listMeshes().flatMap((mesh) => mesh.listPrimitives());
21714
+ }
21715
+ function primitiveSignature(primitive) {
21716
+ if (primitive.listTargets().length > 0) return null;
21717
+ const position = primitive.getAttribute("POSITION");
21718
+ if (position === null) return null;
21719
+ const hash = createHash("sha1");
21720
+ const min2 = position.getMin([0, 0, 0]);
21721
+ const max2 = position.getMax([0, 0, 0]);
21722
+ const extent = Math.max(
21723
+ (max2[0] ?? 0) - (min2[0] ?? 0),
21724
+ (max2[1] ?? 0) - (min2[1] ?? 0),
21725
+ (max2[2] ?? 0) - (min2[2] ?? 0),
21726
+ 1e-9
21727
+ );
21728
+ for (const semantic of [...primitive.listSemantics()].sort()) {
21729
+ const attribute = primitive.getAttribute(semantic);
21730
+ if (attribute === null) continue;
21731
+ hash.update(semantic);
21732
+ hash.update(":");
21733
+ const array = attribute.getArray();
21734
+ const stride = attribute.getElementSize();
21735
+ const normalized = attribute.getNormalized();
21736
+ for (let index = 0; index < attribute.getCount(); index += 1) {
21737
+ for (let axis = 0; axis < stride; axis += 1) {
21738
+ let value = array[index * stride + axis] ?? 0;
21739
+ if (semantic === "POSITION" && !normalized && axis < 3) {
21740
+ value = (value - (min2[axis] ?? 0)) / extent;
21741
+ }
21742
+ hash.update(Math.round(value * 1e4).toString());
21743
+ hash.update(",");
21744
+ }
21745
+ }
21746
+ }
21747
+ const indices = primitive.getIndices();
21748
+ if (indices !== null) {
21749
+ const array = indices.getArray();
21750
+ hash.update("indices:");
21751
+ for (let index = 0; index < indices.getCount(); index += 1) {
21752
+ hash.update((array[index] ?? 0).toString());
21753
+ hash.update(",");
21754
+ }
21755
+ }
21756
+ return hash.digest("hex");
21757
+ }
21758
+ function meshShapeSignature(mesh) {
21759
+ const signatures = [];
21760
+ for (const primitive of mesh.listPrimitives()) {
21761
+ const signature = primitiveSignature(primitive);
21762
+ if (signature === null) return null;
21763
+ signatures.push(signature);
21764
+ }
21765
+ signatures.sort();
21766
+ return signatures.join("|");
21767
+ }
21768
+ function joinExclusions(root) {
21769
+ const excluded = /* @__PURE__ */ new Set();
21770
+ const signatures = /* @__PURE__ */ new Map();
21771
+ const byNode = /* @__PURE__ */ new Map();
21772
+ for (const node of sceneNodes(root)) {
21773
+ if (listNodeScenes(node).length > 1) excluded.add(node);
21774
+ const mesh = node.getMesh();
21775
+ if (mesh === null) continue;
21776
+ const signature = meshShapeSignature(mesh);
21777
+ if (signature === null) continue;
21778
+ byNode.set(node, signature);
21779
+ signatures.set(signature, (signatures.get(signature) ?? 0) + 1);
21780
+ }
21781
+ for (const [node, signature] of byNode) {
21782
+ if ((signatures.get(signature) ?? 0) > 1) excluded.add(node);
21783
+ }
21784
+ return excluded;
21785
+ }
21786
+ function accessorsOfPrimitive(primitive) {
21787
+ const accessors = [];
21788
+ for (const semantic of primitive.listSemantics()) {
21789
+ const attribute = primitive.getAttribute(semantic);
21790
+ if (attribute !== null) accessors.push(attribute);
21791
+ }
21792
+ const indices = primitive.getIndices();
21793
+ if (indices !== null) accessors.push(indices);
21794
+ for (const target of primitive.listTargets()) {
21795
+ for (const semantic of target.listSemantics()) {
21796
+ const attribute = target.getAttribute(semantic);
21797
+ if (attribute !== null) accessors.push(attribute);
21798
+ }
21799
+ }
21800
+ return accessors;
21801
+ }
21802
+ function compactRequested(options) {
21803
+ if (options === false) return false;
21804
+ if (options === void 0 || options === true) return true;
21805
+ return options.flatten !== false || options.join !== false || options.instance !== false;
21806
+ }
21807
+ var COMPACT_VERSION = 8;
21808
+ function resolveCompactOptions(options) {
21809
+ if (options === void 0 || options === true) {
21810
+ return {
21811
+ flatten: true,
21812
+ instance: { min: 2 },
21813
+ join: true,
21814
+ protectedNames: [],
21815
+ protectedPattern: DEFAULT_PROTECTED_PATTERN,
21816
+ version: COMPACT_VERSION
21817
+ };
21818
+ }
21819
+ if (options === false) {
21820
+ return {
21821
+ flatten: false,
21822
+ instance: false,
21823
+ join: false,
21824
+ protectedNames: [],
21825
+ protectedPattern: DEFAULT_PROTECTED_PATTERN,
21826
+ version: COMPACT_VERSION
21827
+ };
21828
+ }
21829
+ const instance2 = options.instance;
21830
+ const min2 = typeof instance2 === "object" ? instance2.min ?? 2 : 2;
21831
+ if (!Number.isSafeInteger(min2) || min2 < 2) {
21832
+ throw new Error(
21833
+ "TN_ASSETS_COMPACT_INVALID: assets.models.compact.instance.min must be an integer of at least 2."
21834
+ );
21835
+ }
21836
+ return {
21837
+ flatten: options.flatten !== false,
21838
+ instance: instance2 === false ? false : { min: min2 },
21839
+ join: options.join !== false,
21840
+ protectedNames: options.protectedNames ?? [],
21841
+ protectedPattern: options.protectedPattern ?? DEFAULT_PROTECTED_PATTERN,
21842
+ version: COMPACT_VERSION
21843
+ };
21844
+ }
20382
21845
  var SHARED_IMAGES_DIRECTORY = "shared/images";
20383
21846
  var KEY_LENGTH = 16;
20384
21847
  var EXTENSION_BY_MIME = {
@@ -20587,6 +22050,8 @@ async function readSharedGlb(io, buffer, resolve2) {
20587
22050
  // src/passes/model.ts
20588
22051
  var DRACO_EXTENSION2 = "KHR_draco_mesh_compression";
20589
22052
  var EXT_MESHOPT_EXTENSION = "EXT_meshopt_compression";
22053
+ var MATERIAL_MERGE_VERSION = 1;
22054
+ var MODEL_DEDUPE_VERSION = 1;
20590
22055
  var BBOX_TOLERANCE = 1e-3;
20591
22056
  var SIMPLIFY_BBOX_TOLERANCE = 0.01;
20592
22057
  var SIMPLIFY_RATIO_FLOOR = 0.5;
@@ -20624,6 +22089,27 @@ function primitiveTriangles(primitive) {
20624
22089
  const drawn = primitive.getIndices()?.getCount() ?? primitive.getAttribute("POSITION")?.getCount() ?? 0;
20625
22090
  return Math.floor(drawn / 3);
20626
22091
  }
22092
+ function instanceCount2(batch) {
22093
+ for (const semantic of batch.listSemantics()) {
22094
+ const attribute = batch.getAttribute(semantic);
22095
+ if (attribute !== null) return attribute.getCount();
22096
+ }
22097
+ return 0;
22098
+ }
22099
+ function instanceMatrix(batch, index) {
22100
+ return composeTrsMatrix(
22101
+ readElement(batch.getAttribute("TRANSLATION"), index, [0, 0, 0]),
22102
+ readElement(batch.getAttribute("ROTATION"), index, [0, 0, 0, 1]),
22103
+ readElement(batch.getAttribute("SCALE"), index, [1, 1, 1])
22104
+ );
22105
+ }
22106
+ function readElement(accessor, index, fallback) {
22107
+ if (accessor === null) return [...fallback];
22108
+ const array = accessor.getArray();
22109
+ const size = accessor.getElementSize();
22110
+ const base = index * size;
22111
+ return fallback.map((_, axis) => array[base + axis] ?? 0);
22112
+ }
20627
22113
  function decodeElement(accessor, index) {
20628
22114
  const array = accessor.getArray();
20629
22115
  const base = index * accessor.getElementSize();
@@ -20721,36 +22207,43 @@ function reachableStats(root) {
20721
22207
  if (skin !== null) skins.add(skin);
20722
22208
  const mesh = node.getMesh();
20723
22209
  if (mesh !== null) {
20724
- const worldMatrix = node.getWorldMatrix();
22210
+ const batch = node.getExtension("EXT_mesh_gpu_instancing");
22211
+ const instanceMatrices = batch === null ? [node.getWorldMatrix()] : Array.from(
22212
+ { length: instanceCount2(batch) },
22213
+ (_, index) => multiplyMatrices(node.getWorldMatrix(), instanceMatrix(batch, index))
22214
+ );
22215
+ const skin2 = node.getSkin();
20725
22216
  let jointMatrices;
20726
- if (skin !== null) {
20727
- const inverseBinds = skin.getInverseBindMatrices();
20728
- jointMatrices = skin.listJoints().map((joint, index) => {
22217
+ if (skin2 !== null) {
22218
+ const inverseBinds = skin2.getInverseBindMatrices();
22219
+ jointMatrices = skin2.listJoints().map((joint, index) => {
20729
22220
  const jointWorld = joint.getWorldMatrix();
20730
22221
  const ibm = inverseBinds?.getElement(index, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) ?? IDENTITY_MATRIX2;
20731
22222
  return multiplyMatrices(jointWorld, ibm);
20732
22223
  });
20733
22224
  }
20734
- for (const primitive of mesh.listPrimitives()) {
20735
- triangles += primitiveTriangles(primitive);
20736
- const position = primitive.getAttribute("POSITION");
20737
- if (position === null) continue;
20738
- vertices += position.getCount();
20739
- for (let index = 0; index < position.getCount(); index += 1) {
20740
- const [wx, wy, wz] = evaluateVertex(
20741
- position,
20742
- index,
20743
- primitive.getAttribute("JOINTS_0"),
20744
- primitive.getAttribute("WEIGHTS_0"),
20745
- skin !== null ? jointMatrices : void 0,
20746
- worldMatrix
20747
- );
20748
- minX = Math.min(minX, wx);
20749
- minY = Math.min(minY, wy);
20750
- minZ = Math.min(minZ, wz);
20751
- maxX = Math.max(maxX, wx);
20752
- maxY = Math.max(maxY, wy);
20753
- maxZ = Math.max(maxZ, wz);
22225
+ for (const matrix of instanceMatrices) {
22226
+ for (const primitive of mesh.listPrimitives()) {
22227
+ triangles += primitiveTriangles(primitive);
22228
+ const position = primitive.getAttribute("POSITION");
22229
+ if (position === null) continue;
22230
+ vertices += position.getCount();
22231
+ for (let index = 0; index < position.getCount(); index += 1) {
22232
+ const [wx, wy, wz] = evaluateVertex(
22233
+ position,
22234
+ index,
22235
+ primitive.getAttribute("JOINTS_0"),
22236
+ primitive.getAttribute("WEIGHTS_0"),
22237
+ skin2 !== null ? jointMatrices : void 0,
22238
+ matrix
22239
+ );
22240
+ minX = Math.min(minX, wx);
22241
+ minY = Math.min(minY, wy);
22242
+ minZ = Math.min(minZ, wz);
22243
+ maxX = Math.max(maxX, wx);
22244
+ maxY = Math.max(maxY, wy);
22245
+ maxZ = Math.max(maxZ, wz);
22246
+ }
20754
22247
  }
20755
22248
  }
20756
22249
  }
@@ -20934,6 +22427,7 @@ function lodCacheKey(lod) {
20934
22427
  }
20935
22428
  function modelPass(options = {}) {
20936
22429
  return {
22430
+ appliesTo: ["model"],
20937
22431
  configuration: {
20938
22432
  passes: {
20939
22433
  dedup: options.passes?.dedup ?? true,
@@ -20951,8 +22445,18 @@ function modelPass(options = {}) {
20951
22445
  // Part of the compile cache key: change the cap or a codec and stale outputs must not
20952
22446
  // be re-served.
20953
22447
  simplify: options.simplify ?? null,
22448
+ // The resolved compaction policy: a sub-pass switch, allow-list or regex edit changes the
22449
+ // output, so it must change the cache key.
22450
+ compact: resolveCompactOptions(options.compact),
20954
22451
  // Generation-only identity; runtime budget edits must not invalidate baked geometry.
20955
22452
  lod: lodCacheKey(options.lod),
22453
+ // The material merge is unconditional and lossless, so the version string is the whole knob:
22454
+ // it moves when the signature does, which invalidates every stale output at once.
22455
+ materials: MATERIAL_MERGE_VERSION,
22456
+ // Unconditional and lossless for the same reason: the cook shares one output between
22457
+ // content-identical sources, so a bake published before this key existed named one output
22458
+ // per copy and must not be re-served as if it had deduped.
22459
+ dedupe: MODEL_DEDUPE_VERSION,
20956
22460
  // `"none"` and "absent" are different cache keys on purpose: absent bakes with defaults.
20957
22461
  virtual: options.virtual === "none" ? "none" : {
20958
22462
  ...options.virtual === void 0 ? {} : options.virtual,
@@ -20960,6 +22464,7 @@ function modelPass(options = {}) {
20960
22464
  },
20961
22465
  sharedImages: options.sharedImages !== void 0,
20962
22466
  textures: options.textures === "none" ? "none" : {
22467
+ decoderFree: options.textures?.decoderFree ?? false,
20963
22468
  encoder: KTX2_ENCODER_VERSION,
20964
22469
  maxSize: options.textures?.maxSize ?? null,
20965
22470
  keepSmallerSource: true,
@@ -20970,7 +22475,7 @@ function modelPass(options = {}) {
20970
22475
  name: "model",
20971
22476
  // A mobile compile replaces these two decoder-backed sub-passes in the effective options;
20972
22477
  // geometry rewrites and shared-image emission remain plain glTF and stay enabled there.
20973
- needsRuntimeDecoder: (options.passes?.meshopt ?? true) || options.textures !== "none",
22478
+ needsRuntimeDecoder: (options.passes?.meshopt ?? true) || options.textures !== "none" && options.textures?.decoderFree !== true,
20974
22479
  apply: async (input, logicalPath) => {
20975
22480
  if (classify2(logicalPath) !== "model") return input;
20976
22481
  const passes2 = options.passes ?? {};
@@ -20984,7 +22489,7 @@ function modelPass(options = {}) {
20984
22489
  const textureOptions = options.textures === "none" ? void 0 : options.textures ?? {};
20985
22490
  const virtualOptions = options.virtual === "none" ? void 0 : options.virtual ?? {};
20986
22491
  const lodOptions = options.lod === void 0 || options.lod === false || options.lod === "none" ? void 0 : options.lod;
20987
- const geometryActive = options.vertexLayout !== void 0 || Object.values(enabled).some(Boolean) || options.simplify !== void 0 || lodOptions !== void 0 || virtualOptions !== void 0;
22492
+ const geometryActive = options.vertexLayout !== void 0 || Object.values(enabled).some(Boolean) || options.simplify !== void 0 || lodOptions !== void 0 || virtualOptions !== void 0 || compactRequested(options.compact);
20988
22493
  if (!geometryActive && textureOptions === void 0) return input;
20989
22494
  const document = await readDocument2(input, logicalPath);
20990
22495
  if (!geometryActive && document.getRoot().listTextures().length === 0) return input;
@@ -20995,8 +22500,21 @@ function modelPass(options = {}) {
20995
22500
  const source = reachableStats(document.getRoot());
20996
22501
  const jointFloor = jointComponentSizes(document.getRoot());
20997
22502
  if (enabled.dedup) await dedup()(document);
20998
- if (enabled.prune)
22503
+ const namesBeforeCompact = compactRequested(options.compact) ? sceneNodeNames(document) : void 0;
22504
+ let compact = compactRequested(options.compact) ? await compactModel(document, resolveCompactOptions(options.compact)) : void 0;
22505
+ if (enabled.prune) {
20999
22506
  await prune({ keepAttributes: options.preserveLightmapUv === true })(document);
22507
+ if (compact !== void 0 && namesBeforeCompact !== void 0) {
22508
+ const nodes = countCompactNodes(document);
22509
+ compact = {
22510
+ ...compact,
22511
+ nodesAfter: nodes,
22512
+ primitivesAfter: countCompactPrimitives(document),
22513
+ removed: removedNames(namesBeforeCompact, sceneNodeNames(document))
22514
+ };
22515
+ }
22516
+ }
22517
+ const materials = mergeIdenticalMaterials(document);
21000
22518
  if (options.simplify !== void 0) {
21001
22519
  await MeshoptSimplifier.ready;
21002
22520
  await simplify({
@@ -21063,9 +22581,11 @@ function modelPass(options = {}) {
21063
22581
  assertNoTextureDrift(sourceTextures, textureBindings(verified), logicalPath);
21064
22582
  if (lod !== void 0 && lod.generated > 0) validateDiscreteLod(verified);
21065
22583
  const entry = {
22584
+ ...compact === void 0 ? {} : { compact },
21066
22585
  ...embeddedTextures === void 0 ? {} : { embeddedTextures },
21067
22586
  extensions: [...extensions].sort(),
21068
22587
  ...lod === void 0 ? {} : { lod },
22588
+ materials,
21069
22589
  ...options.simplify === void 0 ? {} : {
21070
22590
  simplify: {
21071
22591
  achievedRatio: source.triangles === 0 ? 1 : output.triangles / source.triangles,
@@ -21097,6 +22617,7 @@ function sharedSettings(texture, textureOptions) {
21097
22617
  colorSpace: getTextureColorSpace(texture),
21098
22618
  slots: [...listTextureSlots(texture)].sort(),
21099
22619
  textures: textureOptions === void 0 ? "none" : {
22620
+ decoderFree: textureOptions.decoderFree ?? false,
21100
22621
  encoder: KTX2_ENCODER_VERSION,
21101
22622
  keepSmallerSource: true,
21102
22623
  maxSize: textureOptions.maxSize ?? null,
@@ -21431,6 +22952,7 @@ function serialFor(logicalPath) {
21431
22952
  // src/passes/audio.ts
21432
22953
  function audioPass(options = {}) {
21433
22954
  return {
22955
+ appliesTo: ["audio"],
21434
22956
  configuration: {
21435
22957
  normalise: options.normalise ?? "ceiling",
21436
22958
  overrides: options.overrides ?? [],
@@ -21653,6 +23175,8 @@ function buildInstances(specs, root) {
21653
23175
  });
21654
23176
  case "texture":
21655
23177
  return texturePass(spec.options);
23178
+ case "texture-resize":
23179
+ return textureResizePass(spec.options);
21656
23180
  }
21657
23181
  });
21658
23182
  }