function_plotter.dart 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // tools/function_plotter.dart
  2. //
  3. // 函数图像绘制:一个或多个 y = f(x) 原样交给 Symja 采样,再用 [CurvePlot] 画。
  4. //
  5. // 采样部分(区间、点数、由 sample 拿点)照旧不变;变的只是外壳 —— 输入框交给
  6. // [ToolScaffold],绘制按钮就是外壳的「计算」,重置按钮也是外壳自带的。
  7. import 'package:flutter/material.dart';
  8. import '../curve_plot.dart';
  9. import '../symja.dart';
  10. import 'tool_kit.dart';
  11. class FunctionPlotterPage extends StatefulWidget {
  12. const FunctionPlotterPage({super.key});
  13. @override
  14. State<FunctionPlotterPage> createState() => _FunctionPlotterPageState();
  15. }
  16. class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
  17. /// 横纵范围在**画图之前**定:两个滑动条放在「计算」按钮上方,
  18. /// 采样区间和显示窗口都用它们(原来滑条在图下面、只管显示窗口,
  19. /// 曲线拉出去就断在那儿)。
  20. RangeValues _xRange = const RangeValues(-10, 10);
  21. RangeValues _yRange = const RangeValues(-10, 10);
  22. /// 滑条的取值范围(比默认宽,方便拉大拉小)。
  23. static const RangeValues _rangeLimits = RangeValues(-50, 50);
  24. /// 每单位 x 采几个点。
  25. static const double _perUnit = 60;
  26. static const int _maxCurves = 5;
  27. /// 每条曲线的固定颜色,下标与输入框一一对应。
  28. static const List<Color> _curveColors = <Color>[
  29. Colors.blue,
  30. Colors.red,
  31. Colors.green,
  32. Colors.orange,
  33. Colors.purple,
  34. ];
  35. /// 可增减的表达式输入框。列表**就地增删**(对象始终是同一个),
  36. /// 外壳靠 `identical(fields)` 判断要不要清空结果,换新 List 会把结果清掉。
  37. final List<ToolField> _exprs = <ToolField>[
  38. ToolField(
  39. '函数 1',
  40. hint: '例如 x^2、Sin[x]、Sqrt[x]',
  41. keyboard: TextInputType.text,
  42. ),
  43. ];
  44. @override
  45. void initState() {
  46. super.initState();
  47. // 打开就该看到一条曲线:第一个框预置 x^2(autoRun 立刻画)
  48. _exprs.first.controller.text = 'x^2';
  49. }
  50. void _addCurve() {
  51. if (_exprs.length >= _maxCurves) return;
  52. setState(() {
  53. _exprs.add(
  54. ToolField(
  55. '函数 ${_exprs.length + 1}',
  56. hint: '例如 x^2、Sin[x]、Sqrt[x]',
  57. keyboard: TextInputType.text,
  58. ),
  59. );
  60. });
  61. }
  62. void _removeCurve() {
  63. if (_exprs.length <= 1) return;
  64. // 不 dispose 被移除的控制器:外壳约定不 dispose 输入框,
  65. // 而且这一帧旧的 TextField 可能还引用着它,提前 dispose 会抛异常。
  66. setState(() => _exprs.removeLast());
  67. }
  68. /// 一条范围滑动条:左边名字、中间滑条、右边当前范围。
  69. Widget _rangeRow(
  70. String label,
  71. RangeValues v,
  72. ValueChanged<RangeValues> onChanged,
  73. ) => Row(
  74. children: [
  75. SizedBox(
  76. width: 64,
  77. child: Text(label, style: const TextStyle(fontSize: 12)),
  78. ),
  79. Expanded(
  80. child: RangeSlider(
  81. values: v,
  82. min: _rangeLimits.start,
  83. max: _rangeLimits.end,
  84. divisions: (_rangeLimits.end - _rangeLimits.start).round(),
  85. labels: RangeLabels('${v.start.round()}', '${v.end.round()}'),
  86. onChanged: onChanged,
  87. ),
  88. ),
  89. SizedBox(
  90. width: 76,
  91. child: Text(
  92. '${v.start.round()} ~ ${v.end.round()}',
  93. textAlign: TextAlign.right,
  94. style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
  95. ),
  96. ),
  97. ],
  98. );
  99. /// 两个范围滑动条放在「计算」上方:**先定范围再画图**。
  100. Widget _intro() => Column(
  101. crossAxisAlignment: CrossAxisAlignment.start,
  102. children: [
  103. _rangeRow('横轴范围', _xRange, (v) => setState(() => _xRange = v)),
  104. _rangeRow('纵轴范围', _yRange, (v) => setState(() => _yRange = v)),
  105. const SizedBox(height: 8),
  106. Row(
  107. children: [
  108. OutlinedButton.icon(
  109. onPressed: _exprs.length < _maxCurves ? _addCurve : null,
  110. icon: const Icon(Icons.add),
  111. label: const Text('添加函数'),
  112. ),
  113. const SizedBox(width: 12),
  114. OutlinedButton.icon(
  115. onPressed: _exprs.length > 1 ? _removeCurve : null,
  116. icon: const Icon(Icons.remove),
  117. label: const Text('删除函数'),
  118. ),
  119. ],
  120. ),
  121. ],
  122. );
  123. @override
  124. Widget build(BuildContext context) => ToolScaffold(
  125. title: '函数图像绘制',
  126. // 默认已经有一个 x^2,打开就该看到图。
  127. autoRun: true,
  128. fields: _exprs,
  129. intro: _intro(),
  130. compute: (session) async {
  131. final series = <CurveSeries>[];
  132. final failed = <String>[];
  133. final from = _xRange.start, to = _xRange.end;
  134. final count = ((to - from) * _perUnit).round().clamp(200, 4000);
  135. // 逐条采样:一条失败只记下它自己,别的照画。
  136. for (var i = 0; i < _exprs.length; i++) {
  137. final raw = _exprs[i].text;
  138. if (raw.isEmpty) continue;
  139. try {
  140. // 表达式原样交给 Symja 解析,谁解析谁负责。
  141. final s = await session.sample(
  142. raw,
  143. variable: 'x',
  144. from: from,
  145. to: to,
  146. count: count,
  147. );
  148. series.add(
  149. CurveSeries(
  150. label: raw,
  151. color: _curveColors[i % _curveColors.length],
  152. // 非有限值(极点处的 ComplexInfinity 等)原样保留,
  153. // CurvePlot 会把它们当成抬笔断点。
  154. points: <Offset>[
  155. for (var k = 0; k < s.ys.length; k++) Offset(s.xAt(k), s.ys[k]),
  156. ],
  157. ),
  158. );
  159. } on SymjaException catch (e) {
  160. failed.add('$raw:$e');
  161. }
  162. }
  163. if (series.isEmpty && failed.isEmpty) {
  164. throw Exception('请至少填写一个函数表达式');
  165. }
  166. return <ToolOutput>[
  167. if (series.isNotEmpty)
  168. ToolChart(
  169. '函数图像',
  170. // CurvePlot 内部用 Expanded,必须给一个确定高度,
  171. // 否则放在外层 Column 里会因为高度无界而报错。
  172. SizedBox(
  173. height: 360,
  174. child: CurvePlot(
  175. series: series,
  176. // 窗口由上面两个滑动条定死,图自己的滑条收起来
  177. xWindow: (from, to),
  178. yWindow: (_yRange.start, _yRange.end),
  179. showControls: false,
  180. ),
  181. ),
  182. ),
  183. if (failed.isNotEmpty) ToolText('未能绘制的函数', failed.join('\n')),
  184. ];
  185. },
  186. );
  187. }