@wandelbots/wandelbots-js-react-components 2.27.1-pr.feature-update-to-react-19.361.57aba09 → 2.27.1-pr.feature-add-program-control-component.367.8730636

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/Setup.d.ts +1 -1
  3. package/dist/Setup.d.ts.map +1 -1
  4. package/dist/components/3d-viewport/SafetyZonesRenderer.d.ts +2 -2
  5. package/dist/components/3d-viewport/SafetyZonesRenderer.d.ts.map +1 -1
  6. package/dist/components/3d-viewport/TrajectoryRenderer.d.ts +1 -1
  7. package/dist/components/3d-viewport/TrajectoryRenderer.d.ts.map +1 -1
  8. package/dist/components/ProgramControl.d.ts +43 -0
  9. package/dist/components/ProgramControl.d.ts.map +1 -0
  10. package/dist/components/robots/DHRobot.d.ts.map +1 -1
  11. package/dist/components/robots/GenericRobot.d.ts +2 -2
  12. package/dist/components/robots/GenericRobot.d.ts.map +1 -1
  13. package/dist/components/robots/Robot.d.ts +2 -2
  14. package/dist/components/robots/Robot.d.ts.map +1 -1
  15. package/dist/components/robots/RobotAnimator.d.ts.map +1 -1
  16. package/dist/components/robots/SupportedRobot.d.ts +3 -3
  17. package/dist/components/robots/SupportedRobot.d.ts.map +1 -1
  18. package/dist/externalizeComponent.d.ts +1 -1
  19. package/dist/externalizeComponent.d.ts.map +1 -1
  20. package/dist/index.cjs +47 -39
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9441 -7734
  25. package/dist/index.js.map +1 -1
  26. package/package.json +32 -33
  27. package/src/Setup.tsx +1 -1
  28. package/src/components/3d-viewport/SafetyZonesRenderer.tsx +2 -2
  29. package/src/components/3d-viewport/TrajectoryRenderer.tsx +1 -1
  30. package/src/components/ProgramControl.tsx +217 -0
  31. package/src/components/jogging/JoggingOptions.tsx +1 -1
  32. package/src/components/robots/DHRobot.tsx +10 -37
  33. package/src/components/robots/GenericRobot.tsx +5 -4
  34. package/src/components/robots/Robot.tsx +2 -2
  35. package/src/components/robots/RobotAnimator.tsx +23 -38
  36. package/src/components/robots/SupportedRobot.tsx +3 -3
  37. package/src/components/utils/converters.ts +1 -1
  38. package/src/externalizeComponent.tsx +1 -1
  39. package/src/i18n/locales/de/translations.json +5 -1
  40. package/src/i18n/locales/en/translations.json +5 -1
  41. package/src/index.ts +1 -1
  42. package/dist/components/robots/RobotAnimator.test.d.ts +0 -2
  43. package/dist/components/robots/RobotAnimator.test.d.ts.map +0 -1
  44. package/dist/components/utils/interpolation.d.ts +0 -159
  45. package/dist/components/utils/interpolation.d.ts.map +0 -1
  46. package/dist/components/utils/interpolation.test.d.ts +0 -2
  47. package/dist/components/utils/interpolation.test.d.ts.map +0 -1
  48. package/dist/test/setup.d.ts +0 -2
  49. package/dist/test/setup.d.ts.map +0 -1
  50. package/src/components/robots/RobotAnimator.test.tsx +0 -113
  51. package/src/components/utils/interpolation.test.ts +0 -1123
  52. package/src/components/utils/interpolation.ts +0 -379
  53. package/src/test/setup.ts +0 -111
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.27.1-pr.feature-update-to-react-19.361.57aba09",
3
+ "version": "2.27.1-pr.feature-add-program-control-component.367.8730636",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -25,8 +25,6 @@
25
25
  "dev:wbjs": "nodemon --watch $WBJS_PATH -e tgz --exec 'npm install file:$WBJS_PATH && storybook dev -p 6006 --no-open'",
