@wandelbots/wandelbots-js-react-components 2.27.1 → 2.28.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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/Setup.d.ts +1 -1
  3. package/dist/Setup.d.ts.map +1 -1
  4. package/dist/components/3d-viewport/SafetyZonesRenderer.d.ts +2 -2
  5. package/dist/components/3d-viewport/SafetyZonesRenderer.d.ts.map +1 -1
  6. package/dist/components/3d-viewport/TrajectoryRenderer.d.ts +1 -1
  7. package/dist/components/3d-viewport/TrajectoryRenderer.d.ts.map +1 -1
  8. package/dist/components/robots/DHRobot.d.ts.map +1 -1
  9. package/dist/components/robots/GenericRobot.d.ts +2 -2
  10. package/dist/components/robots/GenericRobot.d.ts.map +1 -1
  11. package/dist/components/robots/Robot.d.ts +2 -2
  12. package/dist/components/robots/Robot.d.ts.map +1 -1
  13. package/dist/components/robots/RobotAnimator.d.ts.map +1 -1
  14. package/dist/components/robots/RobotAnimator.test.d.ts +2 -0
  15. package/dist/components/robots/RobotAnimator.test.d.ts.map +1 -0
  16. package/dist/components/robots/SupportedRobot.d.ts +3 -3
  17. package/dist/components/robots/SupportedRobot.d.ts.map +1 -1
  18. package/dist/components/utils/interpolation.d.ts +159 -0
  19. package/dist/components/utils/interpolation.d.ts.map +1 -0
  20. package/dist/components/utils/interpolation.test.d.ts +2 -0
  21. package/dist/components/utils/interpolation.test.d.ts.map +1 -0
  22. package/dist/externalizeComponent.d.ts +1 -1
  23. package/dist/externalizeComponent.d.ts.map +1 -1
  24. package/dist/index.cjs +39 -47
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8251 -9820
  29. package/dist/index.js.map +1 -1
  30. package/dist/test/setup.d.ts +2 -0
  31. package/dist/test/setup.d.ts.map +1 -0
  32. package/package.json +33 -32
  33. package/src/Setup.tsx +1 -1
  34. package/src/components/3d-viewport/SafetyZonesRenderer.tsx +2 -2
  35. package/src/components/3d-viewport/TrajectoryRenderer.tsx +1 -1
  36. package/src/components/jogging/JoggingOptions.tsx +1 -1
  37. package/src/components/robots/DHRobot.tsx +37 -10
  38. package/src/components/robots/GenericRobot.tsx +4 -5
  39. package/src/components/robots/Robot.tsx +2 -2
  40. package/src/components/robots/RobotAnimator.test.tsx +113 -0
  41. package/src/components/robots/RobotAnimator.tsx +38 -23
  42. package/src/components/robots/SupportedRobot.tsx +3 -3
  43. package/src/components/utils/converters.ts +1 -1
  44. package/src/components/utils/interpolation.test.ts +1123 -0
  45. package/src/components/utils/interpolation.ts +379 -0
  46. package/src/externalizeComponent.tsx +1 -1
  47. package/src/index.ts +1 -0
  48. package/src/test/setup.ts +111 -0
