Skip to content

Commit ecbdf4f

Browse files
MarcinSzalomskiBrunoBellmannWandelbots3m5. Marcin Szalomski
authored
feat(RB-3107): Add motion group visualizer (#510)
[Storybook Link](https://wandelbotsgmbh.github.io/wandelbots-js-react-components/overview.html) --------- Co-authored-by: Bruno Bellmann <bruno.bellmann@wandelbots.com> Co-authored-by: 3m5. Marcin Szalomski <marcin.szalomski@partner.wandelbots.com>
1 parent 45386b1 commit ecbdf4f

13 files changed

+292
-19
lines changed

.storybook/preview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const preview: Preview = {
2424
"Getting Started",
2525
"3D View",
2626
[
27-
"Robot",
27+
"Model Visualization",
2828
"TrajectoryRenderer",
2929
"SafetyZonesRenderer",
3030
"CollisionSceneRenderer",

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"@types/react": "^19.1.8",
9494
"@types/three": "^0.182.0",
9595
"@vitejs/plugin-react": "^4.3.4",
96-
"@wandelbots/nova-js": "^3.5.0",
96+
"@wandelbots/nova-js": "^3.5.2",
9797
"add": "^2.0.6",
9898
"eslint-plugin-storybook": "^10.1.10",
9999
"glob": "^13.0.0",
@@ -176,7 +176,7 @@
176176
"dependencies": {
177177
"@mui/x-charts": "^8.27.0",
178178
"@mui/x-data-grid": "^8.27.0",
179-
"@wandelbots/nova-js": "3.5.1",
179+
"@wandelbots/nova-js": "^3.5.2",
180180
"axios": "^1.13.2",
181181
"dotenv": "^17.2.3",
182182
"i18next-browser-languagedetector": "^8.2.0",

src/3d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export * from "./components/robots/Robot"
1414
export { defaultGetModel } from "./components/robots/robotModelLogic"
1515
export * from "./components/robots/SupportedLinearAxis"
1616
export * from "./components/robots/SupportedRobot"
17+
export * from "./components/robots/MotionGroupVisualizer"
1718

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { useCallback, useEffect, useMemo, useState } from "react"
2+
import { NovaClient } from "@wandelbots/nova-js/v2"
3+
import type { KinematicModel } from "@wandelbots/nova-api/v2"
4+
5+
import { externalizeComponent } from "../../externalizeComponent"
6+
import { SupportedRobot, type SupportedRobotProps } from "./SupportedRobot"
7+
import { SupportedLinearAxis, type SupportedLinearAxisProps } from "./SupportedLinearAxis"
8+
9+
export type MotionGroupVisualizerProps = {
10+
instanceUrl: string
11+
inverseSolver?: string | null
12+
} & (SupportedRobotProps | SupportedLinearAxisProps)
13+
14+
export const MotionGroupVisualizer: React.FC<MotionGroupVisualizerProps> = externalizeComponent((props: MotionGroupVisualizerProps) => {
15+
const {
16+
instanceUrl,
17+
inverseSolver: inverseSolverProp,
18+
modelFromController,
19+
...rest
20+
} = props
21+
22+
const [inverseSolver, setInverseSolver] = useState<string | null | undefined>(
23+
inverseSolverProp,
24+
)
25+
const [, forceThreeRerender] = useState<boolean>(false)
26+
27+
/**
28+
* Fetches the kinematic model from the API and saved the inverse_solver property, which defined
29+
* whether the motion group should be displayed as a robot or linear axis
30+
*/
31+
const fetchKinematicModel = useCallback(async () => {
32+
const nova = new NovaClient({ instanceUrl })
33+
34+
try {
35+
const kinematicModel: KinematicModel =
36+
await nova.api.motionGroupModels.getMotionGroupKinematicModel(
37+
modelFromController,
38+
)
39+
40+
setInverseSolver(kinematicModel.inverse_solver)
41+
} catch (err) {
42+
console.warn(
43+
`Failed to fetch kinematic model from API for ${modelFromController}, falling back to local config`,
44+
)
45+
} finally {
46+
/**
47+
* The following is a workaround to force a rerender of the Three.js scene, due
48+
* to a race condition (if the component gets re-initialized, the robot
49+
* is rendered with the initial joint position). The forceThreeRerender tells
50+
* the Three.JS to re-render with the right joint position
51+
*
52+
* TODO remove the three rerender function and get rid of the
53+
* stale state condition
54+
* https://wandelbots.atlassian.net/browse/RB-3134
55+
*/
56+
setTimeout(() => forceThreeRerender(true), 0)
57+
}
58+
}, [modelFromController, instanceUrl])
59+
60+
/**
61+
* Checks the delivered inverseSolver property from the consumer
62+
* Defined, Null - carry on with the delivered value - the inverseSolver was fetched via consumer app
63+
* Undefined - carry out a request to fetch the inverseSolver value
64+
*/
65+
useEffect(() => {
66+
const shouldKinematicBeFetched =
67+
inverseSolverProp === undefined && !!modelFromController && !!instanceUrl
68+
69+
if (shouldKinematicBeFetched) {
70+
fetchKinematicModel()
71+
}
72+
}, [inverseSolverProp, modelFromController, fetchKinematicModel, instanceUrl])
73+
74+
/**
75+
* The turntable models return inverseSolver = null - however these models
76+
* should be rendered with SupportedRobot instead of SupportedLinearAxis
77+
*
78+
* TODO
79+
* In the future - this hard coded check would be replaced with a new "type" property
80+
* from dh parameters
81+
*/
82+
const isTurnTable = useMemo(() => {
83+
return [
84+
"KUKA_DKP250",
85+
"KUKA_DKP500_2",
86+
"YASKAWA_TURN1",
87+
"YASKAWA_TURN2",
88+
"YASKAWA_TURN3",
89+
].includes(modelFromController)
90+
}, [modelFromController])
91+
92+
if (inverseSolver || isTurnTable) {
93+
return (
94+
<SupportedRobot
95+
instanceUrl={instanceUrl}
96+
modelFromController={modelFromController}
97+
{...rest}
98+
/>
99+
)
100+
}
101+
102+
if (inverseSolver === null) {
103+
return (
104+
<SupportedLinearAxis
105+
instanceUrl={instanceUrl}
106+
modelFromController={modelFromController}
107+
{...rest}
108+
/>
109+
)
110+
}
111+
112+
return null
113+
})

src/components/robots/SupportedLinearAxis.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export type SupportedLinearAxisProps = {
2121
modelFromController: string
2222
dhParameters: DHParameter[]
2323
flangeRef?: React.Ref<THREE.Group>
24-
getModel?: (modelFromController: string) => Promise<string> | undefined
24+
instanceUrl?: string
25+
getModel?: (modelFromController: string, instanceUrl?: string) => Promise<string> | undefined
2526
postModelRender?: () => void
2627
transparentColor?: string
2728
} & ThreeElements["group"]
@@ -35,6 +36,7 @@ export const SupportedLinearAxis = externalizeComponent(
3536
flangeRef,
3637
postModelRender,
3738
transparentColor,
39+
instanceUrl,
3840
...props
3941
}: SupportedLinearAxisProps) => {
4042
const [robotGroup, setRobotGroup] = useState<THREE.Group | null>(null)
@@ -77,7 +79,7 @@ export const SupportedLinearAxis = externalizeComponent(
7779
>
7880
<GenericRobot
7981
modelURL={(() => {
80-
const result = getModel(modelFromController)
82+
const result = getModel(modelFromController, instanceUrl)
8183
if (!result) {
8284
const mockBlob = new Blob([], { type: 'model/gltf-binary' })
8385
const mockFile = new File([mockBlob], `${modelFromController}.glb`, { type: 'model/gltf-binary' })

src/components/robots/SupportedRobot.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export type SupportedRobotProps = {
2222
modelFromController: string
2323
dhParameters: DHParameter[]
2424
flangeRef?: React.Ref<THREE.Group>
25-
getModel?: (modelFromController: string) => Promise<string> | undefined
25+
instanceUrl?: string
26+
getModel?: (modelFromController: string, instanceUrl?: string) => Promise<string> | undefined
2627
postModelRender?: () => void
2728
transparentColor?: string
2829
} & ThreeElements["group"]
@@ -36,6 +37,7 @@ export const SupportedRobot = externalizeComponent(
3637
flangeRef,
3738
postModelRender,
3839
transparentColor,
40+
instanceUrl,
3941
...props
4042
}: SupportedRobotProps) => {
4143
const [robotGroup, setRobotGroup] = useState<THREE.Group | null>(null)
@@ -78,7 +80,7 @@ export const SupportedRobot = externalizeComponent(
7880
>
7981
<GenericRobot
8082
modelURL={(() => {
81-
const result = getModel(modelFromController)
83+
const result = getModel(modelFromController, instanceUrl)
8284
if (!result) {
8385
const mockBlob = new Blob([], { type: 'model/gltf-binary' })
8486
const mockFile = new File([mockBlob], `${modelFromController}.glb`, { type: 'model/gltf-binary' })

src/components/robots/robotModelLogic.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ export async function revokeAllModelUrls(): Promise<void> {
4040
modelCache.clear()
4141
}
4242

43-
export async function defaultGetModel(modelFromController: string): Promise<string> {
43+
export async function defaultGetModel(modelFromController: string, instanceUrlProp?: string): Promise<string> {
4444
// Check cache first
4545
if (modelCache.has(modelFromController)) {
4646
return modelCache.get(modelFromController)!
4747
}
4848

4949
// Create the promise and cache it immediately to prevent duplicate calls
5050
const modelPromise = (async () => {
51-
const instanceUrl = import.meta.env.WANDELAPI_BASE_URL
51+
const instanceUrl = instanceUrlProp || import.meta.env.WANDELAPI_BASE_URL
5252
const nova = new NovaClient({ instanceUrl })
5353

5454
// Configure axios to handle binary responses for GLB files

stories/MotionGroupVisualizer.mdx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Canvas, Meta, Source } from '@storybook/addon-docs/blocks';
2+
import * as MotionGroupVisualizerStories from '../stories/robots/MotionGroupVisualizer.stories';
3+
4+
5+
<Meta title="3D View/Model Visualization/MotionGroupVisualizer"/>
6+
7+
# MotionGroupVisualizer
8+
9+
This `MotionGroupVisualizer` component adds the robot or linear axis to the 3D viewport. It can be used on top of a robot or linear axis if we want to include the internal differentiation of what should be rendered.
10+
11+
The differentiation is dependent on the `inverseSolver` variable, which can also be passed as a prop:
12+
- `inverseSolver` set: `SupportedRobot` component is returned.
13+
- `inverseSolver` null: `SupportedLinearAxis` component is returned.
14+
- `inverseSolver` undefined: internal kinematics request to find out what the `inverseSolver` state is.
15+
16+
Use it together with the `ConnectedMotionGroup` from this library:
17+
18+
```tsx
19+
import { NovaClient } from "@wandelbots/nova-js/v2"
20+
import { ConnectedMotionGroup } from "@wandelbots/wandelbots-js-react-components/core"
21+
import { MotionGroupVisualizer } from "@wandelbots/wandelbots-js-react-components/3d"
22+
23+
const instanceUrl = "https://your-nova-instance.com"
24+
const nova = new NovaClient({ instanceUrl })
25+
const connectedMotionGroup = await ConnectedMotionGroup.connect(nova, "0@your-motion-group-id");
26+
27+
<MotionGroupVisualizer
28+
instanceUrl={instanceUrl}
29+
modelFromController={connectedMotionGroup.modelFromController}
30+
rapidlyChangingMotionState={connectedMotionGroup.rapidlyChangingMotionState}
31+
dhParameters={connectedMotionGroup.dhParameters ?? []}
32+
/>
33+
```
34+
35+
The model files are loaded from your connected Nova instance.
36+
37+
```tsx
38+
const file = await nova.api.motionGroupModels.getMotionGroupGlbModel(modelFromController)
39+
const url = URL.createObjectURL(file)
40+
return url
41+
```
42+
43+
44+
In case you want to use the application offline, you can download the models and host them locally. You can override the URL resolver of the `MotionGroupVisualizer` component by passing a `getModel` function like:
45+
46+
```tsx
47+
<MotionGroupVisualizer
48+
instanceUrl={instanceUrl}
49+
modelFromController={connectedMotionGroup.modelFromController}
50+
rapidlyChangingMotionState={connectedMotionGroup.rapidlyChangingMotionState}
51+
dhParameters={connectedMotionGroup.dhParameters ?? []}
52+
getModel={() => `public/${connectedMotionGroup.modelFromController}.glb`}
53+
/>
54+
```
55+
56+
<Canvas of={MotionGroupVisualizerStories.MotionGroupVisualizerStory} />

stories/Robot.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Canvas, Meta, Source } from '@storybook/addon-docs/blocks';
22
import * as RobotStories from '../stories/robots/Robot.stories';
33

44

5-
<Meta title="3D View/Robot/Robot"/>
5+
<Meta title="3D View/Model Visualization/Robot"/>
66

77
# Robot
88

0 commit comments

Comments
 (0)