symja.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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. ///
  179. /// 默认**不套**顶栏那个「精确 / 近似」开关([applyMode]),工具要近似值时
  180. /// 自己在表达式里写 `N[...]` —— 否则"精确值"那一条也会被改写成近似值,
  181. /// 两条输出就没区别了。
  182. Future<SymjaResult> compute(
  183. String expr, {
  184. Duration timeout = const Duration(seconds: 20),
  185. bool wantTeX = true,
  186. bool applyMode = false,
  187. }) async {
  188. final input = expr.trim();
  189. if (input.isEmpty) throw const SymjaException('表达式为空');
  190. try {
  191. final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
  192. 'taskId': ++_seq,
  193. // 工具页传 applyMode: false —— 它在表达式里自己写 N[...],
  194. // 不该被计算器顶栏那个"精确 / 近似"开关二次改写(否则"精确值"
  195. // 那一条也会变成近似值,两条输出就没区别了)。
  196. 'expr': applyMode ? _applyMode(input) : input,
  197. 'timeoutMs': timeout.inMilliseconds,
  198. 'wantTeX': wantTeX,
  199. // preview=true 同时意味着原生侧不更新 lastOutput,
  200. // 所以工具算完不会顶掉用户在计算器上按 ANS 拿到的东西。
  201. 'preview': true,
  202. });
  203. if (m == null || m['ok'] != true) {
  204. throw SymjaException('${m?['error'] ?? '未知错误'}');
  205. }
  206. return SymjaResult(
  207. text: '${m['text']}',
  208. tex: m['tex'] as String?,
  209. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  210. );
  211. } on PlatformException catch (e) {
  212. throw SymjaException('通道错误 ${e.code}: ${e.message}');
  213. }
  214. }
  215. /// 取消当前求值。原生侧只是取消 Future,Symja 自身不一定能立刻中断
  216. /// (复杂表达式可能要算完才返回)。
  217. Future<void> cancel() async {
  218. final id = _running;
  219. if (id == null) return;
  220. await symjaChannel.invokeMethod<Object?>('cancel', <String, dynamic>{
  221. 'taskId': id,
  222. });
  223. }
  224. /// 在 [from, to] 上均匀取 [count] 个点求值,用于绘图。
  225. Future<SymjaSamples> sample(
  226. String expr, {
  227. String variable = 'x',
  228. required double from,
  229. required double to,
  230. int count = 200,
  231. Duration timeout = const Duration(seconds: 20),
  232. }) async {
  233. final id = ++_seq;
  234. _running = id;
  235. busy = true;
  236. notifyListeners();
  237. try {
  238. final m = await symjaChannel.invokeMapMethod<String, dynamic>('sample', {
  239. 'taskId': id,
  240. 'expr': expr.trim(),
  241. 'variable': variable,
  242. 'from': from,
  243. 'to': to,
  244. 'count': count,
  245. 'timeoutMs': timeout.inMilliseconds,
  246. });
  247. if (m == null || m['ok'] != true) {
  248. throw SymjaException('${m?['error'] ?? '采样失败'}');
  249. }
  250. final raw = (m['ys'] as List?) ?? const [];
  251. return SymjaSamples(
  252. from: from,
  253. to: to,
  254. ys: raw.map((e) => (e as num).toDouble()).toList(growable: false),
  255. elapsedMs: (m['ms'] as num?)?.toInt() ?? 0,
  256. );
  257. } on PlatformException catch (e) {
  258. throw SymjaException('通道错误 ${e.code}: ${e.message}');
  259. } finally {
  260. if (_running == id) {
  261. _running = null;
  262. busy = false;
  263. notifyListeners();
  264. }
  265. }
  266. }
  267. /// 重新拉取全局变量列表。
  268. Future<void> refreshVariables() async {
  269. try {
  270. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  271. 'variables',
  272. <String, dynamic>{'taskId': 0},
  273. );
  274. final raw = (m?['vars'] as List?) ?? const [];
  275. variables = raw
  276. .map((e) => SymjaVariable('${(e as Map)['name']}', '${e['value']}'))
  277. .toList(growable: false);
  278. notifyListeners();
  279. } on PlatformException {
  280. // 变量面板不是关键路径,失败就保持原状
  281. }
  282. }
  283. Future<void> clearVariable(String name) async {
  284. await symjaChannel.invokeMethod<Object?>('clearVariable', <String, dynamic>{
  285. 'taskId': 0,
  286. 'name': name,
  287. });
  288. await refreshVariables();
  289. }
  290. /// 清空所有变量和 `%`。
  291. Future<void> clearAll() async {
  292. await symjaChannel.invokeMethod<Object?>('clearAll', <String, dynamic>{
  293. 'taskId': 0,
  294. });
  295. history.clear();
  296. variables = const [];
  297. notifyListeners();
  298. }
  299. void clearHistory() {
  300. history.clear();
  301. notifyListeners();
  302. }
  303. // ------------------------------------------------------------------ 本地消息
  304. /// 把一条「不是 Symja 算出来的」输出记进历史(help 文本、命令反馈等),
  305. /// 这样控制台可以用同一套渲染,也能被一起持久化。
  306. void addLocal(String input, {String? text, String? error}) {
  307. _record(
  308. input,
  309. result: text == null
  310. ? null
  311. : SymjaResult(text: text, tex: null, elapsedMs: 0),
  312. error: error,
  313. );
  314. }
  315. /// 从存档恢复历史。不触发求值。
  316. void restoreHistory(Iterable<({String input, String text, int ms})> entries) {
  317. for (final e in entries) {
  318. history.add(SymjaEntry(
  319. input: e.input,
  320. result: SymjaResult(text: e.text, tex: null, elapsedMs: e.ms),
  321. ));
  322. }
  323. while (history.length > maxHistory) {
  324. history.removeAt(0);
  325. }
  326. notifyListeners();
  327. }
  328. // ------------------------------------------------------------------ 持久化
  329. /// 导出当前会话里所有定义(赋值 + 函数定义)。
  330. ///
  331. /// 原生侧用 ISymbol.fullDefinitionToString(),一个调用同时覆盖 `a = 5` 和
  332. /// `f(x_) := x^2 + 1`(后者没有 assignedValue)。刻意不用 Save/Get —— 它们属于
  333. /// IOFunctions,和真机上失效的 Names 同族。
  334. Future<List<String>> exportSession() async {
  335. try {
  336. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  337. 'exportSession',
  338. <String, dynamic>{'taskId': ++_seq},
  339. );
  340. final raw = (m?['defs'] as List?) ?? const [];
  341. return raw.map((e) => '$e').toList(growable: false);
  342. } on PlatformException {
  343. return const [];
  344. }
  345. }
  346. /// 回灌定义。返回 (成功条数, 失败明细)。单条失败不影响其它条。
  347. Future<({int applied, List<String> failed})> importSession(
  348. List<String> defs,
  349. ) async {
  350. if (defs.isEmpty) return (applied: 0, failed: const <String>[]);
  351. try {
  352. final m = await symjaChannel.invokeMapMethod<String, dynamic>(
  353. 'importSession',
  354. <String, dynamic>{'taskId': ++_seq, 'defs': defs},
  355. );
  356. final failed = ((m?['failed'] as List?) ?? const [])
  357. .map((e) => '$e')
  358. .toList(growable: false);
  359. return (applied: (m?['applied'] as num?)?.toInt() ?? 0, failed: failed);
  360. } on PlatformException catch (e) {
  361. return (applied: 0, failed: ['通道错误: ${e.message}']);
  362. }
  363. }
  364. void _record(String input, {SymjaResult? result, String? error}) {
  365. history.add(SymjaEntry(input: input, result: result, error: error));
  366. while (history.length > maxHistory) {
  367. history.removeAt(0);
  368. }
  369. notifyListeners();
  370. }
  371. }