// tools/function_plotter.dart // // 函数图像绘制:一个或多个 y = f(x) 原样交给 Symja 采样,再用 [CurvePlot] 画。 // // 采样部分(区间、点数、由 sample 拿点)照旧不变;变的只是外壳 —— 输入框交给 // [ToolScaffold],绘制按钮就是外壳的「计算」,重置按钮也是外壳自带的。 import 'package:flutter/material.dart'; import '../curve_plot.dart'; import '../symja.dart'; import 'tool_kit.dart'; class FunctionPlotterPage extends StatefulWidget { const FunctionPlotterPage({super.key}); @override State createState() => _FunctionPlotterPageState(); } class _FunctionPlotterPageState extends State { /// 固定的采样区间与点数。横纵范围由图下面的滑条控制,点取得密一些, /// 放大到一小段时也还有足够的点。 static const double _sampleFrom = -10; static const double _sampleTo = 10; static const int _sampleCount = 800; static const int _maxCurves = 5; /// 每条曲线的固定颜色,下标与输入框一一对应。 static const List _curveColors = [ Colors.blue, Colors.red, Colors.green, Colors.orange, Colors.purple, ]; /// 可增减的表达式输入框。列表**就地增删**(对象始终是同一个), /// 外壳靠 `identical(fields)` 判断要不要清空结果,换新 List 会把结果清掉。 final List _exprs = [ ToolField( '函数 1', hint: '例如 x^2、Sin[x]、Sqrt[x]', keyboard: TextInputType.text, ), ]; void _addCurve() { if (_exprs.length >= _maxCurves) return; setState(() { _exprs.add(ToolField( '函数 ${_exprs.length + 1}', hint: '例如 x^2、Sin[x]、Sqrt[x]', keyboard: TextInputType.text, )); }); } void _removeCurve() { if (_exprs.length <= 1) return; // 不 dispose 被移除的控制器:外壳约定不 dispose 输入框, // 而且这一帧旧的 TextField 可能还引用着它,提前 dispose 会抛异常。 setState(() => _exprs.removeLast()); } Widget _intro() => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('函数表达式 (使用 x 作为变量)'), const Text( '支持: +, -, *, /, ^, sin, cos, tan, ln, sqrt, pi, e', style: TextStyle(fontSize: 12), ), const SizedBox(height: 8), Row( children: [ OutlinedButton.icon( onPressed: _exprs.length < _maxCurves ? _addCurve : null, icon: const Icon(Icons.add), label: const Text('添加函数'), ), const SizedBox(width: 12), OutlinedButton.icon( onPressed: _exprs.length > 1 ? _removeCurve : null, icon: const Icon(Icons.remove), label: const Text('删除函数'), ), ], ), ], ); @override Widget build(BuildContext context) => ToolScaffold( title: '函数图像绘制', // 默认已经有一个 x^2,打开就该看到图。 autoRun: true, fields: _exprs, intro: _intro(), compute: (session) async { final series = []; final failed = []; // 逐条采样:一条失败只记下它自己,别的照画。 for (var i = 0; i < _exprs.length; i++) { final raw = _exprs[i].text; if (raw.isEmpty) continue; try { // 表达式原样交给 Symja 解析,谁解析谁负责。 final s = await session.sample( raw, variable: 'x', from: _sampleFrom, to: _sampleTo, count: _sampleCount, ); series.add(CurveSeries( label: raw, color: _curveColors[i % _curveColors.length], // 非有限值(极点处的 ComplexInfinity 等)原样保留, // CurvePlot 会把它们当成抬笔断点。 points: [ for (var k = 0; k < s.ys.length; k++) Offset(s.xAt(k), s.ys[k]), ], )); } on SymjaException catch (e) { failed.add('$raw:$e'); } } if (series.isEmpty && failed.isEmpty) { throw Exception('请至少填写一个函数表达式'); } return [ if (series.isNotEmpty) ToolChart( '函数图像', // CurvePlot 内部用 Expanded,必须给一个确定高度, // 否则放在外层 Column 里会因为高度无界而报错。 SizedBox(height: 360, child: CurvePlot(series: series)), ), if (failed.isNotEmpty) ToolText('未能绘制的函数', failed.join('\n')), ]; }, ); }