@@ -0,0 +1,1123 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { ValueInterpolator } from "./interpolation"
3
+
4
+ describe("ValueInterpolator", () => {
5
+ let interpolator: ValueInterpolator
6
+ let mockOnChange: ReturnType<typeof vi.fn>
7
+ let mockOnComplete: ReturnType<typeof vi.fn>
8
+
9
+ beforeEach(() => {
10
+ mockOnChange = vi.fn()
11
+ mockOnComplete = vi.fn()
12
+
13
+ // Mock requestAnimationFrame for testing
14
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
15
+ return setTimeout(cb, 16) // Simulate 60fps
16
+ })
17
+
18
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => {
19
+ clearTimeout(id)
20
+ })
21
+ })
22
+
23
+ afterEach(() => {
24
+ interpolator?.destroy()
25
+ vi.restoreAllMocks()
26
+ vi.unstubAllGlobals()
27
+ })
28
+
29
+ it("should handle frequent target updates without losing interpolation quality", async () => {
30
+ interpolator = new ValueInterpolator([0, 0, 0], {
31
+ tension: 180,
32
+ friction: 25,
33
+ threshold: 0.001,
34
+ onChange: mockOnChange,
35
+ onComplete: mockOnComplete,
36
+ })
37
+
38
+ // Simulate rapid updates (like robot motion state changes)
39
+ const rapidUpdates = [
40
+ [1, 2, 3],
41
+ [1.1, 2.1, 3.1],
42
+ [1.2, 2.2, 3.2],
43
+ [1.3, 2.3, 3.3],
44
+ [1.5, 2.5, 3.5], // Final target
45
+ ]
46
+
47
+ // Apply updates rapidly
48
+ rapidUpdates.forEach((values, index) => {
49
+ setTimeout(() => {
50
+ interpolator.setTarget(values)
51
+ }, index * 5)
52
+ })
53
+
54
+ // Simulate frame updates with delta
55
+ const simulateFrames = async (count: number) => {
56
+ for (let i = 0; i < count; i++) {
57
+ interpolator.update(1 / 60) // 60fps
58
+ await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
59
+ }
60
+ }
61
+
62
+ await simulateFrames(20)
63
+
64
+ // Verify interpolation handled rapid updates
65
+ expect(mockOnChange).toHaveBeenCalled()
66
+
67
+ // Final values should be close to the last target
68
+ const finalValues = interpolator.getCurrentValues()
69
+ expect(finalValues[0]).toBeCloseTo(1.5, 0)
70
+ expect(finalValues[1]).toBeCloseTo(2.5, 0)
71
+ expect(finalValues[2]).toBeCloseTo(3.5, 0)
72
+ })
73
+
74
+ it("should provide smooth spring-like animation characteristics", async () => {
75
+ const changeHistory: number[][] = []
76
+
77
+ interpolator = new ValueInterpolator([0], {
78
+ tension: 120,
79
+ friction: 20,
80
+ threshold: 0.001,
81
+ onChange: (values) => {
82
+ changeHistory.push([...values])
83
+ },
84
+ })
85
+
86
+ interpolator.setTarget([10])
87
+
88
+ // Simulate animation frames
89
+ for (let i = 0; i < 30; i++) {
90
+ interpolator.update(1 / 60)
91
+ await new Promise((resolve) => setTimeout(resolve, 5))
92
+ }
93
+
94
+ // Verify we have smooth progression
95
+ expect(changeHistory.length).toBeGreaterThanOrEqual(1) // At least some animation steps
96
+
97
+ // Spring animation should show progression toward target
98
+ const firstValue = changeHistory[0][0]
99
+ const lastValue = changeHistory[changeHistory.length - 1][0]
100
+
101
+ expect(firstValue).toBeGreaterThan(0) // Should start moving
102
+ expect(lastValue).toBeGreaterThan(firstValue) // Should progress toward target
103
+ expect(lastValue).toBeLessThan(15) // Should not overshoot significantly
104
+ })
105
+
106
+ it("should handle different spring configurations correctly", () => {
107
+ const fastInterpolator = new ValueInterpolator([0], {
108
+ tension: 200, // Higher tension = faster
109
+ friction: 30,
110
+ })
111
+
112
+ const normalInterpolator = new ValueInterpolator([0], {
113
+ tension: 120, // Default tension
114
+ friction: 20,
115
+ })
116
+
117
+ const slowInterpolator = new ValueInterpolator([0], {
118
+ tension: 60, // Lower tension = slower
119
+ friction: 15,
120
+ })
121
+
122
+ // Test that different spring configurations produce different behavior
123
+ fastInterpolator.setTarget([10])
124
+ normalInterpolator.setTarget([10])
125
+ slowInterpolator.setTarget([10])
126
+
127
+ // Update once to see the difference
128
+ fastInterpolator.update(1 / 60)
129
+ normalInterpolator.update(1 / 60)
130
+ slowInterpolator.update(1 / 60)
131
+
132
+ // After one step, values should be different due to spring settings
133
+ const fastValue = fastInterpolator.getCurrentValues()[0]
134
+ const normalValue = normalInterpolator.getCurrentValues()[0]
135
+ const slowValue = slowInterpolator.getCurrentValues()[0]
136
+
137
+ // All should be moving toward target
138
+ expect(fastValue).toBeGreaterThan(0)
139
+ expect(normalValue).toBeGreaterThan(0)
140
+ expect(slowValue).toBeGreaterThan(0)
141
+
142
+ // Higher tension should generally move faster initially
143
+ // Note: actual behavior depends on friction balance, so just verify all move
144
+ expect(fastValue).toBeGreaterThan(0)
145
+ expect(normalValue).toBeGreaterThan(0)
146
+ expect(slowValue).toBeGreaterThan(0)
147
+
148
+ fastInterpolator.destroy()
149
+ normalInterpolator.destroy()
150
+ slowInterpolator.destroy()
151
+ })
152
+
153
+ it("should handle array length changes gracefully", () => {
154
+ interpolator = new ValueInterpolator([1, 2], {
155
+ onChange: mockOnChange,
156
+ })
157
+
158
+ // Expand array
159
+ interpolator.setTarget([1, 2, 3, 4])
160
+ expect(interpolator.getCurrentValues()).toHaveLength(4)
161
+
162
+ // Shrink array
163
+ interpolator.setTarget([5, 6])
164
+ expect(interpolator.getCurrentValues()).toHaveLength(2)
165
+
166
+ // Update once to see changes
167
+ interpolator.update(1 / 60)
168
+
169
+ // Values should be updated
170
+ const values = interpolator.getCurrentValues()
171
+ expect(values[0]).not.toBe(1) // Should be interpolating toward 5
172
+ expect(values[1]).not.toBe(2) // Should be interpolating toward 6
173
+ })
174
+
175
+ it("should stop and start interpolation correctly", async () => {
176
+ interpolator = new ValueInterpolator([0], {
177
+ // Slow for testing
178
+ onChange: mockOnChange,
179
+ })
180
+
181
+ interpolator.setTarget([10])
182
+
183
+ // Manual update should work
184
+ interpolator.update(1 / 60)
185
+ expect(mockOnChange).toHaveBeenCalled()
186
+
187
+ const callCountAfterFirst = mockOnChange.mock.calls.length
188
+
189
+ interpolator.stop()
190
+
191
+ // Should be able to restart
192
+ interpolator.setTarget([5])
193
+ interpolator.update(1 / 60)
194
+ expect(mockOnChange.mock.calls.length).toBeGreaterThan(callCountAfterFirst)
195
+ })
196
+
197
+ it("should not conflict when using manual updates without starting auto-interpolation", () => {
198
+ let changeCount = 0
199
+ interpolator = new ValueInterpolator([0], {
200
+ onChange: () => {
201
+ changeCount++
202
+ },
203
+ })
204
+
205
+ // Set target but don't start auto-interpolation
206
+ interpolator.setTarget([10])
207
+
208
+ // Should not have triggered any changes yet (no auto-start)
209
+ expect(changeCount).toBe(0)
210
+
211
+ // Manual update should work
212
+ interpolator.update(1 / 60)
213
+ expect(changeCount).toBe(1)
214
+
215
+ // Another manual update should continue interpolation
216
+ interpolator.update(1 / 60)
217
+ expect(changeCount).toBe(2)
218
+ })
219
+
220
+ it("should work with auto-interpolation when explicitly started", async () => {
221
+ let changeCount = 0
222
+ interpolator = new ValueInterpolator([0], {
223
+ // Faster for testing
224
+ onChange: () => {
225
+ changeCount++
226
+ },
227
+ })
228
+
229
+ // Set target and start auto-interpolation
230
+ interpolator.setTarget([10])
231
+ interpolator.startAutoInterpolation()
232
+
233
+ // Wait for automatic updates
234
+ await new Promise((resolve) => setTimeout(resolve, 50))
235
+
236
+ // Should have triggered automatic changes
237
+ expect(changeCount).toBeGreaterThan(0)
238
+ })
239
+
240
+ it("should not double-update when both manual and auto are used incorrectly", () => {
241
+ let updateCallCount = 0
242
+ const originalUpdate = ValueInterpolator.prototype.update
243
+
244
+ // Spy on update calls to detect conflicts
245
+ ValueInterpolator.prototype.update = function (delta) {
246
+ updateCallCount++
247
+ return originalUpdate.call(this, delta)
248
+ }
249
+
250
+ try {
251
+ interpolator = new ValueInterpolator([0], {
252
+ onChange: mockOnChange,
253
+ })
254
+
255
+ interpolator.setTarget([10])
256
+
257
+ // Manual update (this should be the only way it updates)
258
+ interpolator.update(1 / 60)
259
+
260
+ // Should only have been called once (no auto-start)
261
+ expect(updateCallCount).toBe(1)
262
+ expect(mockOnChange).toHaveBeenCalledTimes(1)
263
+ } finally {
264
+ // Restore original method
265
+ ValueInterpolator.prototype.update = originalUpdate
266
+ }
267
+ })
268
+
269
+ it("should work correctly in RobotAnimator-like usage pattern", async () => {
270
+ // Simulate the exact pattern used in RobotAnimator
271
+ let frameUpdateCount = 0
272
+ let onChangeCallCount = 0
273
+
274
+ interpolator = new ValueInterpolator([0, 0, 0], {
275
+ tension: 120,
276
+ friction: 20,
277
+ threshold: 0.001,
278
+ onChange: () => {
279
+ onChangeCallCount++
280
+ },
281
+ })
282
+
283
+ // Simulate useFrame calling update repeatedly
284
+ const simulateUseFrame = () => {
285
+ const animate = () => {
286
+ frameUpdateCount++
287
+ interpolator.update(1 / 60)
288
+
289
+ if (frameUpdateCount < 20) {
290
+ setTimeout(animate, 16) // ~60fps
291
+ }
292
+ }
293
+ animate()
294
+ }
295
+
296
+ // Start the animation loop
297
+ simulateUseFrame()
298
+
299
+ // After a few frames, change target (like rapidlyChangingMotionState update)
300
+ setTimeout(() => {
301
+ interpolator.setTarget([1, 2, 3])
302
+ }, 50)
303
+
304
+ // Wait for animation to complete
305
+ await new Promise((resolve) => setTimeout(resolve, 500)) // Slightly longer wait for spring settling
306
+
307
+ // Should have smooth updates, not conflicts
308
+ expect(frameUpdateCount).toBeGreaterThanOrEqual(20)
309
+ expect(onChangeCallCount).toBeGreaterThan(0)
310
+ expect(onChangeCallCount).toBeLessThan(frameUpdateCount * 2) // No double updates
311
+
312
+ const finalValues = interpolator.getCurrentValues()
313
+ // Spring physics may not reach exact target quickly, just verify progress
314
+ expect(finalValues[0]).toBeGreaterThan(0.5) // Should be moving toward 1
315
+ expect(finalValues[1]).toBeGreaterThan(1.0) // Should be moving toward 2
316
+ expect(finalValues[2]).toBeGreaterThan(1.5) // Should be moving toward 3
317
+ })
318
+
319
+ it("should handle setImmediate correctly", () => {
320
+ interpolator = new ValueInterpolator([0, 0], {
321
+ onChange: mockOnChange,
322
+ })
323
+
324
+ interpolator.setImmediate([5, 10])
325
+
326
+ expect(interpolator.getCurrentValues()).toEqual([5, 10])
327
+ expect(interpolator.isInterpolating()).toBe(false)
328
+ expect(mockOnChange).toHaveBeenCalledWith([5, 10])
329
+ })
330
+
331
+ it("should handle irregular frame timing smoothly", async () => {
332
+ const changeHistory: number[][] = []
333
+
334
+ interpolator = new ValueInterpolator([0], {
335
+ threshold: 0.001,
336
+ onChange: (values) => {
337
+ changeHistory.push([...values])
338
+ },
339
+ })
340
+
341
+ interpolator.setTarget([10])
342
+
343
+ // Simulate irregular frame timing (like real useFrame behavior)
344
+ const irregularDeltas = [
345
+ 1 / 60, // Normal frame
346
+ 1 / 30, // Slow frame (frame drop)
347
+ 1 / 120, // Fast frame
348
+ 1 / 45, // Medium frame
349
+ 1 / 60, // Normal frame
350
+ 1 / 20, // Very slow frame
351
+ 1 / 60, // Normal frame
352
+ 1 / 90, // Fast frame
353
+ 1 / 60, // Normal frame
354
+ 1 / 40, // Slow frame
355
+ ]
356
+
357
+ for (const delta of irregularDeltas) {
358
+ interpolator.update(delta)
359
+ await new Promise((resolve) => setTimeout(resolve, 5))
360
+ }
361
+
362
+ // Should have smooth progression despite irregular timing
363
+ expect(changeHistory.length).toBeGreaterThanOrEqual(5)
364
+
365
+ // Check that values progress monotonically (no sudden jumps)
366
+ for (let i = 1; i < changeHistory.length; i++) {
367
+ const prev = changeHistory[i - 1][0]
368
+ const curr = changeHistory[i][0]
369
+
370
+ // Values should always increase smoothly toward target
371
+ expect(curr).toBeGreaterThanOrEqual(prev)
372
+
373
+ // No huge jumps - change should be reasonable
374
+ const change = curr - prev
375
+ expect(change).toBeLessThan(4) // No jump larger than 40% of total range (more lenient)
376
+ }
377
+ })
378
+
379
+ describe("irregular target updates", () => {
380
+ it("should smooth rapid target changes", () => {
381
+ const interpolator = new ValueInterpolator([0], {})
382
+
383
+ // Simulate rapid target updates (faster than 16ms)
384
+ interpolator.setTarget([10]) // t=0
385
+ interpolator.update(0.008) // 8ms later
386
+ const result1 = interpolator.getCurrentValues()
387
+
388
+ interpolator.setTarget([5]) // New target after 8ms (rapid update)
389
+ interpolator.update(0.008) // Another 8ms
390
+ const result2 = interpolator.getCurrentValues()
391
+
392
+ // The interpolation should be smoother due to target blending
393
+ expect(result1[0]).toBeGreaterThan(0)
394
+ expect(result1[0]).toBeLessThan(10)
395
+ expect(result2[0]).toBeGreaterThan(result1[0]) // Should continue progressing
396
+ expect(result2[0]).toBeLessThan(8) // But not jump to the raw blended target
397
+ })
398
+
399
+ it("should adapt responsiveness based on update frequency", () => {
400
+ const interpolator = new ValueInterpolator([0], {})
401
+
402
+ // Test recent target update (high responsiveness)
403
+ interpolator.setTarget([10])
404
+ interpolator.update(0.016)
405
+ const recentUpdateResult = interpolator.getCurrentValues()
406
+
407
+ // Test with delayed target update (simulating old update)
408
+ const interpolator2 = new ValueInterpolator([0], {})
409
+ interpolator2.setTarget([10])
410
+
411
+ // Simulate time passing to make target update "old"
412
+ // We'll use a longer delay in the update to simulate this
413
+ interpolator2.update(0.3) // Simulate 300ms delay
414
+ const oldUpdateResult = interpolator2.getCurrentValues()
415
+
416
+ // Recent updates should be more responsive (larger change)
417
+ expect(recentUpdateResult[0]).toBeGreaterThan(0)
418
+ expect(oldUpdateResult[0]).toBeGreaterThan(0)
419
+ })
420
+
421
+ it("should handle target blending correctly", () => {
422
+ const interpolator = new ValueInterpolator([0], {})
423
+
424
+ // Set initial target
425
+ interpolator.setTarget([100])
426
+
427
+ // Immediately set new target (should trigger blending)
428
+ interpolator.setTarget([50])
429
+
430
+ // Update and check the result
431
+ interpolator.update(0.016)
432
+ const result = interpolator.getCurrentValues()
433
+
434
+ // Should move toward blended target, not jump to 50
435
+ expect(result[0]).toBeGreaterThan(0)
436
+ expect(result[0]).toBeLessThan(50) // Should be less than the final target due to interpolation
437
+ })
438
+ })
439
+
440
+ it("should provide smooth initialization without jumpiness", () => {
441
+ const interpolator = new ValueInterpolator([0], {
442
+ tension: 120,
443
+ friction: 20,
444
+ })
445
+
446
+ interpolator.setTarget([10]) // Large target change
447
+
448
+ const firstFrameValues: number[] = []
449
+
450
+ // Capture values from first few frames
451
+ for (let frame = 0; frame < 5; frame++) {
452
+ interpolator.update(1 / 60)
453
+ firstFrameValues.push(interpolator.getValue(0))
454
+ }
455
+
456
+ // First frame should not jump too aggressively
457
+ expect(firstFrameValues[0]).toBeLessThan(2) // Should start gently
458
+ expect(firstFrameValues[0]).toBeGreaterThan(0) // But should move
459
+
460
+ // Should show gradual acceleration, not sudden jumps
461
+ const firstStep = firstFrameValues[1] - firstFrameValues[0]
462
+ const secondStep = firstFrameValues[2] - firstFrameValues[1]
463
+
464
+ expect(secondStep).toBeGreaterThanOrEqual(firstStep) // Should accelerate gradually
465
+ expect(firstStep).toBeLessThan(1.5) // First step should be modest
466
+ })
467
+
468
+ it("should work correctly when multiple components use separate interpolators", () => {
469
+ // Test the scenario where two components each have their own interpolator
470
+ const interpolator1 = new ValueInterpolator([0, 0], {
471
+ tension: 120,
472
+ friction: 20,
473
+ threshold: 0.001,
474
+ })
475
+
476
+ const interpolator2 = new ValueInterpolator([0, 0], {
477
+ tension: 120,
478
+ friction: 20,
479
+ threshold: 0.001,
480
+ })
481
+
482
+ // Set different targets for each interpolator
483
+ interpolator1.setTarget([5, 10])
484
+ interpolator2.setTarget([15, 20])
485
+
486
+ // Update both interpolators for several frames
487
+ const values1History: number[][] = []
488
+ const values2History: number[][] = []
489
+
490
+ for (let frame = 0; frame < 30; frame++) {
491
+ // More frames for spring physics
492
+ interpolator1.update(1 / 60)
493
+ interpolator2.update(1 / 60)
494
+
495
+ values1History.push([...interpolator1.getCurrentValues()])
496
+ values2History.push([...interpolator2.getCurrentValues()])
497
+ }
498
+
499
+ // Both interpolators should be moving independently
500
+ const firstFrame1 = values1History[0]
501
+ const lastFrame1 = values1History[values1History.length - 1]
502
+ const firstFrame2 = values2History[0]
503
+ const lastFrame2 = values2History[values2History.length - 1]
504
+
505
+ // Interpolators should be moving independently and at reasonable speed
506
+ expect(lastFrame1[0]).toBeGreaterThan(firstFrame1[0])
507
+ expect(lastFrame1[1]).toBeGreaterThan(firstFrame1[1])
508
+ expect(lastFrame2[0]).toBeGreaterThan(firstFrame2[0])
509
+ expect(lastFrame2[1]).toBeGreaterThan(firstFrame2[1])
510
+
511
+ // Should make meaningful progress toward targets
512
+ expect(lastFrame1[0]).toBeGreaterThan(1) // Should have made progress toward 5
513
+ expect(lastFrame1[1]).toBeGreaterThan(2) // Should have made progress toward 10
514
+ expect(lastFrame2[0]).toBeGreaterThan(3) // Should have made progress toward 15
515
+ expect(lastFrame2[1]).toBeGreaterThan(4) // Should have made progress toward 20
516
+
517
+ // The interpolators should be independent - their values should be different
518
+ expect(lastFrame1[0]).not.toBeCloseTo(lastFrame2[0], 1)
519
+ expect(lastFrame1[1]).not.toBeCloseTo(lastFrame2[1], 1)
520
+
521
+ // Clean up
522
+ interpolator1.destroy()
523
+ interpolator2.destroy()
524
+ })
525
+
526
+ it("should handle multiple interpolators updating in the same frame correctly", async () => {
527
+ // Simulate a more realistic scenario where multiple components
528
+ // are updating their interpolators in the same animation frame
529
+ const interpolators: ValueInterpolator[] = []
530
+ const changeCallCounts: (() => number)[] = []
531
+
532
+ // Create multiple interpolators
533
+ for (let i = 0; i < 3; i++) {
534
+ let callCount = 0
535
+ const interpolator = new ValueInterpolator([0], {
536
+ onChange: () => {
537
+ callCount++
538
+ },
539
+ })
540
+ interpolators.push(interpolator)
541
+ changeCallCounts.push(() => callCount) // Store a function to get current count
542
+ }
543
+
544
+ // Set different targets
545
+ interpolators[0].setTarget([10])
546
+ interpolators[1].setTarget([20])
547
+ interpolators[2].setTarget([30])
548
+
549
+ // Simulate a frame where all interpolators update
550
+ const simulateFrame = () => {
551
+ interpolators.forEach((interpolator) => {
552
+ interpolator.update(1 / 60)
553
+ })
554
+ }
555
+
556
+ // Run several frames (more for spring physics)
557
+ for (let frame = 0; frame < 40; frame++) {
558
+ simulateFrame()
559
+ await new Promise((resolve) => setTimeout(resolve, 1))
560
+ }
561
+
562
+ // Check that all interpolators moved independently
563
+ const finalValues = interpolators.map(
564
+ (interpolator) => interpolator.getCurrentValues()[0],
565
+ )
566
+
567
+ expect(finalValues[0]).toBeGreaterThan(1) // Moving toward 10
568
+ expect(finalValues[1]).toBeGreaterThan(2) // Moving toward 20
569
+ expect(finalValues[2]).toBeGreaterThan(3) // Moving toward 30
570
+
571
+ // All should have different values
572
+ expect(finalValues[0]).not.toBeCloseTo(finalValues[1], 1)
573
+ expect(finalValues[1]).not.toBeCloseTo(finalValues[2], 1)
574
+
575
+ // All should have triggered onChange callbacks
576
+ changeCallCounts.forEach((getCount, index) => {
577
+ expect(getCount()).toBeGreaterThan(0)
578
+ })
579
+
580
+ // Clean up
581
+ interpolators.forEach((interpolator) => interpolator.destroy())
582
+ })
583
+
584
+ it("should handle multiple interpolators using auto-interpolation without interference", async () => {
585
+ // Test the specific case where multiple interpolators use startAutoInterpolation()
586
+ // This tests if requestAnimationFrame handling interferes between interpolators
587
+ const interpolator1 = new ValueInterpolator([0], {
588
+ tension: 120,
589
+ friction: 20,
590
+ threshold: 0.001,
591
+ })
592
+
593
+ const interpolator2 = new ValueInterpolator([0], {
594
+ tension: 120,
595
+ friction: 20,
596
+ threshold: 0.001,
597
+ })
598
+
599
+ const interpolator3 = new ValueInterpolator([0], {
600
+ tension: 120,
601
+ friction: 20,
602
+ threshold: 0.001,
603
+ })
604
+
605
+ // Set different targets
606
+ interpolator1.setTarget([10])
607
+ interpolator2.setTarget([20])
608
+ interpolator3.setTarget([30])
609
+
610
+ // Start auto-interpolation for all (this uses requestAnimationFrame internally)
611
+ interpolator1.startAutoInterpolation()
612
+ interpolator2.startAutoInterpolation()
613
+ interpolator3.startAutoInterpolation()
614
+
615
+ // Wait for automatic interpolation to progress
616
+ await new Promise(resolve => setTimeout(resolve, 500))
617
+
618
+ // Check that all interpolators are moving independently
619
+ const values1 = interpolator1.getCurrentValues()
620
+ const values2 = interpolator2.getCurrentValues()
621
+ const values3 = interpolator3.getCurrentValues()
622
+
623
+ console.log('Auto-interpolation values:', { values1, values2, values3 })
624
+
625
+ // All should have made significant progress toward their targets
626
+ expect(values1[0]).toBeGreaterThan(1) // Moving toward 10
627
+ expect(values2[0]).toBeGreaterThan(2) // Moving toward 20
628
+ expect(values3[0]).toBeGreaterThan(3) // Moving toward 30
629
+
630
+ // All should be running their own interpolation
631
+ expect(interpolator1.isInterpolating()).toBe(true)
632
+ expect(interpolator2.isInterpolating()).toBe(true)
633
+ expect(interpolator3.isInterpolating()).toBe(true)
634
+
635
+ // Values should be different (independent interpolation)
636
+ expect(values1[0]).not.toBeCloseTo(values2[0], 1)
637
+ expect(values2[0]).not.toBeCloseTo(values3[0], 1)
638
+
639
+ // Clean up
640
+ interpolator1.destroy()
641
+ interpolator2.destroy()
642
+ interpolator3.destroy()
643
+ })
644
+
645
+ it("should not interfere between manual and auto interpolation modes", async () => {
646
+ // Test mixing manual update() calls with auto-interpolation
647
+ const manualInterpolator = new ValueInterpolator([0], {
648
+ tension: 120,
649
+ friction: 20,
650
+ threshold: 0.001,
651
+ })
652
+
653
+ const autoInterpolator = new ValueInterpolator([0], {
654
+ tension: 120,
655
+ friction: 20,
656
+ threshold: 0.001,
657
+ })
658
+
659
+ // Set targets
660
+ manualInterpolator.setTarget([10])
661
+ autoInterpolator.setTarget([10])
662
+
663
+ // Start auto-interpolation for one
664
+ autoInterpolator.startAutoInterpolation()
665
+
666
+ // Manual updates for the other
667
+ const manualValues: number[] = []
668
+ for (let frame = 0; frame < 30; frame++) {
669
+ manualInterpolator.update(1 / 60)
670
+ manualValues.push(manualInterpolator.getCurrentValues()[0])
671
+ await new Promise(resolve => setTimeout(resolve, 16)) // ~60fps
672
+ }
673
+
674
+ // Check auto interpolator after same time
675
+ const autoValue = autoInterpolator.getCurrentValues()[0]
676
+
677
+ console.log('Mixed mode - Manual final:', manualValues[manualValues.length - 1], 'Auto final:', autoValue)
678
+
679
+ // Both should have made progress
680
+ expect(manualValues[manualValues.length - 1]).toBeGreaterThan(0.5)
681
+ expect(autoValue).toBeGreaterThan(0.5)
682
+
683
+ // Manual interpolator should not be auto-interpolating
684
+ expect(manualInterpolator.isInterpolating()).toBe(false)
685
+ expect(autoInterpolator.isInterpolating()).toBe(true)
686
+
687
+ // Clean up
688
+ manualInterpolator.destroy()
689
+ autoInterpolator.destroy()
690
+ })
691
+
692
+ it("should handle rapid start/stop of auto-interpolation correctly", async () => {
693
+ // Test the edge case of rapidly starting and stopping auto-interpolation
694
+ const interpolator = new ValueInterpolator([0], {
695
+ tension: 120,
696
+ friction: 20,
697
+ threshold: 0.001,
698
+ })
699
+
700
+ interpolator.setTarget([10])
701
+
702
+ // Rapidly start and stop multiple times
703
+ for (let i = 0; i < 5; i++) {
704
+ interpolator.startAutoInterpolation()
705
+ expect(interpolator.isInterpolating()).toBe(true)
706
+
707
+ interpolator.stop()
708
+ expect(interpolator.isInterpolating()).toBe(false)
709
+
710
+ await new Promise(resolve => setTimeout(resolve, 10))
711
+ }
712
+
713
+ // Final start
714
+ interpolator.startAutoInterpolation()
715
+ await new Promise(resolve => setTimeout(resolve, 200))
716
+
717
+ // Should still be working correctly
718
+ const finalValue = interpolator.getCurrentValues()[0]
719
+ expect(finalValue).toBeGreaterThan(0.5)
720
+ expect(interpolator.isInterpolating()).toBe(true)
721
+
722
+ // Clean up
723
+ interpolator.destroy()
724
+ })
725
+
726
+ it("should handle multiple interpolators with rapid requestAnimationFrame setTarget calls", async () => {
727
+ // This test simulates the exact pattern used in RobotAnimator:
728
+ // useAutorun(() => {
729
+ // requestAnimationFrame(() => updateJoints(newJointValues))
730
+ // })
731
+
732
+ const interpolator1 = new ValueInterpolator([0, 0, 0], {
733
+ tension: 120,
734
+ friction: 20,
735
+ threshold: 0.001,
736
+ })
737
+
738
+ const interpolator2 = new ValueInterpolator([0, 0, 0], {
739
+ tension: 120,
740
+ friction: 20,
741
+ threshold: 0.001,
742
+ })
743
+
744
+ const interpolator3 = new ValueInterpolator([0, 0, 0], {
745
+ tension: 120,
746
+ friction: 20,
747
+ threshold: 0.001,
748
+ })
749
+
750
+ const interpolators = [interpolator1, interpolator2, interpolator3]
751
+
752
+ // Simulate the RobotAnimator pattern where multiple components
753
+ // schedule setTarget calls via requestAnimationFrame
754
+ const targetSets = [
755
+ [1, 2, 3],
756
+ [4, 5, 6],
757
+ [7, 8, 9]
758
+ ]
759
+
760
+ // Schedule all setTarget calls via requestAnimationFrame (like RobotAnimator does)
761
+ targetSets.forEach((targets, index) => {
762
+ requestAnimationFrame(() => {
763
+ interpolators[index].setTarget(targets)
764
+ })
765
+ })
766
+
767
+ // Wait for requestAnimationFrame calls to execute
768
+ await new Promise(resolve => setTimeout(resolve, 50))
769
+
770
+ // Now manually update all interpolators for several frames
771
+ for (let frame = 0; frame < 30; frame++) {
772
+ interpolators.forEach(interpolator => {
773
+ interpolator.update(1 / 60)
774
+ })
775
+ await new Promise(resolve => setTimeout(resolve, 16))
776
+ }
777
+
778
+ // Check the results
779
+ const finalValues = interpolators.map(interpolator => interpolator.getCurrentValues())
780
+
781
+ console.log('RAF setTarget pattern results:', finalValues)
782
+
783
+ // All should have made progress toward their respective targets
784
+ expect(finalValues[0][0]).toBeGreaterThan(0.3) // Moving toward 1
785
+ expect(finalValues[1][0]).toBeGreaterThan(1.0) // Moving toward 4
786
+ expect(finalValues[2][0]).toBeGreaterThan(2.0) // Moving toward 7
787
+
788
+ // Values should be different (no interference)
789
+ expect(finalValues[0][0]).not.toBeCloseTo(finalValues[1][0], 0)
790
+ expect(finalValues[1][0]).not.toBeCloseTo(finalValues[2][0], 0)
791
+
792
+ // Clean up
793
+ interpolators.forEach(interpolator => interpolator.destroy())
794
+ })
795
+
796
+ it("should handle simultaneous setTarget calls without target blending interference", () => {
797
+ // Test the specific case where setTarget is called on multiple interpolators
798
+ // at nearly the same time (which could trigger the target blending bug)
799
+
800
+ const interpolator1 = new ValueInterpolator([0], {
801
+ tension: 120,
802
+ friction: 20,
803
+ threshold: 0.001,
804
+ })
805
+
806
+ const interpolator2 = new ValueInterpolator([0], {
807
+ tension: 120,
808
+ friction: 20,
809
+ threshold: 0.001,
810
+ })
811
+
812
+ // Call setTarget on both interpolators immediately (same timestamp)
813
+ const now = performance.now()
814
+ interpolator1.setTarget([10])
815
+ interpolator2.setTarget([20])
816
+
817
+ // Verify the targets were set correctly (not blended)
818
+ expect(interpolator1.getCurrentValues()[0]).toBe(0) // Should start at 0
819
+ expect(interpolator2.getCurrentValues()[0]).toBe(0) // Should start at 0
820
+
821
+ // Update once and check the direction of movement
822
+ interpolator1.update(1 / 60)
823
+ interpolator2.update(1 / 60)
824
+
825
+ const values1 = interpolator1.getCurrentValues()[0]
826
+ const values2 = interpolator2.getCurrentValues()[0]
827
+
828
+ console.log('Simultaneous setTarget - Values after 1 frame:', { values1, values2 })
829
+
830
+ // Both should be moving in the right direction
831
+ expect(values1).toBeGreaterThan(0) // Moving toward 10
832
+ expect(values2).toBeGreaterThan(0) // Moving toward 20
833
+
834
+ // Interpolator2 should be moving faster (higher target)
835
+ expect(values2).toBeGreaterThan(values1)
836
+
837
+ // Clean up
838
+ interpolator1.destroy()
839
+ interpolator2.destroy()
840
+ })
841
+
842
+ it("should handle multiple interpolators with real requestAnimationFrame without interference", async () => {
843
+ // This test uses REAL requestAnimationFrame to simulate actual browser behavior
844
+ // Remove mocks temporarily for this test
845
+ vi.restoreAllMocks()
846
+
847
+ const interpolator1 = new ValueInterpolator([0], {
848
+ tension: 120,
849
+ friction: 20,
850
+ threshold: 0.001,
851
+ })
852
+
853
+ const interpolator2 = new ValueInterpolator([0], {
854
+ tension: 120,
855
+ friction: 20,
856
+ threshold: 0.001,
857
+ })
858
+
859
+ const interpolator3 = new ValueInterpolator([0], {
860
+ tension: 120,
861
+ friction: 20,
862
+ threshold: 0.001,
863
+ })
864
+
865
+ // Set targets
866
+ interpolator1.setTarget([10])
867
+ interpolator2.setTarget([20])
868
+ interpolator3.setTarget([30])
869
+
870
+ // Start real auto-interpolation (uses real requestAnimationFrame)
871
+ interpolator1.startAutoInterpolation()
872
+ interpolator2.startAutoInterpolation()
873
+ interpolator3.startAutoInterpolation()
874
+
875
+ // Wait for REAL animation frames to process
876
+ await new Promise(resolve => {
877
+ let frameCount = 0
878
+ const checkProgress = () => {
879
+ frameCount++
880
+ if (frameCount >= 30) { // Wait for ~30 real frames
881
+ resolve(undefined)
882
+ } else {
883
+ requestAnimationFrame(checkProgress)
884
+ }
885
+ }
886
+ requestAnimationFrame(checkProgress)
887
+ })
888
+
889
+ // Check results after real animation frames
890
+ const values1 = interpolator1.getCurrentValues()[0]
891
+ const values2 = interpolator2.getCurrentValues()[0]
892
+ const values3 = interpolator3.getCurrentValues()[0]
893
+
894
+ console.log('Real RAF test results:', { values1, values2, values3 })
895
+
896
+ // All should have made significant progress
897
+ expect(values1).toBeGreaterThan(1)
898
+ expect(values2).toBeGreaterThan(2)
899
+ expect(values3).toBeGreaterThan(3)
900
+
901
+ // Values should be proportional to their targets
902
+ expect(values2).toBeGreaterThan(values1 * 1.5) // 20 is 2x 10
903
+ expect(values3).toBeGreaterThan(values1 * 2.5) // 30 is 3x 10
904
+
905
+ // All should still be interpolating
906
+ expect(interpolator1.isInterpolating()).toBe(true)
907
+ expect(interpolator2.isInterpolating()).toBe(true)
908
+ expect(interpolator3.isInterpolating()).toBe(true)
909
+
910
+ // Clean up
911
+ interpolator1.destroy()
912
+ interpolator2.destroy()
913
+ interpolator3.destroy()
914
+
915
+ // Restore mocks for other tests
916
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
917
+ return setTimeout(cb, 16)
918
+ })
919
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => {
920
+ clearTimeout(id)
921
+ })
922
+ })
923
+
924
+ it("should handle multiple useFrame-like manual updates without interference", async () => {
925
+ // Simulate multiple React Three Fiber components using useFrame
926
+ const interpolator1 = new ValueInterpolator([0, 0, 0], {
927
+ tension: 120,
928
+ friction: 20,
929
+ threshold: 0.001,
930
+ })
931
+
932
+ const interpolator2 = new ValueInterpolator([0, 0, 0], {
933
+ tension: 120,
934
+ friction: 20,
935
+ threshold: 0.001,
936
+ })
937
+
938
+ // Set targets (like RobotAnimator would)
939
+ interpolator1.setTarget([1, 2, 3])
940
+ interpolator2.setTarget([4, 5, 6])
941
+
942
+ // Simulate useFrame callbacks running simultaneously for 500ms
943
+ const startTime = performance.now()
944
+ let lastTime = startTime
945
+
946
+ const runSimulation = () => {
947
+ return new Promise<void>((resolve) => {
948
+ const frame = () => {
949
+ const currentTime = performance.now()
950
+ const deltaTime = (currentTime - lastTime) / 1000 // Convert to seconds
951
+ lastTime = currentTime
952
+
953
+ // Both interpolators update with real delta timing (like useFrame)
954
+ interpolator1.update(deltaTime)
955
+ interpolator2.update(deltaTime)
956
+
957
+ // Continue for 500ms
958
+ if (currentTime - startTime < 500) {
959
+ requestAnimationFrame(frame)
960
+ } else {
961
+ resolve()
962
+ }
963
+ }
964
+ requestAnimationFrame(frame)
965
+ })
966
+ }
967
+
968
+ await runSimulation()
969
+
970
+ const values1 = interpolator1.getCurrentValues()
971
+ const values2 = interpolator2.getCurrentValues()
972
+
973
+ console.log('useFrame simulation results:', { values1, values2 })
974
+
975
+ // Both should have made good progress
976
+ expect(values1[0]).toBeGreaterThan(0.5) // Moving toward 1
977
+ expect(values1[1]).toBeGreaterThan(1.0) // Moving toward 2
978
+ expect(values1[2]).toBeGreaterThan(1.5) // Moving toward 3
979
+
980
+ expect(values2[0]).toBeGreaterThan(2.0) // Moving toward 4
981
+ expect(values2[1]).toBeGreaterThan(2.5) // Moving toward 5
982
+ expect(values2[2]).toBeGreaterThan(3.0) // Moving toward 6
983
+
984
+ // Values should be different (no interference)
985
+ expect(values1[0]).not.toBeCloseTo(values2[0], 0)
986
+ expect(values1[1]).not.toBeCloseTo(values2[1], 0)
987
+
988
+ // Clean up
989
+ interpolator1.destroy()
990
+ interpolator2.destroy()
991
+ })
992
+
993
+ it("should handle CartesianJoggingAxisVisualization pattern with multiple components", async () => {
994
+ // This test replicates the exact pattern from CartesianJoggingAxisVisualization
995
+ // where multiple components watch the same MobX observable and call setTarget via RAF
996
+
997
+ const interpolator1 = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], {
998
+ tension: 120,
999
+ friction: 20,
1000
+ threshold: 0.001,
1001
+ })
1002
+
1003
+ const interpolator2 = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], {
1004
+ tension: 120,
1005
+ friction: 20,
1006
+ threshold: 0.001,
1007
+ })
1008
+
1009
+ const interpolator3 = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], {
1010
+ tension: 120,
1011
+ friction: 20,
1012
+ threshold: 0.001,
1013
+ })
1014
+
1015
+ // Simulate poseToArray function results
1016
+ const pose1 = [0.1, 0.2, 0.3, 0, 0, 0, 1] // Different poses for each component
1017
+ const pose2 = [0.4, 0.5, 0.6, 0, 0, 0, 1]
1018
+ const pose3 = [0.7, 0.8, 0.9, 0, 0, 0, 1]
1019
+
1020
+ // Simulate the useAutorun pattern where all components respond to the same data change
1021
+ // and schedule their setTarget calls via requestAnimationFrame
1022
+ const updateFlangePose1 = (newPose: number[]) => {
1023
+ interpolator1.setTarget(newPose)
1024
+ }
1025
+ const updateFlangePose2 = (newPose: number[]) => {
1026
+ interpolator2.setTarget(newPose)
1027
+ }
1028
+ const updateFlangePose3 = (newPose: number[]) => {
1029
+ interpolator3.setTarget(newPose)
1030
+ }
1031
+
1032
+ // All three components react to the same observable change and schedule RAF calls
1033
+ requestAnimationFrame(() => updateFlangePose1(pose1))
1034
+ requestAnimationFrame(() => updateFlangePose2(pose2))
1035
+ requestAnimationFrame(() => updateFlangePose3(pose3))
1036
+
1037
+ // Wait for RAF calls to execute
1038
+ await new Promise(resolve => setTimeout(resolve, 32))
1039
+
1040
+ // Now simulate useFrame updates for all three interpolators
1041
+ for (let frame = 0; frame < 30; frame++) {
1042
+ const delta = 1 / 60
1043
+ interpolator1.update(delta)
1044
+ interpolator2.update(delta)
1045
+ interpolator3.update(delta)
1046
+ await new Promise(resolve => setTimeout(resolve, 16))
1047
+ }
1048
+
1049
+ const values1 = interpolator1.getCurrentValues()
1050
+ const values2 = interpolator2.getCurrentValues()
1051
+ const values3 = interpolator3.getCurrentValues()
1052
+
1053
+ console.log('CartesianJoggingAxisVisualization pattern results:')
1054
+ console.log('Component 1:', values1.slice(0, 3))
1055
+ console.log('Component 2:', values2.slice(0, 3))
1056
+ console.log('Component 3:', values3.slice(0, 3))
1057
+
1058
+ // Each interpolator should be moving toward its own target
1059
+ expect(values1[0]).toBeCloseTo(0.1, 1) // Should be close to target 0.1
1060
+ expect(values1[1]).toBeCloseTo(0.2, 1) // Should be close to target 0.2
1061
+ expect(values1[2]).toBeCloseTo(0.3, 1) // Should be close to target 0.3
1062
+
1063
+ expect(values2[0]).toBeCloseTo(0.4, 1) // Should be close to target 0.4
1064
+ expect(values2[1]).toBeCloseTo(0.5, 1) // Should be close to target 0.5
1065
+ expect(values2[2]).toBeCloseTo(0.6, 1) // Should be close to target 0.6
1066
+
1067
+ expect(values3[0]).toBeCloseTo(0.7, 1) // Should be close to target 0.7
1068
+ expect(values3[1]).toBeCloseTo(0.8, 1) // Should be close to target 0.8
1069
+ expect(values3[2]).toBeCloseTo(0.9, 1) // Should be close to target 0.9
1070
+
1071
+ // Clean up
1072
+ interpolator1.destroy()
1073
+ interpolator2.destroy()
1074
+ interpolator3.destroy()
1075
+ })
1076
+
1077
+ it("should handle rapid pose updates like MobX observable changes", async () => {
1078
+ // Test the scenario where MobX triggers rapid updates to multiple components
1079
+ const interpolator = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], {
1080
+ tension: 120,
1081
+ friction: 20,
1082
+ threshold: 0.001,
1083
+ })
1084
+
1085
+ // Simulate rapid pose changes (like rapidlyChangingMotionState.flange_pose)
1086
+ const poseSequence = [
1087
+ [0.1, 0.1, 0.1, 0, 0, 0, 1],
1088
+ [0.11, 0.11, 0.11, 0, 0, 0, 1],
1089
+ [0.12, 0.12, 0.12, 0, 0, 0, 1],
1090
+ [0.13, 0.13, 0.13, 0, 0, 0, 1],
1091
+ [0.15, 0.15, 0.15, 0, 0, 0, 1], // Final target
1092
+ ]
1093
+
1094
+ // Simulate useAutorun triggering for each pose change with RAF delay
1095
+ poseSequence.forEach((pose, index) => {
1096
+ setTimeout(() => {
1097
+ requestAnimationFrame(() => {
1098
+ interpolator.setTarget(pose)
1099
+ })
1100
+ }, index * 5) // 5ms apart (very rapid)
1101
+ })
1102
+
1103
+ // Wait for all RAF calls to execute
1104
+ await new Promise(resolve => setTimeout(resolve, 100))
1105
+
1106
+ // Run interpolation for a while
1107
+ for (let frame = 0; frame < 50; frame++) {
1108
+ interpolator.update(1 / 60)
1109
+ await new Promise(resolve => setTimeout(resolve, 16))
1110
+ }
1111
+
1112
+ const finalValues = interpolator.getCurrentValues()
1113
+ console.log('Rapid pose updates result:', finalValues.slice(0, 3))
1114
+
1115
+ // Should converge toward the final target [0.15, 0.15, 0.15]
1116
+ expect(finalValues[0]).toBeCloseTo(0.15, 1)
1117
+ expect(finalValues[1]).toBeCloseTo(0.15, 1)
1118
+ expect(finalValues[2]).toBeCloseTo(0.15, 1)
1119
+
1120
+ // Clean up
1121
+ interpolator.destroy()
1122
+ })
1123
+ })