@babylonjs/loaders 9.20.0 → 9.21.0

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 (50) hide show
  1. package/SPLAT/gaussianSplattingStream.d.ts +170 -1
  2. package/SPLAT/gaussianSplattingStream.js +516 -40
  3. package/SPLAT/gaussianSplattingStream.js.map +1 -1
  4. package/SPLAT/gaussianSplattingWorkBuffer.d.ts +163 -4
  5. package/SPLAT/gaussianSplattingWorkBuffer.js +683 -35
  6. package/SPLAT/gaussianSplattingWorkBuffer.js.map +1 -1
  7. package/SPLAT/gaussianSplattingWorkBufferShaders.d.ts +70 -2
  8. package/SPLAT/gaussianSplattingWorkBufferShaders.js +465 -4
  9. package/SPLAT/gaussianSplattingWorkBufferShaders.js.map +1 -1
  10. package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.d.ts +16 -1
  11. package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js +232 -25
  12. package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js.map +1 -1
  13. package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.d.ts +52 -2
  14. package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js +108 -7
  15. package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js.map +1 -1
  16. package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.d.ts +37 -0
  17. package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js +69 -0
  18. package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js.map +1 -0
  19. package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.d.ts +44 -0
  20. package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.js +51 -0
  21. package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.js.map +1 -0
  22. package/glTF/2.0/Extensions/KHR_interactivity.pure.d.ts +1 -0
  23. package/glTF/2.0/Extensions/KHR_interactivity.pure.js +143 -12
  24. package/glTF/2.0/Extensions/KHR_interactivity.pure.js.map +1 -1
  25. package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js +6 -0
  26. package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js.map +1 -1
  27. package/glTF/2.0/Extensions/KHR_node_selectability.pure.js +4 -0
  28. package/glTF/2.0/Extensions/KHR_node_selectability.pure.js.map +1 -1
  29. package/glTF/2.0/Extensions/KHR_node_visibility.pure.js +11 -0
  30. package/glTF/2.0/Extensions/KHR_node_visibility.pure.js.map +1 -1
  31. package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.d.ts +126 -0
  32. package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.js +251 -0
  33. package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.js.map +1 -0
  34. package/glTF/2.0/Extensions/compositePathToObjectConverter.d.ts +51 -0
  35. package/glTF/2.0/Extensions/compositePathToObjectConverter.js +51 -0
  36. package/glTF/2.0/Extensions/compositePathToObjectConverter.js.map +1 -0
  37. package/glTF/2.0/Extensions/gltfPathToObjectConverter.js +69 -7
  38. package/glTF/2.0/Extensions/gltfPathToObjectConverter.js.map +1 -1
  39. package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.d.ts +41 -0
  40. package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.js +108 -0
  41. package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.js.map +1 -0
  42. package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.d.ts +28 -0
  43. package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js +71 -0
  44. package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js.map +1 -0
  45. package/glTF/2.0/Extensions/objectModelMapping.d.ts +118 -30
  46. package/glTF/2.0/Extensions/objectModelMapping.js +472 -70
  47. package/glTF/2.0/Extensions/objectModelMapping.js.map +1 -1
  48. package/glTF/2.0/glTFLoader.pure.js +2 -1
  49. package/glTF/2.0/glTFLoader.pure.js.map +1 -1
  50. package/package.json +3 -3
@@ -3,14 +3,32 @@ import { Matrix, Quaternion, Vector2 } from "@babylonjs/core/Maths/math.vector.p
3
3
  import { Constants } from "@babylonjs/core/Engines/constants.js";
4
4
  import { Color4 } from "@babylonjs/core/Maths/math.color.pure.js";
5
5
  import { SpotLight } from "@babylonjs/core/Lights/spotLight.pure.js";
6
+ import { FlowGraphInteger } from "@babylonjs/core/FlowGraph/CustomTypes/flowGraphInteger.pure.js";
6
7
  import { GLTFPathToObjectConverter } from "./gltfPathToObjectConverter.js";
