diff --git a/lib/widgets/resources/arm_new.dart b/lib/widgets/resources/arm_new.dart index ce9eb653243..5a9fd85328c 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/pose_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: () {}, - ), - ], - ), + PoseWidget(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, - ), - ), - ); - } -} diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart new file mode 100644 index 00000000000..4f16925381c --- /dev/null +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../viam_sdk.dart' as viam; + +class PoseWidget extends StatefulWidget { + final viam.Arm arm; + const PoseWidget({super.key, required this.arm}); + + @override + State createState() => _PoseWidgetState(); +} + +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 _maxPosition = 1000.0; + + bool _isLive = false; + List _controlValues = []; + + late final List _textControllers; + + @override + void initState() { + super.initState(); + _getStartPose(); + } + + Future _getStartPose() async { + 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)), + ); + setState(() {}); + } + + @override + void dispose() { + for (final controller in _textControllers) { + controller.dispose(); + } + super.dispose(); + } + + void _updateControlValue(int index, double value) { + setState(() { + _controlValues[index] = 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), + ); + } + }); + } + + @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( + mainAxisSize: MainAxisSize.min, + children: [ + _BuildJointControlRow( + label: 'X', + value: _controlValues[0], + controller: _textControllers[0], + min: _minPosition, + max: _maxPosition, + onValueChanged: (newValue) => _updateControlValue(0, newValue.clamp(_minPosition, _maxPosition)), + ), + _BuildJointControlRow( + label: 'Y', + value: _controlValues[1], + controller: _textControllers[1], + min: _minPosition, + max: _maxPosition, + onValueChanged: (newValue) => _updateControlValue(1, newValue.clamp(_minPosition, _maxPosition)), + ), + _BuildJointControlRow( + label: 'Z', + value: _controlValues[2], + controller: _textControllers[2], + min: _minPosition, + max: _maxPosition, + onValueChanged: (newValue) => _updateControlValue(2, newValue.clamp(_minPosition, _maxPosition)), + ), + _BuildJointControlRow( + label: 'OX', + value: _controlValues[3], + controller: _textControllers[3], + min: _minOrientation, + max: _maxOrientation, + onValueChanged: (newValue) => _updateControlValue(3, newValue.clamp(_minOrientation, _maxOrientation)), + ), + _BuildJointControlRow( + label: 'OY', + value: _controlValues[4], + controller: _textControllers[4], + min: _minOrientation, + max: _maxOrientation, + onValueChanged: (newValue) => _updateControlValue(4, newValue.clamp(_minOrientation, _maxOrientation)), + ), + _BuildJointControlRow( + label: 'OZ', + value: _controlValues[5], + controller: _textControllers[5], + min: _minOrientation, + max: _maxOrientation, + onValueChanged: (newValue) => _updateControlValue(5, newValue.clamp(_minOrientation, _maxOrientation)), + ), + _BuildJointControlRow( + label: 'Theta', + value: _controlValues[6], + controller: _textControllers[6], + min: _minTheta, + max: _maxTheta, + onValueChanged: (newValue) => _updateControlValue(6, newValue.clamp(_minTheta, _maxTheta)), + ), + ], + ), + ), + 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), + ), + ], + ), + ), + ], + ); + } +} + +class _BuildJointControlRow extends StatelessWidget { + final String label; + final double value; + final TextEditingController controller; + final double min; + final double max; + final ValueChanged onValueChanged; + + const _BuildJointControlRow({ + required this.label, + required this.value, + required this.controller, + required this.min, + required this.max, + required this.onValueChanged, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + 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, + ), + ), + 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); + }, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () => onValueChanged(value - 0.1), + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: () => onValueChanged(value + 0.1), + ), + ], + ), + ); + } +}