26
26
  "tsc": "tsc --pretty --noEmit -p stories/tsconfig.json",
27
27
  "test": "test-storybook --url http://127.0.0.1:6006 --index-json --browsers chromium",
28
- "test:unit": "vitest",
29
- "test:unit:run": "vitest run",
30
28
  "ci:test": "run-s ci:test:build ci:test:built",
31
29
  "ci:test:build": "storybook build --quiet --test",
32
30
  "ci:test:built": "run-p --race ci:test:server ci:test:runner",
@@ -50,44 +48,45 @@
50
48
  "devDependencies": {
51
49
  "@emotion/react": "^11.14.0",
52
50
  "@emotion/styled": "^11.14.0",
53
- "@mui/icons-material": "^7.1.1",
54
- "@mui/material": "^7.1.1",
55
- "@react-three/drei": "^10.2.0",
56
- "@react-three/fiber": "^9.1.2",
51
+ "@mui/icons-material": "^6.4.1",
52
+ "@mui/material": "^6.4.0",
53
+ "@react-spring/three": "^9.7.5",
54
+ "@react-three/drei": "^9.122.0",
55
+ "@react-three/fiber": "^8.18.0",
57
56
  "@rollup/plugin-commonjs": "^28.0.2",
58
57
  "@rollup/plugin-json": "^6.1.0",
59
58
  "@rollup/plugin-node-resolve": "^16.0.0",
60
59
  "@rollup/plugin-terser": "^0.4.4",
61
60
  "@rollup/plugin-typescript": "^12.1.2",
62
- "@storybook/addon-docs": "^9.0.8",
63
- "@storybook/react-vite": "^9.0.8",
64
- "@storybook/test-runner": "^0.23.0",
61
+ "@storybook/addon-docs": "^8.6.4",
62
+ "@storybook/addon-essentials": "^8.6.4",
63
+ "@storybook/addon-interactions": "^8.6.4",
64
+ "@storybook/blocks": "^8.6.4",
65
+ "@storybook/csf-tools": "^8.6.4",
66
+ "@storybook/react": "^8.6.4",
67
+ "@storybook/react-vite": "^8.6.4",
68
+ "@storybook/test": "^8.6.4",
69
+ "@storybook/test-runner": "^0.21.3",
70
+ "@storybook/types": "^8.6.4",
65
71
  "@svgr/rollup": "^8.1.0",
66
- "@testing-library/jest-dom": "^6.6.3",
67
- "@testing-library/react": "^16.3.0",
68
- "@testing-library/user-event": "^14.6.1",
69
72
  "@types/lodash-es": "^4.17.12",
70
- "@types/react": "^19.1.8",
73
+ "@types/react": "^18.3.12",
71
74
  "@types/three": "^0.174.0",
72
75
  "@vitejs/plugin-react": "^4.3.4",
73
- "@wandelbots/nova-js": "^2.1.0",
76
+ "@wandelbots/nova-js": "^2.0.1",
74
77
  "add": "^2.0.6",
75
- "eslint-plugin-storybook": "^9.0.8",
76
78
  "glob": "^11.0.1",
77
79
  "http-server": "^14.1.1",
78
80
  "husky": "^9.1.7",
79
- "i18next": "^25.2.1",
80
81
  "jest-simple-dot-reporter": "^1.0.5",
81
82
  "jest-summary-reporter": "^0.0.2",
82
- "jsdom": "^26.1.0",
83
- "monaco-editor": "^0.52.2",
84
83
  "nodemon": "^3.1.9",
85
84
  "npm-run-all": "^4.1.5",
86
85
  "postcss": "^8.5.3",
87
86
  "prettier-eslint": "^16.3.0",
88
87
  "prop-types": "^15.8.1",
89
- "react": "^19.1.0",
90
- "react-dom": "^19.1.0",
88
+ "react": "^18.3.1",
89
+ "react-dom": "^18.3.1",
91
90
  "rimraf": "^6.0.1",
92
91
  "rollup": "^4.34.9",
93
92
  "rollup-plugin-dts": "^6.1.1",
@@ -95,7 +94,8 @@
95
94
  "rollup-plugin-peer-deps-external": "^2.2.4",
96
95
  "rollup-plugin-postcss": "^4.0.2",
97
96
  "semantic-release": "^24.2.3",
98
- "storybook": "^9.0.8",
97
+ "storybook": "^8.6.4",
98
+ "storybook-dark-mode": "^4.0.2",
99
99
  "storybook-preset-inline-svg": "^1.0.1",
100
100
  "three": "^0.174.0",
101
101
  "three-stdlib": "^2.35.14",
@@ -110,12 +110,11 @@
110
110
  "peerDependencies": {
111
111
  "@emotion/react": "^11.11.1",
112
112
  "@emotion/styled": "^11.11.0",
113
- "@mui/icons-material": "^6 || ^7",
114
- "@mui/material": "^6 || ^7",
115
- "@react-three/drei": "^9.122.0 || ^10",
116
- "@react-three/fiber": "^8 || ^9",
117
- "react": "^18.0.0 || ^19.0.0",
118
- "react-dom": "^18.0.0 || ^19.0.0",
113
+ "@mui/icons-material": "^6",
114
+ "@mui/material": "^6",
115
+ "@react-spring/three": "^9",
116
+ "@react-three/drei": "^9.122.0",
117
+ "@react-three/fiber": "^8.18.0",
119
118
  "three": ">=0.174",
120
119
  "three-stdlib": ">=2"
121
120
  },
@@ -123,6 +122,9 @@
123
122
  "react-dom": {
124
123
  "optional": true
125
124
  },
125
+ "@react-spring/three": {
126
+ "optional": true
127
+ },
126
128
  "@react-three/drei": {
127
129
  "optional": true
128
130
  },
@@ -143,11 +145,8 @@
143
145
  "lodash-es": "^4.17.21",
144
146
  "mobx": "^6.13.6",
145
147
  "mobx-react-lite": "^4.1.0",
146
- "react-error-boundary": "^6.0.0",
147
- "react-i18next": "^15.5.2",
148
+ "react-error-boundary": "^5.0.0",
149
+ "react-i18next": "^15.4.1",
148
150
  "shiki": "^3.1.0"
149
- },
150
- "overrides": {
151
- "storybook": "$storybook"
152
151
  }
