symja.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import 'package:flutter/foundation.dart';
  2. import 'package:flutter/services.dart';
  3. /// 与 Java 内核通信的通道。原生侧注册在 [MainActivity]。
  4. const MethodChannel symjaChannel = MethodChannel('com.nonvioce.cal/symja');
  5. /// 一次求值的结果。
  6. class SymjaResult {
  7. /// Symja 的纯文本输出,例如 `x^3/3`。
  8. final String text;
  9. /// 同一结果的 LaTeX 形式,例如 `\frac{{x}^{3}}{3}`。
  10. ///
  11. /// 为 null 表示没生成(结果太长,或格式化失败)——此时只显示 [text]。
  12. final String? tex;
  13. final int elapsedMs;
  14. const SymjaResult({required this.text, this.tex, required this.elapsedMs});
  15. }
  16. /// 一条历史记录,成功或失败都有。
  17. class SymjaEntry {
  18. final String input;
  19. final SymjaResult? result;
  20. final String? error;
  21. final DateTime at;
  22. SymjaEntry({required this.input, this.result, this.error, DateTime? at})
  23. : at = at ?? DateTime.now();
  24. bool get ok => result != null;
  25. }
  26. class SymjaVariable {
  27. final String name;
  28. final String value;
  29. const SymjaVariable(this.name, this.value);
  30. @override
  31. String toString() => '$name = $value';
  32. }
  33. /// 采样结果(绘图用)。ys[i] 对应 x = from + i * (to - from) / (ys.length - 1)。
  34. class SymjaSamples {
  35. final double from;
  36. final double to;
  37. /// 非数值结果(断点处的 ComplexInfinity / Indeterminate 等)是 NaN。
  38. final List<double> ys;
  39. final int elapsedMs;
  40. const SymjaSamples({
  41. required this.from,
  42. required this.to,
  43. required this.ys,
  44. required this.elapsedMs,
  45. });
  46. double xAt(int i) =>
  47. ys.length <= 1 ? from : from + i * (to - from) / (ys.length - 1);
  48. }
  49. /// Symja 侧的失败。
  50. class SymjaException implements Exception {
  51. final String message;
  52. const SymjaException(this.message);
  53. @override
  54. String toString() => message;
  55. }
  56. /// 会话 —— 所有 Symja 交互的唯一入口。
  57. ///
  58. /// 真正的求值全部发生在 Java 侧的**同一条专用线程**上串行执行:Symja 的
  59. /// EvalEngine 非线程安全,实测并发调用会算错(30/40 次异常)甚至空转卡死。
  60. /// 这里只负责发起、取消、以及维护 UI 状态。
  61. class SymjaSession extends ChangeNotifier {
  62. static const Duration defaultTimeout = Duration(seconds: 15);
  63. /// 历史,新的在后。
  64. final List<SymjaEntry> history = [];
  65. /// 当前的全局变量(由 [refreshVariables] 刷新)。
  66. List<SymjaVariable> variables = const [];
  67. /// 是否有求值正在进行。
  68. bool busy = false;
  69. /// 保留多少条历史。
  70. int maxHistory = 200;
  71. int _seq = 0;
  72. int? _running;
  73. /// 当前正在跑的任务 id,没有则为 null。
  74. int? get runningTaskId => _running;
  75. /// 求值一个表达式。
  76. ///
  77. /// [expr] 里的 `%` 由原生侧替换成上一次的用户可见结果(不是由 Symja 自己处理,
  78. /// 因为任何内部 eval 都会污染 Symja 的 `%`,见 SymjaBridge 的类注释)。
  79. ///
  80. /// 失败时抛 [SymjaException],同时失败本身也会记进 [history]。
  81. Future<SymjaResult> evaluate(
  82. String expr, {
  83. Duration timeout = defaultTimeout,
  84. bool wantTeX = true,
  85. }) async {
  86. final input = expr.trim();
  87. if (input.isEmpty) {
  88. throw const SymjaException('表达式为空');
  89. }
  90. final id = ++_seq;
  91. _running = id;
  92. busy = true;
  93. notifyListeners();
  94. try {
  95. final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
  96. 'taskId': id,
  97. 'expr': input,
  98. 'timeoutMs': timeout.inMilliseconds,
  99. 'wantTeX': wantTeX,
  100. // 显式声明这不是预览:预览不更新 `%`,正式求值要更新
  101. 'preview': false,
  102. });
  103. if (m == null || m['ok'] != true) {
  104. _record(input, error: '${m?['error'] ?? '原生侧返回了空结果'}');
  105. throw SymjaException('${m?['error'] ?? '原生侧返回了空结果'}');
  106. }
  107. final r = SymjaResult(
  108. text: '${m['text']}',
  109. tex: m['tex'] as String?,
  110. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  111. );
  112. _record(input, result: r);
  113. return r;
  114. } on PlatformException catch (e) {
  115. final msg = '通道错误 ${e.code}: ${e.message}';
  116. _record(input, error: msg);
  117. throw SymjaException(msg);
  118. } finally {
  119. if (_running == id) {
  120. _running = null;
  121. busy = false;
  122. notifyListeners();
  123. }
  124. }
  125. }
  126. /// 实时预览:求值但**不记进历史、也不影响 `%`**。
  127. ///
  128. /// 计算器边输入边预览走这条路(原生侧收到 preview=true 就不更新 lastOutput)。
  129. /// 不动 [busy]——预览不该把界面变成"计算中"。
  130. Future<SymjaResult?> preview(String expr) async {
  131. final input = expr.trim();
  132. if (input.isEmpty) return null;
  133. try {
  134. final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
  135. 'taskId': ++_seq,
  136. 'expr': input,
  137. 'timeoutMs': 4000,
  138. 'wantTeX': true,
  139. 'preview': true,
  140. });
  141. if (m == null || m['ok'] != true) return null;
  142. return SymjaResult(
  143. text: '${m['text']}',
  144. tex: m['tex'] as String?,
  145. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  146. );
  147. } on PlatformException {
  148. // 预览失败静默即可——用户按 = 的时候会看到真正的错误
  149. return null;
  150. }
  151. }
  152. /// 取消当前求值。原生侧只是取消 Future,Symja 自身不一定能立刻中断
  153. /// (复杂表达式可能要算完才返回)。
  154. Future<void> cancel() async {
  155. final id = _running;
  156. if (id == null) return;
  157. await symjaChannel.invokeMethod<Object?>('cancel', <String, dynamic>{
  158. 'taskId': id,
  159. });
  160. }
  161. /// 在 [from, to] 上均匀取 [count] 个点求值,用于绘图。
  162. Future<SymjaSamples> sample(
  163. String expr, {
  164. String variable = 'x',
  165. required double from,
  166. required double to,
  167. int count = 200,
  168. Duration timeout = const Duration(seconds: 20),
  169. }) async {
  170. final id = ++_seq;
  171. _running = id;
  172. busy = true;
  173. notifyListeners();
  174. try {
  175. final m = await symjaChannel.invokeMapMethod<String, dynamic>('sample', {
  176. 'taskId': id,
  177. 'expr': expr.trim(),
  178. 'variable': variable,
  179. 'from': from,
  180. 'to': to,
  181. 'count': count,
  182. 'timeoutMs': timeout.inMilliseconds,
  183. });
  184. if (m == null || m['ok'] != true) {
  185. throw SymjaException('${m?['error'] ?? '采样失败'}');
  186. }
  187. final raw = (m['ys'] as List?) ?? const [];
  188. return SymjaSamples(
  189. from: from,
  190. to: to,
  191. ys: raw.map((e) => (e as num).toDouble()).toList(growable: false),
  192. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  193. );
  194. } on PlatformException catch (e) {
  195. throw SymjaException('通道错误 ${e.code}: ${e.message}');
  196. } finally {
  197. if (_running == id) {
  198. _running = null;
  199. busy = false;
  200. notifyListeners();
  201. }
  202. }
  203. }
  204. /// 重新拉取全局变量列表。
  205. Future<void> refreshVariables() async {
  206. try {
  207. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  208. 'variables',
  209. <String, dynamic>{'taskId': 0},
  210. );
  211. final raw = (m?['vars'] as List?) ?? const [];
  212. variables = raw
  213. .map((e) => SymjaVariable('${(e as Map)['name']}', '${e['value']}'))
  214. .toList(growable: false);
  215. notifyListeners();
  216. } on PlatformException {
  217. // 变量面板不是关键路径,失败就保持原状
  218. }
  219. }
  220. Future<void> clearVariable(String name) async {
  221. await symjaChannel.invokeMethod<Object?>('clearVariable', <String, dynamic>{
  222. 'taskId': 0,
  223. 'name': name,
  224. });
  225. await refreshVariables();
  226. }
  227. /// 清空所有变量和 `%`。
  228. Future<void> clearAll() async {
  229. await symjaChannel.invokeMethod<Object?>('clearAll', <String, dynamic>{
  230. 'taskId': 0,
  231. });
  232. history.clear();
  233. variables = const [];
  234. notifyListeners();
  235. }
  236. void clearHistory() {
  237. history.clear();
  238. notifyListeners();
  239. }
  240. // ------------------------------------------------------------------ 本地消息
  241. /// 把一条「不是 Symja 算出来的」输出记进历史(help 文本、命令反馈等),
  242. /// 这样控制台可以用同一套渲染,也能被一起持久化。
  243. void addLocal(String input, {String? text, String? error}) {
  244. _record(
  245. input,
  246. result: text == null
  247. ? null
  248. : SymjaResult(text: text, tex: null, elapsedMs: 0),
  249. error: error,
  250. );
  251. }
  252. /// 从存档恢复历史。不触发求值。
  253. void restoreHistory(Iterable<({String input, String text, int ms})> entries) {
  254. for (final e in entries) {
  255. history.add(SymjaEntry(
  256. input: e.input,
  257. result: SymjaResult(text: e.text, tex: null, elapsedMs: e.ms),
  258. ));
  259. }
  260. while (history.length > maxHistory) {
  261. history.removeAt(0);
  262. }
  263. notifyListeners();
  264. }
  265. // ------------------------------------------------------------------ 持久化
  266. /// 导出当前会话里所有定义(赋值 + 函数定义)。
  267. ///
  268. /// 原生侧用 ISymbol.fullDefinitionToString(),一个调用同时覆盖 `a = 5` 和
  269. /// `f(x_) := x^2 + 1`(后者没有 assignedValue)。刻意不用 Save/Get —— 它们属于
  270. /// IOFunctions,和真机上失效的 Names 同族。
  271. Future<List<String>> exportSession() async {
  272. try {
  273. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  274. 'exportSession',
  275. <String, dynamic>{'taskId': ++_seq},
  276. );
  277. final raw = (m?['defs'] as List?) ?? const [];
  278. return raw.map((e) => '$e').toList(growable: false);
  279. } on PlatformException {
  280. return const [];
  281. }
  282. }
  283. /// 回灌定义。返回 (成功条数, 失败明细)。单条失败不影响其它条。
  284. Future<({int applied, List<String> failed})> importSession(
  285. List<String> defs,
  286. ) async {
  287. if (defs.isEmpty) return (applied: 0, failed: const <String>[]);
  288. try {
  289. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  290. 'importSession',
  291. <String, dynamic>{'taskId': ++_seq, 'defs': defs},
  292. );
  293. final failed = ((m?['failed'] as List?) ?? const [])
  294. .map((e) => '$e')
  295. .toList(growable: false);
  296. return (applied: (m?['applied'] as num?)?.toInt() ?? 0, failed: failed);
  297. } on PlatformException catch (e) {
  298. return (applied: 0, failed: ['通道错误: ${e.message}']);
  299. }
  300. }
  301. void _record(String input, {SymjaResult? result, String? error}) {
  302. history.add(SymjaEntry(input: input, result: result, error: error));
  303. while (history.length > maxHistory) {
  304. history.removeAt(0);
  305. }
  306. notifyListeners();
  307. }
  308. }