symja.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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. /// 结果用**近似值**还是**精确值**。默认精确。
  72. ///
  73. /// 精确是 Symja 的天然行为(`1/3` 就是 `1/3`,`Sqrt[2]` 就是 `Sqrt[2]`),
  74. /// 也是符号计算的意义所在,所以默认不开。开起来时给表达式套一层 `N[expr, 30]`。
  75. bool approximate = false;
  76. /// 近似模式下的有效位数。
  77. ///
  78. /// 15 位是刻意选的:再高就放不下了。`N[1/3, 30]` 会给出三十多个字符的长串,
  79. /// 手机上怎么看都显示不全(用户实测反馈)。15 位比一般科学计算器还多,
  80. /// 而且刚好一屏能看完。
  81. int approximateDigits = 15;
  82. static final _assignment = RegExp(r'=(?!=)');
  83. /// 按当前模式把表达式包成近似求值。
  84. ///
  85. /// **跳过带赋值的表达式**:`a = 1/3` 包成 `N[a = 1/3]` 就变成"先赋值再取近似",
  86. /// 显示和存储都可能走样;`f[x_] := x^2` 更会被直接毁掉。
  87. /// `=(?!=)` 恰好放过 `==`(方程),所以 `Solve[x^2-1==0, x]` 不受影响。
  88. String _applyMode(String expr) {
  89. if (!approximate) return expr;
  90. if (_assignment.hasMatch(expr)) return expr;
  91. return 'N[$expr, $approximateDigits]';
  92. }
  93. int _seq = 0;
  94. int? _running;
  95. /// 当前正在跑的任务 id,没有则为 null。
  96. int? get runningTaskId => _running;
  97. /// 求值一个表达式。
  98. ///
  99. /// [expr] 里的 `%` 由原生侧替换成上一次的用户可见结果(不是由 Symja 自己处理,
  100. /// 因为任何内部 eval 都会污染 Symja 的 `%`,见 SymjaBridge 的类注释)。
  101. ///
  102. /// 失败时抛 [SymjaException],同时失败本身也会记进 [history]。
  103. Future<SymjaResult> evaluate(
  104. String expr, {
  105. Duration timeout = defaultTimeout,
  106. bool wantTeX = true,
  107. }) async {
  108. final input = expr.trim();
  109. if (input.isEmpty) {
  110. throw const SymjaException('表达式为空');
  111. }
  112. final id = ++_seq;
  113. _running = id;
  114. busy = true;
  115. notifyListeners();
  116. try {
  117. final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
  118. 'taskId': id,
  119. 'expr': _applyMode(input),
  120. 'timeoutMs': timeout.inMilliseconds,
  121. 'wantTeX': wantTeX,
  122. // 显式声明这不是预览:预览不更新 `%`,正式求值要更新
  123. 'preview': false,
  124. });
  125. if (m == null || m['ok'] != true) {
  126. _record(input, error: '${m?['error'] ?? '原生侧返回了空结果'}');
  127. throw SymjaException('${m?['error'] ?? '原生侧返回了空结果'}');
  128. }
  129. final r = SymjaResult(
  130. text: '${m['text']}',
  131. tex: m['tex'] as String?,
  132. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  133. );
  134. _record(input, result: r);
  135. return r;
  136. } on PlatformException catch (e) {
  137. final msg = '通道错误 ${e.code}: ${e.message}';
  138. _record(input, error: msg);
  139. throw SymjaException(msg);
  140. } finally {
  141. if (_running == id) {
  142. _running = null;
  143. busy = false;
  144. notifyListeners();
  145. }
  146. }
  147. }
  148. /// 实时预览:求值但**不记进历史、也不影响 `%`**。
  149. ///
  150. /// 计算器边输入边预览走这条路(原生侧收到 preview=true 就不更新 lastOutput)。
  151. /// 不动 [busy]——预览不该把界面变成"计算中"。
  152. Future<SymjaResult?> preview(String expr) async {
  153. final input = expr.trim();
  154. if (input.isEmpty) return null;
  155. try {
  156. final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
  157. 'taskId': ++_seq,
  158. 'expr': _applyMode(input),
  159. 'timeoutMs': 4000,
  160. 'wantTeX': true,
  161. 'preview': true,
  162. });
  163. if (m == null || m['ok'] != true) return null;
  164. return SymjaResult(
  165. text: '${m['text']}',
  166. tex: m['tex'] as String?,
  167. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  168. );
  169. } on PlatformException {
  170. // 预览失败静默即可——用户按 = 的时候会看到真正的错误
  171. return null;
  172. }
  173. }
  174. /// 求值但**不记历史、不动 `%`**,失败抛 [SymjaException]。
  175. ///
  176. /// 工具页做内部计算用它:那些表达式不是用户在控制台里敲的,塞进历史只是噪音;
  177. /// 而 [preview] 会把错误吞掉,工具却需要把错误显示出来。
  178. Future<SymjaResult> compute(
  179. String expr, {
  180. Duration timeout = const Duration(seconds: 20),
  181. bool wantTeX = true,
  182. }) async {
  183. final input = expr.trim();
  184. if (input.isEmpty) throw const SymjaException('表达式为空');
  185. try {
  186. final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
  187. 'taskId': ++_seq,
  188. 'expr': _applyMode(input),
  189. 'timeoutMs': timeout.inMilliseconds,
  190. 'wantTeX': wantTeX,
  191. // preview=true 同时意味着原生侧不更新 lastOutput,
  192. // 所以工具算完不会顶掉用户在计算器上按 ANS 拿到的东西。
  193. 'preview': true,
  194. });
  195. if (m == null || m['ok'] != true) {
  196. throw SymjaException('${m?['error'] ?? '未知错误'}');
  197. }
  198. return SymjaResult(
  199. text: '${m['text']}',
  200. tex: m['tex'] as String?,
  201. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  202. );
  203. } on PlatformException catch (e) {
  204. throw SymjaException('通道错误 ${e.code}: ${e.message}');
  205. }
  206. }
  207. /// 取消当前求值。原生侧只是取消 Future,Symja 自身不一定能立刻中断
  208. /// (复杂表达式可能要算完才返回)。
  209. Future<void> cancel() async {
  210. final id = _running;
  211. if (id == null) return;
  212. await symjaChannel.invokeMethod<Object?>('cancel', <String, dynamic>{
  213. 'taskId': id,
  214. });
  215. }
  216. /// 在 [from, to] 上均匀取 [count] 个点求值,用于绘图。
  217. Future<SymjaSamples> sample(
  218. String expr, {
  219. String variable = 'x',
  220. required double from,
  221. required double to,
  222. int count = 200,
  223. Duration timeout = const Duration(seconds: 20),
  224. }) async {
  225. final id = ++_seq;
  226. _running = id;
  227. busy = true;
  228. notifyListeners();
  229. try {
  230. final m = await symjaChannel.invokeMapMethod<String, dynamic>('sample', {
  231. 'taskId': id,
  232. 'expr': expr.trim(),
  233. 'variable': variable,
  234. 'from': from,
  235. 'to': to,
  236. 'count': count,
  237. 'timeoutMs': timeout.inMilliseconds,
  238. });
  239. if (m == null || m['ok'] != true) {
  240. throw SymjaException('${m?['error'] ?? '采样失败'}');
  241. }
  242. final raw = (m['ys'] as List?) ?? const [];
  243. return SymjaSamples(
  244. from: from,
  245. to: to,
  246. ys: raw.map((e) => (e as num).toDouble()).toList(growable: false),
  247. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  248. );
  249. } on PlatformException catch (e) {
  250. throw SymjaException('通道错误 ${e.code}: ${e.message}');
  251. } finally {
  252. if (_running == id) {
  253. _running = null;
  254. busy = false;
  255. notifyListeners();
  256. }
  257. }
  258. }
  259. /// 重新拉取全局变量列表。
  260. Future<void> refreshVariables() async {
  261. try {
  262. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  263. 'variables',
  264. <String, dynamic>{'taskId': 0},
  265. );
  266. final raw = (m?['vars'] as List?) ?? const [];
  267. variables = raw
  268. .map((e) => SymjaVariable('${(e as Map)['name']}', '${e['value']}'))
  269. .toList(growable: false);
  270. notifyListeners();
  271. } on PlatformException {
  272. // 变量面板不是关键路径,失败就保持原状
  273. }
  274. }
  275. Future<void> clearVariable(String name) async {
  276. await symjaChannel.invokeMethod<Object?>('clearVariable', <String, dynamic>{
  277. 'taskId': 0,
  278. 'name': name,
  279. });
  280. await refreshVariables();
  281. }
  282. /// 清空所有变量和 `%`。
  283. Future<void> clearAll() async {
  284. await symjaChannel.invokeMethod<Object?>('clearAll', <String, dynamic>{
  285. 'taskId': 0,
  286. });
  287. history.clear();
  288. variables = const [];
  289. notifyListeners();
  290. }
  291. void clearHistory() {
  292. history.clear();
  293. notifyListeners();
  294. }
  295. // ------------------------------------------------------------------ 本地消息
  296. /// 把一条「不是 Symja 算出来的」输出记进历史(help 文本、命令反馈等),
  297. /// 这样控制台可以用同一套渲染,也能被一起持久化。
  298. void addLocal(String input, {String? text, String? error}) {
  299. _record(
  300. input,
  301. result: text == null
  302. ? null
  303. : SymjaResult(text: text, tex: null, elapsedMs: 0),
  304. error: error,
  305. );
  306. }
  307. /// 从存档恢复历史。不触发求值。
  308. void restoreHistory(Iterable<({String input, String text, int ms})> entries) {
  309. for (final e in entries) {
  310. history.add(SymjaEntry(
  311. input: e.input,
  312. result: SymjaResult(text: e.text, tex: null, elapsedMs: e.ms),
  313. ));
  314. }
  315. while (history.length > maxHistory) {
  316. history.removeAt(0);
  317. }
  318. notifyListeners();
  319. }
  320. // ------------------------------------------------------------------ 持久化
  321. /// 导出当前会话里所有定义(赋值 + 函数定义)。
  322. ///
  323. /// 原生侧用 ISymbol.fullDefinitionToString(),一个调用同时覆盖 `a = 5` 和
  324. /// `f(x_) := x^2 + 1`(后者没有 assignedValue)。刻意不用 Save/Get —— 它们属于
  325. /// IOFunctions,和真机上失效的 Names 同族。
  326. Future<List<String>> exportSession() async {
  327. try {
  328. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  329. 'exportSession',
  330. <String, dynamic>{'taskId': ++_seq},
  331. );
  332. final raw = (m?['defs'] as List?) ?? const [];
  333. return raw.map((e) => '$e').toList(growable: false);
  334. } on PlatformException {
  335. return const [];
  336. }
  337. }
  338. /// 回灌定义。返回 (成功条数, 失败明细)。单条失败不影响其它条。
  339. Future<({int applied, List<String> failed})> importSession(
  340. List<String> defs,
  341. ) async {
  342. if (defs.isEmpty) return (applied: 0, failed: const <String>[]);
  343. try {
  344. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  345. 'importSession',
  346. <String, dynamic>{'taskId': ++_seq, 'defs': defs},
  347. );
  348. final failed = ((m?['failed'] as List?) ?? const [])
  349. .map((e) => '$e')
  350. .toList(growable: false);
  351. return (applied: (m?['applied'] as num?)?.toInt() ?? 0, failed: failed);
  352. } on PlatformException catch (e) {
  353. return (applied: 0, failed: ['通道错误: ${e.message}']);
  354. }
  355. }
  356. void _record(String input, {SymjaResult? result, String? error}) {
  357. history.add(SymjaEntry(input: input, result: result, error: error));
  358. while (history.length > maxHistory) {
  359. history.removeAt(0);
  360. }
  361. notifyListeners();
  362. }
  363. }