@wandelbots/wandelbots-js-react-components 1.35.0 → 1.36.0

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 (38) hide show
  1. package/dist/components/jogging/JoggingOptions.d.ts.map +1 -1
  2. package/dist/components/jogging/PoseCartesianValues.d.ts +1 -1
  3. package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
  4. package/dist/components/robots/DHRobot.d.ts.map +1 -1
  5. package/dist/components/robots/GenericRobot.d.ts +13 -2
  6. package/dist/components/robots/GenericRobot.d.ts.map +1 -1
  7. package/dist/components/robots/Robot.d.ts.map +1 -1
  8. package/dist/components/robots/RobotAnimator.d.ts +3 -5
  9. package/dist/components/robots/RobotAnimator.d.ts.map +1 -1
  10. package/dist/components/robots/SupportedRobot.d.ts +4 -4
  11. package/dist/components/robots/SupportedRobot.d.ts.map +1 -1
  12. package/dist/components/robots/ghostStyle.d.ts +4 -0
  13. package/dist/components/robots/ghostStyle.d.ts.map +1 -0
  14. package/dist/components/robots/robotModelLogic.d.ts +25 -0
  15. package/dist/components/robots/robotModelLogic.d.ts.map +1 -0
  16. package/dist/index.cjs +30 -30
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3464 -3398
  21. package/dist/index.js.map +1 -1
  22. package/package.json +49 -34
  23. package/src/components/jogging/JoggingOptions.tsx +1 -0
  24. package/src/components/jogging/PoseCartesianValues.tsx +2 -4
  25. package/src/components/robots/DHRobot.tsx +2 -10
  26. package/src/components/robots/GenericRobot.tsx +30 -9
  27. package/src/components/robots/Robot.tsx +2 -1
  28. package/src/components/robots/RobotAnimator.tsx +6 -13
  29. package/src/components/robots/SupportedRobot.tsx +35 -111
  30. package/src/components/robots/ghostStyle.ts +70 -0
  31. package/src/components/robots/robotModelLogic.ts +90 -0
  32. package/src/index.ts +1 -0
  33. package/dist/components/robots/types.d.ts +0 -19
  34. package/dist/components/robots/types.d.ts.map +0 -1
  35. package/dist/components/utils/robotTreeQuery.d.ts +0 -6
  36. package/dist/components/utils/robotTreeQuery.d.ts.map +0 -1
  37. package/src/components/robots/types.ts +0 -21
  38. package/src/components/utils/robotTreeQuery.ts +0 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -24,7 +24,12 @@
24
24
  "dev:build": "nodemon -V -w src -e '*' -x \"vite build && tsc --declaration --emitDeclarationOnly\"",
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
- "test": "npm run build",
27
+ "test": "test-storybook --url http://127.0.0.1:6006 --index-json --browsers chromium",
28
+ "ci:test": "run-s ci:test:build ci:test:built",
29
+ "ci:test:build": "storybook build --quiet --test",
30
+ "ci:test:built": "run-p --race ci:test:server ci:test:runner",
31
+ "ci:test:server": "http-server storybook-static --port 9009 --silent",
32
+ "ci:test:runner": "wait-on tcp:127.0.0.1:9009 && test-storybook --url http://127.0.0.1:9009 --index-json --browsers chromium",
28
33
  "build": "rimraf dist && vite build && tsc --declaration --emitDeclarationOnly",
29
34
  "build-storybook": "storybook build"
30
35
  },
@@ -43,55 +48,65 @@
43
48
  "@emotion/react": "^11.13.3",
44
49
  "@emotion/styled": "^11.13.0",
45
50
  "@mui/material": "^5.16.7",
46
- "@rollup/plugin-commonjs": "^26.0.1",
51
+ "@rollup/plugin-commonjs": "^28.0.1",
47
52
  "@rollup/plugin-json": "^6.1.0",
48
- "@rollup/plugin-node-resolve": "^15.2.3",
53
+ "@rollup/plugin-node-resolve": "^15.3.0",
49
54
  "@rollup/plugin-terser": "^0.4.4",
