// tools/parametric_plotter.dart import 'package:flutter/material.dart'; import 'package:math_expressions/math_expressions.dart'; import 'package:fl_chart/fl_chart.dart'; class ParametricPlotterPage extends StatefulWidget { @override _ParametricPlotterPageState createState() => _ParametricPlotterPageState(); } class _ParametricPlotterPageState extends State { final TextEditingController xController = TextEditingController(text: 'cos(t)'); final TextEditingController yController = TextEditingController(text: 'sin(t)'); final TextEditingController tMinController = TextEditingController(text: '0'); final TextEditingController tMaxController = TextEditingController(text: '6.3'); final TextEditingController stepController = TextEditingController(text: '0.1'); List points = []; String errorMessage = ''; bool isLoading = false; double minX = 0; double maxX = 0; double minY = 0; double maxY = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _plotGraph(); }); } void _plotGraph() { setState(() { isLoading = true; errorMessage = ''; }); try { final tMin = double.parse(tMinController.text); final tMax = double.parse(tMaxController.text); final step = double.parse(stepController.text); if (tMin >= tMax) { throw Exception('t最小值必须小于t最大值'); } if (step <= 0) { throw Exception('步长必须大于0'); } final xExpr = _parseExpression(xController.text); final yExpr = _parseExpression(yController.text); List newPoints = []; double currentMinX = double.infinity; double currentMaxX = -double.infinity; double currentMinY = double.infinity; double currentMaxY = -double.infinity; for (double t = tMin; t <= tMax; t += step) { try { final x = _evaluateExpression(xExpr, t); final y = _evaluateExpression(yExpr, t); if (x.isFinite && y.isFinite) { newPoints.add(FlSpot(x, y)); currentMinX = x < currentMinX ? x : currentMinX; currentMaxX = x > currentMaxX ? x : currentMaxX; currentMinY = y < currentMinY ? y : currentMinY; currentMaxY = y > currentMaxY ? y : currentMaxY; } } catch (e) { // 跳过无效点 } } if (newPoints.isEmpty) { throw Exception('未生成有效数据点,请检查参数方程'); } setState(() { points = newPoints; minX = currentMinX; maxX = currentMaxX; minY = currentMinY; maxY = currentMaxY; }); } catch (e) { setState(() { errorMessage = e.toString(); points = []; }); } finally { setState(() { isLoading = false; }); } } Expression _parseExpression(String expression) { // 清理表达式中的空格 String cleaned = expression.replaceAll(' ', ''); // 将常见函数名转换为math_expressions识别的格式 cleaned = cleaned.replaceAllMapped(RegExp(r'(\d)([a-zA-Z])'), (match) { return '${match.group(1)}*${match.group(2)}'; }); // 移除这行,因为math_expressions使用^作为幂运算符 // cleaned = cleaned.replaceAll('^', '**'); try { Parser p = Parser(); return p.parse(cleaned); } catch (e) { throw Exception('表达式解析错误: $e'); } } double _evaluateExpression(Expression expr, double t) { ContextModel cm = ContextModel() ..bindVariable(Variable('t'), Number(t)) ..bindVariable(Variable('pi'), Number(3.141592653589793)) ..bindVariable(Variable('e'), Number(2.718281828459045)); try { return expr.evaluate(EvaluationType.REAL, cm); } catch (e) { throw Exception('在 t=$t 时计算错误: $e'); } } Widget _buildInputPanel() { return Card( elevation: 4, margin: EdgeInsets.all(16), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '参数方程设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), SizedBox(height: 16), TextField( controller: xController, decoration: InputDecoration( labelText: 'x = f(t)', hintText: '例如: cos(t), t^2, 2*t+1', border: OutlineInputBorder(), ), ), SizedBox(height: 12), TextField( controller: yController, decoration: InputDecoration( labelText: 'y = g(t)', hintText: '例如: sin(t), t^3, 3*t-2', border: OutlineInputBorder(), ), ), SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: tMinController, decoration: InputDecoration( labelText: 't最小值', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), SizedBox(width: 12), Expanded( child: TextField( controller: tMaxController, decoration: InputDecoration( labelText: 't最大值', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), SizedBox(width: 12), Expanded( child: TextField( controller: stepController, decoration: InputDecoration( labelText: '步长', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), ], ), SizedBox(height: 16), if (errorMessage.isNotEmpty) Container( width: double.infinity, padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red), ), child: Text( errorMessage, style: TextStyle(color: Colors.red), ), ), SizedBox(height: 16), ElevatedButton.icon( onPressed: _plotGraph, icon: Icon(Icons.show_chart), label: Text('绘制图像'), style: ElevatedButton.styleFrom( minimumSize: Size(double.infinity, 50), ), ), ], ), ), ); } Widget _buildGraph() { if (points.isEmpty) { return Container( height: 300, child: Center( child: Text( '暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey), ), ), ); } // 计算坐标轴范围,确保x和y轴有相同的缩放比例 double rangeX = maxX - minX; double rangeY = maxY - minY; double maxRange = rangeX > rangeY ? rangeX : rangeY; double centerX = (minX + maxX) / 2; double centerY = (minY + maxY) / 2; double adjustedMinX = centerX - maxRange / 2; double adjustedMaxX = centerX + maxRange / 2; double adjustedMinY = centerY - maxRange / 2; double adjustedMaxY = centerY + maxRange / 2; // 添加一些边距 double margin = maxRange * 0.1; adjustedMinX -= margin; adjustedMaxX += margin; adjustedMinY -= margin; adjustedMaxY += margin; return Card( elevation: 4, margin: EdgeInsets.all(16), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '参数方程图像', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), SizedBox(height: 16), // 修复:使用 AspectRatio 确保图表始终保持正方形 AspectRatio( aspectRatio: 1, // 强制宽高比为1:1 child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(8), ), child: LineChart( LineChartData( gridData: FlGridData(show: true), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles(showTitles: true), ), leftTitles: AxisTitles( sideTitles: SideTitles(showTitles: true), ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData(show: true), minX: adjustedMinX, maxX: adjustedMaxX, minY: adjustedMinY, maxY: adjustedMaxY, lineTouchData: LineTouchData(enabled: false), lineBarsData: [ LineChartBarData( spots: points, isCurved: true, color: Colors.blue, barWidth: 3, dotData: FlDotData(show: false), belowBarData: BarAreaData(show: false), ), ], ), ), ), ), SizedBox(height: 16), Text( '方程: x = ${xController.text}, y = ${yController.text}', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), Text( '参数范围: t ∈ [${tMinController.text}, ${tMaxController.text}], 步长: ${stepController.text}', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('参数方程绘图', style: TextStyle(color: Colors.black)), backgroundColor: Colors.white, elevation: 1, ), body: isLoading ? Center(child: CircularProgressIndicator()) : SingleChildScrollView( child: Column( children: [ _buildInputPanel(), _buildGraph(), _buildExamples(), ], ), ), ); } Widget _buildExamples() { return Card( elevation: 4, margin: EdgeInsets.all(16), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '示例方程', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), 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: () { setState(() { xController.text = xExpr; yController.text = yExpr; tMinController.text = tMin; tMaxController.text = tMax; stepController.text = '0.1'; }); _plotGraph(); }, backgroundColor: Colors.blue[50], ); } }