From 474a328973b8fea546b038b5a5cd64b0f5764076 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Wed, 29 Oct 2025 14:48:29 -0400 Subject: [PATCH 01/11] add joint positions widget --- .../arm_widgets/joint_positions_widget.dart | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 lib/widgets/resources/arm_widgets/joint_positions_widget.dart diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart new file mode 100644 index 00000000000..70dcf912683 --- /dev/null +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../viam_sdk.dart' as viam; + +class JointPositionsWidget extends StatefulWidget { + final viam.Arm arm; + const JointPositionsWidget({super.key, required this.arm}); + + @override + State createState() => _JointPositionsWidgetState(); +} + +class _JointPositionsWidgetState extends State { + List _jointValues = []; + bool _isLive = false; + + @override + void initState() { + super.initState(); + _getJointInfo(); + } + + Future _getJointInfo() async { + _jointValues = await widget.arm.jointPositions(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Divider(), + Text( + 'Joint Angles', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + Divider(), + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: _jointValues.isEmpty + ? [CircularProgressIndicator.adaptive()] + : List.generate(_jointValues.length, (index) { + return _BuildJointControlRow(index: index, arm: widget.arm, startJointValues: _jointValues); + }), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0, 20.0, 20.0), + child: Row( + spacing: 8, + children: [ + Switch( + value: _isLive, + activeColor: Colors.green, + inactiveTrackColor: Colors.transparent, + onChanged: (newValue) { + setState(() { + _isLive = newValue; + }); + }, + ), + Text( + "Live", + style: TextStyle(color: Colors.black), + ), + Spacer(), + OutlinedButtonTheme( + data: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.black, + iconColor: Colors.black, + overlayColor: Colors.grey, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4)))), + ), + child: OutlinedButton.icon( + onPressed: _isLive ? null : () {}, + label: Text("Execute"), + icon: Icon(Icons.play_arrow), + ), + ) + ], + ), + ), + ], + ); + } +} + +class _BuildJointControlRow extends StatefulWidget { + final int index; + final viam.Arm arm; + final List startJointValues; + const _BuildJointControlRow({required this.index, required this.arm, required this.startJointValues}); + + @override + State<_BuildJointControlRow> createState() => _BuildJointControlRowState(); +} + +class _BuildJointControlRowState extends State<_BuildJointControlRow> { + static const double _minPosition = 0.0; + static const double _maxPosition = 180.0; + + List _jointValues = []; + List _textControllers = []; + + @override + void initState() { + _jointValues = widget.startJointValues; + _textControllers = List.generate( + _jointValues.length, + (index) => TextEditingController(text: _jointValues[index].toStringAsFixed(1)), + ); + super.initState(); + } + + @override + void dispose() { + for (final controller in _textControllers) { + controller.dispose(); + } + super.dispose(); + } + + void _updateJointValue(int index, double value) { + final clampedValue = value.clamp(_minPosition, _maxPosition); + + setState(() { + _jointValues[index] = clampedValue; + final formattedValue = clampedValue.toStringAsFixed(1); + if (_textControllers[index].text != formattedValue) { + _textControllers[index].text = formattedValue; + _textControllers[index].selection = TextSelection.fromPosition( + TextPosition(offset: _textControllers[index].text.length), + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + SizedBox( + width: 30, + child: Text( + 'J${widget.index + 1}', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Expanded( + child: SliderTheme( + data: SliderThemeData( + activeTrackColor: Colors.black, + inactiveTrackColor: Colors.grey, + thumbColor: Colors.black, + overlayColor: Colors.transparent, + showValueIndicator: ShowValueIndicator.never, + ), + child: Slider( + value: _jointValues[widget.index], + min: _minPosition, + max: _maxPosition, + divisions: (_maxPosition - _minPosition).toInt(), + onChanged: (newValue) => _updateJointValue(widget.index, newValue), + ), + ), + ), + SizedBox( + width: 70, + child: TextField( + controller: _textControllers[widget.index], + textAlign: TextAlign.center, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), + ], + style: const TextStyle(color: Colors.black), + cursorColor: Colors.black, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.black), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 8), + ), + onSubmitted: (newValue) { + final parsedValue = double.tryParse(newValue) ?? _jointValues[widget.index]; + _updateJointValue(widget.index, parsedValue); + }, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + _updateJointValue(widget.index, _jointValues[widget.index] - 1.0); + }, + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + _updateJointValue(widget.index, _jointValues[widget.index] + 1.0); + }, + ), + ], + ), + ); + } +} From a60ae2eed1340e24295025a5c92da83e1fa88dab Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Wed, 29 Oct 2025 14:50:03 -0400 Subject: [PATCH 02/11] arm_new generic screen --- lib/widgets/resources/arm_new.dart | 232 +---------------------------- 1 file changed, 2 insertions(+), 230 deletions(-) diff --git a/lib/widgets/resources/arm_new.dart b/lib/widgets/resources/arm_new.dart index ce9eb653243..f031e1f549c 100644 --- a/lib/widgets/resources/arm_new.dart +++ b/lib/widgets/resources/arm_new.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import '../../viam_sdk.dart'; - -final size = 300.0; +import 'arm_widgets/joint_positions_widget.dart'; /// A widget to control an [Arm]. class ViamArmWidgetNew extends StatelessWidget { @@ -18,235 +17,8 @@ class ViamArmWidgetNew extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - Divider(), - Text( - 'End-effector Position', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Divider(), - Stack( - children: [ - _SlantedArrowPad( - // TODO: add functions for arrow functionality - onUp: () {}, - onDown: () {}, - onLeft: () {}, - onRight: () {}, - ), - _BuildCornerButton( - alignment: Alignment.topLeft, - direction: ArrowDirection.up, - label: 'Z+', - onPressed: () {}, - ), - _BuildCornerButton( - alignment: Alignment.topRight, - direction: ArrowDirection.down, - label: 'Z-', - onPressed: () {}, - ), - ], - ), + JointPositionsWidget(arm: arm), ], ); } } - -class _BuildCornerButton extends StatelessWidget { - final Alignment alignment; - final ArrowDirection direction; - final String label; - final VoidCallback onPressed; - - const _BuildCornerButton({ - required this.alignment, - required this.direction, - required this.label, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return Align( - alignment: alignment, - child: Padding( - padding: const EdgeInsets.all(24.0), - child: SizedBox( - width: 100, - height: 100, - child: IconButton( - icon: Stack( - alignment: Alignment.center, - children: [ - CustomPaint( - painter: _LinearArrowPainter(direction: direction, color: Colors.black), - child: const SizedBox.expand(), - ), - Text( - label, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - onPressed: onPressed, - ), - ), - ), - ); - } -} - -enum ArrowDirection { up, down, left, right } - -class _LinearArrowPainter extends CustomPainter { - final ArrowDirection direction; - final Color color; - - _LinearArrowPainter({required this.direction, required this.color}); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - - final path = Path(); - final w = size.width; - final h = size.height; - - switch (direction) { - case ArrowDirection.up: - path.moveTo(w / 2, 0); - path.lineTo(w, h * 0.6); - path.lineTo(w * 0.8, h * 0.6); - path.lineTo(w * 0.8, h); - path.lineTo(w * 0.2, h); - path.lineTo(w * 0.2, h * 0.6); - path.lineTo(0, h * 0.6); - break; - case ArrowDirection.down: - path.moveTo(w / 2, h); - path.lineTo(0, h * 0.4); - path.lineTo(w * 0.2, h * 0.4); - path.lineTo(w * 0.2, 0); - path.lineTo(w * 0.8, 0); - path.lineTo(w * 0.8, h * 0.4); - path.lineTo(w, h * 0.4); - break; - case ArrowDirection.left: - path.moveTo(0, h / 2); - path.lineTo(w * 0.6, 0); - path.lineTo(w * 0.6, h * 0.2); - path.lineTo(w, h * 0.2); - path.lineTo(w, h * 0.8); - path.lineTo(w * 0.6, h * 0.8); - path.lineTo(w * 0.6, h); - break; - case ArrowDirection.right: - path.moveTo(w, h / 2); - path.lineTo(w * 0.4, h); - path.lineTo(w * 0.4, h * 0.8); - path.lineTo(0, h * 0.8); - path.lineTo(0, h * 0.2); - path.lineTo(w * 0.4, h * 0.2); - path.lineTo(w * 0.4, 0); - break; - } - - path.close(); - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(covariant _LinearArrowPainter oldDelegate) => false; -} - -class _SlantedArrowPad extends StatelessWidget { - final VoidCallback? onUp; - final VoidCallback? onDown; - final VoidCallback? onLeft; - final VoidCallback? onRight; - - const _SlantedArrowPad({ - super.key, - this.onUp, - this.onDown, - this.onLeft, - this.onRight, - }); - - @override - Widget build(BuildContext context) { - return Center( - child: Transform( - transform: Matrix4.identity() - ..setEntry(3, 2, 0.0015) - ..rotateX(-0.9), - alignment: FractionalOffset.center, - child: SizedBox( - height: size, - width: size, - child: Stack( - children: [ - _BuildArrowButton(alignment: Alignment.topCenter, direction: ArrowDirection.up, onPressed: onUp, label: 'X-'), - _BuildArrowButton(alignment: Alignment.bottomCenter, direction: ArrowDirection.down, onPressed: onDown, label: 'X+'), - _BuildArrowButton(alignment: Alignment.centerLeft, direction: ArrowDirection.left, onPressed: onLeft, label: 'Y-'), - _BuildArrowButton(alignment: Alignment.centerRight, direction: ArrowDirection.right, onPressed: onRight, label: 'Y+'), - ], - ), - ), - ), - ); - } -} - -class _BuildArrowButton extends StatelessWidget { - final Alignment alignment; - final ArrowDirection direction; - final String label; - final VoidCallback? onPressed; - - const _BuildArrowButton({ - required this.alignment, - required this.direction, - required this.onPressed, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Align( - alignment: alignment, - child: SizedBox( - width: size / 2.5, - height: size / 2.5, - child: IconButton( - icon: Stack( - alignment: Alignment.center, - children: [ - CustomPaint( - painter: _LinearArrowPainter(direction: direction, color: Colors.black), - child: const SizedBox.expand(), - ), - Text( - label, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - onPressed: onPressed, - ), - ), - ); - } -} From 6e38c07a887a56a17d36ec1c6245f3bf95416aea Mon Sep 17 00:00:00 2001 From: martha-johnston <106617924+martha-johnston@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:03:28 -0400 Subject: [PATCH 03/11] Change label from 'Joint Angles' to 'Joint Positions' --- lib/widgets/resources/arm_widgets/joint_positions_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index 70dcf912683..4acd8c2af5a 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -32,7 +32,7 @@ class _JointPositionsWidgetState extends State { children: [ Divider(), Text( - 'Joint Angles', + 'Joint Positions', style: TextStyle( fontWeight: FontWeight.bold, ), From 3d0d5a95178573c656875dba8950a87aea53f026 Mon Sep 17 00:00:00 2001 From: martha-johnston <106617924+martha-johnston@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:06:47 -0400 Subject: [PATCH 04/11] Implement joint position functionality (#441) --- .../arm_widgets/joint_positions_widget.dart | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index 4acd8c2af5a..352a95c3629 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -11,21 +11,26 @@ class JointPositionsWidget extends StatefulWidget { State createState() => _JointPositionsWidgetState(); } +bool _isLive = false; + class _JointPositionsWidgetState extends State { List _jointValues = []; - bool _isLive = false; @override void initState() { super.initState(); - _getJointInfo(); + _getJointPositions(); } - Future _getJointInfo() async { + Future _getJointPositions() async { _jointValues = await widget.arm.jointPositions(); setState(() {}); } + Future _setJointPositions() async { + await widget.arm.moveToJointPositions(_jointValues); + } + @override Widget build(BuildContext context) { return Column( @@ -78,7 +83,7 @@ class _JointPositionsWidgetState extends State { shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4)))), ), child: OutlinedButton.icon( - onPressed: _isLive ? null : () {}, + onPressed: _isLive ? null : _setJointPositions, label: Text("Execute"), icon: Icon(Icons.play_arrow), ), @@ -139,6 +144,10 @@ class _BuildJointControlRowState extends State<_BuildJointControlRow> { ); } }); + + if (_isLive) { + widget.arm.moveToJointPositions(_jointValues); + } } @override From 7c5540d63c7c599ed6a9357111b0b2c93560b606 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Thu, 30 Oct 2025 14:44:30 -0400 Subject: [PATCH 05/11] use theme for colors --- .../arm_widgets/joint_positions_widget.dart | 54 +++++-------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index 352a95c3629..a2381b561c7 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -61,8 +61,6 @@ class _JointPositionsWidgetState extends State { children: [ Switch( value: _isLive, - activeColor: Colors.green, - inactiveTrackColor: Colors.transparent, onChanged: (newValue) { setState(() { _isLive = newValue; @@ -70,24 +68,14 @@ class _JointPositionsWidgetState extends State { }, ), Text( - "Live", - style: TextStyle(color: Colors.black), + 'Live', ), Spacer(), - OutlinedButtonTheme( - data: OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.black, - iconColor: Colors.black, - overlayColor: Colors.grey, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4)))), - ), - child: OutlinedButton.icon( - onPressed: _isLive ? null : _setJointPositions, - label: Text("Execute"), - icon: Icon(Icons.play_arrow), - ), - ) + OutlinedButton.icon( + onPressed: _isLive ? null : _setJointPositions, + label: Text('Execute'), + icon: Icon(Icons.play_arrow), + ), ], ), ), @@ -164,21 +152,12 @@ class _BuildJointControlRowState extends State<_BuildJointControlRow> { ), ), Expanded( - child: SliderTheme( - data: SliderThemeData( - activeTrackColor: Colors.black, - inactiveTrackColor: Colors.grey, - thumbColor: Colors.black, - overlayColor: Colors.transparent, - showValueIndicator: ShowValueIndicator.never, - ), - child: Slider( - value: _jointValues[widget.index], - min: _minPosition, - max: _maxPosition, - divisions: (_maxPosition - _minPosition).toInt(), - onChanged: (newValue) => _updateJointValue(widget.index, newValue), - ), + child: Slider( + value: _jointValues[widget.index], + min: _minPosition, + max: _maxPosition, + divisions: (_maxPosition - _minPosition).toInt(), + onChanged: (newValue) => _updateJointValue(widget.index, newValue), ), ), SizedBox( @@ -190,15 +169,6 @@ class _BuildJointControlRowState extends State<_BuildJointControlRow> { inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), ], - style: const TextStyle(color: Colors.black), - cursorColor: Colors.black, - decoration: const InputDecoration( - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.black), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 8), - ), onSubmitted: (newValue) { final parsedValue = double.tryParse(newValue) ?? _jointValues[widget.index]; _updateJointValue(widget.index, parsedValue); From a3a68c185418d102071cfa98bfba4cd2672373b7 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Fri, 31 Oct 2025 14:50:22 -0400 Subject: [PATCH 06/11] update joint angle limits --- lib/widgets/resources/arm_widgets/joint_positions_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index a2381b561c7..07cf1a68cd8 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -95,8 +95,8 @@ class _BuildJointControlRow extends StatefulWidget { } class _BuildJointControlRowState extends State<_BuildJointControlRow> { - static const double _minPosition = 0.0; - static const double _maxPosition = 180.0; + static const double _minPosition = -359.0; + static const double _maxPosition = 359.0; List _jointValues = []; List _textControllers = []; From ea583bcfbbb6a4013088edb7a7286fb78fa1d742 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Mon, 3 Nov 2025 23:46:17 +0200 Subject: [PATCH 07/11] update arm values --- lib/widgets/resources/arm_new.dart | 25 +- .../arm_widgets/joint_positions_widget.dart | 164 +++++----- .../resources/arm_widgets/pose_widget.dart | 296 +++++++++++++----- 3 files changed, 325 insertions(+), 160 deletions(-) diff --git a/lib/widgets/resources/arm_new.dart b/lib/widgets/resources/arm_new.dart index d52efe4e5da..4c12adbd37d 100644 --- a/lib/widgets/resources/arm_new.dart +++ b/lib/widgets/resources/arm_new.dart @@ -4,8 +4,14 @@ import '../../viam_sdk.dart'; import 'arm_widgets/joint_positions_widget.dart'; import 'arm_widgets/pose_widget.dart'; +class ArmNotifier extends ChangeNotifier { + void armHasMoved() { + notifyListeners(); + } +} + /// A widget to control an [Arm]. -class ViamArmWidgetNew extends StatelessWidget { +class ViamArmWidgetNew extends StatefulWidget { /// The [Arm] final Arm arm; @@ -14,12 +20,25 @@ class ViamArmWidgetNew extends StatelessWidget { required this.arm, }); + @override + State createState() => _ViamArmWidgetNewState(); +} + +class _ViamArmWidgetNewState extends State { + final ArmNotifier _armNotifier = ArmNotifier(); + + @override + void dispose() { + _armNotifier.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( children: [ - JointPositionsWidget(arm: arm), - PoseWidget(arm: arm), + JointPositionsWidget(arm: widget.arm, updateNotifier: _armNotifier), + PoseWidget(arm: widget.arm, updateNotifier: _armNotifier), ], ); } diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index 07cf1a68cd8..3bb3de76698 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -1,34 +1,82 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import '../../../viam_sdk.dart' as viam; +import '../arm_new.dart'; + class JointPositionsWidget extends StatefulWidget { final viam.Arm arm; - const JointPositionsWidget({super.key, required this.arm}); + final ArmNotifier updateNotifier; + const JointPositionsWidget({ + super.key, + required this.arm, + required this.updateNotifier, + }); @override State createState() => _JointPositionsWidgetState(); } -bool _isLive = false; - class _JointPositionsWidgetState extends State { List _jointValues = []; + bool _isLive = false; + List _textControllers = []; @override void initState() { super.initState(); + widget.updateNotifier.addListener(_getJointPositions); _getJointPositions(); } + @override + void dispose() { + widget.updateNotifier.removeListener(_getJointPositions); + for (final controller in _textControllers) { + controller.dispose(); + } + super.dispose(); + } + Future _getJointPositions() async { + for (final controller in _textControllers) { + controller.dispose(); + } + _jointValues = await widget.arm.jointPositions(); - setState(() {}); + _textControllers = List.generate( + _jointValues.length, + (index) => TextEditingController(text: _jointValues[index].toStringAsFixed(1)), + ); + if (mounted) { + setState(() {}); + } + } + + void _updateJointValue(int index, double value) { + const double minPosition = -359.0; + const double maxPosition = 359.0; + final clampedValue = value.clamp(minPosition, maxPosition); + + setState(() { + _jointValues[index] = clampedValue; + final formattedValue = clampedValue.toStringAsFixed(1); + if (_textControllers[index].text != formattedValue) { + _textControllers[index].text = formattedValue; + _textControllers[index].selection = TextSelection.fromPosition( + TextPosition(offset: _textControllers[index].text.length), + ); + } + }); + + if (_isLive) { + _setJointPositions(); + } } Future _setJointPositions() async { await widget.arm.moveToJointPositions(_jointValues); + widget.updateNotifier.armHasMoved(); } @override @@ -50,7 +98,18 @@ class _JointPositionsWidgetState extends State { children: _jointValues.isEmpty ? [CircularProgressIndicator.adaptive()] : List.generate(_jointValues.length, (index) { - return _BuildJointControlRow(index: index, arm: widget.arm, startJointValues: _jointValues); + return _BuildJointControlRow( + index: index, + value: _jointValues[index], + controller: _textControllers[index], + onSliderChanged: (newValue) => _updateJointValue(index, newValue), + onSubmitted: (newValue) { + final parsedValue = double.tryParse(newValue) ?? _jointValues[index]; + _updateJointValue(index, parsedValue); + }, + onDecrement: () => _updateJointValue(index, _jointValues[index] - 1.0), + onIncrement: () => _updateJointValue(index, _jointValues[index] + 1.0), + ); }), ), ), @@ -84,59 +143,27 @@ class _JointPositionsWidgetState extends State { } } -class _BuildJointControlRow extends StatefulWidget { - final int index; - final viam.Arm arm; - final List startJointValues; - const _BuildJointControlRow({required this.index, required this.arm, required this.startJointValues}); - - @override - State<_BuildJointControlRow> createState() => _BuildJointControlRowState(); -} - -class _BuildJointControlRowState extends State<_BuildJointControlRow> { +class _BuildJointControlRow extends StatelessWidget { static const double _minPosition = -359.0; static const double _maxPosition = 359.0; - List _jointValues = []; - List _textControllers = []; - - @override - void initState() { - _jointValues = widget.startJointValues; - _textControllers = List.generate( - _jointValues.length, - (index) => TextEditingController(text: _jointValues[index].toStringAsFixed(1)), - ); - super.initState(); - } - - @override - void dispose() { - for (final controller in _textControllers) { - controller.dispose(); - } - super.dispose(); - } - - void _updateJointValue(int index, double value) { - final clampedValue = value.clamp(_minPosition, _maxPosition); - - setState(() { - _jointValues[index] = clampedValue; - final formattedValue = clampedValue.toStringAsFixed(1); - if (_textControllers[index].text != formattedValue) { - _textControllers[index].text = formattedValue; - _textControllers[index].selection = TextSelection.fromPosition( - TextPosition(offset: _textControllers[index].text.length), - ); - } - }); - - if (_isLive) { - widget.arm.moveToJointPositions(_jointValues); - } - } + final int index; + final double value; + final TextEditingController controller; + final ValueChanged onSliderChanged; + final ValueChanged onSubmitted; + final VoidCallback onIncrement; + final VoidCallback onDecrement; + + const _BuildJointControlRow({ + required this.index, + required this.value, + required this.controller, + required this.onSliderChanged, + required this.onSubmitted, + required this.onIncrement, + required this.onDecrement, + }); @override Widget build(BuildContext context) { @@ -147,46 +174,39 @@ class _BuildJointControlRowState extends State<_BuildJointControlRow> { SizedBox( width: 30, child: Text( - 'J${widget.index + 1}', + 'J${index + 1}', style: Theme.of(context).textTheme.titleMedium, ), ), Expanded( child: Slider( - value: _jointValues[widget.index], + value: value, min: _minPosition, max: _maxPosition, - divisions: (_maxPosition - _minPosition).toInt(), - onChanged: (newValue) => _updateJointValue(widget.index, newValue), + divisions: (_maxPosition - _minPosition).round(), + onChanged: onSliderChanged, ), ), SizedBox( width: 70, child: TextField( - controller: _textControllers[widget.index], + controller: controller, textAlign: TextAlign.center, - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), ], - onSubmitted: (newValue) { - final parsedValue = double.tryParse(newValue) ?? _jointValues[widget.index]; - _updateJointValue(widget.index, parsedValue); - }, + onSubmitted: onSubmitted, ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.remove), - onPressed: () { - _updateJointValue(widget.index, _jointValues[widget.index] - 1.0); - }, + onPressed: onDecrement, ), IconButton( icon: const Icon(Icons.add), - onPressed: () { - _updateJointValue(widget.index, _jointValues[widget.index] + 1.0); - }, + onPressed: onIncrement, ), ], ), diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart index 4f16925381c..e1fbbeda91a 100644 --- a/lib/widgets/resources/arm_widgets/pose_widget.dart +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -1,11 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:grpc/grpc.dart'; +import '../../../protos/common/common.dart'; import '../../../viam_sdk.dart' as viam; +import '../arm_new.dart'; + +class _TextControlStruct { + TextEditingController x; + TextEditingController y; + TextEditingController z; + TextEditingController oX; + TextEditingController oY; + TextEditingController oZ; + TextEditingController theta; + + _TextControlStruct(this.x, this.y, this.z, this.oX, this.oY, this.oZ, this.theta); +} class PoseWidget extends StatefulWidget { final viam.Arm arm; - const PoseWidget({super.key, required this.arm}); + final ArmNotifier updateNotifier; + const PoseWidget({ + super.key, + required this.arm, + required this.updateNotifier, + }); @override State createState() => _PoseWidgetState(); @@ -14,49 +34,104 @@ class PoseWidget extends StatefulWidget { class _PoseWidgetState extends State { static const double _minOrientation = -1.0; static const double _maxOrientation = 1.0; - static const double _minTheta = -180.0; - static const double _maxTheta = 180.0; - static const double _minPosition = 0.0; + static const double _minTheta = -359.0; + static const double _maxTheta = 359.0; + static const double _minPosition = -1000; static const double _maxPosition = 1000.0; bool _isLive = false; - List _controlValues = []; + bool _isGoingToPose = false; + Pose _controlValues = Pose(); - late final List _textControllers; + _TextControlStruct? _textControllers; @override void initState() { super.initState(); + widget.updateNotifier.addListener(_getStartPose); _getStartPose(); } + @override + void dispose() { + widget.updateNotifier.removeListener(_getStartPose); + _disposeControllers(); + super.dispose(); + } + + void _disposeControllers() { + if (_textControllers != null) { + _textControllers!.x.dispose(); + _textControllers!.y.dispose(); + _textControllers!.z.dispose(); + _textControllers!.oX.dispose(); + _textControllers!.oY.dispose(); + _textControllers!.oZ.dispose(); + _textControllers!.theta.dispose(); + } + } + Future _getStartPose() async { + _disposeControllers(); + final startPose = await widget.arm.endPosition(); - _controlValues = [startPose.x, startPose.y, startPose.z, startPose.oX, startPose.oY, startPose.oZ, startPose.theta]; - _textControllers = List.generate( - _controlValues.length, - (index) => TextEditingController(text: _controlValues[index].toStringAsFixed(1)), + _controlValues = startPose; + _textControllers = _TextControlStruct( + TextEditingController(text: _controlValues.x.toStringAsFixed(1)), + TextEditingController(text: _controlValues.y.toStringAsFixed(1)), + TextEditingController(text: _controlValues.z.toStringAsFixed(1)), + TextEditingController(text: _controlValues.oX.toStringAsFixed(1)), + TextEditingController(text: _controlValues.oY.toStringAsFixed(1)), + TextEditingController(text: _controlValues.oZ.toStringAsFixed(1)), + TextEditingController(text: _controlValues.theta.toStringAsFixed(1)), ); setState(() {}); } - @override - void dispose() { - for (final controller in _textControllers) { - controller.dispose(); + Future _updatePose() async { + try { + if (!_isGoingToPose) { + setState(() { + _isGoingToPose = true; + }); + await widget.arm.moveToPosition(_controlValues); + widget.updateNotifier.armHasMoved(); + } + } on GrpcError catch (e) { + debugPrint('An error occurred updating pose: $e'); + } finally { + if (mounted) { + setState(() { + _isGoingToPose = false; + }); + } } - super.dispose(); } - void _updateControlValue(int index, double value) { + void _updateControlValue(String index, TextEditingController textController, double value) { setState(() { - _controlValues[index] = value; + switch (index) { + case 'x': + _controlValues.x = value; + case 'y': + _controlValues.y = value; + case 'z': + _controlValues.z = value; + case 'oX': + _controlValues.oX = value; + case 'oY': + _controlValues.oY = value; + case 'oZ': + _controlValues.oZ = value; + case 'theta': + _controlValues.theta = value; + } final formattedValue = value.toStringAsFixed(1); - if (_textControllers[index].text != formattedValue) { - _textControllers[index].text = formattedValue; - _textControllers[index].selection = TextSelection.fromPosition( - TextPosition(offset: _textControllers[index].text.length), + if (textController.text != formattedValue) { + textController.text = formattedValue; + textController.selection = TextSelection.fromPosition( + TextPosition(offset: textController.text.length), ); } }); @@ -64,110 +139,151 @@ class _PoseWidgetState extends State { @override Widget build(BuildContext context) { - if (_controlValues.length != 7) _controlValues = []; - return Column( - children: [ - Divider(), - Text( - 'Pose Values', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Divider(), - Padding( - padding: const EdgeInsets.all(16.0), - child: _controlValues.isEmpty - ? CircularProgressIndicator.adaptive() - : Column( + return _textControllers == null + ? Center(child: CircularProgressIndicator.adaptive()) + : Column( + children: [ + Divider(), + Text( + 'Pose Values', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + Divider(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( mainAxisSize: MainAxisSize.min, children: [ _BuildJointControlRow( label: 'X', - value: _controlValues[0], - controller: _textControllers[0], + value: _controlValues.x.roundToDouble(), + controller: _textControllers!.x, min: _minPosition, max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue(0, newValue.clamp(_minPosition, _maxPosition)), + onValueChanged: (newValue) => _updateControlValue( + 'x', + _textControllers!.x, + newValue.clamp(_minPosition, _maxPosition), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), _BuildJointControlRow( label: 'Y', - value: _controlValues[1], - controller: _textControllers[1], + value: _controlValues.y.roundToDouble(), + controller: _textControllers!.y, min: _minPosition, max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue(1, newValue.clamp(_minPosition, _maxPosition)), + onValueChanged: (newValue) => _updateControlValue( + 'y', + _textControllers!.y, + newValue.clamp(_minPosition, _maxPosition), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), _BuildJointControlRow( label: 'Z', - value: _controlValues[2], - controller: _textControllers[2], + value: _controlValues.z.roundToDouble(), + controller: _textControllers!.z, min: _minPosition, max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue(2, newValue.clamp(_minPosition, _maxPosition)), + onValueChanged: (newValue) => _updateControlValue( + 'z', + _textControllers!.z, + newValue.clamp(_minPosition, _maxPosition), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), _BuildJointControlRow( label: 'OX', - value: _controlValues[3], - controller: _textControllers[3], + value: _controlValues.oX.roundToDouble(), + controller: _textControllers!.oX, min: _minOrientation, max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue(3, newValue.clamp(_minOrientation, _maxOrientation)), + onValueChanged: (newValue) => _updateControlValue( + 'oX', + _textControllers!.oX, + newValue.clamp(_minOrientation, _maxOrientation), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), _BuildJointControlRow( label: 'OY', - value: _controlValues[4], - controller: _textControllers[4], + value: _controlValues.oY.roundToDouble(), + controller: _textControllers!.oY, min: _minOrientation, max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue(4, newValue.clamp(_minOrientation, _maxOrientation)), + onValueChanged: (newValue) => _updateControlValue( + 'oY', + _textControllers!.oY, + newValue.clamp(_minOrientation, _maxOrientation), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), _BuildJointControlRow( label: 'OZ', - value: _controlValues[5], - controller: _textControllers[5], + value: _controlValues.oZ.roundToDouble(), + controller: _textControllers!.oZ, min: _minOrientation, max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue(5, newValue.clamp(_minOrientation, _maxOrientation)), + onValueChanged: (newValue) => _updateControlValue( + 'oZ', + _textControllers!.oZ, + newValue.clamp(_minOrientation, _maxOrientation), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), _BuildJointControlRow( label: 'Theta', - value: _controlValues[6], - controller: _textControllers[6], + value: _controlValues.theta.roundToDouble(), + controller: _textControllers!.theta, min: _minTheta, max: _maxTheta, - onValueChanged: (newValue) => _updateControlValue(6, newValue.clamp(_minTheta, _maxTheta)), + onValueChanged: (newValue) => _updateControlValue( + 'theta', + _textControllers!.theta, + newValue.clamp(_minTheta, _maxTheta), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), ], ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0, 20.0, 20.0), - child: Row( - spacing: 8, - children: [ - Switch( - value: _isLive, - onChanged: (newValue) { - setState(() { - _isLive = newValue; - }); - }, - ), - Text( - "Live", ), - Spacer(), - OutlinedButton.icon( - onPressed: _isLive ? null : () {}, - label: Text("Execute"), - icon: Icon(Icons.play_arrow), + Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0, 20.0, 20.0), + child: Row( + spacing: 8, + children: [ + Switch( + value: _isLive, + onChanged: (newValue) { + setState(() { + _isLive = newValue; + }); + }, + ), + Text( + "Live", + ), + Tooltip( + message: "In Live mode, pose will update \non release of the slider", + textAlign: TextAlign.center, + triggerMode: TooltipTriggerMode.tap, + preferBelow: false, + child: Icon(Icons.info_outline), + ), + Spacer(), + OutlinedButton.icon( + onPressed: _isLive ? null : _updatePose, + label: Text("Execute"), + icon: Icon(Icons.play_arrow), + ), + ], + ), ), ], - ), - ), - ], - ); + ); } } @@ -178,6 +294,7 @@ class _BuildJointControlRow extends StatelessWidget { final double min; final double max; final ValueChanged onValueChanged; + final ValueChanged onValueChangedEnd; const _BuildJointControlRow({ required this.label, @@ -186,6 +303,7 @@ class _BuildJointControlRow extends StatelessWidget { required this.min, required this.max, required this.onValueChanged, + required this.onValueChangedEnd, }); @override @@ -208,6 +326,7 @@ class _BuildJointControlRow extends StatelessWidget { max: max, label: value.toStringAsFixed(1), onChanged: onValueChanged, + onChangeEnd: onValueChangedEnd, ), ), const SizedBox(width: 16), @@ -223,17 +342,24 @@ class _BuildJointControlRow extends StatelessWidget { onSubmitted: (newValue) { final parsedValue = double.tryParse(newValue) ?? value; onValueChanged(parsedValue); + onValueChangedEnd(parsedValue); }, ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.remove), - onPressed: () => onValueChanged(value - 0.1), + onPressed: () async { + onValueChanged(value - (max == 1 ? 0.1 : 1.0)); + onValueChangedEnd(value); + }, ), IconButton( icon: const Icon(Icons.add), - onPressed: () => onValueChanged(value + 0.1), + onPressed: () async { + onValueChanged(value + (max == 1 ? 0.1 : 1.0)); + onValueChangedEnd(value); + }, ), ], ), From 4a082e286204ab69566b66694a77d9afeac54f44 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Fri, 7 Nov 2025 16:57:50 +0200 Subject: [PATCH 08/11] update branch --- .../pubspec.lock | 26 +++++++++---------- .../resources/arm_widgets/pose_widget.dart | 18 +++---------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/example/hotspot_provisioning_example_app/pubspec.lock b/example/hotspot_provisioning_example_app/pubspec.lock index b7b08ba0422..3a9ae3bc49f 100644 --- a/example/hotspot_provisioning_example_app/pubspec.lock +++ b/example/hotspot_provisioning_example_app/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -300,26 +300,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -633,10 +633,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.4" typed_data: dependency: transitive description: @@ -657,10 +657,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" viam_flutter_hotspot_provisioning_widget: dependency: "direct main" description: @@ -717,5 +717,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.27.0" diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart index 116c5d311b5..a03204b800b 100644 --- a/lib/widgets/resources/arm_widgets/pose_widget.dart +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -19,18 +19,6 @@ class _TextControlStruct { _TextControlStruct(this.x, this.y, this.z, this.oX, this.oY, this.oZ, this.theta); } -class _TextControlStruct { - TextEditingController x; - TextEditingController y; - TextEditingController z; - TextEditingController oX; - TextEditingController oY; - TextEditingController oZ; - TextEditingController theta; - - _TextControlStruct(this.x, this.y, this.z, this.oX, this.oY, this.oZ, this.theta); -} - class PoseWidget extends StatefulWidget { final viam.Arm arm; final ArmNotifier updateNotifier; @@ -277,10 +265,10 @@ class _PoseWidgetState extends State { }, ), Text( - "Live", + 'Live', ), Tooltip( - message: "In Live mode, pose will update \non release of the slider", + message: 'In Live mode, pose will update \non release of the slider', textAlign: TextAlign.center, triggerMode: TooltipTriggerMode.tap, preferBelow: false, @@ -289,7 +277,7 @@ class _PoseWidgetState extends State { Spacer(), OutlinedButton.icon( onPressed: _isLive ? null : _updatePose, - label: Text("Execute"), + label: Text('Execute'), icon: Icon(Icons.play_arrow), ), ], From 7a432db3adcc76a6cc9622cfb3ee74f76ebd21d9 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Fri, 7 Nov 2025 18:31:52 +0200 Subject: [PATCH 09/11] switch arm_new to arm --- lib/widgets/resources/arm.dart | 162 ++---------------- lib/widgets/resources/arm_new.dart | 45 ----- .../arm_widgets/joint_positions_widget.dart | 4 +- .../resources/arm_widgets/pose_widget.dart | 2 +- 4 files changed, 21 insertions(+), 192 deletions(-) delete mode 100644 lib/widgets/resources/arm_new.dart diff --git a/lib/widgets/resources/arm.dart b/lib/widgets/resources/arm.dart index d9de9c26d6a..4c12adbd37d 100644 --- a/lib/widgets/resources/arm.dart +++ b/lib/widgets/resources/arm.dart @@ -1,171 +1,45 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import '../../viam_sdk.dart'; -import '../../widgets.dart'; +import 'arm_widgets/joint_positions_widget.dart'; +import 'arm_widgets/pose_widget.dart'; + +class ArmNotifier extends ChangeNotifier { + void armHasMoved() { + notifyListeners(); + } +} /// A widget to control an [Arm]. -class ViamArmWidget extends StatefulWidget { +class ViamArmWidgetNew extends StatefulWidget { /// The [Arm] final Arm arm; - const ViamArmWidget({ + const ViamArmWidgetNew({ super.key, required this.arm, }); @override - State createState() => _ViamArmWidgetState(); -} - -enum _PoseField { - x, - y, - z, - theta, - oX, - oY, - oZ; - - String get title { - switch (this) { - case x: - return 'X'; - case y: - return 'Y'; - case z: - return 'Z'; - case theta: - return 'Theta'; - case oX: - return 'OX'; - case oY: - return 'OY'; - case oZ: - return 'OZ'; - } - } + State createState() => _ViamArmWidgetNewState(); } -class _ViamArmWidgetState extends State { - Pose endPosition = Pose(); - List jointPositions = []; - - Future _getPositions() async { - final ep = await widget.arm.endPosition(); - final jp = await widget.arm.jointPositions(); - setState(() { - jointPositions = jp; - endPosition = ep; - }); - } +class _ViamArmWidgetNewState extends State { + final ArmNotifier _armNotifier = ArmNotifier(); @override - void initState() { - super.initState(); - _getPositions(); - } - - Future updateEndPosition(_PoseField field, double increment) async { - final ep = endPosition; - switch (field) { - case _PoseField.x: - ep.x += increment; - case _PoseField.y: - ep.y += increment; - case _PoseField.z: - ep.z += increment; - case _PoseField.theta: - ep.theta += increment; - case _PoseField.oX: - ep.oX += increment; - case _PoseField.oY: - ep.oY += increment; - case _PoseField.oZ: - ep.oZ += increment; - } - - await widget.arm.moveToPosition(ep); - await _getPositions(); - } - - Future updateJointPosition(int joint, double increment) async { - final jp = jointPositions; - jp[joint] += increment; - await widget.arm.moveToJointPositions(jp); - await _getPositions(); - } - - TableRow _getEndPositionRow(_PoseField field) { - double value; - switch (field) { - case _PoseField.x: - value = endPosition.x; - case _PoseField.y: - value = endPosition.y; - case _PoseField.z: - value = endPosition.z; - case _PoseField.theta: - value = endPosition.theta; - case _PoseField.oX: - value = endPosition.oX; - case _PoseField.oY: - value = endPosition.oY; - case _PoseField.oZ: - value = endPosition.oZ; - } - - return TableRow(children: [ - _ArmTableCell(Text(field.title, textAlign: TextAlign.end)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, -10), text: '--', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, -1), text: '-', size: ViamButtonSizeClass.small)), - _ArmTableCell(Text(value.toStringAsFixed(2), textAlign: TextAlign.center)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, 1), text: '+', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, 10), text: '++', size: ViamButtonSizeClass.small)), - ]); + void dispose() { + _armNotifier.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text('End Positions (mm)', style: TextStyle(fontWeight: FontWeight.bold)), - Table( - columnWidths: const {0: IntrinsicColumnWidth()}, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: _PoseField.values.map((e) => _getEndPositionRow(e)).toList(), - ), - const SizedBox(height: 16), - const Text('Joints (degrees)', style: TextStyle(fontWeight: FontWeight.bold)), - Table( - columnWidths: const {0: IntrinsicColumnWidth()}, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: jointPositions - .mapIndexed((index, element) => TableRow(children: [ - _ArmTableCell(Text('Joint $index', textAlign: TextAlign.end)), - _ArmTableCell( - ViamButton(onPressed: () => updateJointPosition(index, -10), text: '--', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateJointPosition(index, -1), text: '-', size: ViamButtonSizeClass.small)), - _ArmTableCell(Text(element.toStringAsFixed(2), textAlign: TextAlign.center)), - _ArmTableCell(ViamButton(onPressed: () => updateJointPosition(index, 1), text: '+', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateJointPosition(index, 10), text: '++', size: ViamButtonSizeClass.small)), - ])) - .toList(), - ) + JointPositionsWidget(arm: widget.arm, updateNotifier: _armNotifier), + PoseWidget(arm: widget.arm, updateNotifier: _armNotifier), ], ); } } - -class _ArmTableCell extends StatelessWidget { - final Widget child; - - const _ArmTableCell(this.child); - - @override - Widget build(BuildContext context) { - return TableCell(child: Padding(padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1), child: child)); - } -} diff --git a/lib/widgets/resources/arm_new.dart b/lib/widgets/resources/arm_new.dart deleted file mode 100644 index 4c12adbd37d..00000000000 --- a/lib/widgets/resources/arm_new.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../viam_sdk.dart'; -import 'arm_widgets/joint_positions_widget.dart'; -import 'arm_widgets/pose_widget.dart'; - -class ArmNotifier extends ChangeNotifier { - void armHasMoved() { - notifyListeners(); - } -} - -/// A widget to control an [Arm]. -class ViamArmWidgetNew extends StatefulWidget { - /// The [Arm] - final Arm arm; - - const ViamArmWidgetNew({ - super.key, - required this.arm, - }); - - @override - State createState() => _ViamArmWidgetNewState(); -} - -class _ViamArmWidgetNewState extends State { - final ArmNotifier _armNotifier = ArmNotifier(); - - @override - void dispose() { - _armNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - JointPositionsWidget(arm: widget.arm, updateNotifier: _armNotifier), - PoseWidget(arm: widget.arm, updateNotifier: _armNotifier), - ], - ); - } -} diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index 3bb3de76698..a2d61cec68b 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../../../viam_sdk.dart' as viam; -import '../arm_new.dart'; +import '../../../viam_sdk.dart' as viam; +import '../arm.dart'; class JointPositionsWidget extends StatefulWidget { final viam.Arm arm; diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart index a03204b800b..4c14878710b 100644 --- a/lib/widgets/resources/arm_widgets/pose_widget.dart +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -5,7 +5,7 @@ import 'package:grpc/grpc.dart'; import '../../../protos/common/common.dart'; import '../../../src/utils.dart'; import '../../../viam_sdk.dart' as viam; -import '../arm_new.dart'; +import '../arm.dart'; class _TextControlStruct { TextEditingController x; From 9d42291637c817301b86ec545c4a54a9c4b674c4 Mon Sep 17 00:00:00 2001 From: martha-johnston <106617924+martha-johnston@users.noreply.github.com> Date: Fri, 7 Nov 2025 22:32:48 +0200 Subject: [PATCH 10/11] extend sliders (#449) --- .../arm_widgets/joint_positions_widget.dart | 81 ++++++++------ .../resources/arm_widgets/pose_widget.dart | 104 ++++++++++-------- 2 files changed, 101 insertions(+), 84 deletions(-) diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index a2d61cec68b..1f048d1b935 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -169,44 +169,53 @@ class _BuildJointControlRow extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( + child: Column( children: [ - SizedBox( - width: 30, - child: Text( - 'J${index + 1}', - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Expanded( - child: Slider( - value: value, - min: _minPosition, - max: _maxPosition, - divisions: (_maxPosition - _minPosition).round(), - onChanged: onSliderChanged, - ), - ), - SizedBox( - width: 70, - child: TextField( - controller: controller, - textAlign: TextAlign.center, - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), - ], - onSubmitted: onSubmitted, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.remove), - onPressed: onDecrement, + Row( + children: [ + SizedBox( + width: 35, + child: Text( + 'J${index + 1}', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + SizedBox( + width: 70, + child: TextField( + controller: controller, + textAlign: TextAlign.center, + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), + ], + onSubmitted: onSubmitted, + ), + ), + Spacer(), + IconButton( + icon: const Icon(Icons.remove), + onPressed: onDecrement, + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: onIncrement, + ), + ], ), - IconButton( - icon: const Icon(Icons.add), - onPressed: onIncrement, + Row( + children: [ + SizedBox(width: 15), + Expanded( + child: Slider( + value: value, + min: _minPosition, + max: _maxPosition, + divisions: (_maxPosition - _minPosition).round(), + onChanged: onSliderChanged, + ), + ), + ], ), ], ), diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart index 4c14878710b..a164255eb77 100644 --- a/lib/widgets/resources/arm_widgets/pose_widget.dart +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -311,56 +311,64 @@ class _BuildJointControlRow extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( + child: Column( children: [ - SizedBox( - width: 50, - child: Text( - label, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Expanded( - child: Slider( - value: value, - min: min, - max: max, - label: value.toStringAsFixed(1), - onChanged: onValueChanged, - onChangeEnd: onValueChangedEnd, - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 70, - child: TextField( - controller: controller, - textAlign: TextAlign.center, - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^-?\d+\.?\d{0,1}')), - ], - onSubmitted: (newValue) { - final parsedValue = double.tryParse(newValue) ?? value; - onValueChanged(parsedValue); - onValueChangedEnd(parsedValue); - }, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () async { - onValueChanged(value - (max == 1 ? 0.1 : 1.0)); - onValueChangedEnd(value); - }, + Row( + children: [ + SizedBox( + width: 55, + child: Text( + label, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + SizedBox( + width: 70, + child: TextField( + controller: controller, + textAlign: TextAlign.center, + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^-?\d+\.?\d{0,1}')), + ], + onSubmitted: (newValue) { + final parsedValue = double.tryParse(newValue) ?? value; + onValueChanged(parsedValue); + onValueChangedEnd(parsedValue); + }, + ), + ), + Spacer(), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () async { + onValueChanged(value - (max == 1 ? 0.1 : 1.0)); + onValueChangedEnd(value); + }, + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + onValueChanged(value + (max == 1 ? 0.1 : 1.0)); + onValueChangedEnd(value); + }, + ), + ], ), - IconButton( - icon: const Icon(Icons.add), - onPressed: () async { - onValueChanged(value + (max == 1 ? 0.1 : 1.0)); - onValueChangedEnd(value); - }, + Row( + children: [ + SizedBox(width: 35), + Expanded( + child: Slider( + value: value, + min: min, + max: max, + label: value.toStringAsFixed(1), + onChanged: onValueChanged, + onChangeEnd: onValueChangedEnd, + ), + ), + ], ), ], ), From 8bf470a1977d1b8d2a5acd925e203ef97245ead3 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Fri, 7 Nov 2025 23:17:13 +0200 Subject: [PATCH 11/11] pr comments --- .../resources/arm_widgets/pose_widget.dart | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart index a164255eb77..601c49750ce 100644 --- a/lib/widgets/resources/arm_widgets/pose_widget.dart +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -91,13 +91,12 @@ class _PoseWidgetState extends State { Future _updatePose() async { try { - if (!_isGoingToPose) { - setState(() { - _isGoingToPose = true; - }); - await widget.arm.moveToPosition(_controlValues); - widget.updateNotifier.armHasMoved(); - } + if (_isGoingToPose) return; + setState(() { + _isGoingToPose = true; + }); + await widget.arm.moveToPosition(_controlValues); + widget.updateNotifier.armHasMoved(); } on GrpcError catch (e) { if (mounted) await showErrorDialog(context, title: 'An error occurred', error: e.message); } finally { @@ -109,9 +108,9 @@ class _PoseWidgetState extends State { } } - void _updateControlValue(String index, TextEditingController textController, double value) { + void _updateControlValue(String axis, TextEditingController textController, double value) { setState(() { - switch (index) { + switch (axis) { case 'x': _controlValues.x = value; case 'y':