153
152
  }
package/src/Setup.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OrbitControls } from "@react-three/drei"
2
- import { Canvas, type CanvasProps } from "@react-three/fiber"
2
+ import { Canvas, type Props as CanvasProps } from "@react-three/fiber"
3
3
  import * as React from "react"
4
4
  import { Vector3 } from "three"
5
5
  import { PresetEnvironment } from "./components/3d-viewport/PresetEnvironment"
@@ -1,4 +1,4 @@
1
- import { type ThreeElements } from "@react-three/fiber"
1
+ import { type GroupProps } from "@react-three/fiber"
2
2
  import type { Geometry } from "@wandelbots/nova-api/v1"
3
3
  import type { SafetySetupSafetyZone } from "@wandelbots/nova-js/v1"
4
4
  import * as THREE from "three"
@@ -6,7 +6,7 @@ import { ConvexGeometry } from "three-stdlib"
6
6
 
7
7
  export type SafetyZonesRendererProps = {
8
8
  safetyZones: SafetySetupSafetyZone[]
9
- } & ThreeElements["group"]
9
+ } & GroupProps
10
10
 
11
11
  interface CoplanarityResult {
12
12
  isCoplanar: boolean
@@ -4,7 +4,7 @@ import * as THREE from "three"
4
4
 
5
5
  export type TrajectoryRendererProps = {
6
6
  trajectory: GetTrajectoryResponse
7
- } & React.JSX.IntrinsicElements["group"]
7
+ } & JSX.IntrinsicElements["group"]
8
8
 
