symmetry.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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. return [_line(await _withApprox(session, expr))];
  96. }
  97. // 原直线 Px + Qy + R = 0 关于对称轴 Ax + By + C = 0
  98. final p = _need(f[0]);
  99. final q = _need(f[1]);
  100. final rr = _need(f[2]);
  101. final a = _need(f[3]);
  102. final b = _need(f[4]);
  103. final c = _need(f[5]);
  104. final expr =
  105. 'With[{P=$p,Q=$q,R=$rr,A=$a,B=$b,C=$c}, '
  106. 'Module[{k=2*(P*A+Q*B)/(A^2+B^2)}, {P-A*k, Q-B*k, R-C*k}]]';
  107. return [_line(await _withApprox(session, expr))];
  108. },
  109. );
  110. }
  111. /// 精确值 + 近似值:每个分量各自配上自己的小数。
  112. Future<List<Object>> _withApprox(SymjaSession session, String expr) async {
  113. final r = parseSymjaList((await session.compute(expr)).text);
  114. final n = parseSymjaList(
  115. (await session.compute(
  116. approxExpr('Rationalize[$expr]', session.approximateDigits),
  117. )).text,
  118. );
  119. return [
  120. for (var i = 0; i < r.length; i++)
  121. withApprox('${r[i]}', i < n.length ? '${n[i]}' : ''),
  122. ];
  123. }
  124. String _pair(List<Object> r, List<Object> n, int i) =>
  125. withApprox('${r[i]}', i < n.length ? '${n[i]}' : '');
  126. /// 把 Symja 回来的一串 {A, B, C} 拼成直线方程文本。
  127. ToolOutput _line(List<Object> r) =>
  128. ToolText('对称直线方程', '${r[0]}·x + ${r[1]}·y + ${r[2]} = 0');
  129. /// 取一个标量输入框:空着或填了多个值都直接报中文错。
  130. String _need(ToolField f) {
  131. if (f.count == 0) throw Exception('请填写「${f.label}」');
  132. if (f.count > 1) throw Exception('「${f.label}」只能填一个数');
  133. return f.text;
  134. }
  135. }