@wandelbots/wandelbots-js-react-components 2.26.0-pr.feature-update-to-react-19.361.ec4e097 → 2.26.0-pr.feature-update-to-react-19.361.edd4b6b

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.26.0-pr.feature-update-to-react-19.361.ec4e097",
3
+ "version": "2.26.0-pr.feature-update-to-react-19.361.edd4b6b",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -4,7 +4,7 @@ import type {
4
4
  DHParameter,
5
5
  MotionGroupStateResponse,
6
6
  } from "@wandelbots/nova-api/v1"
7
- import React, { useCallback, useEffect, useRef, useState } from "react"
7
+ import React, { useRef } from "react"
8
8
  import type { Group, Object3D } from "three"
9
9
  import { useAutorun } from "../utils/hooks"
10
10
  import { collectJoints } from "./robotModelLogic"
@@ -25,56 +25,57 @@ export default function RobotAnimator({
25
25
  Globals.assign({ frameLoop: "always" })
26
26
  const jointValues = useRef<number[]>([])
27
27
  const jointObjects = useRef<Object3D[]>([])
28
- const [isInitialized, setIsInitialized] = useState(false)
29
28
  const { invalidate } = useThree()
30
29
 
31
30
  function setGroupRef(group: Group | null) {
32
- if (!group) {
33
- setIsInitialized(false)
34
- return
35
- }
31
+ if (!group) return
36
32
 
37
33
  jointObjects.current = collectJoints(group)
38
34
 
39
- // Only mark as initialized if we have valid joint objects
35
+ // Set initial position only if we have valid joint objects
40
36
  if (
41
37
  jointObjects.current.length > 0 &&
42
38
  jointObjects.current.every((obj) => obj != null)
43
39
  ) {
44
- setIsInitialized(true)
45
- } else {
46
- setIsInitialized(false)
40
+ // Use requestAnimationFrame to ensure axisValues is ready
41
+ requestAnimationFrame(() => {
42
+ setRotation()
43
+ invalidate()
44
+ })
47
45
  }
48
46
  }
49
47
 
50
48
  function updateJoints(newJointValues: number[]) {
51
- if (!isInitialized) return
52
-
53
49
  jointValues.current = newJointValues
54
50
  setSpring.start(Object.assign({}, jointValues.current) as any)
55
51
  }
56
52
 
57
- // Initialize spring only after joints are ready
58
- const [axisValues, setSpring] = useSpring(() => ({
59
- ...Object.assign(
60
- {},
61
- rapidlyChangingMotionState.state.joint_position.joints,
62
- ),
63
- onChange: () => {
64
- if (isInitialized) {
53
+ const [axisValues, setSpring] = useSpring(() => {
54
+ const currentJointValues =
55
+ rapidlyChangingMotionState.state.joint_position.joints.filter(
56
+ (item) => item !== undefined,
57
+ )
58
+
59
+ return {
60
+ ...Object.assign({}, currentJointValues),
61
+ onChange: () => {
65
62
  setRotation()
66
63
  invalidate()
67
- }
68
- },
69
- onResolve: () => {
70
- if (isInitialized) {
64
+ },
65
+ onResolve: () => {
71
66
  setRotation()
72
- }
73
- },
74
- }))
67
+ },
68
+ }
69
+ })
75
70
 
76
- const setRotation = useCallback(() => {
77
- if (!isInitialized) return
71
+ function setRotation() {
72
+ // Safety check to prevent race condition
73
+ if (
74
+ jointObjects.current.length === 0 ||
75
+ !jointObjects.current.every((obj) => obj != null)
76
+ ) {
77
+ return
78
+ }
78
79
 
79
80
  const updatedJointValues = jointObjects.current.map((_, objectIndex) =>
80
81
  (axisValues as any)[objectIndex].get(),
@@ -92,25 +93,40 @@ export default function RobotAnimator({
92
93
  rotationSign * updatedJointValues[index]! + rotationOffset
93
94
  }
94
95
  }
95
- }, [isInitialized, axisValues, onRotationChanged, dhParameters])
96
-
97
- // Effect to handle initial rotation when component becomes initialized
98
- useEffect(() => {
99
- if (isInitialized) {
100
- setRotation()
101
- invalidate()
102
- }
103
- }, [isInitialized, setRotation, invalidate])
96
+ }
104
97
 
105
98
  useAutorun(() => {
106
- if (!isInitialized) return
107
-
108
99
  const newJointValues =
109
100
  rapidlyChangingMotionState.state.joint_position.joints.filter(
110
101
  (item) => item !== undefined,
111
102
  )
112
103
 
113
- requestAnimationFrame(() => updateJoints(newJointValues))
104
+ requestAnimationFrame(() => {
105
+ // Check if spring values are significantly different from target values
106
+ const currentSpringValues = jointObjects.current.map(
107
+ (_, index) => (axisValues as any)[index]?.get() || 0,
108
+ )
109
+
110
+ const maxDifference = Math.max(
111
+ ...newJointValues.map((newValue, index) =>
112
+ Math.abs(newValue - (currentSpringValues[index] || 0)),
113
+ ),
114
+ )
115
+
116
+ // Be more aggressive about detecting "snap back" situations
117
+ // If difference is large (> 0.05 radians ~= 2.9 degrees), reset immediately
118
+ // This handles focus restoration and other cases where animation would be jarring
119
+ if (maxDifference > 0.05) {
120
+ jointValues.current = newJointValues
121
+ setSpring.set(Object.assign({}, jointValues.current) as any)
122
+ // Force immediate rotation update
123
+ setRotation()
124
+ invalidate()
125
+ } else {
126
+ // Normal smooth animation for small changes
127
+ updateJoints(newJointValues)
128
+ }
129
+ })
114
130
  })
115
131
 
116
132
  return <group ref={setGroupRef}>{children}</group>
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=RobotAnimator.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"RobotAnimator.test.d.ts","sourceRoot":"","sources":["../../../src/components/robots/RobotAnimator.test.tsx"],"names":[],"mappings":""}
@@ -1,349 +0,0 @@
1
- import { Canvas } from "@react-three/fiber"
2
- import { act, render, waitFor } from "@testing-library/react"
3
- import type {
4
- DHParameter,
5
- MotionGroupStateResponse,
6
- } from "@wandelbots/nova-api/v1"
7
- import React from "react"
8
- import { Object3D } from "three"
9
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
10
- import RobotAnimator from "./RobotAnimator"
11
-
12
- // Mock the dependencies
13
- vi.mock("../utils/hooks", () => ({
14
- useAutorun: vi.fn(),
15
- }))
16
-
17
- vi.mock("./robotModelLogic", () => ({
18
- collectJoints: vi.fn(),
19
- }))
20
-
21
- vi.mock("@react-spring/three", () => ({
22
- Globals: {
23
- assign: vi.fn(),
24
- },
25
- useSpring: vi.fn(),
26
- }))
27
-
28
- vi.mock("@react-three/fiber", async () => {
29
- const actual = await vi.importActual("@react-three/fiber")
30
- return {
31
- ...actual,
32
- useThree: () => ({
33
- invalidate: vi.fn(),
34
- }),
35
- }
36
- })
37
-
38
- import { useSpring } from "@react-spring/three"
39
- import { useAutorun } from "../utils/hooks"
40
- import { collectJoints } from "./robotModelLogic"
41
-
42
- const mockUseAutorun = useAutorun as any
43
- const mockCollectJoints = collectJoints as any
44
- const mockUseSpring = useSpring as any
45
-
46
- describe("RobotAnimator", () => {
47
- const mockMotionState: MotionGroupStateResponse = {
48
- state: {
49
- joint_position: {
50
- joints: [0, 0.5, 1, 1.5, 2, 2.5],
51
- },
52
- },
53
- } as any
54
-
55
- const mockDhParameters: DHParameter[] = [
56
- { theta: 0, reverse_rotation_direction: false },
57
- { theta: 0.1, reverse_rotation_direction: true },
58
- { theta: 0.2, reverse_rotation_direction: false },
59
- { theta: 0.3, reverse_rotation_direction: true },
60
- { theta: 0.4, reverse_rotation_direction: false },
61
- { theta: 0.5, reverse_rotation_direction: true },
62
- ]
63
-
64
- let mockJointObjects: Object3D[]
65
- let mockAxisValues: any
66
- let mockSetSpring: any
67
- let mockInvalidate: any
68
- let mockSpringConfig: any
69
- let mockAutorunCallback: any
70
-
71
- beforeEach(() => {
72
- // Create mock joint objects
73
- mockJointObjects = Array.from({ length: 6 }, (_, i) => {
74
- const obj = new Object3D()
75
- obj.name = `Joint_J${i + 1}`
76
- obj.rotation.y = 0
77
- return obj
78
- })
79
-
80
- // Mock axis values object that acts like spring values
81
- mockAxisValues = {}
82
- for (let i = 0; i < 6; i++) {
83
- mockAxisValues[i] = {
84
- get: vi
85
- .fn()
86
- .mockReturnValue(mockMotionState.state.joint_position.joints[i]),
87
- }
88
- }
89
-
90
- mockSetSpring = {
91
- start: vi.fn(),
92
- }
93
-
94
- mockInvalidate = vi.fn()
95
-
96
- // Mock spring config that will be returned by useSpring
97
- mockSpringConfig = {
98
- onChange: vi.fn(),
99
- onResolve: vi.fn(),
100
- }
101
-
102
- // Mock collectJoints to return our mock joint objects
103
- mockCollectJoints.mockReturnValue(mockJointObjects)
104
-
105
- // Mock useSpring to return our mock values and accept a factory function
106
- mockUseSpring.mockImplementation((factory: any) => {
107
- const config = factory()
108
- mockSpringConfig.onChange = config.onChange
109
- mockSpringConfig.onResolve = config.onResolve
110
- return [mockAxisValues, mockSetSpring]
111
- })
112
-
113
- // Mock useAutorun to store the callback
114
- mockUseAutorun.mockImplementation((callback: any) => {
115
- mockAutorunCallback = callback
116
- })
117
- })
118
-
119
- afterEach(() => {
120
- vi.clearAllMocks()
121
- })
122
-
123
- const renderRobotAnimator = (
124
- props: Partial<React.ComponentProps<typeof RobotAnimator>> = {},
125
- ) => {
126
- const defaultProps = {
127
- rapidlyChangingMotionState: mockMotionState,
128
- dhParameters: mockDhParameters,
129
- children: <mesh />,
130
- }
131
-
132
- return render(
133
- <Canvas>
134
- <RobotAnimator {...defaultProps} {...props} />
135
- </Canvas>,
136
- )
137
- }
138
-
139
- it("should render without crashing", () => {
140
- expect(() => renderRobotAnimator()).not.toThrow()
141
- })
142
-
143
- it("should initialize joint objects when group is set", async () => {
144
- const { container } = renderRobotAnimator()
145
-
146
- // Wait for the component to render and set up
147
- await waitFor(() => {
148
- expect(mockCollectJoints).toHaveBeenCalled()
149
- })
150
- })
151
-
152
- it("should call onRotationChanged when provided", async () => {
153
- const mockOnRotationChanged = vi.fn()
154
-
155
- renderRobotAnimator({ onRotationChanged: mockOnRotationChanged })
156
-
157
- // Wait for initialization
158
- await waitFor(() => {
159
- expect(mockCollectJoints).toHaveBeenCalled()
160
- })
161
-
162
- // Simulate spring onChange callback
163
- await act(async () => {
164
- if (mockSpringConfig.onChange) {
165
- mockSpringConfig.onChange()
166
- }
167
- })
168
-
169
- expect(mockOnRotationChanged).toHaveBeenCalledWith(
170
- mockJointObjects,
171
- expect.arrayContaining([0, 0.5, 1, 1.5, 2, 2.5]),
172
- )
173
- })
174
-
175
- it("should update joint rotations when onRotationChanged is not provided", async () => {
176
- renderRobotAnimator()
177
-
178
- // Wait for initialization
179
- await waitFor(() => {
180
- expect(mockCollectJoints).toHaveBeenCalled()
181
- })
182
-
183
- await act(async () => {
184
- // Simulate spring animation callback
185
- if (mockSpringConfig.onChange) {
186
- mockSpringConfig.onChange()
187
- }
188
- })
189
-
190
- // Check that joint rotations were updated according to DH parameters
191
- mockJointObjects.forEach((joint, index) => {
192
- const dhParam = mockDhParameters[index]
193
- const rotationSign = dhParam.reverse_rotation_direction ? -1 : 1
194
- const expectedRotation =
195
- rotationSign * mockMotionState.state.joint_position.joints[index]! +
196
- dhParam.theta!
197
-
198
- expect(joint.rotation.y).toBe(expectedRotation)
199
- })
200
- })
201
-
202
- it("should not update rotations when not initialized", async () => {
203
- // Mock collectJoints to return empty array (not initialized)
204
- mockCollectJoints.mockReturnValue([])
205
-
206
- renderRobotAnimator()
207
-
208
- await act(async () => {
209
- // Simulate spring animation callback
210
- if (mockSpringConfig.onChange) {
211
- mockSpringConfig.onChange()
212
- }
213
- })
214
-
215
- // Verify that joint rotations were not modified
216
- mockJointObjects.forEach((joint) => {
217
- expect(joint.rotation.y).toBe(0)
218
- })
219
- })
220
-
221
- it("should handle spring updates correctly", async () => {
222
- renderRobotAnimator()
223
-
224
- const newValues = [1, 2, 3, 4, 5, 6]
225
-
226
- // Wait for initialization
227
- await waitFor(() => {
228
- expect(mockCollectJoints).toHaveBeenCalled()
229
- })
230
-
231
- await act(async () => {
232
- // Simulate motion state update by calling the autorun callback
233
- if (mockAutorunCallback) {
234
- mockAutorunCallback()
235
- }
236
- })
237
-
238
- expect(mockSetSpring.start).toHaveBeenCalledWith(
239
- expect.objectContaining({
240
- 0: 0,
241
- 1: 0.5,
242
- 2: 1,
243
- 3: 1.5,
244
- 4: 2,
245
- 5: 2.5,
246
- }),
247
- )
248
- })
249
-
250
- it("should handle group reference being null", async () => {
251
- renderRobotAnimator()
252
-
253
- // This should not throw an error
254
- await act(async () => {
255
- // Component should handle null group reference gracefully
256
- })
257
-
258
- expect(mockCollectJoints).toHaveBeenCalled()
259
- })
260
-
261
- it("should call invalidate when updating rotations", async () => {
262
- renderRobotAnimator()
263
-
264
- // Wait for initialization
265
- await waitFor(() => {
266
- expect(mockCollectJoints).toHaveBeenCalled()
267
- })
268
-
269
- await act(async () => {
270
- // Simulate spring animation callback
271
- if (mockSpringConfig.onChange) {
272
- mockSpringConfig.onChange()
273
- }
274
- })
275
-
276
- // Note: invalidate is called from useThree hook which is mocked
277
- // The actual invalidate call would happen in the real component
278
- expect(mockSpringConfig.onChange).toHaveBeenCalled()
279
- })
280
-
281
- it("should handle onResolve callback", async () => {
282
- const mockOnRotationChanged = vi.fn()
283
- renderRobotAnimator({ onRotationChanged: mockOnRotationChanged })
284
-
285
- // Wait for initialization
286
- await waitFor(() => {
287
- expect(mockCollectJoints).toHaveBeenCalled()
288
- })
289
-
290
- await act(async () => {
291
- // Simulate spring onResolve callback
292
- if (mockSpringConfig.onResolve) {
293
- mockSpringConfig.onResolve()
294
- }
295
- })
296
-
297
- expect(mockOnRotationChanged).toHaveBeenCalled()
298
- })
299
-
300
- it("should filter out undefined joint values", async () => {
301
- const motionStateWithUndefined = {
302
- state: {
303
- joint_position: {
304
- joints: [1, undefined, 3, undefined, 5, 6],
305
- },
306
- },
307
- } as any
308
-
309
- renderRobotAnimator({
310
- rapidlyChangingMotionState: motionStateWithUndefined,
311
- })
312
-
313
- // Wait for initialization
314
- await waitFor(() => {
315
- expect(mockCollectJoints).toHaveBeenCalled()
316
- })
317
-
318
- await act(async () => {
319
- if (mockAutorunCallback) {
320
- mockAutorunCallback()
321
- }
322
- })
323
-
324
- // Should call setSpring.start with filtered values
325
- expect(mockSetSpring.start).toHaveBeenCalledWith(
326
- expect.objectContaining({
327
- 0: 1,
328
- 1: 3,
329
- 2: 5,
330
- 3: 6,
331
- }),
332
- )
333
- })
334
-
335
- it("should handle empty joint objects gracefully", async () => {
336
- mockCollectJoints.mockReturnValue([])
337
-
338
- const { container } = renderRobotAnimator()
339
-
340
- await act(async () => {
341
- if (mockAutorunCallback) {
342
- mockAutorunCallback()
343
- }
344
- })
345
-
346
- // Should not throw and should not call setSpring.start
347
- expect(mockSetSpring.start).not.toHaveBeenCalled()
348
- })
349
- })