More toStrings and tests for physics (#82503)

diff --git a/packages/flutter/lib/src/physics/clamped_simulation.dart b/packages/flutter/lib/src/physics/clamped_simulation.dart
index c50dbc7..6c5b7bb 100644
--- a/packages/flutter/lib/src/physics/clamped_simulation.dart
+++ b/packages/flutter/lib/src/physics/clamped_simulation.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/foundation.dart';
+
 import 'simulation.dart';
 
 /// A simulation that applies limits to another simulation.
@@ -15,8 +17,13 @@
 /// difference would just be that the position would be reported as pinned to
 /// the maximum value for the times that it would otherwise have been reported
 /// as higher.
+///
+/// Similarly, this means that the [x] value will change at a rate that does not
+/// match the reported [dx] value while one or the other is being clamped.
+///
+/// The [isDone] logic is unaffected by the clamping; it reflects the logic of
+/// the underlying simulation.
 class ClampedSimulation extends Simulation {
-
   /// Creates a [ClampedSimulation] that clamps the given simulation.
   ///
   /// The named arguments specify the ranges for the clamping behavior, as
@@ -55,4 +62,7 @@
 
   @override
   bool isDone(double time) => simulation.isDone(time);
+
+  @override
+  String toString() => '${objectRuntimeType(this, 'ClampedSimulation')}(simulation: $simulation, x: ${xMin.toStringAsFixed(1)}..${xMax.toStringAsFixed(1)}, dx: ${dxMin.toStringAsFixed(1)}..${dxMax.toStringAsFixed(1)})';
 }
diff --git a/packages/flutter/lib/src/physics/friction_simulation.dart b/packages/flutter/lib/src/physics/friction_simulation.dart
index 1879d79..17ac10e 100644
--- a/packages/flutter/lib/src/physics/friction_simulation.dart
+++ b/packages/flutter/lib/src/physics/friction_simulation.dart
@@ -4,6 +4,8 @@
 
 import 'dart:math' as math;
 
+import 'package:flutter/foundation.dart';
+
 import 'simulation.dart';
 import 'tolerance.dart';
 
@@ -15,8 +17,8 @@
 /// the current velocity [tolerance]).
 class FrictionSimulation extends Simulation {
   /// Creates a [FrictionSimulation] with the given arguments, namely: the fluid
-  /// drag coefficient, a unitless value; the initial position, in the same
-  /// length units as used for [x]; and the initial velocity, in the same
+  /// drag coefficient _cₓ_, a unitless value; the initial position _x₀_, in the same
+  /// length units as used for [x]; and the initial velocity _dx₀_, in the same
   /// velocity units as used for [dx].
   FrictionSimulation(
     double drag,
@@ -29,7 +31,7 @@
        _v = velocity,
        super(tolerance: tolerance);
 
-  /// Creates a new friction simulation with its fluid drag coefficient set so
+  /// Creates a new friction simulation with its fluid drag coefficient (_cₓ_) set so
   /// as to ensure that the simulation starts and ends at the specified
   /// positions and velocities.
   ///
@@ -90,14 +92,20 @@
 
   @override
   bool isDone(double time) => dx(time).abs() < tolerance.velocity;
+
+  @override
+  String toString() => '${objectRuntimeType(this, 'FrictionSimulation')}(cₓ: ${_drag.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)})';
 }
 
 /// A [FrictionSimulation] that clamps the modeled particle to a specific range
 /// of values.
+///
+/// Only the position is clamped. The velocity [dx] will continue to report
+/// unbounded simulated velocities once the particle has reached the bounds.
 class BoundedFrictionSimulation extends FrictionSimulation {
   /// Creates a [BoundedFrictionSimulation] with the given arguments, namely:
-  /// the fluid drag coefficient, a unitless value; the initial position, in the
-  /// same length units as used for [x]; the initial velocity, in the same
+  /// the fluid drag coefficient _cₓ_, a unitless value; the initial position _x₀_, in the
+  /// same length units as used for [x]; the initial velocity _dx₀_, in the same
   /// velocity units as used for [dx], the minimum value for the position, and
   /// the maximum value for the position. The minimum and maximum values must be
   /// in the same units as the initial position, and the initial position must
@@ -125,4 +133,7 @@
       (x(time) - _minX).abs() < tolerance.distance ||
       (x(time) - _maxX).abs() < tolerance.distance;
   }
+
+  @override
+  String toString() => '${objectRuntimeType(this, 'BoundedFrictionSimulation')}(cₓ: ${_drag.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)}, x: ${_minX.toStringAsFixed(1)}..${_maxX.toStringAsFixed(1)})';
 }
diff --git a/packages/flutter/lib/src/physics/gravity_simulation.dart b/packages/flutter/lib/src/physics/gravity_simulation.dart
index ac0629e..d3715e2 100644
--- a/packages/flutter/lib/src/physics/gravity_simulation.dart
+++ b/packages/flutter/lib/src/physics/gravity_simulation.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/foundation.dart';
+
 import 'simulation.dart';
 
 // Examples can assume:
@@ -10,7 +12,7 @@
 /// A simulation that applies a constant accelerating force.
 ///
 /// Models a particle that follows Newton's second law of motion. The simulation
-/// ends when the position reaches a defined point.
+/// ends when the position exceeds a defined threshold.
 ///
 /// {@tool snippet}
 ///
@@ -32,6 +34,18 @@
 /// This [AnimationController] could be used with an [AnimatedBuilder] to
 /// animate the position of a child as if it was falling.
 ///
+/// The end distance threshold (the constructor's third argument) must be
+/// specified as a positive number but can be reached in either the positive or
+/// negative direction. For example (assuming negative numbers represent higher
+/// physical positions than positive numbers, as is the case with the normal
+/// [Canvas] coordinate system), if the acceleration is positive ("down") the
+/// starting velocity is negative ("up"), and the starting distance is zero, the
+/// particle will climb from the origin, reach a plateau, then fall back towards
+/// and past the origin. If the end distance threshold is less than the height
+/// of the plateau, then the simulation will end during the climb; otherwise, it
+/// will end during the fall, after the particle travels below the origin by
+/// that distance.
+///
 /// See also:
 ///
 ///  * [Curves.bounceOut], a [Curve] that has a similar aesthetics but includes
@@ -79,4 +93,7 @@
 
   @override
   bool isDone(double time) => x(time).abs() >= _end;
+
+  @override
+  String toString() => '${objectRuntimeType(this, 'GravitySimulation')}(g: ${_a.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)}, xₘₐₓ: ±${_end.toStringAsFixed(1)})';
 }
