@forgeax/engine-gltf 0.1.20 → 0.1.23

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 (49) hide show
  1. package/README.md +59 -0
  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/check-extensions.d.ts.map +1 -1
  7. package/dist/cli-gltf.mjs +183 -8
  8. package/dist/cli-gltf.mjs.map +1 -1
  9. package/dist/errors.d.ts +6 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/gltf-importer.d.ts.map +1 -1
  12. package/dist/importer-entry.mjs +145 -3
  13. package/dist/importer-entry.mjs.map +1 -1
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.mjs +238 -6
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/lod/parse-lod.d.ts +15 -0
  19. package/dist/lod/parse-lod.d.ts.map +1 -0
  20. package/dist/lod/project-meta.d.ts +23 -0
  21. package/dist/lod/project-meta.d.ts.map +1 -0
  22. package/dist/node-file-entry.mjs +86 -2
  23. package/dist/node-file-entry.mjs.map +1 -1
  24. package/dist/parse-gltf.d.ts +3 -1
  25. package/dist/parse-gltf.d.ts.map +1 -1
  26. package/dist/reimport-reuse-meta.d.ts +1 -1
  27. package/dist/reimport-reuse-meta.d.ts.map +1 -1
  28. package/package.json +12 -12
  29. package/src/__tests__/fixtures/lod/core-only.gltf +1 -0
  30. package/src/__tests__/fixtures/lod/msft-lod-malformed.gltf +1 -0
  31. package/src/__tests__/fixtures/lod/msft-lod-multi-root.gltf +1 -0
  32. package/src/__tests__/fixtures/lod/msft-lod-node-extras.gltf +1 -0
  33. package/src/__tests__/fixtures/lod/msft-lod.gltf +1 -0
  34. package/src/__tests__/gltf-error-derived-views.test-d.ts +4 -0
  35. package/src/__tests__/gltf.unit.test.ts +12 -3
  36. package/src/__tests__/lod-import.integration.test.ts +106 -0
  37. package/src/__tests__/lod-meta-publication.integration.test.ts +24 -0
  38. package/src/__tests__/parse-gltf.unit.test.ts +22 -0
  39. package/src/__tests__/source-key-producer.integration.test.ts +6 -2
  40. package/src/bridge.ts +1 -1
  41. package/src/check-extensions.ts +2 -0
  42. package/src/errors.ts +11 -0
  43. package/src/gltf-importer.ts +87 -1
  44. package/src/index.ts +2 -0
  45. package/src/lod/parse-lod.ts +129 -0
  46. package/src/lod/project-meta.ts +48 -0
  47. package/src/parse-gltf.ts +97 -1
  48. package/src/reimport-reuse-meta.ts +3 -1
  49. package/dist/.tsbuildinfo +0 -1
