function_plotter.dart 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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. static const double _sampleFrom = -10;
  20. static const double _sampleTo = 10;
  21. static const int _sampleCount = 800;
  22. static const int _maxCurves = 5;
  23. /// 每条曲线的固定颜色,下标与输入框一一对应。
  24. static const List<Color> _curveColors = <Color>[
  25. Colors.blue,
  26. Colors.red,
  27. Colors.green,
  28. Colors.orange,
  29. Colors.purple,
  30. ];
  31. /// 可增减的表达式输入框。列表**就地增删**(对象始终是同一个),
  32. /// 外壳靠 `identical(fields)` 判断要不要清空结果,换新 List 会把结果清掉。
  33. final List<ToolField> _exprs = <ToolField>[
  34. ToolField(
  35. '函数 1',
  36. hint: '例如 x^2、Sin[x]、Sqrt[x]',
  37. keyboard: TextInputType.text,
  38. ),
  39. ];
  40. void _addCurve() {
  41. if (_exprs.length >= _maxCurves) return;
  42. setState(() {
  43. _exprs.add(ToolField(
  44. '函数 ${_exprs.length + 1}',
  45. hint: '例如 x^2、Sin[x]、Sqrt[x]',
  46. keyboard: TextInputType.text,
  47. ));
  48. });
  49. }
  50. void _removeCurve() {
  51. if (_exprs.length <= 1) return;
  52. // 不 dispose 被移除的控制器:外壳约定不 dispose 输入框,
  53. // 而且这一帧旧的 TextField 可能还引用着它,提前 dispose 会抛异常。
  54. setState(() => _exprs.removeLast());
  55. }
  56. Widget _intro() => Column(
  57. crossAxisAlignment: CrossAxisAlignment.start,
  58. children: [
  59. const Text('函数表达式 (使用 x 作为变量)'),
  60. const Text(
  61. '支持: +, -, *, /, ^, sin, cos, tan, ln, sqrt, pi, e',
  62. style: TextStyle(fontSize: 12),
  63. ),
  64. const SizedBox(height: 8),
  65. Row(
  66. children: [
  67. OutlinedButton.icon(
  68. onPressed: _exprs.length < _maxCurves ? _addCurve : null,
  69. icon: const Icon(Icons.add),
  70. label: const Text('添加函数'),
  71. ),
  72. const SizedBox(width: 12),
  73. OutlinedButton.icon(
  74. onPressed: _exprs.length > 1 ? _removeCurve : null,
  75. icon: const Icon(Icons.remove),
  76. label: const Text('删除函数'),
  77. ),
  78. ],
  79. ),
  80. ],
  81. );
  82. @override
  83. Widget build(BuildContext context) => ToolScaffold(
  84. title: '函数图像绘制',
  85. // 默认已经有一个 x^2,打开就该看到图。
  86. autoRun: true,
  87. fields: _exprs,
  88. intro: _intro(),
  89. compute: (session) async {
  90. final series = <CurveSeries>[];
  91. final failed = <String>[];
  92. // 逐条采样:一条失败只记下它自己,别的照画。
  93. for (var i = 0; i < _exprs.length; i++) {
  94. final raw = _exprs[i].text;
  95. if (raw.isEmpty) continue;
  96. try {
  97. // 表达式原样交给 Symja 解析,谁解析谁负责。
  98. final s = await session.sample(
  99. raw,
  100. variable: 'x',
  101. from: _sampleFrom,
  102. to: _sampleTo,
  103. count: _sampleCount,
  104. );
  105. series.add(CurveSeries(
  106. label: raw,
  107. color: _curveColors[i % _curveColors.length],
  108. // 非有限值(极点处的 ComplexInfinity 等)原样保留,
  109. // CurvePlot 会把它们当成抬笔断点。
  110. points: <Offset>[
  111. for (var k = 0; k < s.ys.length; k++) Offset(s.xAt(k), s.ys[k]),
  112. ],
  113. ));
  114. } on SymjaException catch (e) {
  115. failed.add('$raw:$e');
  116. }
  117. }
  118. if (series.isEmpty && failed.isEmpty) {
  119. throw Exception('请至少填写一个函数表达式');
  120. }
  121. return <ToolOutput>[
  122. if (series.isNotEmpty)
  123. ToolChart(
  124. '函数图像',
  125. // CurvePlot 内部用 Expanded,必须给一个确定高度,
  126. // 否则放在外层 Column 里会因为高度无界而报错。
  127. SizedBox(height: 360, child: CurvePlot(series: series)),
  128. ),
  129. if (failed.isNotEmpty)
  130. ToolText('未能绘制的函数', failed.join('\n')),
  131. ];
  132. },
  133. );
  134. }