@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.
- package/README.md +1 -1
- package/dist/Setup.d.ts +1 -1
- package/dist/Setup.d.ts.map +1 -1
- package/dist/components/3d-viewport/SafetyZonesRenderer.d.ts +2 -2
- package/dist/components/3d-viewport/SafetyZonesRenderer.d.ts.map +1 -1
- package/dist/components/3d-viewport/TrajectoryRenderer.d.ts +1 -1
- package/dist/components/3d-viewport/TrajectoryRenderer.d.ts.map +1 -1
- package/dist/components/robots/DHRobot.d.ts.map +1 -1
- package/dist/components/robots/GenericRobot.d.ts +2 -2
- package/dist/components/robots/GenericRobot.d.ts.map +1 -1
- package/dist/components/robots/Robot.d.ts +2 -2
- package/dist/components/robots/Robot.d.ts.map +1 -1
- package/dist/components/robots/RobotAnimator.d.ts.map +1 -1
- package/dist/components/robots/RobotAnimator.test.d.ts +2 -0
- package/dist/components/robots/RobotAnimator.test.d.ts.map +1 -0
- package/dist/components/robots/SupportedRobot.d.ts +3 -3
- package/dist/components/robots/SupportedRobot.d.ts.map +1 -1
- package/dist/components/utils/interpolation.d.ts +159 -0
- package/dist/components/utils/interpolation.d.ts.map +1 -0
- package/dist/components/utils/interpolation.test.d.ts +2 -0
- package/dist/components/utils/interpolation.test.d.ts.map +1 -0
- package/dist/externalizeComponent.d.ts +1 -1
- package/dist/externalizeComponent.d.ts.map +1 -1
- package/dist/index.cjs +39 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8251 -9820
- package/dist/index.js.map +1 -1
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/package.json +33 -32
- package/src/Setup.tsx +1 -1
- package/src/components/3d-viewport/SafetyZonesRenderer.tsx +2 -2
- package/src/components/3d-viewport/TrajectoryRenderer.tsx +1 -1
- package/src/components/jogging/JoggingOptions.tsx +1 -1
- package/src/components/robots/DHRobot.tsx +37 -10
- package/src/components/robots/GenericRobot.tsx +4 -5
- package/src/components/robots/Robot.tsx +2 -2
- package/src/components/robots/RobotAnimator.test.tsx +113 -0
- package/src/components/robots/RobotAnimator.tsx +38 -23
- package/src/components/robots/SupportedRobot.tsx +3 -3
- package/src/components/utils/converters.ts +1 -1
- package/src/components/utils/interpolation.test.ts +1123 -0
- package/src/components/utils/interpolation.ts +379 -0
- package/src/externalizeComponent.tsx +1 -1
- package/src/index.ts +1 -0
- 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
|
+
})
|