| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- // tools/parametric_plotter.dart
- import 'package:flutter/material.dart';
- import 'package:provider/provider.dart';
- import '../curve_plot.dart';
- import '../expr_syntax.dart';
- import '../symja.dart';
- /// 参数方程绘图:x = f(t), y = g(t),用 Symja 采样后交给 [CurvePlot] 画。
- ///
- /// 采样要点:Symja 的 `sample` 一次只能算一个表达式,所以 x(t) 和 y(t) 各调用
- /// 一次,参数区间和点数完全一致,再把两个结果按下标配成 (x, y) 点。
- class ParametricPlotterPage extends StatefulWidget {
- const ParametricPlotterPage({super.key});
- @override
- State<ParametricPlotterPage> createState() => _ParametricPlotterPageState();
- }
- class _ParametricPlotterPageState extends State<ParametricPlotterPage> {
- final TextEditingController _xCtrl = TextEditingController(text: 'cos(t)');
- final TextEditingController _yCtrl = TextEditingController(text: 'sin(t)');
- final TextEditingController _tMinCtrl = TextEditingController(text: '0');
- final TextEditingController _tMaxCtrl = TextEditingController(text: '6.3');
- final TextEditingController _stepCtrl = TextEditingController(text: '0.1');
- List<CurveSeries> _series = const <CurveSeries>[];
- String? _error;
- bool _busy = false;
- @override
- void initState() {
- super.initState();
- // 首帧后再绘制:context.read 需要 element 已经挂载到 Provider 之下。
- WidgetsBinding.instance.addPostFrameCallback((_) => _plot());
- }
- @override
- void dispose() {
- _xCtrl.dispose();
- _yCtrl.dispose();
- _tMinCtrl.dispose();
- _tMaxCtrl.dispose();
- _stepCtrl.dispose();
- super.dispose();
- }
- Future<void> _plot() async {
- if (_busy) return;
- final xRaw = _xCtrl.text.trim();
- final yRaw = _yCtrl.text.trim();
- if (xRaw.isEmpty || yRaw.isEmpty) {
- setState(() => _error = '请填写 x(t) 和 y(t) 表达式');
- return;
- }
- final tMin = double.tryParse(_tMinCtrl.text.trim());
- final tMax = double.tryParse(_tMaxCtrl.text.trim());
- final step = double.tryParse(_stepCtrl.text.trim());
- if (tMin == null || tMax == null || step == null) {
- setState(() => _error = '请输入有效的 t 范围与步长');
- return;
- }
- if (tMin >= tMax) {
- setState(() => _error = 't最小值必须小于t最大值');
- return;
- }
- if (step <= 0) {
- setState(() => _error = '步长必须大于0');
- return;
- }
- // Symja 的 sample 只接受「点数」,这里把用户给的步长换算成点数,
- // 保留步长输入的含义;再做上下限保护,避免点数过多拖垮求值。
- final count = ((tMax - tMin) / step).round() + 1;
- final n = count.clamp(2, 5000).toInt();
- final session = context.read<SymjaSession>();
- setState(() {
- _busy = true;
- _error = null;
- });
- List<CurveSeries> series = const <CurveSeries>[];
- try {
- // 同一个 t 区间、同一点数分别采样两条曲线——Symja 一次只能算一个表达式。
- final xs = await session.sample(
- toSymjaSyntax(xRaw),
- variable: 't',
- from: tMin,
- to: tMax,
- count: n,
- );
- final ys = await session.sample(
- toSymjaSyntax(yRaw),
- variable: 't',
- from: tMin,
- to: tMax,
- count: n,
- );
- if (!mounted) return;
- 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) {
- if (!mounted) return;
- setState(() {
- _error = '$e';
- _busy = false;
- });
- return;
- }
- if (!mounted) return;
- setState(() {
- _series = series;
- _busy = false;
- });
- }
- void _applyPreset(String xExpr, String yExpr, String tMin, String tMax) {
- setState(() {
- _xCtrl.text = xExpr;
- _yCtrl.text = yExpr;
- _tMinCtrl.text = tMin;
- _tMaxCtrl.text = tMax;
- _stepCtrl.text = '0.1';
- });
- _plot();
- }
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: const Text('参数方程绘图', style: TextStyle(color: Colors.black)),
- backgroundColor: Colors.white,
- elevation: 1,
- ),
- body: SingleChildScrollView(
- child: Column(
- children: [
- _buildInputPanel(),
- _buildGraph(),
- _buildExamples(),
- ],
- ),
- ),
- );
- }
- Widget _buildInputPanel() {
- return Card(
- elevation: 4,
- margin: const EdgeInsets.all(16),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- '参数方程设置',
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 16),
- _buildExprField(_xCtrl, 'x = f(t)', '例如: cos(t), t^2, Cos[t]'),
- const SizedBox(height: 12),
- _buildExprField(_yCtrl, 'y = g(t)', '例如: sin(t), t^3, Sin[t]'),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(child: _buildNumberField(_tMinCtrl, 't最小值')),
- const SizedBox(width: 12),
- Expanded(child: _buildNumberField(_tMaxCtrl, 't最大值')),
- const SizedBox(width: 12),
- Expanded(child: _buildNumberField(_stepCtrl, '步长')),
- ],
- ),
- const SizedBox(height: 12),
- if (_error != null)
- Container(
- width: double.infinity,
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: Colors.red[50],
- borderRadius: BorderRadius.circular(8),
- border: Border.all(color: Colors.red),
- ),
- child: Text(_error!, style: const TextStyle(color: Colors.red)),
- ),
- const SizedBox(height: 16),
- ElevatedButton.icon(
- onPressed: _busy ? null : _plot,
- icon: _busy
- ? const SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(strokeWidth: 2),
- )
- : const Icon(Icons.show_chart),
- label: Text(_busy ? '绘制中…' : '绘制图像'),
- style: ElevatedButton.styleFrom(
- minimumSize: const Size(double.infinity, 50),
- ),
- ),
- ],
- ),
- ),
- );
- }
- /// 表达式输入框 + 语法转换提示(只有真的改写了才显示)。
- Widget _buildExprField(
- TextEditingController controller,
- String label,
- String hint,
- ) {
- final raw = controller.text.trim();
- final rewrite = raw.isEmpty ? null : symjaRewriteOf(raw);
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- TextField(
- controller: controller,
- // 输入时重建,让下面的语法转换提示实时更新。
- onChanged: (_) => setState(() {}),
- decoration: InputDecoration(
- labelText: label,
- hintText: hint,
- border: const OutlineInputBorder(),
- ),
- ),
- if (rewrite != null)
- Padding(
- padding: const EdgeInsets.only(top: 4, left: 4),
- child: Text(
- '已按 Symja 语法解释为: $rewrite',
- style: TextStyle(fontSize: 11, color: Colors.grey[600]),
- ),
- ),
- ],
- );
- }
- Widget _buildNumberField(TextEditingController controller, String label) {
- return TextField(
- controller: controller,
- decoration: InputDecoration(
- labelText: label,
- border: const OutlineInputBorder(),
- ),
- keyboardType: TextInputType.numberWithOptions(
- signed: true,
- decimal: true,
- ),
- );
- }
- Widget _buildGraph() {
- return Card(
- elevation: 4,
- margin: const EdgeInsets.all(16),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- '参数方程图像',
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 16),
- // 用 AspectRatio 强制正方形,参数曲线的 x/y 比例才不会被拉伸。
- AspectRatio(
- aspectRatio: 1,
- child: Container(
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey[300]!),
- borderRadius: BorderRadius.circular(8),
- ),
- child: _busy
- ? const Center(child: CircularProgressIndicator())
- : (_series.isEmpty
- ? const Center(
- child: Text(
- '暂无数据',
- style: TextStyle(fontSize: 16, color: Colors.grey),
- ),
- )
- : CurvePlot(series: _series)),
- ),
- ),
- const SizedBox(height: 16),
- Text(
- '方程: x = ${_xCtrl.text}, y = ${_yCtrl.text}',
- style: TextStyle(fontSize: 14, color: Colors.grey[600]),
- ),
- Text(
- '参数范围: t ∈ [${_tMinCtrl.text}, ${_tMaxCtrl.text}], 步长: ${_stepCtrl.text}',
- style: TextStyle(fontSize: 14, color: Colors.grey[600]),
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildExamples() {
- return Card(
- elevation: 4,
- margin: const EdgeInsets.all(16),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- '示例方程',
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 12),
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: [
- _buildExampleChip('椭圆', '2*cos(t)', 'sin(t)', '0', '6.3'),
- _buildExampleChip('螺旋线', 't*cos(t)', 't*sin(t)', '0', '12.6'),
- _buildExampleChip('抛物线', 't', 't^2', '-2', '2'),
- _buildExampleChip(
- '心形线',
- '2*cos(t)-cos(2*t)',
- '2*sin(t)-sin(2*t)',
- '0',
- '6.3',
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildExampleChip(
- String title,
- String xExpr,
- String yExpr,
- String tMin,
- String tMax,
- ) {
- return ActionChip(
- label: Text(title),
- onPressed: () => _applyPreset(xExpr, yExpr, tMin, tMax),
- backgroundColor: Colors.blue[50],
- );
- }
- }
|