@wandelbots/wandelbots-js-react-components 2.32.0-pr.feature-robot-precondition-list.372.8ed54a6 → 2.32.0-pr.feature-robot-precondition-list.372.5d8a86e

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.
@@ -1,7 +1,14 @@
1
- import { Box, Button, Card, Divider, Typography, useTheme } from "@mui/material"
1
+ import {
2
+ Box,
3
+ Button,
4
+ Card,
5
+ Divider,
6
+ Typography,
7
+ useMediaQuery,
8
+ useTheme,
9
+ } from "@mui/material"
2
10
  import { Bounds, useBounds } from "@react-three/drei"
3
11
  import { Canvas } from "@react-three/fiber"
4
- import type { DHParameter } from "@wandelbots/nova-api/v1"
5
12
  import type {
6
13
  ConnectedMotionGroup,
7
14
  RobotControllerStateOperationModeEnum,
@@ -16,27 +23,30 @@ import { PresetEnvironment } from "./3d-viewport/PresetEnvironment"
16
23
  import { CycleTimer } from "./CycleTimer"
17
24
  import type { ProgramState } from "./ProgramControl"
18
25
  import { ProgramStateIndicator } from "./ProgramStateIndicator"
19
- import { SupportedRobot } from "./robots/SupportedRobot"
26
+ import { Robot } from "./robots/Robot"
20
27
 
21
28
  // Component to refresh bounds when model renders
22
29
  function BoundsRefresher({
23
30
  modelRenderTrigger,
24
31
  children,
32
+ isPortrait = false,
25
33
  }: {
26
- modelRenderTrigger?: number
34
+ modelRenderTrigger: number
27
35
  children: React.ReactNode
36
+ isPortrait?: boolean
28
37
  }) {
29
- const api = useBounds()
38
+ const bounds = useBounds()
30
39
 
31
40
  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)
41
+ if (modelRenderTrigger > 0) {
42
+ // For portrait mode, use more aggressive fitting
43
+ if (isPortrait) {
44
+ bounds.refresh().clip().fit()
45
+ } else {
46
+ bounds.refresh().clip().fit()
47
+ }
38
48
  }
39
- }, [modelRenderTrigger, api])
49
+ }, [modelRenderTrigger, bounds, isPortrait])
40
50
 
41
51
  return <>{children}</>
42
52
  }
