@wandelbots/wandelbots-js-react-components 1.3.1 → 1.4.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 (78) hide show
  1. package/package.json +18 -8
  2. package/src/components/3d-viewport/CoordinateSystemTransform.tsx +44 -0
  3. package/src/components/3d-viewport/PresetEnvironment.tsx +78 -0
  4. package/src/components/3d-viewport/SafetyZonesRenderer.tsx +55 -0
  5. package/src/components/LoadingButton.stories.tsx +61 -0
  6. package/src/components/LoadingButton.tsx +19 -0
  7. package/src/components/LoadingCover.tsx +75 -0
  8. package/src/components/ThemeSelect.tsx +49 -0
  9. package/src/components/VelocitySlider.stories.tsx +32 -0
  10. package/src/components/VelocitySlider.tsx +52 -0
  11. package/src/components/jogging/JoggingCartesianAxisControl.stories.tsx +41 -0
  12. package/src/components/jogging/JoggingCartesianAxisControl.tsx +127 -0
  13. package/src/components/jogging/JoggingCartesianTab.tsx +265 -0
  14. package/src/components/jogging/JoggingCartesianValues.tsx +45 -0
  15. package/src/components/jogging/JoggingFreedriveTab.tsx +9 -0
  16. package/src/components/jogging/JoggingJointLimitDetector.tsx +51 -0
  17. package/src/components/jogging/JoggingJointRotationControl.stories.tsx +38 -0
  18. package/src/components/jogging/JoggingJointRotationControl.tsx +197 -0
  19. package/src/components/jogging/JoggingJointTab.tsx +93 -0
  20. package/src/components/jogging/JoggingJointValues.tsx +45 -0
  21. package/src/components/jogging/JoggingOptions.tsx +96 -0
  22. package/src/components/jogging/JoggingPanel.stories.tsx +26 -0
  23. package/src/components/jogging/JoggingPanel.tsx +148 -0
  24. package/src/components/jogging/JoggingStore.tsx +294 -0
  25. package/src/components/jogging/JoggingVelocitySlider.tsx +56 -0
  26. package/src/components/robots/ABB_1200_07_7.tsx +127 -0
  27. package/src/components/robots/AxisConfig.ts +3 -0
  28. package/src/components/robots/DHRobot.tsx +128 -0
  29. package/src/components/robots/FANUC_ARC_Mate_100iD.tsx +187 -0
  30. package/src/components/robots/FANUC_ARC_Mate_120iD.tsx +187 -0
  31. package/src/components/robots/FANUC_CRX10iA.tsx +171 -0
  32. package/src/components/robots/FANUC_CRX25iA.tsx +171 -0
  33. package/src/components/robots/FANUC_CRX25iAL.tsx +182 -0
  34. package/src/components/robots/KUKA_KR210_R2700.tsx +291 -0
  35. package/src/components/robots/KUKA_KR270_R2700.tsx +244 -0
  36. package/src/components/robots/Robot.tsx +42 -0
  37. package/src/components/robots/RobotAnimator.tsx +82 -0
  38. package/src/components/robots/SupportedRobot.tsx +144 -0
  39. package/src/components/robots/UniversalRobots_UR10.tsx +112 -0
  40. package/src/components/robots/UniversalRobots_UR10e.tsx +275 -0
  41. package/src/components/robots/UniversalRobots_UR3.tsx +112 -0
  42. package/src/components/robots/UniversalRobots_UR3e.tsx +112 -0
  43. package/src/components/robots/UniversalRobots_UR5.tsx +111 -0
  44. package/src/components/robots/UniversalRobots_UR5e.tsx +280 -0
  45. package/src/components/robots/Yaskawa_AR1440.tsx +156 -0
  46. package/src/components/robots/Yaskawa_AR1730.tsx +169 -0
  47. package/src/components/robots/Yaskawa_AR2010.tsx +163 -0
  48. package/src/components/robots/Yaskawa_AR3120.tsx +164 -0
  49. package/src/components/robots/Yaskawa_AR900.tsx +125 -0
  50. package/src/components/utils/converters.ts +23 -0
  51. package/src/components/utils/errorHandling.ts +30 -0
  52. package/src/components/utils/hooks.tsx +54 -0
  53. package/src/components/utils/robotTreeQuery.ts +27 -0
  54. package/src/components/wandelscript-editor/WandelscriptEditor.stories.tsx +45 -0
  55. package/src/components/wandelscript-editor/WandelscriptEditor.tsx +114 -0
  56. package/src/components/wandelscript-editor/wandelscript.tmLanguage.ts +62 -0
  57. package/src/declarations.d.ts +10 -0
  58. package/src/i18n/config.ts +27 -0
  59. package/src/i18n/locales/de/translations.json +12 -0
  60. package/src/i18n/locales/en/translations.json +12 -0
  61. package/src/icons/arrowForwardFilled.tsx +7 -0
  62. package/src/icons/axis-x.svg +3 -0
  63. package/src/icons/axis-y.svg +3 -0
  64. package/src/icons/axis-z.svg +3 -0
  65. package/src/icons/expandFilled.tsx +11 -0
  66. package/src/icons/home.tsx +12 -0
  67. package/src/icons/index.ts +6 -0
  68. package/src/icons/infoOutlined.tsx +10 -0
  69. package/src/icons/jogging.svg +3 -0
  70. package/src/icons/robot.svg +3 -0
  71. package/src/icons/robot.tsx +14 -0
  72. package/src/icons/rotation.svg +4 -0
  73. package/src/icons/wbLogo.tsx +21 -0
  74. package/src/index.ts +8 -0
  75. package/src/themes/color.tsx +74 -0
  76. package/src/themes/theme.ts +150 -0
  77. package/src/themes/wbTheme.stories.tsx +64 -0
  78. package/src/themes/wbTheme.ts +186 -0
