@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79

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 (220) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +204 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
  7. package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
  9. package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
  11. package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
  13. package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
  14. package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
  15. package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
  17. package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
  19. package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
  20. package/dist/__tests__/bridge.unit.test.d.ts +2 -0
  21. package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
  23. package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
  25. package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
  27. package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
  29. package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
  31. package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
  33. package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
  34. package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
  35. package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/gltf-importer.test.d.ts +2 -0
  37. package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
  38. package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
  39. package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
  40. package/dist/__tests__/gltf.unit.test.d.ts +2 -0
  41. package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
  43. package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
  44. package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
  45. package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
  47. package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
  48. package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
  49. package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
  51. package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
  53. package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
  55. package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
  57. package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
  59. package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
  60. package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
  61. package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
  62. package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
  63. package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
  64. package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
  65. package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
  67. package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
  69. package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
  70. package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
  71. package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
  73. package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
  75. package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
  76. package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
  77. package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
  79. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
  80. package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
  81. package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
  82. package/dist/__tests__/source-key.unit.test.d.ts +2 -0
  83. package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
  84. package/dist/accessor/decode-accessor.d.ts +75 -0
  85. package/dist/accessor/decode-accessor.d.ts.map +1 -0
  86. package/dist/accessor/decode-color.d.ts +15 -0
  87. package/dist/accessor/decode-color.d.ts.map +1 -0
  88. package/dist/accessor/index.d.ts +3 -0
  89. package/dist/accessor/index.d.ts.map +1 -0
  90. package/dist/bridge.d.ts +103 -0
  91. package/dist/bridge.d.ts.map +1 -0
  92. package/dist/check-extensions.d.ts +29 -0
  93. package/dist/check-extensions.d.ts.map +1 -0
  94. package/dist/cli-gltf.d.ts +8 -0
  95. package/dist/cli-gltf.d.ts.map +1 -0
  96. package/dist/cli-gltf.mjs +2435 -0
  97. package/dist/cli-gltf.mjs.map +1 -0
  98. package/dist/data-uri.d.ts +6 -0
  99. package/dist/data-uri.d.ts.map +1 -0
  100. package/dist/errors.d.ts +201 -0
  101. package/dist/errors.d.ts.map +1 -0
  102. package/dist/extract-image-bytes.d.ts +42 -0
  103. package/dist/extract-image-bytes.d.ts.map +1 -0
  104. package/dist/gltf-importer.d.ts +21 -0
  105. package/dist/gltf-importer.d.ts.map +1 -0
  106. package/dist/image-color-space.d.ts +35 -0
  107. package/dist/image-color-space.d.ts.map +1 -0
  108. package/dist/importer-entry.d.ts +10 -0
  109. package/dist/importer-entry.d.ts.map +1 -0
  110. package/dist/importer-entry.mjs +3378 -0
  111. package/dist/importer-entry.mjs.map +1 -0
  112. package/dist/index.d.ts +25 -0
  113. package/dist/index.d.ts.map +1 -0
  114. package/dist/index.mjs +3673 -0
  115. package/dist/index.mjs.map +1 -0
  116. package/dist/meshopt-decode.d.ts +39 -0
  117. package/dist/meshopt-decode.d.ts.map +1 -0
  118. package/dist/node-file-entry.d.ts +14 -0
  119. package/dist/node-file-entry.d.ts.map +1 -0
  120. package/dist/node-file-entry.mjs +1946 -0
  121. package/dist/node-file-entry.mjs.map +1 -0
  122. package/dist/node-path.d.ts +14 -0
  123. package/dist/node-path.d.ts.map +1 -0
  124. package/dist/parse-animation.d.ts +70 -0
  125. package/dist/parse-animation.d.ts.map +1 -0
  126. package/dist/parse-glb-chunks.d.ts +25 -0
  127. package/dist/parse-glb-chunks.d.ts.map +1 -0
  128. package/dist/parse-glb.d.ts +3 -0
  129. package/dist/parse-glb.d.ts.map +1 -0
  130. package/dist/parse-gltf-header.d.ts +18 -0
  131. package/dist/parse-gltf-header.d.ts.map +1 -0
  132. package/dist/parse-gltf.d.ts +263 -0
  133. package/dist/parse-gltf.d.ts.map +1 -0
  134. package/dist/parse-skin.d.ts +70 -0
  135. package/dist/parse-skin.d.ts.map +1 -0
  136. package/dist/reimport-reuse-meta.d.ts +48 -0
  137. package/dist/reimport-reuse-meta.d.ts.map +1 -0
  138. package/dist/serialize-meta.d.ts +2 -0
  139. package/dist/serialize-meta.d.ts.map +1 -0
  140. package/dist/source-key.d.ts +44 -0
  141. package/dist/source-key.d.ts.map +1 -0
  142. package/dist/sub-asset-key.d.ts +12 -0
  143. package/dist/sub-asset-key.d.ts.map +1 -0
  144. package/dist/to-asset-pack.d.ts +2 -0
  145. package/dist/to-asset-pack.d.ts.map +1 -0
  146. package/dist/transform.d.ts +37 -0
  147. package/dist/transform.d.ts.map +1 -0
  148. package/package.json +92 -0
  149. package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
  150. package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
  151. package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
  152. package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
  153. package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
  154. package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
  155. package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
  156. package/src/__tests__/bridge-test-helpers.ts +9 -0
  157. package/src/__tests__/bridge.unit.test.ts +1227 -0
  158. package/src/__tests__/cli-gltf.integration.test.ts +122 -0
  159. package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
  160. package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
  161. package/src/__tests__/extension-admission.unit.test.ts +19 -0
  162. package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
  163. package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
  164. package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
  165. package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
  166. package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
  167. package/src/__tests__/gltf-error-migration.test.ts +147 -0
  168. package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
  169. package/src/__tests__/gltf-importer.test.ts +12 -0
  170. package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
  171. package/src/__tests__/gltf.unit.test.ts +2975 -0
  172. package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
  173. package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
  174. package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
  175. package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
  176. package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
  177. package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
  178. package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
  179. package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
  180. package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
  181. package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
  182. package/src/__tests__/morph-import.integration.test.ts +115 -0
  183. package/src/__tests__/morph-import.unit.test.ts +62 -0
  184. package/src/__tests__/parse-decode.unit.test.ts +919 -0
  185. package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
  186. package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
  187. package/src/__tests__/parse-gltf.unit.test.ts +133 -0
  188. package/src/__tests__/pick-e2e.integration.test.ts +238 -0
  189. package/src/__tests__/public-surface.unit.test.ts +10 -0
  190. package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
  191. package/src/__tests__/source-key-producer.integration.test.ts +171 -0
  192. package/src/__tests__/source-key.unit.test.ts +159 -0
  193. package/src/accessor/decode-accessor.ts +309 -0
  194. package/src/accessor/decode-color.ts +128 -0
  195. package/src/accessor/index.ts +15 -0
  196. package/src/bridge.ts +928 -0
  197. package/src/check-extensions.ts +85 -0
  198. package/src/cli-gltf.ts +339 -0
  199. package/src/data-uri.ts +29 -0
  200. package/src/errors.ts +386 -0
  201. package/src/extract-image-bytes.ts +343 -0
  202. package/src/gltf-importer.ts +976 -0
  203. package/src/image-color-space.ts +104 -0
  204. package/src/importer-entry.ts +24 -0
  205. package/src/index.ts +148 -0
  206. package/src/meshopt-decode.ts +200 -0
  207. package/src/node-file-entry.ts +98 -0
  208. package/src/node-path.ts +41 -0
  209. package/src/parse-animation.ts +286 -0
  210. package/src/parse-glb-chunks.ts +167 -0
  211. package/src/parse-glb.ts +16 -0
  212. package/src/parse-gltf-header.ts +58 -0
  213. package/src/parse-gltf.ts +1731 -0
  214. package/src/parse-skin.ts +238 -0
  215. package/src/reimport-reuse-meta.ts +134 -0
  216. package/src/serialize-meta.ts +24 -0
  217. package/src/source-key.ts +106 -0
  218. package/src/sub-asset-key.ts +61 -0
  219. package/src/to-asset-pack.ts +8 -0
  220. package/src/transform.ts +117 -0
