@forgeax/engine-gltf 0.1.21 → 0.1.24

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.
Files changed (68) hide show
  1. package/README.md +115 -3
  2. package/dist/__tests__/lod-import.integration.test.d.ts +2 -0
  3. package/dist/__tests__/lod-import.integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/lod-meta-publication.integration.test.d.ts +2 -0
  5. package/dist/__tests__/lod-meta-publication.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/physical-clearcoat.unit.test.d.ts +2 -0
  7. package/dist/__tests__/physical-clearcoat.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/physical-material-import.integration.test.d.ts +2 -0
  9. package/dist/__tests__/physical-material-import.integration.test.d.ts.map +1 -0
  10. package/dist/bridge.d.ts +1 -0
  11. package/dist/bridge.d.ts.map +1 -1
  12. package/dist/check-extensions.d.ts +1 -1
  13. package/dist/check-extensions.d.ts.map +1 -1
  14. package/dist/cli-gltf.mjs +362 -14
  15. package/dist/cli-gltf.mjs.map +1 -1
  16. package/dist/errors.d.ts +13 -0
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/gltf-importer.d.ts.map +1 -1
  19. package/dist/image-color-space.d.ts +12 -0
  20. package/dist/image-color-space.d.ts.map +1 -1
  21. package/dist/importer-entry.mjs +516 -25
  22. package/dist/importer-entry.mjs.map +1 -1
  23. package/dist/index.d.ts +4 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.mjs +609 -28
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/lod/parse-lod.d.ts +15 -0
  28. package/dist/lod/parse-lod.d.ts.map +1 -0
  29. package/dist/lod/project-meta.d.ts +23 -0
  30. package/dist/lod/project-meta.d.ts.map +1 -0
  31. package/dist/material/parse-material.d.ts +66 -0
  32. package/dist/material/parse-material.d.ts.map +1 -1
  33. package/dist/node-file-entry.mjs +265 -8
  34. package/dist/node-file-entry.mjs.map +1 -1
  35. package/dist/parse-gltf.d.ts +3 -1
  36. package/dist/parse-gltf.d.ts.map +1 -1
  37. package/dist/reimport-reuse-meta.d.ts +1 -1
  38. package/dist/reimport-reuse-meta.d.ts.map +1 -1
  39. package/package.json +12 -12
  40. package/src/__tests__/bridge-material-values.unit.test.ts +84 -1
  41. package/src/__tests__/bridge-skin-stride.unit.test.ts +22 -0
  42. package/src/__tests__/fixtures/lod/core-only.gltf +1 -0
  43. package/src/__tests__/fixtures/lod/msft-lod-malformed.gltf +1 -0
  44. package/src/__tests__/fixtures/lod/msft-lod-multi-root.gltf +1 -0
  45. package/src/__tests__/fixtures/lod/msft-lod-node-extras.gltf +1 -0
  46. package/src/__tests__/fixtures/lod/msft-lod.gltf +1 -0
  47. package/src/__tests__/gltf-error-derived-views.test-d.ts +8 -0
  48. package/src/__tests__/gltf-error-migration.test.ts +3 -3
  49. package/src/__tests__/gltf.unit.test.ts +60 -5
  50. package/src/__tests__/lod-import.integration.test.ts +106 -0
  51. package/src/__tests__/lod-meta-publication.integration.test.ts +24 -0
  52. package/src/__tests__/material-pack-refs.integration.test.ts +53 -0
  53. package/src/__tests__/parse-gltf.unit.test.ts +50 -0
  54. package/src/__tests__/physical-clearcoat.unit.test.ts +110 -0
  55. package/src/__tests__/physical-material-import.integration.test.ts +239 -0
  56. package/src/__tests__/source-key-producer.integration.test.ts +6 -2
  57. package/src/bridge.ts +259 -14
  58. package/src/check-extensions.ts +16 -8
  59. package/src/errors.ts +32 -2
  60. package/src/gltf-importer.ts +122 -1
  61. package/src/image-color-space.ts +29 -4
  62. package/src/index.ts +10 -1
  63. package/src/lod/parse-lod.ts +129 -0
  64. package/src/lod/project-meta.ts +48 -0
  65. package/src/material/parse-material.ts +307 -0
  66. package/src/parse-gltf.ts +97 -1
  67. package/src/reimport-reuse-meta.ts +3 -1
  68. package/dist/.tsbuildinfo +0 -1
