symmetry.dart 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // tools/symmetry.dart
  2. import 'package:flutter/material.dart';
  3. import '../symja.dart';
  4. import 'tool_kit.dart';
  5. /// 对称变换 —— 三种模式共用一个页面,靠下拉框切换。
  6. ///
  7. /// 输入框按模式分别建好、缓存在 [_fields] 里:切回来时原来填的数还在,
  8. /// 而外壳看到的是不同的 list 实例,会自动把上一次的结果清掉。
  9. class SymmetryPage extends StatefulWidget {
  10. const SymmetryPage({super.key});
  11. @override
  12. State<SymmetryPage> createState() => _SymmetryPageState();
  13. }
  14. class _SymmetryPageState extends State<SymmetryPage> {
  15. static const _modes = ['点关于直线对称', '直线关于点对称', '直线关于直线对称'];
  16. int _mode = 0;
  17. // 每种模式一套输入框,建一次、一直留着重用。
  18. final Map<int, List<ToolField>> _fields = {
  19. 0: [
  20. ToolField('点 x', hint: '分数如 1/2,或小数'),
  21. // 点的横纵坐标是一对,并排一行
  22. ToolField('点 y', hint: '分数如 1/2,或小数', rowWithPrevious: true),
  23. ToolField('直线 A', hint: 'Ax + By + C = 0 的 A'),
  24. ToolField('直线 B', hint: 'Ax + By + C = 0 的 B'),
  25. ToolField('直线 C', hint: 'Ax + By + C = 0 的 C'),
  26. ],
  27. 1: [
  28. ToolField('直线 A', hint: 'Ax + By + C = 0 的 A'),
  29. ToolField('直线 B', hint: 'Ax + By + C = 0 的 B'),
  30. ToolField('直线 C', hint: 'Ax + By + C = 0 的 C'),
  31. ToolField('点 x', hint: '分数如 1/2,或小数'),
  32. ToolField('点 y', hint: '分数如 1/2,或小数', rowWithPrevious: true),
  33. ],
  34. 2: [
  35. ToolField('原直线 A', hint: 'Px + Qy + R = 0 的 P'),
  36. ToolField('原直线 B', hint: 'Px + Qy + R = 0 的 Q'),
  37. ToolField('原直线 C', hint: 'Px + Qy + R = 0 的 R'),
  38. ToolField('对称轴 A', hint: 'Ax + By + C = 0 的 A'),
  39. ToolField('对称轴 B', hint: 'Ax + By + C = 0 的 B'),
  40. ToolField('对称轴 C', hint: 'Ax + By + C = 0 的 C'),
  41. ],
  42. };
  43. @override
  44. Widget build(BuildContext context) {
  45. final fields = _fields[_mode]!;
  46. return ToolScaffold(
  47. title: '对称变换',
  48. fields: fields,
  49. intro: Column(
  50. crossAxisAlignment: CrossAxisAlignment.start,
  51. children: [
  52. DropdownButton<int>(
  53. value: _mode,
  54. isExpanded: true,
  55. items: [
  56. for (var i = 0; i < _modes.length; i++)
  57. DropdownMenuItem(value: i, child: Text(_modes[i])),
  58. ],
  59. onChanged: (v) {
  60. if (v != null) setState(() => _mode = v);
  61. },
  62. ),
  63. ],
  64. ),
  65. compute: (session) async {
  66. final f = _fields[_mode]!;
  67. if (_mode == 0) {
  68. // 点 (x, y) 关于直线 Ax + By + C = 0 的对称点
  69. final x = _need(f[0]);
  70. final y = _need(f[1]);
  71. final a = _need(f[2]);
  72. final b = _need(f[3]);
  73. final c = _need(f[4]);
  74. final expr =
  75. 'With[{A=$a,B=$b,C=$c,x=$x,y=$y}, '
  76. 'Module[{t=(A*x+B*y+C)/(A^2+B^2)}, {x-2*A*t, y-2*B*t}]]';
  77. final r = parseSymjaList((await session.compute(expr)).text);
  78. final rN = parseSymjaList(
  79. (await session.compute(
  80. approxExpr('Rationalize[$expr]', session.approximateDigits),
  81. )).text,
  82. );
  83. return [ToolText('对称点', '(${_pair(r, rN, 0)}, ${_pair(r, rN, 1)})')];
  84. }
  85. if (_mode == 1) {
  86. // 直线 Ax + By + C = 0 关于点 (x, y):A、B 不变,C' = -2Ax - 2By - C
  87. final a = _need(f[0]);
  88. final b = _need(f[1]);
  89. final c = _need(f[2]);
  90. final x = _need(f[3]);
  91. final y = _need(f[4]);
  92. final expr =
  93. 'With[{A=$a,B=$b,C=$c,x=$x,y=$y}, '
  94. '{A, B, -2*A*x-2*B*y-C}]';
  95. final (r, n) = await _exactAndApprox(session, expr);
  96. return _line(r, n);
  97. }
  98. // 原直线 Px + Qy + R = 0 关于对称轴 Ax + By + C = 0
  99. final p = _need(f[0]);
  100. final q = _need(f[1]);
  101. final rr = _need(f[2]);
  102. final a = _need(f[3]);
  103. final b = _need(f[4]);
  104. final c = _need(f[5]);
  105. final expr =
  106. 'With[{P=$p,Q=$q,R=$rr,A=$a,B=$b,C=$c}, '
  107. 'Module[{k=2*(P*A+Q*B)/(A^2+B^2)}, {P-A*k, Q-B*k, R-C*k}]]';
  108. final (r, n) = await _exactAndApprox(session, expr);
  109. return _line(r, n);
  110. },
  111. );
  112. }
  113. /// 一次拿两组:精确值(原样)和小数版(截到设置的位数)。
  114. Future<(List<Object>, List<Object>)> _exactAndApprox(
  115. SymjaSession session,
  116. String expr,
  117. ) async {
  118. final r = parseSymjaList((await session.compute(expr)).text);
  119. final n = parseSymjaList(
  120. (await session.compute(
  121. approxExpr('Rationalize[$expr]', session.approximateDigits),
  122. )).text,
  123. );
  124. return (r, n);
  125. }
  126. String _pair(List<Object> r, List<Object> n, int i) =>
  127. withApprox('${r[i]}', i < n.length ? '${n[i]}' : '');
  128. /// 把 Symja 回来的一串 {A, B, C} 拼成直线方程 —— 渲染成数学排版。
  129. ///
  130. /// 系数的正负交给 polyEqualsZero 处理(`+ -1·y` 要写成 `- y`),
  131. /// 另外给一条小数版:系数是分数时两个形式都有用。
  132. List<ToolOutput> _line(List<Object> exact, List<Object> approx) {
  133. ({String plain, String tex}) eq(List<Object> r) => polyEqualsZero([
  134. ('${r[0]}', 'x', 'x'),
  135. ('${r[1]}', 'y', 'y'),
  136. ('${r[2]}', '', ''),
  137. ]);
  138. final e = eq(exact);
  139. final out = [ToolText('对称直线方程', e.plain, tex: e.tex)];
  140. final a = eq(approx);
  141. if (a.plain != e.plain) {
  142. out.add(ToolText('对称直线方程(近似)', a.plain, tex: a.tex));
  143. }
  144. return out;
  145. }
  146. /// 取一个标量输入框:空着或填了多个值都直接报中文错。
  147. String _need(ToolField f) {
  148. if (f.count == 0) throw Exception('请填写「${f.label}」');
  149. if (f.count > 1) throw Exception('「${f.label}」只能填一个数');
  150. return f.text;
  151. }
  152. }