@@ -0,0 +1,15 @@
1
+ import { gltfErr, type Result } from '../errors.js';
2
+ export interface GltfLodRelation {
3
+ readonly rootNode: number;
4
+ readonly lodNodeIds: readonly number[];
5
+ readonly screenCoverages: readonly number[];
6
+ /** All node-level MSFT_lod groups, retained for multi-root scenes. */
7
+ readonly groups: readonly GltfLodGroup[];
8
+ }
9
+ export interface GltfLodGroup {
10
+ readonly rootNode: number;
11
+ readonly lodNodeIds: readonly number[];
12
+ readonly screenCoverages: readonly number[];
13
+ }
14
+ export declare function parseGltfLodExtension(json: unknown): Result<GltfLodRelation, ReturnType<typeof gltfErr<'gltf-lod-invalid'>>>;
15
+ //# sourceMappingURL=parse-lod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-lod.d.ts","sourceRoot":"","sources":["../../src/lod/parse-lod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,OAAO,EAAM,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAmBD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAgEzE"}
@@ -0,0 +1,23 @@
1
+ import { type Result } from '@forgeax/engine-types';
2
+ export interface GltfLodMetaLevel {
3
+ readonly sourceKey: string;
4
+ readonly guid: string;
5
+ readonly screenCoverage?: number;
6
+ }
7
+ export interface GltfLodMetaCandidate {
8
+ readonly rootSourceKey: string;
9
+ readonly levels: readonly GltfLodMetaLevel[];
10
+ readonly previous?: readonly GltfLodMetaLevel[] | undefined;
11
+ }
12
+ export interface GltfLodMetaOutput {
13
+ readonly sourceKey: string;
14
+ readonly meshGuid: string;
15
+ readonly screenCoverage: number;
16
+ }
17
+ export declare function projectGltfLodMeta(input: GltfLodMetaCandidate): Result<{
18
+ readonly lods: readonly GltfLodMetaOutput[];
19
+ }, {
20
+ readonly code: string;
21
+ readonly reason: string;
22
+ }>;
23
+ //# sourceMappingURL=project-meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-meta.d.ts","sourceRoot":"","sources":["../../src/lod/project-meta.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,gBAAgB,EAAE,GAAG,SAAS,CAAC;CAC7D;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,oBAAoB,GAC1B,MAAM,CACP;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAAE,EAC/C;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CACnD,CAqBA"}
@@ -22,6 +22,10 @@ var gltfErrorPolicy = {
22
22
  expected: "extension listed in v1 allowlist (see EXTENSION_ALLOWLIST in @forgeax/engine-gltf)",
23
23
  hint: "see feat-future-gltf-extensions-allowlist; remove this extension or wait for the allowlist to expand"
24
24
  },
