Compare commits

...

2 Commits

Author SHA1 Message Date
6590c33732 ♻️ Break down the graph and solver 2025-09-14 14:08:23 +08:00
587e243ee3 ♻️ Split up the function graph mode and solving mode 2025-09-14 14:03:02 +08:00
2 changed files with 300 additions and 240 deletions

View File

@@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
import 'package:latext/latext.dart'; import 'package:latext/latext.dart';
import 'package:simple_math_calc/models/calculation_step.dart'; import 'package:simple_math_calc/models/calculation_step.dart';
import 'package:simple_math_calc/solver.dart'; import 'package:simple_math_calc/solver.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:simple_math_calc/widgets/graph_card.dart';
import 'package:simple_math_calc/calculator.dart';
import 'package:simple_math_calc/parser.dart';
import 'dart:math'; import 'dart:math';
class CalculatorHomePage extends StatefulWidget { class CalculatorHomePage extends StatefulWidget {
@@ -21,6 +19,7 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> {
CalculationResult? _result; CalculationResult? _result;
bool _isLoading = false; bool _isLoading = false;
bool _isFunctionMode = false;
double _zoomFactor = 1.0; double _zoomFactor = 1.0;
@override @override
@@ -41,125 +40,23 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> {
setState(() {}); setState(() {});
} }
/// 生成函数图表的点
List<FlSpot> _generatePlotPoints(String expression, double zoomFactor) {
try {
// 如果是方程,取左边作为函数
String functionExpr = expression;
if (expression.contains('=')) {
functionExpr = expression.split('=')[0].trim();
}
// 如果表达式不包含 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<FlSpot> 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<FlSpot> 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() { void _solveEquation() {
if (_controller.text.isEmpty) { if (_controller.text.isEmpty) {
return; return;
} }
final input = _controller.text.trim();
final normalizedInput = input.replaceAll(' ', '');
if (normalizedInput.toLowerCase().startsWith('y=')) {
setState(() {
_isFunctionMode = true;
_result = null;
});
return;
}
setState(() { setState(() {
_isFunctionMode = false;
_isLoading = true; _isLoading = true;
_result = null; // 清除上次结果 _result = null; // 清除上次结果
}); });
@@ -240,6 +137,13 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> {
Expanded( Expanded(
child: _isLoading child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _isFunctionMode
? GraphCard(
expression: _controller.text,
zoomFactor: _zoomFactor,
onZoomIn: _zoomIn,
onZoomOut: _zoomOut,
)
: _result == null : _result == null
? const Center(child: Text('请输入方程开始计算')) ? const Center(child: Text('请输入方程开始计算'))
: buildResultView(_result!), : buildResultView(_result!),
@@ -354,129 +258,6 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> {
), ),
), ),
), ),
const SizedBox(height: 16),
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: 40,
getTitlesWidget: (value, meta) =>
SideTitleWidget(
axisSide: meta.axisSide,
child: Text(value.toStringAsFixed(2)),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
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,
),
);
},
),
),
],
),
),
),
], ],
); );
} }

279
lib/widgets/graph_card.dart Normal file
View File

@@ -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<GraphCard> createState() => _GraphCardState();
}
class _GraphCardState extends State<GraphCard> {
/// 生成函数图表的点
List<FlSpot> _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<FlSpot> 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<FlSpot> 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,
),
);
},
),
),
],
),
),
),
],
);
}
}