@wandelbots/wandelbots-js-react-components 2.27.1 → 2.28.0-pr.feature-add-program-control-component.367.28afd72

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/RobotAnimator.test.d.ts +2 -0
  17. package/dist/components/robots/RobotAnimator.test.d.ts.map +1 -0
  18. package/dist/components/robots/SupportedRobot.d.ts +3 -3
  19. package/dist/components/robots/SupportedRobot.d.ts.map +1 -1
  20. package/dist/components/utils/interpolation.d.ts +159 -0
  21. package/dist/components/utils/interpolation.d.ts.map +1 -0
  22. package/dist/components/utils/interpolation.test.d.ts +2 -0
  23. package/dist/components/utils/interpolation.test.d.ts.map +1 -0
  24. package/dist/externalizeComponent.d.ts +1 -1
  25. package/dist/externalizeComponent.d.ts.map +1 -1
  26. package/dist/index.cjs +39 -47
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +8283 -9714
  31. package/dist/index.js.map +1 -1
  32. package/dist/test/setup.d.ts +2 -0
  33. package/dist/test/setup.d.ts.map +1 -0
  34. package/package.json +33 -32
  35. package/src/Setup.tsx +1 -1
  36. package/src/components/3d-viewport/SafetyZonesRenderer.tsx +2 -2
  37. package/src/components/3d-viewport/TrajectoryRenderer.tsx +1 -1
  38. package/src/components/ProgramControl.tsx +217 -0
  39. package/src/components/jogging/JoggingOptions.tsx +1 -1
  40. package/src/components/robots/DHRobot.tsx +37 -10
  41. package/src/components/robots/GenericRobot.tsx +4 -5
  42. package/src/components/robots/Robot.tsx +2 -2
  43. package/src/components/robots/RobotAnimator.test.tsx +113 -0
  44. package/src/components/robots/RobotAnimator.tsx +38 -23
  45. package/src/components/robots/SupportedRobot.tsx +3 -3
  46. package/src/components/utils/converters.ts +1 -1
  47. package/src/components/utils/interpolation.test.ts +1123 -0
  48. package/src/components/utils/interpolation.ts +379 -0
  49. package/src/externalizeComponent.tsx +1 -1
  50. package/src/i18n/locales/de/translations.json +5 -1
  51. package/src/i18n/locales/en/translations.json +5 -1
  52. package/src/index.ts +2 -0
  53. package/src/test/setup.ts +111 -0
@@ -0,0 +1,2 @@
1
+ import "@testing-library/jest-dom";
2
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/test/setup.ts"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.27.1",
3
+ "version": "2.28.0-pr.feature-add-program-control-component.367.28afd72",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -25,6 +25,8 @@
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 --workspace vitest.workspace.ts --project unit",
29
+ "test:unit:run": "vitest run --workspace vitest.workspace.ts --project unit",
28
30
  "ci:test": "run-s ci:test:build ci:test:built",
29
31
  "ci:test:build": "storybook build --quiet --test",
30
32
  "ci:test:built": "run-p --race ci:test:server ci:test:runner",
