@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,1227 @@
1
+ // Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
2
+ // biome-ignore-all lint/complexity/noUselessLoneBlockStatements: scope isolation between merged source files
3
+ //
4
+ // Source files (N=6):
5
+ // - packages/gltf/src/__tests__/bridge-name-instances.test.ts
6
+ // - packages/gltf/src/__tests__/bridge-recursive.test.ts
7
+ // - packages/gltf/src/__tests__/bridge-register-integration.test.ts
8
+ // - packages/gltf/src/__tests__/bridge-snapshot.test.ts
9
+ // - packages/gltf/src/__tests__/bridge-multi-prim.test.ts
10
+ // - packages/gltf/src/__tests__/bridge-recurse-and-transform.test.ts
11
+ //
12
+ // Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
13
+ // source as ancestorTitles[0]. Top-level imports merged + deduped.
14
+
15
+ import type { Handle, SceneAsset } from '@forgeax/engine-types';
16
+ import { describe, expect, it } from 'vitest';
17
+ import { gltfDocToSceneAsset, toMaterialAsset } from '../bridge.js';
18
+ import type { GltfDoc, GltfMaterialIr, GltfMeshIr } from '../parse-gltf.js';
19
+ import { unwrapMeshAsset as meshIrToMeshAsset } from './bridge-test-helpers.js';
20
+
21
+ function fakeMeshHandle(id: number): Handle<'MeshAsset', 'shared'> {
22
+ return id as unknown as Handle<'MeshAsset', 'shared'>;
23
+ }
24
+
25
+ {
26
+ describe('KHR_lights_punctual bridge contract', () => {
27
+ it('projects imported light facts to existing light kinds without a peer component', () => {
28
+ const doc = {
29
+ meshes: [],
30
+ materials: [],
31
+ nodes: [],
32
+ scenes: [{ nodes: [] }],
33
+ textures: undefined,
34
+ images: undefined,
35
+ samplers: undefined,
36
+ skeletons: [],
37
+ animationClips: [],
38
+ defaultSceneIndex: 0,
39
+ diagnostics: {
40
+ nodeNames: [],
41
+ unsupportedExtensions: [],
42
+ matrixTrsCoexistNodes: [],
43
+ },
44
+ extensions: {
45
+ KHR_lights_punctual: {
46
+ lights: [
47
+ { type: 'directional', intensity: 2 },
48
+ { type: 'point', intensity: 3, range: 4 },
49
+ {
50
+ type: 'spot',
51
+ intensity: 5,
52
+ range: 6,
53
+ spot: { innerConeAngle: 0.2, outerConeAngle: 0.6 },
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ } as unknown as GltfDoc;
59
+
60
+ const scene = gltfDocToSceneAsset(doc, {
61
+ meshHandles: new Map(),
62
+ materialHandles: new Map(),
63
+ });
64
+ const lights = (scene as unknown as { lights?: readonly Record<string, unknown>[] }).lights;
65
+ expect(lights?.map((light) => light.kind)).toEqual(['directional', 'point', 'spot']);
66
+ expect(lights?.map((light) => light.intensity)).toEqual([2, 3, 5]);
67
+ expect(lights?.[1]?.range).toBe(4);
68
+ });
69
+ });
70
+ }
71
+
72
+ function fakeMaterialHandle(id: number): Handle<'MaterialAsset', 'shared'> {
73
+ return id as unknown as Handle<'MaterialAsset', 'shared'>;
74
+ }
75
+
76
+ const FAKE_TEXTURE_HANDLE = 1 as unknown as Handle<'TextureAsset', 'shared'>;
77
+ const FAKE_SAMPLER_HANDLE = 2 as unknown as Handle<'SamplerAsset', 'shared'>;
78
+
79
+ const STANDARD_MATERIAL: GltfMaterialIr = {
80
+ name: 'TestMat',
81
+ baseColorFactor: [0.8, 0.2, 0.1, 1.0],
82
+ metallicFactor: 0.5,
83
+ roughnessFactor: 0.7,
84
+ baseColorTexture: { texture: 0 },
85
+ };
86
+
87
+ {
88
+ // ─── from bridge-name-instances.test.ts ───
89
+
90
+ const baseDoc = (): GltfDoc => ({
91
+ meshes: [
92
+ {
93
+ name: 'MeshFoo',
94
+ positions: new Float32Array([0, 0, 0]),
95
+ indices: new Uint16Array([0]),
96
+ materialIndex: 0,
97
+ meshIndex: 0,
98
+ },
99
+ ],
100
+ materials: [
101
+ {
102
+ name: 'MatFoo',
103
+ baseColorFactor: [1, 1, 1, 1],
104
+ metallicFactor: 1.0,
105
+ roughnessFactor: 1.0,
106
+ },
107
+ ],
108
+ nodes: [
109
+ {
110
+ name: 'InstancedBox',
111
+ transform: { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
112
+ meshIndex: 0,
113
+ skinIndex: null,
114
+ camera: null,
115
+ children: [],
116
+ instancing: { count: 4, transforms: new Float32Array(4 * 16) },
117
+ },
118
+ ],
119
+ scenes: [{ name: 'SceneFoo', nodes: [0] }],
120
+ textures: undefined,
121
+ images: undefined,
122
+ samplers: undefined,
123
+ skeletons: [],
124
+ animationClips: [],
125
+ defaultSceneIndex: 0,
126
+ diagnostics: {
127
+ nodeNames: ['InstancedBox'],
128
+ unsupportedExtensions: [],
129
+ matrixTrsCoexistNodes: [],
130
+ },
131
+ });
132
+
133
+ const ctx = {
134
+ meshHandles: new Map([[0, fakeMeshHandle(1)]]),
135
+ materialHandles: new Map([[0, fakeMaterialHandle(7)]]),
136
+ };
137
+
138
+ describe('bridge-name-instances.test.ts', () => {
139
+ describe('bridge.ts Name attachment (M4 w11/w12)', () => {
140
+ it('AC-06 attaches Name component when node.name is non-empty', () => {
141
+ const scene = gltfDocToSceneAsset(baseDoc(), ctx);
142
+ const node = scene.entities[0];
143
+ expect(node?.components.Name).toEqual({ value: 'InstancedBox' });
144
+ });
145
+
146
+ it('AC-07 omits Name when node.name is undefined', () => {
147
+ const doc: GltfDoc = {
148
+ ...baseDoc(),
149
+ nodes: [{ ...baseDoc().nodes[0], name: undefined } as never],
150
+ };
151
+ const scene = gltfDocToSceneAsset(doc, ctx);
152
+ const node = scene.entities[0];
153
+ expect(node?.components.Name).toBeUndefined();
154
+ });
155
+
156
+ it('AC-07 omits Name when node.name is empty string', () => {
157
+ const doc: GltfDoc = {
158
+ ...baseDoc(),
159
+ nodes: [{ ...baseDoc().nodes[0], name: '' } as never],
160
+ };
161
+ const scene = gltfDocToSceneAsset(doc, ctx);
162
+ const node = scene.entities[0];
163
+ expect(node?.components.Name).toBeUndefined();
164
+ });
165
+
166
+ it('AC-08 mesh / material / scene names do NOT route into Name on the node', () => {
167
+ const scene = gltfDocToSceneAsset(baseDoc(), ctx);
168
+ for (const node of scene.entities) {
169
+ const name = node.components.Name;
170
+ if (name !== undefined) {
171
+ expect(name.value).not.toBe('MeshFoo');
172
+ expect(name.value).not.toBe('MatFoo');
173
+ expect(name.value).not.toBe('SceneFoo');
174
+ }
175
+ }
176
+ });
177
+ });
178
+
179
+ describe('bridge.ts Instances attachment (M4 w11/w12)', () => {
180
+ it('AC-10 attaches Instances with transforms.length === N*16', () => {
181
+ const scene = gltfDocToSceneAsset(baseDoc(), ctx);
182
+ const node = scene.entities[0];
183
+ expect(node?.components.Instances).toBeDefined();
184
+ const inst = node?.components.Instances;
185
+ if (inst !== undefined) {
186
+ const transforms = inst.transforms as Float32Array;
187
+ expect(transforms.length).toBe(4 * 16);
188
+ }
189
+ });
190
+
191
+ it('AC-10 transforms identity is byte-equal to importer composed buffer', () => {
192
+ const seed = new Float32Array(4 * 16);
193
+ for (let i = 0; i < seed.length; i++) seed[i] = i;
194
+ const doc: GltfDoc = {
195
+ ...baseDoc(),
196
+ nodes: [
197
+ {
198
+ ...baseDoc().nodes[0],
199
+ instancing: { count: 4, transforms: seed },
200
+ } as never,
201
+ ],
202
+ };
203
+ const scene = gltfDocToSceneAsset(doc, ctx);
204
+ const transforms = scene.entities[0]?.components.Instances?.transforms as Float32Array;
205
+ for (let i = 0; i < seed.length; i++) {
206
+ expect(transforms[i]).toBe(seed[i]);
207
+ }
208
+ });
209
+
210
+ it('AC-11 1 entity carries MeshFilter+MeshRenderer+Instances (no flatten)', () => {
211
+ const scene = gltfDocToSceneAsset(baseDoc(), ctx);
212
+ const candidates = scene.entities.filter(
213
+ (n) =>
214
+ n.components.MeshFilter !== undefined &&
215
+ n.components.MeshRenderer !== undefined &&
216
+ n.components.Instances !== undefined,
217
+ );
218
+ expect(candidates.length).toBe(1);
219
+ });
220
+
221
+ it('omits Instances when node has no instancing IR', () => {
222
+ const doc: GltfDoc = {
223
+ ...baseDoc(),
224
+ nodes: [{ ...baseDoc().nodes[0], instancing: undefined } as never],
225
+ };
226
+ const scene = gltfDocToSceneAsset(doc, ctx);
227
+ expect(scene.entities[0]?.components.Instances).toBeUndefined();
228
+ });
229
+ });
230
+ });
231
+ }
232
+
233
+ {
234
+ // ─── from bridge-recursive.test.ts ───
235
+
236
+ function node(
237
+ overrides: Partial<{
238
+ meshIndex: number | null;
239
+ children: number[];
240
+ name: string;
241
+ }> = {},
242
+ ): GltfDoc['nodes'][number] {
243
+ return {
244
+ transform: { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
245
+ meshIndex: overrides.meshIndex ?? null,
246
+ skinIndex: null,
247
+ camera: null,
248
+ children: overrides.children ?? [],
249
+ ...(overrides.name !== undefined ? { name: overrides.name } : {}),
250
+ };
251
+ }
252
+
253
+ const twoLevelDoc: GltfDoc = {
254
+ meshes: [
255
+ {
256
+ name: 'M0',
257
+ positions: new Float32Array([0, 0, 0]),
258
+ indices: new Uint16Array([0]),
259
+ materialIndex: 0,
260
+ meshIndex: 0,
261
+ },
262
+ {
263
+ name: 'M1',
264
+ positions: new Float32Array([1, 1, 1]),
265
+ indices: new Uint16Array([0]),
266
+ materialIndex: 0,
267
+ meshIndex: 1,
268
+ },
269
+ ],
270
+ materials: [{ baseColorFactor: [1, 1, 1, 1], metallicFactor: 1, roughnessFactor: 1 }],
271
+ nodes: [
272
+ node({ meshIndex: 0, children: [1], name: 'root' }),
273
+ node({ meshIndex: 1, children: [], name: 'child' }),
274
+ ],
275
+ scenes: [{ name: 'S', nodes: [0] }],
276
+ textures: undefined,
277
+ images: undefined,
278
+ samplers: undefined,
279
+ skeletons: [],
280
+ animationClips: [],
281
+ defaultSceneIndex: 0,
282
+ diagnostics: {
283
+ nodeNames: ['root', 'child'],
284
+ unsupportedExtensions: [],
285
+ matrixTrsCoexistNodes: [],
286
+ },
287
+ };
288
+
289
+ const threeLevelDoc: GltfDoc = {
290
+ meshes: [
291
+ {
292
+ name: 'M0',
293
+ positions: new Float32Array([0, 0, 0]),
294
+ indices: new Uint16Array([0]),
295
+ materialIndex: 0,
296
+ meshIndex: 0,
297
+ },
298
+ {
299
+ name: 'M1',
300
+ positions: new Float32Array([1, 1, 1]),
301
+ indices: new Uint16Array([0]),
302
+ materialIndex: 0,
303
+ meshIndex: 1,
304
+ },
305
+ {
306
+ name: 'M2',
307
+ positions: new Float32Array([2, 2, 2]),
308
+ indices: new Uint16Array([0]),
309
+ materialIndex: 0,
310
+ meshIndex: 2,
311
+ },
312
+ ],
313
+ materials: [{ baseColorFactor: [1, 1, 1, 1], metallicFactor: 1, roughnessFactor: 1 }],
314
+ nodes: [
315
+ node({ meshIndex: 0, children: [1], name: 'root' }),
316
+ node({ meshIndex: 1, children: [2], name: 'middle' }),
317
+ node({ meshIndex: 2, children: [], name: 'grandchild' }),
318
+ ],
319
+ scenes: [{ name: 'S', nodes: [0] }],
320
+ textures: undefined,
321
+ images: undefined,
322
+ samplers: undefined,
323
+ skeletons: [],
324
+ animationClips: [],
325
+ defaultSceneIndex: 0,
326
+ diagnostics: {
327
+ nodeNames: ['root', 'middle', 'grandchild'],
328
+ unsupportedExtensions: [],
329
+ matrixTrsCoexistNodes: [],
330
+ },
331
+ };
332
+
333
+ describe('bridge-recursive.test.ts', () => {
334
+ describe('gltfDocToSceneAsset DFS recursion (w24)', () => {
335
+ it('visits child nodes (two-level hierarchy)', () => {
336
+ const ctx = {
337
+ meshHandles: new Map([
338
+ [0, fakeMeshHandle(10)],
339
+ [1, fakeMeshHandle(11)],
340
+ ]),
341
+ materialHandles: new Map([[0, fakeMaterialHandle(20)]]),
342
+ };
343
+ const scene: SceneAsset = gltfDocToSceneAsset(twoLevelDoc, ctx);
344
+ expect(scene.entities.length).toBe(2);
345
+ expect(scene.entities[0]?.components.MeshFilter?.assetHandle).toBe(10);
346
+ expect(scene.entities[1]?.components.MeshFilter?.assetHandle).toBe(11);
347
+ expect(scene.entities[0]?.localId).toBe(0);
348
+ expect(scene.entities[1]?.localId).toBe(1);
349
+ });
350
+
351
+ it('visits full three-level hierarchy in DFS order', () => {
352
+ const ctx = {
353
+ meshHandles: new Map([
354
+ [0, fakeMeshHandle(10)],
355
+ [1, fakeMeshHandle(11)],
356
+ [2, fakeMeshHandle(12)],
357
+ ]),
358
+ materialHandles: new Map([[0, fakeMaterialHandle(20)]]),
359
+ };
360
+ const scene: SceneAsset = gltfDocToSceneAsset(threeLevelDoc, ctx);
361
+ expect(scene.entities.length).toBe(3);
362
+ expect(scene.entities[0]?.localId).toBe(0);
363
+ expect(scene.entities[1]?.localId).toBe(1);
364
+ expect(scene.entities[2]?.localId).toBe(2);
365
+ });
366
+
367
+ it('includes child entities with Name components from node.name', () => {
368
+ const ctx = {
369
+ meshHandles: new Map([
370
+ [0, fakeMeshHandle(10)],
371
+ [1, fakeMeshHandle(11)],
372
+ ]),
373
+ materialHandles: new Map([[0, fakeMaterialHandle(20)]]),
374
+ };
375
+ const scene: SceneAsset = gltfDocToSceneAsset(twoLevelDoc, ctx);
376
+ expect(scene.entities[0]?.components.Name?.value).toBe('root');
377
+ expect(scene.entities[1]?.components.Name?.value).toBe('child');
378
+ });
379
+ });
380
+ });
381
+ }
382
+
383
+ {
384
+ // ─── from bridge-register-integration.test.ts ───
385
+
386
+ describe('bridge-register-integration.test.ts', () => {
387
+ describe('toMaterialAsset -> registerMaterialAsset integration (pass-based)', () => {
388
+ it('PBR material with full ctx produces pass-based MaterialAsset', () => {
389
+ const textureHandles = new Map([[0, FAKE_TEXTURE_HANDLE]]);
390
+ const samplerHandles = new Map([[0, FAKE_SAMPLER_HANDLE]]);
391
+
392
+ const asset = toMaterialAsset(STANDARD_MATERIAL, {
393
+ textureHandles,
394
+ samplerHandles,
395
+ });
396
+
397
+ expect(asset.kind).toBe('material');
398
+ expect(asset.passes).toBeDefined();
399
+ expect(asset.passes?.[0]?.program.module).toBe('forgeax::default-standard-pbr');
400
+ expect(asset.passes?.[0]?.name).toBe('Forward');
401
+ expect(asset.values?.baseColorTexture).toMatchObject({ texture: FAKE_TEXTURE_HANDLE });
402
+ expect(asset.values?.metallic).toBe(0.5);
403
+ expect(asset.values?.roughness).toBe(0.7);
404
+ });
405
+
406
+ it('maps glTF emissiveFactor and emissiveTexture', () => {
407
+ const mat: GltfMaterialIr = {
408
+ name: 'EmissiveMat',
409
+ baseColorFactor: [1, 1, 1, 1],
410
+ metallicFactor: 0,
411
+ roughnessFactor: 0.5,
412
+ emissiveFactor: [0.8, 0.4, 0.1],
413
+ emissiveTexture: { texture: 1 },
414
+ };
415
+ const asset = toMaterialAsset(mat, {
416
+ textureHandles: new Map([[1, FAKE_TEXTURE_HANDLE]]),
417
+ });
418
+
419
+ expect(asset.values?.emissive).toEqual([0.8, 0.4, 0.1]);
420
+ expect(asset.values?.emissiveIntensity).toBe(1);
421
+ expect(asset.values?.emissiveTexture).toMatchObject({ texture: FAKE_TEXTURE_HANDLE });
422
+ });
423
+
424
+ it('standard material without textures omits texture values', () => {
425
+ const { baseColorTexture: _, ...rest } = STANDARD_MATERIAL;
426
+ const mat: GltfMaterialIr = rest;
427
+ const samplerHandles = new Map([[0, FAKE_SAMPLER_HANDLE]]);
428
+
429
+ const asset = toMaterialAsset(mat, {
430
+ textureHandles: new Map(),
431
+ samplerHandles,
432
+ });
433
+
434
+ expect(asset.values?.baseColorTexture).toBeUndefined();
435
+ expect(asset.values?.sampler).toBeUndefined();
436
+ });
437
+
438
+ it('PBR material exposes a standard-root program and values', () => {
439
+ const asset = toMaterialAsset(STANDARD_MATERIAL, {
440
+ textureHandles: new Map(),
441
+ samplerHandles: new Map([[0, FAKE_SAMPLER_HANDLE]]),
442
+ });
443
+ expect(asset.passes?.[0]?.program.module).toBe('forgeax::default-standard-pbr');
444
+ expect(asset.values?.baseColor).toEqual([0.8, 0.2, 0.1, 1]);
445
+ });
446
+
447
+ it('PBR material preserves RGBA baseColor, including blend opacity', () => {
448
+ const samplerHandles = new Map([[0, FAKE_SAMPLER_HANDLE]]);
449
+ const asset = toMaterialAsset(STANDARD_MATERIAL, {
450
+ textureHandles: new Map(),
451
+ samplerHandles,
452
+ });
453
+
454
+ const baseColor = asset.values?.baseColor as number[];
455
+ expect(baseColor).toHaveLength(4);
456
+ expect(baseColor).toEqual([0.8, 0.2, 0.1, 1]);
457
+ });
458
+
459
+ it('BLEND material preserves baseColor alpha for the standard-PBR UBO overlay', () => {
460
+ const mat: GltfMaterialIr = {
461
+ ...STANDARD_MATERIAL,
462
+ baseColorFactor: [0.041667, 0.041667, 0.041667, 0.5],
463
+ alphaMode: 'BLEND',
464
+ };
465
+ const asset = toMaterialAsset(mat, {
466
+ textureHandles: new Map(),
467
+ samplerHandles: new Map(),
468
+ });
469
+
470
+ expect(asset.values?.baseColor).toEqual([0.041667, 0.041667, 0.041667, 0.5]);
471
+ });
472
+
473
+ it('omits coordinates when texCoord is absent', () => {
474
+ const asset = toMaterialAsset(STANDARD_MATERIAL, {
475
+ textureHandles: new Map(),
476
+ samplerHandles: new Map(),
477
+ });
478
+ expect(asset.values?.baseColorTexture).toBeUndefined();
479
+ });
480
+
481
+ it('keeps texCoord on the individual texture slot', () => {
482
+ const mat: GltfMaterialIr = {
483
+ ...STANDARD_MATERIAL,
484
+ baseColorTexture: { texture: 0, texCoord: 0 },
485
+ };
486
+ const asset = toMaterialAsset(mat, {
487
+ textureHandles: new Map([[0, FAKE_TEXTURE_HANDLE]]),
488
+ samplerHandles: new Map(),
489
+ });
490
+ expect(asset.values?.baseColorTexture).toMatchObject({ coordinates: { set: 0 } });
491
+ });
492
+
493
+ it('keeps a nonzero texCoord on the individual texture slot', () => {
494
+ const mat: GltfMaterialIr = {
495
+ ...STANDARD_MATERIAL,
496
+ baseColorTexture: { texture: 0, texCoord: 1 },
497
+ };
498
+ const asset = toMaterialAsset(mat, {
499
+ textureHandles: new Map([[0, FAKE_TEXTURE_HANDLE]]),
500
+ samplerHandles: new Map(),
501
+ });
502
+ expect(asset.values?.baseColorTexture).toMatchObject({ coordinates: { set: 1 } });
503
+ });
504
+
505
+ it('OPAQUE material has no renderState.blend', () => {
506
+ const asset = toMaterialAsset(STANDARD_MATERIAL, {
507
+ textureHandles: new Map(),
508
+ samplerHandles: new Map(),
509
+ });
510
+ const pass = asset.passes?.[0];
511
+ expect(pass?.renderState?.blend).toBeUndefined();
512
+ });
513
+
514
+ it('BLEND material sets renderState.blend and depthWriteEnabled=false', () => {
515
+ const mat: GltfMaterialIr = { ...STANDARD_MATERIAL, alphaMode: 'BLEND' };
516
+ const asset = toMaterialAsset(mat, {
517
+ textureHandles: new Map(),
518
+ samplerHandles: new Map(),
519
+ });
520
+ const pass = asset.passes?.[0];
521
+ expect(pass?.renderState?.blend).toBeDefined();
522
+ expect(pass?.renderState?.depthWriteEnabled).toBe(false);
523
+ });
524
+
525
+ it('double-sided material disables face culling without changing its queue', () => {
526
+ const mat: GltfMaterialIr = { ...STANDARD_MATERIAL, doubleSided: true };
527
+ const asset = toMaterialAsset(mat, {
528
+ textureHandles: new Map(),
529
+ samplerHandles: new Map(),
530
+ });
531
+ const pass = asset.passes?.[0];
532
+ expect(pass?.renderState?.queue).toBe(2000);
533
+ expect(pass?.renderState?.cullMode).toBe('none');
534
+ expect(pass?.renderState?.blend).toBeUndefined();
535
+ });
536
+
537
+ it('double-sided BLEND material keeps both blend and no-cull state', () => {
538
+ const mat: GltfMaterialIr = {
539
+ ...STANDARD_MATERIAL,
540
+ alphaMode: 'BLEND',
541
+ doubleSided: true,
542
+ };
543
+ const asset = toMaterialAsset(mat, {
544
+ textureHandles: new Map(),
545
+ samplerHandles: new Map(),
546
+ });
547
+ const pass = asset.passes?.[0];
548
+ expect(pass?.renderState?.blend).toBeDefined();
549
+ expect(pass?.renderState?.depthWriteEnabled).toBe(false);
550
+ expect(pass?.renderState?.cullMode).toBe('none');
551
+ });
552
+ });
553
+ });
554
+ }
555
+
556
+ {
557
+ // ─── from bridge-snapshot.test.ts ───
558
+
559
+ const helloGltfDoc: GltfDoc = {
560
+ meshes: [
561
+ {
562
+ name: 'Box',
563
+ positions: new Float32Array([0, 0, 0]),
564
+ indices: new Uint16Array([0]),
565
+ materialIndex: 0,
566
+ meshIndex: 0,
567
+ },
568
+ ],
569
+ materials: [
570
+ {
571
+ name: 'BoxMaterial',
572
+ baseColorFactor: [1, 1, 1, 1],
573
+ metallicFactor: 1.0,
574
+ roughnessFactor: 1.0,
575
+ },
576
+ ],
577
+ nodes: [
578
+ {
579
+ name: 'BoxNode',
580
+ transform: {
581
+ translation: [0, 0, 0],
582
+ rotation: [0, 0, 0, 1],
583
+ scale: [1, 1, 1],
584
+ },
585
+ meshIndex: 0,
586
+ skinIndex: null,
587
+ camera: null,
588
+ children: [],
589
+ },
590
+ {
591
+ name: 'CameraNode',
592
+ transform: {
593
+ translation: [0, 0, 3],
594
+ rotation: [0, 0, 0, 1],
595
+ scale: [1, 1, 1],
596
+ },
597
+ meshIndex: null,
598
+ skinIndex: null,
599
+ camera: 0,
600
+ children: [],
601
+ },
602
+ ],
603
+ scenes: [{ name: 'Scene', nodes: [0, 1] }],
604
+ textures: undefined,
605
+ images: undefined,
606
+ samplers: undefined,
607
+ skeletons: [],
608
+ animationClips: [],
609
+ defaultSceneIndex: 0,
610
+ diagnostics: {
611
+ nodeNames: ['BoxNode', 'CameraNode'],
612
+ unsupportedExtensions: [],
613
+ matrixTrsCoexistNodes: [],
614
+ },
615
+ };
616
+
617
+ describe('bridge-snapshot.test.ts', () => {
618
+ describe('gltfDocToSceneAsset (M3 w8/w9)', () => {
619
+ it('matches the legacy hello-gltf inline-impl POD output', () => {
620
+ const ctx = {
621
+ meshHandles: new Map([[0, fakeMeshHandle(1)]]),
622
+ materialHandles: new Map([[0, fakeMaterialHandle(7)]]),
623
+ };
624
+ const scene: SceneAsset = gltfDocToSceneAsset(helloGltfDoc, ctx);
625
+ expect(scene.kind).toBe('scene');
626
+ expect(scene.entities.length).toBeGreaterThanOrEqual(1);
627
+ const meshNode = scene.entities[0];
628
+ expect(meshNode?.components.MeshFilter?.assetHandle).toBe(1);
629
+ const renderer = meshNode?.components.MeshRenderer;
630
+ expect(renderer).toBeDefined();
631
+ if (renderer !== undefined) {
632
+ const materials = renderer.materials as unknown[];
633
+ expect(Array.isArray(materials)).toBe(true);
634
+ expect(materials).toEqual([]);
635
+ }
636
+ expect(meshNode?.components.Transform).toBeDefined();
637
+ });
638
+
639
+ it('attaches Camera component when a node references a camera (via GltfNodeIr.camera)', () => {
640
+ const ctx = {
641
+ meshHandles: new Map([[0, fakeMeshHandle(1)]]),
642
+ materialHandles: new Map([[0, fakeMaterialHandle(7)]]),
643
+ };
644
+ const scene = gltfDocToSceneAsset(helloGltfDoc, ctx);
645
+ const cameraNode = scene.entities.find((n) => n.components.Camera !== undefined);
646
+ expect(cameraNode).toBeDefined();
647
+ });
648
+ });
649
+ });
650
+ }
651
+
652
+ {
653
+ // ─── from bridge-multi-prim.test.ts ───
654
+
655
+ describe('bridge-multi-prim.test.ts', () => {
656
+ describe('bridge.ts B1 multi-primitive merge (M3 w14)', () => {
657
+ it('2-prim glTF mesh produces 1 entity (meshHandles keyed by glTF mesh index)', () => {
658
+ const mesh0Prim0: GltfMeshIr = {
659
+ positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
660
+ indices: new Uint16Array([0, 1, 2]),
661
+ materialIndex: 0,
662
+ meshIndex: 0,
663
+ };
664
+ const mesh0Prim1: GltfMeshIr = {
665
+ positions: new Float32Array([0, 0, 0, -1, 0, 0, 0, -1, 0]),
666
+ indices: new Uint16Array([0, 1, 2]),
667
+ materialIndex: 1,
668
+ meshIndex: 0,
669
+ };
670
+ const doc: GltfDoc = {
671
+ meshes: [mesh0Prim0, mesh0Prim1],
672
+ materials: [
673
+ { baseColorFactor: [1, 0, 0, 1], metallicFactor: 1, roughnessFactor: 1 },
674
+ { baseColorFactor: [0, 0, 1, 1], metallicFactor: 1, roughnessFactor: 1 },
675
+ ],
676
+ nodes: [
677
+ {
678
+ name: 'MeshNode',
679
+ transform: { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
680
+ meshIndex: 0,
681
+ skinIndex: null,
682
+ camera: null,
683
+ children: [],
684
+ },
685
+ ],
686
+ scenes: [{ name: 'Scene', nodes: [0] }],
687
+ textures: undefined,
688
+ images: undefined,
689
+ samplers: undefined,
690
+ skeletons: [],
691
+ animationClips: [],
692
+ defaultSceneIndex: 0,
693
+ diagnostics: {
694
+ nodeNames: ['MeshNode'],
695
+ unsupportedExtensions: [],
696
+ matrixTrsCoexistNodes: [],
697
+ },
698
+ };
699
+ const ctx = {
700
+ meshHandles: new Map([[0, fakeMeshHandle(10)]]),
701
+ materialHandles: new Map([
702
+ [0, fakeMaterialHandle(20)],
703
+ [1, fakeMaterialHandle(21)],
704
+ ]),
705
+ };
706
+ const scene = gltfDocToSceneAsset(doc, ctx);
707
+ expect(scene.entities.length).toBe(1);
708
+
709
+ const entityNode = scene.entities[0];
710
+ expect(entityNode).toBeDefined();
711
+ expect(entityNode?.components.MeshFilter?.assetHandle).toBe(10);
712
+ expect(entityNode?.components.MeshRenderer).toBeDefined();
713
+ });
714
+
715
+ it('single-prim glTF produces 1 entity with 1 material (backward compatible)', () => {
716
+ const doc: GltfDoc = {
717
+ meshes: [
718
+ {
719
+ name: 'Box',
720
+ positions: new Float32Array([0, 0, 0]),
721
+ indices: new Uint16Array([0]),
722
+ materialIndex: 0,
723
+ meshIndex: 0,
724
+ },
725
+ ],
726
+ materials: [{ baseColorFactor: [1, 0, 0, 1], metallicFactor: 1, roughnessFactor: 1 }],
727
+ nodes: [
728
+ {
729
+ name: 'BoxNode',
730
+ transform: { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
731
+ meshIndex: 0,
732
+ skinIndex: null,
733
+ camera: null,
734
+ children: [],
735
+ },
736
+ ],
737
+ scenes: [{ name: 'Scene', nodes: [0] }],
738
+ textures: undefined,
739
+ images: undefined,
740
+ samplers: undefined,
741
+ skeletons: [],
742
+ animationClips: [],
743
+ defaultSceneIndex: 0,
744
+ diagnostics: {
745
+ nodeNames: ['BoxNode'],
746
+ unsupportedExtensions: [],
747
+ matrixTrsCoexistNodes: [],
748
+ },
749
+ };
750
+ const ctx = {
751
+ meshHandles: new Map([[0, fakeMeshHandle(1)]]),
752
+ materialHandles: new Map([[0, fakeMaterialHandle(7)]]),
753
+ };
754
+ const scene = gltfDocToSceneAsset(doc, ctx);
755
+ expect(scene.entities.length).toBe(1);
756
+ const node = scene.entities[0];
757
+ expect(node?.components.MeshFilter?.assetHandle).toBe(1);
758
+ expect(node?.components.MeshRenderer).toBeDefined();
759
+ });
760
+
761
+ it('transform-only node (no mesh, no camera) produces entity with Transform only', () => {
762
+ const doc: GltfDoc = {
763
+ meshes: [
764
+ {
765
+ positions: new Float32Array([0, 0, 0]),
766
+ indices: new Uint16Array([0]),
767
+ materialIndex: 0,
768
+ meshIndex: 0,
769
+ },
770
+ ],
771
+ materials: [{ baseColorFactor: [1, 1, 1, 1], metallicFactor: 1, roughnessFactor: 1 }],
772
+ nodes: [
773
+ {
774
+ name: 'TransformNode',
775
+ transform: { translation: [1, 2, 3], rotation: [0, 0, 0, 1], scale: [2, 2, 2] },
776
+ meshIndex: null,
777
+ skinIndex: null,
778
+ camera: null,
779
+ children: [],
780
+ },
781
+ ],
782
+ scenes: [{ name: 'Scene', nodes: [0] }],
783
+ textures: undefined,
784
+ images: undefined,
785
+ samplers: undefined,
786
+ skeletons: [],
787
+ animationClips: [],
788
+ defaultSceneIndex: 0,
789
+ diagnostics: {
790
+ nodeNames: ['TransformNode'],
791
+ unsupportedExtensions: [],
792
+ matrixTrsCoexistNodes: [],
793
+ },
794
+ };
795
+ const ctx = {
796
+ meshHandles: new Map(),
797
+ materialHandles: new Map(),
798
+ };
799
+ const scene = gltfDocToSceneAsset(doc, ctx);
800
+ expect(scene.entities.length).toBe(1);
801
+ const node = scene.entities[0];
802
+ expect(node?.components.Transform).toBeDefined();
803
+ expect(node?.components.MeshFilter).toBeUndefined();
804
+ expect(node?.components.MeshRenderer).toBeUndefined();
805
+ });
806
+
807
+ it('multi glTF-mesh document: each node materials filtered by node.meshIndex (verify-r1 charter-defect fix)', () => {
808
+ const m0p0: GltfMeshIr = {
809
+ positions: new Float32Array([0, 0, 0]),
810
+ indices: new Uint16Array([0]),
811
+ materialIndex: 0,
812
+ meshIndex: 0,
813
+ };
814
+ const m0p1: GltfMeshIr = {
815
+ positions: new Float32Array([0, 0, 0]),
816
+ indices: new Uint16Array([0]),
817
+ materialIndex: 1,
818
+ meshIndex: 0,
819
+ };
820
+ const m1p0: GltfMeshIr = {
821
+ positions: new Float32Array([0, 0, 0]),
822
+ indices: new Uint16Array([0]),
823
+ materialIndex: 2,
824
+ meshIndex: 1,
825
+ };
826
+ const doc: GltfDoc = {
827
+ meshes: [m0p0, m0p1, m1p0],
828
+ materials: [
829
+ { baseColorFactor: [1, 0, 0, 1], metallicFactor: 1, roughnessFactor: 1 },
830
+ { baseColorFactor: [0, 1, 0, 1], metallicFactor: 1, roughnessFactor: 1 },
831
+ { baseColorFactor: [0, 0, 1, 1], metallicFactor: 1, roughnessFactor: 1 },
832
+ ],
833
+ nodes: [
834
+ {
835
+ name: 'NodeA',
836
+ transform: { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
837
+ meshIndex: 0,
838
+ skinIndex: null,
839
+ camera: null,
840
+ children: [],
841
+ },
842
+ {
843
+ name: 'NodeB',
844
+ transform: { translation: [1, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
845
+ meshIndex: 1,
846
+ skinIndex: null,
847
+ camera: null,
848
+ children: [],
849
+ },
850
+ ],
851
+ scenes: [{ name: 'Scene', nodes: [0, 1] }],
852
+ textures: undefined,
853
+ images: undefined,
854
+ samplers: undefined,
855
+ skeletons: [],
856
+ animationClips: [],
857
+ defaultSceneIndex: 0,
858
+ diagnostics: {
859
+ nodeNames: ['NodeA', 'NodeB'],
860
+ unsupportedExtensions: [],
861
+ matrixTrsCoexistNodes: [],
862
+ },
863
+ };
864
+ const ctx = {
865
+ meshHandles: new Map([
866
+ [0, fakeMeshHandle(10)],
867
+ [1, fakeMeshHandle(11)],
868
+ ]),
869
+ materialHandles: new Map([
870
+ [0, fakeMaterialHandle(20)],
871
+ [1, fakeMaterialHandle(21)],
872
+ [2, fakeMaterialHandle(22)],
873
+ ]),
874
+ };
875
+ const scene = gltfDocToSceneAsset(doc, ctx);
876
+ expect(scene.entities.length).toBe(2);
877
+ const nodeA = scene.entities.find((n) => n.components.Name?.value === 'NodeA');
878
+ const nodeB = scene.entities.find((n) => n.components.Name?.value === 'NodeB');
879
+ expect(nodeA?.components.MeshRenderer?.materials).toEqual([]);
880
+ expect(nodeB?.components.MeshRenderer?.materials).toEqual([]);
881
+ });
882
+ });
883
+ });
884
+ }
885
+
886
+ {
887
+ // ─── from bridge-recurse-and-transform.test.ts ───
888
+
889
+ describe('bridge-recurse-and-transform.test.ts', () => {
890
+ describe('bridge.ts B2 child recursion (M3 w14)', () => {
891
+ it('3-level hierarchy (root->child->grandchild) produces 3 entities with ChildOf links', () => {
892
+ const doc: GltfDoc = {
893
+ meshes: [
894
+ {
895
+ positions: new Float32Array([0, 0, 0]),
896
+ indices: new Uint16Array([0]),
897
+ materialIndex: 0,
898
+ meshIndex: 0,
899
+ },
900
+ ],
901
+ materials: [{ baseColorFactor: [1, 1, 1, 1], metallicFactor: 1, roughnessFactor: 1 }],
902
+ nodes: [
903
+ {
904
+ name: 'Root',
905
+ transform: { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
906
+ meshIndex: 0,
907
+ skinIndex: null,
908
+ camera: null,
909
+ children: [1],
910
+ },
911
+ {
912
+ name: 'Child',
913
+ transform: { translation: [1, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
914
+ meshIndex: null,
915
+ skinIndex: null,
916
+ camera: null,
917
+ children: [2],
918
+ },
919
+ {
920
+ name: 'Grandchild',
921
+ transform: { translation: [0, 1, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
922
+ meshIndex: 0,
923
+ skinIndex: null,
924
+ camera: null,
925
+ children: [],
926
+ },
927
+ ],
928
+ scenes: [{ name: 'Scene', nodes: [0] }],
929
+ textures: undefined,
930
+ images: undefined,
931
+ samplers: undefined,
932
+ skeletons: [],
933
+ animationClips: [],
934
+ defaultSceneIndex: 0,
935
+ diagnostics: {
936
+ nodeNames: ['Root', 'Child', 'Grandchild'],
937
+ unsupportedExtensions: [],
938
+ matrixTrsCoexistNodes: [],
939
+ },
940
+ };
941
+ const ctx = {
942
+ meshHandles: new Map([[0, fakeMeshHandle(1)]]),
943
+ materialHandles: new Map([[0, fakeMaterialHandle(7)]]),
944
+ };
945
+ const scene = gltfDocToSceneAsset(doc, ctx);
946
+ expect(scene.entities.length).toBe(3);
947
+
948
+ const rootNode = scene.entities.find((n) => n.components.Name?.value === 'Root');
949
+ const childNode = scene.entities.find((n) => n.components.Name?.value === 'Child');
950
+ const grandchildNode = scene.entities.find(
951
+ (n) => n.components.Name?.value === 'Grandchild',
952
+ );
953
+ expect(rootNode).toBeDefined();
954
+ expect(childNode).toBeDefined();
955
+ expect(grandchildNode).toBeDefined();
956
+
957
+ expect(rootNode?.components.MeshFilter).toBeDefined();
958
+ expect(childNode?.components.MeshFilter).toBeUndefined();
959
+ expect(grandchildNode?.components.MeshFilter).toBeDefined();
960
+
961
+ expect(childNode?.components.ChildOf?.parent).toBe(rootNode?.localId);
962
+ expect(grandchildNode?.components.ChildOf?.parent).toBe(childNode?.localId);
963
+ });
964
+ });
965
+
966
+ describe('bridge.ts B3 transform accumulation (M3 w14)', () => {
967
+ // bug-20260613: bridge writes LOCAL TRS into Transform; ChildOf is also
968
+ // emitted, so propagateTransforms is the single accumulation path.
969
+ // Writing world TRS here would double-bake every child node.
970
+ it('child Transform mirrors local TRS (propagateTransforms accumulates world)', () => {
971
+ const doc: GltfDoc = {
972
+ meshes: [
973
+ {
974
+ positions: new Float32Array([0, 0, 0]),
975
+ indices: new Uint16Array([0]),
976
+ materialIndex: 0,
977
+ meshIndex: 0,
978
+ },
979
+ ],
980
+ materials: [{ baseColorFactor: [1, 1, 1, 1], metallicFactor: 1, roughnessFactor: 1 }],
981
+ nodes: [
982
+ {
983
+ name: 'Parent',
984
+ transform: { translation: [5, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
985
+ meshIndex: 0,
986
+ skinIndex: null,
987
+ camera: null,
988
+ children: [1],
989
+ },
990
+ {
991
+ name: 'Child',
992
+ transform: { translation: [2, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
993
+ meshIndex: 0,
994
+ skinIndex: null,
995
+ camera: null,
996
+ children: [],
997
+ },
998
+ ],
999
+ scenes: [{ name: 'Scene', nodes: [0] }],
1000
+ textures: undefined,
1001
+ images: undefined,
1002
+ samplers: undefined,
1003
+ skeletons: [],
1004
+ animationClips: [],
1005
+ defaultSceneIndex: 0,
1006
+ diagnostics: {
1007
+ nodeNames: ['Parent', 'Child'],
1008
+ unsupportedExtensions: [],
1009
+ matrixTrsCoexistNodes: [],
1010
+ },
1011
+ };
1012
+ const ctx = {
1013
+ meshHandles: new Map([[0, fakeMeshHandle(1)]]),
1014
+ materialHandles: new Map([[0, fakeMaterialHandle(7)]]),
1015
+ };
1016
+ const scene = gltfDocToSceneAsset(doc, ctx);
1017
+ expect(scene.entities.length).toBe(2);
1018
+
1019
+ const childNode = scene.entities.find((n) => n.components.Name?.value === 'Child');
1020
+ expect(childNode?.components.Transform).toBeDefined();
1021
+ const t = childNode?.components.Transform;
1022
+ // Local TRS: child node carries its own [2, 0, 0]; parent's [5, 0, 0]
1023
+ // is encoded via ChildOf -> parent's Transform, accumulated by
1024
+ // propagateTransforms downstream.
1025
+ expect(t?.pos).toEqual([2, 0, 0]);
1026
+ expect(childNode?.components.ChildOf?.parent).toBeDefined();
1027
+ });
1028
+ });
1029
+
1030
+ describe('bridge.ts B6 camera detection (M3 w14)', () => {
1031
+ it('glTF node with camera index produces entity with Camera component', () => {
1032
+ const doc = {
1033
+ meshes: [
1034
+ {
1035
+ positions: new Float32Array([0, 0, 0]),
1036
+ indices: new Uint16Array([0]),
1037
+ materialIndex: 0,
1038
+ meshIndex: 0,
1039
+ },
1040
+ ],
1041
+ materials: [{ baseColorFactor: [1, 1, 1, 1], metallicFactor: 1, roughnessFactor: 1 }],
1042
+ nodes: [
1043
+ {
1044
+ name: 'BoxNode',
1045
+ transform: { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
1046
+ meshIndex: 0,
1047
+ skinIndex: null,
1048
+ camera: null,
1049
+ children: [],
1050
+ },
1051
+ {
1052
+ name: 'CameraNode',
1053
+ transform: { translation: [0, 0, 3], rotation: [0, 0, 0, 1], scale: [1, 1, 1] },
1054
+ meshIndex: null,
1055
+ skinIndex: null,
1056
+ children: [],
1057
+ camera: 0,
1058
+ },
1059
+ ],
1060
+ scenes: [{ name: 'Scene', nodes: [0, 1] }],
1061
+ textures: undefined,
1062
+ images: undefined,
1063
+ samplers: undefined,
1064
+ skeletons: [],
1065
+ animationClips: [],
1066
+ defaultSceneIndex: 0,
1067
+ diagnostics: {
1068
+ nodeNames: ['BoxNode', 'CameraNode'],
1069
+ unsupportedExtensions: [],
1070
+ matrixTrsCoexistNodes: [],
1071
+ },
1072
+ };
1073
+ const ctx = {
1074
+ meshHandles: new Map([[0, fakeMeshHandle(1)]]),
1075
+ materialHandles: new Map([[0, fakeMaterialHandle(7)]]),
1076
+ };
1077
+ const scene = gltfDocToSceneAsset(doc as unknown as GltfDoc, ctx);
1078
+
1079
+ const cameraNode = scene.entities.find((n) => n.components.Name?.value === 'CameraNode');
1080
+ expect(cameraNode).toBeDefined();
1081
+ expect(cameraNode?.components.Camera).toBeDefined();
1082
+ expect(cameraNode?.components.Camera?.fov).toBe(0.7853981633974483);
1083
+
1084
+ const boxNode = scene.entities.find((n) => n.components.Name?.value === 'BoxNode');
1085
+ expect(boxNode?.components.Camera).toBeUndefined();
1086
+ });
1087
+ });
1088
+ });
1089
+ }
1090
+
1091
+ {
1092
+ // ─── from bridge-mesh-aabb.test.ts (RED) ───
1093
+ //
1094
+ // TDD fail-first for bug-20260706-import-path-meshasset-aabb-undefined-blocks-pick M3.
1095
+ // M4 (gltf producer emit aabb) will turn these assertions green.
1096
+ //
1097
+ // Assertions assume M4 will compute aabb via box3.fromPositions on the
1098
+ // concatenated positionsCat, which merges all primitives' positions in
1099
+ // vertex-buffer order.
1100
+
1101
+ describe('bridge-mesh-aabb.test.ts (RED)', () => {
1102
+ describe('meshIrToMeshAsset aabb (TDD fail-first, M4 turns green)', () => {
1103
+ it('single primitive: aabb equals position component min/max', () => {
1104
+ const prim: GltfMeshIr = {
1105
+ positions: new Float32Array([1, 2, 3, 4, 5, 6, -1, -2, -3]),
1106
+ indices: new Uint16Array([0, 1, 2]),
1107
+ materialIndex: 0,
1108
+ meshIndex: 0,
1109
+ };
1110
+ const asset = meshIrToMeshAsset([prim]);
1111
+ expect(asset.aabb).toBeInstanceOf(Float32Array);
1112
+ expect(asset.aabb).toHaveLength(6);
1113
+ // min=(-1,-2,-3) max=(4,5,6)
1114
+ expect(asset.aabb?.[0]).toBe(-1);
1115
+ expect(asset.aabb?.[1]).toBe(-2);
1116
+ expect(asset.aabb?.[2]).toBe(-3);
1117
+ expect(asset.aabb?.[3]).toBe(4);
1118
+ expect(asset.aabb?.[4]).toBe(5);
1119
+ expect(asset.aabb?.[5]).toBe(6);
1120
+ });
1121
+
1122
+ it('multi-primitive: merged AABB with offset prims so merged != any single box', () => {
1123
+ // Prim 0: box near origin, min=(0,0,-2), max=(3,3,0)
1124
+ const prim0: GltfMeshIr = {
1125
+ positions: new Float32Array([0, 0, -2, 3, 0, -2, 0, 3, 0]),
1126
+ indices: new Uint16Array([0, 1, 2]),
1127
+ materialIndex: 0,
1128
+ meshIndex: 0,
1129
+ };
1130
+ // Prim 1: box offset far away, min=(10,-1,10), max=(12,2,15)
1131
+ const prim1: GltfMeshIr = {
1132
+ positions: new Float32Array([10, -1, 10, 12, -1, 10, 10, 2, 15]),
1133
+ indices: new Uint16Array([0, 1, 2]),
1134
+ materialIndex: 1,
1135
+ meshIndex: 0,
1136
+ };
1137
+ const asset = meshIrToMeshAsset([prim0, prim1]);
1138
+ expect(asset.aabb).toBeInstanceOf(Float32Array);
1139
+ expect(asset.aabb).toHaveLength(6);
1140
+ // Merged: min=(0,-1,-2), max=(12,3,15)
1141
+ // Neither prim alone covers x=0..12 or y=-1..3 or z=-2..15.
1142
+ expect(asset.aabb?.[0]).toBe(0);
1143
+ expect(asset.aabb?.[1]).toBe(-1);
1144
+ expect(asset.aabb?.[2]).toBe(-2);
1145
+ expect(asset.aabb?.[3]).toBe(12);
1146
+ expect(asset.aabb?.[4]).toBe(3);
1147
+ expect(asset.aabb?.[5]).toBe(15);
1148
+ });
1149
+
1150
+ it('skinned input (18-float stride): aabb computed from position attribute only', () => {
1151
+ const prim: GltfMeshIr = {
1152
+ positions: new Float32Array([5, 1, 8, 2, 9, 4, 7, 3, 0]),
1153
+ indices: new Uint16Array([0, 1, 2]),
1154
+ joints0: new Uint16Array([0, 1, 2, 3, 0, 0, 0, 0, 1, 0, 0, 0]),
1155
+ weights0: new Float32Array([1, 0, 0, 0, 0.5, 0.5, 0, 0, 0, 1, 0, 0]),
1156
+ materialIndex: 0,
1157
+ meshIndex: 0,
1158
+ };
1159
+ const asset = meshIrToMeshAsset([prim]);
1160
+ expect(asset.aabb).toBeInstanceOf(Float32Array);
1161
+ expect(asset.aabb).toHaveLength(6);
1162
+ // min=(2,1,0) max=(7,9,8) — from positions, ignoring skin/joint data
1163
+ expect(asset.aabb?.[0]).toBe(2);
1164
+ expect(asset.aabb?.[1]).toBe(1);
1165
+ expect(asset.aabb?.[2]).toBe(0);
1166
+ expect(asset.aabb?.[3]).toBe(7);
1167
+ expect(asset.aabb?.[4]).toBe(9);
1168
+ expect(asset.aabb?.[5]).toBe(8);
1169
+ });
1170
+
1171
+ it('empty input (0 vertices): inverted-empty box', () => {
1172
+ const prim: GltfMeshIr = {
1173
+ positions: new Float32Array(0),
1174
+ materialIndex: 0,
1175
+ meshIndex: 0,
1176
+ };
1177
+ const asset = meshIrToMeshAsset([prim]);
1178
+ expect(asset.aabb).toBeInstanceOf(Float32Array);
1179
+ expect(asset.aabb).toHaveLength(6);
1180
+ // Inverted-empty: min=(+Inf,+Inf,+Inf), max=(-Inf,-Inf,-Inf)
1181
+ expect(asset.aabb?.[0]).toBe(Number.POSITIVE_INFINITY);
1182
+ expect(asset.aabb?.[1]).toBe(Number.POSITIVE_INFINITY);
1183
+ expect(asset.aabb?.[2]).toBe(Number.POSITIVE_INFINITY);
1184
+ expect(asset.aabb?.[3]).toBe(Number.NEGATIVE_INFINITY);
1185
+ expect(asset.aabb?.[4]).toBe(Number.NEGATIVE_INFINITY);
1186
+ expect(asset.aabb?.[5]).toBe(Number.NEGATIVE_INFINITY);
1187
+ });
1188
+ });
1189
+ });
1190
+ }
1191
+
1192
+ describe('meshIrToMeshAsset material slots', () => {
1193
+ it('deduplicates source materials into stable A/default/A slots', () => {
1194
+ const primitive = (materialIndex: number | null): GltfMeshIr => ({
1195
+ positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
1196
+ indices: new Uint16Array([0, 1, 2]),
1197
+ materialIndex,
1198
+ meshIndex: 0,
1199
+ });
1200
+ const guid = '019d0000-0000-7000-8000-000000000004';
1201
+ const asset = meshIrToMeshAsset([primitive(0), primitive(null), primitive(0)], {
1202
+ guidByIndex: new Map([[0, guid]]),
1203
+ nameByIndex: new Map([[0, 'Paint']]),
1204
+ });
1205
+
1206
+ expect(asset.submeshes.map((submesh) => submesh.materialSlot)).toEqual([0, 1, 0]);
1207
+ expect(asset.materialSlots.map((slot) => slot.slotName)).toEqual(['Paint', 'Default']);
1208
+ expect(asset.materialSlots.map((slot) => slot.sourceKey)).toEqual([
1209
+ 'gltf:material:0',
1210
+ 'gltf:default',
1211
+ ]);
1212
+ expect(asset.materialSlots[0]?.defaultMaterial).toBeInstanceOf(Uint8Array);
1213
+ expect(asset.materialSlots[1]?.defaultMaterial).toBeUndefined();
1214
+ });
1215
+
1216
+ it('projects importer COLOR_0 into the canonical color attribute', () => {
1217
+ const primitive = {
1218
+ positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
1219
+ indices: new Uint16Array([0, 1, 2]),
1220
+ colors0: new Float32Array([1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1]),
1221
+ materialIndex: 0,
1222
+ meshIndex: 0,
1223
+ } as GltfMeshIr & { readonly colors0: Float32Array };
1224
+ const asset = meshIrToMeshAsset([primitive]);
1225
+ expect(asset.attributes.color).toEqual(primitive.colors0);
1226
+ });
1227
+ });