@wandelbots/wandelbots-js-react-components 3.7.2 → 3.7.4-pr.feat-add-linear-axis-support.509.b65ac1c

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 (94) hide show
  1. package/README.md +46 -0
  2. package/dist/3d.cjs.js +1 -1
  3. package/dist/3d.d.ts +2 -1
  4. package/dist/3d.d.ts.map +1 -1
  5. package/dist/3d.es.js +8 -7
  6. package/dist/{LoadingCover-Dr9hDTku.js → LoadingCover-6gWr11KP.js} +11 -10
  7. package/dist/{LoadingCover-Dr9hDTku.js.map → LoadingCover-6gWr11KP.js.map} +1 -1
  8. package/dist/LoadingCover-CukpS_aj.cjs +2 -0
  9. package/dist/{LoadingCover-r2yhJZF9.cjs.map → LoadingCover-CukpS_aj.cjs.map} +1 -1
  10. package/dist/SupportedLinearAxis-Cc5zohcs.cjs +2 -0
  11. package/dist/SupportedLinearAxis-Cc5zohcs.cjs.map +1 -0
  12. package/dist/SupportedLinearAxis-DAK0IyqZ.js +1228 -0
  13. package/dist/SupportedLinearAxis-DAK0IyqZ.js.map +1 -0
  14. package/dist/{WandelscriptEditor-DnJvITTA.js → WandelscriptEditor-7eN-Yw7m.js} +3 -3
  15. package/dist/{WandelscriptEditor-DnJvITTA.js.map → WandelscriptEditor-7eN-Yw7m.js.map} +1 -1
  16. package/dist/WandelscriptEditor-D6_vS5Uk.cjs +2 -0
  17. package/dist/{WandelscriptEditor-Dj7TBCkF.cjs.map → WandelscriptEditor-D6_vS5Uk.cjs.map} +1 -1
  18. package/dist/auth0-spa-js.production.esm-D_IhPirK.cjs +5 -0
  19. package/dist/auth0-spa-js.production.esm-D_IhPirK.cjs.map +1 -0
  20. package/dist/auth0-spa-js.production.esm-mvPojIrB.js +4043 -0
  21. package/dist/auth0-spa-js.production.esm-mvPojIrB.js.map +1 -0
  22. package/dist/components/RobotCard.d.ts +1 -1
  23. package/dist/components/RobotCard.d.ts.map +1 -1
  24. package/dist/components/robots/DHLinearAxis.d.ts +3 -0
  25. package/dist/components/robots/DHLinearAxis.d.ts.map +1 -0
  26. package/dist/components/robots/GenericRobot.d.ts +2 -2
  27. package/dist/components/robots/GenericRobot.d.ts.map +1 -1
  28. package/dist/components/robots/LinearAxis.d.ts +25 -0
  29. package/dist/components/robots/LinearAxis.d.ts.map +1 -0
  30. package/dist/components/robots/LinearAxisAnimator.d.ts +12 -0
  31. package/dist/components/robots/LinearAxisAnimator.d.ts.map +1 -0
  32. package/dist/components/robots/Robot.d.ts +3 -1
  33. package/dist/components/robots/Robot.d.ts.map +1 -1
  34. package/dist/components/robots/SupportedLinearAxis.d.ts +18 -0
  35. package/dist/components/robots/SupportedLinearAxis.d.ts.map +1 -0
  36. package/dist/components/robots/SupportedRobot.d.ts +1 -1
  37. package/dist/components/robots/SupportedRobot.d.ts.map +1 -1
  38. package/dist/components/robots/robotModelLogic.d.ts +11 -1
  39. package/dist/components/robots/robotModelLogic.d.ts.map +1 -1
  40. package/dist/core.cjs.js +1 -1
  41. package/dist/core.es.js +4 -4
  42. package/dist/externalizeComponent-CkVWk2F_.cjs +24 -0
  43. package/dist/externalizeComponent-CkVWk2F_.cjs.map +1 -0
  44. package/dist/externalizeComponent-Dc3fViZA.js +489 -0
  45. package/dist/externalizeComponent-Dc3fViZA.js.map +1 -0
  46. package/dist/index.cjs.js +1 -1
  47. package/dist/index.es.js +58 -57
  48. package/dist/interpolation-DZhBKo-u.cjs +20 -0
  49. package/dist/interpolation-DZhBKo-u.cjs.map +1 -0
  50. package/dist/interpolation-baUmFLkh.js +6924 -0
  51. package/dist/interpolation-baUmFLkh.js.map +1 -0
  52. package/dist/lib/JoggerConnection.d.ts.map +1 -1
  53. package/dist/{theming-DxiD0Q3m.js → theming-BTlS2afw.js} +4951 -6394
  54. package/dist/theming-BTlS2afw.js.map +1 -0
  55. package/dist/theming-DPoEjzxv.cjs +115 -0
  56. package/dist/theming-DPoEjzxv.cjs.map +1 -0
  57. package/dist/wandelscript.cjs.js +1 -1
  58. package/dist/wandelscript.es.js +1 -1
  59. package/package.json +16 -6
  60. package/src/3d.ts +3 -2
  61. package/src/components/RobotCard.tsx +1 -1
  62. package/src/components/robots/DHLinearAxis.tsx +128 -0
  63. package/src/components/robots/GenericRobot.tsx +97 -36
  64. package/src/components/robots/LinearAxis.tsx +73 -0
  65. package/src/components/robots/LinearAxisAnimator.tsx +115 -0
  66. package/src/components/robots/Robot.tsx +3 -1
  67. package/src/components/robots/RobotAnimator.test.tsx +1 -1
  68. package/src/components/robots/SupportedLinearAxis.tsx +99 -0
  69. package/src/components/robots/SupportedRobot.tsx +11 -3
  70. package/src/components/robots/robotModelLogic.ts +75 -6
  71. package/src/env.d.ts +3 -0
  72. package/src/lib/JoggerConnection.ts +10 -10
  73. package/src/lib/MotionStreamConnection.ts +1 -1
  74. package/dist/LoadingCover-r2yhJZF9.cjs +0 -2
  75. package/dist/WandelscriptEditor-Dj7TBCkF.cjs +0 -2
  76. package/dist/auth0-spa-js.production.esm-DL9f1uDJ.js +0 -1438
  77. package/dist/auth0-spa-js.production.esm-DL9f1uDJ.js.map +0 -1
  78. package/dist/auth0-spa-js.production.esm-DTiWXa87.cjs +0 -5
  79. package/dist/auth0-spa-js.production.esm-DTiWXa87.cjs.map +0 -1
  80. package/dist/index-CAib4NKw.js +0 -2261
  81. package/dist/index-CAib4NKw.js.map +0 -1
  82. package/dist/index-CqMZL0FV.cjs +0 -2
  83. package/dist/index-CqMZL0FV.cjs.map +0 -1
  84. package/dist/index-CxasuX80.js +0 -5212
  85. package/dist/index-CxasuX80.js.map +0 -1
  86. package/dist/index-DxwppshT.cjs +0 -29
  87. package/dist/index-DxwppshT.cjs.map +0 -1
  88. package/dist/manufacturerHomePositions-BW0KbWeg.js +0 -980
  89. package/dist/manufacturerHomePositions-BW0KbWeg.js.map +0 -1
  90. package/dist/manufacturerHomePositions-r9dAEtsx.cjs +0 -2
  91. package/dist/manufacturerHomePositions-r9dAEtsx.cjs.map +0 -1
  92. package/dist/theming-CKlFOjJ_.cjs +0 -133
  93. package/dist/theming-CKlFOjJ_.cjs.map +0 -1
  94. package/dist/theming-DxiD0Q3m.js.map +0 -1
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./WandelscriptEditor-Dj7TBCkF.cjs");exports.WandelscriptEditor=t.WandelscriptEditor;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./WandelscriptEditor-D6_vS5Uk.cjs");exports.WandelscriptEditor=t.WandelscriptEditor;
2
2
  //# sourceMappingURL=wandelscript.cjs.js.map
