// 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 createState() => _SymmetryPageState(); } class _SymmetryPageState extends State { static const _modes = ['点关于直线对称', '直线关于点对称', '直线关于直线对称']; int _mode = 0; // 每种模式一套输入框,建一次、一直留着重用。 final Map> _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( 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, List)> _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 r, List n, int i) => withApprox('${r[i]}', i < n.length ? '${n[i]}' : ''); /// 把 Symja 回来的一串 {A, B, C} 拼成直线方程 —— 渲染成数学排版。 /// /// 系数的正负交给 polyEqualsZero 处理(`+ -1·y` 要写成 `- y`), /// 另外给一条小数版:系数是分数时两个形式都有用。 List _line(List exact, List approx) { ({String plain, String tex}) eq(List 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; } }