diff --git a/packages/flutter/lib/src/physics/spring_simulation.dart b/packages/flutter/lib/src/physics/spring_simulation.dart
index 5639ba5..d25c033 100644
--- a/packages/flutter/lib/src/physics/spring_simulation.dart
+++ b/packages/flutter/lib/src/physics/spring_simulation.dart
@@ -121,7 +121,7 @@
   }
 
   @override
-  String toString() => '${objectRuntimeType(this, 'SpringSimulation')}(end: $_endPosition, $type)';
+  String toString() => '${objectRuntimeType(this, 'SpringSimulation')}(end: ${_endPosition.toStringAsFixed(1)}, $type)';
 }
 
 /// A [SpringSimulation] where the value of [x] is guaranteed to have exactly the
diff --git a/packages/flutter/lib/src/physics/tolerance.dart b/packages/flutter/lib/src/physics/tolerance.dart
index 6351a2d..10842dc 100644
--- a/packages/flutter/lib/src/physics/tolerance.dart
+++ b/packages/flutter/lib/src/physics/tolerance.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/foundation.dart';
+
 /// Structure that specifies maximum allowable magnitudes for distances,
 /// durations, and velocity differences to be considered equal.
 class Tolerance {
@@ -42,5 +44,5 @@
   final double velocity;
 
   @override
-  String toString() => 'Tolerance(distance: ±$distance, time: ±$time, velocity: ±$velocity)';
+  String toString() => '${objectRuntimeType(this, 'Tolerance')}(distance: ±$distance, time: ±$time, velocity: ±$velocity)';
 }
