|
|
@@ -1,6 +1,15 @@
|
|
|
// tools/vector_normal.dart
|
|
|
+//
|
|
|
+// 计算部分走 Symja(符号计算库),不再用自研的 rationalsqrt2.dart。
|
|
|
+// 那个自研的 √n 代数只能表示"有理数 × 一层根号 + 若干项之和",遇到
|
|
|
+// 坐标里出现两层根号、分数套根号、或者三角函数就得放弃;交给 CAS 之后
|
|
|
+// 输入什么就能精确算什么。
|
|
|
import 'package:flutter/material.dart';
|
|
|
-import '../utils/rationalsqrt2.dart';
|
|
|
+import 'package:flutter_math_fork/flutter_math.dart';
|
|
|
+import 'package:provider/provider.dart';
|
|
|
+
|
|
|
+import '../expr_syntax.dart';
|
|
|
+import '../symja.dart';
|
|
|
|
|
|
class VectorNormalPage extends StatefulWidget {
|
|
|
@override
|
|
|
@@ -9,106 +18,55 @@ class VectorNormalPage extends StatefulWidget {
|
|
|
|
|
|
class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
final List<TextEditingController> _controllers = List.generate(9, (_) => TextEditingController());
|
|
|
- final List<FocusNode> _focusNodes = List.generate(9, (_) => FocusNode());
|
|
|
- TextEditingController? _currentFocusedController;
|
|
|
- FocusNode? _currentFocusedNode;
|
|
|
String _result = '';
|
|
|
+ String? _resultTex;
|
|
|
+ String? _error;
|
|
|
bool _isCalculating = false;
|
|
|
|
|
|
- @override
|
|
|
- void initState() {
|
|
|
- super.initState();
|
|
|
- // 为每个焦点节点添加监听器
|
|
|
- for (int i = 0; i < _focusNodes.length; i++) {
|
|
|
- _focusNodes[i].addListener(() {
|
|
|
- if (_focusNodes[i].hasFocus) {
|
|
|
- _setCurrentFocusedController(_controllers[i], _focusNodes[i]);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
@override
|
|
|
void dispose() {
|
|
|
for (var controller in _controllers) {
|
|
|
controller.dispose();
|
|
|
}
|
|
|
- for (var focusNode in _focusNodes) {
|
|
|
- focusNode.dispose();
|
|
|
- }
|
|
|
super.dispose();
|
|
|
}
|
|
|
|
|
|
- // 设置当前聚焦的控制器和焦点节点
|
|
|
- void _setCurrentFocusedController(TextEditingController controller, FocusNode focusNode) {
|
|
|
- setState(() {
|
|
|
- _currentFocusedController = controller;
|
|
|
- _currentFocusedNode = focusNode;
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 插入根号到当前光标位置
|
|
|
- void _insertSqrt() {
|
|
|
- if (_currentFocusedController == null || _currentFocusedNode == null) return;
|
|
|
-
|
|
|
- final controller = _currentFocusedController!;
|
|
|
- final focusNode = _currentFocusedNode!;
|
|
|
- final text = controller.text;
|
|
|
- final selection = controller.selection;
|
|
|
- final newText = text.substring(0, selection.start) + '√' + text.substring(selection.end);
|
|
|
- final newCursorPosition = selection.start + 1;
|
|
|
-
|
|
|
- controller.value = TextEditingValue(
|
|
|
- text: newText,
|
|
|
- selection: TextSelection.collapsed(offset: newCursorPosition),
|
|
|
- );
|
|
|
-
|
|
|
- // 确保焦点保持在当前输入框
|
|
|
- if (!focusNode.hasFocus) {
|
|
|
- focusNode.requestFocus();
|
|
|
- }
|
|
|
-
|
|
|
- // 延迟设置光标位置,避免全选
|
|
|
- Future.delayed(Duration(milliseconds: 10), () {
|
|
|
- controller.selection = TextSelection.collapsed(offset: newCursorPosition);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 计算法向量
|
|
|
- void _calculateNormal() {
|
|
|
+ // 计算法向量:AB × AC,全部交给 Symja 做符号运算
|
|
|
+ Future<void> _calculateNormal() async {
|
|
|
+ final session = context.read<SymjaSession>();
|
|
|
setState(() {
|
|
|
_isCalculating = true;
|
|
|
_result = '';
|
|
|
+ _resultTex = null;
|
|
|
+ _error = null;
|
|
|
});
|
|
|
|
|
|
- try {
|
|
|
- // 解析三个点的坐标
|
|
|
- List<Vector3> points = [];
|
|
|
- for (int i = 0; i < 3; i++) {
|
|
|
- final x = RationalExtend.parse(_controllers[i * 3].text.isEmpty ? '0' : _controllers[i * 3].text);
|
|
|
- final y = RationalExtend.parse(_controllers[i * 3 + 1].text.isEmpty ? '0' : _controllers[i * 3 + 1].text);
|
|
|
- final z = RationalExtend.parse(_controllers[i * 3 + 2].text.isEmpty ? '0' : _controllers[i * 3 + 2].text);
|
|
|
- points.add(Vector3(x, y, z));
|
|
|
- }
|
|
|
+ // 每个坐标先过一遍语法转换,这样用户沿用旧的 `2√3` 写法也能用
|
|
|
+ String coord(int point, int axis) {
|
|
|
+ final raw = _controllers[point * 3 + axis].text.trim();
|
|
|
+ return raw.isEmpty ? '0' : toSymjaSyntax(raw);
|
|
|
+ }
|
|
|
|
|
|
- // 计算两个向量
|
|
|
- final vector1 = points[1] - points[0];
|
|
|
- final vector2 = points[2] - points[0];
|
|
|
+ // 边向量: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 normal = vector1.cross(vector2);
|
|
|
+ final expr = 'Cross[${vec(0, 1)}, ${vec(0, 2)}]';
|
|
|
|
|
|
+ try {
|
|
|
+ final r = await session.compute(expr);
|
|
|
+ if (!mounted) return;
|
|
|
setState(() {
|
|
|
- _result = '法向量:(${normal.x}, ${normal.y}, ${normal.z})';
|
|
|
- });
|
|
|
- } catch (e) {
|
|
|
- setState(() {
|
|
|
- _result = '计算错误:$e';
|
|
|
+ _result = r.text;
|
|
|
+ _resultTex = r.tex;
|
|
|
});
|
|
|
+ } on SymjaException catch (e) {
|
|
|
+ if (!mounted) return;
|
|
|
+ setState(() => _error = '$e');
|
|
|
} finally {
|
|
|
- setState(() {
|
|
|
- _isCalculating = false;
|
|
|
- });
|
|
|
+ if (mounted) setState(() => _isCalculating = false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -119,8 +77,8 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
}
|
|
|
setState(() {
|
|
|
_result = '';
|
|
|
- _currentFocusedController = null;
|
|
|
- _currentFocusedNode = null;
|
|
|
+ _resultTex = null;
|
|
|
+ _error = null;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -169,11 +127,11 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
SizedBox(height: 12),
|
|
|
Row(
|
|
|
children: [
|
|
|
- _buildCoordinateInput('x', _controllers[startIndex], _focusNodes[startIndex]),
|
|
|
+ _buildCoordinateInput('x', _controllers[startIndex]),
|
|
|
SizedBox(width: 12),
|
|
|
- _buildCoordinateInput('y', _controllers[startIndex + 1], _focusNodes[startIndex + 1]),
|
|
|
+ _buildCoordinateInput('y', _controllers[startIndex + 1]),
|
|
|
SizedBox(width: 12),
|
|
|
- _buildCoordinateInput('z', _controllers[startIndex + 2], _focusNodes[startIndex + 2]),
|
|
|
+ _buildCoordinateInput('z', _controllers[startIndex + 2]),
|
|
|
],
|
|
|
),
|
|
|
],
|
|
|
@@ -182,7 +140,7 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- Widget _buildCoordinateInput(String label, TextEditingController controller, FocusNode focusNode) {
|
|
|
+ Widget _buildCoordinateInput(String label, TextEditingController controller) {
|
|
|
return Expanded(
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
@@ -194,7 +152,6 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
SizedBox(height: 4),
|
|
|
TextField(
|
|
|
controller: controller,
|
|
|
- focusNode: focusNode,
|
|
|
decoration: InputDecoration(
|
|
|
hintText: '0',
|
|
|
border: OutlineInputBorder(),
|
|
|
@@ -244,29 +201,33 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
child: Text('清除', style: TextStyle(fontSize: 16)),
|
|
|
),
|
|
|
),
|
|
|
- SizedBox(width: 12),
|
|
|
- // 使用 GestureDetector 而不是 ElevatedButton 来避免焦点转移
|
|
|
- GestureDetector(
|
|
|
- onTap: _insertSqrt,
|
|
|
- child: Container(
|
|
|
- width: 60,
|
|
|
- padding: EdgeInsets.symmetric(vertical: 12),
|
|
|
- decoration: BoxDecoration(
|
|
|
- color: Colors.blue,
|
|
|
- borderRadius: BorderRadius.circular(4),
|
|
|
- ),
|
|
|
- child: Text(
|
|
|
- '√',
|
|
|
- style: TextStyle(fontSize: 18, color: Colors.white),
|
|
|
- textAlign: TextAlign.center,
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
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(
|
|
|
@@ -282,9 +243,28 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
|
),
|
|
|
SizedBox(height: 12),
|
|
|
- Text(
|
|
|
+ // 法向量是精确的符号结果(可能带根号、分数),用 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: 16, color: Colors.blue[700]),
|
|
|
+ style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
|
|
),
|
|
|
SizedBox(height: 8),
|
|
|
Text(
|
|
|
@@ -312,8 +292,13 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
),
|
|
|
SizedBox(height: 12),
|
|
|
Text(
|
|
|
- '√2/2请输入为1/2√2\n'
|
|
|
- '如果已知两向量坐标,将一个点设为原点即可',
|
|
|
+ '坐标按 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]),
|
|
|
),
|
|
|
],
|
|
|
@@ -322,28 +307,3 @@ class _VectorNormalPageState extends State<VectorNormalPage> {
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-// 三维向量类,使用RationalExtend进行计算
|
|
|
-class Vector3 {
|
|
|
- final RationalExtend x, y, z;
|
|
|
-
|
|
|
- Vector3(this.x, this.y, this.z);
|
|
|
-
|
|
|
- // 向量减法
|
|
|
- Vector3 operator -(Vector3 other) {
|
|
|
- return Vector3(x - other.x, y - other.y, z - other.z);
|
|
|
- }
|
|
|
-
|
|
|
- // 向量叉积
|
|
|
- Vector3 cross(Vector3 other) {
|
|
|
- final newX = y * other.z - z * other.y;
|
|
|
- final newY = z * other.x - x * other.z;
|
|
|
- final newZ = x * other.y - y * other.x;
|
|
|
- return Vector3(newX, newY, newZ);
|
|
|
- }
|
|
|
-
|
|
|
- @override
|
|
|
- String toString() {
|
|
|
- return '($x, $y, $z)';
|
|
|
- }
|
|
|
-}
|