@needle-tools/three-animation-pointer 1.0.7 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,9 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.1.0] - 2026-06-01
8
+ - Add: support for `/nodes/{}/weights/{}` to animate individual morph target weights (works with multi-material meshes / primitives) — by @0b5vr in [#1](https://github.com/needle-tools/three-animation-pointer/pull/1)
9
+
7
10
  ## [1.0.7] - 2025-10-20
8
11
  - Fix: missing "extensions" in KHR_node_visibility
9
12
 
@@ -1,20 +1,20 @@
1
- import { SkinnedMesh as S, InterpolateDiscrete as M, InterpolateLinear as F, AnimationClip as W, PropertyBinding as C, QuaternionKeyframeTrack as w, ColorKeyframeTrack as B, VectorKeyframeTrack as D, BooleanKeyframeTrack as E, NumberKeyframeTrack as O } from "three";
1
+ import { SkinnedMesh as S, InterpolateDiscrete as w, InterpolateLinear as F, AnimationClip as M, PropertyBinding as C, QuaternionKeyframeTrack as W, ColorKeyframeTrack as B, VectorKeyframeTrack as D, BooleanKeyframeTrack as E, NumberKeyframeTrack as O } from "three";
2
2
  const d = {
3
3
  node: "node",
4
4
  material: "material",
5
5
  camera: "camera",
6
6
  light: "light"
7
- }, R = "KHR_animation_pointer", U = {
7
+ }, y = "KHR_animation_pointer", U = {
8
8
  // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
9
9
  // keyframe track will be initialized with a default interpolation type, then modified.
10
10
  CUBICSPLINE: void 0,
11
11
  LINEAR: F,
12
- STEP: M
12
+ STEP: w
13
13
  };
14
14
  class G {
15
15
  /** @type {import("three/examples/jsm/loaders/GLTFLoader").GLTFParser} */
16
16
  constructor(a) {
17
- this.name = R, this.parser = a, this.animationPointerResolver = null;
17
+ this.name = y, this.parser = a, this.animationPointerResolver = null;
18
18
  }
19
19
  /**
20
20
  * @param {import("..").AnimationPointerResolver | null} animationPointerResolver
@@ -28,13 +28,13 @@ class G {
28
28
  return this.parser.getDependency("node", r);
29
29
  }
30
30
  loadAnimationTargetFromChannelWithAnimationPointer(a) {
31
- var u;
31
+ var m;
32
32
  j();
33
- const t = a.target, r = t.extensions && t.extensions[R] && t.path && t.path === "pointer";
33
+ const t = a.target, r = t.extensions && t.extensions[y] && t.path && t.path === "pointer";
34
34
  if (!r) return null;
35
35
  let e, n = d.node, s;
36
36
  if (r) {
37
- const h = t.extensions[R];
37
+ const h = t.extensions[y];
38
38
  let o = h.pointer;
39
39
  if (!o) {
40
40
  console.warn("Invalid path", h, t);
@@ -46,7 +46,7 @@ class G {
46
46
  }
47
47
  switch (n) {
48
48
  case d.material:
49
- const T = ("/materials/" + s.toString() + "/").length, f = o.substring(0, T);
49
+ const T = ("/materials/" + s.toString() + "/").length, g = o.substring(0, T);
50
50
  switch (e = o.substring(T), e) {
51
51
  case "pbrMetallicRoughness/baseColorFactor":
52
52
  e = "color";
@@ -130,32 +130,36 @@ class G {
130
130
  e = "specularColor";
131
131
  break;
132
132
  }
133
- o = f + e;
133
+ o = g + e;
134
134
  break;
135
135
  case d.node:
136
- const k = ("/nodes/" + s.toString() + "/").length, l = o.substring(0, k);
137
- switch (e = o.substring(k), e) {
138
- case "translation":
139
- e = "position";
140
- break;
141
- case "rotation":
142
- e = "quaternion";
143
- break;
144
- case "scale":
145
- e = "scale";
146
- break;
147
- case "weights":
148
- e = "morphTargetInfluences";
149
- break;
150
- case "extensions/KHR_node_visibility/visible":
151
- e = "visible";
152
- break;
153
- }
154
- o = l + e;
136
+ const _ = ("/nodes/" + s.toString() + "/").length, b = o.substring(0, _);
137
+ if (e = o.substring(_), e.startsWith("weights/")) {
138
+ const f = e.substring(8);
139
+ /^\d+$/.test(f) ? e = "morphTargetInfluences[" + f + "]" : console.warn(y + ": invalid weight index", e, o);
140
+ } else
141
+ switch (e) {
142
+ case "translation":
143
+ e = "position";
144
+ break;
145
+ case "rotation":
146
+ e = "quaternion";
147
+ break;
148
+ case "scale":
149
+ e = "scale";
150
+ break;
151
+ case "weights":
152
+ e = "morphTargetInfluences";
153
+ break;
154
+ case "extensions/KHR_node_visibility/visible":
155
+ e = "visible";
156
+ break;
157
+ }
158
+ o = b + e;
155
159
  break;
156
160
  case d.light:
157
- const c = ("/extensions/KHR_lights_punctual/lights/" + s.toString() + "/").length;
158
- switch (e = o.substring(c), e) {
161
+ const u = ("/extensions/KHR_lights_punctual/lights/" + s.toString() + "/").length;
162
+ switch (e = o.substring(u), e) {
159
163
  case "color":
160
164
  break;
161
165
  case "intensity":
@@ -173,8 +177,8 @@ class G {
173
177
  o = "/lights/" + s.toString() + "/" + e;
174
178
  break;
175
179
  case d.camera:
176
- const g = ("/cameras/" + s.toString() + "/").length, b = o.substring(0, g);
177
- switch (e = o.substring(g), e) {
180
+ const l = ("/cameras/" + s.toString() + "/").length, k = o.substring(0, l);
181
+ switch (e = o.substring(l), e) {
178
182
  case "perspective/yfov":
179
183
  e = "fov";
180
184
  break;
@@ -196,10 +200,10 @@ class G {
196
200
  e = "zoom";
197
201
  break;
198
202
  }
199
- o = b + e;
203
+ o = k + e;
200
204
  break;
201
205
  }
202
- (u = this.animationPointerResolver) != null && u.resolvePath && (o = this.animationPointerResolver.resolvePath(o)), t.extensions[R].pointer = o;
206
+ (m = this.animationPointerResolver) != null && m.resolvePath && (o = this.animationPointerResolver.resolvePath(o)), t.extensions[y].pointer = o;
203
207
  }
204
208
  if (s == null || isNaN(s)) {
205
209
  console.warn("Failed resolving animation node id: " + s, t);
@@ -209,70 +213,74 @@ class G {
209
213
  return n === d.node ? i = this.parser.getDependency("node", s) : n === d.material ? i = this.parser.getDependency("material", s) : n === d.light ? i = this.parser.getDependency("light", s) : n === d.camera ? i = this.parser.getDependency("camera", s) : console.error("Unhandled type", n), i;
210
214
  }
211
215
  createAnimationTracksWithAnimationPointer(a, t, r, e, n) {
212
- if (!(n.extensions && n.extensions[R] && n.path && n.path === "pointer")) return null;
213
- let i = n.extensions[R].pointer;
216
+ var b;
217
+ if (!(n.extensions && n.extensions[y] && n.path && n.path === "pointer")) return null;
218
+ let i = n.extensions[y].pointer;
214
219
  if (!i) return null;
215
- const u = [];
220
+ const m = [];
216
221
  i = i.replaceAll("/", ".");
217
222
  const h = i.split(".");
218
223
  var T = a.name !== void 0 && a.name !== null ? a.name : a.uuid;
219
- if (h[2] = T, h[3] === "morphTargetInfluences" && a.type === "Group") {
220
- for (const l of a.children)
221
- l instanceof S && l.morphTargetInfluences && (h[3] = l.name, h[4] = "morphTargetInfluences", k(this.parser));
222
- return u;
224
+ if (h[2] = T, (b = h[3]) != null && b.startsWith("morphTargetInfluences")) {
225
+ const u = h[3];
226
+ if (a.type === "Group") {
227
+ for (const l of a.children)
228
+ l instanceof S && l.morphTargetInfluences && (h[3] = l.name, h[4] = u, _(this.parser));
229
+ return m;
230
+ }
223
231
  }
224
- k(this.parser);
225
- function f(l, c) {
232
+ _(this.parser);
233
+ function g(u, l) {
226
234
  try {
227
- const g = c.split(".").filter(Boolean), m = g[g.length - 1].replace(/\[.*\]$/, "");
228
- return m in l ? typeof l[m] == "boolean" : !1;
235
+ const k = l.split(".").filter(Boolean), c = k[k.length - 1].replace(/\[.*\]$/, "");
236
+ return c in u ? typeof u[c] == "boolean" : !1;
229
237
  } catch {
230
238
  return !1;
231
239
  }
232
240
  }
233
- function k(l) {
241
+ function _(u) {
234
242
  i = h.join(".");
235
- let c, g = !1;
243
+ let l, k = !1;
236
244
  switch (r.itemSize) {
237
245
  case 1:
238
246
  Object.prototype.toString.call(r.array) === "[object Uint8Array]" && // UNSIGNED_BYTE as a strong hint
239
- f(a, i) ? (c = E, g = !0) : c = O;
247
+ g(a, i) ? (l = E, k = !0) : l = O;
240
248
  break;
241
249
  case 2:
242
250
  case 3:
243
- c = D;
251
+ l = D;
244
252
  break;
245
253
  case 4:
246
- i.endsWith(".quaternion") ? c = w : c = B;
254
+ i.endsWith(".quaternion") ? l = W : l = B;
247
255
  break;
248
256
  }
249
- if (!c) {
257
+ if (!l) {
250
258
  console.warn("Unsupported output accessor format", r);
251
259
  return;
252
260
  }
253
- const b = e.interpolation !== void 0 ? U[e.interpolation] : F;
254
- let m = l._getArrayFromAccessor(r);
255
- i.endsWith(".fov") && (m = m.map((_) => _ / Math.PI * 180)), g && (m = m.map((_) => _ > 0));
256
- const p = new c(
261
+ const f = e.interpolation !== void 0 ? U[e.interpolation] : F;
262
+ let c = u._getArrayFromAccessor(r);
263
+ i.endsWith(".fov") && (c = c.map((p) => p / Math.PI * 180)), k && (c = c.map((p) => p > 0));
264
+ const R = new l(
257
265
  i,
258
266
  t.array,
259
- m,
260
- b
267
+ c,
268
+ f
261
269
  );
262
- if (b === "CUBICSPLINE" && l._createCubicSplineTrackInterpolant(p), u.push(p), i && r.itemSize === 4 && i.startsWith(".materials.") && i.endsWith(".color")) {
263
- const _ = new Float32Array(m.length / 4);
264
- for (let y = 0, P = m.length / 4; y < P; y += 1)
265
- _[y] = m[y * 4 + 3];
266
- const x = new c(
270
+ if (f === "CUBICSPLINE" && u._createCubicSplineTrackInterpolant(R), m.push(R), i && r.itemSize === 4 && i.startsWith(".materials.") && i.endsWith(".color")) {
271
+ const p = new Float32Array(c.length / 4);
272
+ for (let x = 0, A = c.length / 4; x < A; x += 1)
273
+ p[x] = c[x * 4 + 3];
274
+ const v = new l(
267
275
  i.replace(".color", ".opacity"),
268
276
  t.array,
269
- _,
270
- b
277
+ p,
278
+ f
271
279
  );
272
- b === "CUBICSPLINE" && l._createCubicSplineTrackInterpolant(p), u.push(x);
280
+ f === "CUBICSPLINE" && u._createCubicSplineTrackInterpolant(R), m.push(v);
273
281
  }
274
282
  }
275
- return u;
283
+ return m;
276
284
  }
277
285
  _tryResolveTargetId(a, t) {
278
286
  let r = "";
@@ -285,30 +293,30 @@ class G {
285
293
  * @return {Promise<AnimationClip>}
286
294
  */
287
295
  loadAnimation(a) {
288
- const t = this, r = this.parser.json, e = this.parser, n = r.animations[a], s = n.name ? n.name : "animation_" + a, i = [], u = [], h = [], o = [], T = [];
289
- for (let f = 0, k = n.channels.length; f < k; f++) {
290
- const l = n.channels[f], c = n.samplers[l.sampler], g = l.target, b = n.parameters !== void 0 ? n.parameters[c.input] : c.input, m = n.parameters !== void 0 ? n.parameters[c.output] : c.output;
291
- let p = t.loadAnimationTargetFromChannelWithAnimationPointer(l);
292
- p || (p = t.loadAnimationTargetFromChannel(l)), i.push(p), u.push(e.getDependency("accessor", b)), h.push(e.getDependency("accessor", m)), o.push(c), T.push(g);
296
+ const t = this, r = this.parser.json, e = this.parser, n = r.animations[a], s = n.name ? n.name : "animation_" + a, i = [], m = [], h = [], o = [], T = [];
297
+ for (let g = 0, _ = n.channels.length; g < _; g++) {
298
+ const b = n.channels[g], u = n.samplers[b.sampler], l = b.target, k = n.parameters !== void 0 ? n.parameters[u.input] : u.input, f = n.parameters !== void 0 ? n.parameters[u.output] : u.output;
299
+ let c = t.loadAnimationTargetFromChannelWithAnimationPointer(b);
300
+ c || (c = t.loadAnimationTargetFromChannel(b)), i.push(c), m.push(e.getDependency("accessor", k)), h.push(e.getDependency("accessor", f)), o.push(u), T.push(l);
293
301
  }
294
302
  return Promise.all([
295
303
  Promise.all(i),
296
- Promise.all(u),
304
+ Promise.all(m),
297
305
  Promise.all(h),
298
306
  Promise.all(o),
299
307
  Promise.all(T)
300
- ]).then(function(f) {
301
- const k = f[0], l = f[1], c = f[2], g = f[3], b = f[4], m = [];
302
- for (let p = 0, _ = k.length; p < _; p++) {
303
- const x = k[p], y = l[p], P = c[p], K = g[p], N = b[p];
304
- if (x === void 0) continue;
305
- x.updateMatrix && (x.updateMatrix(), x.matrixAutoUpdate = !0);
306
- let v = t.createAnimationTracksWithAnimationPointer(x, y, P, K, N);
307
- if (v || (v = e._createAnimationTracks(x, y, P, K, N)), v)
308
- for (let A = 0; A < v.length; A++)
309
- m.push(v[A]);
308
+ ]).then(function(g) {
309
+ const _ = g[0], b = g[1], u = g[2], l = g[3], k = g[4], f = [];
310
+ for (let c = 0, R = _.length; c < R; c++) {
311
+ const p = _[c], v = b[c], x = u[c], A = l[c], N = k[c];
312
+ if (p === void 0) continue;
313
+ p.updateMatrix && (p.updateMatrix(), p.matrixAutoUpdate = !0);
314
+ let I = t.createAnimationTracksWithAnimationPointer(p, v, x, A, N);
315
+ if (I || (I = e._createAnimationTracks(p, v, x, A, N)), I)
316
+ for (let K = 0; K < I.length; K++)
317
+ f.push(I[K]);
310
318
  }
311
- return new W(s, void 0, m);
319
+ return new M(s, void 0, f);
312
320
  });
313
321
  }
314
322
  }
@@ -316,9 +324,9 @@ let H = !1, L = null;
316
324
  function j() {
317
325
  if (H) return;
318
326
  H = !0;
319
- const I = L || (L = C.findNode);
327
+ const P = L || (L = C.findNode);
320
328
  C.findNode = function(a, t) {
321
- if (!t) return I(a, t);
329
+ if (!t) return P(a, t);
322
330
  if (t.startsWith(".materials.")) {
323
331
  const r = t.substring(11).substring(t.indexOf(".")), e = r.indexOf("."), n = e < 0 ? r : r.substring(0, e);
324
332
  let s = null;
@@ -333,21 +341,21 @@ function j() {
333
341
  if (s.length == 36)
334
342
  e = a.getObjectByProperty("uuid", s);
335
343
  else if (e && e[s]) {
336
- const u = Number.parseInt(s);
344
+ const m = Number.parseInt(s);
337
345
  let h = s;
338
- u >= 0 && (h = u), e = e[h];
346
+ m >= 0 && (h = m), e = e[h];
339
347
  } else {
340
- const u = a.getObjectByName(s);
341
- u && (e = u);
348
+ const m = a.getObjectByName(s);
349
+ m && (e = m);
342
350
  }
343
351
  }
344
352
  if (!e) {
345
- const n = I(a, r[2]);
346
- return n || console.warn(R + ": Property binding not found", t, a, a.name, r), n;
353
+ const n = P(a, r[2]);
354
+ return n || console.warn(y + ": Property binding not found", t, a, a.name, r), n;
347
355
  }
348
356
  return e;
349
357
  }
350
- return I(a, t);
358
+ return P(a, t);
351
359
  };
352
360
  }
353
361
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/three-animation-pointer",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "KHR_animation_pointer support for three.js",
5
5
  "type": "module",
6
6
  "main": "dist/three-animation-pointer.js",
@@ -247,23 +247,37 @@ export class GLTFAnimationPointerExtension {
247
247
  const pathStartNode = path.substring( 0, pathIndexNode );
248
248
  targetProperty = path.substring( pathIndexNode );
249
249
 
250
- switch ( targetProperty ) {
250
+ if ( targetProperty.startsWith( 'weights/' ) ) {
251
+ // `/nodes/{}/weights/{}` - control individual morph weights
252
+ const weightIndex = targetProperty.substring( 'weights/'.length );
253
+ if ( /^\d+$/.test( weightIndex ) ) {
251
254
 
252
- case 'translation':
253
- targetProperty = 'position';
254
- break;
255
- case 'rotation':
256
- targetProperty = 'quaternion';
257
- break;
258
- case 'scale':
259
- targetProperty = 'scale';
260
- break;
261
- case 'weights':
262
- targetProperty = 'morphTargetInfluences';
263
- break;
264
- case "extensions/KHR_node_visibility/visible":
265
- targetProperty = 'visible';
266
- break;
255
+ targetProperty = 'morphTargetInfluences[' + weightIndex + ']';
256
+
257
+ } else {
258
+
259
+ console.warn( KHR_ANIMATION_POINTER + ': invalid weight index', targetProperty, path );
260
+
261
+ }
262
+ } else {
263
+ switch ( targetProperty ) {
264
+
265
+ case 'translation':
266
+ targetProperty = 'position';
267
+ break;
268
+ case 'rotation':
269
+ targetProperty = 'quaternion';
270
+ break;
271
+ case 'scale':
272
+ targetProperty = 'scale';
273
+ break;
274
+ case 'weights':
275
+ targetProperty = 'morphTargetInfluences';
276
+ break;
277
+ case "extensions/KHR_node_visibility/visible":
278
+ targetProperty = 'visible';
279
+ break;
280
+ }
267
281
  }
268
282
 
269
283
  path = pathStartNode + targetProperty;
@@ -385,8 +399,11 @@ export class GLTFAnimationPointerExtension {
385
399
 
386
400
  // specially handle the morphTargetInfluences property for multi-material meshes
387
401
  // in which case the target object is a Group and the children are the actual targets
402
+ // Note that there is also morphTargetInfluences[i] for individual morph targets
388
403
  // see NE-3311
389
- if ( parts[ 3 ] === 'morphTargetInfluences' ) {
404
+ if ( parts[ 3 ]?.startsWith( 'morphTargetInfluences' ) ) {
405
+
406
+ const property = parts[ 3 ];
390
407
 
391
408
  if ( node.type === 'Group' ) {
392
409
 
@@ -399,7 +416,7 @@ export class GLTFAnimationPointerExtension {
399
416
  if ( ch instanceof SkinnedMesh && ch.morphTargetInfluences ) {
400
417
 
401
418
  parts[ 3 ] = ch.name;
402
- parts[ 4 ] = 'morphTargetInfluences';
419
+ parts[ 4 ] = property;
403
420
  __createTrack( this.parser );
404
421
 
405
422
  }