diff --git a/packages/flutter/test/physics/clamped_simulation_test.dart b/packages/flutter/test/physics/clamped_simulation_test.dart
index 592f319..e53fd6c 100644
--- a/packages/flutter/test/physics/clamped_simulation_test.dart
+++ b/packages/flutter/test/physics/clamped_simulation_test.dart
@@ -6,7 +6,7 @@
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
-  test('Clamped simulation', () {
+  test('Clamped simulation 1', () {
     final GravitySimulation gravity = GravitySimulation(9.81, 10.0, 0.0, 0.0);
     final ClampedSimulation clamped = ClampedSimulation(gravity, xMin: 20.0, xMax: 100.0, dxMin: 7.0, dxMax: 11.0);
 
@@ -16,4 +16,25 @@
     expect(clamped.x(100.0), equals(100.0));
     expect(clamped.dx(100.0), equals(11.0));
   });
+
+  test('Clamped simulation 2', () {
+    final GravitySimulation gravity = GravitySimulation(-10, 0.0, 6.0, 10.0);
+    final ClampedSimulation clamped = ClampedSimulation(gravity, xMin: 0.0, xMax: 2.5, dxMin: -1.0, dxMax: 1.0);
+
+    expect(clamped.x(0.0), equals(0.0));
+    expect(clamped.dx(0.0), equals(1.0));
+    expect(clamped.isDone(0.0), isFalse);
+
+    expect(clamped.x(1.0), equals(2.5));
+    expect(clamped.dx(1.0), equals(0.0));
+    expect(clamped.isDone(0.2), isFalse);
+
+    expect(clamped.x(2.0), equals(0.0));
+    expect(clamped.dx(2.0), equals(-1.0));
+    expect(clamped.isDone(2.0), isFalse);
+
+    expect(clamped.x(3.0), equals(0.0));
+    expect(clamped.dx(3.0), equals(-1.0));
+    expect(clamped.isDone(3.0), isTrue);
+  });
 }
diff --git a/packages/flutter/test/physics/gravity_simulation_test.dart b/packages/flutter/test/physics/gravity_simulation_test.dart
index ccd6874..de8aa92 100644
--- a/packages/flutter/test/physics/gravity_simulation_test.dart
+++ b/packages/flutter/test/physics/gravity_simulation_test.dart
@@ -6,8 +6,28 @@
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
-  test('gravity simulation', () {
+  test('Gravity simulation 1', () {
     expect(GravitySimulation(9.81, 10.0, 0.0, 0.0), hasOneLineDescription);
     expect(GravitySimulation(9.81, 10.0, 0.0, 0.0).x(10.0), moreOrLessEquals(50.0 * 9.81 + 10.0));
   });
+
+  test('Gravity simulation 2', () {
+    final GravitySimulation gravity = GravitySimulation(-10, 0.0, 6.0, 10.0);
+
+    expect(gravity.x(0.0), equals(0.0));
+    expect(gravity.dx(0.0), equals(10.0));
+    expect(gravity.isDone(0.0), isFalse);
+
+    expect(gravity.x(1.0), equals(5.0));
+    expect(gravity.dx(1.0), equals(0.0));
+    expect(gravity.isDone(0.2), isFalse);
+
+    expect(gravity.x(2.0), equals(0.0));
+    expect(gravity.dx(2.0), equals(-10.0));
+    expect(gravity.isDone(2.0), isFalse);
+
+    expect(gravity.x(3.0), equals(-15.0));
+    expect(gravity.dx(3.0), equals(-20.0));
+    expect(gravity.isDone(3.0), isTrue);
+  });
 }
diff --git a/packages/flutter/test/physics/to_string_test.dart b/packages/flutter/test/physics/to_string_test.dart
new file mode 100644
index 0000000..f0f9bfc
--- /dev/null
+++ b/packages/flutter/test/physics/to_string_test.dart
@@ -0,0 +1,30 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/physics.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+class TestSimulation extends Simulation {
+  @override
+  double x(double t) => 0.0;
+
+  @override
+  double dx(double t) => 0.0;
+
+  @override
+  bool isDone(double t) => true;
+}
+
+void main() {
+  test('Simulation.toString', () {
+    expect(ClampedSimulation(TestSimulation(), xMin: -1.0, xMax: 2.0, dxMin: -3.0, dxMax: 4.0).toString(), 'ClampedSimulation(simulation: TestSimulation, x: -1.0..2.0, dx: -3.0..4.0)');
+    expect(TestSimulation().toString(), 'TestSimulation');
+    expect(GravitySimulation(1.0, -2.0, 3.0, -4.0).toString(), 'GravitySimulation(g: 1.0, x₀: -2.0, dx₀: -4.0, xₘₐₓ: ±3.0)');
+    expect(FrictionSimulation(1.0, -2.0, 3.0).toString(), 'FrictionSimulation(cₓ: 1.0, x₀: -2.0, dx₀: 3.0)');
+    expect(BoundedFrictionSimulation(1.0, -2.0, 3.0, -4.0, 5.0).toString(), 'BoundedFrictionSimulation(cₓ: 1.0, x₀: -2.0, dx₀: 3.0, x: -4.0..5.0)');
+    expect(const SpringDescription(mass: 1.0, stiffness: -2.0, damping: 3.0).toString(), 'SpringDescription(mass: 1.0, stiffness: -2.0, damping: 3.0)');
+    expect(SpringDescription.withDampingRatio(mass: 1.0, stiffness: 9.0).toString(), 'SpringDescription(mass: 1.0, stiffness: 9.0, damping: 6.0)');
+    expect(SpringSimulation(const SpringDescription(mass: 1.0, stiffness: 2.0, damping: 3.0), 0.0, 1.0, 2.0).toString(), 'SpringSimulation(end: 1.0, SpringType.overDamped)');
+  });
+}