7
- const nodesTree = {
8
- length: {
9
- type: "number",
10
- get: (nodes) => nodes.length,
11
- getTarget: (nodes) => nodes.map((node) => node._babylonTransformNode),
8
+ /**
9
+ * Builds an accessor for an array `length` pointer (e.g. `/nodes.length`, `/meshes/{}/primitives.length`).
10
+ *
11
+ * The KHR object model types these `length` values as `int`, so the accessor reports the count as a
12
+ * {@link FlowGraphInteger} (type {@link FlowGraphTypes.Integer}). Keeping the representation an integer
13
+ * at the source lets integer-typed consumers such as `FlowGraphIntToFloat` accept the precise
14
+ * `RichTypeFlowGraphInteger` instead of a widened `RichTypeAny`.
15
+ * @param getLength returns the raw count for the addressed array; may be `undefined` when the array is not addressable
16
+ * @param getTarget returns the Babylon object(s) backing the array
17
+ * @returns an object accessor whose value is the count as a {@link FlowGraphInteger}
18
+ */
19
+ function _CreateLengthAccessor(getLength, getTarget) {
20
+ return {
21
+ type: "FlowGraphInteger" /* FlowGraphTypes.Integer */,
22
+ get: (target) => {
23
+ const length = getLength(target);
24
+ return length === undefined ? undefined : FlowGraphInteger.FromValue(length);
25
+ },
26
+ getTarget,
12
27
  getPropertyName: [() => "length"],
13
- },
28
+ };
29
+ }
30
+ const nodesTree = {
31
+ length: _CreateLengthAccessor((nodes) => nodes.length, (nodes) => nodes.map((node) => node._babylonTransformNode)),
14
32
  __array__: {
15
33
  __target__: true,
16
34
  translation: {
@@ -35,24 +53,60 @@ const nodesTree = {
35
53
  getPropertyName: [() => "scaling"],
36
54
  },
37
55
  weights: {
38
- length: {
39
- type: "number",
40
- get: (node) => node._numMorphTargets,
41
- getTarget: (node) => node._babylonTransformNode,
42
- getPropertyName: [() => "influence"],
43
- },
56
+ // Skip glTF objectTree traversal — weights may be undefined on the glTF node
57
+ // but accessible via the Babylon MorphTargetManager on the INode's meshes
58
+ __passThroughTarget__: true,
59
+ length: _CreateLengthAccessor((node) => {
60
+ const found = _findNodeMorphTargets(node);
61
+ return found ? found.mtm.numTargets : node?.mesh !== undefined ? 0 : undefined;
62
+ }, (node) => node?._babylonTransformNode),
44
63
  __array__: {
45
64
  __target__: true,
46
65
  type: "number",
47
- get: (node, index) => (index !== undefined ? node._primitiveBabylonMeshes?.[0].morphTargetManager?.getTarget(index).influence : undefined),
48
- // set: (value: number, node: INode, index?: number) => node._babylonTransformNode?.getMorphTargetManager()?.getTarget(index)?.setInfluence(value),
49
- getTarget: (node) => node._babylonTransformNode,
66
+ get: (node, index) => {
67
+ const found = _findNodeMorphTargets(node);
68
+ if (found && index !== undefined && index >= 0 && index < found.mtm.numTargets) {
69
+ return _roundFloat32Artifact(found.mtm.getTarget(index).influence);
70
+ }
71
+ return undefined;
72
+ },
73
+ set: (value, node, index) => {
74
+ const numValue = typeof value === "number" ? value : typeof value?.value === "number" ? value.value : value;
75
+ const found = _findNodeMorphTargets(node);
76
+ if (!found || index === undefined || index < 0 || index >= found.mtm.numTargets) {
77
+ return;
78
+ }
79
+ // Fan out to every mesh that shares this morph target manager so
80
+ // multi-primitive meshes stay in sync.
81
+ for (const mesh of found.meshes) {
82
+ const target = mesh.morphTargetManager?.getTarget(index);
83
+ if (target) {
84
+ target.influence = numValue;
85
+ }
86
+ }
87
+ },
88
+ getTarget: (node, index) => {
89
+ const found = _findNodeMorphTargets(node);
90
+ if (found && index !== undefined && index >= 0 && index < found.mtm.numTargets) {
91
+ return found.mtm.getTarget(index);
92
+ }
93
+ return node?._babylonTransformNode;
94
+ },
50
95
  getPropertyName: [() => "influence"],
51
96
  },
52
97
  type: "number[]",
53
- get: (node, index) => [0], // TODO: get the weights correctly
54
- // set: (value: number, node: INode, index?: number) => node._babylonTransformNode?.getMorphTargetManager()?.getTarget(index)?.setInfluence(value),
55
- getTarget: (node) => node._babylonTransformNode,
98
+ get: (node) => {
99
+ const found = _findNodeMorphTargets(node);
100
+ if (!found) {
101
+ return [];
102
+ }
103
+ const weights = [];
104
+ for (let i = 0; i < found.mtm.numTargets; i++) {
105
+ weights.push(_roundFloat32Artifact(found.mtm.getTarget(i).influence));
106
+ }
107
+ return weights;
108
+ },
109
+ getTarget: (node) => node?._babylonTransformNode,
56
110
  getPropertyName: [() => "influence"],
57
111
  },
58
112
  // readonly!
@@ -88,6 +142,45 @@ const nodesTree = {
88
142
  getTarget: (node) => node._babylonTransformNode,
89
143
  isReadOnly: true,
90
144
  },
145
+ camera: {
146
+ type: "string",
147
+ // Per KHR_interactivity Object Model: read-only ref pointing to the
148
+ // attached camera, encoded as a JSON Pointer string. Empty string
149
+ // when no camera is attached (the spec's null-ref convention).
150
+ get: (node) => (node.camera !== undefined ? `/cameras/${node.camera}` : ""),
151
+ getTarget: (node) => node,
152
+ isReadOnly: true,
153
+ },
154
+ mesh: {
155
+ type: "string",
156
+ get: (node) => (node.mesh !== undefined ? `/meshes/${node.mesh}` : ""),
157
+ getTarget: (node) => node,
158
+ isReadOnly: true,
159
+ },
160
+ skin: {
161
+ type: "string",
162
+ get: (node) => (node.skin !== undefined ? `/skins/${node.skin}` : ""),
163
+ getTarget: (node) => node,
164
+ isReadOnly: true,
165
+ },
166
+ parent: {
167
+ type: "string",
168
+ get: (node) => (node.parent && node.parent.index !== undefined ? `/nodes/${node.parent.index}` : ""),
169
+ getTarget: (node) => node,
170
+ isReadOnly: true,
171
+ },
172
+ children: {
173
+ length: _CreateLengthAccessor((children) => children?.length ?? 0, (children) => children ?? []),
174
+ __array__: {
175
+ __target__: true,
176
+ type: "string",
177
+ // The wrapping converter passes the indexed child value (an
178
+ // INode index); convert it to a JSON Pointer ref string.
179
+ get: (childIndex) => (typeof childIndex === "number" ? `/nodes/${childIndex}` : ""),
180
+ getTarget: () => ({ __nodeIndex: true }),
181
+ isReadOnly: true,
182
+ },
183
+ },
91
184
  extensions: {
92
185
  EXT_lights_ies: {
93
186
  multiplier: {
@@ -139,24 +232,50 @@ const nodesTree = {
139
232
  },
140
233
  };
141
234
  const animationsTree = {
142
- length: {
143
- type: "number",
144
- get: (animations) => animations.length,
145
- getTarget: (animations) => animations.map((animation) => animation._babylonAnimationGroup),
146
- getPropertyName: [() => "length"],
235
+ length: _CreateLengthAccessor((animations) => animations.length, (animations) => animations.map((animation) => animation._babylonAnimationGroup)),
236
+ __array__: {
237
+ // Indexed access to the animation, surfaced as a JSON Pointer ref string so blocks like
238
+ // ``animation/start`` can consume it directly. Uses the animation's own ``index`` property
239
+ // (populated by the loader's ArrayItem.Assign step) so the ref is resolved without needing
240
+ // a separate index payload from the path converter.
241
+ __target__: true,
242
+ type: "string",
243
+ get: (animation) => (animation && typeof animation.index === "number" ? `/animations/${animation.index}` : ""),
244
+ getTarget: (animation) => animation._babylonAnimationGroup,
245
+ isReadOnly: true,
147
246
  },
148
- __array__: {},
149
247
  };
150
248
  const meshesTree = {
151
- length: {
152
- type: "number",
153
- get: (meshes) => meshes.length,
154
- getTarget: (meshes) => meshes.map((mesh) => mesh.primitives[0]._instanceData?.babylonSourceMesh),
155
- getPropertyName: [() => "length"],
249
+ length: _CreateLengthAccessor((meshes) => meshes.length, (meshes) => meshes.map((mesh) => mesh.primitives[0]._instanceData?.babylonSourceMesh)),
250
+ __array__: {
251
+ __target__: true,
252
+ primitives: {
253
+ length: _CreateLengthAccessor((primitives) => primitives?.length ?? 0, (primitives) => primitives ?? []),
254
+ __array__: {
255
+ __target__: true,
256
+ material: {
257
+ type: "string",
258
+ // Read-only ref to the assigned material, JSON Pointer encoded.
259
+ get: (primitive) => (primitive.material !== undefined ? `/materials/${primitive.material}` : ""),
260
+ getTarget: (primitive) => primitive,
261
+ isReadOnly: true,
262
+ },
263
+ },
264
+ },
265
+ weights: {
266
+ length: _CreateLengthAccessor((weights) => weights?.length ?? 0, (weights) => weights ?? []),
267
+ __array__: {
268
+ __target__: true,
269
+ type: "number",
270
+ get: (weightValue) => weightValue,
271
+ getTarget: () => ({ __weightValue: true }),
272
+ isReadOnly: true,
273
+ },
274
+ },
156
275
  },
157
- __array__: {},
158
276
  };
159
277
  const camerasTree = {
278
+ length: _CreateLengthAccessor((cameras) => cameras.length, (cameras) => cameras.map((camera) => camera._babylonCamera)),
160
279
  __array__: {
161
280
  __target__: true,
162
281
  orthographic: {
@@ -254,8 +373,33 @@ const camerasTree = {
254
373
  },
255
374
  };
256
375
  const materialsTree = {
376
+ length: _CreateLengthAccessor((materials) => materials.length, (materials) => materials.map((material) => material._data?.[Constants.MATERIAL_TriangleFillMode]?.babylonMaterial)),
257
377
  __array__: {
258
378
  __target__: true,
379
+ doubleSided: {
380
+ type: "boolean",
381
+ get: (material, index, payload) => !GetMaterial(material, index, payload)?.backFaceCulling,
382
+ set: (value, material, index, payload) => {
383
+ const mat = GetMaterial(material, index, payload);
384
+ if (mat) {
385
+ mat.backFaceCulling = !value;
386
+ }
387
+ },
388
+ getTarget: (material, index, payload) => GetMaterial(material, index, payload),
389
+ getPropertyName: [() => "backFaceCulling"],
390
+ },
391
+ alphaCutoff: {
392
+ type: "number",
393
+ get: (material, index, payload) => GetMaterial(material, index, payload)?.alphaCutOff,
394
+ set: (value, material, index, payload) => {
395
+ const mat = GetMaterial(material, index, payload);
396
+ if (mat) {
397
+ mat.alphaCutOff = value;
398
+ }
399
+ },
400
+ getTarget: (material, index, payload) => GetMaterial(material, index, payload),
401
+ getPropertyName: [() => "alphaCutOff"],
402
+ },
259
403
  emissiveFactor: {
260
404
  type: "Color3",
261
405
  get: (material, index, payload) => GetMaterial(material, index, payload).emissiveColor,
@@ -357,18 +501,24 @@ const materialsTree = {
357
501
  KHR_materials_anisotropy: {
358
502
  anisotropyStrength: {
359
503
  type: "number",
360
- get: (material, index, payload) => GetMaterial(material, index, payload).anisotropy.intensity,
504
+ get: (material, index, payload) => GetMaterial(material, index, payload)?.anisotropy?.intensity,
361
505
  set: (value, material, index, payload) => {
362
- GetMaterial(material, index, payload).anisotropy.intensity = value;
506
+ const mat = GetMaterial(material, index, payload);
507
+ if (mat) {
508
+ mat.anisotropy.intensity = value;
509
+ }
363
510
  },
364
511
  getTarget: (material, index, payload) => GetMaterial(material, index, payload),
365
512
  getPropertyName: [() => "anisotropy.intensity"],
366
513
  },
367
514
  anisotropyRotation: {
368
515
  type: "number",
369
- get: (material, index, payload) => GetMaterial(material, index, payload).anisotropy.angle,
516
+ get: (material, index, payload) => GetMaterial(material, index, payload)?.anisotropy?.angle,
370
517
  set: (value, material, index, payload) => {
371
- GetMaterial(material, index, payload).anisotropy.angle = value;
518
+ const mat = GetMaterial(material, index, payload);
519
+ if (mat) {
520
+ mat.anisotropy.angle = value;
521
+ }
372
522
  },
373
523
  getTarget: (material, index, payload) => GetMaterial(material, index, payload),
374
524
  getPropertyName: [() => "anisotropy.angle"],
@@ -500,7 +650,7 @@ const materialsTree = {
500
650
  },
501
651
  sheenRoughnessTexture: {
502
652
  extensions: {
503
- KHR_texture_transform: GenerateTextureMap("sheen", "thicknessTexture"),
653
+ KHR_texture_transform: GenerateTextureMap("sheen", "textureRoughness"),
504
654
  },
505
655
  },
506
656
  },
@@ -540,7 +690,10 @@ const materialsTree = {
540
690
  },
541
691
  transmissionTexture: {
542
692
  extensions: {
543
- KHR_texture_transform: GenerateTextureMap("subSurface", "refractionIntensityTexture"),
693
+ KHR_texture_transform: GenerateTextureMap("subSurface", "refractionIntensityTexture", {
694
+ extensionKey: "KHR_materials_transmission",
695
+ texturePath: ["transmissionTexture"],
696
+ }),
544
697
  },
545
698
  },
546
699
  },
@@ -553,7 +706,10 @@ const materialsTree = {
553
706
  },
554
707
  diffuseTransmissionTexture: {
555
708
  extensions: {
556
- KHR_texture_transform: GenerateTextureMap("subSurface", "translucencyIntensityTexture"),
709
+ KHR_texture_transform: GenerateTextureMap("subSurface", "translucencyIntensityTexture", {
710
+ extensionKey: "KHR_materials_diffuse_transmission",
711
+ texturePath: ["diffuseTransmissionTexture"],
712
+ }),
557
713
  },
558
714
  },
559
715
  diffuseTransmissionColorFactor: {
@@ -564,7 +720,10 @@ const materialsTree = {
564
720
  },
565
721
  diffuseTransmissionColorTexture: {
566
722
  extensions: {
567
- KHR_texture_transform: GenerateTextureMap("subSurface", "translucencyColorTexture"),
723
+ KHR_texture_transform: GenerateTextureMap("subSurface", "translucencyColorTexture", {
724
+ extensionKey: "KHR_materials_diffuse_transmission",
725
+ texturePath: ["diffuseTransmissionColorTexture"],
726
+ }),
568
727
  },
569
728
  },
570
729
  },
@@ -589,7 +748,7 @@ const materialsTree = {
589
748
  },
590
749
  thicknessTexture: {
591
750
  extensions: {
592
- KHR_texture_transform: GenerateTextureMap("subSurface", "thicknessTexture"),
751
+ KHR_texture_transform: GenerateTextureMap("subSurface", "thicknessTexture", { extensionKey: "KHR_materials_volume", texturePath: ["thicknessTexture"] }),
593
752
  },
594
753
  },
595
754
  },
@@ -599,12 +758,7 @@ const materialsTree = {
599
758
  const extensionsTree = {
600
759
  KHR_lights_punctual: {
601
760
  lights: {
602
- length: {
603
- type: "number",
604
- get: (lights) => lights.length,
605
- getTarget: (lights) => lights.map((light) => light._babylonLight),
606
- getPropertyName: [(_lights) => "length"],
607
- },
761
+ length: _CreateLengthAccessor((lights) => lights.length, (lights) => lights.map((light) => light._babylonLight)),
608
762
  __array__: {
609
763
  __target__: true,
610
764
  color: {
@@ -649,12 +803,7 @@ const extensionsTree = {
649
803
  },
650
804
  EXT_lights_area: {
651
805
  lights: {
652
- length: {
653
- type: "number",
654
- get: (lights) => lights.length,
655
- getTarget: (lights) => lights.map((light) => light._babylonLight),
656
- getPropertyName: [(_lights) => "length"],
657
- },
806
+ length: _CreateLengthAccessor((lights) => lights.length, (lights) => lights.map((light) => light._babylonLight)),
658
807
  __array__: {
659
808
  __target__: true,
660
809
  color: {
@@ -692,22 +841,12 @@ const extensionsTree = {
692
841
  },
693
842
  EXT_lights_ies: {
694
843
  lights: {
695
- length: {
696
- type: "number",
697
- get: (lights) => lights.length,
698
- getTarget: (lights) => lights.map((light) => light._babylonLight),
699
- getPropertyName: [(_lights) => "length"],
700
- },
844
+ length: _CreateLengthAccessor((lights) => lights.length, (lights) => lights.map((light) => light._babylonLight)),
701
845
  },
702
846
  },
703
847
  EXT_lights_image_based: {
704
848
  lights: {
705
- length: {
706
- type: "number",
707
- get: (lights) => lights.length,
708
- getTarget: (lights) => lights.map((light) => light._babylonTexture),
709
- getPropertyName: [(_lights) => "length"],
710
- },
849
+ length: _CreateLengthAccessor((lights) => lights.length, (lights) => lights.map((light) => light._babylonTexture)),
711
850
  __array__: {
712
851
  __target__: true,
713
852
  intensity: {
@@ -746,7 +885,159 @@ function GetTexture(material, payload, textureType, textureInObject) {
746
885
  function GetMaterial(material, _index, payload) {
747
886
  return material._data?.[payload?.fillMode ?? Constants.MATERIAL_TriangleFillMode]?.babylonMaterial;
748
887
  }
749
- function GenerateTextureMap(textureType, textureInObject) {
888
+ function _getNodeMorphTargetManager(node) {
889
+ const tn = node?._babylonTransformNode;
890
+ if (!tn) {
891
+ return undefined;
892
+ }
893
+ // Single primitive: transform node IS the mesh with morphTargetManager
894
+ if (tn.morphTargetManager) {
895
+ return tn.morphTargetManager;
896
+ }
897
+ // Multiple primitives: check each primitive mesh and its source
898
+ const primMeshes = node._primitiveBabylonMeshes;
899
+ if (primMeshes) {
900
+ for (const mesh of primMeshes) {
901
+ if (mesh?.morphTargetManager) {
902
+ return mesh.morphTargetManager;
903
+ }
904
+ // Check source mesh for instanced meshes
905
+ if (mesh?.sourceMesh?.morphTargetManager) {
906
+ return mesh.sourceMesh.morphTargetManager;
907
+ }
908
+ }
909
+ }
910
+ return undefined;
911
+ }
912
+ /**
913
+ * KHR_interactivity test assets routinely use a glTF hierarchy where the
914
+ * **parent** node has no `mesh` but a descendant does. The Khronos morph-weight
915
+ * tests query `/nodes/<parent>/weights/*` and expect the result to come from
916
+ * the morph targets of the first descendant mesh. To support this we walk the
917
+ * Babylon-side scene graph below the queried INode looking for a Mesh that has
918
+ * a morphTargetManager.
919
+ *
920
+ * For multi-primitive meshes (one INode → several Babylon meshes parented to a
921
+ * wrapper TransformNode) we also collect the sibling primitives so a `set`
922
+ * touches every mesh that shares the manager.
923
+ * @param node the glTF node to start the lookup from
924
+ * @returns the active morph target manager and every Babylon mesh that shares
925
+ * it, or `undefined` when no morph target manager is reachable from the node.
926
+ */
927
+ function _findNodeMorphTargets(node) {
928
+ const tn = node?._babylonTransformNode;
929
+ if (!tn) {
930
+ return undefined;
931
+ }
932
+ // Direct: this node's own mesh has a morph target manager.
933
+ const directMtm = _getNodeMorphTargetManager(node);
934
+ if (directMtm && node._primitiveBabylonMeshes && node._primitiveBabylonMeshes.length > 0) {
935
+ return { mtm: directMtm, meshes: node._primitiveBabylonMeshes };
936
+ }
937
+ // Fallback: search descendants in the Babylon scene graph for the first
938
+ // mesh that has a morph target manager, then collect every sibling mesh
939
+ // that shares it (covers the multi-primitive case).
940
+ const descendants = tn.getDescendants(false);
941
+ for (const desc of descendants) {
942
+ const candidate = desc;
943
+ const mtm = candidate.morphTargetManager ?? candidate.sourceMesh?.morphTargetManager;
944
+ if (!mtm) {
945
+ continue;
946
+ }
947
+ const meshes = [];
948
+ const parent = candidate.parent;
949
+ if (parent) {
950
+ for (const sib of parent.getChildMeshes(true)) {
951
+ const sibMtm = sib.morphTargetManager ?? sib.sourceMesh?.morphTargetManager;
952
+ if (sibMtm === mtm) {
953
+ meshes.push(sib);
954
+ }
955
+ }
956
+ }
957
+ if (meshes.length === 0) {
958
+ meshes.push(candidate);
959
+ }
960
+ return { mtm, meshes };
961
+ }
962
+ return undefined;
963
+ }
964
+ /**
965
+ * Collapse float32-precision artifacts back to the closest "clean" double.
966
+ *
967
+ * glTF stores numbers as JSON, but tools usually serialize float32 morph weights
968
+ * with their full double-precision text — `0.1` becomes `0.10000000149011612`.
969
+ * KHR_interactivity tests then compare the read-back weight via strict `math/eq`
970
+ * against literals like `0.1`, which fails because the two doubles aren't bitwise
971
+ * equal. Rounding the value to 7 significant figures (the precision of a float32)
972
+ * recovers the original "clean" double for any value that survived a float32
973
+ * round-trip while leaving genuinely high-precision doubles essentially intact.
974
+ * @param v the value to round
975
+ * @returns the rounded value, or the input unchanged if it is not finite
976
+ */
977
+ function _roundFloat32Artifact(v) {
978
+ if (!Number.isFinite(v)) {
979
+ return v;
980
+ }
981
+ return parseFloat(v.toPrecision(7));
982
+ }
983
+ /**
984
+ * Read the KHR_texture_transform object stored in the source glTF JSON for a
985
+ * texture-info that lives under one of the material's extensions, creating
986
+ * empty parent objects on demand so callers can write through it. Returns
987
+ * `undefined` when the input shape is incompatible.
988
+ * @param material the source IMaterial owning the extension
989
+ * @param gltfPath the path describing where the texture-info lives
990
+ * @param createMissing when true, create missing parent objects so writes succeed
991
+ * @returns the glTF-side KHR_texture_transform object, or undefined
992
+ */
993
+ function _gltfTextureTransform(material, gltfPath, createMissing) {
994
+ if (!material) {
995
+ return undefined;
996
+ }
997
+ let extensions = material.extensions;
998
+ if (!extensions) {
999
+ if (!createMissing) {
1000
+ return undefined;
1001
+ }
1002
+ extensions = {};
1003
+ material.extensions = extensions;
1004
+ }
1005
+ let cursor = extensions[gltfPath.extensionKey];
1006
+ if (!cursor) {
1007
+ if (!createMissing) {
1008
+ return undefined;
1009
+ }
1010
+ cursor = {};
1011
+ extensions[gltfPath.extensionKey] = cursor;
1012
+ }
1013
+ for (const key of gltfPath.texturePath) {
1014
+ let next = cursor[key];
1015
+ if (!next) {
1016
+ if (!createMissing) {
1017
+ return undefined;
1018
+ }
1019
+ next = {};
1020
+ cursor[key] = next;
1021
+ }
1022
+ cursor = next;
1023
+ }
1024
+ if (!cursor.extensions) {
1025
+ if (!createMissing) {
1026
+ return undefined;
1027
+ }
1028
+ cursor.extensions = {};
1029
+ }
1030
+ let xform = cursor.extensions.KHR_texture_transform;
1031
+ if (!xform) {
1032
+ if (!createMissing) {
1033
+ return undefined;
1034
+ }
1035
+ xform = {};
1036
+ cursor.extensions.KHR_texture_transform = xform;
1037
+ }
1038
+ return xform;
1039
+ }
1040
+ function GenerateTextureMap(textureType, textureInObject, gltfPath) {
750
1041
  return {
751
1042
  offset: {
752
1043
  componentsCount: 2,
@@ -754,12 +1045,29 @@ function GenerateTextureMap(textureType, textureInObject) {
754
1045
  type: "Vector2",
755
1046
  get: (material, _index, payload) => {
756
1047
  const texture = GetTexture(material, payload, textureType, textureInObject);
757
- return new Vector2(texture?.uOffset, texture?.vOffset);
1048
+ if (texture) {
1049
+ return new Vector2(texture.uOffset, texture.vOffset);
1050
+ }
1051
+ if (gltfPath) {
1052
+ const xform = _gltfTextureTransform(material, gltfPath, false);
1053
+ const o = xform?.offset;
1054
+ return new Vector2(o?.[0] ?? 0, o?.[1] ?? 0);
1055
+ }
1056
+ return new Vector2(0, 0);
758
1057
  },
759
1058
  getTarget: GetMaterial,
760
1059
  set: (value, material, _index, payload) => {
761
1060
  const texture = GetTexture(material, payload, textureType, textureInObject);
762
- ((texture.uOffset = value.x), (texture.vOffset = value.y));
1061
+ if (texture) {
1062
+ texture.uOffset = value.x;
1063
+ texture.vOffset = value.y;
1064
+ }
1065
+ if (gltfPath) {
1066
+ const xform = _gltfTextureTransform(material, gltfPath, true);
1067
+ if (xform) {
1068
+ xform.offset = [value.x, value.y];
1069
+ }
1070
+ }
763
1071
  },
764
1072
  getPropertyName: [
765
1073
  () => `${textureType}${textureInObject ? "." + textureInObject : ""}.uOffset`,
@@ -768,9 +1076,30 @@ function GenerateTextureMap(textureType, textureInObject) {
768
1076
  },
769
1077
  rotation: {
770
1078
  type: "number",
771
- get: (material, _index, payload) => GetTexture(material, payload, textureType, textureInObject)?.wAng,
1079
+ get: (material, _index, payload) => {
1080
+ const texture = GetTexture(material, payload, textureType, textureInObject);
1081
+ if (texture) {
1082
+ return texture.wAng;
1083
+ }
1084
+ if (gltfPath) {
1085
+ const xform = _gltfTextureTransform(material, gltfPath, false);
1086
+ return xform?.rotation ?? 0;
1087
+ }
1088
+ return 0;
1089
+ },
772
1090
  getTarget: GetMaterial,
773
- set: (value, material, _index, payload) => (GetTexture(material, payload, textureType, textureInObject).wAng = value),
1091
+ set: (value, material, _index, payload) => {
1092
+ const texture = GetTexture(material, payload, textureType, textureInObject);
1093
+ if (texture) {
1094
+ texture.wAng = value;
1095
+ }
1096
+ if (gltfPath) {
1097
+ const xform = _gltfTextureTransform(material, gltfPath, true);
1098
+ if (xform) {
1099
+ xform.rotation = value;
1100
+ }
1101
+ }
1102
+ },
774
1103
  getPropertyName: [() => `${textureType}${textureInObject ? "." + textureInObject : ""}.wAng`],
775
1104
  },
776
1105
  scale: {
@@ -778,12 +1107,29 @@ function GenerateTextureMap(textureType, textureInObject) {
778
1107
  type: "Vector2",
779
1108
  get: (material, _index, payload) => {
780
1109
  const texture = GetTexture(material, payload, textureType, textureInObject);
781
- return new Vector2(texture?.uScale, texture?.vScale);
1110
+ if (texture) {
1111
+ return new Vector2(texture.uScale, texture.vScale);
1112
+ }
1113
+ if (gltfPath) {
1114
+ const xform = _gltfTextureTransform(material, gltfPath, false);
1115
+ const s = xform?.scale;
1116
+ return new Vector2(s?.[0] ?? 1, s?.[1] ?? 1);
1117
+ }
1118
+ return new Vector2(1, 1);
782
1119
  },
783
1120
  getTarget: GetMaterial,
784
1121
  set: (value, material, index, payload) => {
785
1122
  const texture = GetTexture(material, payload, textureType, textureInObject);
786
- ((texture.uScale = value.x), (texture.vScale = value.y));
1123
+ if (texture) {
1124
+ texture.uScale = value.x;
1125
+ texture.vScale = value.y;
1126
+ }
1127
+ if (gltfPath) {
1128
+ const xform = _gltfTextureTransform(material, gltfPath, true);
1129
+ if (xform) {
1130
+ xform.scale = [value.x, value.y];
1131
+ }
1132
+ }
787
1133
  },
788
1134
  getPropertyName: [
789
1135
  () => `${textureType}${textureInObject ? "." + textureInObject : ""}.uScale`,
@@ -792,13 +1138,69 @@ function GenerateTextureMap(textureType, textureInObject) {
792
1138
  },
793
1139
  };
794
1140
  }
1141
+ const scenesTree = {
1142
+ length: _CreateLengthAccessor((scenes) => scenes.length, (scenes) => scenes),
1143
+ __array__: {
1144
+ __target__: true,
1145
+ nodes: {
1146
+ length: _CreateLengthAccessor((nodes) => nodes?.length ?? 0, (nodes) => nodes ?? []),
1147
+ __array__: {
1148
+ __target__: true,
1149
+ type: "string",
1150
+ // Indexed scene root: the underlying value is the INode index;
1151
+ // KHR_interactivity expects a ref-typed JSON Pointer string.
1152
+ get: (nodeIndex) => (typeof nodeIndex === "number" ? `/nodes/${nodeIndex}` : ""),
1153
+ getTarget: () => ({ __nodeIndex: true }),
1154
+ isReadOnly: true,
1155
+ },
1156
+ },
1157
+ },
1158
+ };
1159
+ const skinsTree = {
1160
+ length: _CreateLengthAccessor((skins) => skins.length, (skins) => skins.map((skin) => skin._data?.babylonSkeleton)),
1161
+ __array__: {
1162
+ __target__: true,
1163
+ joints: {
1164
+ length: _CreateLengthAccessor((joints) => joints?.length ?? 0, (joints) => joints ?? []),
1165
+ __array__: {
1166
+ __target__: true,
1167
+ type: "string",
1168
+ // Indexed skin joint: returns a ref to the joint node.
1169
+ get: (jointIndex) => (typeof jointIndex === "number" ? `/nodes/${jointIndex}` : ""),
1170
+ getTarget: () => ({ __nodeIndex: true }),
1171
+ isReadOnly: true,
1172
+ },
1173
+ },
1174
+ skeleton: {
1175
+ type: "string",
1176
+ // Skin's skeleton root: returns a ref to the root node, or empty
1177
+ // (null ref) when no skeleton root is declared.
1178
+ get: (skin) => {
1179
+ const skeleton = skin.skeleton;
1180
+ return typeof skeleton === "number" ? `/nodes/${skeleton}` : "";
1181
+ },
1182
+ getTarget: (skin) => skin,
1183
+ isReadOnly: true,
1184
+ },
1185
+ },
1186
+ };
795
1187
  const objectModelMapping = {
1188
+ scene: {
1189
+ __target__: true,
1190
+ type: "number",
1191
+ get: (sceneIndex) => sceneIndex ?? 0,
1192
+ getTarget: () => ({ __gltfRoot: true }),
1193
+ isReadOnly: true,
1194
+ getPropertyName: [() => "scene"],
1195
+ },
796
1196
  cameras: camerasTree,
797
1197
  nodes: nodesTree,
798
1198
  materials: materialsTree,
799
1199
  extensions: extensionsTree,
800
1200
  animations: animationsTree,
801
1201
  meshes: meshesTree,
1202
+ scenes: scenesTree,
1203
+ skins: skinsTree,
802
1204
  };
803
1205
  /**
804
1206
  * get a path-to-object converter for the given glTF tree