@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,104 @@
1
+ // image-color-space.ts - sRGB / linear classifier for glTF images
2
+ // (feat-20260608 M3 D-3 / requirements AC-08 + AC-13 + C-3).
3
+ //
4
+ // glTF 2.0 spec section 6.2 ("Material → Texture and Sampler") names which
5
+ // texture slots are colour-encoded vs data-encoded:
6
+ // - sRGB : baseColorTexture, emissiveTexture
7
+ // - linear : metallicRoughnessTexture, normalTexture, occlusionTexture
8
+ // MaterialIr carries baseColor / metallicRoughness / normal / emissive;
9
+ // occlusion remains outside this Tier-C subset.
10
+ // We pre-scan the doc so the gltfImporter knows each `images[]` row's
11
+ // colorSpace before decoding (TextureAsset.colorSpace + .format derive
12
+ // from this).
13
+ //
14
+ // Conflict resolution (requirements section 8 edge cases): when the same
15
+ // glTF image is bound to multiple textures whose colour expectations
16
+ // disagree, sRGB wins. baseColor leakage into a normal slot is far worse
17
+ // than the inverse, and bevy_gltf takes the same stance (knowledge-base /
18
+ // research finding §5.6).
19
+ //
20
+ // Orphan images (declared in `images[]` but unreferenced by any
21
+ // `textures[]` entry) default to linear (no colour-encoded purpose
22
+ // inferable; AC-13).
23
+ //
24
+ // Pure function, no I/O. Input = the parts of the parsed GltfDoc that
25
+ // matter (images count, textures, materials); output = a Map keyed by
26
+ // the image array index.
27
+
28
+ export type ImageColorSpaceSrgbOrLinear = 'srgb' | 'linear';
29
+
30
+ type TextureBinding = number | { readonly texture: number };
31
+
32
+ /** Slim view of a parsed material the classifier reads (subset of MaterialIr). */
33
+ export interface MaterialColorSpaceInput {
34
+ readonly baseColorTexture?: TextureBinding;
35
+ readonly metallicRoughnessTexture?: TextureBinding;
36
+ readonly normalTexture?: TextureBinding;
37
+ readonly emissiveTexture?: TextureBinding;
38
+ readonly occlusionTexture?: TextureBinding;
39
+ }
40
+
41
+ /** Slim view of a parsed `textures[]` row. */
42
+ export interface TextureColorSpaceInput {
43
+ readonly source: number;
44
+ }
45
+
46
+ /**
47
+ * Inputs for {@link deriveTextureColorSpace}: just the parts of the parsed
48
+ * doc that the classifier reads. Decoupled from `GltfDoc` so the helper is
49
+ * trivially testable without building a full doc.
50
+ */
51
+ export interface DeriveTextureColorSpaceInput {
52
+ readonly imageCount: number;
53
+ readonly textures: readonly TextureColorSpaceInput[] | undefined;
54
+ readonly materials: readonly MaterialColorSpaceInput[];
55
+ }
56
+
57
+ /**
58
+ * Walk the materials, follow each texture-slot binding back to the image
59
+ * it references, and produce `Map<imageIndex, 'srgb' | 'linear'>`. Slots
60
+ * disagreeing on the same image resolve to sRGB (see module header for
61
+ * the rationale). Orphan images default to linear.
62
+ */
63
+ export function deriveTextureColorSpace(
64
+ input: DeriveTextureColorSpaceInput,
65
+ ): Map<number, ImageColorSpaceSrgbOrLinear> {
66
+ const result = new Map<number, ImageColorSpaceSrgbOrLinear>();
67
+ const textures = input.textures ?? [];
68
+
69
+ function imageOfTexture(binding: TextureBinding | undefined): number | undefined {
70
+ if (binding === undefined) return undefined;
71
+ const textureIndex = typeof binding === 'number' ? binding : binding.texture;
72
+ const tex = textures[textureIndex];
73
+ if (tex === undefined) return undefined;
74
+ return tex.source;
75
+ }
76
+
77
+ function record(imageIndex: number | undefined, colorSpace: ImageColorSpaceSrgbOrLinear): void {
78
+ if (imageIndex === undefined) return;
79
+ const prior = result.get(imageIndex);
80
+ if (prior === undefined) {
81
+ result.set(imageIndex, colorSpace);
82
+ return;
83
+ }
84
+ if (prior === 'srgb' || colorSpace === 'srgb') {
85
+ result.set(imageIndex, 'srgb');
86
+ }
87
+ }
88
+
89
+ for (const mat of input.materials) {
90
+ record(imageOfTexture(mat.baseColorTexture), 'srgb');
91
+ record(imageOfTexture(mat.emissiveTexture), 'srgb');
92
+ record(imageOfTexture(mat.metallicRoughnessTexture), 'linear');
93
+ record(imageOfTexture(mat.normalTexture), 'linear');
94
+ record(imageOfTexture(mat.occlusionTexture), 'linear');
95
+ }
96
+
97
+ for (let i = 0; i < input.imageCount; i++) {
98
+ if (!result.has(i)) {
99
+ result.set(i, 'linear');
100
+ }
101
+ }
102
+
103
+ return result;
104
+ }
@@ -0,0 +1,24 @@
1
+ import { MeshoptDecoder } from 'meshoptimizer';
2
+ import { createGltfImporter } from './gltf-importer.js';
3
+ import type { GltfBufferViewDecodeCapability } from './meshopt-decode.js';
4
+
5
+ await MeshoptDecoder.ready;
6
+
7
+ /** Browser/build consumer capability for EXT_meshopt_compression. */
8
+ export const meshoptDecoder: GltfBufferViewDecodeCapability = {
9
+ decode: ({ source, count, stride, mode, filter }) => {
10
+ const target = new Uint8Array(count * stride);
11
+ MeshoptDecoder.decodeGltfBuffer(target, count, stride, source, mode, filter);
12
+ return target;
13
+ },
14
+ };
15
+
16
+ /**
17
+ * Build-only glTF importer entry. The player-facing package remains free of
18
+ * the Meshopt WASM dependency; hosts opt into this entry when importing files.
19
+ */
20
+ export const gltfImporter = createGltfImporter({
21
+ decode: meshoptDecoder.decode,
22
+ });
23
+
24
+ export { createGltfImporter } from './gltf-importer.js';
package/src/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ // @forgeax/engine-gltf - runtime glTF 2.0 importer (Tier-B subset).
2
+ //
3
+ // M2 surface: pure-function pipeline `parseGlb` / `parseGltf` /
4
+ // `toAssetPack` plus helpers `parseGltfHeader` / `parseGlbChunks` /
5
+ // `decodeAccessor` / `decomposeNodeTransform` / `subAssetKey` /
6
+ // `reimportReuseMeta` / `checkExtensions`.
7
+ // M3 adds the file-entry stubs `parseGltfFromFile` / `parseGlbFromFile`.
8
+ //
9
+ // Surface contract anchors:
10
+ // - plan-strategy section 3.1 (component map gltf_pkg.Pure / Helpers / Errors)
11
+ // - plan-strategy section 8 "Naming convention" (parseGlb / parseGltf /
12
+ // toAssetPack lowercase camel; gltf word lowercase)
13
+ // - GltfErrorCode closed union + GltfErrorDetail discriminated
14
+ // detail live in @forgeax/engine-types as the SSOT (decision section
15
+ // 2.3); the single-import surface re-exports them here.
16
+
17
+ // Sub-asset POD SSOT re-exports (from @forgeax/engine-types; replaces
18
+ // the old unprefixed Ir types which are now Gltf-prefixed exports above).
19
+ export type {
20
+ AnimationChannelPod,
21
+ AnimationClipPod,
22
+ AnimationSamplerPod,
23
+ MaterialPod,
24
+ MeshPod,
25
+ MeshSubmeshPod,
26
+ SceneEntityPod,
27
+ ScenePod,
28
+ SkeletonPod,
29
+ SkinPod,
30
+ SkinVertexInfluencePod,
31
+ TexturePod,
32
+ } from '@forgeax/engine-types';
33
+ export type {
34
+ AccessorJson,
35
+ AccessorRole,
36
+ BufferViewJson,
37
+ ComponentTypeId,
38
+ DecodeAccessorInput,
39
+ DecodedAccessor,
40
+ } from './accessor/decode-accessor.js';
41
+ // Accessor decoder (w9).
42
+ export { COMPONENT_TYPE, decodeAccessor } from './accessor/decode-accessor.js';
43
+ export type { DecodeColorAccessorInput } from './accessor/decode-color.js';
44
+ export { decodeColorAccessor } from './accessor/decode-color.js';
45
+ export type { GltfBridgeContext, MaterialBridgeContext } from './bridge.js';
46
+ // Bridge: gltfDocToSceneAsset + toMaterialAsset + meshIrToMeshAsset (SSOT for
47
+ // hello-gltf + hello-gltf-instancing, feat-20260518 M3 w9; M3 Tier-C material
48
+ // bridge; meshIrToMeshAsset excised from the hello-gltf demo in
49
+ // feat-20260603-asset-import-loader-injection M2 w19).
50
+ export { gltfDocToSceneAsset, meshIrToMeshAsset, toMaterialAsset } from './bridge.js';
51
+ export type { ExtensionsCheckResult, GltfExtensionsJson } from './check-extensions.js';
52
+ // KHR extensions gate (w14).
53
+ export { checkExtensions, EXTENSION_ALLOWLIST } from './check-extensions.js';
54
+ export type {
55
+ GltfAccessorTypeMismatchDetail,
56
+ GltfAnimationCubicsplineUnsupportedDetail,
57
+ GltfBufferOutOfBoundsDetail,
58
+ GltfColorAccessorMalformedDetail,
59
+ GltfColorAccessorUnsupportedDetail,
60
+ GltfError,
61
+ GltfErrorCode,
62
+ GltfErrorDetail,
63
+ GltfExtensionUnsupportedDetail,
64
+ GltfImageExtractFailedDetail,
65
+ GltfImageMimeUnsupportedDetail,
66
+ GltfInstancingCountMismatchDetail,
67
+ GltfMalformedHeaderDetail,
68
+ GltfMetaMissingDetail,
69
+ GltfMorphInvalidDetail,
70
+ GltfMorphUnsupportedDetail,
71
+ GltfSkinAttrAsymmetricDetail,
72
+ GltfSkinJointCountExceededDetail,
73
+ GltfSkinJointNameMissingDetail,
74
+ GltfTextureLoadFailedDetail,
75
+ GltfVersionUnsupportedDetail,
76
+ Result,
77
+ } from './errors.js';
78
+ // Errors / Result alias / factory (errors.ts SSOT re-exports).
79
+ export { err, GLTF_ERROR_HINTS, gltfErr, ok } from './errors.js';
80
+ // gltfImporter: the build-time `{ key: 'gltf', import }` Importer
81
+ // (feat-20260603-asset-import-loader-injection M2 / w19).
82
+ export { gltfImporter } from './gltf-importer.js';
83
+ export type { GlbChunks } from './parse-glb-chunks.js';
84
+ // Header / chunk parsers (w8).
85
+ export { parseGlbChunks, parseGltfHeader } from './parse-glb-chunks.js';
86
+ export type {
87
+ ExternalLoader,
88
+ GltfAssetPack,
89
+ GltfAssetPackResult,
90
+ GltfDiagnosticsIr,
91
+ GltfDoc,
92
+ GltfImageIr,
93
+ GltfMaterialIr,
94
+ GltfMeshIr,
95
+ GltfNodeIr,
96
+ GltfNormalTextureInfoIr,
97
+ GltfOcclusionTextureInfoIr,
98
+ GltfSamplerIr,
99
+ GltfSceneIr,
100
+ GltfTextureInfoIr,
101
+ GltfTextureIr,
102
+ GltfTextureTransformIr,
103
+ MeshJson,
104
+ MeshPrimitiveJson,
105
+ NodeInstancingIr,
106
+ } from './parse-gltf.js';
107
+ // Main pipeline (w15 + w17) + file-entry wrappers (w19).
108
+ export { parseGlb, parseGltf, toAssetPack } from './parse-gltf.js';
109
+ // File-entry wrappers (parseGltfFromFile / parseGlbFromFile) live in
110
+ // `./node-file-entry.ts` and ship under the `./node` sub-entry to keep
111
+ // the main entry browser-clean -- they touch `node:fs/promises` /
112
+ // `node:path` via dynamic import and would otherwise trip vite's "module
113
+ // externalized for browser compatibility" warning when consumers bundle
114
+ // the demo for the browser. Node consumers:
115
+ //
116
+ // import { parseGltfFromFile } from '@forgeax/engine-gltf/node';
117
+ export type { GltfHeaderJson } from './parse-gltf-header.js';
118
+ export type {
119
+ GltfDocItem,
120
+ GltfMetaJson,
121
+ GltfSubAssetEntry,
122
+ ReimportReuseResult,
123
+ ReimportReuseValue,
124
+ SubAssetKey,
125
+ } from './reimport-reuse-meta.js';
126
+ // Sub-asset key + reimport-reuse algorithm (w13).
127
+ export {
128
+ reimportReuseMeta,
129
+ subAssetKey,
130
+ } from './reimport-reuse-meta.js';
131
+ // Byte-stable meta JSON serialization (D-3; AC-04).
132
+ export { serializeMetaJson } from './serialize-meta.js';
133
+ export type {
134
+ GltfSourceKeyConflictEntry,
135
+ GltfSourceKeyError,
136
+ GltfSourceKeyErrorCode,
137
+ GltfSourceKeyResult,
138
+ } from './source-key.js';
139
+ export { deriveGltfSourceKeys, sourceKeyForGltfOutput } from './source-key.js';
140
+ export type {
141
+ DecomposedTransform,
142
+ NodeTransformJson,
143
+ TransformDiagnostics,
144
+ Vec3Tuple,
145
+ Vec4Tuple,
146
+ } from './transform.js';
147
+ // Node transform decomposer (w11).
148
+ export { decomposeNodeTransform } from './transform.js';
@@ -0,0 +1,200 @@
1
+ import type { BufferViewJson } from './accessor/decode-accessor.js';
2
+ import {
3
+ err,
4
+ GLTF_MESHOPT_FILTERS,
5
+ GLTF_MESHOPT_MODES,
6
+ type GltfError,
7
+ type GltfMeshoptFilter,
8
+ type GltfMeshoptMode,
9
+ gltfErr,
10
+ ok,
11
+ type Result,
12
+ } from './errors.js';
13
+
14
+ export type MeshoptMode = GltfMeshoptMode;
15
+ export type MeshoptFilter = GltfMeshoptFilter;
16
+
17
+ export interface MeshoptCompressionJson {
18
+ readonly buffer: number;
19
+ readonly byteOffset?: number;
20
+ readonly byteLength: number;
21
+ readonly byteStride: number;
22
+ readonly count: number;
23
+ readonly mode: MeshoptMode;
24
+ readonly filter?: MeshoptFilter;
25
+ }
26
+
27
+ export interface MeshoptBufferViewJson extends BufferViewJson {
28
+ readonly extensions?: {
29
+ readonly EXT_meshopt_compression?: MeshoptCompressionJson;
30
+ };
31
+ }
32
+
33
+ export interface GltfBufferViewDecodeCapability {
34
+ readonly decode: (input: {
35
+ readonly source: Uint8Array;
36
+ readonly count: number;
37
+ readonly stride: number;
38
+ readonly mode: MeshoptMode;
39
+ readonly filter: MeshoptFilter;
40
+ }) => Uint8Array | Promise<Uint8Array>;
41
+ }
42
+
43
+ export interface MeshoptProjection {
44
+ readonly bufferViews: readonly MeshoptBufferViewJson[];
45
+ readonly buffers: readonly Uint8Array[];
46
+ readonly decodedCount: number;
47
+ }
48
+
49
+ function viewBytes(
50
+ view: MeshoptBufferViewJson,
51
+ buffers: readonly Uint8Array[],
52
+ ): Uint8Array | undefined {
53
+ const buffer = buffers[view.buffer];
54
+ const offset = view.byteOffset ?? 0;
55
+ if (
56
+ buffer === undefined ||
57
+ view.byteLength <= 0 ||
58
+ offset < 0 ||
59
+ offset + view.byteLength > buffer.length
60
+ ) {
61
+ return undefined;
62
+ }
63
+ return buffer.subarray(offset, offset + view.byteLength);
64
+ }
65
+
66
+ function validMode(mode: string): mode is MeshoptMode {
67
+ return GLTF_MESHOPT_MODES.some((candidate) => candidate === mode);
68
+ }
69
+
70
+ function validFilter(filter: string): filter is MeshoptFilter {
71
+ return GLTF_MESHOPT_FILTERS.some((candidate) => candidate === filter);
72
+ }
73
+
74
+ function filterAllowed(mode: MeshoptMode, filter: MeshoptFilter): boolean {
75
+ return mode === 'ATTRIBUTES' || filter === 'NONE';
76
+ }
77
+
78
+ /**
79
+ * Resolve EXT_meshopt_compression bufferViews before any accessor consumer.
80
+ * Optional assets keep a valid core bufferView fallback when no decoder is
81
+ * installed; required or compressed-only assets fail with a structured error.
82
+ */
83
+ export async function projectMeshoptBufferViews(
84
+ inputViews: readonly MeshoptBufferViewJson[],
85
+ inputBuffers: readonly Uint8Array[],
86
+ extensionsRequired: readonly string[],
87
+ capability?: GltfBufferViewDecodeCapability,
88
+ ): Promise<Result<MeshoptProjection, GltfError>> {
89
+ const buffers = [...inputBuffers];
90
+ const bufferViews = inputViews.map((view) => ({ ...view }));
91
+ let decodedCount = 0;
92
+
93
+ for (let index = 0; index < bufferViews.length; index++) {
94
+ const view = bufferViews[index];
95
+ if (view === undefined) continue;
96
+ const extension = view.extensions?.EXT_meshopt_compression;
97
+ if (extension === undefined) continue;
98
+
99
+ const filter = extension.filter ?? 'NONE';
100
+ if (
101
+ !validMode(extension.mode) ||
102
+ !validFilter(filter) ||
103
+ !filterAllowed(extension.mode, filter) ||
104
+ !Number.isInteger(extension.byteStride) ||
105
+ extension.byteStride <= 0 ||
106
+ !Number.isInteger(extension.count) ||
107
+ extension.count <= 0 ||
108
+ !Number.isInteger(extension.byteLength) ||
109
+ extension.byteLength <= 0
110
+ ) {
111
+ return err(
112
+ gltfErr('gltf-meshopt-decode-failed', {
113
+ bufferView: index,
114
+ actual: 'invalid mode/filter/stride/count/byteLength',
115
+ mode: validMode(extension.mode) ? extension.mode : 'ATTRIBUTES',
116
+ filter: validFilter(filter) ? filter : 'NONE',
117
+ }),
118
+ );
119
+ }
120
+
121
+ const fallback = viewBytes(view, buffers);
122
+ const required = extensionsRequired.includes('EXT_meshopt_compression');
123
+ if (capability === undefined && !required && fallback !== undefined) continue;
124
+ if (capability === undefined) {
125
+ return err(
126
+ gltfErr('gltf-meshopt-decoder-required', {
127
+ bufferView: index,
128
+ actual: required ? 'required' : 'compressed-only',
129
+ hasCoreFallback: fallback !== undefined,
130
+ }),
131
+ );
132
+ }
133
+
134
+ const compressedBuffer = buffers[extension.buffer];
135
+ const compressedOffset = extension.byteOffset ?? 0;
136
+ if (
137
+ compressedBuffer === undefined ||
138
+ !Number.isInteger(compressedOffset) ||
139
+ compressedOffset < 0 ||
140
+ compressedOffset + extension.byteLength > compressedBuffer.length
141
+ ) {
142
+ return err(
143
+ gltfErr('gltf-meshopt-decode-failed', {
144
+ bufferView: index,
145
+ actual: 'compressed range out of bounds',
146
+ mode: extension.mode,
147
+ filter,
148
+ }),
149
+ );
150
+ }
151
+
152
+ let decoded: Uint8Array;
153
+ try {
154
+ decoded = await capability.decode({
155
+ source: compressedBuffer.subarray(
156
+ compressedOffset,
157
+ compressedOffset + extension.byteLength,
158
+ ),
159
+ count: extension.count,
160
+ stride: extension.byteStride,
161
+ mode: extension.mode,
162
+ filter,
163
+ });
164
+ } catch (cause) {
165
+ return err(
166
+ gltfErr('gltf-meshopt-decode-failed', {
167
+ bufferView: index,
168
+ actual: `decoder rejected compressed range: ${cause instanceof Error ? cause.message : String(cause)}`,
169
+ mode: extension.mode,
170
+ filter,
171
+ }),
172
+ );
173
+ }
174
+
175
+ const expectedLength = extension.count * extension.byteStride;
176
+ if (!(decoded instanceof Uint8Array) || decoded.byteLength !== expectedLength) {
177
+ return err(
178
+ gltfErr('gltf-meshopt-decode-failed', {
179
+ bufferView: index,
180
+ actual: `decoded byteLength=${decoded?.byteLength ?? 'invalid'}, expected=${expectedLength}`,
181
+ mode: extension.mode,
182
+ filter,
183
+ }),
184
+ );
185
+ }
186
+
187
+ const decodedBufferIndex = buffers.length;
188
+ buffers.push(decoded);
189
+ const { extensions: _extensions, ...plainView } = view;
190
+ bufferViews[index] = {
191
+ ...plainView,
192
+ buffer: decodedBufferIndex,
193
+ byteOffset: 0,
194
+ byteLength: expectedLength,
195
+ };
196
+ decodedCount++;
197
+ }
198
+
199
+ return ok({ bufferViews, buffers, decodedCount });
200
+ }
@@ -0,0 +1,98 @@
1
+ // File-entry wrappers (Node-only, dynamic-import isolated).
2
+ //
3
+ // `parseGltfFromFile` / `parseGlbFromFile` are the only members of this
4
+ // package that touch the filesystem. They live in this dedicated module so
5
+ // the main entry (`index.ts`) stays browser-clean: the `node:fs/promises`
6
+ // and `node:path` dynamic imports below would otherwise trip vite's
7
+ // "module externalized for browser compatibility" warning when the demo
8
+ // bundles `@forgeax/engine-gltf` for the browser even though it never
9
+ // calls these helpers.
10
+ //
11
+ // Sidecar policy (feat-20260521 unify-sidecar-meta-dispatch-by-content):
12
+ // <name>.gltf -> <name>.gltf.meta.json
13
+ // <name>.glb -> <name>.glb.meta.json
14
+ //
15
+ // Both wrappers add a single fail-fast pre-step: stat the sibling
16
+ // `<source>.meta.json`; return `gltf-meta-missing` if absent (same code
17
+ // surfaced by the vite-plugin and the console CLI dry-run, charter
18
+ // proposition 5 consistent abstraction). Callers with an ArrayBuffer /
19
+ // parsed JSON should keep using `parseGltf` / `parseGlb` directly.
20
+
21
+ import { err, type GltfError, gltfErr, type Result } from './errors.js';
22
+ import { type ExternalLoader, type GltfDoc, parseGlb, parseGltf } from './parse-gltf.js';
23
+
24
+ interface FsLike {
25
+ readonly stat: (path: string) => Promise<unknown>;
26
+ readonly readFile: (path: string) => Promise<Buffer>;
27
+ }
28
+
29
+ interface PathLike {
30
+ readonly dirname: (p: string) => string;
31
+ readonly resolve: (...segments: string[]) => string;
32
+ }
33
+
34
+ async function loadFsModule(): Promise<FsLike> {
35
+ const mod = (await import('node:fs/promises')) as unknown as FsLike;
36
+ return mod;
37
+ }
38
+
39
+ async function loadPathModule(): Promise<PathLike> {
40
+ const mod = (await import('node:path')) as unknown as PathLike;
41
+ return mod;
42
+ }
43
+
44
+ async function statExists(fs: FsLike, path: string): Promise<boolean> {
45
+ try {
46
+ await fs.stat(path);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function metaPathFor(filePath: string): string {
54
+ return `${filePath}.meta.json`;
55
+ }
56
+
57
+ /**
58
+ * Parse a `.gltf` file from disk. Pre-step: stat the sibling
59
+ * `<source>.meta.json`; if missing, return `gltf-meta-missing` without
60
+ * reading the source bytes.
61
+ */
62
+ export async function parseGltfFromFile(filePath: string): Promise<Result<GltfDoc, GltfError>> {
63
+ const fs = await loadFsModule();
64
+ const path = await loadPathModule();
65
+ const expectedMetaPath = metaPathFor(filePath);
66
+ if (!(await statExists(fs, expectedMetaPath))) {
67
+ return err(gltfErr('gltf-meta-missing', { filePath, expectedMetaPath }));
68
+ }
69
+ const text = (await fs.readFile(filePath)).toString('utf-8');
70
+ let json: unknown;
71
+ try {
72
+ json = JSON.parse(text);
73
+ } catch (_e) {
74
+ return err(gltfErr('gltf-malformed-header', { filePath, byteOffset: 0 }));
75
+ }
76
+ const baseDir = path.dirname(filePath);
77
+ const externalLoader: ExternalLoader = async (uri: string) => {
78
+ const abs = path.resolve(baseDir, uri);
79
+ const buf = await fs.readFile(abs);
80
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
81
+ };
82
+ return parseGltf(json, externalLoader, filePath);
83
+ }
84
+
85
+ /**
86
+ * Parse a `.glb` file from disk. Same fail-fast meta-stat pre-step as
87
+ * `parseGltfFromFile`.
88
+ */
89
+ export async function parseGlbFromFile(filePath: string): Promise<Result<GltfDoc, GltfError>> {
90
+ const fs = await loadFsModule();
91
+ const expectedMetaPath = metaPathFor(filePath);
92
+ if (!(await statExists(fs, expectedMetaPath))) {
93
+ return err(gltfErr('gltf-meta-missing', { filePath, expectedMetaPath }));
94
+ }
95
+ const buf = await fs.readFile(filePath);
96
+ const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
97
+ return parseGlb(ab, filePath);
98
+ }
@@ -0,0 +1,41 @@
1
+ export interface NamedNode {
2
+ readonly name?: string;
3
+ readonly children?: readonly number[];
4
+ }
5
+
6
+ export function buildNodeParentMap(nodes: readonly NamedNode[]): ReadonlyMap<number, number> {
7
+ const parents = new Map<number, number>();
8
+ for (let index = 0; index < nodes.length; index++) {
9
+ for (const child of nodes[index]?.children ?? []) parents.set(child, index);
10
+ }
11
+ return parents;
12
+ }
13
+
14
+ export function resolveNamedNodePath(
15
+ nodes: readonly NamedNode[],
16
+ parents: ReadonlyMap<number, number>,
17
+ nodeIndex: number,
18
+ ):
19
+ | { readonly ok: true; readonly value: readonly string[] }
20
+ | {
21
+ readonly ok: false;
22
+ readonly reason: 'name-missing' | 'hierarchy-cycle';
23
+ readonly nodeIndex: number;
24
+ } {
25
+ const reversed: string[] = [];
26
+ const visited = new Set<number>();
27
+ let current: number | undefined = nodeIndex;
28
+ while (current !== undefined) {
29
+ if (visited.has(current)) {
30
+ return { ok: false, reason: 'hierarchy-cycle', nodeIndex: current };
31
+ }
32
+ visited.add(current);
33
+ const name = nodes[current]?.name;
34
+ if (name === undefined || name.length === 0) {
35
+ return { ok: false, reason: 'name-missing', nodeIndex: current };
36
+ }
37
+ reversed.push(name);
38
+ current = parents.get(current);
39
+ }
40
+ return { ok: true, value: reversed.reverse() };
41
+ }