Skip to content

Commit aee1a6f

Browse files
BrunoBellmannWandelbots3m5. Marcin Szalomski
andauthored
feat: Add linear axis support (#509)
[Storybook Link](https://wandelbotsgmbh.github.io/wandelbots-js-react-components/overview.html) --------- Co-authored-by: 3m5. Marcin Szalomski <marcin.szalomski@partner.wandelbots.com>
1 parent 2bea234 commit aee1a6f

File tree

7 files changed

+431
-2
lines changed

7 files changed

+431
-2
lines changed

src/3d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ export * from "./components/3d-viewport/SafetyZonesRenderer"
55
export * from "./components/3d-viewport/TrajectoryRenderer"
66
export * from "./components/RobotCard"
77
export * from "./components/robots/AxisConfig"
8+
export * from "./components/robots/LinearAxis"
89
export {
9-
MANUFACTURER_HOME_CONFIGS,
1010
extractManufacturer,
11-
getDefaultHomeConfig,
11+
getDefaultHomeConfig, MANUFACTURER_HOME_CONFIGS
1212
} from "./components/robots/manufacturerHomePositions"
1313
export * from "./components/robots/Robot"
1414
export { defaultGetModel } from "./components/robots/robotModelLogic"
15+
export * from "./components/robots/SupportedLinearAxis"
1516
export * from "./components/robots/SupportedRobot"
17+
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 X axis
79+
// Convert from millimeters to meters
80+
// ToDo: change back to y axis when we want to display the proper orientation
81+
object.position.x =
82+
(translationSign * (updatedJointValues[index] || 0)) / 1000
83+
}
84+
}
85+
}
86+
87+
const updateJoints = useCallback(() => {
88+
const newJointValues = rapidlyChangingMotionState.joint_position.filter(
89+
(item) => item !== undefined,
90+
)
91+
92+
requestAnimationFrame(() => {
93+
jointValues.current = newJointValues
94+
interpolatorRef.current?.setTarget(newJointValues)
95+
})
96+
}, [rapidlyChangingMotionState])
97+
98+
/**
99+
* Fire an update joints call on every motion state change.
100+
* requestAnimationFrame used to avoid blocking main thread
101+
*/
102+
useEffect(() => {
103+
updateJoints()
104+
}, [rapidlyChangingMotionState, updateJoints])
105+
106+
/**
107+
* As some consumer applications (eg. storybook) deliver
108+
* mobx observable for rapidlyChangingMotionState, we need to
109+
* register the watcher to get the newest value updates
110+
*/
111+
useAutorun(() => {
112+
updateJoints()
113+
})
114+
115+
return <group ref={setGroupRef}>{children}</group>
116+
}

0 commit comments

Comments
 (0)