@wandelbots/wandelbots-js-react-components 2.32.0-pr.feature-robot-precondition-list.372.297bf4f → 2.32.0-pr.feature-robot-precondition-list.372.8ed54a6
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.
- package/dist/components/CycleTimer.d.ts.map +1 -1
- package/dist/components/ProgramStateIndicator.d.ts.map +1 -1
- package/dist/components/RobotCard.d.ts +71 -0
- package/dist/components/RobotCard.d.ts.map +1 -0
- package/dist/components/RobotSetupReadinessIndicator.d.ts.map +1 -1
- package/dist/index.cjs +48 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6247 -5876
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CycleTimer.tsx +43 -64
- package/src/components/ProgramStateIndicator.tsx +15 -3
- package/src/components/RobotCard.tsx +507 -0
- package/src/components/RobotSetupReadinessIndicator.tsx +2 -0
- package/src/i18n/locales/de/translations.json +3 -1
- package/src/i18n/locales/en/translations.json +3 -1
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wandelbots/wandelbots-js-react-components",
|
|
3
|
-
"version": "2.32.0-pr.feature-robot-precondition-list.372.
|
|
3
|
+
"version": "2.32.0-pr.feature-robot-precondition-list.372.8ed54a6",
|
|
4
4
|
"description": "React UI toolkit for building applications on top of the Wandelbots platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -301,77 +301,56 @@ export const CycleTimer = externalizeComponent(
|
|
|
301
301
|
sx={{
|
|
302
302
|
display: "flex",
|
|
303
303
|
alignItems: "center",
|
|
304
|
-
|
|
304
|
+
m: 0,
|
|
305
|
+
gap: 1, // 8px gap between circle and text
|
|
305
306
|
}}
|
|
306
307
|
>
|
|
307
|
-
{/* Animated progress
|
|
308
|
+
{/* Animated progress ring icon */}
|
|
308
309
|
<Box
|
|
309
310
|
sx={{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
height: 40,
|
|
311
|
+
width: 20,
|
|
312
|
+
height: 20,
|
|
313
313
|
display: "flex",
|
|
314
314
|
alignItems: "center",
|
|
315
315
|
justifyContent: "center",
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
opacity: isPausedState ? 0.6 : 1,
|
|
317
|
+
transition: "opacity 0.2s ease",
|
|
318
318
|
}}
|
|
319
319
|
>
|
|
320
|
-
<
|
|
321
|
-
width=
|
|
322
|
-
height=
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
},
|
|
355
|
-
[`& circle`]: {
|
|
356
|
-
display: "none",
|
|
357
|
-
},
|
|
358
|
-
}}
|
|
359
|
-
/>
|
|
360
|
-
|
|
361
|
-
{/* Inner circle overlay to prevent flashing */}
|
|
362
|
-
<Box
|
|
363
|
-
sx={{
|
|
364
|
-
position: "absolute",
|
|
365
|
-
top: "50%",
|
|
366
|
-
left: "50%",
|
|
367
|
-
transform: "translate(-50%, -50%)",
|
|
368
|
-
width: 13,
|
|
369
|
-
height: 13,
|
|
370
|
-
borderRadius: "50%",
|
|
371
|
-
backgroundColor: theme.palette.background?.paper || "white",
|
|
372
|
-
pointerEvents: "none",
|
|
373
|
-
}}
|
|
374
|
-
/>
|
|
320
|
+
<svg
|
|
321
|
+
width="20"
|
|
322
|
+
height="20"
|
|
323
|
+
viewBox="0 0 20 20"
|
|
324
|
+
style={{ transform: "rotate(-90deg)" }}
|
|
325
|
+
role="img"
|
|
326
|
+
aria-label="Timer progress"
|
|
327
|
+
>
|
|
328
|
+
{/* Background ring */}
|
|
329
|
+
<circle
|
|
330
|
+
cx="10"
|
|
331
|
+
cy="10"
|
|
332
|
+
r="8"
|
|
333
|
+
fill="none"
|
|
334
|
+
stroke={theme.palette.success.main}
|
|
335
|
+
strokeWidth="2"
|
|
336
|
+
opacity={0.3}
|
|
337
|
+
/>
|
|
338
|
+
{/* Progress ring */}
|
|
339
|
+
<circle
|
|
340
|
+
cx="10"
|
|
341
|
+
cy="10"
|
|
342
|
+
r="8"
|
|
343
|
+
fill="none"
|
|
344
|
+
stroke={theme.palette.success.main}
|
|
345
|
+
strokeWidth="2"
|
|
346
|
+
strokeLinecap="round"
|
|
347
|
+
strokeDasharray={`${2 * Math.PI * 8}`}
|
|
348
|
+
strokeDashoffset={`${2 * Math.PI * 8 * (1 - progressValue / 100)}`}
|
|
349
|
+
style={{
|
|
350
|
+
transition: "stroke-dashoffset 0.1s ease-out",
|
|
351
|
+
}}
|
|
352
|
+
/>
|
|
353
|
+
</svg>
|
|
375
354
|
</Box>
|
|
376
355
|
|
|
377
356
|
{/* Timer text display */}
|
|
@@ -383,8 +362,8 @@ export const CycleTimer = externalizeComponent(
|
|
|
383
362
|
}}
|
|
384
363
|
>
|
|
385
364
|
{compact
|
|
386
|
-
? // Compact mode: show
|
|
387
|
-
formatTime(remainingTime)
|
|
365
|
+
? // Compact mode: show remaining time with "min." suffix
|
|
366
|
+
`${formatTime(remainingTime)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
|
|
388
367
|
: // Full mode: show "remaining / of total min." format
|
|
389
368
|
`${formatTime(remainingTime)} / ${t("CycleTimer.Time.lb", { time: formatTime(maxTime) })}`}
|
|
390
369
|
</Typography>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Chip, useTheme } from "@mui/material"
|
|
1
|
+
import { Chip, Typography, useTheme } from "@mui/material"
|
|
2
2
|
import type {
|
|
3
3
|
RobotControllerStateOperationModeEnum,
|
|
4
4
|
RobotControllerStateSafetyStateEnum,
|
|
@@ -131,14 +131,26 @@ export const ProgramStateIndicator = externalizeComponent(
|
|
|
131
131
|
return (
|
|
132
132
|
<Chip
|
|
133
133
|
className={className}
|
|
134
|
-
label={
|
|
134
|
+
label={
|
|
135
|
+
<Typography
|
|
136
|
+
variant="body2"
|
|
137
|
+
sx={{
|
|
138
|
+
fontSize: "0.75rem", // Smaller than body2
|
|
139
|
+
lineHeight: 1.2,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{fullLabel}
|
|
143
|
+
</Typography>
|
|
144
|
+
}
|
|
135
145
|
variant="filled"
|
|
136
146
|
sx={{
|
|
137
147
|
backgroundColor: color,
|
|
138
148
|
color: theme.palette.getContrastText(color),
|
|
139
149
|
fontWeight: 500,
|
|
150
|
+
height: "auto",
|
|
140
151
|
"& .MuiChip-label": {
|
|
141
|
-
paddingX:
|
|
152
|
+
paddingX: 1.5,
|
|
153
|
+
paddingY: 0.5,
|
|
142
154
|
},
|
|
143
155
|
}}
|
|
144
156
|
/>
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { Box, Button, Card, Divider, Typography, useTheme } from "@mui/material"
|
|
2
|
+
import { Bounds, useBounds } from "@react-three/drei"
|
|
3
|
+
import { Canvas } from "@react-three/fiber"
|
|
4
|
+
import type { DHParameter } from "@wandelbots/nova-api/v1"
|
|
5
|
+
import type {
|
|
6
|
+
ConnectedMotionGroup,
|
|
7
|
+
RobotControllerStateOperationModeEnum,
|
|
8
|
+
RobotControllerStateSafetyStateEnum,
|
|
9
|
+
} from "@wandelbots/nova-js/v1"
|
|
10
|
+
import { observer } from "mobx-react-lite"
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
12
|
+
import { useTranslation } from "react-i18next"
|
|
13
|
+
import type { Group } from "three"
|
|
14
|
+
import { externalizeComponent } from "../externalizeComponent"
|
|
15
|
+
import { PresetEnvironment } from "./3d-viewport/PresetEnvironment"
|
|
16
|
+
import { CycleTimer } from "./CycleTimer"
|
|
17
|
+
import type { ProgramState } from "./ProgramControl"
|
|
18
|
+
import { ProgramStateIndicator } from "./ProgramStateIndicator"
|
|
19
|
+
import { SupportedRobot } from "./robots/SupportedRobot"
|
|
20
|
+
|
|
21
|
+
// Component to refresh bounds when model renders
|
|
22
|
+
function BoundsRefresher({
|
|
23
|
+
modelRenderTrigger,
|
|
24
|
+
children,
|
|
25
|
+
}: {
|
|
26
|
+
modelRenderTrigger?: number
|
|
27
|
+
children: React.ReactNode
|
|
28
|
+
}) {
|
|
29
|
+
const api = useBounds()
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (modelRenderTrigger && modelRenderTrigger > 0) {
|
|
33
|
+
// Small delay to ensure the model is fully rendered
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
api.refresh().fit()
|
|
36
|
+
}, 100)
|
|
37
|
+
return () => clearTimeout(timer)
|
|
38
|
+
}
|
|
39
|
+
}, [modelRenderTrigger, api])
|
|
40
|
+
|
|
41
|
+
return <>{children}</>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RobotCardProps {
|
|
45
|
+
/** Name of the robot displayed at the top */
|
|
46
|
+
robotName: string
|
|
47
|
+
/** Current program state */
|
|
48
|
+
programState: ProgramState
|
|
49
|
+
/** Current safety state of the robot controller */
|
|
50
|
+
safetyState: RobotControllerStateSafetyStateEnum
|
|
51
|
+
/** Current operation mode of the robot controller */
|
|
52
|
+
operationMode: RobotControllerStateOperationModeEnum
|
|
53
|
+
/** Whether the "Drive to Home" button should be enabled */
|
|
54
|
+
driveToHomeEnabled?: boolean
|
|
55
|
+
/** Callback fired when "Drive to Home" button is pressed */
|
|
56
|
+
onDriveToHomePress?: () => void
|
|
57
|
+
/** Callback fired when "Drive to Home" button is released */
|
|
58
|
+
onDriveToHomeRelease?: () => void
|
|
59
|
+
/** Connected motion group for the robot */
|
|
60
|
+
connectedMotionGroup: ConnectedMotionGroup
|
|
61
|
+
/** Custom robot component to render (optional, defaults to SupportedRobot) */
|
|
62
|
+
robotComponent?: React.ComponentType<{
|
|
63
|
+
rapidlyChangingMotionState: ConnectedMotionGroup["rapidlyChangingMotionState"]
|
|
64
|
+
modelFromController: string
|
|
65
|
+
dhParameters: DHParameter[]
|
|
66
|
+
flangeRef?: React.Ref<Group>
|
|
67
|
+
postModelRender?: () => void
|
|
68
|
+
transparentColor?: string
|
|
69
|
+
getModel?: (modelFromController: string) => string
|
|
70
|
+
}>
|
|
71
|
+
/** Custom cycle timer component (optional, defaults to CycleTimer) */
|
|
72
|
+
cycleTimerComponent?: React.ComponentType<{
|
|
73
|
+
variant?: "default" | "small"
|
|
74
|
+
compact?: boolean
|
|
75
|
+
onCycleComplete: (controls: {
|
|
76
|
+
startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
|
|
77
|
+
pause: () => void
|
|
78
|
+
resume: () => void
|
|
79
|
+
isPaused: () => boolean
|
|
80
|
+
}) => void
|
|
81
|
+
onCycleEnd?: () => void
|
|
82
|
+
autoStart?: boolean
|
|
83
|
+
className?: string
|
|
84
|
+
}>
|
|
85
|
+
/** Additional CSS class name */
|
|
86
|
+
className?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A responsive card component that displays a 3D robot with states and controls.
|
|
91
|
+
* The card automatically adapts to its container's size and aspect ratio.
|
|
92
|
+
*
|
|
93
|
+
* Features:
|
|
94
|
+
* - Fully responsive Material-UI Card that adapts to container dimensions
|
|
95
|
+
* - Automatic layout switching based on aspect ratio:
|
|
96
|
+
* - Portrait mode: Vertical layout with robot in center
|
|
97
|
+
* - Landscape mode: Horizontal layout with robot on left, content on right (left-aligned)
|
|
98
|
+
* - Minimum size constraints (300px width, 400px height in portrait, 250px height in landscape) for usability
|
|
99
|
+
* - Robot name displayed in Typography h6 at top-left
|
|
100
|
+
* - Program state indicator below the name
|
|
101
|
+
* - Auto-fitting 3D robot model that scales with container size
|
|
102
|
+
* - Compact cycle time component with small variant
|
|
103
|
+
* - Transparent gray divider line
|
|
104
|
+
* - "Drive to Home" button with press-and-hold functionality
|
|
105
|
+
* - Localization support via react-i18next
|
|
106
|
+
* - Material-UI theming integration
|
|
107
|
+
*/
|
|
108
|
+
export const RobotCard = externalizeComponent(
|
|
109
|
+
observer(
|
|
110
|
+
({
|
|
111
|
+
robotName,
|
|
112
|
+
programState,
|
|
113
|
+
safetyState,
|
|
114
|
+
operationMode,
|
|
115
|
+
driveToHomeEnabled = false,
|
|
116
|
+
onDriveToHomePress,
|
|
117
|
+
onDriveToHomeRelease,
|
|
118
|
+
connectedMotionGroup,
|
|
119
|
+
robotComponent: RobotComponent = SupportedRobot,
|
|
120
|
+
cycleTimerComponent: CycleTimerComponent = CycleTimer,
|
|
121
|
+
className,
|
|
122
|
+
}: RobotCardProps) => {
|
|
123
|
+
const theme = useTheme()
|
|
124
|
+
const { t } = useTranslation()
|
|
125
|
+
const [isDriveToHomePressed, setIsDriveToHomePressed] = useState(false)
|
|
126
|
+
const driveButtonRef = useRef<HTMLButtonElement>(null)
|
|
127
|
+
const robotRef = useRef<Group>(null)
|
|
128
|
+
const cardRef = useRef<HTMLDivElement>(null)
|
|
129
|
+
const [isLandscape, setIsLandscape] = useState(false)
|
|
130
|
+
const [modelRenderTrigger, setModelRenderTrigger] = useState(0)
|
|
131
|
+
|
|
132
|
+
// Hook to detect aspect ratio changes
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const checkAspectRatio = () => {
|
|
135
|
+
if (cardRef.current) {
|
|
136
|
+
const { offsetWidth, offsetHeight } = cardRef.current
|
|
137
|
+
setIsLandscape(offsetWidth > offsetHeight)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Initial check
|
|
142
|
+
checkAspectRatio()
|
|
143
|
+
|
|
144
|
+
// Set up ResizeObserver to watch for size changes
|
|
145
|
+
const resizeObserver = new ResizeObserver(checkAspectRatio)
|
|
146
|
+
if (cardRef.current) {
|
|
147
|
+
resizeObserver.observe(cardRef.current)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
resizeObserver.disconnect()
|
|
152
|
+
}
|
|
153
|
+
}, [])
|
|
154
|
+
|
|
155
|
+
const handleModelRender = useCallback(() => {
|
|
156
|
+
// Trigger bounds refresh when model renders
|
|
157
|
+
setModelRenderTrigger((prev) => prev + 1)
|
|
158
|
+
}, [])
|
|
159
|
+
|
|
160
|
+
const handleDriveToHomeMouseDown = useCallback(() => {
|
|
161
|
+
if (!driveToHomeEnabled || !onDriveToHomePress) return
|
|
162
|
+
setIsDriveToHomePressed(true)
|
|
163
|
+
onDriveToHomePress()
|
|
164
|
+
}, [driveToHomeEnabled, onDriveToHomePress])
|
|
165
|
+
|
|
166
|
+
const handleDriveToHomeMouseUp = useCallback(() => {
|
|
167
|
+
if (!driveToHomeEnabled || !onDriveToHomeRelease) return
|
|
168
|
+
setIsDriveToHomePressed(false)
|
|
169
|
+
onDriveToHomeRelease()
|
|
170
|
+
}, [driveToHomeEnabled, onDriveToHomeRelease])
|
|
171
|
+
|
|
172
|
+
const handleDriveToHomeMouseLeave = useCallback(() => {
|
|
173
|
+
if (isDriveToHomePressed && onDriveToHomeRelease) {
|
|
174
|
+
setIsDriveToHomePressed(false)
|
|
175
|
+
onDriveToHomeRelease()
|
|
176
|
+
}
|
|
177
|
+
}, [isDriveToHomePressed, onDriveToHomeRelease])
|
|
178
|
+
|
|
179
|
+
// Mock cycle timer controls for now
|
|
180
|
+
const handleCycleComplete = useCallback(
|
|
181
|
+
(_controls: {
|
|
182
|
+
startNewCycle: (
|
|
183
|
+
maxTimeSeconds: number,
|
|
184
|
+
elapsedSeconds?: number,
|
|
185
|
+
) => void
|
|
186
|
+
pause: () => void
|
|
187
|
+
resume: () => void
|
|
188
|
+
isPaused: () => boolean
|
|
189
|
+
}) => {
|
|
190
|
+
// TODO: Implement cycle timer integration if needed
|
|
191
|
+
// Controls are available here for future integration
|
|
192
|
+
},
|
|
193
|
+
[],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Card
|
|
198
|
+
ref={cardRef}
|
|
199
|
+
className={className}
|
|
200
|
+
sx={{
|
|
201
|
+
width: "100%",
|
|
202
|
+
height: "100%",
|
|
203
|
+
display: "flex",
|
|
204
|
+
flexDirection: isLandscape ? "row" : "column",
|
|
205
|
+
position: "relative",
|
|
206
|
+
overflow: "hidden",
|
|
207
|
+
minWidth: 300,
|
|
208
|
+
minHeight: isLandscape ? 300 : 400,
|
|
209
|
+
background: "var(--background-paper-elevation-8, #292B3F)",
|
|
210
|
+
border:
|
|
211
|
+
"1px solid var(--secondary-_states-outlinedBorder, #FFFFFF1F)",
|
|
212
|
+
borderRadius: "18px",
|
|
213
|
+
boxShadow: "none",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{isLandscape ? (
|
|
217
|
+
<>
|
|
218
|
+
{/* Landscape Layout: Robot on left, content on right */}
|
|
219
|
+
<Box
|
|
220
|
+
sx={{
|
|
221
|
+
flex: "0 0 50%",
|
|
222
|
+
position: "relative",
|
|
223
|
+
height: "100%",
|
|
224
|
+
minHeight: "100%",
|
|
225
|
+
maxHeight: "100%",
|
|
226
|
+
borderRadius: 1,
|
|
227
|
+
m: 2,
|
|
228
|
+
mr: 1,
|
|
229
|
+
overflow: "hidden", // Prevent content from affecting container size
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<Canvas
|
|
233
|
+
camera={{
|
|
234
|
+
position: [2, 2, 2],
|
|
235
|
+
fov: 45,
|
|
236
|
+
}}
|
|
237
|
+
shadows
|
|
238
|
+
style={{
|
|
239
|
+
borderRadius: theme.shape.borderRadius,
|
|
240
|
+
width: "100%",
|
|
241
|
+
height: "100%",
|
|
242
|
+
background: "transparent",
|
|
243
|
+
position: "absolute",
|
|
244
|
+
top: 0,
|
|
245
|
+
left: 0,
|
|
246
|
+
}}
|
|
247
|
+
dpr={[1, 2]}
|
|
248
|
+
gl={{ alpha: true, antialias: true }}
|
|
249
|
+
>
|
|
250
|
+
<PresetEnvironment />
|
|
251
|
+
<Bounds fit clip observe margin={1}>
|
|
252
|
+
<BoundsRefresher modelRenderTrigger={modelRenderTrigger}>
|
|
253
|
+
<group ref={robotRef}>
|
|
254
|
+
<RobotComponent
|
|
255
|
+
rapidlyChangingMotionState={
|
|
256
|
+
connectedMotionGroup.rapidlyChangingMotionState
|
|
257
|
+
}
|
|
258
|
+
modelFromController={
|
|
259
|
+
connectedMotionGroup.modelFromController || ""
|
|
260
|
+
}
|
|
261
|
+
dhParameters={connectedMotionGroup.dhParameters || []}
|
|
262
|
+
postModelRender={handleModelRender}
|
|
263
|
+
/>
|
|
264
|
+
</group>
|
|
265
|
+
</BoundsRefresher>
|
|
266
|
+
</Bounds>
|
|
267
|
+
</Canvas>
|
|
268
|
+
</Box>
|
|
269
|
+
|
|
270
|
+
{/* Content container on right */}
|
|
271
|
+
<Box
|
|
272
|
+
sx={{
|
|
273
|
+
flex: "1",
|
|
274
|
+
display: "flex",
|
|
275
|
+
flexDirection: "column",
|
|
276
|
+
justifyContent: "flex-start",
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{/* Header section with robot name and program state */}
|
|
280
|
+
<Box
|
|
281
|
+
sx={{
|
|
282
|
+
p: 3,
|
|
283
|
+
pb: 2,
|
|
284
|
+
textAlign: "left",
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<Typography variant="h6" component="h2" sx={{ mb: 1 }}>
|
|
288
|
+
{robotName}
|
|
289
|
+
</Typography>
|
|
290
|
+
<ProgramStateIndicator
|
|
291
|
+
programState={programState}
|
|
292
|
+
safetyState={safetyState}
|
|
293
|
+
operationMode={operationMode}
|
|
294
|
+
/>
|
|
295
|
+
</Box>
|
|
296
|
+
|
|
297
|
+
{/* Bottom section with runtime, cycle time, and button */}
|
|
298
|
+
<Box
|
|
299
|
+
sx={{
|
|
300
|
+
p: 3,
|
|
301
|
+
pt: 0,
|
|
302
|
+
flex: "1",
|
|
303
|
+
display: "flex",
|
|
304
|
+
flexDirection: "column",
|
|
305
|
+
justifyContent: "space-between",
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
<Box>
|
|
309
|
+
{/* Runtime display */}
|
|
310
|
+
<Typography
|
|
311
|
+
variant="body1"
|
|
312
|
+
sx={{
|
|
313
|
+
mb: 0,
|
|
314
|
+
color: "var(--text-secondary, #FFFFFFB2)",
|
|
315
|
+
textAlign: "left",
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
{t("RobotCard.Runtime.lb")}
|
|
319
|
+
</Typography>
|
|
320
|
+
|
|
321
|
+
{/* Compact cycle time component directly below runtime */}
|
|
322
|
+
<Box sx={{ textAlign: "left" }}>
|
|
323
|
+
<CycleTimerComponent
|
|
324
|
+
variant="small"
|
|
325
|
+
compact
|
|
326
|
+
onCycleComplete={handleCycleComplete}
|
|
327
|
+
/>
|
|
328
|
+
</Box>
|
|
329
|
+
|
|
330
|
+
{/* Divider */}
|
|
331
|
+
<Divider
|
|
332
|
+
sx={{
|
|
333
|
+
mt: 2,
|
|
334
|
+
mb: 2,
|
|
335
|
+
borderColor: theme.palette.divider,
|
|
336
|
+
opacity: 0.5,
|
|
337
|
+
}}
|
|
338
|
+
/>
|
|
339
|
+
</Box>
|
|
340
|
+
|
|
341
|
+
<Box sx={{ mt: "auto" }}>
|
|
342
|
+
{/* Drive to Home button with some space */}
|
|
343
|
+
<Box
|
|
344
|
+
sx={{
|
|
345
|
+
display: "flex",
|
|
346
|
+
justifyContent: "flex-start",
|
|
347
|
+
mt: 2,
|
|
348
|
+
mb: 2,
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<Button
|
|
352
|
+
ref={driveButtonRef}
|
|
353
|
+
variant="contained"
|
|
354
|
+
color="secondary"
|
|
355
|
+
size="small"
|
|
356
|
+
disabled={true}
|
|
357
|
+
onMouseDown={handleDriveToHomeMouseDown}
|
|
358
|
+
onMouseUp={handleDriveToHomeMouseUp}
|
|
359
|
+
onMouseLeave={handleDriveToHomeMouseLeave}
|
|
360
|
+
onTouchStart={handleDriveToHomeMouseDown}
|
|
361
|
+
onTouchEnd={handleDriveToHomeMouseUp}
|
|
362
|
+
sx={{
|
|
363
|
+
textTransform: "none",
|
|
364
|
+
px: 1.5,
|
|
365
|
+
py: 0.5,
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{t("RobotCard.DriveToHome.bt")}
|
|
369
|
+
</Button>
|
|
370
|
+
</Box>
|
|
371
|
+
</Box>
|
|
372
|
+
</Box>
|
|
373
|
+
</Box>
|
|
374
|
+
</>
|
|
375
|
+
) : (
|
|
376
|
+
<>
|
|
377
|
+
{/* Portrait Layout: Header, Robot, Footer */}
|
|
378
|
+
|
|
379
|
+
{/* Header section with robot name and program state */}
|
|
380
|
+
<Box sx={{ p: 3, pb: 1 }}>
|
|
381
|
+
<Typography variant="h6" component="h2" sx={{ mb: 1 }}>
|
|
382
|
+
{robotName}
|
|
383
|
+
</Typography>
|
|
384
|
+
<ProgramStateIndicator
|
|
385
|
+
programState={programState}
|
|
386
|
+
safetyState={safetyState}
|
|
387
|
+
operationMode={operationMode}
|
|
388
|
+
/>
|
|
389
|
+
</Box>
|
|
390
|
+
|
|
391
|
+
{/* 3D Robot viewport in center */}
|
|
392
|
+
<Box
|
|
393
|
+
sx={{
|
|
394
|
+
flex: 1,
|
|
395
|
+
position: "relative",
|
|
396
|
+
minHeight: 200,
|
|
397
|
+
borderRadius: 1,
|
|
398
|
+
mx: 3,
|
|
399
|
+
mb: 1,
|
|
400
|
+
overflow: "hidden", // Prevent content from affecting container size
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
<Canvas
|
|
404
|
+
camera={{
|
|
405
|
+
position: [2, 2, 2],
|
|
406
|
+
fov: 45,
|
|
407
|
+
}}
|
|
408
|
+
shadows
|
|
409
|
+
style={{
|
|
410
|
+
borderRadius: theme.shape.borderRadius,
|
|
411
|
+
width: "100%",
|
|
412
|
+
height: "100%",
|
|
413
|
+
background: "transparent",
|
|
414
|
+
position: "absolute",
|
|
415
|
+
top: 0,
|
|
416
|
+
left: 0,
|
|
417
|
+
}}
|
|
418
|
+
dpr={[1, 2]}
|
|
419
|
+
gl={{ alpha: true, antialias: true }}
|
|
420
|
+
>
|
|
421
|
+
<PresetEnvironment />
|
|
422
|
+
<Bounds fit clip observe margin={1.2}>
|
|
423
|
+
<BoundsRefresher modelRenderTrigger={modelRenderTrigger}>
|
|
424
|
+
<group ref={robotRef}>
|
|
425
|
+
<RobotComponent
|
|
426
|
+
rapidlyChangingMotionState={
|
|
427
|
+
connectedMotionGroup.rapidlyChangingMotionState
|
|
428
|
+
}
|
|
429
|
+
modelFromController={
|
|
430
|
+
connectedMotionGroup.modelFromController || ""
|
|
431
|
+
}
|
|
432
|
+
dhParameters={connectedMotionGroup.dhParameters || []}
|
|
433
|
+
postModelRender={handleModelRender}
|
|
434
|
+
/>
|
|
435
|
+
</group>
|
|
436
|
+
</BoundsRefresher>
|
|
437
|
+
</Bounds>
|
|
438
|
+
</Canvas>
|
|
439
|
+
</Box>
|
|
440
|
+
|
|
441
|
+
{/* Bottom section with runtime, cycle time, and button */}
|
|
442
|
+
<Box sx={{ p: 3, pt: 0 }}>
|
|
443
|
+
{/* Runtime display */}
|
|
444
|
+
<Typography
|
|
445
|
+
variant="body1"
|
|
446
|
+
sx={{
|
|
447
|
+
mb: 0,
|
|
448
|
+
color: "var(--text-secondary, #FFFFFFB2)",
|
|
449
|
+
}}
|
|
450
|
+
>
|
|
451
|
+
{t("RobotCard.Runtime.lb")}
|
|
452
|
+
</Typography>
|
|
453
|
+
|
|
454
|
+
{/* Compact cycle time component directly below runtime */}
|
|
455
|
+
<CycleTimerComponent
|
|
456
|
+
variant="small"
|
|
457
|
+
compact
|
|
458
|
+
onCycleComplete={handleCycleComplete}
|
|
459
|
+
/>
|
|
460
|
+
|
|
461
|
+
{/* Divider */}
|
|
462
|
+
<Divider
|
|
463
|
+
sx={{
|
|
464
|
+
mt: 2,
|
|
465
|
+
mb: 2,
|
|
466
|
+
borderColor: theme.palette.divider,
|
|
467
|
+
opacity: 0.5,
|
|
468
|
+
}}
|
|
469
|
+
/>
|
|
470
|
+
|
|
471
|
+
{/* Drive to Home button with some space */}
|
|
472
|
+
<Box
|
|
473
|
+
sx={{
|
|
474
|
+
display: "flex",
|
|
475
|
+
justifyContent: "flex-start",
|
|
476
|
+
mt: 5,
|
|
477
|
+
mb: 2,
|
|
478
|
+
}}
|
|
479
|
+
>
|
|
480
|
+
<Button
|
|
481
|
+
ref={driveButtonRef}
|
|
482
|
+
variant="contained"
|
|
483
|
+
color="secondary"
|
|
484
|
+
size="small"
|
|
485
|
+
disabled={true}
|
|
486
|
+
onMouseDown={handleDriveToHomeMouseDown}
|
|
487
|
+
onMouseUp={handleDriveToHomeMouseUp}
|
|
488
|
+
onMouseLeave={handleDriveToHomeMouseLeave}
|
|
489
|
+
onTouchStart={handleDriveToHomeMouseDown}
|
|
490
|
+
onTouchEnd={handleDriveToHomeMouseUp}
|
|
491
|
+
sx={{
|
|
492
|
+
textTransform: "none",
|
|
493
|
+
px: 1.5,
|
|
494
|
+
py: 0.5,
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
{t("RobotCard.DriveToHome.bt")}
|
|
498
|
+
</Button>
|
|
499
|
+
</Box>
|
|
500
|
+
</Box>
|
|
501
|
+
</>
|
|
502
|
+
)}
|
|
503
|
+
</Card>
|
|
504
|
+
)
|
|
505
|
+
},
|
|
506
|
+
),
|
|
507
|
+
)
|
|
@@ -112,8 +112,10 @@ export const RobotSetupReadinessIndicator = externalizeComponent(
|
|
|
112
112
|
backgroundColor,
|
|
113
113
|
color: theme.palette.getContrastText(backgroundColor),
|
|
114
114
|
fontWeight: 500,
|
|
115
|
+
height: "auto",
|
|
115
116
|
"& .MuiChip-label": {
|
|
116
117
|
paddingX: 1.5,
|
|
118
|
+
paddingY: 0.5,
|
|
117
119
|
},
|
|
118
120
|
}}
|
|
119
121
|
/>
|