@@ -0,0 +1,79 @@
1
+ /// <reference types="node" />
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import { describe, expect, expectTypeOf, it } from 'vitest';
5
+ import type {
6
+ GLTF_MESHOPT_FILTERS,
7
+ GLTF_MESHOPT_MODES,
8
+ GltfMeshoptDecodeFailedDetail,
9
+ GltfMeshoptFilter,
10
+ GltfMeshoptMode,
11
+ } from '../errors.js';
12
+ import type {
13
+ GltfBufferViewDecodeCapability,
14
+ MeshoptCompressionJson,
15
+ MeshoptFilter,
16
+ MeshoptMode,
17
+ } from '../meshopt-decode.js';
18
+
19
+ const errorsSource = readFileSync(new URL('../errors.ts', import.meta.url), 'utf8');
20
+ const meshoptSource = readFileSync(new URL('../meshopt-decode.ts', import.meta.url), 'utf8');
21
+
22
+ type DecodedMode = Parameters<GltfBufferViewDecodeCapability['decode']>[0]['mode'];
23
+ type DecodedFilter = Parameters<GltfBufferViewDecodeCapability['decode']>[0]['filter'];
24
+
25
+ describe('glTF Meshopt mode/filter owner', () => {
26
+ it('keeps tuple order and derives every Meshopt vocabulary projection', () => {
27
+ expectTypeOf<typeof GLTF_MESHOPT_MODES>().toEqualTypeOf<
28
+ readonly ['ATTRIBUTES', 'TRIANGLES', 'INDICES']
29
+ >();
30
+ expectTypeOf<typeof GLTF_MESHOPT_FILTERS>().toEqualTypeOf<
31
+ readonly ['NONE', 'OCTAHEDRAL', 'QUATERNION', 'EXPONENTIAL']
32
+ >();
33
+
34
+ expectTypeOf<GltfMeshoptMode>().toEqualTypeOf<(typeof GLTF_MESHOPT_MODES)[number]>();
35
+ expectTypeOf<(typeof GLTF_MESHOPT_MODES)[number]>().toEqualTypeOf<GltfMeshoptMode>();
36
+ expectTypeOf<GltfMeshoptFilter>().toEqualTypeOf<(typeof GLTF_MESHOPT_FILTERS)[number]>();
37
+ expectTypeOf<(typeof GLTF_MESHOPT_FILTERS)[number]>().toEqualTypeOf<GltfMeshoptFilter>();
38
+
39
+ expectTypeOf<MeshoptMode>().toEqualTypeOf<GltfMeshoptMode>();
40
+ expectTypeOf<GltfMeshoptMode>().toEqualTypeOf<MeshoptMode>();
41
+ expectTypeOf<MeshoptFilter>().toEqualTypeOf<GltfMeshoptFilter>();
42
+ expectTypeOf<GltfMeshoptFilter>().toEqualTypeOf<MeshoptFilter>();
43
+ expectTypeOf<MeshoptCompressionJson['mode']>().toEqualTypeOf<MeshoptMode>();
44
+ expectTypeOf<MeshoptCompressionJson['filter']>().toEqualTypeOf<MeshoptFilter | undefined>();
45
+ expectTypeOf<DecodedMode>().toEqualTypeOf<MeshoptMode>();
46
+ expectTypeOf<DecodedFilter>().toEqualTypeOf<MeshoptFilter>();
47
+ expectTypeOf<GltfMeshoptDecodeFailedDetail['mode']>().toEqualTypeOf<MeshoptMode>();
48
+ expectTypeOf<GltfMeshoptDecodeFailedDetail['filter']>().toEqualTypeOf<MeshoptFilter>();
49
+ });
50
+
51
+ it('keeps the owner declarations and tuple-driven validation single-sourced', () => {
52
+ expect(errorsSource).toContain(
53
+ 'export type GltfMeshoptMode = (typeof GLTF_MESHOPT_MODES)[number];',
54
+ );
55
+ expect(errorsSource).toContain(
56
+ 'export type GltfMeshoptFilter = (typeof GLTF_MESHOPT_FILTERS)[number];',
57
+ );
58
+ expect(errorsSource).toContain('readonly mode: GltfMeshoptMode;');
59
+ expect(errorsSource).toContain('readonly filter: GltfMeshoptFilter;');
60
+ expect(meshoptSource).toContain('export type MeshoptMode = GltfMeshoptMode;');
61
+ expect(meshoptSource).toContain('export type MeshoptFilter = GltfMeshoptFilter;');
62
+ expect(meshoptSource).toContain(
63
+ 'return GLTF_MESHOPT_MODES.some((candidate) => candidate === mode);',
64
+ );
65
+ expect(meshoptSource).toContain(
66
+ 'return GLTF_MESHOPT_FILTERS.some((candidate) => candidate === filter);',
67
+ );
68
+ expect(meshoptSource).not.toContain(
69
+ "export type MeshoptMode = 'ATTRIBUTES' | 'TRIANGLES' | 'INDICES';",
70
+ );
71
+ expect(meshoptSource).not.toContain(
72
+ "export type MeshoptFilter = 'NONE' | 'OCTAHEDRAL' | 'QUATERNION' | 'EXPONENTIAL';",
73
+ );
74
+ expect(errorsSource).not.toContain("readonly mode: 'ATTRIBUTES' | 'TRIANGLES' | 'INDICES';");
75
+ expect(errorsSource).not.toContain(
76
+ "readonly filter: 'NONE' | 'OCTAHEDRAL' | 'QUATERNION' | 'EXPONENTIAL';",
77
+ );
78
+ });
79
+ });
@@ -0,0 +1,370 @@
1
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
2
+ import type { Asset, ImportContext, ImportedAsset } from '@forgeax/engine-types';
3
+ import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { createGltfImporter } from '../gltf-importer.js';
6
+ import { parseGlb } from '../parse-gltf.js';
7
+ import { unwrapMeshAsset as meshIrToMeshAsset } from './bridge-test-helpers.js';
8
+
9
+ function bytesOf(view: ArrayBufferView): Uint8Array {
10
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
11
+ }
12
+
13
+ function pad4(bytes: Uint8Array, fill: number): Uint8Array {
14
+ const padded = new Uint8Array((bytes.byteLength + 3) & ~3);
15
+ padded.fill(fill);
16
+ padded.set(bytes);
17
+ return padded;
18
+ }
19
+
20
+ function makeGlb(json: Record<string, unknown>, binary: Uint8Array): ArrayBuffer {
21
+ const jsonBytes = pad4(new TextEncoder().encode(JSON.stringify(json)), 0x20);
22
+ const binBytes = pad4(binary, 0);
23
+ const totalLength = 12 + 8 + jsonBytes.byteLength + 8 + binBytes.byteLength;
24
+ const glb = new Uint8Array(totalLength);
25
+ const view = new DataView(glb.buffer);
26
+ view.setUint32(0, 0x46546c67, true);
27
+ view.setUint32(4, 2, true);
28
+ view.setUint32(8, totalLength, true);
29
+ view.setUint32(12, jsonBytes.byteLength, true);
30
+ view.setUint32(16, 0x4e4f534a, true);
31
+ glb.set(jsonBytes, 20);
32
+ const binHeader = 20 + jsonBytes.byteLength;
33
+ view.setUint32(binHeader, binBytes.byteLength, true);
34
+ view.setUint32(binHeader + 4, 0x004e4942, true);
35
+ glb.set(binBytes, binHeader + 8);
36
+ return glb.buffer;
37
+ }
38
+
39
+ function compressedGlb(): ArrayBuffer {
40
+ const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
41
+ const indices = new Uint16Array([0, 1, 2]);
42
+ const positionBytes = MeshoptEncoder.encodeGltfBuffer(
43
+ bytesOf(positions),
44
+ positions.length / 3,
45
+ 12,
46
+ 'ATTRIBUTES',
47
+ );
48
+ const indexBytes = MeshoptEncoder.encodeGltfBuffer(
49
+ bytesOf(indices),
50
+ indices.length,
51
+ 2,
52
+ 'TRIANGLES',
53
+ );
54
+ const indexOffset = positionBytes.byteLength;
55
+ const binary = new Uint8Array(indexOffset + indexBytes.byteLength);
56
+ binary.set(positionBytes, 0);
57
+ binary.set(indexBytes, indexOffset);
58
+ return makeGlb(
59
+ {
60
+ asset: { version: '2.0' },
61
+ extensionsUsed: ['EXT_meshopt_compression'],
62
+ extensionsRequired: ['EXT_meshopt_compression'],
63
+ buffers: [{ byteLength: binary.byteLength }],
64
+ bufferViews: [
65
+ {
66
+ buffer: 0,
67
+ byteLength: 0,
68
+ extensions: {
69
+ EXT_meshopt_compression: {
70
+ buffer: 0,
71
+ byteOffset: 0,
72
+ byteLength: positionBytes.byteLength,
73
+ byteStride: 12,
74
+ count: 3,
75
+ mode: 'ATTRIBUTES',
76
+ },
77
+ },
78
+ },
79
+ {
80
+ buffer: 0,
81
+ byteLength: 0,
82
+ extensions: {
83
+ EXT_meshopt_compression: {
84
+ buffer: 0,
85
+ byteOffset: indexOffset,
86
+ byteLength: indexBytes.byteLength,
87
+ byteStride: 2,
88
+ count: 3,
89
+ mode: 'TRIANGLES',
90
+ },
91
+ },
92
+ },
93
+ ],
94
+ accessors: [
95
+ { bufferView: 0, componentType: 5126, count: 3, type: 'VEC3' },
96
+ { bufferView: 1, componentType: 5123, count: 3, type: 'SCALAR' },
97
+ ],
98
+ meshes: [
99
+ {
100
+ primitives: [{ attributes: { POSITION: 0 }, indices: 1 }],
101
+ },
102
+ ],
103
+ },
104
+ binary,
105
+ );
106
+ }
107
+
108
+ describe('real EXT_meshopt_compression glTF matrix', () => {
109
+ function decoder() {
110
+ return {
111
+ decode({
112
+ source,
113
+ count,
114
+ stride,
115
+ mode,
116
+ filter,
117
+ }: {
118
+ source: Uint8Array;
119
+ count: number;
120
+ stride: number;
121
+ mode: 'ATTRIBUTES' | 'TRIANGLES' | 'INDICES';
122
+ filter: 'NONE' | 'OCTAHEDRAL' | 'QUATERNION' | 'EXPONENTIAL';
123
+ }) {
124
+ const target = new Uint8Array(count * stride);
125
+ MeshoptDecoder.decodeGltfBuffer(target, count, stride, source, mode, filter);
126
+ return target;
127
+ },
128
+ };
129
+ }
130
+
131
+ it('decodes compressed ATTRIBUTES and TRIANGLES streams before bridge projection', async () => {
132
+ await MeshoptEncoder.ready;
133
+ await MeshoptDecoder.ready;
134
+ const result = await parseGlb(compressedGlb(), 'meshopt-real.glb', { meshopt: decoder() });
135
+
136
+ expect(result.ok).toBe(true);
137
+ if (!result.ok) return;
138
+ expect(result.value.meshes).toHaveLength(1);
139
+ expect(Array.from(result.value.meshes[0]?.positions ?? [])).toEqual([
140
+ 0, 0, 0, 1, 0, 0, 0, 1, 0,
141
+ ]);
142
+ expect(Array.from(result.value.meshes[0]?.indices ?? [])).toEqual([0, 1, 2]);
143
+
144
+ const mesh = meshIrToMeshAsset(result.value.meshes);
145
+ expect([
146
+ mesh.vertices[0],
147
+ mesh.vertices[1],
148
+ mesh.vertices[2],
149
+ mesh.vertices[12],
150
+ mesh.vertices[13],
151
+ mesh.vertices[14],
152
+ mesh.vertices[24],
153
+ mesh.vertices[25],
154
+ mesh.vertices[26],
155
+ ]).toEqual([0, 0, 0, 1, 0, 0, 0, 1, 0]);
156
+ expect(Array.from(mesh.indices ?? [])).toEqual([0, 1, 2]);
157
+ });
158
+
159
+ it('routes the same compressed source through the build-time importer capability', async () => {
160
+ await MeshoptEncoder.ready;
161
+ await MeshoptDecoder.ready;
162
+ const bytes = new Uint8Array(compressedGlb());
163
+ const context: ImportContext = {
164
+ source: 'meshopt-real.glb',
165
+ readSource: async () => ({ ok: true as const, value: bytes }),
166
+ readSibling: async () => {
167
+ throw new Error('the compressed fixture has no sibling buffers');
168
+ },
169
+ decodeImage: async () => {
170
+ throw new Error('the compressed fixture has no images');
171
+ },
172
+ subAssets: [
173
+ {
174
+ guid: '019f0000-0000-7000-8000-000000000201',
175
+ sourceIndex: 0,
176
+ kind: 'mesh',
177
+ },
178
+ ],
179
+ importSettings: {},
180
+ };
181
+ const result = await createGltfImporter(decoder()).import(context);
182
+
183
+ expect(result.ok).toBe(true);
184
+ if (!result.ok) return;
185
+ const mesh = result.value.assets.find((asset: { kind: string }) => asset.kind === 'mesh');
186
+ expect(mesh?.guid).toBe('019f0000-0000-7000-8000-000000000201');
187
+ expect(mesh?.payload.indices).toEqual(new Uint16Array([0, 1, 2]));
188
+ });
189
+
190
+ it('loads the imported compressed artifact through Pack, Catalog, and loadByGuid', async () => {
191
+ await MeshoptEncoder.ready;
192
+ await MeshoptDecoder.ready;
193
+ const guid = '019f0000-0000-7000-8000-000000000202';
194
+ const sourceBytes = new Uint8Array(compressedGlb());
195
+ const context: ImportContext = {
196
+ source: 'meshopt-pack.glb',
197
+ readSource: async () => ({ ok: true as const, value: sourceBytes }),
198
+ readSibling: async () => {
199
+ throw new Error('the compressed fixture has no sibling buffers');
200
+ },
201
+ decodeImage: async () => {
202
+ throw new Error('the compressed fixture has no images');
203
+ },
204
+ subAssets: [{ guid, sourceIndex: 0, kind: 'mesh' }],
205
+ importSettings: {},
206
+ };
207
+ const imported = await createGltfImporter(decoder()).import(context);
208
+ expect(imported.ok).toBe(true);
209
+ if (!imported.ok) return;
210
+ const mesh = imported.value.assets.find((asset: ImportedAsset) => asset.guid === guid);
211
+ expect(mesh?.kind).toBe('mesh');
212
+ if (mesh?.kind !== 'mesh') return;
213
+ const meshPayload = mesh.payload as Extract<Asset, { kind: 'mesh' }>;
214
+ const body = mesh.artifacts.body;
215
+ expect(body?.bytes.byteLength).toBeGreaterThan(0);
216
+ if (body === undefined) return;
217
+
218
+ const packIndexUrl = '/meshopt-pack-index.json';
219
+ const packageUrl = '/ddc/meshopt-pack.pack.json';
220
+ const originalFetch = globalThis.fetch;
221
+ const registry = new AssetRegistry({} as never);
222
+ registry.configurePackIndex(packIndexUrl);
223
+ globalThis.fetch = ((input: string) => {
224
+ const url = typeof input === 'string' ? input : String(input);
225
+ if (url === packIndexUrl) {
226
+ return Promise.resolve({
227
+ ok: true,
228
+ json: async () => [{ guid, packageUrl, kind: 'mesh' }],
229
+ });
230
+ }
231
+ if (url === packageUrl) {
232
+ const pack = {
233
+ schemaVersion: '2.0.0',
234
+ kind: 'internal-text-package',
235
+ assets: [
236
+ {
237
+ guid,
238
+ kind: 'mesh',
239
+ payload: meshPayload,
240
+ refs: [],
241
+ artifacts: {
242
+ body: {
243
+ path: 'mesh.bin',
244
+ mediaType: body.mediaType,
245
+ byteLength: body.bytes.byteLength,
246
+ assetCodec: body.assetCodec,
247
+ },
248
+ },
249
+ },
250
+ ],
251
+ };
252
+ return Promise.resolve({
253
+ ok: true,
254
+ json: async () => JSON.parse(JSON.stringify(pack)),
255
+ });
256
+ }
257
+ return Promise.resolve({
258
+ ok: true,
259
+ arrayBuffer: async () =>
260
+ body.bytes.buffer.slice(
261
+ body.bytes.byteOffset,
262
+ body.bytes.byteOffset + body.bytes.byteLength,
263
+ ),
264
+ });
265
+ }) as unknown as typeof globalThis.fetch;
266
+
267
+ try {
268
+ const loaded = await registry.loadByGuid(registry.parseGuid(guid));
269
+ expect(loaded.ok).toBe(true);
270
+ if (!loaded.ok) return;
271
+ const payload = loaded.value as { vertices: Float32Array; indices?: Uint16Array };
272
+ expect([
273
+ payload.vertices[0],
274
+ payload.vertices[1],
275
+ payload.vertices[2],
276
+ payload.vertices[12],
277
+ payload.vertices[13],
278
+ payload.vertices[14],
279
+ payload.vertices[24],
280
+ payload.vertices[25],
281
+ payload.vertices[26],
282
+ ]).toEqual([0, 0, 0, 1, 0, 0, 0, 1, 0]);
283
+ expect(Array.from(payload.indices ?? [])).toEqual([0, 1, 2]);
284
+ } finally {
285
+ globalThis.fetch = originalFetch;
286
+ }
287
+ });
288
+
289
+ it('rejects a damaged compressed artifact after importer success', async () => {
290
+ await MeshoptEncoder.ready;
291
+ await MeshoptDecoder.ready;
292
+ const guid = '019f0000-0000-7000-8000-000000000203';
293
+ const sourceBytes = new Uint8Array(compressedGlb());
294
+ const context: ImportContext = {
295
+ source: 'meshopt-damaged.glb',
296
+ readSource: async () => ({ ok: true as const, value: sourceBytes }),
297
+ readSibling: async () => {
298
+ throw new Error('the compressed fixture has no sibling buffers');
299
+ },
300
+ decodeImage: async () => {
301
+ throw new Error('the compressed fixture has no images');
302
+ },
303
+ subAssets: [{ guid, sourceIndex: 0, kind: 'mesh' }],
304
+ importSettings: {},
305
+ };
306
+ const imported = await createGltfImporter(decoder()).import(context);
307
+ expect(imported.ok).toBe(true);
308
+ if (!imported.ok) return;
309
+ const mesh = imported.value.assets.find((asset: ImportedAsset) => asset.guid === guid);
310
+ if (mesh?.kind !== 'mesh' || mesh.artifacts.body === undefined) return;
311
+ const meshPayload = mesh.payload as Extract<Asset, { kind: 'mesh' }>;
312
+ const damaged = new Uint8Array(mesh.artifacts.body.bytes);
313
+ const lastByte = damaged.length - 1;
314
+ damaged[lastByte] = (damaged[lastByte] ?? 0) ^ 0xff;
315
+ const registry = new AssetRegistry({} as never);
316
+ const packIndexUrl = '/meshopt-damaged-index.json';
317
+ const packageUrl = '/ddc/meshopt-damaged.pack.json';
318
+ registry.configurePackIndex(packIndexUrl);
319
+ const originalFetch = globalThis.fetch;
320
+ globalThis.fetch = ((input: string) => {
321
+ const url = typeof input === 'string' ? input : String(input);
322
+ if (url === packIndexUrl)
323
+ return Promise.resolve({
324
+ ok: true,
325
+ json: async () => [{ guid, packageUrl, kind: 'mesh' }],
326
+ });
327
+ if (url === packageUrl)
328
+ return Promise.resolve({
329
+ ok: true,
330
+ json: async () => ({
331
+ schemaVersion: '2.0.0',
332
+ kind: 'internal-text-package',
333
+ assets: [
334
+ {
335
+ guid,
336
+ kind: 'mesh',
337
+ payload: meshPayload,
338
+ refs: [],
339
+ artifacts: {
340
+ body: {
341
+ path: 'mesh.bin',
342
+ mediaType: mesh.artifacts.body?.mediaType,
343
+ byteLength: damaged.byteLength,
344
+ assetCodec: mesh.artifacts.body?.assetCodec,
345
+ },
346
+ },
347
+ },
348
+ ],
349
+ }),
350
+ });
351
+ return Promise.resolve({
352
+ ok: true,
353
+ arrayBuffer: async () =>
354
+ damaged.buffer.slice(damaged.byteOffset, damaged.byteOffset + damaged.byteLength),
355
+ });
356
+ }) as unknown as typeof globalThis.fetch;
357
+ try {
358
+ const loaded = await registry.loadByGuid(registry.parseGuid(guid));
359
+ expect(loaded.ok).toBe(false);
360
+ if (loaded.ok) return;
361
+ expect([
362
+ 'asset-parse-failed',
363
+ 'asset-artifact-integrity-mismatch',
364
+ 'mesh-bin-contract-violation',
365
+ ]).toContain(loaded.error.code);
366
+ } finally {
367
+ globalThis.fetch = originalFetch;
368
+ }
369
+ });
370
+ });
@@ -0,0 +1,115 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
3
+ import { ImporterRegistry, type RunImportMeta, runImport } from '@forgeax/engine-import';
4
+ import type { AnimationClip, MeshAsset, SceneAsset } from '@forgeax/engine-types';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { gltfImporter } from '../gltf-importer.js';
7
+
8
+ const MESH_GUID = '11111111-1111-4111-8111-111111111111';
9
+ const MATERIAL_GUID = '22222222-2222-4222-8222-222222222222';
10
+ const SCENE_GUID = '33333333-3333-4333-8333-333333333333';
11
+ const ANIMATION_GUID = '44444444-4444-4444-8444-444444444444';
12
+
13
+ const sourceUrl = new URL(
14
+ '../../../../apps/hello/format-tier1/fixtures/animated-morph-cube.gltf',
15
+ import.meta.url,
16
+ );
17
+
18
+ function meta(): RunImportMeta {
19
+ return {
20
+ importer: 'gltf',
21
+ source: 'apps/hello/format-tier1/fixtures/animated-morph-cube.gltf',
22
+ subAssets: [
23
+ { guid: MESH_GUID, sourceIndex: 0, sourceKey: 'gltf:mesh:Cube', kind: 'mesh' },
24
+ {
25
+ guid: MATERIAL_GUID,
26
+ sourceIndex: 0,
27
+ sourceKey: 'gltf:material:Material',
28
+ kind: 'material',
29
+ },
30
+ { guid: SCENE_GUID, sourceIndex: 0, sourceKey: 'gltf:scene:default', kind: 'scene' },
31
+ {
32
+ guid: ANIMATION_GUID,
33
+ sourceIndex: 0,
34
+ sourceKey: 'gltf:animation:Square',
35
+ kind: 'animation-clip',
36
+ },
37
+ ],
38
+ };
39
+ }
40
+
41
+ async function importFixture() {
42
+ const bytes = new Uint8Array(await readFile(sourceUrl));
43
+ const registry = new ImporterRegistry();
44
+ registry.register(gltfImporter);
45
+ const fs = { readSource: async () => ({ ok: true as const, value: bytes }) };
46
+ const result = await runImport(meta(), registry, fs);
47
+ if (!result.ok) throw new Error(`glTF import failed: ${JSON.stringify(result.error)}`);
48
+ if ('skipped' in result.value) throw new Error('expected glTF DDC pack');
49
+ return result.value.pack;
50
+ }
51
+
52
+ describe('real glTF morph import through Pack and AssetRegistry', () => {
53
+ it('preserves two targets and two animation keyframes through loadByGuid', async () => {
54
+ const pack = await importFixture();
55
+ const meshEntry = pack.assets.find((asset) => asset.guid === MESH_GUID);
56
+ const sceneEntry = pack.assets.find((asset) => asset.guid === SCENE_GUID);
57
+ const animationEntry = pack.assets.find((asset) => asset.guid === ANIMATION_GUID);
58
+ expect(meshEntry?.kind).toBe('mesh');
59
+ expect(sceneEntry?.kind).toBe('scene');
60
+ expect(animationEntry?.kind).toBe('animation-clip');
61
+
62
+ const meshPayload = meshEntry?.payload as {
63
+ readonly vertices: number[];
64
+ readonly morphTargets?: readonly { readonly position?: number[] }[];
65
+ readonly morphWeights?: number[];
66
+ };
67
+ expect(meshPayload.morphTargets).toHaveLength(2);
68
+ expect(meshPayload.morphTargets?.every((target) => target.position?.length === 24 * 3)).toBe(
69
+ true,
70
+ );
71
+ expect(meshPayload.morphTargets?.[0]?.position?.some((value) => value !== 0)).toBe(true);
72
+ expect(meshPayload.morphTargets?.[1]?.position?.some((value) => value !== 0)).toBe(true);
73
+ expect(meshPayload.morphWeights).toEqual([0, 0]);
74
+
75
+ const animationPayload = animationEntry?.payload as {
76
+ readonly channels: readonly {
77
+ readonly property: string;
78
+ readonly sampler: { readonly input: readonly number[]; readonly output: readonly number[] };
79
+ }[];
80
+ };
81
+ expect(animationPayload.channels).toHaveLength(1);
82
+ expect(animationPayload.channels[0]?.property).toBe('weights');
83
+ expect(animationPayload.channels[0]?.sampler.input).toEqual([0, 1]);
84
+ expect(animationPayload.channels[0]?.sampler.output).toEqual([0, 0.75, 0.25, 0]);
85
+
86
+ const runtime = new AssetRegistry({} as never);
87
+ const parsedMesh = runtime.parseAssetPayload('mesh', meshEntry?.payload ?? {});
88
+ const parsedAnimation = runtime.parseAssetPayload(
89
+ 'animation-clip',
90
+ animationEntry?.payload ?? {},
91
+ );
92
+ expect(parsedMesh).toMatchObject({ kind: 'mesh', morphWeights: new Float32Array([0, 0]) });
93
+ expect(parsedAnimation).toMatchObject({ kind: 'animation-clip' });
94
+ expect(runtime.catalog(MESH_GUID, parsedMesh as never).ok).toBe(true);
95
+ expect(runtime.catalog(SCENE_GUID, sceneEntry?.payload as never).ok).toBe(true);
96
+ expect(runtime.catalog(ANIMATION_GUID, parsedAnimation as never).ok).toBe(true);
97
+
98
+ const loadedMesh = await runtime.loadByGuid<MeshAsset>(runtime.parseGuid(MESH_GUID));
99
+ const loadedScene = await runtime.loadByGuid<SceneAsset>(runtime.parseGuid(SCENE_GUID));
100
+ const loadedAnimation = await runtime.loadByGuid<AnimationClip>(
101
+ runtime.parseGuid(ANIMATION_GUID),
102
+ );
103
+ expect(loadedMesh.ok).toBe(true);
104
+ expect(loadedScene.ok).toBe(true);
105
+ expect(loadedAnimation.ok).toBe(true);
106
+ if (!loadedMesh.ok || !loadedScene.ok || !loadedAnimation.ok) return;
107
+ expect(Array.from(loadedMesh.value.morphWeights ?? [])).toEqual([0, 0]);
108
+ const sceneEntity = loadedScene.value.entities[0];
109
+ expect(sceneEntity?.components.MorphWeights?.weights).toEqual([0, 0]);
110
+ expect(loadedAnimation.value.channels[0]?.property).toBe('weights');
111
+ expect(Array.from(loadedAnimation.value.channels[0]?.sampler.output ?? [])).toEqual([
112
+ 0, 0.75, 0.25, 0,
113
+ ]);
114
+ });
115
+ });
@@ -0,0 +1,62 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { parseGltf } from '../parse-gltf.js';
4
+ import { unwrapMeshAsset as meshIrToMeshAsset } from './bridge-test-helpers.js';
5
+
6
+ function documentWithMorph(targetAccessor: number | object = 1) {
7
+ const values = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1.25, 0, 0, 0, 1, 0]);
8
+ const uri = `data:application/octet-stream;base64,${Buffer.from(values.buffer).toString('base64')}`;
9
+ return {
10
+ asset: { version: '2.0' },
11
+ buffers: [{ uri, byteLength: values.byteLength }],
12
+ bufferViews: [
13
+ { buffer: 0, byteOffset: 0, byteLength: 36 },
14
+ { buffer: 0, byteOffset: 36, byteLength: 36 },
15
+ ],
16
+ accessors: [
17
+ { bufferView: 0, componentType: 5126, count: 3, type: 'VEC3' },
18
+ { bufferView: 1, componentType: 5126, count: 3, type: 'VEC3' },
19
+ ],
20
+ meshes: [
21
+ {
22
+ weights: [0.25],
23
+ primitives: [{ attributes: { POSITION: 0 }, targets: [{ POSITION: targetAccessor }] }],
24
+ },
25
+ ],
26
+ nodes: [{ mesh: 0 }],
27
+ scenes: [{ nodes: [0] }],
28
+ scene: 0,
29
+ };
30
+ }
31
+
32
+ describe('glTF morph target import', () => {
33
+ it('projects additive POSITION deltas and default weights into MeshAsset', async () => {
34
+ const parsed = await parseGltf(
35
+ documentWithMorph(),
36
+ async () => new ArrayBuffer(0),
37
+ 'morph.gltf',
38
+ );
39
+ expect(parsed.ok).toBe(true);
40
+ if (!parsed.ok) return;
41
+ const first = parsed.value.meshes[0];
42
+ expect(first).toBeDefined();
43
+ if (first === undefined) return;
44
+ const mesh = meshIrToMeshAsset([first]);
45
+ const morphMesh = mesh as typeof mesh & {
46
+ readonly morphTargets?: readonly { readonly position?: Float32Array }[];
47
+ readonly morphWeights?: Float32Array;
48
+ };
49
+ expect(morphMesh.morphTargets?.[0]?.position?.[3]).toBeCloseTo(1.25);
50
+ expect(Array.from(morphMesh.morphWeights ?? [])).toEqual([0.25]);
51
+ });
52
+
53
+ it('returns a structured morph error for a missing target accessor', async () => {
54
+ const parsed = await parseGltf(
55
+ documentWithMorph(99),
56
+ async () => new ArrayBuffer(0),
57
+ 'morph.gltf',
58
+ );
59
+ expect(parsed.ok).toBe(false);
60
+ if (!parsed.ok) expect(parsed.error.code).toBe('gltf-morph-invalid');
61
+ });
62
+ });