| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- 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<double> 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<SymjaEntry> history = [];
- /// 当前的全局变量(由 [refreshVariables] 刷新)。
- List<SymjaVariable> 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<SymjaResult> 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<String, dynamic>(
- '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<SymjaResult?> preview(String expr) async {
- final input = expr.trim();
- if (input.isEmpty) return null;
- try {
- final m = await symjaChannel.invokeMapMethod<String, dynamic>(
- '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<SymjaResult> 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<String, dynamic>(
- '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<void> cancel() async {
- final id = _running;
- if (id == null) return;
- await symjaChannel.invokeMethod<Object?>('cancel', <String, dynamic>{
- 'taskId': id,
- });
- }
- /// 在 [from, to] 上均匀取 [count] 个点求值,用于绘图。
- Future<SymjaSamples> 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<String, dynamic>('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<void> refreshVariables() async {
- try {
- final m = await symjaChannel.invokeMapMethod<String, dynamic>(
- 'variables',
- <String, dynamic>{'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<void> clearVariable(String name) async {
- await symjaChannel.invokeMethod<Object?>('clearVariable', <String, dynamic>{
- 'taskId': 0,
- 'name': name,
- });
- await refreshVariables();
- }
- /// 清空所有变量和 `%`。
- Future<void> clearAll() async {
- await symjaChannel.invokeMethod<Object?>('clearAll', <String, dynamic>{
- '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<List<String>> exportSession() async {
- try {
- final m = await symjaChannel.invokeMapMethod<String, dynamic>(
- 'exportSession',
- <String, dynamic>{'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<String> failed})> importSession(
- List<String> defs,
- ) async {
- if (defs.isEmpty) return (applied: 0, failed: const <String>[]);
- try {
- final m = await symjaChannel.invokeMapMethod<String, dynamic>(
- 'importSession',
- <String, dynamic>{'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();
- }
- }
|