From 91bb1f77ba1fe49e2ee824116d43bbd56f5448a6 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 14 Sep 2025 17:37:45 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Seprate=20calculate,=20=E5=8F=8D?= =?UTF-8?q?=E6=AF=94=E4=BE=8B=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/calculator.dart | 26 +++- lib/screens/calculator_home_page.dart | 4 - lib/widgets/graph_card.dart | 176 ++++++++++++++++++++++---- pubspec.yaml | 2 +- 4 files changed, 176 insertions(+), 32 deletions(-) diff --git a/lib/calculator.dart b/lib/calculator.dart index 35aedea..81b1de5 100644 --- a/lib/calculator.dart +++ b/lib/calculator.dart @@ -80,11 +80,18 @@ class FractionExpr extends Expr { final int denominator; FractionExpr(this.numerator, this.denominator) { - if (denominator == 0) throw Exception("分母不能为0"); + // Allow denominator 0 to handle division by zero } @override Expr simplify() { + if (denominator == 0) { + if (numerator == 0) return DoubleExpr(double.nan); + return DoubleExpr( + numerator.isNegative ? double.negativeInfinity : double.infinity, + ); + } + int g = _gcd(numerator.abs(), denominator.abs()); int n = numerator ~/ g; int d = denominator ~/ g; @@ -469,6 +476,23 @@ class DivExpr extends Expr { ).simplify(); } + // Handle DoubleExpr cases + if (l is DoubleExpr && r is DoubleExpr) { + return DoubleExpr(l.value / r.value); + } + if (l is IntExpr && r is DoubleExpr) { + return DoubleExpr(l.value.toDouble() / r.value); + } + if (l is DoubleExpr && r is IntExpr) { + return DoubleExpr(l.value / r.value.toDouble()); + } + if (l is FractionExpr && r is DoubleExpr) { + return DoubleExpr((l.numerator.toDouble() / l.denominator) / r.value); + } + if (l is DoubleExpr && r is FractionExpr) { + return DoubleExpr(l.value / (r.numerator.toDouble() / r.denominator)); + } + // handle (k * sqrt(X)) / d 约分 if (l is MulExpr && l.left is IntExpr && diff --git a/lib/screens/calculator_home_page.dart b/lib/screens/calculator_home_page.dart index 9a2a3f7..2e99428 100644 --- a/lib/screens/calculator_home_page.dart +++ b/lib/screens/calculator_home_page.dart @@ -146,10 +146,6 @@ class _CalculatorHomePageState extends State { floatingLabelAlignment: FloatingLabelAlignment.center, hintText: '例如: 2x^2 - 8x + 6 = 0', ), - keyboardType: TextInputType.numberWithOptions( - signed: true, - decimal: true, - ), onSubmitted: (_) => _solveEquation(), ), ), diff --git a/lib/widgets/graph_card.dart b/lib/widgets/graph_card.dart index 68ae73f..9096328 100644 --- a/lib/widgets/graph_card.dart +++ b/lib/widgets/graph_card.dart @@ -28,9 +28,14 @@ class GraphCard extends StatefulWidget { class _GraphCardState extends State { final SolverService _solverService = SolverService(); FlSpot? _currentTouchedPoint; + final TextEditingController _xController = TextEditingController(); + double? _manualY; /// 生成函数图表的点 - List _generatePlotPoints(String expression, double zoomFactor) { + ({List leftPoints, List rightPoints}) _generatePlotPoints( + String expression, + double zoomFactor, + ) { try { // 使用solver准备函数表达式(展开因式形式) String functionExpr = _solverService.prepareFunctionForGraphing( @@ -39,7 +44,7 @@ class _GraphCardState extends State { // 如果表达式不包含 x,返回空列表 if (!functionExpr.contains('x') && !functionExpr.contains('X')) { - return []; + return (leftPoints: [], rightPoints: []); } // 预处理表达式,确保格式正确 @@ -69,11 +74,15 @@ class _GraphCardState extends State { // 根据缩放因子动态调整范围和步长 final range = 10.0 * zoomFactor; - final step = max(0.05, 0.2 / zoomFactor); // 缩放时步长更小,放大时步长更大 + final step = max(0.01, 0.05 / zoomFactor); // 更小的步长以获得更好的分辨率 // 生成点 - List points = []; + List leftPoints = []; + List rightPoints = []; for (double i = -range; i <= range; i += step) { + // 跳过 x = 0 以避免在 y=1/x 等函数中的奇点 + if (i.abs() < 1e-10) continue; + try { // 替换变量 x 为当前值 final substituted = expr.substitute('x', DoubleExpr(i)); @@ -81,8 +90,12 @@ class _GraphCardState extends State { if (evaluated is DoubleExpr) { final y = evaluated.value; - if (y.isFinite && !y.isNaN) { - points.add(FlSpot(i, y)); + if (y.isFinite && y.abs() <= 100.0) { + if (i < 0) { + leftPoints.add(FlSpot(i, y)); + } else { + rightPoints.add(FlSpot(i, y)); + } } } } catch (e) { @@ -91,22 +104,17 @@ class _GraphCardState extends State { } } - // 如果没有足够的点,返回空列表 - if (points.length < 2) { - debugPrint('Generated ${points.length} dots'); - return []; - } - // 排序点按 x 值 - points.sort((a, b) => a.x.compareTo(b.x)); + leftPoints.sort((a, b) => a.x.compareTo(b.x)); + rightPoints.sort((a, b) => a.x.compareTo(b.x)); debugPrint( - 'Generated ${points.length} dots with zoom factor $zoomFactor', + 'Generated ${leftPoints.length} left dots and ${rightPoints.length} right dots with zoom factor $zoomFactor', ); - return points; + return (leftPoints: leftPoints, rightPoints: rightPoints); } catch (e) { debugPrint('Error generating plot points: $e'); - return []; + return (leftPoints: [], rightPoints: []); } } @@ -136,6 +144,11 @@ class _GraphCardState extends State { maxY = max(maxY, point.y); } + // Limit y range to prevent extreme values from making the chart unreadable + const double maxYRange = 100.0; + if (maxY > maxYRange) maxY = maxYRange; + if (minY < -maxYRange) minY = -maxYRange; + // 添加边距 final xPadding = (maxX - minX) * 0.1; final yPadding = (maxY - minY) * 0.1; @@ -161,6 +174,61 @@ class _GraphCardState extends State { return value.toStringAsFixed(4); } + double? _calculateYForX(double x) { + try { + String functionExpr = _solverService.prepareFunctionForGraphing( + widget.expression, + ); + if (!functionExpr.contains('x') && !functionExpr.contains('X')) { + return null; + } + functionExpr = functionExpr.replaceAll(' ', ''); + functionExpr = functionExpr.replaceAllMapped( + RegExp(r'(\d)([a-zA-Z])'), + (match) => '${match.group(1)}*${match.group(2)}', + ); + functionExpr = functionExpr.replaceAllMapped( + RegExp(r'([a-zA-Z])(\d)'), + (match) => '${match.group(1)}*${match.group(2)}', + ); + functionExpr = functionExpr.replaceAllMapped( + RegExp(r'%([a-zA-Z\d])'), + (match) => '%*${match.group(1)}', + ); + final parser = Parser(functionExpr); + final expr = parser.parse(); + final substituted = expr.substitute('x', DoubleExpr(x)); + final evaluated = substituted.evaluate(); + if (evaluated is DoubleExpr && + evaluated.value.isFinite && + !evaluated.value.isNaN) { + return evaluated.value; + } + } catch (e) { + // Handle error + } + return 0 / 0; + } + + void _performCalculation() { + final x = double.tryParse(_xController.text); + if (x != null) { + setState(() { + _manualY = _calculateYForX(x); + }); + } else { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('请输入有效的数字'))); + } + } + + @override + void dispose() { + _xController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return ListView( @@ -212,12 +280,13 @@ class _GraphCardState extends State { height: 340, child: Builder( builder: (context) { - final points = _generatePlotPoints( + final (:leftPoints, :rightPoints) = _generatePlotPoints( widget.expression, widget.zoomFactor, ); + final allPoints = [...leftPoints, ...rightPoints]; final bounds = _calculateChartBounds( - points, + allPoints, widget.zoomFactor, ); @@ -315,14 +384,24 @@ class _GraphCardState extends State { ), ), lineBarsData: [ - LineChartBarData( - spots: points, - isCurved: true, - color: Theme.of(context).colorScheme.primary, - barWidth: 3, - belowBarData: BarAreaData(show: false), - dotData: FlDotData(show: false), - ), + if (leftPoints.isNotEmpty) + LineChartBarData( + spots: leftPoints, + isCurved: true, + color: Theme.of(context).colorScheme.primary, + barWidth: 3, + belowBarData: BarAreaData(show: false), + dotData: FlDotData(show: false), + ), + if (rightPoints.isNotEmpty) + LineChartBarData( + spots: rightPoints, + isCurved: true, + color: Theme.of(context).colorScheme.primary, + barWidth: 3, + belowBarData: BarAreaData(show: false), + dotData: FlDotData(show: false), + ), ], minX: bounds.minX, maxX: bounds.maxX, @@ -355,6 +434,51 @@ class _GraphCardState extends State { ], ), ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + controller: _xController, + decoration: InputDecoration( + labelText: '输入 x 值', + border: OutlineInputBorder(), + isDense: true, + ), + keyboardType: TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + onSubmitted: (_) => _performCalculation(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: _performCalculation, + icon: Icon(Icons.calculate_outlined), + tooltip: '计算 y', + ), + ], + ), + if (_manualY != null) + Container( + margin: const EdgeInsets.only(top: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: LaTexT( + laTeXCode: Text( + '\$\$x = ${double.parse(_xController.text).toStringAsFixed(4)},\\quad y = ${_manualY!.toStringAsFixed(4)}\$\$', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index e337fca..a1a8a6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+4 +version: 1.0.0+5 environment: sdk: ^3.9.2