@@ -58,11 +68,9 @@ export interface RobotCardProps {
58
68
  onDriveToHomeRelease?: () => void
59
69
  /** Connected motion group for the robot */
60
70
  connectedMotionGroup: ConnectedMotionGroup
61
- /** Custom robot component to render (optional, defaults to SupportedRobot) */
71
+ /** Custom robot component to render (optional, defaults to Robot) */
62
72
  robotComponent?: React.ComponentType<{
63
- rapidlyChangingMotionState: ConnectedMotionGroup["rapidlyChangingMotionState"]
64
- modelFromController: string
65
- dhParameters: DHParameter[]
73
+ connectedMotionGroup: ConnectedMotionGroup
66
74
  flangeRef?: React.Ref<Group>
67
75
  postModelRender?: () => void
68
76
  transparentColor?: string
@@ -95,7 +103,12 @@ export interface RobotCardProps {
95
103
  * - Automatic layout switching based on aspect ratio:
96
104
  * - Portrait mode: Vertical layout with robot in center
97
105
  * - 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
106
+ * - Responsive 3D robot rendering:
107
+ * - Scales dynamically with container size
108
+ * - Hides at very small sizes to preserve usability
109
+ * - Adaptive margin based on available space
110
+ * - Smart spacing and padding that reduces at smaller sizes
111
+ * - Minimum size constraints for usability while maximizing content density
99
112
  * - Robot name displayed in Typography h6 at top-left
100
113
  * - Program state indicator below the name
101
114
  * - Auto-fitting 3D robot model that scales with container size
@@ -116,7 +129,7 @@ export const RobotCard = externalizeComponent(
116
129
  onDriveToHomePress,
117
130
  onDriveToHomeRelease,
118
131
  connectedMotionGroup,
119
- robotComponent: RobotComponent = SupportedRobot,
132
+ robotComponent: RobotComponent = Robot,
120
133
  cycleTimerComponent: CycleTimerComponent = CycleTimer,
121
134
  className,
122
135
  }: RobotCardProps) => {
@@ -127,22 +140,31 @@ export const RobotCard = externalizeComponent(
127
140
  const robotRef = useRef<Group>(null)
128
141
  const cardRef = useRef<HTMLDivElement>(null)
129
142
  const [isLandscape, setIsLandscape] = useState(false)
143
+ const [cardSize, setCardSize] = useState<{
144
+ width: number
145
+ height: number
146
+ }>({ width: 400, height: 600 })
130
147
  const [modelRenderTrigger, setModelRenderTrigger] = useState(0)
131
148
 
132
- // Hook to detect aspect ratio changes
149
+ // Responsive breakpoints
150
+ const isExtraSmall = useMediaQuery(theme.breakpoints.down("sm"))
151
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"))
152
+
153
+ // Hook to detect aspect ratio and size changes
133
154
  useEffect(() => {
134
- const checkAspectRatio = () => {
155
+ const checkDimensions = () => {
135
156
  if (cardRef.current) {
136
157
  const { offsetWidth, offsetHeight } = cardRef.current
137
158
  setIsLandscape(offsetWidth > offsetHeight)
159
+ setCardSize({ width: offsetWidth, height: offsetHeight })
138
160
  }
139
161
  }
140
162
 
141
163
  // Initial check
142
- checkAspectRatio()
164
+ checkDimensions()
143
165
 
144
166
  // Set up ResizeObserver to watch for size changes
145
- const resizeObserver = new ResizeObserver(checkAspectRatio)
167
+ const resizeObserver = new ResizeObserver(checkDimensions)
146
168
  if (cardRef.current) {
147
169
  resizeObserver.observe(cardRef.current)
148
170
  }
@@ -193,10 +215,36 @@ export const RobotCard = externalizeComponent(
193
215
  [],
194
216
  )
195
217
 
218
+ // Determine if robot should be hidden at small sizes to save space
219
+ // Less aggressive hiding for portrait mode since robot is in center
220
+ const shouldHideRobot = isLandscape
221
+ ? cardSize.width < 350 || cardSize.height < 250 // Aggressive for landscape
222
+ : cardSize.width < 250 || cardSize.height < 180 // Less aggressive for portrait
223
+
224
+ // Calculate responsive robot scale based on card size and orientation
225
+ const getRobotScale = () => {
226
+ if (shouldHideRobot) return 0
227
+
228
+ if (isLandscape) {
229
+ // More aggressive scaling for landscape since robot is on side
230
+ if (cardSize.width < 450) return 0.8
231
+ if (cardSize.width < 550) return 0.9
232
+ return 1
233
+ } else {
234
+ // Less aggressive scaling for portrait since robot is central
235
+ if (cardSize.width < 300) return 0.8
236
+ if (cardSize.width < 400) return 0.9
237
+ return 1
238
+ }
239
+ }
240
+
241
+ const robotScale = getRobotScale()
242
+
196
243
  return (
197
244
  <Card
198
245
  ref={cardRef}
199
246
  className={className}
247
+ elevation={5}
200
248
  sx={{
201
249
  width: "100%",
202
250
  height: "100%",
@@ -204,9 +252,10 @@ export const RobotCard = externalizeComponent(
204
252
  flexDirection: isLandscape ? "row" : "column",
205
253
  position: "relative",
206
254
  overflow: "hidden",
207
- minWidth: 300,
208
- minHeight: isLandscape ? 300 : 400,
209
- background: "var(--background-paper-elevation-8, #292B3F)",
255
+ minWidth: { xs: 180, sm: 220, md: 250 },
256
+ minHeight: isLandscape
257
+ ? { xs: 160, sm: 200, md: 250 }
258
+ : { xs: 200, sm: 280, md: 350 },
210
259
  border:
211
260
  "1px solid var(--secondary-_states-outlinedBorder, #FFFFFF1F)",
212
261
  borderRadius: "18px",
@@ -224,63 +273,65 @@ export const RobotCard = externalizeComponent(
224
273
  minHeight: "100%",
225
274
  maxHeight: "100%",
226
275
  borderRadius: 1,
227
- m: 2,
228
- mr: 1,
276
+ m: { xs: 1.5, sm: 2, md: 3 },
277
+ mr: { xs: 0.75, sm: 1, md: 1.5 },
229
278
  overflow: "hidden", // Prevent content from affecting container size
279
+ display: shouldHideRobot ? "none" : "block",
230
280
  }}
231
281
  >
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>
282
+ {!shouldHideRobot && (
283
+ <Canvas
284
+ camera={{
285
+ position: [4, 4, 4], // Move camera further back for orthographic view
286
+ fov: 15, // Low FOV for near-orthographic projection
287
+ }}
288
+ shadows
289
+ frameloop="demand"
290
+ style={{
291
+ borderRadius: theme.shape.borderRadius,
292
+ width: "100%",
293
+ height: "100%",
294
+ background: "transparent",
295
+ position: "absolute",
296
+ top: 0,
297
+ left: 0,
298
+ }}
299
+ dpr={[1, 2]}
300
+ gl={{ alpha: true, antialias: true }}
301
+ >
302
+ <PresetEnvironment />
303
+ <Bounds fit clip observe margin={1}>
304
+ <BoundsRefresher
305
+ modelRenderTrigger={modelRenderTrigger}
306
+ isPortrait={false}
307
+ >
308
+ <group ref={robotRef} scale={robotScale}>
309
+ <RobotComponent
310
+ connectedMotionGroup={connectedMotionGroup}
311
+ postModelRender={handleModelRender}
312
+ />
313
+ </group>
314
+ </BoundsRefresher>
315
+ </Bounds>
316
+ </Canvas>
317
+ )}
268
318
  </Box>
269
319
 
270
320
  {/* Content container on right */}
271
321
  <Box
272
322
  sx={{
273
- flex: "1",
323
+ flex: shouldHideRobot ? "1" : "1",
274
324
  display: "flex",
275
325
  flexDirection: "column",
276
326
  justifyContent: "flex-start",
327
+ width: shouldHideRobot ? "100%" : "50%",
277
328
  }}
278
329
  >
279
330
  {/* Header section with robot name and program state */}
280
331
  <Box
281
332
  sx={{
282
- p: 3,
283
- pb: 2,
333
+ p: { xs: 1.5, sm: 2, md: 3 },
334
+ pb: { xs: 1, sm: 1.5, md: 2 },
284
335
  textAlign: "left",
285
336
  }}
286
337
  >
@@ -297,7 +348,7 @@ export const RobotCard = externalizeComponent(
297
348
  {/* Bottom section with runtime, cycle time, and button */}
298
349
  <Box
299
350
  sx={{
300
- p: 3,
351
+ p: { xs: 1.5, sm: 2, md: 3 },
301
352
  pt: 0,
302
353
  flex: "1",
303
354
  display: "flex",
@@ -330,8 +381,8 @@ export const RobotCard = externalizeComponent(
330
381
  {/* Divider */}
331
382
  <Divider
332
383
  sx={{
333
- mt: 2,
334
- mb: 2,
384
+ mt: 1,
385
+ mb: 0,
335
386
  borderColor: theme.palette.divider,
336
387
  opacity: 0.5,
337
388
  }}
@@ -344,8 +395,8 @@ export const RobotCard = externalizeComponent(
344
395
  sx={{
345
396
  display: "flex",
346
397
  justifyContent: "flex-start",
347
- mt: 2,
348
- mb: 2,
398
+ mt: { xs: 1, sm: 1.5, md: 2 },
399
+ mb: { xs: 0.5, sm: 0.75, md: 1 },
349
400
  }}
350
401
  >
351
402
  <Button
@@ -353,7 +404,7 @@ export const RobotCard = externalizeComponent(
353
404
  variant="contained"
354
405
  color="secondary"
355
406
  size="small"
356
- disabled={true}
407
+ disabled={!driveToHomeEnabled}
357
408
  onMouseDown={handleDriveToHomeMouseDown}
358
409
  onMouseUp={handleDriveToHomeMouseUp}
359
410
  onMouseLeave={handleDriveToHomeMouseLeave}
@@ -375,127 +426,137 @@ export const RobotCard = externalizeComponent(
375
426
  ) : (
376
427
  <>
377
428
  {/* 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
429
  <Box
393
430
  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
431
+ p: 3,
432
+ height: "100%",
433
+ display: "flex",
434
+ flexDirection: "column",
401
435
  }}
402
436
  >
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>
437
+ {/* Header section with robot name and program state */}
438
+ <Box>
439
+ <Typography variant="h6" component="h2" sx={{ mb: 1 }}>
440
+ {robotName}
441
+ </Typography>
442
+ <ProgramStateIndicator
443
+ programState={programState}
444
+ safetyState={safetyState}
445
+ operationMode={operationMode}
446
+ />
447
+ </Box>
440
448
 
441
- {/* Bottom section with runtime, cycle time, and button */}
442
- <Box sx={{ p: 3, pt: 0 }}>
443
- {/* Runtime display */}
444
- <Typography
445
- variant="body1"
449
+ {/* 3D Robot viewport in center */}
450
+ <Box
446
451
  sx={{
447
- mb: 0,
448
- color: "var(--text-secondary, #FFFFFFB2)",
452
+ flex: shouldHideRobot ? 0 : 1,
453
+ position: "relative",
454
+ minHeight: shouldHideRobot
455
+ ? 0
456
+ : { xs: 120, sm: 150, md: 200 },
457
+ height: shouldHideRobot ? 0 : "auto",
458
+ borderRadius: 1,
459
+ overflow: "hidden",
460
+ display: shouldHideRobot ? "none" : "block",
449
461
  }}
450
462
  >
451
- {t("RobotCard.Runtime.lb")}
452
- </Typography>
463
+ {!shouldHideRobot && (
464
+ <Canvas
465
+ camera={{
466
+ position: [3, 3, 3], // Closer camera position for portrait to make robot larger
467
+ fov: 20, // Slightly higher FOV for portrait to fill better
468
+ }}
469
+ shadows
470
+ frameloop="demand"
471
+ style={{
472
+ borderRadius: theme.shape.borderRadius,
473
+ width: "100%",
474
+ height: "100%",
475
+ background: "transparent",
476
+ position: "absolute",
477
+ top: 0,
478
+ left: 0,
479
+ }}
480
+ dpr={[1, 2]}
481
+ gl={{ alpha: true, antialias: true }}
482
+ >
483
+ <PresetEnvironment />
484
+ <Bounds fit observe margin={1}>
485
+ <BoundsRefresher
486
+ modelRenderTrigger={modelRenderTrigger}
487
+ isPortrait={true}
488
+ >
489
+ <group ref={robotRef} scale={robotScale}>
490
+ <RobotComponent
491
+ connectedMotionGroup={connectedMotionGroup}
492
+ postModelRender={handleModelRender}
493
+ />
494
+ </group>
495
+ </BoundsRefresher>
496
+ </Bounds>
497
+ </Canvas>
498
+ )}
499
+ </Box>
453
500
 
454
- {/* Compact cycle time component directly below runtime */}
455
- <CycleTimerComponent
456
- variant="small"
457
- compact
458
- onCycleComplete={handleCycleComplete}
459
- />
501
+ {/* Bottom section with runtime, cycle time, and button */}
502
+ <Box>
503
+ {/* Runtime display */}
504
+ <Typography
505
+ variant="body1"
506
+ sx={{
507
+ mb: 0,
508
+ color: "var(--text-secondary, #FFFFFFB2)",
509
+ }}
510
+ >
511
+ {t("RobotCard.Runtime.lb")}
512
+ </Typography>
460
513
 
461
- {/* Divider */}
462
- <Divider
463
- sx={{
464
- mt: 2,
465
- mb: 2,
466
- borderColor: theme.palette.divider,
467
- opacity: 0.5,
468
- }}
469
- />
514
+ {/* Compact cycle time component directly below runtime */}
515
+ <CycleTimerComponent
516
+ variant="small"
517
+ compact
518
+ onCycleComplete={handleCycleComplete}
519
+ />
470
520
 
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}
521
+ {/* Divider */}
522
+ <Divider
491
523
  sx={{
492
- textTransform: "none",
493
- px: 1.5,
494
- py: 0.5,
524
+ mt: 1,
525
+ mb: 0,
526
+ borderColor: theme.palette.divider,
527
+ opacity: 0.5,
528
+ }}
529
+ />
530
+
531
+ {/* Drive to Home button with some space */}
532
+ <Box
533
+ sx={{
534
+ display: "flex",
535
+ justifyContent: "flex-start",
536
+ mt: { xs: 1, sm: 2, md: 5 },
537
+ mb: { xs: 0.5, sm: 0.75, md: 1 },
495
538
  }}
496
539
  >
497
- {t("RobotCard.DriveToHome.bt")}
498
- </Button>
540
+ <Button
541
+ ref={driveButtonRef}
542
+ variant="contained"
543
+ color="secondary"
544
+ size="small"
545
+ disabled={!driveToHomeEnabled}
546
+ onMouseDown={handleDriveToHomeMouseDown}
547
+ onMouseUp={handleDriveToHomeMouseUp}
548
+ onMouseLeave={handleDriveToHomeMouseLeave}
549
+ onTouchStart={handleDriveToHomeMouseDown}
550
+ onTouchEnd={handleDriveToHomeMouseUp}
551
+ sx={{
552
+ textTransform: "none",
553
+ px: 1.5,
554
+ py: 0.5,
555
+ }}
556
+ >
557
+ {t("RobotCard.DriveToHome.bt")}
558
+ </Button>
559
+ </Box>
499
560
  </Box>
500
561
  </Box>
501
562
  </>
@@ -1,15 +1,16 @@
1
- import { type ThreeElements } from "@react-three/fiber"
1
+ import type { ThreeElements } from "@react-three/fiber"
2
2
 
3
3
  import type { ConnectedMotionGroup } from "@wandelbots/nova-js/v1"
4
4
  import type { Group } from "three"
5
- import { SupportedRobot } from "./SupportedRobot"
6
5
  import { defaultGetModel } from "./robotModelLogic"
6
+ import { SupportedRobot } from "./SupportedRobot"
7
7
 
8
8
  export type RobotProps = {
9
9
  connectedMotionGroup: ConnectedMotionGroup
10
10
  getModel?: (modelFromController: string) => string
11
11
  flangeRef?: React.Ref<Group>
12
12
  transparentColor?: string
13
+ postModelRender?: () => void
13
14
  } & ThreeElements["group"]
14
15
 
15
16
  /**
@@ -28,6 +29,7 @@ export function Robot({
28
29
  getModel = defaultGetModel,
29
30
  flangeRef,
30
31
  transparentColor,
32
+ postModelRender,
31
33
  ...props
32
34
  }: RobotProps) {
33
35
  if (!connectedMotionGroup.dhParameters) {
@@ -44,6 +46,7 @@ export function Robot({
44
46
  getModel={getModel}
45
47
  flangeRef={flangeRef}
46
48
  transparentColor={transparentColor}
49
+ postModelRender={postModelRender}
47
50
  {...props}
48
51
  />
49
52
  )