Преглед изворни кода

完成步骤 ④⑤⑥⑦:插件系统、科学计算器、绘图、MCP

④ 插件接口 + 迁移 mathtool2 的 16 个工具
   - lib/plugins.dart 是注册表(编译期模块,Dart AOT 没有运行时加载)
   - lib/tools/ 与 lib/utils/ 从 mathtool2 整包搬入,保持同样的相对路径
   - 命令入口按 Q26b 的约定留空待补(ToolPlugin.command)

⑤ 科学计算器(lib/calculator_page.dart)
   - 卡西欧式键位 + ∫ Σ d/dx lim solve 等 CAS 键(模板插入后光标落在括号里)
   - 变量键 x/n:没有它们就没法只用按钮写出 Integrate[x^2, x]
   - 实时预览走 preview 标志,刻意不污染 `%`
   - 函数面板检索 242 个 Symja 函数名(从 Ijs 的 SymjaEngine 抽出),常量亦可点

⑥ 曲线绘图组件(lib/curve_plot.dart)
   - 坐标轴/网格/刻度/多曲线图例/双指缩放/双击复位
   - 控制台 plot 命令与计算器的画图按钮共用,避免维护两套

⑦ MCP 服务器(只读 + 求值)
   - 从 Ijs 移植并剥离 Rhino、5 个插件工具、全部调试命令
   - 剩 11 个工具:evaluate/differentiate/integrate/solve/simplify/numeric/
     to_latex/session_variables/plot/parametric_plot/list_plot
   - 沿袭 Ijs 的 super("127.0.0.1", port),只监听本机

真机验证(vivo V2353A / Android 14):按钮拼出 Integrate[x^2, x] 并算出 x^3/3、
Sin[x] 绘图 240 点 41ms、MCP 经 adb forward 调通 evaluate 与 session_variables。

Co-Authored-By: Claude Code <noreply@anthropic.com>
nonvioce пре 1 месец
родитељ
комит
9a79671879

+ 0 - 1
android/app/src/main/kotlin/com/nonvioce/cal/MainActivity.kt

@@ -60,7 +60,6 @@ class MainActivity : FlutterActivity() {
                     else -> result.notImplemented()
                 }
             }
-    }
 
         // ------------------------------------------------------------------ MCP 服务器
         // 真正的服务器在 McpService(前台服务,保证退到后台也能被调用)。

+ 79 - 13
lib/calculator_page.dart

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_math_fork/flutter_math.dart';
 import 'package:provider/provider.dart';
 
+import 'curve_plot.dart';
 import 'symja.dart';
 import 'symja_functions.dart';
 
@@ -133,14 +134,56 @@ class _CalculatorPageState extends State<CalculatorPage> {
     await session.refreshVariables();
   }
 