package/dist/cli-gltf.mjs CHANGED
@@ -3,8 +3,9 @@ import { realpath, stat, readFile, writeFile, readdir } from 'fs/promises';
3
3
  import { resolve, dirname, join } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { parseArgs } from 'util';
6
- import { reconcileMeshMaterialSlotTopology, err, MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA, ok, ImportError, IMPORT_ERROR_HINTS } from '@forgeax/engine-types';
6
+ import { err, reconcileMeshMaterialSlotTopology, MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA, ok, ImportError, IMPORT_ERROR_HINTS } from '@forgeax/engine-types';
7
7
  import { vec3, quat, mat4 } from '@forgeax/engine-math';
8
+ import { deriveDefaultLodScreenCoverages, reconcileMeshLodMeta } from '@forgeax/engine-import';
8
9
  import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
9
10
  import { AssetGuid } from '@forgeax/engine-pack/guid';
10
11
 
@@ -24,13 +25,21 @@ var gltfErrorPolicy = {
24
25
  hint: "rebuild .gltf with valid bufferViews; check accessor index; ensure accessor.byteOffset + EFFECTIVE_STRIDE * (count - 1) + element_size <= bufferView.byteLength"
25
26
  },
26
27
  "gltf-extension-unsupported": {
27
- expected: "extension listed in v1 allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)",
28
- hint: "see feat-future-gltf-extensions-allowlist; remove this extension or wait for the allowlist to expand"
28
+ expected: "extension listed in the supported allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)",
29
+ hint: "remove the unsupported required extension or use a supported glTF extension; extensionsUsed-only entries remain diagnostic"
30
+ },
31
+ "gltf-lod-invalid": {
32
+ expected: "MSFT_lod ids to reference unique existing node indices with valid coverage",
33
+ hint: "repair the MSFT_lod node relation or remove it from extensionsRequired before re-importing"
29
34
  },
30
35
  "gltf-material-transmission-invalid": {
31
36
  expected: "KHR transmission, IOR, and volume values are finite and within their glTF ranges",
32
37
  hint: "repair the named glTF material extension value and re-import the source"
33
38
  },