25
+ "gltf-lod-invalid": {
26
+ expected: "MSFT_lod ids to reference unique existing node indices with valid coverage",
27
+ hint: "repair the MSFT_lod node relation or remove it from extensionsRequired before re-importing"
28
+ },
25
29
  "gltf-material-transmission-invalid": {
26
30
  expected: "KHR transmission, IOR, and volume values are finite and within their glTF ranges",
27
31
  hint: "repair the named glTF material extension value and re-import the source"
@@ -389,7 +393,9 @@ function decodeColorAccessor(input, flags = {}) {
389
393
  // src/check-extensions.ts
390
394
  var EXTENSION_ALLOWLIST = [
391
395
  "EXT_mesh_gpu_instancing",
392
- "EXT_meshopt_compression"
396
+ "EXT_meshopt_compression",
397
+ "MSFT_lod",
398
+ "MSFT_screencoverage"
393
399
  ];
394
400
  var SUPPORTED_EXTENSIONS = [
395
401
  ...EXTENSION_ALLOWLIST,
@@ -449,6 +455,81 @@ function decodeBase64(b64) {
449
455
  return out;
450
456
  }
451
457
 
458
+ // src/lod/parse-lod.ts
459
+ function parseGltfLodExtension(json) {
460
+ const doc = json;
461
+ const nodes = doc.nodes ?? [];
462
+ const rawCoverage = doc.extensions?.MSFT_screencoverage?.scales;
463
+ const groups = [];
464
+ for (let rootNode = 0; rootNode < nodes.length; rootNode += 1) {
465
+ if (nodes[rootNode]?.extensions?.MSFT_lod === void 0) continue;
466
+ const idsRaw = nodes[rootNode]?.extensions?.MSFT_lod?.ids;
467
+ if (!Array.isArray(idsRaw)) {
468
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids: [], reason: "not-integer" }));
469
+ }
470
+ const ids = idsRaw.filter((value) => typeof value === "number");
471
+ if (ids.length !== idsRaw.length || ids.some((id) => !Number.isInteger(id) || id < 0 || id >= nodes.length)) {
472
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "missing-node" }));
473
+ }
474
+ if (new Set(ids).size !== ids.length || ids.includes(rootNode)) {
475
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "duplicate-node" }));
476
+ }
477
+ const hasMesh = (node) => {
478
+ if (typeof node?.mesh !== "number" || !Number.isInteger(node.mesh) || node.mesh < 0) {
479
+ return false;
480
+ }
481
+ const mesh = doc.meshes?.[node.mesh];
482
+ return mesh !== void 0 && Array.isArray(mesh.primitives) && mesh.primitives.length > 0;
483
+ };
484
+ if (!hasMesh(nodes[rootNode]) || ids.some((id) => !hasMesh(nodes[id]))) {
485
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "missing-node" }));
486
+ }
487
+ const nodeCoverage = nodes[rootNode]?.extras?.MSFT_screencoverage;
488
+ const coverage = normalizeScreenCoverages(
489
+ nodeCoverage === void 0 ? rawCoverage : nodeCoverage,
490
+ ids.length,
491
+ nodeCoverage !== void 0
492
+ );
493
+ if (coverage === "invalid") {
494
+ return err(gltfErr("gltf-lod-invalid", { rootNode, ids, reason: "coverage" }));
495
+ }
496
+ groups.push({
497
+ rootNode,
498
+ lodNodeIds: ids,
499
+ screenCoverages: coverage ?? []
500
+ });
501
+ }
502
+ const first = groups[0];
503
+ if (first === void 0) {
504
+ if (doc.extensionsRequired?.some((extension) => extension === "MSFT_lod")) {
505
+ return err(gltfErr("gltf-lod-invalid", { rootNode: 0, ids: [], reason: "missing-node" }));
506
+ }
507
+ return ok({ rootNode: 0, lodNodeIds: [], screenCoverages: [], groups: [] });
508
+ }
509
+ return ok({
510
+ rootNode: first.rootNode,
511
+ lodNodeIds: first.lodNodeIds,
512
+ screenCoverages: first.screenCoverages,
513
+ groups
514
+ });
515
+ }
516
+ function normalizeScreenCoverages(raw, lodCount, officialNodeForm) {
517
+ if (raw === void 0) return void 0;
518
+ if (!Array.isArray(raw)) return "invalid";
519
+ if (raw.some(
520
+ (value) => typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1
521
+ )) {
522
+ return "invalid";
523
+ }
524
+ const expectedLengths = officialNodeForm ? [lodCount + 1] : [lodCount, lodCount + 1];
525
+ if (!expectedLengths.includes(raw.length)) return "invalid";
526
+ const values = raw.slice(0, lodCount);
527
+ for (let index = 1; index < values.length; index += 1) {
528
+ if (values[index] >= values[index - 1]) return "invalid";
529
+ }
530
+ return values;
531
+ }
532
+
452
533
  // src/material/parse-material.ts
453
534
  function tuple2(values) {
454
535
  if (values === void 0 || values.length < 2) return void 0;
@@ -1374,6 +1455,8 @@ async function parseGltfWithBin(json, ctx) {
1374
1455
  const extResult = checkExtensions(json);
1375
1456
  if (!extResult.ok) return err(extResult.error);
1376
1457
  const unsupportedExtensions = extResult.value.unsupportedUsed;
1458
+ const lodResult = parseGltfLodExtension(json);
1459
+ if (!lodResult.ok) return err(lodResult.error);
1377
1460
  const lightsResult = parsePunctualLights(json, ctx.filePath);
1378
1461
  if (!lightsResult.ok) return err(lightsResult.error);
1379
1462
  const meshesJson = json.meshes ?? [];
@@ -1914,7 +1997,8 @@ async function parseGltfWithBin(json, ctx) {
1914
1997
  matrixTrsCoexistNodes: diagnostics.matrixTrsCoexistNodes
1915
1998
  },
1916
1999
  meshPrimitiveCount,
1917
- lights: lightsResult.value
2000
+ lights: lightsResult.value,
2001
+ ...lodResult.value.lodNodeIds.length === 0 ? {} : { lod: lodResult.value }
1918
2002
  });
1919
2003
  }
1920
2004
  async function parseGltfForImporter(json, externalLoader, filePath, options = {}) {