package/package.json CHANGED
@@ -1,21 +1,28 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
- "main": "dist/cjs/index.js",
6
- "module": "dist/esm/index.js",
7
5
  "files": [
8
6
  "dist",
9
- "assets"
7
+ "src"
10
8
  ],
11
- "types": "dist/index.d.ts",
9
+ "type": "module",
10
+ "module": "./dist/index.js",
11
+ "main": "./dist/index.cjs",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "types": "./dist/index.d.ts",
12
19
  "scripts": {
13
20
  "dev": "storybook dev -p 6006 --no-open",
21
+ "dev:pack": "nodemon -w \".\" -e ts -i *.tgz -i dist -x \"npm run build && npm pack\"",
14
22
  "dev:wbjs": "nodemon --watch $WBJS_PATH -e tgz --exec 'npm install file:$WBJS_PATH && storybook dev -p 6006 --no-open'",
15
23
  "tsc": "tsc --pretty --noEmit",
16
- "test": "vitest",
17
- "build": "rollup -c",
18
- "build:dev": "rollup -c rollup.config.dev.mjs",
24
+ "test": "npm run build",
25
+ "build": "rimraf dist && vite build && tsc --declaration --emitDeclarationOnly",
19
26
  "build-storybook": "storybook build"
20
27
  },