50
- "@rollup/plugin-typescript": "^11.1.6",
51
- "@storybook/addon-docs": "^8.2.9",
52
- "@storybook/addon-essentials": "^8.2.9",
53
- "@storybook/blocks": "^8.2.9",
54
- "@storybook/react": "^8.2.9",
55
- "@storybook/react-vite": "^8.2.9",
56
- "@storybook/test": "^8.2.9",
55
+ "@rollup/plugin-typescript": "^12.1.1",
56
+ "@storybook/addon-docs": "^8.4.2",
57
+ "@storybook/addon-essentials": "^8.4.2",
58
+ "@storybook/addon-interactions": "^8.4.2",
59
+ "@storybook/blocks": "^8.4.2",
60
+ "@storybook/react": "^8.4.2",
61
+ "@storybook/react-vite": "^8.4.2",
62
+ "@storybook/test": "^8.4.2",
57
63
  "@storybook/test-runner": "^0.19.1",
58
- "@storybook/types": "^8.2.9",
64
+ "@storybook/types": "^8.4.2",
59
65
  "@svgr/rollup": "^8.1.0",
60
66
  "@types/lodash-es": "^4.17.12",
61
- "@types/react": "^18.3.5",
62
- "@types/three": "^0.168.0",
63
- "@vitejs/plugin-react": "^4.3.1",
64
- "nodemon": "^3.1.4",
65
- "postcss": "^8.4.44",
67
+ "@types/react": "^18.3.12",
68
+ "@types/three": "^0.169.0",
69
+ "@vitejs/plugin-react": "^4.3.3",
70
+ "add": "^2.0.6",
71
+ "glob": "^11.0.0",
72
+ "http-server": "^14.1.1",
73
+ "jest-simple-dot-reporter": "^1.0.5",
74
+ "jest-summary-reporter": "^0.0.2",
75
+ "nodemon": "^3.1.7",
76
+ "npm-run-all": "^4.1.5",
77
+ "postcss": "^8.4.47",
66
78
  "prettier-eslint": "^16.3.0",
67
79
  "prop-types": "^15.8.1",
68
80
  "react": "^18.3.1",
69
81
  "react-dom": "^18.3.1",
70
82
  "rimraf": "^6.0.1",
71
- "rollup": "^4.21.2",
83
+ "rollup": "^4.24.4",
72
84
  "rollup-plugin-dts": "^6.1.1",
73
85
  "rollup-plugin-gltf": "^4.0.0",
74
86
  "rollup-plugin-peer-deps-external": "^2.2.4",
75
87
  "rollup-plugin-postcss": "^4.0.2",
76
- "semantic-release": "^24.1.0",
77
- "storybook": "^8.2.9",
88
+ "semantic-release": "^24.2.0",
89
+ "storybook": "^8.4.2",
78
90
  "storybook-dark-mode": "^4.0.2",
79
91
  "storybook-preset-inline-svg": "^1.0.1",
80
- "typescript": "^5.5.4",
81
- "vite": "^5.4.2",
82
- "vite-plugin-svgr": "^4.2.0",
83
- "vitest": "^2.0.5"
92
+ "ts-dedent": "^2.2.0",
93
+ "typescript": "^5.6.3",
94
+ "unplugin": "^1.15.0",
95
+ "vite": "^5.4.10",
96
+ "vite-plugin-svgr": "^4.3.0",
97
+ "vitest": "^2.1.4",
98
+ "wait-on": "^8.0.1"
84
99
  },
85
100
  "peerDependencies": {
86
101
  "@emotion/react": "^11.11.1",
87
102
  "@emotion/styled": "^11.11.0",
88
- "@mui/material": ">=5",
103
+ "@mui/material": "^5",
89
104
  "@react-spring/three": ">=9",
90
105
  "@react-three/drei": ">=9",
91
106
  "@react-three/fiber": ">=8",
92
- "react": "^18.2.0",
93
- "react-dom": "^18.2.0",
94
- "three": ">=0.168",
107
+ "react": ">=18.2.0",
108
+ "react-dom": ">=18.2.0",
109
+ "three": ">=0.170",
95
110
  "three-stdlib": ">=2"
96
111
  },
