UNPKG

@wandelbots/wandelbots-js-react-components

Version:

React UI toolkit for building applications on top of the Wandelbots platform

1,124 lines (900 loc) • 37.6 kB
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { ValueInterpolator } from "./interpolation" describe("ValueInterpolator", () => { let interpolator: ValueInterpolator let mockOnChange: ReturnType<typeof vi.fn> let mockOnComplete: ReturnType<typeof vi.fn> beforeEach(() => { mockOnChange = vi.fn() mockOnComplete = vi.fn() // Mock requestAnimationFrame for testing vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => { return setTimeout(cb, 16) // Simulate 60fps }) vi.stubGlobal("cancelAnimationFrame", (id: number) => { clearTimeout(id) }) }) afterEach(() => { interpolator?.destroy() vi.restoreAllMocks() vi.unstubAllGlobals() }) it("should handle frequent target updates without losing interpolation quality", async () => { interpolator = new ValueInterpolator([0, 0, 0], { tension: 180, friction: 25, threshold: 0.001, onChange: mockOnChange, onComplete: mockOnComplete, }) // Simulate rapid updates (like robot motion state changes) const rapidUpdates = [ [1, 2, 3], [1.1, 2.1, 3.1], [1.2, 2.2, 3.2], [1.3, 2.3, 3.3], [1.5, 2.5, 3.5], // Final target ] // Apply updates rapidly rapidUpdates.forEach((values, index) => { setTimeout(() => { interpolator.setTarget(values) }, index * 5) }) // Simulate frame updates with delta const simulateFrames = async (count: number) => { for (let i = 0; i < count; i++) { interpolator.update(1 / 60) // 60fps await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps } } await simulateFrames(20) // Verify interpolation handled rapid updates expect(mockOnChange).toHaveBeenCalled() // Final values should be close to the last target const finalValues = interpolator.getCurrentValues() expect(finalValues[0]).toBeCloseTo(1.5, 0) expect(finalValues[1]).toBeCloseTo(2.5, 0) expect(finalValues[2]).toBeCloseTo(3.5, 0) }) it("should provide smooth spring-like animation characteristics", async () => { const changeHistory: number[][] = [] interpolator = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, onChange: (values) => { changeHistory.push([...values]) }, }) interpolator.setTarget([10]) // Simulate animation frames for (let i = 0; i < 30; i++) { interpolator.update(1 / 60) await new Promise((resolve) => setTimeout(resolve, 5)) } // Verify we have smooth progression expect(changeHistory.length).toBeGreaterThanOrEqual(1) // At least some animation steps // Spring animation should show progression toward target const firstValue = changeHistory[0][0] const lastValue = changeHistory[changeHistory.length - 1][0] expect(firstValue).toBeGreaterThan(0) // Should start moving expect(lastValue).toBeGreaterThan(firstValue) // Should progress toward target expect(lastValue).toBeLessThan(15) // Should not overshoot significantly }) it("should handle different spring configurations correctly", () => { const fastInterpolator = new ValueInterpolator([0], { tension: 200, // Higher tension = faster friction: 30, }) const normalInterpolator = new ValueInterpolator([0], { tension: 120, // Default tension friction: 20, }) const slowInterpolator = new ValueInterpolator([0], { tension: 60, // Lower tension = slower friction: 15, }) // Test that different spring configurations produce different behavior fastInterpolator.setTarget([10]) normalInterpolator.setTarget([10]) slowInterpolator.setTarget([10]) // Update once to see the difference fastInterpolator.update(1 / 60) normalInterpolator.update(1 / 60) slowInterpolator.update(1 / 60) // After one step, values should be different due to spring settings const fastValue = fastInterpolator.getCurrentValues()[0] const normalValue = normalInterpolator.getCurrentValues()[0] const slowValue = slowInterpolator.getCurrentValues()[0] // All should be moving toward target expect(fastValue).toBeGreaterThan(0) expect(normalValue).toBeGreaterThan(0) expect(slowValue).toBeGreaterThan(0) // Higher tension should generally move faster initially // Note: actual behavior depends on friction balance, so just verify all move expect(fastValue).toBeGreaterThan(0) expect(normalValue).toBeGreaterThan(0) expect(slowValue).toBeGreaterThan(0) fastInterpolator.destroy() normalInterpolator.destroy() slowInterpolator.destroy() }) it("should handle array length changes gracefully", () => { interpolator = new ValueInterpolator([1, 2], { onChange: mockOnChange, }) // Expand array interpolator.setTarget([1, 2, 3, 4]) expect(interpolator.getCurrentValues()).toHaveLength(4) // Shrink array interpolator.setTarget([5, 6]) expect(interpolator.getCurrentValues()).toHaveLength(2) // Update once to see changes interpolator.update(1 / 60) // Values should be updated const values = interpolator.getCurrentValues() expect(values[0]).not.toBe(1) // Should be interpolating toward 5 expect(values[1]).not.toBe(2) // Should be interpolating toward 6 }) it("should stop and start interpolation correctly", async () => { interpolator = new ValueInterpolator([0], { // Slow for testing onChange: mockOnChange, }) interpolator.setTarget([10]) // Manual update should work interpolator.update(1 / 60) expect(mockOnChange).toHaveBeenCalled() const callCountAfterFirst = mockOnChange.mock.calls.length interpolator.stop() // Should be able to restart interpolator.setTarget([5]) interpolator.update(1 / 60) expect(mockOnChange.mock.calls.length).toBeGreaterThan(callCountAfterFirst) }) it("should not conflict when using manual updates without starting auto-interpolation", () => { let changeCount = 0 interpolator = new ValueInterpolator([0], { onChange: () => { changeCount++ }, }) // Set target but don't start auto-interpolation interpolator.setTarget([10]) // Should not have triggered any changes yet (no auto-start) expect(changeCount).toBe(0) // Manual update should work interpolator.update(1 / 60) expect(changeCount).toBe(1) // Another manual update should continue interpolation interpolator.update(1 / 60) expect(changeCount).toBe(2) }) it("should work with auto-interpolation when explicitly started", async () => { let changeCount = 0 interpolator = new ValueInterpolator([0], { // Faster for testing onChange: () => { changeCount++ }, }) // Set target and start auto-interpolation interpolator.setTarget([10]) interpolator.startAutoInterpolation() // Wait for automatic updates await new Promise((resolve) => setTimeout(resolve, 50)) // Should have triggered automatic changes expect(changeCount).toBeGreaterThan(0) }) it("should not double-update when both manual and auto are used incorrectly", () => { let updateCallCount = 0 const originalUpdate = ValueInterpolator.prototype.update // Spy on update calls to detect conflicts ValueInterpolator.prototype.update = function (delta) { updateCallCount++ return originalUpdate.call(this, delta) } try { interpolator = new ValueInterpolator([0], { onChange: mockOnChange, }) interpolator.setTarget([10]) // Manual update (this should be the only way it updates) interpolator.update(1 / 60) // Should only have been called once (no auto-start) expect(updateCallCount).toBe(1) expect(mockOnChange).toHaveBeenCalledTimes(1) } finally { // Restore original method ValueInterpolator.prototype.update = originalUpdate } }) it("should work correctly in RobotAnimator-like usage pattern", async () => { // Simulate the exact pattern used in RobotAnimator let frameUpdateCount = 0 let onChangeCallCount = 0 interpolator = new ValueInterpolator([0, 0, 0], { tension: 120, friction: 20, threshold: 0.001, onChange: () => { onChangeCallCount++ }, }) // Simulate useFrame calling update repeatedly const simulateUseFrame = () => { const animate = () => { frameUpdateCount++ interpolator.update(1 / 60) if (frameUpdateCount < 20) { setTimeout(animate, 16) // ~60fps } } animate() } // Start the animation loop simulateUseFrame() // After a few frames, change target (like rapidlyChangingMotionState update) setTimeout(() => { interpolator.setTarget([1, 2, 3]) }, 50) // Wait for animation to complete await new Promise((resolve) => setTimeout(resolve, 500)) // Slightly longer wait for spring settling // Should have smooth updates, not conflicts expect(frameUpdateCount).toBeGreaterThanOrEqual(20) expect(onChangeCallCount).toBeGreaterThan(0) expect(onChangeCallCount).toBeLessThan(frameUpdateCount * 2) // No double updates const finalValues = interpolator.getCurrentValues() // Spring physics may not reach exact target quickly, just verify progress expect(finalValues[0]).toBeGreaterThan(0.5) // Should be moving toward 1 expect(finalValues[1]).toBeGreaterThan(1.0) // Should be moving toward 2 expect(finalValues[2]).toBeGreaterThan(1.5) // Should be moving toward 3 }) it("should handle setImmediate correctly", () => { interpolator = new ValueInterpolator([0, 0], { onChange: mockOnChange, }) interpolator.setImmediate([5, 10]) expect(interpolator.getCurrentValues()).toEqual([5, 10]) expect(interpolator.isInterpolating()).toBe(false) expect(mockOnChange).toHaveBeenCalledWith([5, 10]) }) it("should handle irregular frame timing smoothly", async () => { const changeHistory: number[][] = [] interpolator = new ValueInterpolator([0], { threshold: 0.001, onChange: (values) => { changeHistory.push([...values]) }, }) interpolator.setTarget([10]) // Simulate irregular frame timing (like real useFrame behavior) const irregularDeltas = [ 1 / 60, // Normal frame 1 / 30, // Slow frame (frame drop) 1 / 120, // Fast frame 1 / 45, // Medium frame 1 / 60, // Normal frame 1 / 20, // Very slow frame 1 / 60, // Normal frame 1 / 90, // Fast frame 1 / 60, // Normal frame 1 / 40, // Slow frame ] for (const delta of irregularDeltas) { interpolator.update(delta) await new Promise((resolve) => setTimeout(resolve, 5)) } // Should have smooth progression despite irregular timing expect(changeHistory.length).toBeGreaterThanOrEqual(5) // Check that values progress monotonically (no sudden jumps) for (let i = 1; i < changeHistory.length; i++) { const prev = changeHistory[i - 1][0] const curr = changeHistory[i][0] // Values should always increase smoothly toward target expect(curr).toBeGreaterThanOrEqual(prev) // No huge jumps - change should be reasonable const change = curr - prev expect(change).toBeLessThan(4) // No jump larger than 40% of total range (more lenient) } }) describe("irregular target updates", () => { it("should smooth rapid target changes", () => { const interpolator = new ValueInterpolator([0], {}) // Simulate rapid target updates (faster than 16ms) interpolator.setTarget([10]) // t=0 interpolator.update(0.008) // 8ms later const result1 = interpolator.getCurrentValues() interpolator.setTarget([5]) // New target after 8ms (rapid update) interpolator.update(0.008) // Another 8ms const result2 = interpolator.getCurrentValues() // The interpolation should be smoother due to target blending expect(result1[0]).toBeGreaterThan(0) expect(result1[0]).toBeLessThan(10) expect(result2[0]).toBeGreaterThan(result1[0]) // Should continue progressing expect(result2[0]).toBeLessThan(8) // But not jump to the raw blended target }) it("should adapt responsiveness based on update frequency", () => { const interpolator = new ValueInterpolator([0], {}) // Test recent target update (high responsiveness) interpolator.setTarget([10]) interpolator.update(0.016) const recentUpdateResult = interpolator.getCurrentValues() // Test with delayed target update (simulating old update) const interpolator2 = new ValueInterpolator([0], {}) interpolator2.setTarget([10]) // Simulate time passing to make target update "old" // We'll use a longer delay in the update to simulate this interpolator2.update(0.3) // Simulate 300ms delay const oldUpdateResult = interpolator2.getCurrentValues() // Recent updates should be more responsive (larger change) expect(recentUpdateResult[0]).toBeGreaterThan(0) expect(oldUpdateResult[0]).toBeGreaterThan(0) }) it("should handle target blending correctly", () => { const interpolator = new ValueInterpolator([0], {}) // Set initial target interpolator.setTarget([100]) // Immediately set new target (should trigger blending) interpolator.setTarget([50]) // Update and check the result interpolator.update(0.016) const result = interpolator.getCurrentValues() // Should move toward blended target, not jump to 50 expect(result[0]).toBeGreaterThan(0) expect(result[0]).toBeLessThan(50) // Should be less than the final target due to interpolation }) }) it("should provide smooth initialization without jumpiness", () => { const interpolator = new ValueInterpolator([0], { tension: 120, friction: 20, }) interpolator.setTarget([10]) // Large target change const firstFrameValues: number[] = [] // Capture values from first few frames for (let frame = 0; frame < 5; frame++) { interpolator.update(1 / 60) firstFrameValues.push(interpolator.getValue(0)) } // First frame should not jump too aggressively expect(firstFrameValues[0]).toBeLessThan(2) // Should start gently expect(firstFrameValues[0]).toBeGreaterThan(0) // But should move // Should show gradual acceleration, not sudden jumps const firstStep = firstFrameValues[1] - firstFrameValues[0] const secondStep = firstFrameValues[2] - firstFrameValues[1] expect(secondStep).toBeGreaterThanOrEqual(firstStep) // Should accelerate gradually expect(firstStep).toBeLessThan(1.5) // First step should be modest }) it("should work correctly when multiple components use separate interpolators", () => { // Test the scenario where two components each have their own interpolator const interpolator1 = new ValueInterpolator([0, 0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator2 = new ValueInterpolator([0, 0], { tension: 120, friction: 20, threshold: 0.001, }) // Set different targets for each interpolator interpolator1.setTarget([5, 10]) interpolator2.setTarget([15, 20]) // Update both interpolators for several frames const values1History: number[][] = [] const values2History: number[][] = [] for (let frame = 0; frame < 30; frame++) { // More frames for spring physics interpolator1.update(1 / 60) interpolator2.update(1 / 60) values1History.push([...interpolator1.getCurrentValues()]) values2History.push([...interpolator2.getCurrentValues()]) } // Both interpolators should be moving independently const firstFrame1 = values1History[0] const lastFrame1 = values1History[values1History.length - 1] const firstFrame2 = values2History[0] const lastFrame2 = values2History[values2History.length - 1] // Interpolators should be moving independently and at reasonable speed expect(lastFrame1[0]).toBeGreaterThan(firstFrame1[0]) expect(lastFrame1[1]).toBeGreaterThan(firstFrame1[1]) expect(lastFrame2[0]).toBeGreaterThan(firstFrame2[0]) expect(lastFrame2[1]).toBeGreaterThan(firstFrame2[1]) // Should make meaningful progress toward targets expect(lastFrame1[0]).toBeGreaterThan(1) // Should have made progress toward 5 expect(lastFrame1[1]).toBeGreaterThan(2) // Should have made progress toward 10 expect(lastFrame2[0]).toBeGreaterThan(3) // Should have made progress toward 15 expect(lastFrame2[1]).toBeGreaterThan(4) // Should have made progress toward 20 // The interpolators should be independent - their values should be different expect(lastFrame1[0]).not.toBeCloseTo(lastFrame2[0], 1) expect(lastFrame1[1]).not.toBeCloseTo(lastFrame2[1], 1) // Clean up interpolator1.destroy() interpolator2.destroy() }) it("should handle multiple interpolators updating in the same frame correctly", async () => { // Simulate a more realistic scenario where multiple components // are updating their interpolators in the same animation frame const interpolators: ValueInterpolator[] = [] const changeCallCounts: (() => number)[] = [] // Create multiple interpolators for (let i = 0; i < 3; i++) { let callCount = 0 const interpolator = new ValueInterpolator([0], { onChange: () => { callCount++ }, }) interpolators.push(interpolator) changeCallCounts.push(() => callCount) // Store a function to get current count } // Set different targets interpolators[0].setTarget([10]) interpolators[1].setTarget([20]) interpolators[2].setTarget([30]) // Simulate a frame where all interpolators update const simulateFrame = () => { interpolators.forEach((interpolator) => { interpolator.update(1 / 60) }) } // Run several frames (more for spring physics) for (let frame = 0; frame < 40; frame++) { simulateFrame() await new Promise((resolve) => setTimeout(resolve, 1)) } // Check that all interpolators moved independently const finalValues = interpolators.map( (interpolator) => interpolator.getCurrentValues()[0], ) expect(finalValues[0]).toBeGreaterThan(1) // Moving toward 10 expect(finalValues[1]).toBeGreaterThan(2) // Moving toward 20 expect(finalValues[2]).toBeGreaterThan(3) // Moving toward 30 // All should have different values expect(finalValues[0]).not.toBeCloseTo(finalValues[1], 1) expect(finalValues[1]).not.toBeCloseTo(finalValues[2], 1) // All should have triggered onChange callbacks changeCallCounts.forEach((getCount, index) => { expect(getCount()).toBeGreaterThan(0) }) // Clean up interpolators.forEach((interpolator) => interpolator.destroy()) }) it("should handle multiple interpolators using auto-interpolation without interference", async () => { // Test the specific case where multiple interpolators use startAutoInterpolation() // This tests if requestAnimationFrame handling interferes between interpolators const interpolator1 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator2 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator3 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) // Set different targets interpolator1.setTarget([10]) interpolator2.setTarget([20]) interpolator3.setTarget([30]) // Start auto-interpolation for all (this uses requestAnimationFrame internally) interpolator1.startAutoInterpolation() interpolator2.startAutoInterpolation() interpolator3.startAutoInterpolation() // Wait for automatic interpolation to progress await new Promise(resolve => setTimeout(resolve, 500)) // Check that all interpolators are moving independently const values1 = interpolator1.getCurrentValues() const values2 = interpolator2.getCurrentValues() const values3 = interpolator3.getCurrentValues() console.log('Auto-interpolation values:', { values1, values2, values3 }) // All should have made significant progress toward their targets expect(values1[0]).toBeGreaterThan(1) // Moving toward 10 expect(values2[0]).toBeGreaterThan(2) // Moving toward 20 expect(values3[0]).toBeGreaterThan(3) // Moving toward 30 // All should be running their own interpolation expect(interpolator1.isInterpolating()).toBe(true) expect(interpolator2.isInterpolating()).toBe(true) expect(interpolator3.isInterpolating()).toBe(true) // Values should be different (independent interpolation) expect(values1[0]).not.toBeCloseTo(values2[0], 1) expect(values2[0]).not.toBeCloseTo(values3[0], 1) // Clean up interpolator1.destroy() interpolator2.destroy() interpolator3.destroy() }) it("should not interfere between manual and auto interpolation modes", async () => { // Test mixing manual update() calls with auto-interpolation const manualInterpolator = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) const autoInterpolator = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) // Set targets manualInterpolator.setTarget([10]) autoInterpolator.setTarget([10]) // Start auto-interpolation for one autoInterpolator.startAutoInterpolation() // Manual updates for the other const manualValues: number[] = [] for (let frame = 0; frame < 30; frame++) { manualInterpolator.update(1 / 60) manualValues.push(manualInterpolator.getCurrentValues()[0]) await new Promise(resolve => setTimeout(resolve, 16)) // ~60fps } // Check auto interpolator after same time const autoValue = autoInterpolator.getCurrentValues()[0] console.log('Mixed mode - Manual final:', manualValues[manualValues.length - 1], 'Auto final:', autoValue) // Both should have made progress expect(manualValues[manualValues.length - 1]).toBeGreaterThan(0.5) expect(autoValue).toBeGreaterThan(0.5) // Manual interpolator should not be auto-interpolating expect(manualInterpolator.isInterpolating()).toBe(false) expect(autoInterpolator.isInterpolating()).toBe(true) // Clean up manualInterpolator.destroy() autoInterpolator.destroy() }) it("should handle rapid start/stop of auto-interpolation correctly", async () => { // Test the edge case of rapidly starting and stopping auto-interpolation const interpolator = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) interpolator.setTarget([10]) // Rapidly start and stop multiple times for (let i = 0; i < 5; i++) { interpolator.startAutoInterpolation() expect(interpolator.isInterpolating()).toBe(true) interpolator.stop() expect(interpolator.isInterpolating()).toBe(false) await new Promise(resolve => setTimeout(resolve, 10)) } // Final start interpolator.startAutoInterpolation() await new Promise(resolve => setTimeout(resolve, 200)) // Should still be working correctly const finalValue = interpolator.getCurrentValues()[0] expect(finalValue).toBeGreaterThan(0.5) expect(interpolator.isInterpolating()).toBe(true) // Clean up interpolator.destroy() }) it("should handle multiple interpolators with rapid requestAnimationFrame setTarget calls", async () => { // This test simulates the exact pattern used in RobotAnimator: // useAutorun(() => { // requestAnimationFrame(() => updateJoints(newJointValues)) // }) const interpolator1 = new ValueInterpolator([0, 0, 0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator2 = new ValueInterpolator([0, 0, 0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator3 = new ValueInterpolator([0, 0, 0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolators = [interpolator1, interpolator2, interpolator3] // Simulate the RobotAnimator pattern where multiple components // schedule setTarget calls via requestAnimationFrame const targetSets = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] // Schedule all setTarget calls via requestAnimationFrame (like RobotAnimator does) targetSets.forEach((targets, index) => { requestAnimationFrame(() => { interpolators[index].setTarget(targets) }) }) // Wait for requestAnimationFrame calls to execute await new Promise(resolve => setTimeout(resolve, 50)) // Now manually update all interpolators for several frames for (let frame = 0; frame < 30; frame++) { interpolators.forEach(interpolator => { interpolator.update(1 / 60) }) await new Promise(resolve => setTimeout(resolve, 16)) } // Check the results const finalValues = interpolators.map(interpolator => interpolator.getCurrentValues()) console.log('RAF setTarget pattern results:', finalValues) // All should have made progress toward their respective targets expect(finalValues[0][0]).toBeGreaterThan(0.3) // Moving toward 1 expect(finalValues[1][0]).toBeGreaterThan(1.0) // Moving toward 4 expect(finalValues[2][0]).toBeGreaterThan(2.0) // Moving toward 7 // Values should be different (no interference) expect(finalValues[0][0]).not.toBeCloseTo(finalValues[1][0], 0) expect(finalValues[1][0]).not.toBeCloseTo(finalValues[2][0], 0) // Clean up interpolators.forEach(interpolator => interpolator.destroy()) }) it("should handle simultaneous setTarget calls without target blending interference", () => { // Test the specific case where setTarget is called on multiple interpolators // at nearly the same time (which could trigger the target blending bug) const interpolator1 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator2 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) // Call setTarget on both interpolators immediately (same timestamp) const now = performance.now() interpolator1.setTarget([10]) interpolator2.setTarget([20]) // Verify the targets were set correctly (not blended) expect(interpolator1.getCurrentValues()[0]).toBe(0) // Should start at 0 expect(interpolator2.getCurrentValues()[0]).toBe(0) // Should start at 0 // Update once and check the direction of movement interpolator1.update(1 / 60) interpolator2.update(1 / 60) const values1 = interpolator1.getCurrentValues()[0] const values2 = interpolator2.getCurrentValues()[0] console.log('Simultaneous setTarget - Values after 1 frame:', { values1, values2 }) // Both should be moving in the right direction expect(values1).toBeGreaterThan(0) // Moving toward 10 expect(values2).toBeGreaterThan(0) // Moving toward 20 // Interpolator2 should be moving faster (higher target) expect(values2).toBeGreaterThan(values1) // Clean up interpolator1.destroy() interpolator2.destroy() }) it("should handle multiple interpolators with real requestAnimationFrame without interference", async () => { // This test uses REAL requestAnimationFrame to simulate actual browser behavior // Remove mocks temporarily for this test vi.restoreAllMocks() const interpolator1 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator2 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator3 = new ValueInterpolator([0], { tension: 120, friction: 20, threshold: 0.001, }) // Set targets interpolator1.setTarget([10]) interpolator2.setTarget([20]) interpolator3.setTarget([30]) // Start real auto-interpolation (uses real requestAnimationFrame) interpolator1.startAutoInterpolation() interpolator2.startAutoInterpolation() interpolator3.startAutoInterpolation() // Wait for REAL animation frames to process await new Promise(resolve => { let frameCount = 0 const checkProgress = () => { frameCount++ if (frameCount >= 30) { // Wait for ~30 real frames resolve(undefined) } else { requestAnimationFrame(checkProgress) } } requestAnimationFrame(checkProgress) }) // Check results after real animation frames const values1 = interpolator1.getCurrentValues()[0] const values2 = interpolator2.getCurrentValues()[0] const values3 = interpolator3.getCurrentValues()[0] console.log('Real RAF test results:', { values1, values2, values3 }) // All should have made significant progress expect(values1).toBeGreaterThan(1) expect(values2).toBeGreaterThan(2) expect(values3).toBeGreaterThan(3) // Values should be proportional to their targets expect(values2).toBeGreaterThan(values1 * 1.5) // 20 is 2x 10 expect(values3).toBeGreaterThan(values1 * 2.5) // 30 is 3x 10 // All should still be interpolating expect(interpolator1.isInterpolating()).toBe(true) expect(interpolator2.isInterpolating()).toBe(true) expect(interpolator3.isInterpolating()).toBe(true) // Clean up interpolator1.destroy() interpolator2.destroy() interpolator3.destroy() // Restore mocks for other tests vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => { return setTimeout(cb, 16) }) vi.stubGlobal("cancelAnimationFrame", (id: number) => { clearTimeout(id) }) }) it("should handle multiple useFrame-like manual updates without interference", async () => { // Simulate multiple React Three Fiber components using useFrame const interpolator1 = new ValueInterpolator([0, 0, 0], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator2 = new ValueInterpolator([0, 0, 0], { tension: 120, friction: 20, threshold: 0.001, }) // Set targets (like RobotAnimator would) interpolator1.setTarget([1, 2, 3]) interpolator2.setTarget([4, 5, 6]) // Simulate useFrame callbacks running simultaneously for 500ms const startTime = performance.now() let lastTime = startTime const runSimulation = () => { return new Promise<void>((resolve) => { const frame = () => { const currentTime = performance.now() const deltaTime = (currentTime - lastTime) / 1000 // Convert to seconds lastTime = currentTime // Both interpolators update with real delta timing (like useFrame) interpolator1.update(deltaTime) interpolator2.update(deltaTime) // Continue for 500ms if (currentTime - startTime < 500) { requestAnimationFrame(frame) } else { resolve() } } requestAnimationFrame(frame) }) } await runSimulation() const values1 = interpolator1.getCurrentValues() const values2 = interpolator2.getCurrentValues() console.log('useFrame simulation results:', { values1, values2 }) // Both should have made good progress expect(values1[0]).toBeGreaterThan(0.5) // Moving toward 1 expect(values1[1]).toBeGreaterThan(1.0) // Moving toward 2 expect(values1[2]).toBeGreaterThan(1.5) // Moving toward 3 expect(values2[0]).toBeGreaterThan(2.0) // Moving toward 4 expect(values2[1]).toBeGreaterThan(2.5) // Moving toward 5 expect(values2[2]).toBeGreaterThan(3.0) // Moving toward 6 // Values should be different (no interference) expect(values1[0]).not.toBeCloseTo(values2[0], 0) expect(values1[1]).not.toBeCloseTo(values2[1], 0) // Clean up interpolator1.destroy() interpolator2.destroy() }) it("should handle CartesianJoggingAxisVisualization pattern with multiple components", async () => { // This test replicates the exact pattern from CartesianJoggingAxisVisualization // where multiple components watch the same MobX observable and call setTarget via RAF const interpolator1 = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator2 = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], { tension: 120, friction: 20, threshold: 0.001, }) const interpolator3 = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], { tension: 120, friction: 20, threshold: 0.001, }) // Simulate poseToArray function results const pose1 = [0.1, 0.2, 0.3, 0, 0, 0, 1] // Different poses for each component const pose2 = [0.4, 0.5, 0.6, 0, 0, 0, 1] const pose3 = [0.7, 0.8, 0.9, 0, 0, 0, 1] // Simulate the useAutorun pattern where all components respond to the same data change // and schedule their setTarget calls via requestAnimationFrame const updateFlangePose1 = (newPose: number[]) => { interpolator1.setTarget(newPose) } const updateFlangePose2 = (newPose: number[]) => { interpolator2.setTarget(newPose) } const updateFlangePose3 = (newPose: number[]) => { interpolator3.setTarget(newPose) } // All three components react to the same observable change and schedule RAF calls requestAnimationFrame(() => updateFlangePose1(pose1)) requestAnimationFrame(() => updateFlangePose2(pose2)) requestAnimationFrame(() => updateFlangePose3(pose3)) // Wait for RAF calls to execute await new Promise(resolve => setTimeout(resolve, 32)) // Now simulate useFrame updates for all three interpolators for (let frame = 0; frame < 30; frame++) { const delta = 1 / 60 interpolator1.update(delta) interpolator2.update(delta) interpolator3.update(delta) await new Promise(resolve => setTimeout(resolve, 16)) } const values1 = interpolator1.getCurrentValues() const values2 = interpolator2.getCurrentValues() const values3 = interpolator3.getCurrentValues() console.log('CartesianJoggingAxisVisualization pattern results:') console.log('Component 1:', values1.slice(0, 3)) console.log('Component 2:', values2.slice(0, 3)) console.log('Component 3:', values3.slice(0, 3)) // Each interpolator should be moving toward its own target expect(values1[0]).toBeCloseTo(0.1, 1) // Should be close to target 0.1 expect(values1[1]).toBeCloseTo(0.2, 1) // Should be close to target 0.2 expect(values1[2]).toBeCloseTo(0.3, 1) // Should be close to target 0.3 expect(values2[0]).toBeCloseTo(0.4, 1) // Should be close to target 0.4 expect(values2[1]).toBeCloseTo(0.5, 1) // Should be close to target 0.5 expect(values2[2]).toBeCloseTo(0.6, 1) // Should be close to target 0.6 expect(values3[0]).toBeCloseTo(0.7, 1) // Should be close to target 0.7 expect(values3[1]).toBeCloseTo(0.8, 1) // Should be close to target 0.8 expect(values3[2]).toBeCloseTo(0.9, 1) // Should be close to target 0.9 // Clean up interpolator1.destroy() interpolator2.destroy() interpolator3.destroy() }) it("should handle rapid pose updates like MobX observable changes", async () => { // Test the scenario where MobX triggers rapid updates to multiple components const interpolator = new ValueInterpolator([0, 0, 0, 0, 0, 0, 1], { tension: 120, friction: 20, threshold: 0.001, }) // Simulate rapid pose changes (like rapidlyChangingMotionState.flange_pose) const poseSequence = [ [0.1, 0.1, 0.1, 0, 0, 0, 1], [0.11, 0.11, 0.11, 0, 0, 0, 1], [0.12, 0.12, 0.12, 0, 0, 0, 1], [0.13, 0.13, 0.13, 0, 0, 0, 1], [0.15, 0.15, 0.15, 0, 0, 0, 1], // Final target ] // Simulate useAutorun triggering for each pose change with RAF delay poseSequence.forEach((pose, index) => { setTimeout(() => { requestAnimationFrame(() => { interpolator.setTarget(pose) }) }, index * 5) // 5ms apart (very rapid) }) // Wait for all RAF calls to execute await new Promise(resolve => setTimeout(resolve, 100)) // Run interpolation for a while for (let frame = 0; frame < 50; frame++) { interpolator.update(1 / 60) await new Promise(resolve => setTimeout(resolve, 16)) } const finalValues = interpolator.getCurrentValues() console.log('Rapid pose updates result:', finalValues.slice(0, 3)) // Should converge toward the final target [0.15, 0.15, 0.15] expect(finalValues[0]).toBeCloseTo(0.15, 1) expect(finalValues[1]).toBeCloseTo(0.15, 1) expect(finalValues[2]).toBeCloseTo(0.15, 1) // Clean up interpolator.destroy() }) })