39
+ "gltf-material-physical-invalid": {
40
+ expected: "KHR clearcoat, anisotropy, sheen, iridescence, and specular values are finite and within their glTF ranges",
41
+ hint: "repair the named physical material extension value and re-import the source"
42
+ },
34
43
  "gltf-accessor-type-mismatch": {
35
44
  expected: "dense fixed-stride accessor with supported componentType",
36
45
  hint: "sparse: see feat-future-gltf-sparse-accessor; morph: see feat-future-gltf-morph; interleaved: see feat-future-gltf-mesh-multi-section"
@@ -394,15 +403,22 @@ function decodeColorAccessor(input, flags = {}) {
394
403
  // src/check-extensions.ts
395
404
  var EXTENSION_ALLOWLIST = [
396
405
  "EXT_mesh_gpu_instancing",
397
- "EXT_meshopt_compression"
398
- ];
399
- var SUPPORTED_EXTENSIONS = [
400
- ...EXTENSION_ALLOWLIST,
406
+ "EXT_meshopt_compression",
401
407
  "KHR_lights_punctual",
402
408
  "KHR_texture_transform",
403
409
  "KHR_materials_transmission",
404
410
  "KHR_materials_ior",
405
- "KHR_materials_volume"
411
+ "KHR_materials_volume",
412
+ "KHR_materials_clearcoat",
413
+ "KHR_materials_anisotropy",
414
+ "KHR_materials_sheen",
415
+ "KHR_materials_iridescence",
416
+ "KHR_materials_specular"
417
+ ];
418
+ var SUPPORTED_EXTENSIONS = [
419
+ ...EXTENSION_ALLOWLIST,
420
+ "MSFT_lod",
421
+ "MSFT_screencoverage"
406
422
  ];
407
423
  function checkExtensions(json) {
408
424
  const required = json.extensionsRequired ?? [];
@@ -454,6 +470,103 @@ function decodeBase64(b64) {
454
470
  return out;
455
471
  }
456
472
 
473
+ // src/lod/parse-lod.ts
474
+ function parseGltfLodExtension(json) {
475
+ const doc = json;
476
+ const nodes = doc.nodes ?? [];
477
+ const rawCoverage = doc.extensions?.MSFT_screencoverage?.scales;
478
+ const groups = [];
479
+ for (let rootNode = 0; rootNode < nodes.length; rootNode += 1) {
480
+ if (nodes[rootNode]?.extensions?.MSFT_lod === void 0) continue;
481
+ const idsRaw = nodes[rootNode]?.extensions?.MSFT_lod?.ids;
482
+ if (!Array.isArray(idsRaw)) {
483
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids: [], reason: "not-integer" }));
484
+ }
485
+ const ids = idsRaw.filter((value) => typeof value === "number");
486
+ if (ids.length !== idsRaw.length || ids.some((id) => !Number.isInteger(id) || id < 0 || id >= nodes.length)) {
487
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "missing-node" }));
488
+ }
489
+ if (new Set(ids).size !== ids.length || ids.includes(rootNode)) {
490
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "duplicate-node" }));
491
+ }
492
+ const hasMesh = (node) => {
493
+ if (typeof node?.mesh !== "number" || !Number.isInteger(node.mesh) || node.mesh < 0) {
494
+ return false;
495
+ }
496
+ const mesh = doc.meshes?.[node.mesh];
497
+ return mesh !== void 0 && Array.isArray(mesh.primitives) && mesh.primitives.length > 0;
498
+ };
499
+ if (!hasMesh(nodes[rootNode]) || ids.some((id) => !hasMesh(nodes[id]))) {
500
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "missing-node" }));
501
+ }
502
+ const nodeCoverage = nodes[rootNode]?.extras?.MSFT_screencoverage;
503
+ const coverage = normalizeScreenCoverages(
504
+ nodeCoverage === void 0 ? rawCoverage : nodeCoverage,
505
+ ids.length,
506
+ nodeCoverage !== void 0
507
+ );
508
+ if (coverage === "invalid") {
509
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "coverage" }));
510
+ }
511
+ groups.push({
512
+ rootNode,
513
+ lodNodeIds: ids,
514
+ screenCoverages: coverage ?? []
515
+ });
516
+ }
517
+ const first = groups[0];
518
+ if (first === void 0) {
519
+ if (doc.extensionsRequired?.some((extension) => extension === "MSFT_lod")) {
520
+ return err(gltfErr("gltf-lod-invalid", { rootNode: 0, ids: [], reason: "missing-node" }));
521
+ }
522
+ return ok({ rootNode: 0, lodNodeIds: [], screenCoverages: [], groups: [] });
523
+ }
524
+ return ok({
525
+ rootNode: first.rootNode,
526
+ lodNodeIds: first.lodNodeIds,
527
+ screenCoverages: first.screenCoverages,
528
+ groups
529
+ });
530
+ }
531
+ function normalizeScreenCoverages(raw, lodCount, officialNodeForm) {
532
+ if (raw === void 0) return void 0;
533
+ if (!Array.isArray(raw)) return "invalid";
534
+ if (raw.some(
535
+ (value) => typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1
536
+ )) {
537
+ return "invalid";
538
+ }
539
+ const expectedLengths = officialNodeForm ? [lodCount + 1] : [lodCount, lodCount + 1];
540
+ if (!expectedLengths.includes(raw.length)) return "invalid";
541
+ const values = raw.slice(0, lodCount);
542
+ for (let index = 1; index < values.length; index += 1) {
543
+ if (values[index] >= values[index - 1]) return "invalid";
544
+ }
545
+ return values;
546
+ }
547
+ function projectGltfLodMeta(input) {
548
+ const defaults = deriveDefaultLodScreenCoverages(input.levels.length + 1);
549
+ const previous = (input.previous ?? []).map((level) => ({
550
+ sourceKey: level.sourceKey,
551
+ meshGuid: level.guid,
552
+ ...level.screenCoverage === void 0 ? {} : { screenCoverage: level.screenCoverage }
553
+ }));
554
+ const next = input.levels.map((level, index) => ({
555
+ sourceKey: level.sourceKey,
556
+ meshGuid: level.guid,
557
+ screenCoverage: level.screenCoverage ?? defaults[index] ?? 0
558
+ }));
559
+ const reconciled = reconcileMeshLodMeta(previous, next);
560
+ if (!reconciled.ok) return err({ code: reconciled.error.code, reason: reconciled.error.reason });
561
+ return ok({
562
+ lods: reconciled.value.lods.map((level) => ({
563
+ sourceKey: level.sourceKey,
564
+ meshGuid: level.meshGuid,
565
+ screenCoverage: level.screenCoverage
566
+ }))
567
+ });
568
+ }
569
+
457
570
  // src/material/parse-material.ts
