@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.4e55758

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.4e55758",
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,53 @@ 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
53
  const [axisValues, setSpring] = useSpring(() => ({
59
54
  ...Object.assign(
60
55
  {},
61
56
  rapidlyChangingMotionState.state.joint_position.joints,
62
57
  ),
63
58
  onChange: () => {
64
- if (isInitialized) {
65
- setRotation()
66
- invalidate()
67
- }
59
+ setRotation()
60
+ invalidate()
68
61
  },
69
62
  onResolve: () => {
70
- if (isInitialized) {
71
- setRotation()
72
- }
63
+ setRotation()
73
64
  },
74
65
  }))
75
66
 
76
- const setRotation = useCallback(() => {
77
- if (!isInitialized) return
67
+ function setRotation() {
68
+ // Safety check to prevent race condition
69
+ if (
70
+ jointObjects.current.length === 0 ||
71
+ !jointObjects.current.every((obj) => obj != null)
72
+ ) {
73
+ return
74
+ }
78
75
 
79
76
  const updatedJointValues = jointObjects.current.map((_, objectIndex) =>
80
77
  (axisValues as any)[objectIndex].get(),
@@ -92,19 +89,9 @@ export default function RobotAnimator({
92
89
  rotationSign * updatedJointValues[index]! + rotationOffset
93
90
  }
94
91
  }
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])
92
+ }
104
93
 
105
94
  useAutorun(() => {
106
- if (!isInitialized) return
107
-
108
95
  const newJointValues =
109
96
  rapidlyChangingMotionState.state.joint_position.joints.filter(
110
97
  (item) => item !== undefined,
@@ -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
- })