| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- // tools/parametric_plotter.dart
- //
- // 参数方程绘图:x = f(t), y = g(t),用 Symja 采样后交给 [CurvePlot] 画。
- //
- // 采样要点:Symja 的 `sample` 一次只能算一个表达式,所以 x(t) 和 y(t) 各调用
- // 一次,参数区间和点数完全一致,再把两个结果按下标配成 (x, y) 点。
- import 'package:flutter/material.dart';
- import '../curve_plot.dart';
- import '../symja.dart';
- import 'tool_kit.dart';
- class ParametricPlotterPage extends StatefulWidget {
- const ParametricPlotterPage({super.key});
- @override
- State<ParametricPlotterPage> createState() => _ParametricPlotterPageState();
- }
- class _ParametricPlotterPageState extends State<ParametricPlotterPage> {
- final ToolField _x = ToolField('x = f(t)', hint: '例如 cos(t)、t^2');
- final ToolField _y = ToolField('y = g(t)', hint: '例如 sin(t)、t^3');
- final ToolField _tMin = ToolField('t 最小值', hint: '例如 0');
- final ToolField _tMax = ToolField('t 最大值', hint: '例如 6.3');
- final ToolField _step = ToolField('步长', hint: '例如 0.1');
- /// 稳定的列表对象:外壳靠 `identical(fields)` 判断要不要清空结果,
- /// 每次 build 新建 List 会让主题一变结果就被清掉。
- late final List<ToolField> _fields = [_x, _y, _tMin, _tMax, _step];
- /// 给外壳换 key 用。示例按钮改完输入框后把 key 一换,外壳就会重建并
- /// autoRun,于是"点了示例就重算"这件事不用外壳额外开放接口。
- int _runToken = 0;
- @override
- void initState() {
- super.initState();
- _x.controller.text = 'cos(t)';
- _y.controller.text = 'sin(t)';
- _tMin.controller.text = '0';
- _tMax.controller.text = '6.3';
- _step.controller.text = '0.1';
- }
- void _applyPreset(String x, String y, String tMin, String tMax) {
- setState(() {
- _x.controller.text = x;
- _y.controller.text = y;
- _tMin.controller.text = tMin;
- _tMax.controller.text = tMax;
- _step.controller.text = '0.1';
- _runToken++; // 换 key → 外壳重建 → autoRun 重新采样
- });
- }
- Widget _intro() => Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: [
- ActionChip(
- label: const Text('椭圆'),
- onPressed: () => _applyPreset('2*cos(t)', 'sin(t)', '0', '6.3'),
- ),
- ActionChip(
- label: const Text('螺旋线'),
- onPressed: () => _applyPreset('t*cos(t)', 't*sin(t)', '0', '12.6'),
- ),
- ActionChip(
- label: const Text('抛物线'),
- onPressed: () => _applyPreset('t', 't^2', '-2', '2'),
- ),
- ActionChip(
- label: const Text('心形线'),
- onPressed: () => _applyPreset(
- '2*cos(t)-cos(2*t)',
- '2*sin(t)-sin(2*t)',
- '0',
- '6.3',
- ),
- ),
- ],
- ),
- ],
- );
- @override
- Widget build(BuildContext context) => ToolScaffold(
- // key 一换,外壳整体重建并重新 autoRun —— 示例按钮靠它重算。
- key: ValueKey<int>(_runToken),
- title: '参数方程绘图',
- runLabel: '绘制',
- runIcon: Icons.show_chart,
- autoRun: true,
- fields: _fields,
- intro: _intro(),
- compute: (session) async {
- final xRaw = _x.text;
- final yRaw = _y.text;
- final tMinRaw = _tMin.text;
- final tMaxRaw = _tMax.text;
- final stepRaw = _step.text;
- // 校验:都是格式检查,Dart 里做即可。
- if (xRaw.isEmpty || yRaw.isEmpty) {
- throw Exception('请填写 x(t) 和 y(t) 表达式');
- }
- final tMin = double.tryParse(tMinRaw);
- final tMax = double.tryParse(tMaxRaw);
- final step = double.tryParse(stepRaw);
- if (tMin == null || tMax == null || step == null) {
- throw Exception('请输入有效的 t 范围与步长');
- }
- if (!tMin.isFinite || !tMax.isFinite || !step.isFinite) {
- throw Exception('t 范围与步长必须是有限数');
- }
- if (tMin >= tMax) {
- throw Exception('t 最小值必须小于 t 最大值');
- }
- if (step <= 0) {
- throw Exception('步长必须大于 0');
- }
- // 把用户给的步长换算成点数,再做上下限保护,避免点数过多拖垮求值。
- final n = (((tMax - tMin) / step).round() + 1).clamp(2, 5000).toInt();
- final List<CurveSeries> series;
- try {
- // 同一个 t 区间、同一点数分别采样两条曲线——Symja 一次只能算一个表达式。
- final xs = await session.sample(
- xRaw,
- variable: 't',
- from: tMin,
- to: tMax,
- count: n,
- );
- final ys = await session.sample(
- yRaw,
- variable: 't',
- from: tMin,
- to: tMax,
- count: n,
- );
- final m = xs.ys.length < ys.ys.length ? xs.ys.length : ys.ys.length;
- series = <CurveSeries>[
- CurveSeries(
- label: 'x=$xRaw, y=$yRaw',
- color: Colors.blue,
- // NaN(极点/无定义点)照传,CurvePlot 会当成断点抬笔。
- points: <Offset>[
- for (var i = 0; i < m; i++) Offset(xs.ys[i], ys.ys[i]),
- ],
- ),
- ];
- } on SymjaException catch (e) {
- throw Exception('采样失败:$e');
- }
- return [
- ToolChart(
- '参数方程图像',
- // 内部用 Expanded,必须给确定高度;参数曲线常常是圆/椭圆,
- // 必须等比例(equalAspect: true)。
- SizedBox(
- height: 360,
- child: CurvePlot(series: series, equalAspect: true),
- ),
- ),
- ToolText(
- '方程',
- 'x = $xRaw,y = $yRaw;t ∈ [$tMinRaw, $tMaxRaw],'
- '步长 $stepRaw(共 $n 个采样点)',
- ),
- ];
- },
- );
- }
|