+  /// 把当前表达式在 x ∈ [-6.3, 6.3] 上采样并画出来。
+  Future<void> _plotCurrent() async {
+    final expr = _ctrl.text.trim();
+    if (expr.isEmpty) return;
+    final session = context.read<SymjaSession>();
+    final messenger = ScaffoldMessenger.of(context);
+    try {
+      final s = await session.sample(expr, from: -6.3, to: 6.3, count: 240);
+      if (!mounted) return;
+      await showDialog<void>(
+        context: context,
+        builder: (_) => AlertDialog(
+          title: Text('$expr   (${s.ys.length} 点,${s.elapsedMs} ms)'),
+          content: SizedBox(
+            width: 360,
+            height: 280,
+            child: CurvePlot(series: [
+              CurveSeries(
+                label: expr,
+                color: const Color(0xFF3F51B5),
+                points: [
+                  for (var i = 0; i < s.ys.length; i++)
+                    Offset(s.xAt(i), s.ys[i]),
+                ],
+              ),
+            ]),
+          ),
+          actions: [
+            TextButton(
+              onPressed: () => Navigator.pop(context),
+              child: const Text('关闭'),
+            ),
+          ],
+        ),
+      );
+    } on SymjaException catch (e) {
+      messenger.showSnackBar(SnackBar(content: Text('$e')));
+    }
+  }
+
   Future<void> _showPalette() async {
-    final picked = await showModalBottomSheet<String>(
+    // 面板返回「要插入什么」+「光标从末尾回退几位」:
+    // 函数是 `Sin[]` 退 1(光标在括号里),常量是 `Pi` 退 0。
+    final picked = await showModalBottomSheet<({String text, int back})>(
       context: context,
       isScrollControlled: true,
       showDragHandle: true,
       builder: (_) => const _FunctionPalette(),
     );
-    if (picked != null) _insert('$picked[]', back: 1);
+    if (picked != null) _insert(picked.text, back: picked.back);
   }
 
   // ------------------------------------------------------------------ UI
@@ -151,6 +194,11 @@ class _CalculatorPageState extends State<CalculatorPage> {
       appBar: AppBar(
         title: const Text('计算器'),
         actions: [
+          IconButton(
+            tooltip: '画图(x ∈ [-6.3, 6.3])',
+            icon: const Icon(Icons.show_chart),
+            onPressed: _plotCurrent,
+          ),
           IconButton(
             tooltip: '函数面板',
             icon: const Icon(Icons.functions),
@@ -310,14 +358,16 @@ class _CalculatorPageState extends State<CalculatorPage> {
             _Key('0', insert: '0'),
             _Key('.', insert: '.'),
             _Key('^', insert: '^'),
-            _Key('ANS', insert: '%', tint: true),
-            _Key(',', insert: ', '),
+            // 变量键:没有它们就没法只用按钮写出 Integrate[x^2, x]。
+            // π / e 挪进了函数面板(那里的常量是可以点的)。
+            _Key('x', insert: 'x'),
+            _Key('n', insert: 'n'),
           ],
           [
             _Key('⌫', action: _KeyAction.backspace, tint: true),
             _Key('CLR', action: _KeyAction.clear, tint: true),
-            _Key('π', insert: 'Pi'),
-            _Key('e', insert: 'E'),
+            _Key(',', insert: ', '),
+            _Key('ANS', insert: '%', tint: true),
             _Key('=', action: _KeyAction.commit, primary: true),
           ],
         ];
@@ -467,14 +517,29 @@ class _FunctionPaletteState extends State<_FunctionPalette> {
               onChanged: (v) => setState(() => _q = v),
             ),
           ),
+          // 常量做成可点的 —— 它们从键盘上挪走了,得有个地方能按。
           Padding(
             padding: const EdgeInsets.symmetric(horizontal: 16),
-            child: Align(
-              alignment: Alignment.centerLeft,
-              child: Text(
-                '常量:${kSymjaConstants.join('  ')}',
-                style: Theme.of(context).textTheme.bodySmall,
-              ),
+            child: Wrap(
+              spacing: 8,
+              runSpacing: 4,
+              crossAxisAlignment: WrapCrossAlignment.center,
+              children: [
+                Text('常量',
+                    style: Theme.of(context).textTheme.bodySmall),
+                for (final c in kSymjaConstants)
+                  ActionChip(
+                    label: Text(
+                      c,
+                      style: const TextStyle(
+                          fontFamily: 'monospace', fontSize: 12),
+                    ),
+                    visualDensity: VisualDensity.compact,
+                    materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+                    onPressed: () =>
+                        Navigator.pop(context, (text: c, back: 0)),
+                  ),
+              ],
             ),
           ),
           const Divider(height: 16),
@@ -490,7 +555,8 @@ class _FunctionPaletteState extends State<_FunctionPalette> {
                         fns[i],
                         style: const TextStyle(fontFamily: 'monospace'),
                       ),
-                      onTap: () => Navigator.pop(context, fns[i]),
+                      onTap: () => Navigator.pop(
+                          context, (text: '${fns[i]}[]', back: 1)),
                     ),
                   ),
           ),

+ 5 - 0
lib/console_page.dart

@@ -295,6 +295,11 @@ class _ConsolePageState extends State<ConsolePage> {
       appBar: AppBar(
         title: const Text('某人的计算器2'),
         actions: [
+          IconButton(
+            tooltip: 'MCP 服务器',
+            icon: const Icon(Icons.cloud_outlined),
+            onPressed: () => showMcpPanel(context),
+          ),
           IconButton(
             tooltip: '变量',
             icon: Badge(

+ 2 - 0
lib/symja.dart

@@ -124,6 +124,8 @@ class SymjaSession extends ChangeNotifier {
         'expr': input,
         'timeoutMs': timeout.inMilliseconds,
         'wantTeX': wantTeX,
+        // 显式声明这不是预览:预览不更新 `%`,正式求值要更新
+        'preview': false,
       });
 
       if (m == null || m['ok'] != true) {

+ 3 - 2
test/widget_test.dart

@@ -176,12 +176,13 @@ void main() {
   });
 
   group('工具页', () {
-    testWidgets('列出 16 个插件,并能搜索', (tester) async {
+    testWidgets('列出插件,并能按名字过滤', (tester) async {
       await pumpApp(tester);
       await goTo(tester, '工具');
 
+      // 网格是懒加载的,靠后的卡片在测试视口外,所以只断言前几个
+      expect(find.text('错位相减法'), findsOneWidget);
       expect(find.text('二项分布'), findsOneWidget);
-      expect(find.text('线性回归'), findsOneWidget);
 
       await tester.enterText(inPage(ToolsPage, find.byType(TextField)), '回归');
       await tester.pumpAndSettle();