parametric_plotter.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. // tools/parametric_plotter.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:math_expressions/math_expressions.dart';
  4. import 'package:fl_chart/fl_chart.dart';
  5. class ParametricPlotterPage extends StatefulWidget {
  6. @override
  7. _ParametricPlotterPageState createState() => _ParametricPlotterPageState();
  8. }
  9. class _ParametricPlotterPageState extends State<ParametricPlotterPage> {
  10. final TextEditingController xController = TextEditingController(text: 'cos(t)');
  11. final TextEditingController yController = TextEditingController(text: 'sin(t)');
  12. final TextEditingController tMinController = TextEditingController(text: '0');
  13. final TextEditingController tMaxController = TextEditingController(text: '6.3');
  14. final TextEditingController stepController = TextEditingController(text: '0.1');
  15. List<FlSpot> points = [];
  16. String errorMessage = '';
  17. bool isLoading = false;
  18. double minX = 0;
  19. double maxX = 0;
  20. double minY = 0;
  21. double maxY = 0;
  22. @override
  23. void initState() {
  24. super.initState();
  25. WidgetsBinding.instance.addPostFrameCallback((_) {
  26. _plotGraph();
  27. });
  28. }
  29. void _plotGraph() {
  30. setState(() {
  31. isLoading = true;
  32. errorMessage = '';
  33. });
  34. try {
  35. final tMin = double.parse(tMinController.text);
  36. final tMax = double.parse(tMaxController.text);
  37. final step = double.parse(stepController.text);
  38. if (tMin >= tMax) {
  39. throw Exception('t最小值必须小于t最大值');
  40. }
  41. if (step <= 0) {
  42. throw Exception('步长必须大于0');
  43. }
  44. final xExpr = _parseExpression(xController.text);
  45. final yExpr = _parseExpression(yController.text);
  46. List<FlSpot> newPoints = [];
  47. double currentMinX = double.infinity;
  48. double currentMaxX = -double.infinity;
  49. double currentMinY = double.infinity;
  50. double currentMaxY = -double.infinity;
  51. for (double t = tMin; t <= tMax; t += step) {
  52. try {
  53. final x = _evaluateExpression(xExpr, t);
  54. final y = _evaluateExpression(yExpr, t);
  55. if (x.isFinite && y.isFinite) {
  56. newPoints.add(FlSpot(x, y));
  57. currentMinX = x < currentMinX ? x : currentMinX;
  58. currentMaxX = x > currentMaxX ? x : currentMaxX;
  59. currentMinY = y < currentMinY ? y : currentMinY;
  60. currentMaxY = y > currentMaxY ? y : currentMaxY;
  61. }
  62. } catch (e) {
  63. // 跳过无效点
  64. }
  65. }
  66. if (newPoints.isEmpty) {
  67. throw Exception('未生成有效数据点,请检查参数方程');
  68. }
  69. setState(() {
  70. points = newPoints;
  71. minX = currentMinX;
  72. maxX = currentMaxX;
  73. minY = currentMinY;
  74. maxY = currentMaxY;
  75. });
  76. } catch (e) {
  77. setState(() {
  78. errorMessage = e.toString();
  79. points = [];
  80. });
  81. } finally {
  82. setState(() {
  83. isLoading = false;
  84. });
  85. }
  86. }
  87. Expression _parseExpression(String expression) {
  88. // 清理表达式中的空格
  89. String cleaned = expression.replaceAll(' ', '');
  90. // 将常见函数名转换为math_expressions识别的格式
  91. cleaned = cleaned.replaceAllMapped(RegExp(r'(\d)([a-zA-Z])'), (match) {
  92. return '${match.group(1)}*${match.group(2)}';
  93. });
  94. // 移除这行,因为math_expressions使用^作为幂运算符
  95. // cleaned = cleaned.replaceAll('^', '**');
  96. try {
  97. Parser p = Parser();
  98. return p.parse(cleaned);
  99. } catch (e) {
  100. throw Exception('表达式解析错误: $e');
  101. }
  102. }
  103. double _evaluateExpression(Expression expr, double t) {
  104. ContextModel cm = ContextModel()
  105. ..bindVariable(Variable('t'), Number(t))
  106. ..bindVariable(Variable('pi'), Number(3.141592653589793))
  107. ..bindVariable(Variable('e'), Number(2.718281828459045));
  108. try {
  109. return expr.evaluate(EvaluationType.REAL, cm);
  110. } catch (e) {
  111. throw Exception('在 t=$t 时计算错误: $e');
  112. }
  113. }
  114. Widget _buildInputPanel() {
  115. return Card(
  116. elevation: 4,
  117. margin: EdgeInsets.all(16),
  118. child: Padding(
  119. padding: EdgeInsets.all(16),
  120. child: Column(
  121. crossAxisAlignment: CrossAxisAlignment.start,
  122. children: [
  123. Text(
  124. '参数方程设置',
  125. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  126. ),
  127. SizedBox(height: 16),
  128. TextField(
  129. controller: xController,
  130. decoration: InputDecoration(
  131. labelText: 'x = f(t)',
  132. hintText: '例如: cos(t), t^2, 2*t+1',
  133. border: OutlineInputBorder(),
  134. ),
  135. ),
  136. SizedBox(height: 12),
  137. TextField(
  138. controller: yController,
  139. decoration: InputDecoration(
  140. labelText: 'y = g(t)',
  141. hintText: '例如: sin(t), t^3, 3*t-2',
  142. border: OutlineInputBorder(),
  143. ),
  144. ),
  145. SizedBox(height: 16),
  146. Row(
  147. children: [
  148. Expanded(
  149. child: TextField(
  150. controller: tMinController,
  151. decoration: InputDecoration(
  152. labelText: 't最小值',
  153. border: OutlineInputBorder(),
  154. ),
  155. keyboardType: TextInputType.numberWithOptions(decimal: true),
  156. ),
  157. ),
  158. SizedBox(width: 12),
  159. Expanded(
  160. child: TextField(
  161. controller: tMaxController,
  162. decoration: InputDecoration(
  163. labelText: 't最大值',
  164. border: OutlineInputBorder(),
  165. ),
  166. keyboardType: TextInputType.numberWithOptions(decimal: true),
  167. ),
  168. ),
  169. SizedBox(width: 12),
  170. Expanded(
  171. child: TextField(
  172. controller: stepController,
  173. decoration: InputDecoration(
  174. labelText: '步长',
  175. border: OutlineInputBorder(),
  176. ),
  177. keyboardType: TextInputType.numberWithOptions(decimal: true),
  178. ),
  179. ),
  180. ],
  181. ),
  182. SizedBox(height: 16),
  183. if (errorMessage.isNotEmpty)
  184. Container(
  185. width: double.infinity,
  186. padding: EdgeInsets.all(12),
  187. decoration: BoxDecoration(
  188. color: Colors.red[50],
  189. borderRadius: BorderRadius.circular(8),
  190. border: Border.all(color: Colors.red),
  191. ),
  192. child: Text(
  193. errorMessage,
  194. style: TextStyle(color: Colors.red),
  195. ),
  196. ),
  197. SizedBox(height: 16),
  198. ElevatedButton.icon(
  199. onPressed: _plotGraph,
  200. icon: Icon(Icons.show_chart),
  201. label: Text('绘制图像'),
  202. style: ElevatedButton.styleFrom(
  203. minimumSize: Size(double.infinity, 50),
  204. ),
  205. ),
  206. ],
  207. ),
  208. ),
  209. );
  210. }
  211. Widget _buildGraph() {
  212. if (points.isEmpty) {
  213. return Container(
  214. height: 300,
  215. child: Center(
  216. child: Text(
  217. '暂无数据',
  218. style: TextStyle(fontSize: 16, color: Colors.grey),
  219. ),
  220. ),
  221. );
  222. }
  223. // 计算坐标轴范围,确保x和y轴有相同的缩放比例
  224. double rangeX = maxX - minX;
  225. double rangeY = maxY - minY;
  226. double maxRange = rangeX > rangeY ? rangeX : rangeY;
  227. double centerX = (minX + maxX) / 2;
  228. double centerY = (minY + maxY) / 2;
  229. double adjustedMinX = centerX - maxRange / 2;
  230. double adjustedMaxX = centerX + maxRange / 2;
  231. double adjustedMinY = centerY - maxRange / 2;
  232. double adjustedMaxY = centerY + maxRange / 2;
  233. // 添加一些边距
  234. double margin = maxRange * 0.1;
  235. adjustedMinX -= margin;
  236. adjustedMaxX += margin;
  237. adjustedMinY -= margin;
  238. adjustedMaxY += margin;
  239. return Card(
  240. elevation: 4,
  241. margin: EdgeInsets.all(16),
  242. child: Padding(
  243. padding: EdgeInsets.all(16),
  244. child: Column(
  245. crossAxisAlignment: CrossAxisAlignment.start,
  246. children: [
  247. Text(
  248. '参数方程图像',
  249. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  250. ),
  251. SizedBox(height: 16),
  252. // 修复:使用 AspectRatio 确保图表始终保持正方形
  253. AspectRatio(
  254. aspectRatio: 1, // 强制宽高比为1:1
  255. child: Container(
  256. decoration: BoxDecoration(
  257. border: Border.all(color: Colors.grey[300]!),
  258. borderRadius: BorderRadius.circular(8),
  259. ),
  260. child: LineChart(
  261. LineChartData(
  262. gridData: FlGridData(show: true),
  263. titlesData: FlTitlesData(
  264. show: true,
  265. bottomTitles: AxisTitles(
  266. sideTitles: SideTitles(showTitles: true),
  267. ),
  268. leftTitles: AxisTitles(
  269. sideTitles: SideTitles(showTitles: true),
  270. ),
  271. rightTitles: AxisTitles(
  272. sideTitles: SideTitles(showTitles: false),
  273. ),
  274. topTitles: AxisTitles(
  275. sideTitles: SideTitles(showTitles: false),
  276. ),
  277. ),
  278. borderData: FlBorderData(show: true),
  279. minX: adjustedMinX,
  280. maxX: adjustedMaxX,
  281. minY: adjustedMinY,
  282. maxY: adjustedMaxY,
  283. lineTouchData: LineTouchData(enabled: false),
  284. lineBarsData: [
  285. LineChartBarData(
  286. spots: points,
  287. isCurved: true,
  288. color: Colors.blue,
  289. barWidth: 3,
  290. dotData: FlDotData(show: false),
  291. belowBarData: BarAreaData(show: false),
  292. ),
  293. ],
  294. ),
  295. ),
  296. ),
  297. ),
  298. SizedBox(height: 16),
  299. Text(
  300. '方程: x = ${xController.text}, y = ${yController.text}',
  301. style: TextStyle(fontSize: 14, color: Colors.grey[600]),
  302. ),
  303. Text(
  304. '参数范围: t ∈ [${tMinController.text}, ${tMaxController.text}], 步长: ${stepController.text}',
  305. style: TextStyle(fontSize: 14, color: Colors.grey[600]),
  306. ),
  307. ],
  308. ),
  309. ),
  310. );
  311. }
  312. @override
  313. Widget build(BuildContext context) {
  314. return Scaffold(
  315. appBar: AppBar(
  316. title: Text('参数方程绘图', style: TextStyle(color: Colors.black)),
  317. backgroundColor: Colors.white,
  318. elevation: 1,
  319. ),
  320. body: isLoading
  321. ? Center(child: CircularProgressIndicator())
  322. : SingleChildScrollView(
  323. child: Column(
  324. children: [
  325. _buildInputPanel(),
  326. _buildGraph(),
  327. _buildExamples(),
  328. ],
  329. ),
  330. ),
  331. );
  332. }
  333. Widget _buildExamples() {
  334. return Card(
  335. elevation: 4,
  336. margin: EdgeInsets.all(16),
  337. child: Padding(
  338. padding: EdgeInsets.all(16),
  339. child: Column(
  340. crossAxisAlignment: CrossAxisAlignment.start,
  341. children: [
  342. Text(
  343. '示例方程',
  344. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  345. ),
  346. SizedBox(height: 12),
  347. Wrap(
  348. spacing: 8,
  349. runSpacing: 8,
  350. children: [
  351. // 删除了圆的示例
  352. _buildExampleChip('椭圆', '2*cos(t)', 'sin(t)', '0', '6.3'),
  353. _buildExampleChip('螺旋线', 't*cos(t)', 't*sin(t)', '0', '12.6'),
  354. _buildExampleChip('抛物线', 't', 't^2', '-2', '2'),
  355. _buildExampleChip('心形线', '2*cos(t)-cos(2*t)', '2*sin(t)-sin(2*t)', '0', '6.3'),
  356. ],
  357. ),
  358. ],
  359. ),
  360. ),
  361. );
  362. }
  363. Widget _buildExampleChip(String title, String xExpr, String yExpr, String tMin, String tMax) {
  364. return ActionChip(
  365. label: Text(title),
  366. onPressed: () {
  367. setState(() {
  368. xController.text = xExpr;
  369. yController.text = yExpr;
  370. tMinController.text = tMin;
  371. tMaxController.text = tMax;
  372. stepController.text = '0.1';
  373. });
  374. _plotGraph();
  375. },
  376. backgroundColor: Colors.blue[50],
  377. );
  378. }
  379. }