// tools/vector_normal.dart // // 计算部分走 Symja(符号计算库),不再用自研的 rationalsqrt2.dart。 // 那个自研的 √n 代数只能表示"有理数 × 一层根号 + 若干项之和",遇到 // 坐标里出现两层根号、分数套根号、或者三角函数就得放弃;交给 CAS 之后 // 输入什么就能精确算什么。 import 'package:flutter/material.dart'; import 'package:flutter_math_fork/flutter_math.dart'; import 'package:provider/provider.dart'; import '../symja.dart'; class VectorNormalPage extends StatefulWidget { @override _VectorNormalPageState createState() => _VectorNormalPageState(); } class _VectorNormalPageState extends State { final List _controllers = List.generate(9, (_) => TextEditingController()); String _result = ''; String? _resultTex; String? _error; bool _isCalculating = false; @override void dispose() { for (var controller in _controllers) { controller.dispose(); } super.dispose(); } // 计算法向量:AB × AC,全部交给 Symja 做符号运算 Future _calculateNormal() async { final session = context.read(); setState(() { _isCalculating = true; _result = ''; _resultTex = null; _error = null; }); // 每个坐标先过一遍语法转换,这样用户沿用旧的 `2√3` 写法也能用 String coord(int point, int axis) { final raw = _controllers[point * 3 + axis].text.trim(); // 原样交给 Symja return raw.isEmpty ? '0' : raw; } // 边向量:to - from String vec(int from, int to) => '{' '(${coord(to, 0)} - ${coord(from, 0)}), ' '(${coord(to, 1)} - ${coord(from, 1)}), ' '(${coord(to, 2)} - ${coord(from, 2)})}'; final expr = 'Cross[${vec(0, 1)}, ${vec(0, 2)}]'; try { final r = await session.compute(expr); if (!mounted) return; setState(() { _result = r.text; _resultTex = r.tex; }); } on SymjaException catch (e) { if (!mounted) return; setState(() => _error = '$e'); } finally { if (mounted) setState(() => _isCalculating = false); } } // 清除所有输入 void _clearAll() { for (var controller in _controllers) { controller.clear(); } setState(() { _result = ''; _resultTex = null; _error = null; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('三点法向量计算'), backgroundColor: Colors.white, elevation: 1, ), body: SingleChildScrollView( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInputSection('点A', 0), SizedBox(height: 16), _buildInputSection('点B', 3), SizedBox(height: 16), _buildInputSection('点C', 6), SizedBox(height: 24), _buildControlButtons(), SizedBox(height: 24), _buildResultSection(), SizedBox(height: 24), _buildInstructions(), ], ), ), ); } Widget _buildInputSection(String title, int startIndex) { return Card( elevation: 2, child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), SizedBox(height: 12), Row( children: [ _buildCoordinateInput('x', _controllers[startIndex]), SizedBox(width: 12), _buildCoordinateInput('y', _controllers[startIndex + 1]), SizedBox(width: 12), _buildCoordinateInput('z', _controllers[startIndex + 2]), ], ), ], ), ), ); } Widget _buildCoordinateInput(String label, TextEditingController controller) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), SizedBox(height: 4), TextField( controller: controller, decoration: InputDecoration( hintText: '0', border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), keyboardType: TextInputType.number, ), ], ), ); } Widget _buildControlButtons() { return Row( children: [ Expanded( flex: 3, child: ElevatedButton( onPressed: _isCalculating ? null : _calculateNormal, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, padding: EdgeInsets.symmetric(vertical: 12), ), child: _isCalculating ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text( '计算法向量', style: TextStyle(fontSize: 16, color: Colors.white), ), ), ), SizedBox(width: 12), Expanded( flex: 2, child: OutlinedButton( onPressed: _clearAll, style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: 12), ), child: Text('清除', style: TextStyle(fontSize: 16)), ), ), ], ); } Widget _buildResultSection() { if (_error != null) { return Card( color: Colors.red[50], elevation: 2, child: Padding( padding: EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.error_outline, color: Colors.red[700], size: 20), SizedBox(width: 8), Expanded( child: SelectableText( '计算错误:$_error', style: TextStyle(color: Colors.red[700], fontSize: 14), ), ), ], ), ), ); } if (_result.isEmpty) return SizedBox(); return Card( color: Colors.grey[50], elevation: 2, child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '计算结果', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), SizedBox(height: 12), // 法向量是精确的符号结果(可能带根号、分数),用 LaTeX 排出来 // 比纯文本好读得多。 if (_resultTex != null) Math.tex( _resultTex!, mathStyle: MathStyle.display, textStyle: TextStyle(fontSize: 20, color: Colors.blue[800]), onErrorFallback: (_) => Text( _result, style: TextStyle(fontSize: 16, color: Colors.blue[700]), ), ) else Text( _result, style: TextStyle(fontSize: 16, color: Colors.blue[700]), ), SizedBox(height: 8), // 纯文本那份留着,方便复制到别处 SelectableText( _result, style: TextStyle(fontSize: 12, color: Colors.grey[700]), ), SizedBox(height: 8), Text( '说明:法向量是通过向量AB和向量AC的叉积计算得到', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], ), ), ); } Widget _buildInstructions() { return Card( color: Colors.grey[50], elevation: 2, child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '使用说明', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), SizedBox(height: 12), Text( '坐标按 Symja 语法输入,例如:\n' ' 2/3 分数\n' ' Sqrt[2] 根号(也可写 √2)\n' ' Sqrt[2]/2、1 + Sqrt[3]、Pi\n' '\n' '结果是精确值:带根号、分数会原样保留,不会转成小数。\n' '如果已知两向量坐标,将一个点设为原点即可。', style: TextStyle(fontSize: 14, color: Colors.grey[700]), ), ], ), ), ); } }