symja.dart 14 KB

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