@@ -1,4 +1,4 @@
1
- import { W as t } from "./WandelscriptEditor-DnJvITTA.js";
1
+ import { W as t } from "./WandelscriptEditor-7eN-Yw7m.js";
2
2
  export {
3
3
  t as WandelscriptEditor
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "3.7.2",
3
+ "version": "3.7.4-pr.feat-add-linear-axis-support.509.b65ac1c",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -50,7 +50,8 @@
50
50
  "ci:test:runner": "wait-on tcp:127.0.0.1:9009 && test-storybook --url http://127.0.0.1:9009 --index-json --browsers chromium",
51
51
  "build": "rimraf dist && vite build && tsc --declaration --emitDeclarationOnly",
52
52
  "build-storybook": "storybook build",
53
- "prepare": "husky || true"
53
+ "prepare": "husky || true",
54
+ "td": "tsx scripts/td-wrapper.ts"
54
55
  },
55
56
  "repository": {
56
57
  "type": "git",
@@ -63,6 +64,10 @@
63
64
  ],
64
65
  "author": "Wandelbots",
65
66
  "license": "Apache-2.0",
67
+ "engines": {
68
+ "node": ">=18.14.0",
69
+ "npm": ">=8.0.0"
70
+ },
66
71
  "devDependencies": {
67
72
  "@emotion/react": "^11.14.0",
68
73
  "@emotion/styled": "^11.14.0",
@@ -88,7 +93,7 @@
88
93
  "@types/react": "^19.1.8",
89
94
  "@types/three": "^0.182.0",
90
95
  "@vitejs/plugin-react": "^4.3.4",
91
- "@wandelbots/nova-js": "^3.4.1",
96
+ "@wandelbots/nova-js": "^3.5.0",
92
97
  "add": "^2.0.6",
93
98
  "eslint-plugin-storybook": "^10.1.10",
94
99
  "glob": "^13.0.0",
@@ -119,6 +124,7 @@
119
124
  "three": "^0.182.0",
120
125
  "three-stdlib": "^2.35.14",
121
126
  "ts-dedent": "^2.2.0",
127
+ "tsx": "^4.21.0",
122
128
  "typescript": "^5.8.2",
123
129
  "unplugin": "^1.15.0",
124
130
  "vite": "^6.2.0",
@@ -170,12 +176,16 @@
170
176
  "dependencies": {
171
177
  "@mui/x-charts": "^8.27.0",
172
178
  "@mui/x-data-grid": "^8.27.0",
173
- "i18next-browser-languagedetector": "^8.0.4",
174
- "lodash-es": "^4.17.23",
179
+ "@wandelbots/nova-js": "3.5.1",
180
+ "axios": "^1.13.2",
181
+ "dotenv": "^17.2.3",
182
+ "i18next-browser-languagedetector": "^8.2.0",
183
+ "lodash-es": "^4.17.21",
175
184
  "mobx": "^6.13.6",
176
185
  "mobx-react-lite": "^4.1.0",
177
186
  "react-error-boundary": "^6.0.3",
178
- "react-i18next": "^16.5.2"
187
+ "react-i18next": "^16.5.2",
188
+ "username": "^7.0.0"
179
189
  },
180
190
  "overrides": {
181
191
  "storybook": "$storybook"
package/src/3d.ts CHANGED
@@ -6,10 +6,11 @@ export * from "./components/3d-viewport/TrajectoryRenderer"
6
6
  export * from "./components/RobotCard"
7
7
  export * from "./components/robots/AxisConfig"
8
8
  export {
9
- MANUFACTURER_HOME_CONFIGS,
10
9
  extractManufacturer,
11
- getDefaultHomeConfig,
10
+ getDefaultHomeConfig, MANUFACTURER_HOME_CONFIGS
12
11
  } from "./components/robots/manufacturerHomePositions"
13
12
  export * from "./components/robots/Robot"
14
13
  export { defaultGetModel } from "./components/robots/robotModelLogic"
14
+ export * from "./components/robots/SupportedLinearAxis"
15
15
  export * from "./components/robots/SupportedRobot"
16
+
@@ -52,7 +52,7 @@ export interface RobotCardProps {
52
52
  flangeRef?: React.Ref<Group>
53
53
  postModelRender?: () => void
54
54
  transparentColor?: string
55
- getModel?: (modelFromController: string) => string
55
+ getModel?: (modelFromController: string) => Promise<string>
56
56
  }>
57
57
  /** Custom component to render in the content area (optional) */
58
58
  customContentComponent?: React.ComponentType<Record<string, unknown>>
@@ -0,0 +1,128 @@
1
+ import { Line } from "@react-three/drei"
2
+ import type { DHParameter } from "@wandelbots/nova-js/v2"
3
+ import React, { useRef } from "react"
4
+ import type * as THREE from "three"
5
+ import { Matrix4, Quaternion, Vector3 } from "three"
6
+ import LinearAxisAnimator from "./LinearAxisAnimator"
7
+ import type { DHLinearAxisProps } from "./SupportedLinearAxis"
8
+
9
+ export function DHLinearAxis({
10
+ rapidlyChangingMotionState,
11
+ dhParameters,
12
+ ...props
13
+ }: DHLinearAxisProps) {
14
+ // reused in every update
15
+ const accumulatedMatrix = new Matrix4()
16
+
17
+ const tcpMeshRef = useRef<THREE.Mesh | null>(null)
18
+ const tcpLineRef = useRef<any>(null)
19
+
20
+ // Calculate initial TCP position
21
+ function calculateTcpPosition(jointValues: number[]): Vector3 {
22
+ const tempMatrix = new Matrix4()
23
+
24
+ for (let i = 0; i < dhParameters.length; i++) {
25
+ const param = dhParameters[i]
26
+ const jointValue = jointValues[i] ?? 0
27
+
28
+ const matrix = new Matrix4()
29
+ .makeRotationY(param.theta!) // Base rotation (if any)
30
+ .multiply(
31
+ new Matrix4().makeTranslation(
32
+ param.a! / 1000,
33
+ (param.d! + jointValue * (param.reverse_rotation_direction ? -1 : 1)) / 1000,
34
+ 0
35
+ )
36
+ ) // Translate along X by a, and Y by d + joint_position
37
+ .multiply(new Matrix4().makeRotationX(param.alpha!)) // Rotate around X
38
+
39
+ tempMatrix.multiply(matrix)
40
+ }
41
+
42
+ const position = new Vector3()
43
+ const quaternion = new Quaternion()
44
+ const scale = new Vector3()
45
+ tempMatrix.decompose(position, quaternion, scale)
46
+ return position
47
+ }
48
+
49
+ // Calculate initial TCP position for rendering
50
+ const initialTcpPosition = calculateTcpPosition(rapidlyChangingMotionState.joint_position)
51
+
52
+ function setTranslation(joints: THREE.Object3D[], jointValues: number[]) {
53
+ accumulatedMatrix.identity()
54
+
55
+ let tcpPosition = new Vector3()
56
+
57
+ // Process all joints based on dhParameters length, not joints array
58
+ // Since we're using DHLinearAxis directly without a model, we don't have joint objects
59
+ for (let jointIndex = 0; jointIndex < dhParameters.length; jointIndex++) {
60
+ const jointValue = jointValues[jointIndex] ?? 0
61
+ const param = dhParameters[jointIndex]
62
+
63
+ // Calculate and accumulate transformation
64
+ const matrix = new Matrix4()
65
+ .makeRotationY(param.theta!) // Base rotation (if any)
66
+ .multiply(
67
+ new Matrix4().makeTranslation(
68
+ param.a! / 1000,
69
+ (param.d! + jointValue * (param.reverse_rotation_direction ? -1 : 1)) / 1000,
70
+ 0
71
+ )
72
+ )
73
+ .multiply(new Matrix4().makeRotationX(param.alpha!))
74
+
75
+ accumulatedMatrix.multiply(matrix)
76
+ }
77
+
78
+ // Get final TCP position from accumulated matrix
79
+ const position = new Vector3()
80
+ const quaternion = new Quaternion()
81
+ const scale = new Vector3()
82
+ accumulatedMatrix.decompose(position, quaternion, scale)
83
+ tcpPosition = position
84
+
85
+ // Update TCP marker
86
+ if (tcpMeshRef.current) {
87
+ tcpMeshRef.current.position.set(tcpPosition.x, tcpPosition.y, tcpPosition.z)
88
+ }
89
+
90
+ // Update TCP line
91
+ if (tcpLineRef.current) {
92
+ const lineGeometry = tcpLineRef.current.geometry
93
+ if (lineGeometry && lineGeometry.setPositions) {
94
+ lineGeometry.setPositions([0, 0, 0, tcpPosition.x, tcpPosition.y, tcpPosition.z])
95
+ }
96
+ }
97
+ }
98
+
99
+ return (
100
+ <>
101
+ <LinearAxisAnimator
102
+ rapidlyChangingMotionState={rapidlyChangingMotionState}
103
+ dhParameters={dhParameters}
104
+ onTranslationChanged={setTranslation}
105
+ >
106
+ <group {...props} name="Scene">
107
+ {/* Base (origin) - Green sphere representing initial previous position */}
108
+ <mesh name="Base" position={[0, 0, 0]}>
109
+ <sphereGeometry args={[0.02, 32, 32]} />
110
+ <meshStandardMaterial color={"green"} depthTest={true} />
111
+ </mesh>
112
+ {/* Line from Base to TCP */}
113
+ <Line
114
+ ref={tcpLineRef}
115
+ points={[new Vector3(0, 0, 0), initialTcpPosition]}
116
+ color={"White"}
117
+ lineWidth={5}
118
+ />
119
+ {/* TCP (Tool Center Point) - Red sphere that shows final position */}
120
+ <mesh ref={tcpMeshRef} name="TCP" position={initialTcpPosition}>
121
+ <sphereGeometry args={[0.025, 32, 32]} />
122
+ <meshStandardMaterial color={"red"} depthTest={true} />
123
+ </mesh>
124
+ </group>
125
+ </LinearAxisAnimator>
126
+ </>
127
+ )
128
+ }
@@ -1,12 +1,12 @@
1
1
  import { useGLTF } from "@react-three/drei"
2
2
  import type { ThreeElements } from "@react-three/fiber"
3
- import React, { useCallback } from "react"
3
+ import React, { useCallback, useEffect, useState } from "react"
4
4
  import type { Group, Mesh } from "three"
5
5
  import { type Object3D } from "three"
6
6
  import { isFlange, parseRobotModel } from "./robotModelLogic"
7
7
 
8
8
  export type RobotModelProps = {
9
- modelURL: string
9
+ modelURL: string | Promise<string>
10
10
  /**
11
11
  * Called after a robot model has been loaded and
12
12
  * rendered into the threejs scene
@@ -19,16 +19,25 @@ function isMesh(node: Object3D): node is Mesh {
19
19
  return node.type === "Mesh"
20
20
  }
21
21
 
22
- export function GenericRobot({
23
- modelURL,
24
- flangeRef,
25
- postModelRender,
26
- ...props
27
- }: RobotModelProps) {
28
- const { gltf } = parseRobotModel(
29
- useGLTF(modelURL),
30
- modelURL.split("/").pop() || modelURL,
31
- )
22
+ // Separate component that only renders when we have a valid URL
23
+ function LoadedRobotModel({
24
+ url,
25
+ flangeRef,
26
+ postModelRender,
27
+ ...props
28
+ }: {
29
+ url: string
30
+ flangeRef?: React.Ref<Group>
31
+ postModelRender?: () => void
32
+ } & ThreeElements["group"]) {
33
+ const gltfResult = useGLTF(url)
34
+ let gltf
35
+ try {
36
+ const parsed = parseRobotModel(gltfResult, 'robot.glb')
37
+ gltf = parsed.gltf
38
+ } catch (err) {
39
+ throw err;
40
+ }
32
41
 
33
42
  const groupRef: React.RefCallback<Group> = useCallback(
34
43
  (group) => {
@@ -36,33 +45,45 @@ export function GenericRobot({
36
45
  postModelRender()
37
46
  }
38
47
  },
39
- [modelURL],
48
+ [postModelRender],
40
49
  )
41
50
 
42
51
  function renderNode(node: Object3D): React.ReactNode {
43
- if (isMesh(node)) {
44
- return (
45
- <mesh
46
- name={node.name}
47
- key={node.uuid}
48
- geometry={node.geometry}
49
- material={node.material}
50
- position={node.position}
51
- rotation={node.rotation}
52
- />
53
- )
54
- } else {
55
- return (
56
- <group
57
- name={node.name}
58
- key={node.uuid}
59
- position={node.position}
60
- rotation={node.rotation}
61
- ref={isFlange(node) ? flangeRef : undefined}
62
- >
63
- {node.children.map(renderNode)}
64
- </group>
65
- )
52
+ try {
53
+ if (isMesh(node)) {
54
+ // Defensive: only render mesh if geometry exists
55
+ if ((node as Mesh).geometry) {
56
+ return (
57
+ <mesh
58
+ name={node.name}
59
+ key={node.uuid}
60
+ geometry={(node as Mesh).geometry}
61
+ material={(node as Mesh).material}
62
+ position={node.position}
63
+ rotation={node.rotation}
64
+ />
65
+ )
66
+ }
67
+ // Fallback to empty group if geometry is missing
68
+ return (
69
+ <group name={node.name} key={node.uuid} position={node.position} rotation={node.rotation} />
70
+ )
71
+ } else {
72
+ return (
73
+ <group
74
+ name={node.name}
75
+ key={node.uuid}
76
+ position={node.position}
77
+ rotation={node.rotation}
78
+ ref={isFlange(node) ? flangeRef : undefined}
79
+ >
80
+ {node.children.map(renderNode)}
81
+ </group>
82
+ )
83
+ }
84
+ } catch (e) {
85
+ console.warn('Error rendering node', node.name, e)
86
+ return null
66
87
  }
67
88
  }
68
89
 
@@ -72,3 +93,43 @@ export function GenericRobot({
72
93
  </group>
73
94
  )
74
95
  }
96
+
97
+ export function GenericRobot({
98
+ modelURL,
99
+ flangeRef,
100
+ postModelRender,
101
+ ...props
102
+ }: RobotModelProps) {
103
+ const [resolvedURL, setResolvedURL] = useState<string | null>(null)
104
+
105
+ useEffect(() => {
106
+ const resolveURL = async () => {
107
+ try {
108
+ if (typeof modelURL === 'string') {
109
+ setResolvedURL(modelURL)
110
+ } else {
111
+ const url = await modelURL
112
+ setResolvedURL(url)
113
+ }
114
+ } catch (error) {
115
+ console.error('Failed to resolve model URL:', error)
116
+ }
117
+ }
118
+
119
+ resolveURL()
120
+ }, [modelURL])
121
+
122
+ // Don't render until we have a resolved URL
123
+ if (!resolvedURL) {
124
+ return null // Loading state
125
+ }
126
+
127
+ return (
128
+ <LoadedRobotModel
129
+ url={resolvedURL}
130
+ flangeRef={flangeRef}
131
+ postModelRender={postModelRender}
132
+ {...props}
133
+ />
134
+ )
135
+ }
@@ -0,0 +1,73 @@
1
+ import type { ThreeElements } from "@react-three/fiber"
2
+
3
+ import type { Group } from "three"
4
+ import type { ConnectedMotionGroup } from "../../lib/ConnectedMotionGroup"
5
+ import { DHLinearAxis } from "./DHLinearAxis"
6
+ import { defaultGetModel } from "./robotModelLogic"
7
+ import { SupportedLinearAxis } from "./SupportedLinearAxis"
8
+
9
+ export type LinearAxisProps = {
10
+ connectedMotionGroup: ConnectedMotionGroup
11
+ getModel?: (modelFromController: string) => Promise<string>
12
+ flangeRef?: React.Ref<Group>
13
+ transparentColor?: string
14
+ postModelRender?: () => void
15
+ } & ThreeElements["group"]
16
+
17
+ /**
18
+ * The LinearAxis component is a wrapper that renders SupportedLinearAxis if a model is available,
19
+ * otherwise falls back to DHLinearAxis for usage with @wandelbots/nova-js ConnectedMotionGroup object.
20
+ *
21
+ * @param {LinearAxisProps} props - The properties for the LinearAxis component.
22
+ * @param {ConnectedMotionGroup} props.connectedMotionGroup - The connected motion group containing motion state and parameters.
23
+ * @param {Function} [props.getModel=defaultGetModel] - Optional function to get the model URL. Defaults to defaultGetModel.
24
+ * @param {Object} props - Additional properties passed to the underlying component.
25
+ *
26
+ * @returns {JSX.Element} The rendered SupportedLinearAxis or DHLinearAxis component.
27
+ */
28
+ export function LinearAxis({
29
+ connectedMotionGroup,
30
+ getModel = defaultGetModel,
31
+ flangeRef,
32
+ transparentColor,
33
+ postModelRender,
34
+ ...props
35
+ }: LinearAxisProps) {
36
+ if (!connectedMotionGroup.dhParameters) {
37
+ return null
38
+ }
39
+
40
+ const modelFromController = connectedMotionGroup.modelFromController || ""
41
+ const hasModel = modelFromController && getModel(modelFromController)
42
+
43
+ // Use SupportedLinearAxis if model is available, otherwise fall back to DHLinearAxis
44
+ if (hasModel) {
45
+ return (
46
+ <SupportedLinearAxis
47
+ rapidlyChangingMotionState={
48
+ connectedMotionGroup.rapidlyChangingMotionState
49
+ }
50
+ modelFromController={modelFromController}
51
+ dhParameters={connectedMotionGroup.dhParameters}
52
+ getModel={getModel}
53
+ flangeRef={flangeRef}
54
+ transparentColor={transparentColor}
55
+ postModelRender={postModelRender}
56
+ {...props}
57
+ />
58
+ )
59
+ }
60
+
61
+ return (
62
+ <DHLinearAxis
63
+ rapidlyChangingMotionState={
64
+ connectedMotionGroup.rapidlyChangingMotionState
65
+ }
66
+ dhParameters={connectedMotionGroup.dhParameters}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ export { defaultGetModel }
73
+
@@ -0,0 +1,115 @@
1
+ import { useFrame, useThree } from "@react-three/fiber"
2
+ import type { DHParameter, MotionGroupState } from "@wandelbots/nova-js/v2"
3
+ import React, { useCallback, useEffect, useRef } from "react"
4
+ import type { Group, Object3D } from "three"
5
+ import { useAutorun } from "../utils/hooks"
6
+ import { ValueInterpolator } from "../utils/interpolation"
7
+ import { collectJoints } from "./robotModelLogic"
8
+
9
+ type LinearAxisAnimatorProps = {
10
+ rapidlyChangingMotionState: MotionGroupState
11
+ dhParameters: DHParameter[]
12
+ onTranslationChanged?: (joints: Object3D[], jointValues: number[]) => void
13
+ children: React.ReactNode
14
+ }
15
+
16
+ export default function LinearAxisAnimator({
17
+ rapidlyChangingMotionState,
18
+ dhParameters,
19
+ onTranslationChanged,
20
+ children,
21
+ }: LinearAxisAnimatorProps) {
22
+ const jointValues = useRef<number[]>([])
23
+ const jointObjects = useRef<Object3D[]>([])
24
+ const interpolatorRef = useRef<ValueInterpolator | null>(null)
25
+ const { invalidate } = useThree()
26
+
27
+ // Initialize interpolator
28
+ useEffect(() => {
29
+ const initialJointValues = rapidlyChangingMotionState.joint_position.filter(
30
+ (item) => item !== undefined,
31
+ )
32
+
33
+ interpolatorRef.current = new ValueInterpolator(initialJointValues, {
34
+ tension: 120, // Controls spring stiffness - higher values create faster, more responsive motion
35
+ friction: 20, // Controls damping - higher values reduce oscillation and create smoother settling
36
+ threshold: 0.001,
37
+ })
38
+
39
+ return () => {
40
+ interpolatorRef.current?.destroy()
41
+ }
42
+ }, [])
43
+
44
+ // Animation loop that runs at the display's refresh rate
45
+ useFrame((state, delta) => {
46
+ if (interpolatorRef.current) {
47
+ const isComplete = interpolatorRef.current.update(delta)
48
+ setTranslation()
49
+
50
+ // Trigger a re-render only if the animation is still running
51
+ if (!isComplete) {
52
+ invalidate()
53
+ }
54
+ }
55
+ })
56
+
57
+ function setGroupRef(group: Group | null) {
58
+ if (!group) return
59
+
60
+ jointObjects.current = collectJoints(group)
61
+
62
+ // Set initial position
63
+ setTranslation()
64
+ invalidate()
65
+ }
66
+
67
+ function setTranslation() {
68
+ const updatedJointValues = interpolatorRef.current?.getCurrentValues() || []
69
+
70
+ if (onTranslationChanged) {
71
+ onTranslationChanged(jointObjects.current, updatedJointValues)
72
+ } else {
73
+ // For linear axes, we apply translation instead of rotation
74
+ for (const [index, object] of jointObjects.current.entries()) {
75
+ const dhParam = dhParameters[index]
76
+ const translationSign = dhParam.reverse_rotation_direction ? -1 : 1
77
+
78
+ // Apply linear translation along Y axis
79
+ // Convert from millimeters to meters
80
+ object.position.y =
81
+ (translationSign * (updatedJointValues[index] || 0)) / 1000
82
+ }
83
+ }
84
+ }
85
+
86
+ const updateJoints = useCallback(() => {
87
+ const newJointValues = rapidlyChangingMotionState.joint_position.filter(
88
+ (item) => item !== undefined,
89
+ )
90
+
91
+ requestAnimationFrame(() => {
92
+ jointValues.current = newJointValues
93
+ interpolatorRef.current?.setTarget(newJointValues)
94
+ })
95
+ }, [rapidlyChangingMotionState])
96
+
97
+ /**
98
+ * Fire an update joints call on every motion state change.
99
+ * requestAnimationFrame used to avoid blocking main thread
100
+ */
101
+ useEffect(() => {
102
+ updateJoints()
103
+ }, [rapidlyChangingMotionState, updateJoints])
104
+
105
+ /**
106
+ * As some consumer applications (eg. storybook) deliver
107
+ * mobx observable for rapidlyChangingMotionState, we need to
108
+ * register the watcher to get the newest value updates
109
+ */
110
+ useAutorun(() => {
111
+ updateJoints()
112
+ })
113
+
114
+ return <group ref={setGroupRef}>{children}</group>
115
+ }
@@ -7,7 +7,7 @@ import { SupportedRobot } from "./SupportedRobot"
7
7
 
8
8
  export type RobotProps = {
9
9
  connectedMotionGroup: ConnectedMotionGroup
10
- getModel?: (modelFromController: string) => string
10
+ getModel?: (modelFromController: string) => Promise<string>
11
11
  flangeRef?: React.Ref<Group>
12
12
  transparentColor?: string
13
13
  postModelRender?: () => void
@@ -51,3 +51,5 @@ export function Robot({
51
51
  />
52
52
  )
53
53
  }
54
+
55
+ export { defaultGetModel }
@@ -1,4 +1,4 @@
1
- import type { DHParameter, MotionGroupState } from "@wandelbots/nova-api/v2"
1
+ import type { DHParameter, MotionGroupState } from "@wandelbots/nova-js/v2"
2
2
  import { describe, expect, it, vi } from "vitest"
3
3
  import RobotAnimator from "./RobotAnimator"
4
4