97
112
  "peerDependenciesMeta": {
@@ -103,14 +118,14 @@
103
118
  "@monaco-editor/react": "^4.6.0",
104
119
  "@mui/icons-material": "^5.16.7",
105
120
  "@mui/lab": "^5.0.0-alpha.173",
106
- "@shikijs/monaco": "^1.16.1",
107
- "@wandelbots/wandelbots-js": "^1.11.1",
121
+ "@shikijs/monaco": "^1.22.2",
122
+ "@wandelbots/wandelbots-js": "^1.11.2",
108
123
  "i18next-browser-languagedetector": "^8.0.0",
109
124
  "lodash-es": "^4.17.21",
110
- "mobx": "^6.13.1",
125
+ "mobx": "^6.13.5",
111
126
  "mobx-react-lite": "^4.0.7",
112
- "react-error-boundary": "^4.0.13",
113
- "react-i18next": "^15.0.1",
114
- "shiki": "^1.16.1"
127
+ "react-error-boundary": "^4.1.2",
128
+ "react-i18next": "^15.1.0",
129
+ "shiki": "^1.22.2"
115
130
  }
116
131
  }
@@ -25,6 +25,7 @@ export const JoggingOptions = observer(({ store }: { store: JoggingStore }) => {
25
25
 
26
26
  return (
27
27
  <Box
28
+ component="div"
28
29
  sx={{
29
30
  display: "grid",
30
31
  gap: "16px",
@@ -1,8 +1,6 @@
1
1
  import { Stack } from "@mui/material"
2
- import {
3
- MotionStreamConnection,
4
- poseToWandelscriptString,
5
- } from "@wandelbots/wandelbots-js"
2
+ import type { MotionStreamConnection } from "@wandelbots/wandelbots-js"
3
+ import { poseToWandelscriptString } from "@wandelbots/wandelbots-js"
6
4
  import { observer } from "mobx-react-lite"
7
5
  import { useRef } from "react"
8
6
  import { CopyableText } from "../CopyableText"
@@ -3,10 +3,6 @@ import type { DHParameter } from "@wandelbots/wandelbots-api-client"
3
3
  import type * as THREE from "three"
4
4
  import { Matrix4, Quaternion, Vector3 } from "three"
5
5
  import type { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js"
6
- import {
7
- getAllJointsByName,
8
- type RobotSceneJoint,
9
- } from "../utils/robotTreeQuery"
10
6
  import RobotAnimator from "./RobotAnimator"
11
7
  import type { DHRobotProps } from "./SupportedRobot"
12
8
 
@@ -85,16 +81,11 @@ export function DHRobot({
85
81
  })
86
82
  }
87
83
 
88
- function jointCollector(rootObject: THREE.Object3D): RobotSceneJoint[] {
89
- return getAllJointsByName(rootObject, "^group_[0-9]+$")
90
- }
91
-
92
84
  return (
93
85
  <>
94
86
  <RobotAnimator
95
87
  rapidlyChangingMotionState={rapidlyChangingMotionState}
96
88
  dhParameters={dhParameters}
97
- jointCollector={jointCollector}
98
89
  onRotationChanged={setRotation}
99
90
  >
100
91
  <group {...props} name="Scene">
@@ -108,8 +99,9 @@ export function DHRobot({
108
99
  rapidlyChangingMotionState.state.joint_position.joints[index] ??
109
100
  0,
110
101
  )
102
+ const jointName = `dhrobot_J0${index}`
111
103
  return (
112
- <group name={`group_${index}`} key={"group_" + index}>
104
+ <group name={jointName} key={jointName}>
113
105
  <Line
114
106
  name={CHILD_LINE}
115
107
  points={[a, b]}
@@ -1,25 +1,46 @@
1
1
  import { animated } from "@react-spring/three"
2
2
  import { useGLTF } from "@react-three/drei"
3
- import type { Mesh } from "three"
3
+ import type { GroupProps } from "@react-three/fiber"
4
+ import React, { useCallback } from "react"
5
+ import type { Group, Mesh } from "three"
4
6
  import { type Object3D } from "three"
5
- import type { RobotModelProps } from "./types"
7
+ import { isFlange, parseRobotModel } from "./robotModelLogic"
8
+
9
+ export type RobotModelProps = {
10
+ modelURL: string
11
+ /**
12
+ * Called after a robot model has been loaded and
13
+ * rendered into the threejs scene
14
+ */
15
+ postModelRender?: () => void
16
+ flangeRef?: React.Ref<Group>
17
+ } & GroupProps
6
18
 
7
19
  function isMesh(node: Object3D): node is Mesh {
8
20
  return node.type === "Mesh"
9
21
  }
10
22
 
11
- function isFlange(node: Object3D): boolean {
12
- return node.name.endsWith("_FLG")
13
- }
14
-
15
23
  export function GenericRobot({
16
24
  modelURL,
17
25
  flangeRef,
26
+ postModelRender,
18
27
  ...props
19
28
  }: RobotModelProps) {
20
- const gltf = useGLTF(modelURL)
29
+ const { gltf } = parseRobotModel(
30
+ useGLTF(modelURL),
31
+ modelURL.split("/").pop() || modelURL,
32
+ )
33
+
34
+ const groupRef: React.RefCallback<Group> = useCallback(
35
+ (group) => {
36
+ if (group && postModelRender) {
37
+ postModelRender()
38
+ }
39
+ },
40
+ [modelURL],
41
+ )
21
42
 
22
- const renderNode = (node: Object3D): React.ReactNode => {
43
+ function renderNode(node: Object3D): React.ReactNode {
23
44
  if (isMesh(node)) {
24
45
  return (
25
46
  <mesh
@@ -47,7 +68,7 @@ export function GenericRobot({
47
68
  }
48
69
 
49
70
  return (
50
- <group {...props} dispose={null}>
71
+ <group {...props} dispose={null} ref={groupRef}>
51
72
  {renderNode(gltf.scene)}
52
73
  </group>
53
74
  )
@@ -2,7 +2,8 @@ import { type GroupProps } from "@react-three/fiber"
2
2
 
3
3
  import type { ConnectedMotionGroup } from "@wandelbots/wandelbots-js"
4
4
  import type { Group } from "three"
5
- import { defaultGetModel, SupportedRobot } from "./SupportedRobot"
5
+ import { SupportedRobot } from "./SupportedRobot"
6
+ import { defaultGetModel } from "./robotModelLogic"
6
7
 
7
8
  export type RobotProps = {
8
9
  connectedMotionGroup: ConnectedMotionGroup
@@ -5,39 +5,32 @@ import type {
5
5
  MotionGroupStateResponse,
6
6
  } from "@wandelbots/wandelbots-api-client"
7
7
  import React, { useRef } from "react"
8
- import type * as THREE from "three"
8
+ import type { Group, Object3D } from "three"
9
9
  import { useAutorun } from "../utils/hooks"
10
- import {
11
- getAllJointsByName,
12
- type RobotSceneJoint,
13
- } from "../utils/robotTreeQuery"
10
+ import { collectJoints } from "./robotModelLogic"
14
11
 
15
12
  type RobotAnimatorProps = {
16
13
  rapidlyChangingMotionState: MotionGroupStateResponse
17
14
  dhParameters: DHParameter[]
18
- onRotationChanged?: (joints: THREE.Object3D[], jointValues: number[]) => void
19
- jointCollector?: (rootObject: THREE.Object3D) => RobotSceneJoint[]
15
+ onRotationChanged?: (joints: Object3D[], jointValues: number[]) => void
20
16
  children: React.ReactNode
21
17
  }
22
18
 
23
19
  export default function RobotAnimator({
24
20
  rapidlyChangingMotionState,
25
21
  dhParameters,
26
- jointCollector,
27
22
  onRotationChanged,
28
23
  children,
29
24
  }: RobotAnimatorProps) {
30
25
  Globals.assign({ frameLoop: "always" })
31
26
  const jointValues = useRef<number[]>([])
32
- const jointObjects = useRef<THREE.Object3D[]>([])
27
+ const jointObjects = useRef<Object3D[]>([])
33
28
  const { invalidate } = useThree()
34
29
 
35
- function setGroupRef(group: THREE.Group | null) {
30
+ function setGroupRef(group: Group | null) {
36
31
  if (!group) return
37
32
 
38
- jointObjects.current = jointCollector
39
- ? jointCollector(group)
40
- : getAllJointsByName(group)
33
+ jointObjects.current = collectJoints(group)
41
34
 
42
35
  // Set initial position
43
36
  setRotation()
@@ -1,20 +1,19 @@
1
- import { Suspense, useCallback, useEffect, useRef } from "react"
2
-
3
- import { version } from "../../../package.json"
4
-
5
1
  import type { GroupProps } from "@react-three/fiber"
6
2
  import type {
7
3
  DHParameter,
8
4
  MotionGroupStateResponse,
9
5
  } from "@wandelbots/wandelbots-api-client"
6
+ import { Suspense, useCallback, useEffect, useState } from "react"
10
7
  import { DHRobot } from "./DHRobot"
11
8
 
12
9
  import { ErrorBoundary } from "react-error-boundary"
13
- import * as THREE from "three"
10
+ import type * as THREE from "three"
14
11
  import { externalizeComponent } from "../../externalizeComponent"
15
12
  import ConsoleFilter from "../ConsoleFilter"
16
13
  import { GenericRobot } from "./GenericRobot"
17
14
  import RobotAnimator from "./RobotAnimator"
15
+ import { applyGhostStyle, removeGhostStyle } from "./ghostStyle"
16
+ import { defaultGetModel } from "./robotModelLogic"
18
17
 
19
18
  export type DHRobotProps = {
20
19
  rapidlyChangingMotionState: MotionGroupStateResponse
@@ -25,19 +24,12 @@ export type SupportedRobotProps = {
25
24
  rapidlyChangingMotionState: MotionGroupStateResponse
26
25
  modelFromController: string
27
26
  dhParameters: DHParameter[]
28
- getModel?: (modelFromController: string) => string
29
27
  isGhost?: boolean
30
28
  flangeRef?: React.Ref<THREE.Group>
29
+ getModel?: (modelFromController: string) => string
30
+ postModelRender?: () => void
31
31
  } & GroupProps
32
32
 
33
- export function defaultGetModel(modelFromController: string): string {
34
- let useVersion = version
35
- if (version.startsWith("0.")) {
36
- useVersion = ""
37
- }
38
- return `https://cdn.jsdelivr.net/gh/wandelbotsgmbh/wandelbots-js-react-components${useVersion ? `@${useVersion}` : ""}/public/models/${modelFromController}.glb`
39
- }
40
-
41
33
  export const SupportedRobot = externalizeComponent(
42
34
  ({
43
35
  rapidlyChangingMotionState,
@@ -46,115 +38,46 @@ export const SupportedRobot = externalizeComponent(
46
38
  getModel = defaultGetModel,
47
39
  isGhost = false,
48
40
  flangeRef,
41
+ postModelRender,
49
42
  ...props
50
43
  }: SupportedRobotProps) => {
51
- const robotRef = useRef<THREE.Group>()
52
-
53
- const setRobotRef = useCallback(
54
- (instance: THREE.Group | null) => {
55
- if (!instance) return
56
- robotRef.current = instance
57
-
58
- if (isGhost) applyGhostStyle()
59
- },
60
- [isGhost],
61
- )
62
-
63
- const applyGhostStyle = () => {
64
- if (!robotRef.current || robotRef.current.userData.isGhost) return
65
-
66
- robotRef.current.traverse((obj) => {
67
- if (obj instanceof THREE.Mesh) {
68
- if (obj.material instanceof THREE.Material) {
69
- obj.material.colorWrite = true
70
- }
71
-
72
- // Create a clone of the mesh
73
- const depth = obj.clone()
74
- const ghost = obj.clone()
75
-
76
- depth.material = new THREE.MeshStandardMaterial({
77
- depthTest: true,
78
- depthWrite: true,
79
- colorWrite: false,
80
- polygonOffset: true,
81
- polygonOffsetFactor: 1,
82
- })
83
- depth.userData.isGhost = true
84
-
85
- // Set the material for the ghost mesh
86
- ghost.material = new THREE.MeshStandardMaterial({
87
- color: "#D91433",
88
- opacity: 0.3,
89
- depthTest: true,
90
- depthWrite: false,
91
- transparent: true,
92
- polygonOffset: true,
93
- polygonOffsetFactor: -1,
94
- })
95
- ghost.userData.isGhost = true
44
+ const [robotGroup, setRobotGroup] = useState<THREE.Group | null>(null)
96
45
 
97
- if (obj.parent) {
98
- obj.parent.add(depth)
99
- obj.parent.add(ghost)
100
- }
101
- }
102
- })
103
-
104
- robotRef.current.userData.isGhost = true
105
- }
106
-
107
- const removeGhostStyle = () => {
108
- if (!robotRef.current || !robotRef.current.userData.isGhost) return
109
-
110
- const objectsToRemove: THREE.Object3D[] = []
111
-
112
- robotRef.current.traverse((obj) => {
113
- if (obj instanceof THREE.Mesh) {
114
- if (obj.userData?.isGhost) {
115
- objectsToRemove.push(obj)
116
- } else if (obj.material instanceof THREE.Material) {
117
- obj.material.colorWrite = true
118
- }
119
- }
120
- })
121
-
122
- objectsToRemove.forEach((obj) => {
123
- if (obj.parent) {
124
- obj.parent.remove(obj)
125
- }
126
- })
127
-
128
- robotRef.current.userData.isGhost = true
129
- }
46
+ const setRobotRef = useCallback((instance: THREE.Group | null) => {
47
+ setRobotGroup(instance)
48
+ }, [])
130
49
 
131
50
  useEffect(() => {
51
+ if (!robotGroup) return
52
+
132
53
  if (isGhost) {
133
- applyGhostStyle()
54
+ applyGhostStyle(robotGroup)
134
55
  } else {
135
- removeGhostStyle()
56
+ removeGhostStyle(robotGroup)
136
57
  }
137
- }, [isGhost])
58
+ }, [robotGroup, isGhost])
59
+
60
+ const dhrobot = (
61
+ <DHRobot
62
+ rapidlyChangingMotionState={rapidlyChangingMotionState}
63
+ dhParameters={dhParameters}
64
+ {...props}
65
+ />
66
+ )
138
67
 
139
68
  return (
140
69
  <ErrorBoundary
141
- fallback={
142
- <DHRobot
143
- rapidlyChangingMotionState={rapidlyChangingMotionState}
144
- dhParameters={dhParameters}
145
- {...props}
146
- />
147
- }
148
- >
149
- <Suspense
150
- fallback={
151
- <DHRobot
152
- rapidlyChangingMotionState={rapidlyChangingMotionState}
153
- dhParameters={dhParameters}
154
- {...props}
155
- />
70
+ fallback={dhrobot}
71
+ onError={(err) => {
72
+ if (err.message.includes("404: Not Found")) {
73
+ // Missing model; show the fallback for now
74
+ console.error(err)
75
+ } else {
76
+ throw err
156
77
  }
157
- >
78
+ }}
79
+ >
80
+ <Suspense fallback={dhrobot}>
158
81
  <group ref={setRobotRef}>
159
82
  <RobotAnimator
160
83
  rapidlyChangingMotionState={rapidlyChangingMotionState}
@@ -162,6 +85,7 @@ export const SupportedRobot = externalizeComponent(
162
85
  >
163
86
  <GenericRobot
164
87
  modelURL={getModel(modelFromController)}
88
+ postModelRender={postModelRender}
165
89
  flangeRef={flangeRef}
166
90
  {...props}
167
91
  />
@@ -0,0 +1,70 @@
1
+ import type { Group, Object3D } from "three"
2
+ import { Material, Mesh, MeshStandardMaterial } from "three"
3
+
4
+ export const applyGhostStyle = (robot: Group) => {
5
+ if (robot.userData.isGhost) return
6
+
7
+ robot.traverse((obj) => {
8
+ if (obj instanceof Mesh) {
9
+ if (obj.material instanceof Material) {
10
+ obj.material.colorWrite = true
11
+ }
12
+
13
+ // Create a clone of the mesh
14
+ const depth = obj.clone()
15
+ const ghost = obj.clone()
16
+
17
+ depth.material = new MeshStandardMaterial({
18
+ depthTest: true,
19
+ depthWrite: true,
20
+ colorWrite: false,
21
+ polygonOffset: true,
22
+ polygonOffsetFactor: 1,
23
+ })
24
+ depth.userData.isGhost = true
25
+
26
+ // Set the material for the ghost mesh
27
+ ghost.material = new MeshStandardMaterial({
28
+ color: "#D91433",
29
+ opacity: 0.3,
30
+ depthTest: true,
31
+ depthWrite: false,
32
+ transparent: true,
33
+ polygonOffset: true,
34
+ polygonOffsetFactor: -1,
35
+ })
36
+ ghost.userData.isGhost = true
37
+
38
+ if (obj.parent) {
39
+ obj.parent.add(depth)
40
+ obj.parent.add(ghost)
41
+ }
42
+ }
43
+ })
44
+
45
+ robot.userData.isGhost = true
46
+ }
47
+
48
+ export const removeGhostStyle = (robot: Group) => {
49
+ if (!robot.userData.isGhost) return
50
+
51
+ const objectsToRemove: Object3D[] = []
52
+
53
+ robot.traverse((obj) => {
54
+ if (obj instanceof Mesh) {
55
+ if (obj.userData?.isGhost) {
56
+ objectsToRemove.push(obj)
57
+ } else if (obj.material instanceof Material) {
58
+ obj.material.colorWrite = true
59
+ }
60
+ }
61
+ })
62
+
63
+ objectsToRemove.forEach((obj) => {
64
+ if (obj.parent) {
65
+ obj.parent.remove(obj)
66
+ }
67
+ })
68
+
69
+ robot.userData.isGhost = true
70
+ }
@@ -0,0 +1,90 @@
1
+ import type { Object3D } from "three"
2
+ import type { GLTF } from "three-stdlib"
3
+ import { version } from "../../../package.json"
4
+
5
+ export function defaultGetModel(modelFromController: string): string {
6
+ let useVersion = version
7
+ if (version.startsWith("0.")) {
8
+ useVersion = ""
9
+ }
10
+ return `https://cdn.jsdelivr.net/gh/wandelbotsgmbh/wandelbots-js-react-components${useVersion ? `@${useVersion}` : ""}/public/models/${modelFromController}.glb`
11
+ }
12
+
13
+ /**
14
+ * Finds all the joint groups in a GLTF tree, as identified
15
+ * by the _Jxx name ending convention.
16
+ */
17
+ export function collectJoints(rootObject: Object3D): Object3D[] {
18
+ function getAllObjects(root: Object3D): Object3D[] {
19
+ if (root.children.length === 0) {
20
+ return [root]
21
+ }
22
+ return [root, ...root.children.flatMap((child) => getAllObjects(child))]
23
+ }
24
+
25
+ return getAllObjects(rootObject).filter((o) => isJoint(o))
26
+ }
27
+
28
+ /**
29
+ * Checks if a specified threejs object represents the flange of a
30
+ * robot, based on the _FLG name ending convention.
31
+ */
32
+ export function isFlange(node: Object3D) {
33
+ return node.name.endsWith("_FLG")
34
+ }
35
+
36
+ /**
37
+ * Checks if a specified threejs object represents a joint of a
38
+ * robot, based on the _Jxx name ending convention.
39
+ */
40
+ export function isJoint(node: Object3D) {
41
+ return /_J[0-9]+$/.test(node.name)
42
+ }
43
+
44
+ /**
45
+ * Validates that the loaded GLTF file has six joints and a flange group.
46
+ */
47
+ export function parseRobotModel(gltf: GLTF, filename: string): { gltf: GLTF } {
48
+ let flange: Object3D | undefined
49
+ const joints: Object3D[] = []
50
+
51
+ function isSixJoints(
52
+ joints: Object3D[],
53
+ ): joints is [Object3D, Object3D, Object3D, Object3D, Object3D, Object3D] {
54
+ return joints.length === 6
55
+ }
56
+
57
+ function parseNode(node: Object3D) {
58
+ if (isFlange(node)) {
59
+ if (flange) {
60
+ throw Error(
61
+ `Found multiple flange groups in robot model ${filename}; first ${flange.name} then ${node.name}. Only one _FLG group is allowed.`,
62
+ )
63
+ }
64
+
65
+ flange = node
66
+ }
67
+
68
+ if (isJoint(node)) {
69
+ joints.push(node)
70
+ }
71
+
72
+ node.children.map(parseNode)
73
+ }
74
+
75
+ parseNode(gltf.scene)
76
+
77
+ if (!isSixJoints(joints)) {
78
+ throw Error(
79
+ `Expected to find 6 joint groups in robot model ${filename} with names _J01, _J02 etc, found ${joints.length}. Only 6-joint robot models are currently supported.`,
80
+ )
81
+ }
82
+
83
+ if (!flange) {
84
+ throw Error(
85
+ `No flange group found in robot model ${filename}. Flange must be identified with a name ending in _FLG.`,
86
+ )
87
+ }
88
+
89
+ return { gltf }
90
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./components/LoadingCover"
10
10
  export * from "./components/modal/NoMotionGroupModal"
11
11
  export * from "./components/robots/AxisConfig"
12
12
  export * from "./components/robots/Robot"
13
+ export { defaultGetModel } from "./components/robots/robotModelLogic"
13
14
  export * from "./components/robots/SupportedRobot"
14
15
  export * from "./components/SelectableFab"
15
16
  export * from "./components/utils/hooks"