| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- // tools/symmetry.dart
- import 'package:flutter/material.dart';
- import '../symja.dart';
- import 'tool_kit.dart';
- /// 对称变换 —— 三种模式共用一个页面,靠下拉框切换。
- ///
- /// 输入框按模式分别建好、缓存在 [_fields] 里:切回来时原来填的数还在,
- /// 而外壳看到的是不同的 list 实例,会自动把上一次的结果清掉。
- class SymmetryPage extends StatefulWidget {
- const SymmetryPage({super.key});
- @override
- State<SymmetryPage> createState() => _SymmetryPageState();
- }
- class _SymmetryPageState extends State<SymmetryPage> {
- static const _modes = ['点关于直线对称', '直线关于点对称', '直线关于直线对称'];
- int _mode = 0;
- // 每种模式一套输入框,建一次、一直留着重用。
- final Map<int, List<ToolField>> _fields = {
- 0: [
- ToolField('点 x', hint: '分数如 1/2,或小数'),
- // 点的横纵坐标是一对,并排一行
- ToolField('点 y', hint: '分数如 1/2,或小数', rowWithPrevious: true),
- ToolField('直线 A', hint: 'Ax + By + C = 0 的 A'),
- ToolField('直线 B', hint: 'Ax + By + C = 0 的 B'),
- ToolField('直线 C', hint: 'Ax + By + C = 0 的 C'),
- ],
- 1: [
- ToolField('直线 A', hint: 'Ax + By + C = 0 的 A'),
- ToolField('直线 B', hint: 'Ax + By + C = 0 的 B'),
- ToolField('直线 C', hint: 'Ax + By + C = 0 的 C'),
- ToolField('点 x', hint: '分数如 1/2,或小数'),
- ToolField('点 y', hint: '分数如 1/2,或小数', rowWithPrevious: true),
- ],
- 2: [
- ToolField('原直线 A', hint: 'Px + Qy + R = 0 的 P'),
- ToolField('原直线 B', hint: 'Px + Qy + R = 0 的 Q'),
- ToolField('原直线 C', hint: 'Px + Qy + R = 0 的 R'),
- ToolField('对称轴 A', hint: 'Ax + By + C = 0 的 A'),
- ToolField('对称轴 B', hint: 'Ax + By + C = 0 的 B'),
- ToolField('对称轴 C', hint: 'Ax + By + C = 0 的 C'),
- ],
- };
- @override
- Widget build(BuildContext context) {
- final fields = _fields[_mode]!;
- return ToolScaffold(
- title: '对称变换',
- fields: fields,
- intro: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- DropdownButton<int>(
- value: _mode,
- isExpanded: true,
- items: [
- for (var i = 0; i < _modes.length; i++)
- DropdownMenuItem(value: i, child: Text(_modes[i])),
- ],
- onChanged: (v) {
- if (v != null) setState(() => _mode = v);
- },
- ),
- ],
- ),
- compute: (session) async {
- final f = _fields[_mode]!;
- if (_mode == 0) {
- // 点 (x, y) 关于直线 Ax + By + C = 0 的对称点
- final x = _need(f[0]);
- final y = _need(f[1]);
- final a = _need(f[2]);
- final b = _need(f[3]);
- final c = _need(f[4]);
- final expr =
- 'With[{A=$a,B=$b,C=$c,x=$x,y=$y}, '
- 'Module[{t=(A*x+B*y+C)/(A^2+B^2)}, {x-2*A*t, y-2*B*t}]]';
- final r = parseSymjaList((await session.compute(expr)).text);
- final rN = parseSymjaList(
- (await session.compute(
- approxExpr('Rationalize[$expr]', session.approximateDigits),
- )).text,
- );
- return [ToolText('对称点', '(${_pair(r, rN, 0)}, ${_pair(r, rN, 1)})')];
- }
- if (_mode == 1) {
- // 直线 Ax + By + C = 0 关于点 (x, y):A、B 不变,C' = -2Ax - 2By - C
- final a = _need(f[0]);
- final b = _need(f[1]);
- final c = _need(f[2]);
- final x = _need(f[3]);
- final y = _need(f[4]);
- final expr =
- 'With[{A=$a,B=$b,C=$c,x=$x,y=$y}, '
- '{A, B, -2*A*x-2*B*y-C}]';
- final (r, n) = await _exactAndApprox(session, expr);
- return _line(r, n);
- }
- // 原直线 Px + Qy + R = 0 关于对称轴 Ax + By + C = 0
- final p = _need(f[0]);
- final q = _need(f[1]);
- final rr = _need(f[2]);
- final a = _need(f[3]);
- final b = _need(f[4]);
- final c = _need(f[5]);
- final expr =
- 'With[{P=$p,Q=$q,R=$rr,A=$a,B=$b,C=$c}, '
- 'Module[{k=2*(P*A+Q*B)/(A^2+B^2)}, {P-A*k, Q-B*k, R-C*k}]]';
- final (r, n) = await _exactAndApprox(session, expr);
- return _line(r, n);
- },
- );
- }
- /// 一次拿两组:精确值(原样)和小数版(截到设置的位数)。
- Future<(List<Object>, List<Object>)> _exactAndApprox(
- SymjaSession session,
- String expr,
- ) async {
- final r = parseSymjaList((await session.compute(expr)).text);
- final n = parseSymjaList(
- (await session.compute(
- approxExpr('Rationalize[$expr]', session.approximateDigits),
- )).text,
- );
- return (r, n);
- }
- String _pair(List<Object> r, List<Object> n, int i) =>
- withApprox('${r[i]}', i < n.length ? '${n[i]}' : '');
- /// 把 Symja 回来的一串 {A, B, C} 拼成直线方程 —— 渲染成数学排版。
- ///
- /// 系数的正负交给 polyEqualsZero 处理(`+ -1·y` 要写成 `- y`),
- /// 另外给一条小数版:系数是分数时两个形式都有用。
- List<ToolOutput> _line(List<Object> exact, List<Object> approx) {
- ({String plain, String tex}) eq(List<Object> r) => polyEqualsZero([
- ('${r[0]}', 'x', 'x'),
- ('${r[1]}', 'y', 'y'),
- ('${r[2]}', '', ''),
- ]);
- final e = eq(exact);
- final out = [ToolText('对称直线方程', e.plain, tex: e.tex)];
- final a = eq(approx);
- if (a.plain != e.plain) {
- out.add(ToolText('对称直线方程(近似)', a.plain, tex: a.tex));
- }
- return out;
- }
- /// 取一个标量输入框:空着或填了多个值都直接报中文错。
- String _need(ToolField f) {
- if (f.count == 0) throw Exception('请填写「${f.label}」');
- if (f.count > 1) throw Exception('「${f.label}」只能填一个数');
- return f.text;
- }
- }
|