diff --git a/src/components/CopyableText.tsx b/src/components/CopyableText.tsx index d2504b4..d263701 100644 --- a/src/components/CopyableText.tsx +++ b/src/components/CopyableText.tsx @@ -1,5 +1,5 @@ -import { Chip, InputLabel } from "@mui/material" -import { forwardRef } from "react" +import { Stack, Tooltip, Typography, useTheme } from "@mui/material" +import { forwardRef, useEffect, useState } from "react" export const CopyableText = forwardRef( ( @@ -12,19 +12,76 @@ export const CopyableText = forwardRef( }, ref: React.ForwardedRef, ) => { + const theme = useTheme() + const [tooltipOpen, setTooltipOpen] = useState(false) + + async function onCopyText(): Promise { + if (!ref || !("current" in ref)) { + console.warn("No copy target found") + return false + } + try { + await navigator.clipboard.writeText(value) + console.log("Copied!") + setTooltipOpen(true) + return true + } catch (err) { + // Direct clipboard copy is not available in non-secure contexts + console.error(err) + + // Let's fall back to selecting the text so the user can manually copy easily + const selection = window.getSelection()! + const range = document.createRange() + range.selectNodeContents(ref.current as any) + selection.removeAllRanges() + selection.addRange(range) + } + return false + } + + useEffect(() => { + if (!tooltipOpen) { + return + } + const timeoutId = setTimeout(() => setTooltipOpen(false), 3000) + return () => { + timeoutId ? clearTimeout(timeoutId) : {} + } + }, [tooltipOpen]) + return ( - <> - {label && {label}} - + - + onClick={() => onCopyText()} + > + + {value} + + + ) }, ) diff --git a/src/components/LoadingCover.tsx b/src/components/LoadingCover.tsx index 4a6906b..66ec556 100644 --- a/src/components/LoadingCover.tsx +++ b/src/components/LoadingCover.tsx @@ -1,7 +1,7 @@ -import { makeErrorMessage } from "./utils/errorHandling" import { capitalize, CircularProgress, Stack, useTheme } from "@mui/material" import { lowerFirst } from "lodash-es" import { useEffect, useState } from "react" +import { makeErrorMessage } from "./utils/errorHandling" export const LoadingCover = (props: { message?: string @@ -11,6 +11,7 @@ export const LoadingCover = (props: { const softTimeout = props.softTimeout || 3000 const [showSlowLoadingMessage, setShowSlowLoadingMessage] = useState(false) + const theme = useTheme() useEffect(() => { const timeout = setTimeout(() => { @@ -26,6 +27,7 @@ export const LoadingCover = (props: { height="100%" alignItems="center" justifyContent="center" + sx={{ color: theme.palette.text.primary }} > {props.error ? ( {"This is taking longer than expected..."} diff --git a/src/components/VelocitySlider.tsx b/src/components/VelocitySlider.tsx index b7507ff..ed3e4a5 100644 --- a/src/components/VelocitySlider.tsx +++ b/src/components/VelocitySlider.tsx @@ -1,7 +1,8 @@ -import { Typography, useTheme } from "@mui/material" +import { Stack, Typography, useTheme, type SxProps } from "@mui/material" import Slider from "@mui/material/Slider" import isNumber from "lodash-es/isNumber" import { observer } from "mobx-react-lite" +import type { ReactNode } from "react" type VelocitySliderProps = { min: number @@ -9,16 +10,13 @@ type VelocitySliderProps = { velocity: number onVelocityChange: (newVelocity: number) => void disabled?: boolean - valueLabelFormat?: (value: number) => string + renderValue?: (value: number) => ReactNode } /** A slider for controlling the movement velocity of a robot */ export const VelocitySlider = observer((props: VelocitySliderProps) => { const theme = useTheme() - const valueLabelFormat = - props.valueLabelFormat || ((value: number) => `${value}`) - function onSliderChange(_event: Event, newVelocity: number | number[]) { if (newVelocity === props.velocity || !isNumber(newVelocity)) return @@ -26,18 +24,7 @@ export const VelocitySlider = observer((props: VelocitySliderProps) => { } return ( - <> - - {valueLabelFormat(props.velocity)} - + { }, }} /> - + {props.renderValue ? ( + props.renderValue(props.velocity) + ) : ( + + )} + ) }) + +type VelocitySliderLabelProps = { + value: string + sx?: SxProps +} + +export function VelocitySliderLabel({ value, sx }: VelocitySliderLabelProps) { + const theme = useTheme() + return ( + + + {value} + + + ) +} diff --git a/src/components/experimental/utils/AdornedSelect.tsx b/src/components/experimental/utils/AdornedSelect.tsx new file mode 100644 index 0000000..670d990 --- /dev/null +++ b/src/components/experimental/utils/AdornedSelect.tsx @@ -0,0 +1,42 @@ +import { + FormControl, + InputLabel, + Select, + styled, + type SelectProps, +} from "@mui/material" + +const AdornedFormControl = styled(FormControl)(({ theme }) => ({ + "&.MuiFormControl-root": { + ".MuiSelect-select": { + paddingTop: "20px", + paddingLeft: "12px", + }, + label: { + pointerEvents: "none", + fontSize: "16px", + }, + ".MuiInputLabel-root": { + "&.Mui-focused": { + color: theme.palette.text.primary, + }, + }, + }, +})) + +type AdornedSelectProps = { + labelValue: string + labelId: string +} & SelectProps + +export default function AdornedSelect({ + labelValue, + ...props +}: AdornedSelectProps) { + return ( + + {labelValue} + { - store.setSelectedCoordSystemId(event.target.value as string) - }} - disabled={store.isLocked} - > - {store.coordSystems.map((cs) => ( - - {/* Distinguish coordinate systems with the same name */} - {cs.name && store.coordSystemCountByName[cs.name] > 1 - ? `${cs.name} / ${cs.coordinate_system}` - : cs.name || cs.coordinate_system} - - ))} - - + {store.coordSystems.map((cs) => ( + + {/* Distinguish coordinate systems with the same name */} + {cs.name && store.coordSystemCountByName[cs.name] > 1 + ? `${cs.name} / ${cs.coordinate_system}` + : cs.name || cs.coordinate_system} + + ))} + - {/* TCP selection */} - - TCP - - - + {/* TCP selection */} - { + store.setSelectedTcpId(event.target.value as string) }} + disabled={store.isLocked} > - {/* Orientation */} - - - {t("Jogging.Cartesian.Orientation.lb")} - - - - - - - - - - + {store.tcps.map((tcp) => ( + + {tcp.id} + + ))} + - {/* Increment selection */} - - {"Increment"} - - - - + {store.selectedOrientation === "tool" + ? null + : store.discreteIncrementOptions.map((inc) => ( + + {store.currentMotionType === "translate" + ? `${inc.mm}mm` + : `${inc.degrees}°`} + + ))} + + ) }) diff --git a/src/components/jogging/JoggingPanel.tsx b/src/components/jogging/JoggingPanel.tsx index 03bd83a..18ffe31 100644 --- a/src/components/jogging/JoggingPanel.tsx +++ b/src/components/jogging/JoggingPanel.tsx @@ -1,4 +1,4 @@ -import { Stack, Tab, Tabs } from "@mui/material" +import { Stack, Tab, Tabs, type SxProps } from "@mui/material" import { NovaClient } from "@wandelbots/wandelbots-js" import { isString } from "lodash-es" import { runInAction } from "mobx" @@ -11,6 +11,8 @@ import { JoggingCartesianTab } from "./JoggingCartesianTab" import { JoggingJointTab } from "./JoggingJointTab" import { JoggingStore } from "./JoggingStore" +export type JoggingPanelTabId = "cartesian" | "joint" + export type JoggingPanelProps = { /** Either an existing NovaClient or the base url of a deployed Nova instance */ nova: NovaClient | string @@ -22,6 +24,7 @@ export type JoggingPanelProps = { children?: React.ReactNode /** Set this to true to disable jogging UI temporarily e.g. when a program is executing */ locked?: boolean + sx?: SxProps } /** @@ -80,16 +83,16 @@ export const JoggingPanel = externalizeComponent( {state.joggingStore ? ( - - {props.children} - + ) : ( )} @@ -105,6 +108,7 @@ const JoggingPanelInner = observer( }: { store: JoggingStore children?: React.ReactNode + childrenJoint?: React.ReactNode }) => { // Jogger is only active as long as the tab is focused useEffect(() => { @@ -155,7 +159,7 @@ const JoggingPanelInner = observer( } return ( - + {/* Tab selection */} {/* Current tab content */} - + {renderTabContent()} diff --git a/src/components/jogging/JoggingStore.ts b/src/components/jogging/JoggingStore.ts index 418f7a7..13acac1 100644 --- a/src/components/jogging/JoggingStore.ts +++ b/src/components/jogging/JoggingStore.ts @@ -31,6 +31,9 @@ export type DiscreteIncrementOption = (typeof discreteIncrementOptions)[number] export type IncrementOption = (typeof incrementOptions)[number] export type IncrementOptionId = IncrementOption["id"] +export const ORIENTATION_IDS = ["coordsys", "tool"] +export type OrientationId = (typeof ORIENTATION_IDS)[number] + export class JoggingStore { selectedTabId: "cartesian" | "joint" | "debug" = "cartesian" @@ -69,7 +72,7 @@ export class JoggingStore { * When in tool orientation, the robot moves in a direction relative to the * attached tool rotation. */ - selectedOrientation: "coordsys" | "tool" = "coordsys" + selectedOrientation: OrientationId = "coordsys" /** * Id of selected increment amount for jogging. Options are defined by robot pad. @@ -426,7 +429,7 @@ export class JoggingStore { this.selectedTcpId = id } - setSelectedOrientation(orientation: "coordsys" | "tool") { + setSelectedOrientation(orientation: OrientationId) { this.selectedOrientation = orientation } diff --git a/src/components/jogging/JoggingToggleButtonGroup.tsx b/src/components/jogging/JoggingToggleButtonGroup.tsx new file mode 100644 index 0000000..cb1387a --- /dev/null +++ b/src/components/jogging/JoggingToggleButtonGroup.tsx @@ -0,0 +1,25 @@ +import { styled, ToggleButtonGroup } from "@mui/material" + +export const JoggingToggleButtonGroup = styled(ToggleButtonGroup)( + ({ theme }) => ({ + "&.MuiToggleButtonGroup-root": { + background: theme.palette.backgroundPaperElevation?.[8], + borderRadius: "8px", + padding: "4px", + gap: "4px", + button: { + border: "none", + borderRadius: "4px", + flex: "1 1 0px", + minWidth: 0, + "&.MuiToggleButtonGroup-firstButton": { + borderRadius: "8px 4px 4px 8px", + }, + + "&.MuiToggleButtonGroup-lastButton": { + borderRadius: "4px 8px 8px 4px", + }, + }, + }, + }), +) diff --git a/src/components/jogging/JoggingVelocitySlider.tsx b/src/components/jogging/JoggingVelocitySlider.tsx index fece67c..5a4a685 100644 --- a/src/components/jogging/JoggingVelocitySlider.tsx +++ b/src/components/jogging/JoggingVelocitySlider.tsx @@ -1,7 +1,6 @@ -import { Stack } from "@mui/material" import { observer, useLocalObservable } from "mobx-react-lite" import { useTranslation } from "react-i18next" -import { VelocitySlider } from "../VelocitySlider" +import { VelocitySlider, VelocitySliderLabel } from "../VelocitySlider" import type { JoggingStore } from "./JoggingStore" export const JoggingVelocitySlider = observer( @@ -12,32 +11,31 @@ export const JoggingVelocitySlider = observer( get valueLabelFormat() { if (store.currentMotionType === "translate") { return (value: number) => - `v=${t("Jogging.Cartesian.Translation.velocityMmPerSec.lb", { amount: value })}` + `v = ${t("Jogging.Cartesian.Translation.velocityMmPerSec.lb", { amount: value })}` } else { return (value: number) => - `v=${t("Jogging.Cartesian.Rotation.velocityDegPerSec.lb", { amount: value })}` + `v = ${t("Jogging.Cartesian.Rotation.velocityDegPerSec.lb", { amount: value })}` } }, })) return ( - - - ( + - - + )} + /> ) }, ) diff --git a/src/components/jogging/PoseCartesianValues.tsx b/src/components/jogging/PoseCartesianValues.tsx new file mode 100644 index 0000000..f85df41 --- /dev/null +++ b/src/components/jogging/PoseCartesianValues.tsx @@ -0,0 +1,43 @@ +import { Stack } from "@mui/material" +import { + MotionStreamConnection, + poseToWandelscriptString, +} from "@wandelbots/wandelbots-js" +import { observer } from "mobx-react-lite" +import { useRef } from "react" +import { CopyableText } from "../CopyableText" +import { useAnimationFrame } from "../utils/hooks" + +export const PoseCartesianValues = observer( + ({ motionStream }: { motionStream: MotionStreamConnection }) => { + const poseHolderRef = useRef(null) + + function getCurrentPoseString() { + const tcpPose = motionStream.rapidlyChangingMotionState.tcp_pose + if (!tcpPose) return "" + return poseToWandelscriptString(tcpPose) + } + + useAnimationFrame(() => { + if (!poseHolderRef.current) { + return + } + const newPoseContent = getCurrentPoseString() + if (poseHolderRef.current.textContent === newPoseContent) { + return + } + + poseHolderRef.current.textContent = newPoseContent + }) + + return ( + + + + ) + }, +) diff --git a/src/components/jogging/PoseJointValues.tsx b/src/components/jogging/PoseJointValues.tsx new file mode 100644 index 0000000..6eba565 --- /dev/null +++ b/src/components/jogging/PoseJointValues.tsx @@ -0,0 +1,40 @@ +import { Stack } from "@mui/material" +import type { MotionStreamConnection } from "@wandelbots/wandelbots-js" +import { observer } from "mobx-react-lite" +import { useRef } from "react" +import { CopyableText } from "../CopyableText" +import { useAnimationFrame } from "../utils/hooks" + +export const PoseJointValues = observer( + ({ motionStream }: { motionStream: MotionStreamConnection }) => { + const poseHolderRef = useRef(null) + + function getCurrentPoseString() { + const { joints } = + motionStream.rapidlyChangingMotionState.state.joint_position + return `[${joints.map((j) => parseFloat(j.toFixed(4))).join(", ")}]` + } + + useAnimationFrame(() => { + if (!poseHolderRef.current) { + return + } + + const newPoseContent = getCurrentPoseString() + if (poseHolderRef.current.textContent === newPoseContent) { + return + } + poseHolderRef.current.textContent = newPoseContent + }) + + return ( + + + + ) + }, +) diff --git a/src/i18n/locales/de/translations.json b/src/i18n/locales/de/translations.json index 51db853..aafa4b5 100644 --- a/src/i18n/locales/de/translations.json +++ b/src/i18n/locales/de/translations.json @@ -12,5 +12,7 @@ "Jogging.Cartesian.Orientation.lb": "Orientierung", "Jogging.Activate.bt": "Jogging aktivieren", "Jogging.Activating.lb": "Jogging wird aktiviert", - "Jogging.JointLimitsReached.lb": "Joint-Limit für Joint {{jointNumbers}} erreicht" + "Jogging.JointLimitsReached.lb": "Joint-Limit für Joint {{jointNumbers}} erreicht", + "Jogging.Orientation.coordsys": "Base", + "Jogging.Orientation.tool": "Tool" } diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index fe4d3bc..911b544 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -12,5 +12,7 @@ "Jogging.Cartesian.Orientation.lb": "Orientation", "Jogging.Activate.bt": "Activate jogging", "Jogging.Activating.lb": "Activating jogging", - "Jogging.JointLimitsReached.lb": "Joint limit reached for joint {{jointNumbers}}" + "Jogging.JointLimitsReached.lb": "Joint limit reached for joint {{jointNumbers}}", + "Jogging.Orientation.coordsys": "Base", + "Jogging.Orientation.tool": "Tool" } diff --git a/src/icons/jog-minus.svg b/src/icons/jog-minus.svg new file mode 100644 index 0000000..40f6768 --- /dev/null +++ b/src/icons/jog-minus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/jog-plus.svg b/src/icons/jog-plus.svg new file mode 100644 index 0000000..11e8b60 --- /dev/null +++ b/src/icons/jog-plus.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/index.ts b/src/index.ts index 5e68fcb..423363e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,8 @@ export * from "./components/jogging/JoggingCartesianAxisControl" export * from "./components/jogging/JoggingJointRotationControl" export * from "./components/jogging/JoggingPanel" export type { JoggingStore } from "./components/jogging/JoggingStore" +export * from "./components/jogging/PoseCartesianValues" +export * from "./components/jogging/PoseJointValues" export * from "./components/LoadingCover" export * from "./components/modal/NoMotionGroupModal" export * from "./components/robots/AxisConfig" diff --git a/src/themes/createDarkTheme.ts b/src/themes/createDarkTheme.ts index f7dea07..520ac90 100644 --- a/src/themes/createDarkTheme.ts +++ b/src/themes/createDarkTheme.ts @@ -124,7 +124,7 @@ export function createDarkTheme(): Theme { MuiDivider: { styleOverrides: { root: { - border: "1px solid", + height: "1px", }, }, }, @@ -206,19 +206,34 @@ export function createDarkTheme(): Theme { X: { backgroundColor: "rgba(215, 66, 56, 1)", borderColor: "rgba(215, 66, 56, 1)", - buttonBackgroundColor: "rgba(241, 77, 66, 1)", + buttonBackgroundColor: { + default: "rgba(241, 77, 66, 1)", + pressed: "rgba(138, 41, 35, 1)", + hovered: "rgba(241, 77, 66, 1)", + disabled: "rgba(241, 77, 66, 1)", + }, color: "rgba(255, 255, 255, 1)", }, Y: { backgroundColor: "rgba(20, 151, 108, 1)", borderColor: "rgba(20, 151, 108, 1)", - buttonBackgroundColor: "rgba(28, 188, 135, 1)", + buttonBackgroundColor: { + default: "rgba(28, 188, 135, 1)", + pressed: "rgba(11, 89, 63, 1)", + disabled: "rgba(28, 188, 135, 1)", + hovered: "rgba(28, 188, 135, 1)", + }, color: "rgba(255, 255, 255, 1)", }, Z: { backgroundColor: "rgba(1, 87, 155, 1)", borderColor: "rgba(1, 87, 155, 1)", - buttonBackgroundColor: "rgba(2, 136, 209, 1)", + buttonBackgroundColor: { + default: "rgba(2, 136, 209, 1)", + pressed: "rgba(2, 64, 114, 1)", + disabled: "rgba(2, 136, 209, 1)", + hovered: "rgba(2, 136, 209, 1)", + }, color: "rgba(255, 255, 255, 1)", }, }, diff --git a/src/themes/themeTypes.ts b/src/themes/themeTypes.ts index 1e90805..b994a3e 100644 --- a/src/themes/themeTypes.ts +++ b/src/themes/themeTypes.ts @@ -17,7 +17,12 @@ export interface AxisControlComponentColors { color?: string borderColor?: string backgroundColor?: string - buttonBackgroundColor?: string + buttonBackgroundColor?: { + default?: string + pressed?: string + disabled?: string + hovered?: string + } } interface NovaComponentsExtension { diff --git a/stories/JoggingCartesianAxisControl.stories.tsx b/stories/JoggingCartesianAxisControl.stories.tsx index 410fd24..b5e26ca 100644 --- a/stories/JoggingCartesianAxisControl.stories.tsx +++ b/stories/JoggingCartesianAxisControl.stories.tsx @@ -1,7 +1,9 @@ +import { Typography, useTheme } from "@mui/material" import type { Meta, StoryObj } from "@storybook/react" -import { JoggingCartesianAxisControl } from "../src" import { useRef } from "react" +import { JoggingCartesianAxisControl } from "../src" import { useAnimationFrame } from "../src/components/utils/hooks" +import XAxisIcon from "../src/icons/axis-x.svg" const meta: Meta = { title: "Jogging/JoggingCartesianAxisControl", @@ -9,7 +11,6 @@ const meta: Meta = { component: JoggingCartesianAxisControl, args: { - color: "#F14D42", label: "X", disabled: false, }, @@ -17,6 +18,9 @@ const meta: Meta = { const joggingDirRef = useRef<"+" | "-" | null>(null) const joggingValueRef = useRef(0) + const theme = useTheme() + const colors = theme.componentsExt?.JoggingCartesian?.Axis?.X + useAnimationFrame(() => { if (joggingDirRef.current === "+") { joggingValueRef.current += 1 @@ -28,9 +32,23 @@ const meta: Meta = { return ( (joggingDirRef.current = direction)} stopJogging={() => (joggingDirRef.current = null)} - getDisplayedValue={() => joggingValueRef.current.toString()} + getDisplayedValue={() => `${joggingValueRef.current.toString()} mm`} + label={ + <> + + + X + + + } /> ) }, diff --git a/stories/JoggingPanel.stories.tsx b/stories/JoggingPanel.stories.tsx index 9a083e2..4486c47 100644 --- a/stories/JoggingPanel.stories.tsx +++ b/stories/JoggingPanel.stories.tsx @@ -1,18 +1,33 @@ -import { Box, useTheme } from "@mui/material" +import { Stack, Typography, useTheme } from "@mui/material" import type { Meta, StoryObj } from "@storybook/react" -import { JoggingPanel, type JoggingPanelProps } from "../src/index" +import { runInAction } from "mobx" +import { observer, useLocalObservable } from "mobx-react-lite" +import { + JoggingPanel, + type JoggingPanelProps, + type JoggingStore, +} from "../src/index" const JoggingPanelWrapper = (props: JoggingPanelProps) => { const theme = useTheme() + return ( - - - + + + ) } @@ -36,3 +51,40 @@ export const Default: StoryObj = { }, }, } + +const ChildrenDemoJoggingPanel = observer( + ({ props }: { props: JoggingPanelProps }) => { + const theme = useTheme() + + const state = useLocalObservable(() => ({ + joggingStore: null as JoggingStore | null, + })) + + return ( + runInAction(() => (state.joggingStore = store))} + > + {state.joggingStore && ( + + + {`${state.joggingStore.currentTab.id} children`} + + + )} + + ) + }, +)