@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,159 @@
1
+ import { describe, expect, expectTypeOf, it } from 'vitest';
2
+ import { type GltfMetaJson, reimportReuseMeta } from '../reimport-reuse-meta.js';
3
+ import {
4
+ deriveGltfSourceKeys,
5
+ type GltfSourceKeyError,
6
+ type GltfSourceKeyErrorCode,
7
+ } from '../source-key.js';
8
+
9
+ const META_BASE: GltfMetaJson = {
10
+ schemaVersion: 1,
11
+ kind: 'external-asset-package',
12
+ importer: 'gltf',
13
+ source: 'scene.gltf',
14
+ subAssets: [],
15
+ importSettings: {
16
+ defaultSceneIndex: 0,
17
+ diagnostics: { nodeNames: [], unsupportedExtensions: [], matrixTrsCoexistNodes: [] },
18
+ },
19
+ };
20
+
21
+ describe('glTF producer sourceKey', () => {
22
+ it('derives the public error-code alias from its error owner', () => {
23
+ expectTypeOf<GltfSourceKeyErrorCode>().toEqualTypeOf<GltfSourceKeyError['code']>();
24
+ });
25
+
26
+ it('derives stable semantic keys without using sourceIndex or locator', () => {
27
+ const result = deriveGltfSourceKeys([
28
+ { kind: 'mesh', sourceIndex: 4, name: 'Hero' },
29
+ { kind: 'material', sourceIndex: 0, name: 'Skin' },
30
+ ]);
31
+ expect(result).toEqual({ ok: true, keys: ['mesh:Hero', 'material:Skin'], conflicts: [] });
32
+ });
33
+
34
+ it('reports duplicate semantic names as a structured conflict', () => {
35
+ const result = deriveGltfSourceKeys([
36
+ { kind: 'mesh', sourceIndex: 0, name: 'Hero' },
37
+ { kind: 'mesh', sourceIndex: 1, name: 'Hero' },
38
+ ]);
39
+ expect(result.ok).toBe(false);
40
+ if (!result.ok) expect(result.error.code).toBe('duplicate-source-key');
41
+ });
42
+
43
+ it('uses the semantic kind for one anonymous output, never sourceIndex', () => {
44
+ const result = deriveGltfSourceKeys([{ kind: 'mesh', sourceIndex: 7 }]);
45
+ expect(result).toEqual({ ok: true, keys: ['mesh'], conflicts: [] });
46
+ });
47
+
48
+ it('persists an output display name in the generated sub-asset entry', () => {
49
+ const result = reimportReuseMeta([{ kind: 'mesh', sourceIndex: 0, name: 'Hero' }], undefined);
50
+ expect(result.ok).toBe(true);
51
+ if (!result.ok) return;
52
+ expect(result.value.subAssets[0]).toMatchObject({
53
+ kind: 'mesh',
54
+ sourceKey: 'mesh:Hero',
55
+ name: 'Hero',
56
+ });
57
+ });
58
+
59
+ it('rejects two anonymous outputs of one kind as ambiguous', () => {
60
+ const result = deriveGltfSourceKeys([
61
+ { kind: 'mesh', sourceIndex: 7 },
62
+ { kind: 'mesh', sourceIndex: 9 },
63
+ ]);
64
+ expect(result.ok).toBe(false);
65
+ if (!result.ok) expect(result.error.code).toBe('ambiguous-source-key');
66
+ });
67
+
68
+ it('preserves keyed identity when output order changes', () => {
69
+ const existing: GltfMetaJson = {
70
+ ...META_BASE,
71
+ subAssets: [
72
+ {
73
+ guid: '01928000-7c00-7000-8000-000000000001',
74
+ sourceIndex: 0,
75
+ kind: 'mesh',
76
+ sourceKey: 'mesh:Hero',
77
+ },
78
+ {
79
+ guid: '01928000-7c00-7000-8000-000000000002',
80
+ sourceIndex: 1,
81
+ kind: 'mesh',
82
+ sourceKey: 'mesh:Body',
83
+ },
84
+ ],
85
+ };
86
+ const result = reimportReuseMeta(
87
+ [
88
+ { kind: 'mesh', sourceIndex: 0, name: 'Body' },
89
+ { kind: 'mesh', sourceIndex: 1, name: 'Hero' },
90
+ ],
91
+ existing,
92
+ );
93
+ expect(result.ok).toBe(true);
94
+ if (!result.ok) return;
95
+ expect(result.value.subAssets).toMatchObject([
96
+ { guid: '01928000-7c00-7000-8000-000000000002', sourceIndex: 0, sourceKey: 'mesh:Body' },
97
+ { guid: '01928000-7c00-7000-8000-000000000001', sourceIndex: 1, sourceKey: 'mesh:Hero' },
98
+ ]);
99
+ });
100
+
101
+ it('preserves keyed identity when a new output is inserted', () => {
102
+ const existing: GltfMetaJson = {
103
+ ...META_BASE,
104
+ subAssets: [
105
+ {
106
+ guid: '01928000-7c00-7000-8000-000000000004',
107
+ sourceIndex: 0,
108
+ kind: 'mesh',
109
+ sourceKey: 'mesh:Hero',
110
+ },
111
+ {
112
+ guid: '01928000-7c00-7000-8000-000000000005',
113
+ sourceIndex: 1,
114
+ kind: 'mesh',
115
+ sourceKey: 'mesh:Body',
116
+ },
117
+ ],
118
+ };
119
+ const result = reimportReuseMeta(
120
+ [
121
+ { kind: 'mesh', sourceIndex: 0, name: 'New' },
122
+ { kind: 'mesh', sourceIndex: 1, name: 'Hero' },
123
+ { kind: 'mesh', sourceIndex: 2, name: 'Body' },
124
+ ],
125
+ existing,
126
+ );
127
+ expect(result.ok).toBe(true);
128
+ if (!result.ok) return;
129
+ expect(result.value.subAssets.slice(1)).toMatchObject([
130
+ { guid: '01928000-7c00-7000-8000-000000000004', sourceKey: 'mesh:Hero' },
131
+ { guid: '01928000-7c00-7000-8000-000000000005', sourceKey: 'mesh:Body' },
132
+ ]);
133
+ });
134
+
135
+ it('reuses an anonymous semantic kind key when its sourceIndex changes', () => {
136
+ const existing: GltfMetaJson = {
137
+ ...META_BASE,
138
+ subAssets: [
139
+ {
140
+ guid: '01928000-7c00-7000-8000-000000000003',
141
+ sourceIndex: 0,
142
+ kind: 'scene',
143
+ sourceKey: 'scene',
144
+ },
145
+ ],
146
+ };
147
+ const result = reimportReuseMeta([{ kind: 'scene', sourceIndex: 9 }], existing);
148
+ expect(result.ok).toBe(true);
149
+ if (!result.ok) return;
150
+ expect(result.value.subAssets).toEqual([
151
+ {
152
+ guid: '01928000-7c00-7000-8000-000000000003',
153
+ sourceIndex: 9,
154
+ kind: 'scene',
155
+ sourceKey: 'scene',
156
+ },
157
+ ]);
158
+ });
159
+ });
@@ -0,0 +1,309 @@
1
+ // decode-accessor.ts - glTF 2.0 accessor decoder (w9).
2
+ //
3
+ // Pure helper that walks one accessor and returns the typed array view of
4
+ // its dense values. Tier-B v1 accepts only the dense, fixed-stride,
5
+ // non-sparse, non-morph subset; everything else returns
6
+ // `gltf-accessor-type-mismatch` with the reason discriminator.
7
+ //
8
+ // glTF 2.0 spec section 3.6 (accessor):
9
+ // - componentType : 5120 I8 / 5121 U8 / 5122 I16 / 5123 U16 / 5125 U32 / 5126 F32
10
+ // - type : 'SCALAR' / 'VEC2' / 'VEC3' / 'VEC4' / 'MAT2' / 'MAT3' / 'MAT4'
11
+ // - bufferView : index into bufferViews[]
12
+ // - byteOffset : within bufferView
13
+ // - count : number of elements
14
+ // - sparse : OOS (Tier-B v1 reject)
15
+ // - bufferView.byteStride : when set and != element_size -> interleaved (OOS reject)
16
+ //
17
+ // The U8 -> U16 widening for INDICES is handled inline (a 1-byte index
18
+ // stream is rewritten into a fresh Uint16Array). U16 and U32 index streams
19
+ // pass through at their source width; `MeshAsset.indices` is
20
+ // `Uint16Array | Uint32Array` end-to-end (mesh-bin serializes iwidth 2|4,
21
+ // GPU runtime auto-selects the index format), and bridge.ts narrows U32 to
22
+ // U16 losslessly when the merged mesh's maxIndex fits.
23
+ //
24
+ // Producers MUST use `decodeAccessor` rather than reading typed arrays
25
+ // directly so the closed `GltfErrorCode` surface (charter proposition 4)
26
+ // is the only public failure path.
27
+
28
+ import { err, type GltfError, gltfErr, ok, type Result } from '../errors.js';
29
+
30
+ /** glTF 2.0 component-type IDs (KHR spec section 3.6 table 4). */
31
+ export const COMPONENT_TYPE = {
32
+ I8: 5120,
33
+ U8: 5121,
34
+ I16: 5122,
35
+ U16: 5123,
36
+ U32: 5125,
37
+ F32: 5126,
38
+ } as const;
39
+ export type ComponentTypeId = (typeof COMPONENT_TYPE)[keyof typeof COMPONENT_TYPE];
40
+
41
+ /** Number of components per element for each glTF accessor `type` literal. */
42
+ const TYPE_COMPONENT_COUNT: Readonly<Record<string, number>> = {
43
+ SCALAR: 1,
44
+ VEC2: 2,
45
+ VEC3: 3,
46
+ VEC4: 4,
47
+ MAT2: 4,
48
+ MAT3: 9,
49
+ MAT4: 16,
50
+ };
51
+
52
+ /** Byte size of one component for each component type. */
53
+ const COMPONENT_BYTE_SIZE: Readonly<Record<number, number>> = {
54
+ [COMPONENT_TYPE.I8]: 1,
55
+ [COMPONENT_TYPE.U8]: 1,
56
+ [COMPONENT_TYPE.I16]: 2,
57
+ [COMPONENT_TYPE.U16]: 2,
58
+ [COMPONENT_TYPE.U32]: 4,
59
+ [COMPONENT_TYPE.F32]: 4,
60
+ };
61
+
62
+ /** Minimal accessor JSON shape touched by the decoder. */
63
+ export interface AccessorJson {
64
+ readonly bufferView: number;
65
+ readonly byteOffset?: number;
66
+ readonly componentType: number;
67
+ readonly count: number;
68
+ readonly normalized?: boolean;
69
+ readonly type: string;
70
+ readonly sparse?: unknown;
71
+ }
72
+
73
+ /** Minimal bufferView JSON shape touched by the decoder. */
74
+ export interface BufferViewJson {
75
+ readonly buffer: number;
76
+ readonly byteOffset?: number;
77
+ readonly byteLength: number;
78
+ readonly byteStride?: number;
79
+ }
80
+
81
+ /**
82
+ * Caller intent flag: when this accessor backs a mesh `INDICES` slot, U8
83
+ * indices auto-widen to U16 (Tier-B v1 mesh asset stores U16 only).
84
+ */
85
+ export type AccessorRole = 'attribute' | 'indices' | 'joints';
86
+
87
+ export interface DecodeAccessorInput {
88
+ readonly accessorIndex: number;
89
+ readonly accessor: AccessorJson;
90
+ readonly bufferView: BufferViewJson;
91
+ readonly buffer: Uint8Array;
92
+ readonly role: AccessorRole;
93
+ }
94
+
95
+ export type DecodedAccessor =
96
+ | { readonly kind: 'f32'; readonly data: Float32Array }
97
+ | { readonly kind: 'u16'; readonly data: Uint16Array }
98
+ | { readonly kind: 'u32'; readonly data: Uint32Array };
99
+
100
+ type F32AccessorJson = Omit<AccessorJson, 'bufferView'> & {
101
+ readonly bufferView?: number;
102
+ };
103
+
104
+ /**
105
+ * Decode an F32 accessor for parser-owned type subsets without duplicating
106
+ * buffer lookup and dense-range handling in each parser.
107
+ */
108
+ export function decodeF32Accessor(
109
+ accessorIndex: number,
110
+ accessor: F32AccessorJson,
111
+ allowedTypes: readonly string[],
112
+ bufferViews: readonly BufferViewJson[],
113
+ buffers: readonly Uint8Array[],
114
+ ): Result<Float32Array, GltfError> {
115
+ if (accessor.componentType !== COMPONENT_TYPE.F32 || !allowedTypes.includes(accessor.type)) {
116
+ return err(
117
+ gltfErr('gltf-accessor-type-mismatch', { accessorIndex, reason: 'unknownComponentType' }),
118
+ );
119
+ }
120
+
121
+ const bufferViewIndex = accessor.bufferView;
122
+ if (bufferViewIndex === undefined) {
123
+ return err(
124
+ gltfErr('gltf-buffer-out-of-bounds', {
125
+ accessor: accessorIndex,
126
+ byteOffset: 0,
127
+ byteLength: 0,
128
+ bufferIndex: 0,
129
+ }),
130
+ );
131
+ }
132
+ const bufferView = bufferViews[bufferViewIndex];
133
+ if (bufferView === undefined) {
134
+ return err(
135
+ gltfErr('gltf-buffer-out-of-bounds', {
136
+ accessor: accessorIndex,
137
+ byteOffset: 0,
138
+ byteLength: 0,
139
+ bufferIndex: bufferViewIndex,
140
+ }),
141
+ );
142
+ }
143
+ const buffer = buffers[bufferView.buffer];
144
+ if (buffer === undefined) {
145
+ return err(
146
+ gltfErr('gltf-buffer-out-of-bounds', {
147
+ accessor: accessorIndex,
148
+ byteOffset: bufferView.byteOffset ?? 0,
149
+ byteLength: bufferView.byteLength,
150
+ bufferIndex: bufferView.buffer,
151
+ }),
152
+ );
153
+ }
154
+
155
+ const decoded = decodeAccessor({
156
+ accessor: { ...accessor, bufferView: bufferViewIndex },
157
+ bufferView,
158
+ buffer,
159
+ accessorIndex,
160
+ role: 'attribute',
161
+ });
162
+ if (!decoded.ok) return decoded;
163
+ if (decoded.value.kind !== 'f32') {
164
+ return err(
165
+ gltfErr('gltf-accessor-type-mismatch', { accessorIndex, reason: 'unknownComponentType' }),
166
+ );
167
+ }
168
+ return ok(decoded.value.data);
169
+ }
170
+
171
+ /**
172
+ * Decode a dense glTF accessor into the typed array view of its values.
173
+ * Rejects sparse / morph / interleaved / unknown-componentType variants
174
+ * with `gltf-accessor-type-mismatch`. Out-of-range byte slices return
175
+ * `gltf-buffer-out-of-bounds`.
176
+ *
177
+ * `morph` is signalled by the caller (mesh primitive iteration in
178
+ * parseGltf) by passing an accessor reached via a `targets[]` slot - the
179
+ * decoder cannot infer it from the accessor JSON alone, so this function
180
+ * surfaces the reason via the `morph` parameter when the caller has
181
+ * already detected it.
182
+ */
183
+ export function decodeAccessor(
184
+ input: DecodeAccessorInput,
185
+ flags: { readonly morph?: boolean } = {},
186
+ ): Result<DecodedAccessor, GltfError> {
187
+ const { accessorIndex, accessor, bufferView, buffer, role } = input;
188
+
189
+ if (flags.morph === true) {
190
+ return err(
191
+ gltfErr('gltf-accessor-type-mismatch', {
192
+ accessorIndex,
193
+ reason: 'morph',
194
+ }),
195
+ );
196
+ }
197
+
198
+ if ('sparse' in accessor && accessor.sparse !== undefined) {
199
+ return err(
200
+ gltfErr('gltf-accessor-type-mismatch', {
201
+ accessorIndex,
202
+ reason: 'sparse',
203
+ }),
204
+ );
205
+ }
206
+
207
+ const componentByteSize = COMPONENT_BYTE_SIZE[accessor.componentType];
208
+ const componentCount = TYPE_COMPONENT_COUNT[accessor.type];
209
+ if (componentByteSize === undefined || componentCount === undefined) {
210
+ return err(
211
+ gltfErr('gltf-accessor-type-mismatch', {
212
+ accessorIndex,
213
+ reason: 'unknownComponentType',
214
+ }),
215
+ );
216
+ }
217
+ const elementSize = componentByteSize * componentCount;
218
+ const byteStride = bufferView.byteStride;
219
+ if (byteStride !== undefined && byteStride !== elementSize) {
220
+ return err(
221
+ gltfErr('gltf-accessor-type-mismatch', {
222
+ accessorIndex,
223
+ reason: 'interleaved',
224
+ }),
225
+ );
226
+ }
227
+
228
+ const accessorByteOffset = accessor.byteOffset ?? 0;
229
+ const bufferViewByteOffset = bufferView.byteOffset ?? 0;
230
+ const totalByteLength = elementSize * accessor.count;
231
+ if (accessorByteOffset + totalByteLength > bufferView.byteLength) {
232
+ return err(
233
+ gltfErr('gltf-buffer-out-of-bounds', {
234
+ accessor: accessorIndex,
235
+ byteOffset: bufferViewByteOffset + accessorByteOffset,
236
+ byteLength: totalByteLength,
237
+ bufferIndex: bufferView.buffer,
238
+ }),
239
+ );
240
+ }
241
+ const absoluteOffset = bufferViewByteOffset + accessorByteOffset;
242
+ if (absoluteOffset + totalByteLength > buffer.byteLength) {
243
+ return err(
244
+ gltfErr('gltf-buffer-out-of-bounds', {
245
+ accessor: accessorIndex,
246
+ byteOffset: absoluteOffset,
247
+ byteLength: totalByteLength,
248
+ bufferIndex: bufferView.buffer,
249
+ }),
250
+ );
251
+ }
252
+
253
+ // U8 INDICES auto-widen to U16 (Tier-B v1 mesh asset stores U16 only).
254
+ if (role === 'indices' && accessor.componentType === COMPONENT_TYPE.U8) {
255
+ const widened = new Uint16Array(accessor.count);
256
+ for (let i = 0; i < accessor.count; i++) {
257
+ widened[i] = buffer[absoluteOffset + i] ?? 0;
258
+ }
259
+ return ok({ kind: 'u16', data: widened });
260
+ }
261
+ // U8 JOINTS auto-widen to U16 (Tier-B v1 mesh asset stores U16 only).
262
+ // Reuses the indices widen pattern (E-5) but JOINTS_0 is VEC4 (componentCount=4),
263
+ // not SCALAR like INDICES; total values = accessor.count * componentCount.
264
+ if (role === 'joints' && accessor.componentType === COMPONENT_TYPE.U8) {
265
+ const totalValues = accessor.count * componentCount;
266
+ const widened = new Uint16Array(totalValues);
267
+ for (let i = 0; i < totalValues; i++) {
268
+ widened[i] = buffer[absoluteOffset + i] ?? 0;
269
+ }
270
+ return ok({ kind: 'u16', data: widened });
271
+ }
272
+ if (accessor.componentType === COMPONENT_TYPE.F32) {
273
+ // Copy out (rather than alias) so callers may safely mutate / persist
274
+ // the result independent of the underlying GLB BIN chunk lifetime.
275
+ const out = new Float32Array(accessor.count * componentCount);
276
+ const src = new Float32Array(buffer.buffer, buffer.byteOffset + absoluteOffset, out.length);
277
+ out.set(src);
278
+ return ok({ kind: 'f32', data: out });
279
+ }
280
+ if (accessor.componentType === COMPONENT_TYPE.U16) {
281
+ const out = new Uint16Array(accessor.count * componentCount);
282
+ const src = new Uint16Array(buffer.buffer, buffer.byteOffset + absoluteOffset, out.length);
283
+ out.set(src);
284
+ return ok({ kind: 'u16', data: out });
285
+ }
286
+ if (accessor.componentType === COMPONENT_TYPE.U32) {
287
+ const out = new Uint32Array(accessor.count * componentCount);
288
+ const src = new Uint32Array(buffer.buffer, buffer.byteOffset + absoluteOffset, out.length);
289
+ out.set(src);
290
+ return ok({ kind: 'u32', data: out });
291
+ }
292
+ if (accessor.componentType === COMPONENT_TYPE.I16) {
293
+ const out = new Float32Array(accessor.count * componentCount);
294
+ const src = new Int16Array(buffer.buffer, buffer.byteOffset + absoluteOffset, out.length);
295
+ for (let index = 0; index < src.length; index++) {
296
+ const value = src[index] ?? 0;
297
+ out[index] = accessor.normalized === true ? Math.max(-1, value / 32767) : value;
298
+ }
299
+ return ok({ kind: 'f32', data: out });
300
+ }
301
+ // I8 remains outside the current attribute contract. Keep it rejected
302
+ // rather than silently widening an unowned representation.
303
+ return err(
304
+ gltfErr('gltf-accessor-type-mismatch', {
305
+ accessorIndex,
306
+ reason: 'unknownComponentType',
307
+ }),
308
+ );
309
+ }
@@ -0,0 +1,128 @@
1
+ import { err, gltfErr, ok, type Result } from '../errors.js';
2
+ import type { AccessorJson, BufferViewJson } from './decode-accessor.js';
3
+
4
+ export interface DecodeColorAccessorInput {
5
+ readonly accessorIndex: number;
6
+ readonly accessor: AccessorJson;
7
+ readonly bufferView: BufferViewJson;
8
+ readonly buffer: Uint8Array;
9
+ readonly bufferIndex?: number;
10
+ readonly semantic: 'COLOR_0';
11
+ }
12
+
13
+ const FLOAT = 5126;
14
+ const UNSIGNED_BYTE = 5121;
15
+ const UNSIGNED_SHORT = 5123;
16
+
17
+ function unsupported(
18
+ input: DecodeColorAccessorInput,
19
+ reason: 'component' | 'type' | 'normalized' | 'sparse' | 'morph',
20
+ ) {
21
+ return err(
22
+ gltfErr('gltf-color-accessor-unsupported', {
23
+ semantic: input.semantic,
24
+ accessorIndex: input.accessorIndex,
25
+ reason,
26
+ expectedType: 'VEC3 or VEC4',
27
+ expectedComponent: 'FLOAT or normalized UNSIGNED_BYTE/UNSIGNED_SHORT',
28
+ ...(reason === 'normalized' ? { expectedNormalized: true } : {}),
29
+ }),
30
+ );
31
+ }
32
+
33
+ function malformed(
34
+ input: DecodeColorAccessorInput,
35
+ reason: 'count' | 'bounds' | 'finite' | 'range' | 'reference',
36
+ ) {
37
+ return err(
38
+ gltfErr('gltf-color-accessor-malformed', {
39
+ semantic: input.semantic,
40
+ accessorIndex: input.accessorIndex,
41
+ reason,
42
+ ...(reason === 'count'
43
+ ? { expectedCount: 'matches POSITION vertex count and is greater than zero' }
44
+ : {}),
45
+ ...(reason === 'range' || reason === 'finite'
46
+ ? { expectedRange: '[0,1] finite linear values' }
47
+ : {}),
48
+ }),
49
+ );
50
+ }
51
+
52
+ /** Decode one dense glTF COLOR_0 accessor into linear RGBA float values. */
53
+ export function decodeColorAccessor(
54
+ input: DecodeColorAccessorInput,
55
+ flags: { readonly morph?: boolean } = {},
56
+ ): Result<Float32Array, import('../errors.js').GltfError> {
57
+ const { accessor, bufferView, buffer } = input;
58
+ if (flags.morph === true) return unsupported(input, 'morph');
59
+ if (accessor.sparse !== undefined) return unsupported(input, 'sparse');
60
+
61
+ if (accessor.type !== 'VEC3' && accessor.type !== 'VEC4') return unsupported(input, 'type');
62
+ if (!Number.isSafeInteger(accessor.count) || accessor.count <= 0)
63
+ return malformed(input, 'count');
64
+
65
+ let componentByteSize: 1 | 2 | 4;
66
+ if (accessor.componentType === FLOAT) componentByteSize = 4;
67
+ else if (accessor.componentType === UNSIGNED_BYTE) componentByteSize = 1;
68
+ else if (accessor.componentType === UNSIGNED_SHORT) componentByteSize = 2;
69
+ else return unsupported(input, 'component');
70
+
71
+ if (accessor.componentType === FLOAT) {
72
+ if (accessor.normalized !== undefined) return unsupported(input, 'normalized');
73
+ } else if (accessor.normalized !== true) {
74
+ return unsupported(input, 'normalized');
75
+ }
76
+
77
+ const componentCount = accessor.type === 'VEC3' ? 3 : 4;
78
+ const elementByteLength = componentByteSize * componentCount;
79
+ const byteStride = bufferView.byteStride ?? elementByteLength;
80
+ if (
81
+ !Number.isSafeInteger(byteStride) ||
82
+ byteStride < elementByteLength ||
83
+ byteStride % componentByteSize !== 0
84
+ ) {
85
+ return malformed(input, 'bounds');
86
+ }
87
+ if (input.bufferIndex !== undefined && bufferView.buffer !== input.bufferIndex) {
88
+ return malformed(input, 'reference');
89
+ }
90
+
91
+ const accessorByteOffset = accessor.byteOffset ?? 0;
92
+ const viewByteOffset = bufferView.byteOffset ?? 0;
93
+ const lastElementEnd = accessorByteOffset + byteStride * (accessor.count - 1) + elementByteLength;
94
+ if (
95
+ !Number.isSafeInteger(accessorByteOffset) ||
96
+ accessorByteOffset < 0 ||
97
+ lastElementEnd > bufferView.byteLength
98
+ ) {
99
+ return malformed(input, 'bounds');
100
+ }
101
+ const absoluteLastElementEnd = viewByteOffset + lastElementEnd;
102
+ if (
103
+ !Number.isSafeInteger(viewByteOffset) ||
104
+ viewByteOffset < 0 ||
105
+ absoluteLastElementEnd > buffer.byteLength
106
+ ) {
107
+ return malformed(input, 'bounds');
108
+ }
109
+
110
+ const output = new Float32Array(accessor.count * 4);
111
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
112
+ const base = viewByteOffset + accessorByteOffset;
113
+ for (let vertex = 0; vertex < accessor.count; vertex++) {
114
+ const sourceOffset = base + vertex * byteStride;
115
+ for (let component = 0; component < componentCount; component++) {
116
+ const byteOffset = sourceOffset + component * componentByteSize;
117
+ let value: number;
118
+ if (accessor.componentType === FLOAT) value = view.getFloat32(byteOffset, true);
119
+ else if (accessor.componentType === UNSIGNED_BYTE) value = view.getUint8(byteOffset) / 255;
120
+ else value = view.getUint16(byteOffset, true) / 65535;
121
+ if (!Number.isFinite(value)) return malformed(input, 'finite');
122
+ if (value < 0 || value > 1) return malformed(input, 'range');
123
+ output[vertex * 4 + component] = value;
124
+ }
125
+ if (componentCount === 3) output[vertex * 4 + 3] = 1;
126
+ }
127
+ return ok(output);
128
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ type AccessorJson,
3
+ type AccessorRole,
4
+ type BufferViewJson,
5
+ COMPONENT_TYPE,
6
+ type ComponentTypeId,
7
+ type DecodeAccessorInput,
8
+ type DecodedAccessor,
9
+ decodeAccessor,
10
+ decodeF32Accessor,
11
+ } from './decode-accessor.js';
12
+ export {
13
+ type DecodeColorAccessorInput,
14
+ decodeColorAccessor,
15
+ } from './decode-color.js';