import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; /// 与 Java 内核通信的通道。原生侧注册在 [MainActivity]。 const MethodChannel symjaChannel = MethodChannel('com.nonvioce.cal/symja'); /// 一次求值的结果。 class SymjaResult { /// Symja 的纯文本输出,例如 `x^3/3`。 final String text; /// 同一结果的 LaTeX 形式,例如 `\frac{{x}^{3}}{3}`。 /// /// 为 null 表示没生成(结果太长,或格式化失败)——此时只显示 [text]。 final String? tex; final int elapsedMs; const SymjaResult({required this.text, this.tex, required this.elapsedMs}); } /// 一条历史记录,成功或失败都有。 class SymjaEntry { final String input; final SymjaResult? result; final String? error; final DateTime at; SymjaEntry({required this.input, this.result, this.error, DateTime? at}) : at = at ?? DateTime.now(); bool get ok => result != null; } class SymjaVariable { final String name; final String value; const SymjaVariable(this.name, this.value); @override String toString() => '$name = $value'; } /// 采样结果(绘图用)。ys[i] 对应 x = from + i * (to - from) / (ys.length - 1)。 class SymjaSamples { final double from; final double to; /// 非数值结果(断点处的 ComplexInfinity / Indeterminate 等)是 NaN。 final List ys; final int elapsedMs; const SymjaSamples({ required this.from, required this.to, required this.ys, required this.elapsedMs, }); double xAt(int i) => ys.length <= 1 ? from : from + i * (to - from) / (ys.length - 1); } /// Symja 侧的失败。 class SymjaException implements Exception { final String message; const SymjaException(this.message); @override String toString() => message; } /// 会话 —— 所有 Symja 交互的唯一入口。 /// /// 真正的求值全部发生在 Java 侧的**同一条专用线程**上串行执行:Symja 的 /// EvalEngine 非线程安全,实测并发调用会算错(30/40 次异常)甚至空转卡死。 /// 这里只负责发起、取消、以及维护 UI 状态。 class SymjaSession extends ChangeNotifier { static const Duration defaultTimeout = Duration(seconds: 15); /// 历史,新的在后。 final List history = []; /// 当前的全局变量(由 [refreshVariables] 刷新)。 List variables = const []; /// 是否有求值正在进行。 bool busy = false; /// 保留多少条历史。 int maxHistory = 200; /// 结果用**近似值**还是**精确值**。默认精确。 /// /// 精确是 Symja 的天然行为(`1/3` 就是 `1/3`,`Sqrt[2]` 就是 `Sqrt[2]`), /// 也是符号计算的意义所在,所以默认不开。开起来时给表达式套一层 `N[expr, 30]`。 bool approximate = false; /// 近似模式下保留几位**小数**。 /// /// 15 位是刻意选的:再高就放不下了。`N[1/3, 30]` 会给出三十多个字符的长串, /// 手机上怎么看都显示不全(用户实测反馈)。15 位比一般科学计算器还多, /// 而且刚好一屏能看完。 int approximateDigits = 15; static final _assignment = RegExp(r'=(?!=)'); /// 按当前模式把表达式包成近似求值。 /// /// **跳过带赋值的表达式**:`a = 1/3` 包成 `N[a = 1/3]` 就变成"先赋值再取近似", /// 显示和存储都可能走样;`f[x_] := x^2` 更会被直接毁掉。 /// `=(?!=)` 恰好放过 `==`(方程),所以 `Solve[x^2-1==0, x]` 不受影响。 String _applyMode(String expr) { if (!approximate) return expr; if (_assignment.hasMatch(expr)) return expr; // 保留 k 位小数。用 Round 而不是 N[expr, k]:Symja 的 N 忽略第二个参数 return 'N[Round[$expr, 1/10^$approximateDigits]]'; } int _seq = 0; int? _running; /// 当前正在跑的任务 id,没有则为 null。 int? get runningTaskId => _running; /// 求值一个表达式。 /// /// [expr] 里的 `%` 由原生侧替换成上一次的用户可见结果(不是由 Symja 自己处理, /// 因为任何内部 eval 都会污染 Symja 的 `%`,见 SymjaBridge 的类注释)。 /// /// 失败时抛 [SymjaException],同时失败本身也会记进 [history]。 Future evaluate( String expr, { Duration timeout = defaultTimeout, bool wantTeX = true, }) async { final input = expr.trim(); if (input.isEmpty) { throw const SymjaException('表达式为空'); } final id = ++_seq; _running = id; busy = true; notifyListeners(); try { final m = await symjaChannel.invokeMapMethod( 'evaluate', { 'taskId': id, 'expr': _applyMode(input), 'timeoutMs': timeout.inMilliseconds, 'wantTeX': wantTeX, // 显式声明这不是预览:预览不更新 `%`,正式求值要更新 'preview': false, }, ); if (m == null || m['ok'] != true) { _record(input, error: '${m?['error'] ?? '原生侧返回了空结果'}'); throw SymjaException('${m?['error'] ?? '原生侧返回了空结果'}'); } final r = SymjaResult( text: '${m['text']}', tex: m['tex'] as String?, elapsedMs: (m['ms'] as num?)?.toInt() ?? 0, ); _record(input, result: r); return r; } on PlatformException catch (e) { final msg = '通道错误 ${e.code}: ${e.message}'; _record(input, error: msg); throw SymjaException(msg); } finally { if (_running == id) { _running = null; busy = false; notifyListeners(); } } } /// 实时预览:求值但**不记进历史、也不影响 `%`**。 /// /// 计算器边输入边预览走这条路(原生侧收到 preview=true 就不更新 lastOutput)。 /// 不动 [busy]——预览不该把界面变成"计算中"。 Future preview(String expr) async { final input = expr.trim(); if (input.isEmpty) return null; try { final m = await symjaChannel.invokeMapMethod( 'evaluate', { 'taskId': ++_seq, 'expr': _applyMode(input), 'timeoutMs': 4000, 'wantTeX': true, 'preview': true, }, ); if (m == null || m['ok'] != true) return null; return SymjaResult( text: '${m['text']}', tex: m['tex'] as String?, elapsedMs: (m['ms'] as num?)?.toInt() ?? 0, ); } on PlatformException { // 预览失败静默即可——用户按 = 的时候会看到真正的错误 return null; } } /// 求值但**不记历史、不动 `%`**,失败抛 [SymjaException]。 /// /// 工具页做内部计算用它:那些表达式不是用户在控制台里敲的,塞进历史只是噪音; /// 而 [preview] 会把错误吞掉,工具却需要把错误显示出来。 /// /// 默认**不套**顶栏那个「精确 / 近似」开关([applyMode]),工具要近似值时 /// 自己在表达式里写 `N[...]` —— 否则"精确值"那一条也会被改写成近似值, /// 两条输出就没区别了。 Future compute( String expr, { Duration timeout = const Duration(seconds: 20), bool wantTeX = true, bool applyMode = false, }) async { final input = expr.trim(); if (input.isEmpty) throw const SymjaException('表达式为空'); try { final m = await symjaChannel.invokeMapMethod( 'evaluate', { 'taskId': ++_seq, // 工具页传 applyMode: false —— 它在表达式里自己写 N[...], // 不该被计算器顶栏那个"精确 / 近似"开关二次改写(否则"精确值" // 那一条也会变成近似值,两条输出就没区别了)。 'expr': applyMode ? _applyMode(input) : input, 'timeoutMs': timeout.inMilliseconds, 'wantTeX': wantTeX, // preview=true 同时意味着原生侧不更新 lastOutput, // 所以工具算完不会顶掉用户在计算器上按 ANS 拿到的东西。 'preview': true, }, ); if (m == null || m['ok'] != true) { throw SymjaException('${m?['error'] ?? '未知错误'}'); } return SymjaResult( text: '${m['text']}', tex: m['tex'] as String?, elapsedMs: (m['ms'] as num?)?.toInt() ?? 0, ); } on PlatformException catch (e) { throw SymjaException('通道错误 ${e.code}: ${e.message}'); } } /// 取消当前求值。原生侧只是取消 Future,Symja 自身不一定能立刻中断 /// (复杂表达式可能要算完才返回)。 Future cancel() async { final id = _running; if (id == null) return; await symjaChannel.invokeMethod('cancel', { 'taskId': id, }); } /// 在 [from, to] 上均匀取 [count] 个点求值,用于绘图。 Future sample( String expr, { String variable = 'x', required double from, required double to, int count = 200, Duration timeout = const Duration(seconds: 20), }) async { final id = ++_seq; _running = id; busy = true; notifyListeners(); try { final m = await symjaChannel.invokeMapMethod('sample', { 'taskId': id, 'expr': expr.trim(), 'variable': variable, 'from': from, 'to': to, 'count': count, 'timeoutMs': timeout.inMilliseconds, }); if (m == null || m['ok'] != true) { throw SymjaException('${m?['error'] ?? '采样失败'}'); } final raw = (m['ys'] as List?) ?? const []; return SymjaSamples( from: from, to: to, ys: raw.map((e) => (e as num).toDouble()).toList(growable: false), elapsedMs: (m['ms'] as num?)?.toInt() ?? 0, ); } on PlatformException catch (e) { throw SymjaException('通道错误 ${e.code}: ${e.message}'); } finally { if (_running == id) { _running = null; busy = false; notifyListeners(); } } } /// 重新拉取全局变量列表。 Future refreshVariables() async { try { final m = await symjaChannel.invokeMapMethod( 'variables', {'taskId': 0}, ); final raw = (m?['vars'] as List?) ?? const []; variables = raw .map((e) => SymjaVariable('${(e as Map)['name']}', '${e['value']}')) .toList(growable: false); notifyListeners(); } on PlatformException { // 变量面板不是关键路径,失败就保持原状 } } Future clearVariable(String name) async { await symjaChannel.invokeMethod('clearVariable', { 'taskId': 0, 'name': name, }); await refreshVariables(); } /// 清空所有变量和 `%`。 Future clearAll() async { await symjaChannel.invokeMethod('clearAll', { 'taskId': 0, }); history.clear(); variables = const []; notifyListeners(); } void clearHistory() { history.clear(); notifyListeners(); } // ------------------------------------------------------------------ 本地消息 /// 把一条「不是 Symja 算出来的」输出记进历史(help 文本、命令反馈等), /// 这样控制台可以用同一套渲染,也能被一起持久化。 void addLocal(String input, {String? text, String? error}) { _record( input, result: text == null ? null : SymjaResult(text: text, tex: null, elapsedMs: 0), error: error, ); } /// 从存档恢复历史。不触发求值。 void restoreHistory(Iterable<({String input, String text, int ms})> entries) { for (final e in entries) { history.add( SymjaEntry( input: e.input, result: SymjaResult(text: e.text, tex: null, elapsedMs: e.ms), ), ); } while (history.length > maxHistory) { history.removeAt(0); } notifyListeners(); } // ------------------------------------------------------------------ 持久化 /// 导出当前会话里所有定义(赋值 + 函数定义)。 /// /// 原生侧用 ISymbol.fullDefinitionToString(),一个调用同时覆盖 `a = 5` 和 /// `f(x_) := x^2 + 1`(后者没有 assignedValue)。刻意不用 Save/Get —— 它们属于 /// IOFunctions,和真机上失效的 Names 同族。 Future> exportSession() async { try { final m = await symjaChannel.invokeMapMethod( 'exportSession', {'taskId': ++_seq}, ); final raw = (m?['defs'] as List?) ?? const []; return raw.map((e) => '$e').toList(growable: false); } on PlatformException { return const []; } } /// 回灌定义。返回 (成功条数, 失败明细)。单条失败不影响其它条。 Future<({int applied, List failed})> importSession( List defs, ) async { if (defs.isEmpty) return (applied: 0, failed: const []); try { final m = await symjaChannel.invokeMapMethod( 'importSession', {'taskId': ++_seq, 'defs': defs}, ); final failed = ((m?['failed'] as List?) ?? const []) .map((e) => '$e') .toList(growable: false); return (applied: (m?['applied'] as num?)?.toInt() ?? 0, failed: failed); } on PlatformException catch (e) { return (applied: 0, failed: ['通道错误: ${e.message}']); } } void _record(String input, {SymjaResult? result, String? error}) { history.add(SymjaEntry(input: input, result: result, error: error)); while (history.length > maxHistory) { history.removeAt(0); } notifyListeners(); } }