@@ -48,45 +50,44 @@
48
50
  "devDependencies": {
49
51
  "@emotion/react": "^11.14.0",
50
52
  "@emotion/styled": "^11.14.0",
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",
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",
56
57
  "@rollup/plugin-commonjs": "^28.0.2",
57
58
  "@rollup/plugin-json": "^6.1.0",
58
59
  "@rollup/plugin-node-resolve": "^16.0.0",
59
60
  "@rollup/plugin-terser": "^0.4.4",
60
61
  "@rollup/plugin-typescript": "^12.1.2",
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",
62
+ "@storybook/addon-docs": "^9.0.8",
63
+ "@storybook/react-vite": "^9.0.8",
64
+ "@storybook/test-runner": "^0.23.0",
71
65
  "@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",
72
69
  "@types/lodash-es": "^4.17.12",
73
- "@types/react": "^18.3.12",
70
+ "@types/react": "^19.1.8",
74
71
  "@types/three": "^0.174.0",
75
72
  "@vitejs/plugin-react": "^4.3.4",
76
- "@wandelbots/nova-js": "^2.0.1",
73
+ "@wandelbots/nova-js": "^2.1.0",
77
74
  "add": "^2.0.6",
75
+ "eslint-plugin-storybook": "^9.0.8",
78
76
  "glob": "^11.0.1",
79
77
  "http-server": "^14.1.1",
80
78
  "husky": "^9.1.7",
79
+ "i18next": "^25.2.1",
81
80
  "jest-simple-dot-reporter": "^1.0.5",
82
81
  "jest-summary-reporter": "^0.0.2",
82
+ "jsdom": "^26.1.0",
83
+ "monaco-editor": "^0.52.2",
83
84
  "nodemon": "^3.1.9",
84
85
  "npm-run-all": "^4.1.5",
85
86
  "postcss": "^8.5.3",
86
87
  "prettier-eslint": "^16.3.0",
87
88
  "prop-types": "^15.8.1",
88
- "react": "^18.3.1",
89
- "react-dom": "^18.3.1",
89
+ "react": "^19.1.0",
90
+ "react-dom": "^19.1.0",
90
91
  "rimraf": "^6.0.1",
91
92
  "rollup": "^4.34.9",
92
93
  "rollup-plugin-dts": "^6.1.1",
@@ -94,8 +95,7 @@
94
95
  "rollup-plugin-peer-deps-external": "^2.2.4",
95
96
  "rollup-plugin-postcss": "^4.0.2",
96
97
  "semantic-release": "^24.2.3",
97
- "storybook": "^8.6.4",
98
- "storybook-dark-mode": "^4.0.2",
98
+ "storybook": "^9.0.8",
99
99
  "storybook-preset-inline-svg": "^1.0.1",
100
100
  "three": "^0.174.0",
101
101
  "three-stdlib": "^2.35.14",
@@ -110,11 +110,12 @@
110
110
  "peerDependencies": {
111
111
  "@emotion/react": "^11.11.1",
112
112
  "@emotion/styled": "^11.11.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",
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",
118
119
  "three": ">=0.174",
119
120
  "three-stdlib": ">=2"
120
121
  },
@@ -122,9 +123,6 @@
122
123
  "react-dom": {
123
124
  "optional": true
124
125
  },
125
- "@react-spring/three": {
126
- "optional": true
127
- },
128
126
  "@react-three/drei": {
129
127
  "optional": true
130
128
  },
@@ -145,8 +143,11 @@
145
143
  "lodash-es": "^4.17.21",
146
144
  "mobx": "^6.13.6",
147
145
  "mobx-react-lite": "^4.1.0",
148
- "react-error-boundary": "^5.0.0",
149
- "react-i18next": "^15.4.1",
146
+ "react-error-boundary": "^6.0.0",
147
+ "react-i18next": "^15.5.2",
150
148
  "shiki": "^3.1.0"
149
+ },
150
+ "overrides": {
151
+ "storybook": "$storybook"
151
152
  }
152
153
  }
package/src/Setup.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OrbitControls } from "@react-three/drei"
2
- import { Canvas, type Props as CanvasProps } from "@react-three/fiber"
2
+ import { Canvas, type 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 GroupProps } from "@react-three/fiber"
1
+ import { type ThreeElements } 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
- } & GroupProps
9
+ } & ThreeElements["group"]
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
- } & JSX.IntrinsicElements["group"]
7
+ } & React.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.Start.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 = []
14
+ const joggingOptions: React.ReactElement[] = []
15
15
 
