From 3a620ef5b541b449df444b5182feb61c379f248b Mon Sep 17 00:00:00 2001 From: Michael Jansen Date: Mon, 20 Nov 2023 10:32:35 -0800 Subject: [PATCH] Add option to tie global constraints to default constraints (#484) --- lib/pages/home_page.dart | 1 + lib/pages/project/project_page.dart | 17 ++++++++ lib/path/pathplanner_path.dart | 8 +++- .../simulator/trajectory_generator.dart | 4 ++ lib/widgets/editor/split_path_editor.dart | 16 ++++++++ .../tree_widgets/global_constraints_tree.dart | 41 +++++++++++++++++++ .../editor/tree_widgets/path_tree.dart | 4 ++ lib/widgets/number_text_field.dart | 4 +- test/path/pathplanner_path_test.dart | 8 ++++ .../simulator/trajectory_generator_test.dart | 2 + .../global_constraints_tree_test.dart | 40 ++++++++++++++++++ .../editor/tree_widgets/path_tree_test.dart | 10 +++++ 12 files changed, 151 insertions(+), 4 deletions(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 84e365ee..0ca224b6 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -425,6 +425,7 @@ class _HomePageState extends State with TickerProviderStateMixin { } void _onProjectSettingsChanged() { + ProjectPage.settingsUpdated = true; _saveProjectSettingsToFile(_projectDir!); setState(() { diff --git a/lib/pages/project/project_page.dart b/lib/pages/project/project_page.dart index 96964fba..cfd2fae5 100644 --- a/lib/pages/project/project_page.dart +++ b/lib/pages/project/project_page.dart @@ -33,6 +33,9 @@ class ProjectPage extends StatefulWidget { final VoidCallback? onFoldersChanged; final bool simulatePath; + // Stupid workaround to get when settings are updated + static bool settingsUpdated = false; + const ProjectPage({ super.key, required this.prefs, @@ -170,6 +173,20 @@ class _ProjectPageState extends State { ); } + // Stupid workaround but it works + if (ProjectPage.settingsUpdated) { + PathConstraints defaultConstraints = _getDefaultConstraints(); + + for (PathPlannerPath path in _paths) { + if (path.useDefaultConstraints) { + path.globalConstraints = defaultConstraints.clone(); + path.generateAndSavePath(); + } + } + + ProjectPage.settingsUpdated = false; + } + return Stack( children: [ Container( diff --git a/lib/path/pathplanner_path.dart b/lib/path/pathplanner_path.dart index 7f7780d2..b9424ffa 100644 --- a/lib/path/pathplanner_path.dart +++ b/lib/path/pathplanner_path.dart @@ -32,6 +32,7 @@ class PathPlannerPath { bool reversed; PreviewStartingState? previewStartingState; String? folder; + bool useDefaultConstraints; FileSystem fs; String pathDir; @@ -60,7 +61,8 @@ class PathPlannerPath { rotationTargets = [], eventMarkers = [], reversed = false, - previewStartingState = null { + previewStartingState = null, + useDefaultConstraints = false { waypoints.addAll([ Waypoint( anchor: const Point(2.0, 7.0), @@ -93,6 +95,7 @@ class PathPlannerPath { required this.reversed, required this.folder, required this.previewStartingState, + required this.useDefaultConstraints, }) : pathPoints = [] { generatePathPoints(); } @@ -127,6 +130,7 @@ class PathPlannerPath { previewStartingState: json['previewStartingState'] == null ? null : PreviewStartingState.fromJson(json['previewStartingState']), + useDefaultConstraints: json['useDefaultConstraints'] ?? false, ); void generateAndSavePath() { @@ -214,6 +218,7 @@ class PathPlannerPath { 'reversed': reversed, 'folder': folder, 'previewStartingState': previewStartingState?.toJson(), + 'useDefaultConstraints': useDefaultConstraints, }; } @@ -461,6 +466,7 @@ class PathPlannerPath { reversed: reversed, folder: folder, previewStartingState: previewStartingState?.clone(), + useDefaultConstraints: useDefaultConstraints, ); } diff --git a/lib/services/simulator/trajectory_generator.dart b/lib/services/simulator/trajectory_generator.dart index 2896db73..35db2fb7 100644 --- a/lib/services/simulator/trajectory_generator.dart +++ b/lib/services/simulator/trajectory_generator.dart @@ -125,6 +125,7 @@ class TrajectoryGenerator { reversed: path.reversed, folder: null, previewStartingState: null, + useDefaultConstraints: path.useDefaultConstraints, ); } else if ((closestPointIdx == 0 && robotNextControl == null) || ((closestDist - @@ -185,6 +186,7 @@ class TrajectoryGenerator { reversed: path.reversed, folder: null, previewStartingState: null, + useDefaultConstraints: path.useDefaultConstraints, ); } @@ -234,6 +236,7 @@ class TrajectoryGenerator { reversed: path.reversed, folder: null, previewStartingState: null, + useDefaultConstraints: path.useDefaultConstraints, ); } @@ -361,6 +364,7 @@ class TrajectoryGenerator { reversed: path.reversed, folder: null, previewStartingState: null, + useDefaultConstraints: path.useDefaultConstraints, ); } diff --git a/lib/widgets/editor/split_path_editor.dart b/lib/widgets/editor/split_path_editor.dart index f34fc42d..f2d03113 100644 --- a/lib/widgets/editor/split_path_editor.dart +++ b/lib/widgets/editor/split_path_editor.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:multi_split_view/multi_split_view.dart'; import 'package:pathplanner/path/constraints_zone.dart'; import 'package:pathplanner/path/event_marker.dart'; +import 'package:pathplanner/path/path_constraints.dart'; import 'package:pathplanner/path/pathplanner_path.dart'; import 'package:pathplanner/path/rotation_target.dart'; import 'package:pathplanner/path/waypoint.dart'; @@ -467,6 +468,7 @@ class _SplitPathEditorState extends State waypointsTreeController: _waypointsTreeController, undoStack: widget.undoStack, holonomicMode: _holonomicMode, + defaultConstraints: _getDefaultConstraints(), onPathChanged: () { setState(() { widget.path.generateAndSavePath(); @@ -686,4 +688,18 @@ class _SplitPathEditorState extends State double _pixelsToMeters(double pixels) { return (pixels / PathPainter.scale) / widget.fieldImage.pixelsPerMeter; } + + PathConstraints _getDefaultConstraints() { + return PathConstraints( + maxVelocity: widget.prefs.getDouble(PrefsKeys.defaultMaxVel) ?? + Defaults.defaultMaxVel, + maxAcceleration: widget.prefs.getDouble(PrefsKeys.defaultMaxAccel) ?? + Defaults.defaultMaxAccel, + maxAngularVelocity: widget.prefs.getDouble(PrefsKeys.defaultMaxAngVel) ?? + Defaults.defaultMaxAngVel, + maxAngularAcceleration: + widget.prefs.getDouble(PrefsKeys.defaultMaxAngAccel) ?? + Defaults.defaultMaxAngAccel, + ); + } } diff --git a/lib/widgets/editor/tree_widgets/global_constraints_tree.dart b/lib/widgets/editor/tree_widgets/global_constraints_tree.dart index 7776589d..d52d0ea6 100644 --- a/lib/widgets/editor/tree_widgets/global_constraints_tree.dart +++ b/lib/widgets/editor/tree_widgets/global_constraints_tree.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:pathplanner/path/path_constraints.dart'; import 'package:pathplanner/path/pathplanner_path.dart'; import 'package:pathplanner/widgets/editor/tree_widgets/tree_card_node.dart'; import 'package:pathplanner/widgets/number_text_field.dart'; @@ -9,6 +10,7 @@ class GlobalConstraintsTree extends StatelessWidget { final VoidCallback? onPathChanged; final ChangeStack undoStack; final bool holonomicMode; + final PathConstraints defaultConstraints; const GlobalConstraintsTree({ super.key, @@ -16,6 +18,7 @@ class GlobalConstraintsTree extends StatelessWidget { this.onPathChanged, required this.undoStack, required this.holonomicMode, + required this.defaultConstraints, }); @override @@ -39,6 +42,7 @@ class GlobalConstraintsTree extends StatelessWidget { initialText: path.globalConstraints.maxVelocity.toStringAsFixed(2), label: 'Max Velocity (M/S)', + enabled: !path.useDefaultConstraints, onSubmitted: (value) { if (value != null && value > 0) { _addChange( @@ -53,6 +57,7 @@ class GlobalConstraintsTree extends StatelessWidget { initialText: path.globalConstraints.maxAcceleration.toStringAsFixed(2), label: 'Max Acceleration (M/S²)', + enabled: !path.useDefaultConstraints, onSubmitted: (value) { if (value != null && value > 0) { _addChange( @@ -76,6 +81,7 @@ class GlobalConstraintsTree extends StatelessWidget { .toStringAsFixed(2), label: 'Max Angular Velocity (Deg/S)', arrowKeyIncrement: 1.0, + enabled: !path.useDefaultConstraints, onSubmitted: (value) { if (value != null && value > 0) { _addChange(() => @@ -91,6 +97,7 @@ class GlobalConstraintsTree extends StatelessWidget { .toStringAsFixed(2), label: 'Max Angular Acceleration (Deg/S²)', arrowKeyIncrement: 1.0, + enabled: !path.useDefaultConstraints, onSubmitted: (value) { if (value != null && value > 0) { _addChange(() => path @@ -102,6 +109,40 @@ class GlobalConstraintsTree extends StatelessWidget { ], ), ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Row( + children: [ + Checkbox( + value: path.useDefaultConstraints, + onChanged: (value) { + undoStack.add(Change( + ( + path.useDefaultConstraints, + path.globalConstraints.clone() + ), + () { + path.useDefaultConstraints = value ?? false; + path.globalConstraints = defaultConstraints.clone(); + onPathChanged?.call(); + }, + (oldValue) { + path.useDefaultConstraints = oldValue.$1; + path.globalConstraints = oldValue.$2.clone(); + onPathChanged?.call(); + }, + )); + }, + ), + const SizedBox(width: 4), + const Text( + 'Use Default Constraints', + style: TextStyle(fontSize: 18), + ), + ], + ), + ), ], ); } diff --git a/lib/widgets/editor/tree_widgets/path_tree.dart b/lib/widgets/editor/tree_widgets/path_tree.dart index 9c7c31b1..5f630712 100644 --- a/lib/widgets/editor/tree_widgets/path_tree.dart +++ b/lib/widgets/editor/tree_widgets/path_tree.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:pathplanner/path/path_constraints.dart'; import 'package:pathplanner/path/pathplanner_path.dart'; import 'package:pathplanner/widgets/editor/tree_widgets/constraint_zones_tree.dart'; import 'package:pathplanner/widgets/editor/tree_widgets/editor_settings_tree.dart'; @@ -32,6 +33,7 @@ class PathTree extends StatefulWidget { final ChangeStack undoStack; final num? pathRuntime; final bool holonomicMode; + final PathConstraints defaultConstraints; const PathTree({ super.key, @@ -56,6 +58,7 @@ class PathTree extends StatefulWidget { this.pathRuntime, this.onPathChangedNoSim, required this.holonomicMode, + required this.defaultConstraints, }); @override @@ -109,6 +112,7 @@ class _PathTreeState extends State { onPathChanged: widget.onPathChanged, undoStack: widget.undoStack, holonomicMode: widget.holonomicMode, + defaultConstraints: widget.defaultConstraints, ), GoalEndStateTree( path: widget.path, diff --git a/lib/widgets/number_text_field.dart b/lib/widgets/number_text_field.dart index 1c033e82..a92d2555 100644 --- a/lib/widgets/number_text_field.dart +++ b/lib/widgets/number_text_field.dart @@ -28,8 +28,6 @@ class NumberTextField extends StatelessWidget { @override Widget build(BuildContext context) { - ColorScheme colorScheme = Theme.of(context).colorScheme; - return SizedBox( height: height, child: CallbackShortcuts( @@ -55,7 +53,7 @@ class NumberTextField extends StatelessWidget { FilteringTextInputFormatter.allow( RegExp(r'(^(-?)\d*\.?\d*)([+/\*\-](-?)\d*\.?\d*)*')), ], - style: TextStyle(fontSize: 14, color: colorScheme.onSurface), + style: const TextStyle(fontSize: 14), decoration: InputDecoration( contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4), labelText: label, diff --git a/test/path/pathplanner_path_test.dart b/test/path/pathplanner_path_test.dart index 02d71cb7..60256783 100644 --- a/test/path/pathplanner_path_test.dart +++ b/test/path/pathplanner_path_test.dart @@ -57,6 +57,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); expect(path.name, 'test'); @@ -94,6 +95,7 @@ void main() { reversed: false, folder: null, previewStartingState: PreviewStartingState(rotation: 10, velocity: 1), + useDefaultConstraints: false, ); Map json = path.toJson(); @@ -128,6 +130,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); PathPlannerPath cloned = path.duplicate('test'); @@ -163,6 +166,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); PathPlannerPath path2 = PathPlannerPath( name: 'test', @@ -186,6 +190,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); PathPlannerPath path3 = PathPlannerPath( name: 'test2', @@ -209,6 +214,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); expect(path2, path1); @@ -244,6 +250,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); path.addWaypoint(const Point(6.0, 1.0)); @@ -293,6 +300,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); path.insertWaypointAfter(1); diff --git a/test/services/simulator/trajectory_generator_test.dart b/test/services/simulator/trajectory_generator_test.dart index 7d09b8be..b9e3e7ff 100644 --- a/test/services/simulator/trajectory_generator_test.dart +++ b/test/services/simulator/trajectory_generator_test.dart @@ -41,6 +41,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); Trajectory sim = Trajectory.simulate(test, 0, 0); @@ -81,6 +82,7 @@ void main() { reversed: false, folder: null, previewStartingState: null, + useDefaultConstraints: false, ); // Basic coverage tests, expand in future diff --git a/test/widgets/editor/tree_widgets/global_constraints_tree_test.dart b/test/widgets/editor/tree_widgets/global_constraints_tree_test.dart index b89b7fe6..af76de5b 100644 --- a/test/widgets/editor/tree_widgets/global_constraints_tree_test.dart +++ b/test/widgets/editor/tree_widgets/global_constraints_tree_test.dart @@ -37,6 +37,7 @@ void main() { onPathChanged: () => pathChanged = true, undoStack: undoStack, holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -63,6 +64,7 @@ void main() { onPathChanged: () => pathChanged = true, undoStack: undoStack, holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -92,6 +94,7 @@ void main() { onPathChanged: () => pathChanged = true, undoStack: undoStack, holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -121,6 +124,7 @@ void main() { onPathChanged: () => pathChanged = true, undoStack: undoStack, holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -150,6 +154,7 @@ void main() { onPathChanged: () => pathChanged = true, undoStack: undoStack, holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -170,4 +175,39 @@ void main() { await widgetTester.pump(); expect(path.globalConstraints.maxAngularAcceleration, 1.0); }); + + testWidgets('use defaults checkbox', (widgetTester) async { + await widgetTester.pumpWidget(MaterialApp( + home: Scaffold( + body: GlobalConstraintsTree( + path: path, + onPathChanged: () => pathChanged = true, + undoStack: undoStack, + holonomicMode: true, + defaultConstraints: PathConstraints(), + ), + ), + )); + + final check = find.byType(Checkbox); + + expect(check, findsOneWidget); + + await widgetTester.tap(check); + await widgetTester.pump(); + + expect(pathChanged, true); + expect(path.globalConstraints, PathConstraints()); + + undoStack.undo(); + await widgetTester.pump(); + expect( + path.globalConstraints, + PathConstraints( + maxVelocity: 1.0, + maxAcceleration: 1.0, + maxAngularVelocity: 1.0, + maxAngularAcceleration: 1.0, + )); + }); } diff --git a/test/widgets/editor/tree_widgets/path_tree_test.dart b/test/widgets/editor/tree_widgets/path_tree_test.dart index 09ee1c2d..c6fcd4e1 100644 --- a/test/widgets/editor/tree_widgets/path_tree_test.dart +++ b/test/widgets/editor/tree_widgets/path_tree_test.dart @@ -1,6 +1,7 @@ import 'package:file/memory.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pathplanner/path/path_constraints.dart'; import 'package:pathplanner/path/pathplanner_path.dart'; import 'package:pathplanner/widgets/editor/tree_widgets/constraint_zones_tree.dart'; import 'package:pathplanner/widgets/editor/tree_widgets/event_markers_tree.dart'; @@ -31,6 +32,7 @@ void main() { path: path, undoStack: ChangeStack(), holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -46,6 +48,7 @@ void main() { undoStack: ChangeStack(), onSideSwapped: () => sideSwapped = true, holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -66,6 +69,7 @@ void main() { path: path, undoStack: ChangeStack(), holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -80,6 +84,7 @@ void main() { path: path, undoStack: ChangeStack(), holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -94,6 +99,7 @@ void main() { path: path, undoStack: ChangeStack(), holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -108,6 +114,7 @@ void main() { path: path, undoStack: ChangeStack(), holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -122,6 +129,7 @@ void main() { path: path, undoStack: ChangeStack(), holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -136,6 +144,7 @@ void main() { path: path, undoStack: ChangeStack(), holonomicMode: true, + defaultConstraints: PathConstraints(), ), ), )); @@ -152,6 +161,7 @@ void main() { path: path, undoStack: undoStack, holonomicMode: false, + defaultConstraints: PathConstraints(), ), ), ));