458
571
  function tuple2(values) {
459
572
  if (values === void 0 || values.length < 2) return void 0;
@@ -498,6 +611,11 @@ function parseMaterial(matJson, textures) {
498
611
  const transmission = matJson.extensions?.KHR_materials_transmission;
499
612
  const ior = matJson.extensions?.KHR_materials_ior;
500
613
  const volume = matJson.extensions?.KHR_materials_volume;
614
+ const clearcoat = matJson.extensions?.KHR_materials_clearcoat;
615
+ const anisotropy = matJson.extensions?.KHR_materials_anisotropy;
616
+ const sheen = matJson.extensions?.KHR_materials_sheen;
617
+ const iridescence = matJson.extensions?.KHR_materials_iridescence;
618
+ const specular = matJson.extensions?.KHR_materials_specular;
501
619
  const attenuationColor = tuple3(volume?.attenuationColor);
502
620
  const invalid = (extension, field, reason, actual) => err(
503
621
  gltfErr("gltf-material-transmission-invalid", {
@@ -507,6 +625,14 @@ function parseMaterial(matJson, textures) {
507
625
  ...actual === void 0 ? {} : { actual }
508
626
  })
509
627
  );
628
+ const physicalInvalid = (extension, field, reason, actual) => err(
629
+ gltfErr("gltf-material-physical-invalid", {
630
+ extension,
631
+ field,
632
+ reason,
633
+ ...actual === void 0 ? {} : { actual }
634
+ })
635
+ );
510
636
  if (transmission?.transmissionFactor !== void 0 && (typeof transmission.transmissionFactor !== "number" || !Number.isFinite(transmission.transmissionFactor))) {
511
637
  return invalid(
512
638
  "KHR_materials_transmission",
@@ -557,6 +683,98 @@ function parseMaterial(matJson, textures) {
557
683
  if (alphaMode === "BLEND" && (transmission?.transmissionFactor ?? 0) > 0) {
558
684
  return invalid("KHR_materials_transmission", "alphaMode", "blend", alphaMode);
559
685
  }
686
+ if (clearcoat?.clearcoatFactor !== void 0 && (typeof clearcoat.clearcoatFactor !== "number" || !Number.isFinite(clearcoat.clearcoatFactor))) {
687
+ return physicalInvalid(
688
+ "KHR_materials_clearcoat",
689
+ "clearcoatFactor",
690
+ "non-finite",
691
+ clearcoat.clearcoatFactor
692
+ );
693
+ }
694
+ if (clearcoat?.clearcoatFactor !== void 0 && (clearcoat.clearcoatFactor < 0 || clearcoat.clearcoatFactor > 1)) {
695
+ return physicalInvalid(
696
+ "KHR_materials_clearcoat",
697
+ "clearcoatFactor",
698
+ "range",
699
+ clearcoat.clearcoatFactor
700
+ );
701
+ }
702
+ if (clearcoat?.clearcoatRoughnessFactor !== void 0 && (typeof clearcoat.clearcoatRoughnessFactor !== "number" || !Number.isFinite(clearcoat.clearcoatRoughnessFactor))) {
703
+ return physicalInvalid(
704
+ "KHR_materials_clearcoat",
705
+ "clearcoatRoughnessFactor",
706
+ "non-finite",
707
+ clearcoat.clearcoatRoughnessFactor
708
+ );
709
+ }
710
+ if (clearcoat?.clearcoatRoughnessFactor !== void 0 && (clearcoat.clearcoatRoughnessFactor < 0 || clearcoat.clearcoatRoughnessFactor > 1)) {
711
+ return physicalInvalid(
712
+ "KHR_materials_clearcoat",
713
+ "clearcoatRoughnessFactor",
714
+ "range",
715
+ clearcoat.clearcoatRoughnessFactor
716
+ );
717
+ }
718
+ if (clearcoat?.clearcoatNormalTexture?.scale !== void 0 && (typeof clearcoat.clearcoatNormalTexture.scale !== "number" || !Number.isFinite(clearcoat.clearcoatNormalTexture.scale) || clearcoat.clearcoatNormalTexture.scale < 0)) {
719
+ return physicalInvalid(
720
+ "KHR_materials_clearcoat",
721
+ "clearcoatNormalTexture.scale",
722
+ "range",
723
+ clearcoat.clearcoatNormalTexture.scale
724
+ );
725
+ }
726
+ const validateUnit = (extension, field, value) => {
727
+ if (value === void 0) return void 0;
728
+ if (typeof value !== "number" || !Number.isFinite(value))
729
+ return physicalInvalid(extension, field, "non-finite", value);
730
+ if (value < 0 || value > 1) return physicalInvalid(extension, field, "range", value);
731
+ return void 0;
732
+ };
733
+ const validateFinite = (extension, field, value, minimum) => {
734
+ if (value === void 0) return void 0;
735
+ if (typeof value !== "number" || !Number.isFinite(value))
736
+ return physicalInvalid(extension, field, "non-finite", value);
737
+ if (minimum !== void 0 && value < minimum)
738
+ return physicalInvalid(extension, field, "range", value);
739
+ return void 0;
740
+ };
741
+ const validateColor = (extension, field, value) => {
742
+ if (value === void 0) return void 0;
743
+ if (value.length < 3 || value.slice(0, 3).some((channel) => typeof channel !== "number" || !Number.isFinite(channel))) {
744
+ return physicalInvalid(extension, field, "type", value);
745
+ }
746
+ if (value.slice(0, 3).some((channel) => channel < 0 || channel > 1)) {
747
+ return physicalInvalid(extension, field, "range", value);
748
+ }
749
+ return void 0;
750
+ };
751
+ const physicalChecks = [
752
+ validateUnit("KHR_materials_anisotropy", "anisotropyStrength", anisotropy?.anisotropyStrength),
753
+ validateFinite(
754
+ "KHR_materials_anisotropy",
755
+ "anisotropyRotation",
756
+ anisotropy?.anisotropyRotation
757
+ ),
758
+ validateUnit("KHR_materials_sheen", "sheenRoughnessFactor", sheen?.sheenRoughnessFactor),
759
+ validateColor("KHR_materials_sheen", "sheenColorFactor", sheen?.sheenColorFactor),
760
+ validateUnit("KHR_materials_iridescence", "iridescenceFactor", iridescence?.iridescenceFactor),
761
+ validateFinite("KHR_materials_iridescence", "iridescenceIor", iridescence?.iridescenceIor, 1),
762
+ validateFinite(
763
+ "KHR_materials_iridescence",
764
+ "iridescenceThicknessMinimum",
765
+ iridescence?.iridescenceThicknessMinimum,
766
+ 0
767
+ ),
768
+ validateFinite(
769
+ "KHR_materials_iridescence",
770
+ "iridescenceThicknessMaximum",
771
+ iridescence?.iridescenceThicknessMaximum,
772
+ 0
773
+ ),
774
+ validateUnit("KHR_materials_specular", "specularFactor", specular?.specularFactor),
775
+ validateColor("KHR_materials_specular", "specularColorFactor", specular?.specularColorFactor)
776
+ ];
777
+ for (const check of physicalChecks) if (check !== void 0) return check;
560
778
  return ok({
561
779
  ...matJson.name === void 0 ? {} : { name: matJson.name },
562
780
  baseColorFactor: baseColor4,
@@ -593,6 +811,65 @@ function parseMaterial(matJson, textures) {
593
811
  ...attenuationColor === void 0 ? {} : { attenuationColor },
594
812
  ...finiteOrUndefined(volume.attenuationDistance) === void 0 ? {} : { attenuationDistance: volume.attenuationDistance }
595
813
  },
814
+ ...clearcoat === void 0 ? {} : {
815
+ clearcoatFactor: clearcoat.clearcoatFactor ?? 0,
816
+ clearcoatChannel: 0,
817
+ clearcoatRoughnessFactor: clearcoat.clearcoatRoughnessFactor ?? 0,
818
+ clearcoatRoughnessChannel: 1,
819
+ clearcoatNormalChannel: [0, 1],
820
+ ...clearcoat.clearcoatTexture === void 0 ? {} : { clearcoatTexture: parseTextureInfo(clearcoat.clearcoatTexture, textures) },
821
+ ...clearcoat.clearcoatRoughnessTexture === void 0 ? {} : {
822
+ clearcoatRoughnessTexture: parseTextureInfo(
823
+ clearcoat.clearcoatRoughnessTexture,
824
+ textures
825
+ )
826
+ },
827
+ ...clearcoat.clearcoatNormalTexture === void 0 ? {} : {
828
+ clearcoatNormalTexture: {
829
+ ...parseTextureInfo(clearcoat.clearcoatNormalTexture, textures),
830
+ ...clearcoat.clearcoatNormalTexture.scale === void 0 ? {} : { scale: clearcoat.clearcoatNormalTexture.scale }
831
+ }
832
+ }
833
+ },
834
+ ...anisotropy === void 0 ? {} : {
835
+ anisotropyStrength: anisotropy.anisotropyStrength ?? 0,
836
+ anisotropyRotation: anisotropy.anisotropyRotation ?? 0,
837
+ anisotropyChannel: [0, 1, 2],
838
+ ...anisotropy.anisotropyTexture === void 0 ? {} : { anisotropyTexture: parseTextureInfo(anisotropy.anisotropyTexture, textures) }
839
+ },
840
+ ...sheen === void 0 ? {} : {
841
+ sheenColorFactor: tuple3(sheen.sheenColorFactor) ?? [0, 0, 0],
842
+ sheenColorChannel: [0, 1, 2],
843
+ sheenRoughnessFactor: sheen.sheenRoughnessFactor ?? 0,
844
+ sheenRoughnessChannel: 3,
845
+ ...sheen.sheenColorTexture === void 0 ? {} : { sheenColorTexture: parseTextureInfo(sheen.sheenColorTexture, textures) },
846
+ ...sheen.sheenRoughnessTexture === void 0 ? {} : { sheenRoughnessTexture: parseTextureInfo(sheen.sheenRoughnessTexture, textures) }
847
+ },
848
+ ...iridescence === void 0 ? {} : {
849
+ iridescenceFactor: iridescence.iridescenceFactor ?? 0,
850
+ iridescenceIor: iridescence.iridescenceIor ?? 1.3,
851
+ iridescenceThicknessMinimum: iridescence.iridescenceThicknessMinimum ?? 100,
852
+ iridescenceThicknessMaximum: iridescence.iridescenceThicknessMaximum ?? 400,
853
+ iridescenceChannel: 0,
854
+ iridescenceThicknessChannel: 1,
855
+ ...iridescence.iridescenceTexture === void 0 ? {} : { iridescenceTexture: parseTextureInfo(iridescence.iridescenceTexture, textures) },
856
+ ...iridescence.iridescenceThicknessTexture === void 0 ? {} : {
857
+ iridescenceThicknessTexture: parseTextureInfo(
858
+ iridescence.iridescenceThicknessTexture,
859
+ textures
860
+ )
861
+ }
862
+ },
863
+ ...specular === void 0 ? {} : {
864
+ specularFactor: specular.specularFactor ?? 1,
865
+ specularChannel: 3,
866
+ specularColorFactor: tuple3(specular.specularColorFactor) ?? [1, 1, 1],
867
+ specularColorChannel: [0, 1, 2],
868
+ ...specular.specularTexture === void 0 ? {} : { specularTexture: parseTextureInfo(specular.specularTexture, textures) },
869
+ ...specular.specularColorTexture === void 0 ? {} : {
870
+ specularColorTexture: parseTextureInfo(specular.specularColorTexture, textures)
871
+ }
872
+ },
596
873
  ...alphaMode === void 0 ? {} : { alphaMode },
597
874
  ...alphaCutoff === void 0 ? {} : { alphaCutoff },
598
875
  ...matJson.doubleSided === true ? { doubleSided: true } : {},
@@ -1479,6 +1756,8 @@ async function parseGltfWithBin(json, ctx) {
1479
1756
  const extResult = checkExtensions(json);
1480
1757
  if (!extResult.ok) return err(extResult.error);
1481
1758
  const unsupportedExtensions = extResult.value.unsupportedUsed;
1759
+ const lodResult = parseGltfLodExtension(json);
1760
+ if (!lodResult.ok) return err(lodResult.error);
1482
1761
  const lightsResult = parsePunctualLights(json, ctx.filePath);
1483
1762
  if (!lightsResult.ok) return err(lightsResult.error);
1484
1763
  const meshesJson = json.meshes ?? [];
@@ -2019,7 +2298,8 @@ async function parseGltfWithBin(json, ctx) {
2019
2298
  matrixTrsCoexistNodes: diagnostics.matrixTrsCoexistNodes
2020
2299
  },
2021
2300
  meshPrimitiveCount,
2022
- lights: lightsResult.value
2301
+ lights: lightsResult.value,
2302
+ ...lodResult.value.lodNodeIds.length === 0 ? {} : { lod: lodResult.value }
2023
2303
  });
2024
2304
  }
2025
2305
  async function parseGltfForImporter(json, externalLoader, filePath, options = {}) {
@@ -2158,6 +2438,74 @@ function toAssetPack(doc, existingMeta, source) {
2158
2438
  const sourceOverrides = {
2159
2439
  ...existingMeta?.sourceOverrides ?? {}
2160
2440
  };
2441
+ const lodGroups = doc.lod === void 0 ? [] : doc.lod.groups.length > 0 ? doc.lod.groups : [doc.lod];
2442
+ for (const lodGroup of lodGroups) {
2443
+ if (lodGroup.lodNodeIds.length === 0) continue;
2444
+ const rootMeshIndex = doc.nodes[lodGroup.rootNode]?.meshIndex;
2445
+ const referencedMeshIndices = [
2446
+ rootMeshIndex,
2447
+ ...lodGroup.lodNodeIds.map((nodeIndex) => doc.nodes[nodeIndex]?.meshIndex)
2448
+ ];
2449
+ if (referencedMeshIndices.some(
2450
+ (meshIndex) => !Number.isInteger(meshIndex) || meshIndex === null || meshIndex === void 0 || !seenMeshIndices.has(meshIndex)
2451
+ )) {
2452
+ return err(
2453
+ gltfErr("gltf-lod-invalid", {
2454
+ rootNode: lodGroup.rootNode,
2455
+ ids: lodGroup.lodNodeIds,
2456
+ reason: "missing-node"
2457
+ })
2458
+ );
2459
+ }
2460
+ const rootOutput = rootMeshIndex === void 0 || rootMeshIndex === null ? void 0 : subAssetByKindIndex.get(`mesh:${rootMeshIndex}`);
2461
+ const levels = lodGroup.lodNodeIds.map((nodeIndex, index) => {
2462
+ const meshIndex = doc.nodes[nodeIndex]?.meshIndex;
2463
+ const output = meshIndex === void 0 || meshIndex === null ? void 0 : subAssetByKindIndex.get(`mesh:${meshIndex}`);
2464
+ return {
2465
+ sourceKey: output?.sourceKey ?? `mesh:${meshIndex ?? nodeIndex}`,
2466
+ meshGuid: output?.guid ?? "",
2467
+ ...lodGroup.screenCoverages[index] === void 0 ? {} : { screenCoverage: lodGroup.screenCoverages[index] }
2468
+ };
2469
+ });
2470
+ if (rootOutput?.sourceKey !== void 0 && levels.every((level) => level.meshGuid !== "")) {
2471
+ const previousLodMeta = (() => {
2472
+ const raw = existingMeta?.sourceOverrides?.[rootOutput.sourceKey]?.lods;
2473
+ if (!Array.isArray(raw)) return void 0;
2474
+ return raw.flatMap((entry) => {
2475
+ if (entry === null || typeof entry !== "object") return [];
2476
+ const value = entry;
2477
+ return typeof value.sourceKey === "string" && typeof value.meshGuid === "string" ? [
2478
+ {
2479
+ sourceKey: value.sourceKey,
2480
+ guid: value.meshGuid,
2481
+ ...typeof value.screenCoverage === "number" ? { screenCoverage: value.screenCoverage } : {}
2482
+ }
2483
+ ] : [];
2484
+ });
2485
+ })();
2486
+ const projected = projectGltfLodMeta({
2487
+ rootSourceKey: rootOutput.sourceKey,
2488
+ levels: levels.map((level) => ({
2489
+ sourceKey: level.sourceKey,
2490
+ guid: level.meshGuid,
2491
+ ...level.screenCoverage === void 0 ? {} : { screenCoverage: level.screenCoverage }
2492
+ })),
2493
+ ...previousLodMeta === void 0 ? {} : { previous: previousLodMeta }
2494
+ });
2495
+ if (!projected.ok)
2496
+ return err(
2497
+ gltfErr("gltf-lod-invalid", {
2498
+ rootNode: lodGroup.rootNode,
2499
+ ids: lodGroup.lodNodeIds,
2500
+ reason: "coverage"
2501
+ })
2502
+ );
2503
+ sourceOverrides[rootOutput.sourceKey] = {
2504
+ ...sourceOverrides[rootOutput.sourceKey] ?? {},
2505
+ lods: projected.value.lods
2506
+ };
2507
+ }
2508
+ }
2161
2509
  for (const meshIndex of seenMeshIndices) {
2162
2510
  const meshOutput = subAssetByKindIndex.get(`mesh:${meshIndex}`);
2163
2511
  if (meshOutput?.sourceKey === void 0) continue;
@@ -2252,13 +2600,13 @@ function serializeMetaJson(meta) {
2252
2600
  }
2253
2601
 
2254
2602
  // src/cli-gltf.ts
2255
- function emitError(ctx, err2) {
2603
+ function emitError(ctx, err3) {
2256
2604
  const payload = {
2257
- code: err2.code,
2258
- expected: err2.expected,
2259
- hint: err2.hint
2605
+ code: err3.code,
2606
+ expected: err3.expected,
2607
+ hint: err3.hint
2260
2608
  };
2261
- if (err2.detail !== void 0) payload.detail = err2.detail;
2609
+ if (err3.detail !== void 0) payload.detail = err3.detail;
2262
2610
  ctx.stderrWrite(JSON.stringify(payload));
2263
2611
  return 1;
2264
2612
  }