9
9
  export function TrajectoryRenderer({
10
10
  trajectory,
@@ -0,0 +1,217 @@
1
+ import { Pause, PlayArrow, Stop } from "@mui/icons-material"
2
+ import { Box, Button, Typography, useTheme } from "@mui/material"
3
+ import { observer } from "mobx-react-lite"
4
+ import { useTranslation } from "react-i18next"
5
+ import { externalizeComponent } from "../externalizeComponent"
6
+
7
+ export type ProgramState = "idle" | "running" | "paused" | "stopping"
8
+
9
+ export interface ProgramControlProps {
10
+ /** The current state of the program control */
11
+ state: ProgramState
12
+ /** Callback fired when the run/resume button is clicked */
13
+ onRun: () => void
14
+ /** Callback fired when the pause button is clicked (only available in 'with_pause' variant) */
15
+ onPause?: () => void
16
+ /** Callback fired when the stop button is clicked */
17
+ onStop: () => void
18
+ /**
19
+ * Function to reset the component from 'stopping' state back to 'idle'.
20
+ * This must be called manually by the user when requiresManualReset is true.
21
+ */
22
+ onReset?: () => void
23
+ /**
24
+ * When true, the component will stay in 'stopping' state until onReset is called manually.
25
+ * When false (default), auto-resets to 'idle' after 2 seconds.
26
+ */
27
+ requiresManualReset?: boolean
28
+ /**
29
+ * Variant of the component:
30
+ * - 'with_pause': Shows run/pause/stop buttons (default)
31
+ * - 'without_pause': Shows only run/stop buttons
32
+ */
33
+ variant?: "with_pause" | "without_pause"
34
+ /** Additional CSS class name */
35
+ className?: string
36
+ }
37
+
38
+ interface ButtonConfig {
39
+ enabled: boolean
40
+ label: string
41
+ color: string
42
+ onClick: () => void
43
+ }
44
+
45
+ /**
46
+ * A control component for program execution with run, pause, and stop functionality.
47
+ *
48
+ * Features:
49
+ * - State machine with idle, running, paused, and stopping states
50
+ * - Two variants: with_pause (3 buttons) and without_pause (2 buttons)
51
+ * - Optional manual reset functionality
52
+ * - Responsive design with 110px circular buttons
53
+ * - Material-UI theming integration
54
+ */
55
+ export const ProgramControl = externalizeComponent(
56
+ observer(
57
+ ({
58
+ state,
59
+ onRun,
60
+ onPause,
61
+ onStop,
62
+ onReset,
63
+ requiresManualReset = false,
64
+ variant = "with_pause",
65
+ className,
66
+ }: ProgramControlProps) => {
67
+ const theme = useTheme()
68
+ const { t } = useTranslation()
69
+
70
+ const getButtonConfigs = (): ButtonConfig[] => {
71
+ const baseConfigs: Record<string, ButtonConfig> = {
72
+ run: {
73
+ enabled: state === "idle" || state === "paused",
74
+ label:
75
+ state === "paused"
76
+ ? t("ProgramControl.Resume.bt")
77
+ : t("ProgramControl.Run.bt"),
78
+ color: theme.palette.success.main,
79
+ onClick: onRun,
80
+ },
81
+ pause: {
82
+ enabled: state === "running",
83
+ label: t("ProgramControl.Pause.bt"),
84
+ color: "#FFFFFF33",
85
+ onClick: onPause || (() => {}),
86
+ },
87
+ stop: {
88
+ enabled: state === "running" || state === "paused",
89
+ label: t("ProgramControl.Stop.bt"),
90
+ color: theme.palette.error.main,
91
+ onClick: onStop,
92
+ },
93
+ }
94
+
95
+ if (variant === "without_pause") {
96
+ return [baseConfigs.run, baseConfigs.stop]
97
+ }
98
+
99
+ return [baseConfigs.run, baseConfigs.pause, baseConfigs.stop]
100
+ }
101
+
102
+ const getButtonIcon = (index: number) => {
103
+ const iconProps = { sx: { fontSize: "55px" } }
104
+
105
+ if (variant === "without_pause") {
106
+ return index === 0 ? (
107
+ <PlayArrow {...iconProps} />
108
+ ) : (
109
+ <Stop {...iconProps} />
110
+ )
111
+ }
112
+
113
+ switch (index) {
114
+ case 0:
115
+ return <PlayArrow {...iconProps} />
116
+ case 1:
117
+ return <Pause {...iconProps} />
118
+ case 2:
119
+ return <Stop {...iconProps} />
120
+ default:
121
+ return null
122
+ }
123
+ }
124
+
125
+ const buttonConfigs = getButtonConfigs()
126
+
127
+ return (
128
+ <Box
129
+ className={className}
130
+ sx={{
131
+ display: "flex",
132
+ flexDirection: "column",
133
+ alignItems: "center",
134
+ gap: 2,
135
+ }}
136
+ >
137
+ <Box
138
+ sx={{
139
+ display: "flex",
140
+ gap: "40px",
141
+ flexWrap: "wrap",
142
+ justifyContent: "center",
143
+ alignItems: "center",
144
+ }}
145
+ >
146
+ {buttonConfigs.map((config, index) => (
147
+ <Box
148
+ key={config.label}
149
+ sx={{
150
+ display: "flex",
151
+ flexDirection: "column",
152
+ alignItems: "center",
153
+ gap: 1,
154
+ }}
155
+ >
156
+ <Button
157
+ variant="contained"
158
+ disabled={
159
+ !config.enabled ||
160
+ (state === "stopping" && !requiresManualReset)
161
+ }
162
+ onClick={config.onClick}
163
+ sx={{
164
+ width: "110px",
165
+ height: "110px",
166
+ borderRadius: "110px",
167
+ backgroundColor: config.color,
168
+ opacity:
169
+ config.enabled &&
170
+ !(state === "stopping" && !requiresManualReset)
171
+ ? 1
172
+ : 0.3,
173
+ "&:hover": {
174
+ backgroundColor: config.color,
175
+ opacity:
176
+ config.enabled &&
177
+ !(state === "stopping" && !requiresManualReset)
178
+ ? 0.8
179
+ : 0.3,
180
+ },
181
+ "&:disabled": {
182
+ backgroundColor: config.color,
183
+ opacity: 0.3,
184
+ },
185
+ minWidth: "110px",
186
+ flexShrink: 0,
187
+ }}
188
+ >
189
+ {getButtonIcon(index)}
190
+ </Button>
191
+
192
+ <Typography
193
+ variant="body1"
194
+ sx={{
195
+ color:
196
+ config.enabled &&
197
+ !(state === "stopping" && !requiresManualReset)
198
+ ? config.color
199
+ : theme.palette.text.disabled,
200
+ textAlign: "center",
201
+ opacity:
202
+ config.enabled &&
203
+ !(state === "stopping" && !requiresManualReset)
204
+ ? 1
205
+ : 0.3,
206
+ }}
207
+ >
208
+ {config.label}
209
+ </Typography>
210
+ </Box>
211
+ ))}
212
+ </Box>
213
+ </Box>
214
+ )
215
+ },
216
+ ),
217
+ )
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  export const JoggingOptions = observer(({ store }: { store: JoggingStore }) => {
13
13
  const { t } = useTranslation()
14
- const joggingOptions: React.ReactElement[] = []
14
+ const joggingOptions = []
15
15
 
16
16
  function translateOrientation(orientation: OrientationId): string {
17
17
  switch (orientation) {
@@ -1,6 +1,5 @@
1
1
  import { Line } from "@react-three/drei"
2
2
  import type { DHParameter } from "@wandelbots/nova-api/v1"
3
- import React, { useRef } from "react"
4
3
  import type * as THREE from "three"
5
4
  import { Matrix4, Quaternion, Vector3 } from "three"
6
5
  import type { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js"
@@ -18,16 +17,6 @@ export function DHRobot({
18
17
  // reused in every update
19
18
  const accumulatedMatrix = new Matrix4()
20
19
 
21
- // Store direct references to avoid searching by name
22
- const lineRefs = useRef<any[]>([])
23
- const meshRefs = useRef<(THREE.Mesh | null)[]>([])
24
-
25
- // Initialize refs array when dhParameters change
26
- React.useEffect(() => {
27
- lineRefs.current = new Array(dhParameters.length).fill(null)
28
- meshRefs.current = new Array(dhParameters.length).fill(null)
29
- }, [dhParameters.length])
30
-
31
20
  // Updates accumulatedMatrix with every execution
32
21
  // Reset the matrix to identity if you start a new position update
33
22
  function getLinePoints(
@@ -60,7 +49,7 @@ export function DHRobot({
60
49
 
61
50
  function setJointLineRotation(
62
51
  jointIndex: number,
63
- line: any, // Use any for drei Line component
52
+ line: THREE.Line,
64
53
  mesh: THREE.Mesh,
65
54
  jointValue: number,
66
55
  ) {
@@ -82,20 +71,14 @@ export function DHRobot({
82
71
 
83
72
  function setRotation(joints: THREE.Object3D[], jointValues: number[]) {
84
73
  accumulatedMatrix.identity()
85
-
86
- // Use direct refs instead of searching by name
87
- for (
88
- let jointIndex = 0;
89
- jointIndex < Math.min(joints.length, jointValues.length);
90
- jointIndex++
91
- ) {
92
- const line = lineRefs.current[jointIndex]
93
- const mesh = meshRefs.current[jointIndex]
94
-
95
- if (line && mesh) {
96
- setJointLineRotation(jointIndex, line, mesh, jointValues[jointIndex]!)
97
- }
98
- }
74
+ joints.forEach((joint, jointIndex) => {
75
+ setJointLineRotation(
76
+ jointIndex,
77
+ joint.getObjectByName(CHILD_LINE) as THREE.Line,
78
+ joint.getObjectByName(CHILD_MESH) as THREE.Mesh,
79
+ jointValues[jointIndex]!,
80
+ )
81
+ })
99
82
  }
100
83
 
101
84
  return (
@@ -120,22 +103,12 @@ export function DHRobot({
120
103
  return (
121
104
  <group name={jointName} key={jointName}>
122
105
  <Line
123
- ref={(ref) => {
124
- lineRefs.current[index] = ref
125
- }}
126
106
  name={CHILD_LINE}
127
107
  points={[a, b]}
128
108
  color={"white"}
129
109
  lineWidth={5}
130
110
  />
131
- <mesh
132
- ref={(ref) => {
133
- meshRefs.current[index] = ref
134
- }}
135
- name={CHILD_MESH}
136
- key={"mesh_" + index}
137
- position={b}
138
- >
111
+ <mesh name={CHILD_MESH} key={"mesh_" + index} position={b}>
139
112
  <sphereGeometry args={[0.01, 32, 32]} />
140
113
  <meshStandardMaterial color={"black"} depthTest={true} />
141
114
  </mesh>
@@ -1,5 +1,6 @@
1
+ import { animated } from "@react-spring/three"
1
2
  import { useGLTF } from "@react-three/drei"
2
- import type { ThreeElements } from "@react-three/fiber"
3
+ import type { GroupProps } from "@react-three/fiber"
3
4
  import React, { useCallback } from "react"
4
5
  import type { Group, Mesh } from "three"
5
6
  import { type Object3D } from "three"
@@ -13,7 +14,7 @@ export type RobotModelProps = {
13
14
  */
14
15
  postModelRender?: () => void
15
16
  flangeRef?: React.Ref<Group>
16
- } & ThreeElements["group"]
17
+ } & GroupProps
17
18
 
18
19
  function isMesh(node: Object3D): node is Mesh {
19
20
  return node.type === "Mesh"
@@ -53,7 +54,7 @@ export function GenericRobot({
53
54
  )
54
55
  } else {
55
56
  return (
56
- <group
57
+ <animated.group
57
58
  name={node.name}
58
59
  key={node.uuid}
59
60
  position={node.position}
@@ -61,7 +62,7 @@ export function GenericRobot({
61
62
  ref={isFlange(node) ? flangeRef : undefined}
62
63
  >
63
64
  {node.children.map(renderNode)}
64
- </group>
65
+ </animated.group>
65
66
  )
66
67
  }
67
68
  }
@@ -1,4 +1,4 @@
1
- import { type ThreeElements } from "@react-three/fiber"
1
+ import { type GroupProps } from "@react-three/fiber"
2
2
 
3
3
  import type { ConnectedMotionGroup } from "@wandelbots/nova-js/v1"
4
4
  import type { Group } from "three"
@@ -10,7 +10,7 @@ export type RobotProps = {
10
10
  getModel?: (modelFromController: string) => string
11
11
  flangeRef?: React.Ref<Group>
12
12
  transparentColor?: string
13
- } & ThreeElements["group"]
13
+ } & GroupProps
14
14
 
15
15
  /**
16
16
  * The Robot component is a wrapper around the SupportedRobot component
@@ -1,12 +1,12 @@
1
- import { useFrame, useThree } from "@react-three/fiber"
1
+ import { Globals, useSpring } from "@react-spring/three"
2
+ import { useThree } from "@react-three/fiber"
2
3
  import type {
3
4
  DHParameter,
4
5
  MotionGroupStateResponse,
5
6
  } from "@wandelbots/nova-api/v1"
6
- import React, { useEffect, useRef } from "react"
7
+ import React, { useRef } from "react"
7
8
  import type { Group, Object3D } from "three"
8
9
  import { useAutorun } from "../utils/hooks"
9
- import { ValueInterpolator } from "../utils/interpolation"
10
10
  import { collectJoints } from "./robotModelLogic"
11
11
 
12
12
  type RobotAnimatorProps = {
@@ -22,42 +22,11 @@ export default function RobotAnimator({
22
22
  onRotationChanged,
23
23
  children,
24
24
  }: RobotAnimatorProps) {
25
+ Globals.assign({ frameLoop: "always" })
25
26
  const jointValues = useRef<number[]>([])
26
27
  const jointObjects = useRef<Object3D[]>([])
27
- const interpolatorRef = useRef<ValueInterpolator | null>(null)
28
28
  const { invalidate } = useThree()
29
29
 
30
- // Initialize interpolator
31
- useEffect(() => {
32
- const initialJointValues =
33
- rapidlyChangingMotionState.state.joint_position.joints.filter(
34
- (item) => item !== undefined,
35
- )
36
-
37
- interpolatorRef.current = new ValueInterpolator(initialJointValues, {
38
- tension: 120, // Controls spring stiffness - higher values create faster, more responsive motion
39
- friction: 20, // Controls damping - higher values reduce oscillation and create smoother settling
40
- threshold: 0.001,
41
- })
42
-
43
- return () => {
44
- interpolatorRef.current?.destroy()
45
- }
46
- }, [])
47
-
48
- // Animation loop that runs at the display's refresh rate
49
- useFrame((state, delta) => {
50
- if (interpolatorRef.current) {
51
- const isComplete = interpolatorRef.current.update(delta)
52
- setRotation()
53
-
54
- // Trigger a re-render only if the animation is still running
55
- if (!isComplete) {
56
- invalidate()
57
- }
58
- }
59
- })
60
-
61
30
  function setGroupRef(group: Group | null) {
62
31
  if (!group) return
63
32
 
@@ -70,11 +39,13 @@ export default function RobotAnimator({
70
39
 
71
40
  function updateJoints(newJointValues: number[]) {
72
41
  jointValues.current = newJointValues
73
- interpolatorRef.current?.setTarget(newJointValues)
42
+ setSpring.start(Object.assign({}, jointValues.current) as any)
74
43
  }
75
44
 
76
45
  function setRotation() {
77
- const updatedJointValues = interpolatorRef.current?.getCurrentValues() || []
46
+ const updatedJointValues = jointObjects.current.map((_, objectIndex) =>
47
+ (axisValues as any)[objectIndex].get(),
48
+ )
78
49
 
79
50
  if (onRotationChanged) {
80
51
  onRotationChanged(jointObjects.current, updatedJointValues)
@@ -85,7 +56,7 @@ export default function RobotAnimator({
85
56
  const rotationSign = dhParam.reverse_rotation_direction ? -1 : 1
86
57
 
87
58
  object.rotation.y =
88
- rotationSign * (updatedJointValues[index] || 0) + rotationOffset
59
+ rotationSign * updatedJointValues[index]! + rotationOffset
89
60
  }
90
61
  }
91
62
  }
@@ -99,5 +70,19 @@ export default function RobotAnimator({
99
70
  requestAnimationFrame(() => updateJoints(newJointValues))
100
71
  })
101
72
 
73
+ const [axisValues, setSpring] = useSpring(() => ({
74
+ ...Object.assign(
75
+ {},
76
+ rapidlyChangingMotionState.state.joint_position.joints,
77
+ ),
78
+ onChange: () => {
79
+ setRotation()
80
+ invalidate()
81
+ },
82
+ onResolve: () => {
83
+ setRotation()
84
+ },
85
+ }))
86
+
102
87
  return <group ref={setGroupRef}>{children}</group>
103
88
  }
@@ -1,4 +1,4 @@
1
- import type { ThreeElements } from "@react-three/fiber"
1
+ import type { GroupProps } from "@react-three/fiber"
2
2
  import type {
3
3
  DHParameter,
4
4
  MotionGroupStateResponse,
@@ -18,7 +18,7 @@ import { defaultGetModel } from "./robotModelLogic"
18
18
  export type DHRobotProps = {
19
19
  rapidlyChangingMotionState: MotionGroupStateResponse
20
20
  dhParameters: Array<DHParameter>
21
- } & ThreeElements["group"]
21
+ } & GroupProps
22
22
 
23
23
  export type SupportedRobotProps = {
24
24
  rapidlyChangingMotionState: MotionGroupStateResponse
@@ -28,7 +28,7 @@ export type SupportedRobotProps = {
28
28
  getModel?: (modelFromController: string) => string
29
29
  postModelRender?: () => void
30
30
  transparentColor?: string
31
- } & ThreeElements["group"]
31
+ } & GroupProps
32
32
 
33
33
  export const SupportedRobot = externalizeComponent(
34
34
  ({
@@ -20,4 +20,4 @@ export function tryStringifyJson(json: unknown): string | undefined {
20
20
  } catch {
21
21
  return undefined
22
22
  }
23
- }
23
+ }
@@ -7,7 +7,7 @@ import { i18n } from "./i18n/config"
7
7
  * be provided by the user application; this wrapper ensures
8
8
  * they can be used either way.
9
9
  */
10
- export function externalizeComponent<T extends React.JSX.ElementType>(
10
+ export function externalizeComponent<T extends JSX.ElementType>(
11
11
  Component: T,
12
12
  ): T {
13
13
  const WrappedComponent = ((props: T) => (