|
@@ -19,6 +19,14 @@ class FunctionPlotterPage extends StatefulWidget {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
|
|
+ /// 固定的采样区间与点数。
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// 以前这里是用户可以填的 x/y 范围;现在横纵范围由图下面的滑动条控制,
|
|
|
|
|
+ /// 输入框就用不上了。点取得密一些,放大到一小段时才有足够的点。
|
|
|
|
|
+ static const double _sampleFrom = -10;
|
|
|
|
|
+ static const double _sampleTo = 10;
|
|
|
|
|
+ static const int _sampleCount = 800;
|
|
|
|
|
+
|
|
|
static const int _maxCurves = 5;
|
|
static const int _maxCurves = 5;
|
|
|
|
|
|
|
|
/// 每条曲线的固定颜色,下标与输入框一一对应(沿用旧版配色)。
|
|
/// 每条曲线的固定颜色,下标与输入框一一对应(沿用旧版配色)。
|
|
@@ -33,22 +41,13 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
final List<TextEditingController> _exprs = <TextEditingController>[
|
|
final List<TextEditingController> _exprs = <TextEditingController>[
|
|
|
TextEditingController(text: 'x^2'),
|
|
TextEditingController(text: 'x^2'),
|
|
|
];
|
|
];
|
|
|
- final TextEditingController _xMin = TextEditingController(text: '-5');
|
|
|
|
|
- final TextEditingController _xMax = TextEditingController(text: '5');
|
|
|
|
|
- final TextEditingController _yMin = TextEditingController(text: '-10');
|
|
|
|
|
- final TextEditingController _yMax = TextEditingController(text: '10');
|
|
|
|
|
|
|
|
|
|
List<CurveSeries> _series = const <CurveSeries>[];
|
|
List<CurveSeries> _series = const <CurveSeries>[];
|
|
|
|
|
|
|
|
- /// 上一次绘制实际使用的 y 范围(没绘制过就是 null,交给 CurvePlot 自动定界)。
|
|
|
|
|
- double? _plotYMin;
|
|
|
|
|
- double? _plotYMax;
|
|
|
|
|
-
|
|
|
|
|
/// 每个输入框的错误信息,与 [_exprs] 下标对齐;null 表示该项正常。
|
|
/// 每个输入框的错误信息,与 [_exprs] 下标对齐;null 表示该项正常。
|
|
|
List<String?> _errors = const <String?>[];
|
|
List<String?> _errors = const <String?>[];
|
|
|
|
|
|
|
|
/// 坐标范围本身的错误(与某条曲线无关),显示在绘图区。
|
|
/// 坐标范围本身的错误(与某条曲线无关),显示在绘图区。
|
|
|
- String? _rangeError;
|
|
|
|
|
|
|
|
|
|
bool _busy = false;
|
|
bool _busy = false;
|
|
|
|
|
|
|
@@ -64,10 +63,6 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
for (final c in _exprs) {
|
|
for (final c in _exprs) {
|
|
|
c.dispose();
|
|
c.dispose();
|
|
|
}
|
|
}
|
|
|
- _xMin.dispose();
|
|
|
|
|
- _xMax.dispose();
|
|
|
|
|
- _yMin.dispose();
|
|
|
|
|
- _yMax.dispose();
|
|
|
|
|
super.dispose();
|
|
super.dispose();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -84,26 +79,9 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
Future<void> _plot() async {
|
|
Future<void> _plot() async {
|
|
|
if (_busy) return;
|
|
if (_busy) return;
|
|
|
|
|
|
|
|
- final xMin = double.tryParse(_xMin.text.trim());
|
|
|
|
|
- final xMax = double.tryParse(_xMax.text.trim());
|
|
|
|
|
- final yMin = double.tryParse(_yMin.text.trim());
|
|
|
|
|
- final yMax = double.tryParse(_yMax.text.trim());
|
|
|
|
|
-
|
|
|
|
|
- if (xMin == null || xMax == null || yMin == null || yMax == null) {
|
|
|
|
|
- setState(() => _rangeError = '请输入有效的坐标范围');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (xMin >= xMax || yMin >= yMax) {
|
|
|
|
|
- setState(() => _rangeError = '最小值必须小于最大值');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
final session = context.read<SymjaSession>();
|
|
final session = context.read<SymjaSession>();
|
|
|
setState(() {
|
|
setState(() {
|
|
|
_busy = true;
|
|
_busy = true;
|
|
|
- _rangeError = null;
|
|
|
|
|
- _plotYMin = yMin;
|
|
|
|
|
- _plotYMax = yMax;
|
|
|
|
|
// 每次重绘都重建,长度与输入框对齐,旧错误自然被清掉。
|
|
// 每次重绘都重建,长度与输入框对齐,旧错误自然被清掉。
|
|
|
_errors = List<String?>.filled(_exprs.length, null);
|
|
_errors = List<String?>.filled(_exprs.length, null);
|
|
|
});
|
|
});
|
|
@@ -117,12 +95,14 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
// 表达式**原样交给 Symja 解析** —— 不再自己转换,谁解析谁负责,
|
|
// 表达式**原样交给 Symja 解析** —— 不再自己转换,谁解析谁负责,
|
|
|
// 免得"工具里算得出来、控制台里又不对"。
|
|
// 免得"工具里算得出来、控制台里又不对"。
|
|
|
final expr = raw;
|
|
final expr = raw;
|
|
|
|
|
+ // 采样区间固定,不再由输入框决定 —— 横纵范围交给图下面的滑动条。
|
|
|
|
|
+ // 点取得密一些(800 个),这样放大到一小段时也还有足够的点。
|
|
|
final s = await session.sample(
|
|
final s = await session.sample(
|
|
|
expr,
|
|
expr,
|
|
|
variable: 'x',
|
|
variable: 'x',
|
|
|
- from: xMin,
|
|
|
|
|
- to: xMax,
|
|
|
|
|
- count: 240,
|
|
|
|
|
|
|
+ from: _sampleFrom,
|
|
|
|
|
+ to: _sampleTo,
|
|
|
|
|
+ count: _sampleCount,
|
|
|
);
|
|
);
|
|
|
if (!mounted) return;
|
|
if (!mounted) return;
|
|
|
result.add(CurveSeries(
|
|
result.add(CurveSeries(
|
|
@@ -198,40 +178,6 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
style: TextStyle(fontSize: 12, color: Colors.grey),
|
|
style: TextStyle(fontSize: 12, color: Colors.grey),
|
|
|
),
|
|
),
|
|
|
const SizedBox(height: 10),
|
|
const SizedBox(height: 10),
|
|
|
- LayoutBuilder(
|
|
|
|
|
- builder: (context, constraints) {
|
|
|
|
|
- final fields = <Widget>[
|
|
|
|
|
- _buildRangeField(_xMin, 'x最小值'),
|
|
|
|
|
- _buildRangeField(_xMax, 'x最大值'),
|
|
|
|
|
- _buildRangeField(_yMin, 'y最小值'),
|
|
|
|
|
- _buildRangeField(_yMax, 'y最大值'),
|
|
|
|
|
- ];
|
|
|
|
|
- // 窄屏竖向排,宽屏横向排。
|
|
|
|
|
- if (constraints.maxWidth < 600) {
|
|
|
|
|
- return Column(
|
|
|
|
|
- children: [
|
|
|
|
|
- for (var i = 0; i < fields.length; i++) ...[
|
|
|
|
|
- if (i > 0) const SizedBox(height: 10),
|
|
|
|
|
- fields[i],
|
|
|
|
|
- ],
|
|
|
|
|
- ],
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- return Row(
|
|
|
|
|
- children: [
|
|
|
|
|
- for (var i = 0; i < fields.length; i++) ...[
|
|
|
|
|
- if (i > 0) const SizedBox(width: 10),
|
|
|
|
|
- Expanded(child: fields[i]),
|
|
|
|
|
- ],
|
|
|
|
|
- ],
|
|
|
|
|
- );
|
|
|
|
|
- },
|
|
|
|
|
- ),
|
|
|
|
|
- const SizedBox(height: 4),
|
|
|
|
|
- const Text(
|
|
|
|
|
- '注: x 范围决定采样区间,y 范围决定显示窗口;图上还能双指缩放、拖动、双击复位。',
|
|
|
|
|
- style: TextStyle(fontSize: 11, color: Colors.grey),
|
|
|
|
|
- ),
|
|
|
|
|
const SizedBox(height: 10),
|
|
const SizedBox(height: 10),
|
|
|
Container(
|
|
Container(
|
|
|
constraints: const BoxConstraints(maxHeight: 240),
|
|
constraints: const BoxConstraints(maxHeight: 240),
|
|
@@ -317,21 +263,6 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Widget _buildRangeField(TextEditingController controller, String label) {
|
|
|
|
|
- return TextField(
|
|
|
|
|
- controller: controller,
|
|
|
|
|
- decoration: InputDecoration(
|
|
|
|
|
- labelText: label,
|
|
|
|
|
- border: const OutlineInputBorder(),
|
|
|
|
|
- contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
|
|
|
- ),
|
|
|
|
|
- keyboardType: TextInputType.numberWithOptions(
|
|
|
|
|
- signed: true,
|
|
|
|
|
- decimal: true,
|
|
|
|
|
- ),
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
Widget _buildPlotArea() {
|
|
Widget _buildPlotArea() {
|
|
|
return Card(
|
|
return Card(
|
|
|
elevation: 4,
|
|
elevation: 4,
|
|
@@ -355,11 +286,6 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
if (_busy) {
|
|
if (_busy) {
|
|
|
return const Center(child: CircularProgressIndicator());
|
|
return const Center(child: CircularProgressIndicator());
|
|
|
}
|
|
}
|
|
|
- if (_rangeError != null) {
|
|
|
|
|
- return Center(
|
|
|
|
|
- child: Text(_rangeError!, style: const TextStyle(color: Colors.red)),
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
if (_series.isEmpty) {
|
|
if (_series.isEmpty) {
|
|
|
return Center(
|
|
return Center(
|
|
|
child: Column(
|
|
child: Column(
|
|
@@ -376,6 +302,6 @@ class _FunctionPlotterPageState extends State<FunctionPlotterPage> {
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
- return CurvePlot(series: _series, yMin: _plotYMin, yMax: _plotYMax);
|
|
|
|
|
|
|
+ return CurvePlot(series: _series);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|