21
28
  "repository": {
@@ -34,6 +41,7 @@
34
41
  "@emotion/styled": "^11.13.0",
35
42
  "@mui/material": "^5.13.6",
36
43
  "@rollup/plugin-commonjs": "^26.0.1",
44
+ "@rollup/plugin-json": "^6.1.0",
37
45
  "@rollup/plugin-node-resolve": "^15.2.3",
38
46
  "@rollup/plugin-terser": "^0.4.4",
39
47
  "@rollup/plugin-typescript": "^11.1.6",
@@ -45,6 +53,7 @@
45
53
  "@storybook/react-vite": "^8.2.7",
46
54
  "@storybook/test": "^8.2.7",
47
55
  "@storybook/test-runner": "^0.19.1",
56
+ "@svgr/rollup": "^8.1.0",
48
57
  "@types/lodash-es": "^4.17.12",
49
58
  "@types/react": "^18.3.3",
50
59
  "@vitejs/plugin-react": "^4.3.1",
@@ -53,6 +62,7 @@
53
62
  "prop-types": "^15.8.1",
54
63
  "react": "^18.3.1",
55
64
  "react-dom": "^18.3.1",
65
+ "rimraf": "^6.0.1",
56
66
  "rollup": "^4.19.2",
57
67
  "rollup-plugin-dts": "^6.1.1",
58
68
  "rollup-plugin-gltf": "^4.0.0",
@@ -0,0 +1,44 @@
1
+ import type { CoordinateSystem } from "@wandelbots/wandelbots-api-client"
2
+ import type { ReactNode } from "react"
3
+ import { Vector3, Quaternion } from "three"
4
+
5
+ /**
6
+ * Applies a API coordinate system transformation to
7
+ * all react-three child components.
8
+ */
9
+ export const CoordinateSystemTransform = ({
10
+ coordinateSystem,
11
+ children,
12
+ }: {
13
+ coordinateSystem?: CoordinateSystem
14
+ children: ReactNode
15
+ }) => {
16
+ const position = new Vector3(
17
+ (coordinateSystem?.position?.x ?? 0) / 1000,
18
+ (coordinateSystem?.position?.y ?? 0) / 1000,
19
+ (coordinateSystem?.position?.z ?? 0) / 1000,
20
+ )
21
+
22
+ let rotation = coordinateSystem?.rotation
23
+ const rotationType = rotation?.type
24
+ if (rotationType && rotationType !== "ROTATION_VECTOR") {
25
+ console.warn(
26
+ `Unsupported rotation type ${rotationType}. Only ROTATION_VECTOR is supported.`,
27
+ )
28
+ rotation = { type: "ROTATION_VECTOR", angles: [0, 0, 0, 0] }
29
+ }
30
+
31
+ const rotationVector = new Vector3(
32
+ rotation?.angles[0] ?? 0,
33
+ rotation?.angles[1] ?? 0,
34
+ rotation?.angles[2] ?? 0,
35
+ )
36
+ const magnitude = rotationVector.length()
37
+ const axis = rotationVector.normalize()
38
+ const quaternion = new Quaternion().setFromAxisAngle(axis, magnitude)
39
+ return (
40
+ <group position={position} quaternion={quaternion}>
41
+ {children}
42
+ </group>
43
+ )
44
+ }
@@ -0,0 +1,78 @@
1
+ import { Environment, Lightformer } from "@react-three/drei"
2
+
3
+ /**
4
+ * Renders a preset environment for the 3D scene.
5
+ * This component wraps the scene with an `Environment` component
6
+ * and builds a lightmap build with `Lightformers`.
7
+ */
8
+ export function PresetEnvironment() {
9
+ return (
10
+ <Environment>
11
+ <Lightformers />
12
+ </Environment>
13
+ )
14
+ }
15
+
16
+ function Lightformers({ positions = [2, 0, 2, 0, 2, 0, 2, 0] }) {
17
+ return (
18
+ <>
19
+ {/* Ceiling */}
20
+ <Lightformer
21
+ intensity={5}
22
+ rotation-x={Math.PI / 2}
23
+ position={[0, 5, -9]}
24
+ scale={[10, 10, 1]}
25
+ />
26
+ <group rotation={[0, 0.5, 0]}>
27
+ <group>
28
+ {positions.map((x, i) => (
29
+ <Lightformer
30
+ key={i}
31
+ form="circle"
32
+ intensity={5}
33
+ rotation={[Math.PI / 2, 0, 0]}
34
+ position={[x, 4, i * 4]}
35
+ scale={[3, 1, 1]}
36
+ />
37
+ ))}
38
+ </group>
39
+ </group>
40
+ {/* Sides */}
41
+ <Lightformer
42
+ intensity={40}
43
+ rotation-y={Math.PI / 2}
44
+ position={[-5, 1, -1]}
45
+ scale={[20, 0.1, 1]}
46
+ />
47
+ <Lightformer
48
+ intensity={20}
49
+ rotation-y={-Math.PI}
50
+ position={[-5, -2, -1]}
51
+ scale={[20, 0.1, 1]}
52
+ />
53
+
54
+ <Lightformer
55
+ rotation-y={Math.PI / 2}
56
+ position={[-5, -1, -1]}
57
+ scale={[20, 0.5, 1]}
58
+ intensity={5}
59
+ />
60
+ <Lightformer
61
+ rotation-y={-Math.PI / 2}
62
+ position={[10, 1, 0]}
63
+ scale={[20, 1, 1]}
64
+ intensity={10}
65
+ />
66
+
67
+ {/* Key */}
68
+ <Lightformer
69
+ form="ring"
70
+ color="white"
71
+ intensity={5}
72
+ scale={10}
73
+ position={[-15, 4, -18]}
74
+ target={[0, 0, 0]}
75
+ />
76
+ </>
77
+ )
78
+ }
@@ -0,0 +1,55 @@
1
+ import { SafetySetupSafetyZone } from "@wandelbots/wandelbots-js"
2
+ import { Geometry } from "@wandelbots/wandelbots-api-client"
3
+ import * as THREE from "three"
4
+ import { ConvexGeometry } from "three-stdlib"
5
+ import { type GroupProps } from "@react-three/fiber"
6
+
7
+ export type SafetyZonesRendererProps = {
8
+ safetyZones: SafetySetupSafetyZone[]
9
+ } & GroupProps
10
+
11
+ export function SafetyZonesRenderer({
12
+ safetyZones,
13
+ ...props
14
+ }: SafetyZonesRendererProps) {
15
+ return (
16
+ <group {...props}>
17
+ {safetyZones.map((zone, index) => {
18
+ let geometries: Geometry[] = []
19
+ if (zone.geometry) {
20
+ if (zone.geometry.compound) {
21
+ geometries = zone.geometry.compound.child_geometries
22
+ } else if (zone.geometry.convex_hull) {
23
+ geometries = [zone.geometry]
24
+ }
25
+ }
26
+
27
+ return geometries.map((geometry, i) => {
28
+ if (!geometry.convex_hull) return null
29
+
30
+ const vertices = geometry.convex_hull.vertices.map(
31
+ (v) => new THREE.Vector3(v.x / 1000, v.y / 1000, v.z / 1000),
32
+ )
33
+ const convexGeometry = new ConvexGeometry(vertices)
34
+ return (
35
+ <>
36
+ <mesh key={`${index}-${i}`} geometry={convexGeometry}>
37
+ <meshStandardMaterial
38
+ key={index}
39
+ attach="material"
40
+ color="#009f4d"
41
+ opacity={0.2}
42
+ depthTest={false}
43
+ depthWrite={false}
44
+ transparent
45
+ polygonOffset
46
+ polygonOffsetFactor={-i}
47
+ />
48
+ </mesh>
49
+ </>
50
+ )
51
+ })
52
+ })}
53
+ </group>
54
+ )
55
+ }
@@ -0,0 +1,61 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import { LoadingButton } from "./LoadingButton";
3
+ import { useState } from "react";
4
+ import { PlayArrow } from "@mui/icons-material"
5
+
6
+ async function delay(ms: number) {
7
+ return new Promise<void>((resolve) => {
8
+ setTimeout(() => {
9
+ resolve();
10
+ }, ms);
11
+ });
12
+ }
13
+
14
+ const DemoLoader = (props: React.ComponentProps<typeof LoadingButton>) => {
15
+ const [isLoading, setIsLoading] = useState(false);
16
+
17
+ async function doThing() {
18
+ setIsLoading(true);
19
+ try {
20
+ await delay(1000);
21
+ } finally {
22
+ setIsLoading(false);
23
+ }
24
+ }
25
+
26
+ return (
27
+ <LoadingButton
28
+ loading={isLoading}
29
+ onClick={doThing}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ const meta: Meta<typeof LoadingButton> = {
36
+ component: LoadingButton,
37
+
38
+ args: {
39
+ children: "Click me",
40
+ loadingPosition: "start",
41
+ variant: "contained",
42
+ startIcon: <PlayArrow />,
43
+ },
44
+ argTypes: {
45
+ loadingPosition: {
46
+ options: ["center", "start", "end"],
47
+ control: { type: 'select' },
48
+ },
49
+ variant: {
50
+ options: ["contained", "outlined", "text"],
51
+ control: { type: 'select' },
52
+ }
53
+ },
54
+ render: (props) => {
55
+ return <DemoLoader {...props} />;
56
+ }
57
+ };
58
+ export default meta;
59
+
60
+ export const Default: StoryObj<typeof LoadingButton> = {
61
+ };
@@ -0,0 +1,19 @@
1
+ import { LoadingButton as MUILoadingButton } from "@mui/lab"
2
+ import { forwardRef } from "react"
3
+
4
+ type LoadingButtonProps = React.ComponentProps<typeof MUILoadingButton>
5
+
6
+ /** Button with a loading state to indicate a task is being processed */
7
+ export const LoadingButton = forwardRef<HTMLButtonElement, LoadingButtonProps>(
8
+ ({ sx, ...rest }, ref) => {
9
+ return (
10
+ <MUILoadingButton
11
+ sx={{
12
+ ...sx,
13
+ }}
14
+ ref={ref}
15
+ {...rest}
16
+ />
17
+ )
18
+ },
19
+ )
@@ -0,0 +1,75 @@
1
+ import { makeErrorMessage } from "./utils/errorHandling"
2
+ import { CircularProgress, Stack, useTheme } from "@mui/material"
3
+ import { useEffect, useState } from "react"
4
+
5
+ export const LoadingCover = (props: {
6
+ message?: string
7
+ error?: unknown
8
+ softTimeout?: number
9
+ }) => {
10
+ const softTimeout = props.softTimeout || 3000
11
+
12
+ const [showSlowLoadingMessage, setShowSlowLoadingMessage] = useState(false)
13
+
14
+ useEffect(() => {
15
+ const timeout = setTimeout(() => {
16
+ setShowSlowLoadingMessage(true)
17
+ }, softTimeout)
18
+
19
+ return () => clearTimeout(timeout)
20
+ })
21
+
22
+ return (
23
+ <Stack
24
+ width="100%"
25
+ height="100%"
26
+ alignItems="center"
27
+ justifyContent="center"
28
+ >
29
+ {props.error ? (
30
+ <LoadingErrorMessage message={props.message} error={props.error} />
31
+ ) : (
32
+ <>
33
+ <CircularProgress sx={{ marginBottom: "24px" }} />
34
+ {!!props.message && <div>{props.message}</div>}
35
+ <Stack
36
+ sx={{
37
+ visibility: showSlowLoadingMessage ? "visible" : "hidden",
38
+ marginTop: "1rem",
39
+ color: "gray",
40
+ }}
41
+ >
42
+ {"This is taking longer than expected..."}
43
+ </Stack>
44
+ </>
45
+ )}
46
+ </Stack>
47
+ )
48
+ }
49
+
50
+ const LoadingErrorMessage = (props: { message?: string; error: unknown }) => {
51
+ const errorMessage = makeErrorMessage(props.error)
52
+ const stack = props.error instanceof Error ? props.error.stack : null
53
+ const theme = useTheme()
54
+
55
+ return (
56
+ <Stack
57
+ sx={{
58
+ maxHeight: "100%",
59
+ maxWidth: "min(100%, 800px)",
60
+ padding: 2,
61
+ overflow: "auto",
62
+ color: theme.palette.error.main,
63
+ "& pre": {
64
+ whiteSpace: "pre-wrap",
65
+ wordBreak: "break-word",
66
+ paddingBottom: "3rem",
67
+ },
68
+ }}
69
+ >
70
+ {`Error while: ${props.message} - ${errorMessage}`}
71
+ <br />
72
+ {stack && <pre>{stack}</pre>}
73
+ </Stack>
74
+ )
75
+ }
@@ -0,0 +1,49 @@
1
+ // TODO implement this as part of theme?
2
+
3
+ import { useThemeColors } from "../themes/wbTheme"
4
+ import { Select, type SxProps } from "@mui/material"
5
+ import { defaultsDeep } from "lodash-es"
6
+
7
+ type ThemeSelectProps = {
8
+ kind: "filled" | "outlined" | "text"
9
+ } & React.ComponentProps<typeof Select>
10
+
11
+ export const ThemeSelect = ({ kind, sx, ...rest }: ThemeSelectProps) => {
12
+ const colors = useThemeColors()
13
+
14
+ let style: SxProps = defaultsDeep(sx, {
15
+ backgroundColor: colors.selectBackground,
16
+ borderRadius: "10px",
17
+ borderStyle: "none",
18
+ color: "currentColor",
19
+ "& > div": {
20
+ padding: "4px 16px",
21
+ },
22
+ "& fieldset": {
23
+ border: "none",
24
+ },
25
+ })
26
+
27
+ if (kind === "outlined") {
28
+ style = defaultsDeep(
29
+ {
30
+ backgroundColor: "transparent",
31
+ },
32
+ style,
33
+ )
34
+ } else if (kind === "text") {
35
+ style = defaultsDeep(
36
+ {
37
+ backgroundColor: "transparent",
38
+
39
+ "& .MuiSvgIcon-root": {
40
+ borderStyle: "none",
41
+ color: "inherit",
42
+ },
43
+ },
44
+ style,
45
+ )
46
+ }
47
+
48
+ return <Select sx={style} {...rest} />
49
+ }
@@ -0,0 +1,32 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import { VelocitySlider } from "./VelocitySlider";
3
+ import { useArgs } from "@storybook/preview-api";
4
+
5
+ const meta: Meta<typeof VelocitySlider> = {
6
+ component: VelocitySlider,
7
+
8
+ args: {
9
+ velocity: 1,
10
+ min: 1,
11
+ max: 100,
12
+ disabled: false,
13
+ },
14
+ render: function Component(args) {
15
+ const [, setArgs] = useArgs();
16
+
17
+ function onVelocityChange(newVelocity: number) {
18
+ args.onVelocityChange?.(newVelocity);
19
+ setArgs({ velocity: newVelocity });
20
+ }
21
+
22
+ return <VelocitySlider
23
+ {...args}
24
+ onVelocityChange={onVelocityChange}
25
+ />;
26
+ },
27
+
28
+ };
29
+ export default meta;
30
+
31
+ export const Default: StoryObj<typeof VelocitySlider> = {
32
+ };
@@ -0,0 +1,52 @@
1
+ import Slider from "@mui/material/Slider"
2
+ import { isNumber } from "lodash-es"
3
+ import { observer } from "mobx-react-lite"
4
+ import { Typography } from "@mui/material"
5
+
6
+ type VelocitySliderProps = {
7
+ min: number
8
+ max: number
9
+ velocity: number
10
+ onVelocityChange: (newVelocity: number) => void
11
+ disabled?: boolean
12
+ valueLabelFormat?: (value: number) => string
13
+ }
14
+
15
+ /** A slider for controlling the movement velocity of a robot */
16
+ export const VelocitySlider = observer((props: VelocitySliderProps) => {
17
+ const valueLabelFormat = props.valueLabelFormat || ((value: number) => `${value}`)
18
+
19
+ function onSliderChange(_event: Event, newVelocity: number | number[]) {
20
+ if (newVelocity === props.velocity || !isNumber(newVelocity)) return
21
+
22
+ props.onVelocityChange(newVelocity)
23
+ }
24
+
25
+ return (
26
+ <>
27
+ <Typography
28
+ sx={{
29
+ textAlign: "center",
30
+ fontSize: "15px",
31
+ }}
32
+ >
33
+ {valueLabelFormat(props.velocity)}
34
+ </Typography>
35
+ <Slider
36
+ value={props.velocity}
37
+ onChange={onSliderChange}
38
+ min={props.min}
39
+ max={props.max}
40
+ aria-labelledby="input-slider"
41
+ disabled={props.disabled}
42
+ sx={{
43
+ "& .MuiSlider-valueLabelOpen": {
44
+ zIndex: 100,
45
+ backgroundColor: "transparent",
46
+ top: "0px",
47
+ },
48
+ }}
49
+ />
50
+ </>
51
+ )
52
+ })
@@ -0,0 +1,41 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import { JoggingCartesianAxisControl } from "./JoggingCartesianAxisControl";
3
+ import { useRef, useState } from "react";
4
+ import { useAnimationFrame } from "../utils/hooks";
5
+ import { useArgs } from "@storybook/preview-api";
6
+
7
+ const meta: Meta<typeof JoggingCartesianAxisControl> = {
8
+ component: JoggingCartesianAxisControl,
9
+
10
+ args: {
11
+ color: "#F14D42",
12
+ label: "X",
13
+ disabled: false,
14
+ },
15
+ render: function Component(args) {
16
+ const [, setArgs] = useArgs();
17
+
18
+ const joggingDirRef = useRef<"+" | "-" | null>(null);
19
+ const joggingValueRef = useRef(0);
20
+
21
+ useAnimationFrame(() => {
22
+ if (joggingDirRef.current === "+") {
23
+ joggingValueRef.current += 1;
24
+ } else if (joggingDirRef.current === "-") {
25
+ joggingValueRef.current -= 1;
26
+ }
27
+ })
28
+
29
+ return <JoggingCartesianAxisControl
30
+ {...args}
31
+ startJogging={(direction) => joggingDirRef.current = direction}
32
+ stopJogging={() => joggingDirRef.current = null}
33
+ getDisplayedValue={() => joggingValueRef.current.toString()}
34
+ />;
35
+ },
36
+
37
+ };
38
+ export default meta;
39
+
40
+ export const Default: StoryObj<typeof JoggingCartesianAxisControl> = {
41
+ };
@@ -0,0 +1,127 @@
1
+ import { Button, Typography } from "@mui/material"
2
+ import Stack from "@mui/material/Stack"
3
+ import { observer } from "mobx-react-lite"
4
+ import { useRef, type ReactNode } from "react"
5
+ import { useAnimationFrame } from "../utils/hooks"
6
+
7
+ type JoggingCartesianAxisControlProps = {
8
+ color?: string
9
+ label: ReactNode
10
+ getDisplayedValue: () => string
11
+ startJogging: (direction: "-" | "+") => void
12
+ stopJogging: () => void
13
+ disabled?: boolean
14
+ } & React.ComponentProps<typeof Stack>
15
+
16
+ export const JoggingCartesianAxisControl = observer(
17
+ ({
18
+ color,
19
+ label,
20
+ getDisplayedValue,
21
+ startJogging,
22
+ stopJogging,
23
+ disabled,
24
+ ...rest
25
+ }: JoggingCartesianAxisControlProps) => {
26
+ useAnimationFrame(() => {
27
+ const displayValue = getDisplayedValue()
28
+ const element = valueContainerRef.current
29
+ if (!element) return
30
+
31
+ element.textContent = displayValue
32
+ })
33
+
34
+ const valueContainerRef = useRef<HTMLParagraphElement>(null)
35
+
36
+ color = color || "#F14D42"
37
+
38
+ function onPointerDownMinus(ev: React.PointerEvent) {
39
+ // Stop right click from triggering jog
40
+ if (ev.button === 0) startJogging("-")
41
+ }
42
+
43
+ function onPointerDownPlus(ev: React.PointerEvent) {
44
+ if (ev.button === 0) startJogging("+")
45
+ }
46
+
47
+ return (
48
+ <Stack height="72px" direction="row" {...rest}>
49
+ <Button
50
+ onPointerDown={onPointerDownMinus}
51
+ onPointerUp={stopJogging}
52
+ onPointerOut={stopJogging}
53
+ disabled={disabled}
54
+ sx={{
55
+ width: "105px",
56
+ backgroundColor: color,
57
+ color: "white",
58
+ alignContent: "center",
59
+ fontSize: "37px",
60
+ borderRadius: "16px 0px 0px 16px",
61
+
62
+ ":hover": {
63
+ color: "white",
64
+ backgroundColor: color,
65
+ },
66
+ }}
67
+ >
68
+ {"-"}
69
+ </Button>
70
+
71
+ <Stack
72
+ spacing="6px"
73
+ sx={{
74
+ width: "184px",
75
+ backgroundColor: color,
76
+ alignItems: "center",
77
+ justifyContent: "center",
78
+ opacity: "0.9",
79
+ }}
80
+ >
81
+ <Stack
82
+ height="22px"
83
+ direction="row"
84
+ alignItems="center"
85
+ justifyItems="center"
86
+ spacing={1}
87
+ sx={{ userSelect: "none" }}
88
+ >
89
+ {label}
90
+ </Stack>
91
+ <Typography
92
+ height="22px"
93
+ sx={{
94
+ fontSize: "15px",
95
+ color: "white",
96
+ }}
97
+ ref={valueContainerRef}
98
+ >
99
+ {getDisplayedValue()}
100
+ </Typography>
101
+ </Stack>
102
+
103
+ <Button
104
+ onPointerDown={onPointerDownPlus}
105
+ onPointerUp={stopJogging}
106
+ onPointerOut={stopJogging}
107
+ disabled={disabled}
108
+ sx={{
109
+ width: "105px",
110
+ backgroundColor: color,
111
+ color: "white",
112
+ alignContent: "center",
113
+ fontSize: "37px",
114
+ borderRadius: "0px 16px 16px 0px",
115
+
116
+ ":hover": {
117
+ color: "white",
118
+ backgroundColor: color,
119
+ },
120
+ }}
121
+ >
122
+ {"+"}
123
+ </Button>
124
+ </Stack>
125
+ )
126
+ },
127
+ )