16
16
  function translateOrientation(orientation: OrientationId): string {
17
17
  switch (orientation) {
@@ -1,5 +1,6 @@
1
1
  import { Line } from "@react-three/drei"
2
2
  import type { DHParameter } from "@wandelbots/nova-api/v1"
3
+ import React, { useRef } from "react"
3
4
  import type * as THREE from "three"
4
5
  import { Matrix4, Quaternion, Vector3 } from "three"
5
6
  import type { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js"
@@ -17,6 +18,16 @@ export function DHRobot({
17
18
  // reused in every update
18
19
  const accumulatedMatrix = new Matrix4()
19
20
 
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
+
20
31
  // Updates accumulatedMatrix with every execution
21
32
  // Reset the matrix to identity if you start a new position update
22
33
  function getLinePoints(
@@ -49,7 +60,7 @@ export function DHRobot({
49
60
 
50
61
  function setJointLineRotation(
51
62
  jointIndex: number,
52
- line: THREE.Line,
63
+ line: any, // Use any for drei Line component
53
64
  mesh: THREE.Mesh,
54
65
  jointValue: number,
55
66
  ) {
@@ -71,14 +82,20 @@ export function DHRobot({
71
82
 
72
83
  function setRotation(joints: THREE.Object3D[], jointValues: number[]) {
73
84
  accumulatedMatrix.identity()
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
- })
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
+ }
82
99
  }
83
100
 
84
101
  return (
@@ -103,12 +120,22 @@ export function DHRobot({
103
120
  return (
104
121
  <group name={jointName} key={jointName}>
105
122
  <Line
123
+ ref={(ref) => {
124
+ lineRefs.current[index] = ref
125
+ }}
106
126
  name={CHILD_LINE}
107
127
  points={[a, b]}
108
128
  color={"white"}
109
129
  lineWidth={5}
110
130
  />
111
- <mesh name={CHILD_MESH} key={"mesh_" + index} position={b}>
131
+ <mesh
132
+ ref={(ref) => {
133
+ meshRefs.current[index] = ref
134
+ }}
135
+ name={CHILD_MESH}
136
+ key={"mesh_" + index}
137
+ position={b}
138
+ >
112
139
  <sphereGeometry args={[0.01, 32, 32]} />
113
140
  <meshStandardMaterial color={"black"} depthTest={true} />
114
141
  </mesh>
@@ -1,6 +1,5 @@
1
- import { animated } from "@react-spring/three"
2
1
  import { useGLTF } from "@react-three/drei"
3
- import type { GroupProps } from "@react-three/fiber"
2
+ import type { ThreeElements } from "@react-three/fiber"
4
3
  import React, { useCallback } from "react"
5
4
  import type { Group, Mesh } from "three"
6
5
  import { type Object3D } from "three"
@@ -14,7 +13,7 @@ export type RobotModelProps = {
14
13
  */
15
14
  postModelRender?: () => void
16
15
  flangeRef?: React.Ref<Group>
17
- } & GroupProps
16
+ } & ThreeElements["group"]
18
17
 
19
18
  function isMesh(node: Object3D): node is Mesh {
20
19
  return node.type === "Mesh"
@@ -54,7 +53,7 @@ export function GenericRobot({
54
53
  )
55
54
  } else {
56
55
  return (
57
- <animated.group
56
+ <group
58
57
  name={node.name}
59
58
  key={node.uuid}
60
59
  position={node.position}
@@ -62,7 +61,7 @@ export function GenericRobot({
62
61
  ref={isFlange(node) ? flangeRef : undefined}
63
62
  >
64
63
  {node.children.map(renderNode)}
65
- </animated.group>
64
+ </group>
66
65
  )
67
66
  }
68
67
  }
@@ -1,4 +1,4 @@
1
- import { type GroupProps } from "@react-three/fiber"
1
+ import { type ThreeElements } 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
- } & GroupProps
13
+ } & ThreeElements["group"]
14
14
 
15
15
  /**
16
16
  * The Robot component is a wrapper around the SupportedRobot component
@@ -0,0 +1,113 @@
1
+ import type {
2
+ DHParameter,
3
+ MotionGroupStateResponse,
4
+ } from "@wandelbots/nova-api/v1"
5
+ import { describe, expect, it, vi } from "vitest"
6
+ import RobotAnimator from "./RobotAnimator"
7
+
8
+ // Mock the dependencies
9
+ vi.mock("./robotModelLogic", () => ({
10
+ collectJoints: vi.fn(),
11
+ }))
12
+
13
+ vi.mock("../utils/interpolation", () => ({
14
+ ValueInterpolator: vi.fn().mockImplementation(() => ({
15
+ setTarget: vi.fn(),
16
+ getCurrentValues: vi.fn(() => []),
17
+ destroy: vi.fn(),
18
+ })),
19
+ }))
20
+
21
+ describe("RobotAnimator", () => {
22
+ it("should export the component correctly", () => {
23
+ expect(RobotAnimator).toBeDefined()
24
+ expect(typeof RobotAnimator).toBe("function")
25
+ })
26
+
27
+ it("should handle props with different numbers of joints", () => {
28
+ // Test that the component accepts different numbers of DH parameters
29
+ const mockMotionState4Joints: MotionGroupStateResponse = {
30
+ state: {
31
+ joint_position: {
32
+ joints: [0.1, 0.2, 0.3, 0.4],
33
+ },
34
+ },
35
+ } as any
36
+
37
+ const mockDHParameters4Joints: DHParameter[] = [
38
+ { theta: 0, reverse_rotation_direction: false },
39
+ { theta: 0, reverse_rotation_direction: false },
40
+ { theta: 0, reverse_rotation_direction: false },
41
+ { theta: 0, reverse_rotation_direction: false },
42
+ ]
43
+
44
+ const mockMotionState7Joints: MotionGroupStateResponse = {
45
+ state: {
46
+ joint_position: {
47
+ joints: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7],
48
+ },
49
+ },
50
+ } as any
51
+
52
+ const mockDHParameters7Joints: DHParameter[] = Array(7).fill({
53
+ theta: 0,
54
+ reverse_rotation_direction: false,
55
+ })
56
+
57
+ // These should not throw TypeScript errors
58
+ expect(() => {
59
+ const props4 = {
60
+ rapidlyChangingMotionState: mockMotionState4Joints,
61
+ dhParameters: mockDHParameters4Joints,
62
+ children: null,
63
+ }
64
+ // Verify props are correctly typed
65
+ expect(props4.dhParameters).toHaveLength(4)
66
+ expect(
67
+ props4.rapidlyChangingMotionState.state.joint_position.joints,
68
+ ).toHaveLength(4)
69
+ }).not.toThrow()
70
+
71
+ expect(() => {
72
+ const props7 = {
73
+ rapidlyChangingMotionState: mockMotionState7Joints,
74
+ dhParameters: mockDHParameters7Joints,
75
+ children: null,
76
+ }
77
+ expect(props7.dhParameters).toHaveLength(7)
78
+ expect(
79
+ props7.rapidlyChangingMotionState.state.joint_position.joints,
80
+ ).toHaveLength(7)
81
+ }).not.toThrow()
82
+ })
83
+
84
+ it("should use custom interpolation for smooth value transitions", () => {
85
+ // Test the rotation calculation logic
86
+ const dhParam1 = { theta: 0.1, reverse_rotation_direction: false }
87
+ const dhParam2 = { theta: -0.2, reverse_rotation_direction: true }
88
+ const jointValue1 = 0.5
89
+ const jointValue2 = 1.0
90
+
91
+ // Calculate expected rotations
92
+ const expectedRotation1 = 1 * jointValue1 + 0.1 // 0.6
93
+ const expectedRotation2 = -1 * jointValue2 + -0.2 // -1.2
94
+
95
+ expect(expectedRotation1).toBe(0.6)
96
+ expect(expectedRotation2).toBe(-1.2)
97
+
98
+ // Test edge cases
99
+ const dhParamReverse = { theta: 0, reverse_rotation_direction: true }
100
+ const jointValueZero = 0
101
+ const expectedRotationReverse = -1 * jointValueZero + 0 // 0
102
+
103
+ expect(expectedRotationReverse).toBe(0)
104
+
105
+ // Test rotation direction logic
106
+ const testValue = 1.5
107
+ const normalParam = { theta: 0, reverse_rotation_direction: false }
108
+ const reversedParam = { theta: 0, reverse_rotation_direction: true }
109
+
110
+ expect(1 * testValue + 0).toBe(1.5) // Normal direction
111
+ expect(-1 * testValue + 0).toBe(-1.5) // Reversed direction
112
+ })
113
+ })