From 6590c33732ac4d15f2d8a4013af42f144d71ccb7 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 14 Sep 2025 14:08:23 +0800 Subject: [PATCH] :recycle: Break down the graph and solver --- lib/screens/calculator_home_page.dart | 262 +----------------------- lib/widgets/graph_card.dart | 279 ++++++++++++++++++++++++++ 2 files changed, 286 insertions(+), 255 deletions(-) create mode 100644 lib/widgets/graph_card.dart diff --git a/lib/screens/calculator_home_page.dart b/lib/screens/calculator_home_page.dart index 60d9d11..f2061a3 100644 --- a/lib/screens/calculator_home_page.dart +++ b/lib/screens/calculator_home_page.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import 'package:latext/latext.dart'; import 'package:simple_math_calc/models/calculation_step.dart'; import 'package:simple_math_calc/solver.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:simple_math_calc/calculator.dart'; -import 'package:simple_math_calc/parser.dart'; +import 'package:simple_math_calc/widgets/graph_card.dart'; import 'dart:math'; class CalculatorHomePage extends StatefulWidget { @@ -42,121 +40,6 @@ class _CalculatorHomePageState extends State { setState(() {}); } - /// 生成函数图表的点 - List _generatePlotPoints(String expression, double zoomFactor) { - try { - // 只处理 y=... 格式的函数 - String normalized = expression.replaceAll(' ', ''); - if (!normalized.toLowerCase().startsWith('y=')) { - return []; - } - String functionExpr = normalized.substring(2); - - // 如果表达式不包含 x,返回空列表 - if (!functionExpr.contains('x') && !functionExpr.contains('X')) { - return []; - } - - // 预处理表达式,确保格式正确 - functionExpr = functionExpr.replaceAll(' ', ''); - - // 在数字和变量之间插入乘号 - functionExpr = functionExpr.replaceAllMapped( - RegExp(r'(\d)([a-zA-Z])'), - (match) => '${match.group(1)}*${match.group(2)}', - ); - - // 在变量和数字之间插入乘号 (如 x2 -> x*2) - functionExpr = functionExpr.replaceAllMapped( - RegExp(r'([a-zA-Z])(\d)'), - (match) => '${match.group(1)}*${match.group(2)}', - ); - - // 解析表达式 - final parser = Parser(functionExpr); - final expr = parser.parse(); - - // 根据缩放因子动态调整范围和步长 - final range = 10.0 * zoomFactor; - final step = max(0.05, 0.2 / zoomFactor); // 缩放时步长更小,放大时步长更大 - - // 生成点 - List points = []; - for (double i = -range; i <= range; i += step) { - try { - // 替换变量 x 为当前值 - final substituted = expr.substitute('x', DoubleExpr(i)); - final evaluated = substituted.evaluate(); - - if (evaluated is DoubleExpr) { - final y = evaluated.value; - if (y.isFinite && !y.isNaN) { - points.add(FlSpot(i, y)); - } - } - } catch (e) { - // 跳过无法计算的点 - continue; - } - } - - // 如果没有足够的点,返回空列表 - if (points.length < 2) { - debugPrint('Generated ${points.length} dots'); - return []; - } - - // 排序点按 x 值 - points.sort((a, b) => a.x.compareTo(b.x)); - - debugPrint( - 'Generated ${points.length} dots with zoom factor $zoomFactor', - ); - return points; - } catch (e) { - debugPrint('Error generating plot points: $e'); - return []; - } - } - - /// 计算图表的数据范围 - ({double minX, double maxX, double minY, double maxY}) _calculateChartBounds( - List points, - double zoomFactor, - ) { - if (points.isEmpty) { - return ( - minX: -10 * zoomFactor, - maxX: 10 * zoomFactor, - minY: -50 * zoomFactor, - maxY: 50 * zoomFactor, - ); - } - - double minX = points.first.x; - double maxX = points.first.x; - double minY = points.first.y; - double maxY = points.first.y; - - for (final point in points) { - minX = min(minX, point.x); - maxX = max(maxX, point.x); - minY = min(minY, point.y); - maxY = max(maxY, point.y); - } - - // 添加边距 - final xPadding = (maxX - minX) * 0.1; - final yPadding = (maxY - minY) * 0.1; - - return ( - minX: minX - xPadding, - maxX: maxX + xPadding, - minY: minY - yPadding, - maxY: maxY + yPadding, - ); - } - void _solveEquation() { if (_controller.text.isEmpty) { return; @@ -211,142 +94,6 @@ class _CalculatorHomePageState extends State { }); } - // 构建函数图像卡片 - Widget _buildGraphCard() { - return ListView( - padding: EdgeInsets.only( - left: 16, - right: 16, - bottom: MediaQuery.of(context).padding.bottom + 16, - top: 16, - ), - children: [ - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 4), - child: Text( - '函数图像', - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: _zoomIn, - icon: Icon(Icons.zoom_in), - tooltip: '放大', - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, - ), - IconButton( - onPressed: _zoomOut, - icon: Icon(Icons.zoom_out), - tooltip: '缩小', - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, - ), - ], - ), - ], - ), - const SizedBox(height: 24), - SizedBox( - height: 340, - child: Builder( - builder: (context) { - final points = _generatePlotPoints( - _controller.text, - _zoomFactor, - ); - final bounds = _calculateChartBounds(points, _zoomFactor); - - return LineChart( - LineChartData( - gridData: FlGridData(show: true), - titlesData: FlTitlesData( - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 80, - getTitlesWidget: (value, meta) => - SideTitleWidget( - axisSide: meta.axisSide, - child: Text(value.toStringAsFixed(2)), - ), - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 24, - getTitlesWidget: (value, meta) => - SideTitleWidget( - axisSide: meta.axisSide, - child: Text(value.toStringAsFixed(2)), - ), - ), - ), - topTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - ), - borderData: FlBorderData( - show: true, - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), - ), - lineTouchData: LineTouchData( - enabled: true, - touchTooltipData: LineTouchTooltipData( - getTooltipItems: (touchedSpots) { - return touchedSpots.map((spot) { - return LineTooltipItem( - 'x = ${spot.x.toStringAsFixed(2)}', - const TextStyle(color: Colors.white), - ); - }).toList(); - }, - ), - ), - lineBarsData: [ - LineChartBarData( - spots: points, - isCurved: true, - color: Theme.of(context).colorScheme.primary, - barWidth: 3, - belowBarData: BarAreaData(show: false), - dotData: FlDotData(show: false), - ), - ], - minX: bounds.minX, - maxX: bounds.maxX, - minY: bounds.minY, - maxY: bounds.maxY, - ), - ); - }, - ), - ), - ], - ), - ), - ), - ], - ); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -391,7 +138,12 @@ class _CalculatorHomePageState extends State { child: _isLoading ? const Center(child: CircularProgressIndicator()) : _isFunctionMode - ? _buildGraphCard() + ? GraphCard( + expression: _controller.text, + zoomFactor: _zoomFactor, + onZoomIn: _zoomIn, + onZoomOut: _zoomOut, + ) : _result == null ? const Center(child: Text('请输入方程开始计算')) : buildResultView(_result!), diff --git a/lib/widgets/graph_card.dart b/lib/widgets/graph_card.dart new file mode 100644 index 0000000..cf5a484 --- /dev/null +++ b/lib/widgets/graph_card.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:simple_math_calc/parser.dart'; +import 'package:simple_math_calc/calculator.dart'; +import 'dart:math'; + +class GraphCard extends StatefulWidget { + final String expression; + final double zoomFactor; + final VoidCallback onZoomIn; + final VoidCallback onZoomOut; + + const GraphCard({ + super.key, + required this.expression, + required this.zoomFactor, + required this.onZoomIn, + required this.onZoomOut, + }); + + @override + State createState() => _GraphCardState(); +} + +class _GraphCardState extends State { + /// 生成函数图表的点 + List _generatePlotPoints(String expression, double zoomFactor) { + try { + // 只处理 y=... 格式的函数 + String normalized = expression.replaceAll(' ', ''); + if (!normalized.toLowerCase().startsWith('y=')) { + return []; + } + String functionExpr = normalized.substring(2); + + // 如果表达式不包含 x,返回空列表 + if (!functionExpr.contains('x') && !functionExpr.contains('X')) { + return []; + } + + // 预处理表达式,确保格式正确 + functionExpr = functionExpr.replaceAll(' ', ''); + + // 在数字和变量之间插入乘号 + functionExpr = functionExpr.replaceAllMapped( + RegExp(r'(\d)([a-zA-Z])'), + (match) => '${match.group(1)}*${match.group(2)}', + ); + + // 在变量和数字之间插入乘号 (如 x2 -> x*2) + functionExpr = functionExpr.replaceAllMapped( + RegExp(r'([a-zA-Z])(\d)'), + (match) => '${match.group(1)}*${match.group(2)}', + ); + + // 解析表达式 + final parser = Parser(functionExpr); + final expr = parser.parse(); + + // 根据缩放因子动态调整范围和步长 + final range = 10.0 * zoomFactor; + final step = max(0.05, 0.2 / zoomFactor); // 缩放时步长更小,放大时步长更大 + + // 生成点 + List points = []; + for (double i = -range; i <= range; i += step) { + try { + // 替换变量 x 为当前值 + final substituted = expr.substitute('x', DoubleExpr(i)); + final evaluated = substituted.evaluate(); + + if (evaluated is DoubleExpr) { + final y = evaluated.value; + if (y.isFinite && !y.isNaN) { + points.add(FlSpot(i, y)); + } + } + } catch (e) { + // 跳过无法计算的点 + continue; + } + } + + // 如果没有足够的点,返回空列表 + if (points.length < 2) { + debugPrint('Generated ${points.length} dots'); + return []; + } + + // 排序点按 x 值 + points.sort((a, b) => a.x.compareTo(b.x)); + + debugPrint( + 'Generated ${points.length} dots with zoom factor $zoomFactor', + ); + return points; + } catch (e) { + debugPrint('Error generating plot points: $e'); + return []; + } + } + + /// 计算图表的数据范围 + ({double minX, double maxX, double minY, double maxY}) _calculateChartBounds( + List points, + double zoomFactor, + ) { + if (points.isEmpty) { + return ( + minX: -10 * zoomFactor, + maxX: 10 * zoomFactor, + minY: -50 * zoomFactor, + maxY: 50 * zoomFactor, + ); + } + + double minX = points.first.x; + double maxX = points.first.x; + double minY = points.first.y; + double maxY = points.first.y; + + for (final point in points) { + minX = min(minX, point.x); + maxX = max(maxX, point.x); + minY = min(minY, point.y); + maxY = max(maxY, point.y); + } + + // 添加边距 + final xPadding = (maxX - minX) * 0.1; + final yPadding = (maxY - minY) * 0.1; + + return ( + minX: minX - xPadding, + maxX: maxX + xPadding, + minY: minY - yPadding, + maxY: maxY + yPadding, + ); + } + + @override + Widget build(BuildContext context) { + return ListView( + padding: EdgeInsets.only( + left: 16, + right: 16, + bottom: MediaQuery.of(context).padding.bottom + 16, + top: 16, + ), + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4), + child: Text( + '函数图像', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: widget.onZoomIn, + icon: Icon(Icons.zoom_in), + tooltip: '放大', + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ), + IconButton( + onPressed: widget.onZoomOut, + icon: Icon(Icons.zoom_out), + tooltip: '缩小', + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ), + ], + ), + ], + ), + const SizedBox(height: 24), + SizedBox( + height: 340, + child: Builder( + builder: (context) { + final points = _generatePlotPoints( + widget.expression, + widget.zoomFactor, + ); + final bounds = _calculateChartBounds( + points, + widget.zoomFactor, + ); + + return LineChart( + LineChartData( + gridData: FlGridData(show: true), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 80, + getTitlesWidget: (value, meta) => + SideTitleWidget( + axisSide: meta.axisSide, + child: Text(value.toStringAsFixed(2)), + ), + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 24, + getTitlesWidget: (value, meta) => + SideTitleWidget( + axisSide: meta.axisSide, + child: Text(value.toStringAsFixed(2)), + ), + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + ), + lineTouchData: LineTouchData( + enabled: true, + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (touchedSpots) { + return touchedSpots.map((spot) { + return LineTooltipItem( + 'x = ${spot.x.toStringAsFixed(2)}\ny = ${spot.y.toStringAsFixed(2)}', + const TextStyle(color: Colors.white), + ); + }).toList(); + }, + ), + ), + lineBarsData: [ + LineChartBarData( + spots: points, + isCurved: true, + color: Theme.of(context).colorScheme.primary, + barWidth: 3, + belowBarData: BarAreaData(show: false), + dotData: FlDotData(show: false), + ), + ], + minX: bounds.minX, + maxX: bounds.maxX, + minY: bounds.minY, + maxY: bounds.maxY, + ), + ); + }, + ), + ), + ], + ), + ), + ), + ], + ); + } +}