More formulas, added formula editor

This commit is contained in:
Álvaro González 2026-02-25 09:08:06 +01:00
parent 1bcf829525
commit 77bea838f7
7 changed files with 1386 additions and 37 deletions

20
TODO.md
View file

@ -32,8 +32,18 @@
- [X] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies().
- [X] Replace flutter-markdown with flutter-markdown-plus
- [X] Heron's formula: investigate why a=3, b=40, c=5 yields NaN. Root cause: input values don't form a valid triangle (violate triangle inequality: 3+5=8 is not > 40). Added documentation note to the formula description.
- [R] Refactor ./assets/formulas d4rt files:
- [R] Pretty print files as dart literals (like JSON, but allow raw strings r"""like this""")
- [R] Ensure there is no formula duplicates. If necesary, move or delete the formula in file formulas.d4rt
- [R] defaultCorpus must load all formula files
- [ ] Investigate starup time when there is no previous database and corpus is loaded from assets.
- [X] Refactor ./assets/formulas d4rt files:
- [X] Pretty print files as dart literals (like JSON, but allow raw strings r"""like this""")
- [X] Ensure there is no formula duplicates. If necesary, move or delete the formula in file formulas.d4rt
- [X] defaultCorpus must load all formula files
- [X] Create a formula in ./assets/formulas/networking.d4art: input is a string with ip address and mask, output is ip subnet of this address and broadcast address.
- [R] Develop a new screen that edits a formula in ./lib/ai directory:
- [R] FormulaEditor initializes with a Formula
- [R] A textfield allows editing the "name" of the formula
- [R] A text area allows editing the "description". A button pops up a preview of the markdown.
- [R] There is one row per input variable. The "name" is a textfield. A first drop down allows to select the base unit, and a second dropdown is populated with all derived units of the selected base unit, and the base unit. The unit of the input variable is the derived unit.
- [R] Each input variable can be deleted with a button
- [R] A button after the inputs variables allows to insert a new input variable
- [R] There is one row for the ouput variable, similar to the row for the input variable
- [R] d4rtCode is a text area with dart syntax highligthing
- [R] At the botton, a button allows to test the edited Formula, launching a FormulaScreen

View file

@ -0,0 +1,507 @@
# Formula and Unit File Format Guide
This document describes the format for contributing formulas and units to the d4rt_formulas project. It is intended for formula contributors and developers.
---
## Table of Contents
1. [Overview](#overview)
2. [Formula Files](#formula-files)
3. [Unit Files](#unit-files)
4. [Writing Descriptions](#writing-descriptions)
5. [Best Practices](#best-practices)
6. [Examples](#examples)
---
## Overview
The project uses two types of asset files:
| File Type | Location | Extension | Format |
|--------------|--------------------|---------------|---------------------------------|
| **Formulas** | `assets/formulas/` | `.d4rt` | Dart array literals (JSON-like) |
| **Units** | `assets/units/` | `.d4rt.units` | Dart array literals (JSON-like) |
Both formats use Dart set/array literals with map entries. Files are parsed at runtime to populate the formula calculator.
---
## Formula Files
### File Structure
Formula files are organized by topic (e.g., `geometry.d4rt`, `electromagnetism.d4rt`). Each file contains a Dart array literal with formula objects:
```dart
[
{
"name": "Formula Name",
"description": r"""Markdown description with LaTeX""",
"input": [
{"name": "variable1", "unit": "unit_name"},
{"name": "variable2", "unit": "unit_name"}
],
"output": {"name": "result", "unit": "unit_name"},
"d4rtCode": "result = expression;",
"tags": ["tag1", "tag2"]
},
// More formulas...
]
```
### Formula Object Fields
| Field | Type | Required | Description |
|---------------|--------|----------|------------------------------------------------------------------------------------------|
| `name` | String | Yes | Human-readable formula name |
| `description` | String | Yes | Markdown description with LaTeX math (see [Writing Descriptions](#writing-descriptions)) |
| `input` | Array | Yes | List of input variables with their units |
| `output` | Object | Yes | Output variable name and unit |
| `d4rtCode` | String | Yes | Dart code that computes the result |
| `tags` | Array | Yes | Categorization tags for search/filter |
### Input/Output Format
**Input variables:**
```dart
"input": [
{"name": "m", "unit": "kilogram"},
{"name": "a", "unit": "meters per square second"}
]
```
**Output variable:**
```dart
"output": {"name": "F", "unit": "newton"}
```
### Unit Names
Unit names must match entries in the `assets/units/` directory. Use the full unit name (lowercase), not the symbol:
| Correct | Incorrect |
|-----------------------|-----------|
| `"meter"` | `"m"` |
| `"kilogram"` | `"kg"` |
| `"meters per second"` | `"m/s"` |
| `"square meter"` | `"m²"` |
### Dart Code (`d4rtCode`)
The `d4rtCode` field contains valid Dart code that:
- Uses input variable names directly
- Assigns the result to the output variable name
- Can use Dart's `math` library functions (`sin`, `cos`, `sqrt`, `pow`, `pi`, etc.)
**Simple formula:**
```dart
"d4rtCode": "F = m * a;"
```
**Multi-line formula:**
```dart
"d4rtCode": """
var radians = angle * (pi / 180);
result = sin(radians);
"""
```
**With validation:**
```dart
"d4rtCode": """
if (a + b < c) {
signal("Invalid triangle: sides do not satisfy triangle inequality");
}
var s = (a + b + c) / 2;
A = sqrt(s * (s - a) * (s - b) * (s - c));
"""
```
---
## Unit Files
### File Structure
Unit files define units of measurement organized by category (e.g., `distance.d4rt.units`, `force.d4rt.units`). Each file contains a Dart array literal with unit objects:
```dart
[
{"name": "meter", "symbol": "m", "isBase": true},
{"name": "kilometer", "symbol": "km", "baseUnit": "meter", "factor": 1000},
// More units...
]
```
### Unit Object Fields
| Field | Type | Required | Description |
|------------|---------|-------------|---------------------------------------------------------------------|
| `name` | String | Yes | Full unit name (lowercase) |
| `symbol` | String | Yes | Unit symbol for display |
| `isBase` | Boolean | Conditional | `true` if this is a base unit (no conversion needed) |
| `baseUnit` | String | Conditional | Name of the base unit for conversion |
| `factor` | Number | Conditional | Multiplication factor to convert to base unit |
| `toBase` | String | Conditional | Expression/code to convert to base unit (for complex conversions) |
| `fromBase` | String | Conditional | Expression/code to convert from base unit (for complex conversions) |
### Base Units vs Derived Units
**Base units** define the reference for a category:
```dart
{"name": "meter", "symbol": "m", "isBase": true}
{"name": "newton", "symbol": "N", "isBase": true}
{"name": "joule", "symbol": "J", "isBase": true}
{"name": "Kelvin", "symbol": "K", "isBase": true}
```
**Derived units** specify conversion to their base unit. There are two types:
#### Simple Linear Conversions (using `factor`)
For units where conversion is a simple multiplication:
```dart
{"name": "kilometer", "symbol": "km", "baseUnit": "meter", "factor": 1000}
{"name": "inch", "symbol": "in", "baseUnit": "meter", "factor": 0.0254}
{"name": "pound-force", "baseUnit": "newton", "factor": 4.44822}
```
The `factor` converts **from** the defined unit **to** the base unit:
```dart
// 1 kilometer = 1000 meters
{"name": "kilometer", "baseUnit": "meter", "factor": 1000}
// 1 inch = 0.0254 meters
{"name": "inch", "baseUnit": "meter", "factor": 0.0254}
```
#### Complex Conversions (using `toBase` and `fromBase`)
For units requiring non-linear conversions (e.g., temperature scales), use `toBase` and `fromBase` expressions. The variable `x` represents the value to convert.
**Example: Celsius to Kelvin**
```dart
{
"name": "Celsius",
"symbol": "°C",
"baseUnit": "Kelvin",
"toBase": "x + 273.15", // °C → K
"fromBase": "x - 273.15", // K → °C
}
```
**Example: Fahrenheit to Kelvin**
```dart
{
"name": "Fahrenheit",
"symbol": "°F",
"baseUnit": "Kelvin",
"toBase": "(x - 32) * 5/9 + 273.15", // °F → K
"fromBase": "(x - 273.15) * 9/5 + 32", // K → °F
}
```
**Example: Multi-line conversion (Gas Mark to Kelvin)**
```dart
{
"name": "Gas Mark",
"symbol": "GM",
"baseUnit": "Kelvin",
"toBase": r"""
if (x < 1) {
double celsius = (243 - 25 * (log(1 / x) / log(2))) / 1.8;
return celsius + 273.15;
} else {
double celsius = x * 14 + 121;
return celsius + 273.15;
}
""",
"fromBase": """
double celsius = x - 273.15;
if (celsius < 135) {
return pow(2, (1.8 * celsius - 243) / 25);
} else {
return (celsius - 121) / 14;
}
"""
}
```
### Common Temperature Conversions
| Unit | toBase (→ K) | fromBase (← K) |
|------------|------------------------------|------------------------------|
| Celsius | `x + 273.15` | `x - 273.15` |
| Fahrenheit | `(x - 32) * 5/9 + 273.15` | `(x - 273.15) * 9/5 + 32` |
| Rankine | `x * 5/9` | `x * 9/5` |
| Réaumur | `x * 5/4 + 273.15` | `(x - 273.15) * 4/5` |
| Delisle | `373.15 - x * 2/3` | `(373.15 - x) * 3/2` |
| Rømer | `(x - 7.5) * 40/21 + 273.15` | `(x - 273.15) * 21/40 + 7.5` |
---
## Writing Descriptions
The `description` field uses **raw Dart string literals** (`r"""..."""`) with **Markdown** and **LaTeX** math.
### Format
```dart
"description": r"""
Short description of the formula.
$$F = m \cdot a$$
Where:
- $F$: Force (Newtons)
- $m$: Mass (kilograms)
- $a$: Acceleration (m/s²)
Additional context or notes.""",
```
### LaTeX Math
Use **MathJax/KaTeX** syntax for mathematical expressions:
| Type | Syntax | Example |
|---------------------|-----------------------------|--------------------------|
| Inline math | `$...$` | `$F = ma$` |
| Display math | `$$...$$` | `$$E = mc^2$$` |
| Fractions | `\frac{a}{b}` | `$$\frac{1}{2}mv^2$$` |
| Subscripts | `x_i` | `$v_0$` |
| Superscripts | `x^2` | `$a^2 + b^2$` |
| Greek letters | `\alpha`, `\beta`, `\theta` | `$$\sin(\theta)$$` |
| Special symbols | `\cdot`, `\times`, `\pm` | `$m \cdot a$` |
| Units in math | `\mathrm{m/s^2}` | `$9.81\ \mathrm{m/s^2}$` |
| Scientific notation | `\times 10^{-11}` | `$6.674\times 10^{-11}$` |
### Including Images
Add Wikipedia or other educational images using Markdown:
```markdown
![Description](https://upload.wikimedia.org/wikipedia/commons/...)
```
**Example:**
```dart
"description": r"""
Newton's law of universal gravitation.
$$F = G\frac{m_1m_2}{r^2}$$
![Gravitation Diagram](https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/NewtonsLawOfUniversalGravitation.svg/1200px-NewtonsLawOfUniversalGravitation.svg.png)""",
```
### Description Structure
A well-structured description includes:
1. **Opening sentence** - Brief statement of what the formula calculates
2. **LaTeX formula** - The mathematical expression in display mode
3. **Variable definitions** - List of all variables with units
4. **Additional context** - Notes, assumptions, or applications
5. **Image** (optional) - Diagram or illustration
---
## Best Practices
### For Formulas
1. **Use clear variable names** - Single letters for physics conventions (`F`, `m`, `a`), descriptive names when clarity matters
2. **Match units precisely** - Ensure input/output units match what the formula expects
3. **Add validation** - Use `signal()` for invalid inputs (e.g., triangle inequality)
4. **Include tags** - Add relevant tags for discoverability
5. **Use LaTeX for all math** - Even simple formulas should have LaTeX representation
6. **Add images** - Include diagrams from Wikipedia when helpful
7. **Comment your code** - Use `//` comments before each formula object
### For Units
1. **Use lowercase names** - `"meter"` not `"Meter"`
2. **Include common conversions** - Add both metric and imperial units when relevant
3. **Use standard symbols** - Follow SI conventions where applicable
4. **Document the factor** - Ensure conversion factors are accurate
### For Descriptions
1. **Be concise but complete** - Explain what the formula does and what each variable means
2. **Use consistent formatting** - Follow the established pattern in existing files
3. **Include units in variable definitions** - Always specify units for each variable
4. **Add context** - Explain when/why the formula is used
5. **Note assumptions** - Mention any constraints or special conditions
---
## Examples
### Complete Formula Example
```dart
// Newton's Second Law
{
"name": "Newton's Second Law",
"description": r"""
Force equals mass times acceleration.
$$F = m \cdot a$$
Where:
- $m$: Mass of object ($\mathrm{kg}$)
- $a$: Acceleration ($\mathrm{m/s^2}$)
![Newton's Second Law](https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Newtonslawsofmotion.jpg/800px-Newtonslawsofmotion.jpg)""",
"input": [
{"name": "m", "unit": "kilogram"},
{"name": "a", "unit": "meters per square second"}
],
"output": {"name": "F", "unit": "newton"},
"d4rtCode": "F = m * a;",
"tags": ["physics", "mechanics", "newton"]
}
```
### Complete Unit Example
```dart
[
{
"name": "newton",
"symbol": "N",
"isBase": true
},
{
"name": "kilonewton",
"symbol": "kN",
"baseUnit": "newton",
"factor": 1000
},
{
"name": "pound-force",
"symbol": "lbf",
"baseUnit": "newton",
"factor": 4.44822
}
]
```
### Multi-line Dart Code Example
```dart
// Cosine Rule
{
"name": "Cosine Rule",
"description": r"""
Generalization of the Pythagorean theorem for any triangle.
$$c^2 = a^2 + b^2 - 2ab\cos(C)$$
Where:
- $a$, $b$, $c$: Sides of the triangle
- $C$: Angle opposite to side $c$""",
"input": [
{"name": "a", "unit": "meter"},
{"name": "b", "unit": "meter"},
{"name": "C", "unit": "degree"}
],
"output": {"name": "c", "unit": "meter"},
"d4rtCode": """
var angleCRad = C * (pi / 180);
c = sqrt(pow(a, 2) + pow(b, 2) - 2*a*b*cos(angleCRad));
""",
"tags": ["trigonometry", "triangle", "cosine"]
}
```
---
## File Organization
### Formula Categories
| File | Topic |
|----------------------------------|--------------------------------|
| `formulas.d4rt` | General physics formulas |
| `geometry.d4rt` | Geometric calculations |
| `electromagnetism.d4rt` | Electric and magnetic formulas |
| `energy_and_power.d4rt` | Energy, work, and power |
| `thermodynamics.d4rt` | Heat and thermodynamics |
| `fluids_and_pressure.d4rt` | Fluid mechanics |
| `optics.d4rt` | Light and optics |
| `trigonometry.d4rt` | Trigonometric relations |
| `materials_elasticity.d4rt` | Material properties |
| `medical_and_bio.d4rt` | Medical/biological formulas |
| `networking.d4rt` | Network calculations |
| `conversions_and_constants.d4rt` | Physical constants |
| `misc_math.d4rt` | Miscellaneous mathematics |
### Unit Categories
| File | Unit Type |
|--------------------------|------------------------|
| `distance.d4rt.units` | Length/distance |
| `mass.d4rt.units` | Mass |
| `time.d4rt.units` | Time |
| `force.d4rt.units` | Force |
| `energy.d4rt.units` | Energy |
| `power.d4rt.units` | Power |
| `pressure.d4rt.units` | Pressure |
| `velocity.d4rt.units` | Speed/velocity |
| `area.d4rt.units` | Area |
| `volume.d4rt.units` | Volume |
| `temperature.d4rt.units` | Temperature |
| `angle.d4rt.units` | Angles |
| `frequency.d4rt.units` | Frequency |
| `electricity.d4rt.units` | Electrical units |
| `derived.d4rt.units` | Derived/compound units |
---
## Quick Reference
### Common LaTeX Symbols
| Symbol | LaTeX | Symbol | LaTeX |
|--------|-----------|--------|----------|
| × | `\times` | · | `\cdot` |
| ± | `\pm` | ÷ | `\div` |
| ≤ | `\leq` | ≥ | `\geq` |
| √ | `\sqrt{}` | ∞ | `\infty` |
| π | `\pi` | θ | `\theta` |
| α | `\alpha` | β | `\beta` |
| Δ | `\Delta` | δ | `\delta` |
| Σ | `\Sigma` | σ | `\sigma` |
| Ω | `\Omega` | ω | `\omega` |
### Common Dart Math Functions
| Function | Description |
|---------------------------------|-----------------------------------|
| `sin(x)`, `cos(x)`, `tan(x)` | Trigonometric functions (radians) |
| `asin(x)`, `acos(x)`, `atan(x)` | Inverse trig functions |
| `sqrt(x)` | Square root |
| `pow(x, y)` | x raised to power y |
| `log(x)` | Natural logarithm |
| `log10(x)` | Base-10 logarithm |
| `abs(x)` | Absolute value |
| `exp(x)` | e raised to power x |
| `pi` | π constant |
---
## Contributing
1. **Choose the right file** - Add formulas to the appropriate category file
2. **Follow the format** - Match the structure of existing entries
3. **Test your code** - Ensure `d4rtCode` is valid Dart syntax
4. **Add description** - Include complete LaTeX documentation
5. **Tag appropriately** - Add relevant tags for searchability
6. **Review** - Check existing formulas for consistency
For questions or clarifications, refer to existing formulas in the `assets/formulas/` directory as examples.

View file

@ -0,0 +1,83 @@
[
// IP Subnet and Broadcast Calculator
{
"name": "IP Subnet and Broadcast",
"description": r"""
Calculates the network (subnet) address and broadcast address for an IPv4 address with CIDR notation.
**Input format:** `ip_address/prefix` where:
- `ip_address`: IPv4 address in dotted decimal notation (e.g., `192.168.1.100`)
- `prefix`: CIDR prefix length (1-30) or subnet mask in dotted notation (e.g., `24` or `255.255.255.0`)
**Output:**
- `subnet`: Network address in dotted decimal notation
- `broadcast`: Broadcast address in dotted decimal notation
**Examples:**
- Input: `192.168.1.100/24` → Subnet: `192.168.1.0`, Broadcast: `192.168.1.255`
- Input: `10.0.0.50/8` → Subnet: `10.0.0.0`, Broadcast: `10.255.255.255`
- Input: `172.16.5.100/16` → Subnet: `172.16.0.0`, Broadcast: `172.16.255.255`""",
"input": [
{"name": "ipWithMask", "unit": "scalar"}
],
"output": {"name": "subnet", "unit": "scalar"},
"d4rtCode": """
var input = ipWithMask.toString();
var slashIndex = input.indexOf('/');
if (slashIndex == -1) {
subnet = 'error: no / found';
broadcast = '';
} else {
var ipPart = input.substring(0, slashIndex).trim();
var maskPart = input.substring(slashIndex + 1).trim();
// Parse IP address
var ipParts = ipPart.split('.');
if (ipParts.length != 4) {
subnet = 'error: invalid IP';
broadcast = '';
} else {
var octet1 = int.parse(ipParts[0]);
var octet2 = int.parse(ipParts[1]);
var octet3 = int.parse(ipParts[2]);
var octet4 = int.parse(ipParts[3]);
// Convert IP to 32-bit integer
var ipInt = (octet1 << 24) | (octet2 << 16) | (octet3 << 8) | octet4;
// Parse mask (CIDR prefix or dotted notation)
int maskInt;
if (maskPart.contains('.')) {
var maskParts = maskPart.split('.');
var m1 = int.parse(maskParts[0]);
var m2 = int.parse(maskParts[1]);
var m3 = int.parse(maskParts[2]);
var m4 = int.parse(maskParts[3]);
maskInt = (m1 << 24) | (m2 << 16) | (m3 << 8) | m4;
} else {
var prefix = int.parse(maskPart);
maskInt = prefix == 0 ? 0 : (-1 << (32 - prefix));
}
// Calculate subnet and broadcast
var subnetInt = ipInt & maskInt;
var broadcastInt = subnetInt | (~maskInt & 0xFFFFFFFF);
// Convert back to dotted notation
var s1 = (subnetInt >> 24) & 0xFF;
var s2 = (subnetInt >> 16) & 0xFF;
var s3 = (subnetInt >> 8) & 0xFF;
var s4 = subnetInt & 0xFF;
subnet = '\$s1.\$s2.\$s3.\$s4';
var b1 = (broadcastInt >> 24) & 0xFF;
var b2 = (broadcastInt >> 16) & 0xFF;
var b3 = (broadcastInt >> 8) & 0xFF;
var b4 = broadcastInt & 0xFF;
broadcast = '\$b1.\$b2.\$b3.\$b4';
}
}
""",
"tags": ["networking", "ip", "subnet", "broadcast", "cidr"]
}
]

705
lib/ai/formula_editor.dart Normal file
View file

@ -0,0 +1,705 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown_plus_latex/flutter_markdown_plus_latex.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:markdown/markdown.dart' as markdown;
import '../formula_models.dart';
import '../corpus.dart';
import 'formula_screen.dart';
import 'unit_dropdown.dart';
/// A screen for editing a Formula's properties including name, description,
/// input/output variables, and d4rt code.
class FormulaEditor extends StatefulWidget {
final Formula formula;
final Corpus corpus;
const FormulaEditor({
super.key,
required this.formula,
required this.corpus,
});
@override
State<FormulaEditor> createState() => _FormulaEditorState();
}
class _FormulaEditorState extends State<FormulaEditor> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _descriptionController;
late TextEditingController _d4rtCodeController;
// Track input variables
final List<_InputVariableRowData> _inputVariables = [];
// Output variable
late _OutputVariableRowData _outputVariable;
bool _isPreviewVisible = false;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.formula.name);
_descriptionController = TextEditingController(text: widget.formula.description ?? '');
_d4rtCodeController = TextEditingController(text: widget.formula.d4rtCode);
// Initialize input variables
for (final input in widget.formula.input) {
_inputVariables.add(_InputVariableRowData(
nameController: TextEditingController(text: input.name),
unit: input.unit,
values: input.values != null ? List.from(input.values!) : null,
));
}
// Initialize output variable
_outputVariable = _OutputVariableRowData(
nameController: TextEditingController(text: widget.formula.output.name),
unit: widget.formula.output.unit,
);
}
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_d4rtCodeController.dispose();
for (final variable in _inputVariables) {
variable.nameController.dispose();
}
_outputVariable.nameController.dispose();
super.dispose();
}
void _addInputVariable() {
setState(() {
_inputVariables.add(_InputVariableRowData(
nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'),
unit: null,
values: null,
));
});
}
void _removeInputVariable(int index) {
setState(() {
_inputVariables.removeAt(index);
});
}
void _showPreview() {
setState(() {
_isPreviewVisible = true;
});
}
void _hidePreview() {
setState(() {
_isPreviewVisible = false;
});
}
void _testFormula() {
// Validate the formula before testing
if (!_validateFormula()) {
return;
}
final formula = _buildFormula();
if (formula == null) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FormulaScreen(
formula: formula,
corpus: widget.corpus,
),
),
);
}
bool _validateFormula() {
// Validate name
if (_nameController.text.trim().isEmpty) {
_showErrorDialog('Formula name cannot be empty');
return false;
}
// Validate output name
if (_outputVariable.nameController.text.trim().isEmpty) {
_showErrorDialog('Output variable name cannot be empty');
return false;
}
// Validate input variable names
for (final variable in _inputVariables) {
if (variable.nameController.text.trim().isEmpty) {
_showErrorDialog('Input variable names cannot be empty');
return false;
}
}
// Validate d4rt code
if (_d4rtCodeController.text.trim().isEmpty) {
_showErrorDialog('D4RT code cannot be empty');
return false;
}
return true;
}
Formula? _buildFormula() {
try {
final input = <VariableSpec>[];
for (final variable in _inputVariables) {
input.add(VariableSpec(
name: variable.nameController.text.trim(),
unit: variable.unit,
values: variable.values,
));
}
final output = VariableSpec(
name: _outputVariable.nameController.text.trim(),
unit: _outputVariable.unit,
);
return Formula(
name: _nameController.text.trim(),
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
input: input,
output: output,
d4rtCode: _d4rtCodeController.text,
tags: widget.formula.tags, // Preserve existing tags
);
} catch (e) {
_showErrorDialog('Error building formula: $e');
return null;
}
}
void _saveFormula() {
if (!_validateFormula()) {
return;
}
final formula = _buildFormula();
if (formula == null) return;
// For now, just show a success message
// In a real implementation, this would save to database
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Formula "${formula.name}" saved successfully!'),
backgroundColor: Theme.of(context).colorScheme.primary,
),
);
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Edit Formula'),
actions: [
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: _testFormula,
tooltip: 'Test Formula',
),
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveFormula,
tooltip: 'Save',
),
],
),
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
_buildNameSection(),
const SizedBox(height: 16),
_buildDescriptionSection(),
const SizedBox(height: 16),
_buildInputVariablesSection(),
const SizedBox(height: 16),
_buildOutputVariableSection(),
const SizedBox(height: 16),
_buildD4rtCodeSection(),
const SizedBox(height: 32),
],
),
),
),
);
}
Widget _buildNameSection() {
return TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Formula Name',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.title),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
);
}
Widget _buildDescriptionSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Description (Markdown)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isPreviewVisible)
TextButton.icon(
icon: const Icon(Icons.visibility_off),
label: const Text('Hide Preview'),
onPressed: _hidePreview,
)
else
TextButton.icon(
icon: const Icon(Icons.visibility),
label: const Text('Preview'),
onPressed: _showPreview,
),
],
),
],
),
const SizedBox(height: 8),
if (_isPreviewVisible) ...[
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Markdown(
data: _descriptionController.text,
shrinkWrap: true,
builders: {
'latex': LatexElementBuilder(),
},
extensionSet: markdown.ExtensionSet(
[LatexBlockSyntax()],
[LatexInlineSyntax()],
),
),
),
const SizedBox(height: 16),
],
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
hintText: 'Enter formula description (supports Markdown and LaTeX)',
border: OutlineInputBorder(),
),
maxLines: 5,
),
],
),
),
);
}
Widget _buildInputVariablesSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Input Variables',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
..._inputVariables.asMap().entries.map((entry) {
final index = entry.key;
final variable = entry.value;
return _buildInputVariableRow(index, variable);
}).toList(),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Add Input Variable'),
onPressed: _addInputVariable,
),
],
),
),
);
}
Widget _buildInputVariableRow(int index, _InputVariableRowData variable) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: variable.nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Base Unit', style: TextStyle(fontSize: 12)),
DropdownButtonFormField<String?>(
value: _getBaseUnit(variable.unit),
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('None', style: TextStyle(fontSize: 14)),
),
..._getAllBaseUnits().map((baseUnit) {
return DropdownMenuItem<String?>(
value: baseUnit,
child: Text(baseUnit, style: const TextStyle(fontSize: 14)),
);
}).toList(),
],
onChanged: (baseUnit) {
setState(() {
variable.unit = baseUnit;
});
},
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Derived Unit', style: TextStyle(fontSize: 12)),
DropdownButtonFormField<String?>(
value: variable.unit,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('None', style: TextStyle(fontSize: 14)),
),
..._getDerivedUnits(variable.unit).map((unit) {
final unitSpec = widget.corpus.getUnit(unit);
return DropdownMenuItem<String?>(
value: unit,
child: Text('${unitSpec.symbol} - ${unit}',
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
],
onChanged: (unit) {
setState(() {
variable.unit = unit;
});
},
),
],
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeInputVariable(index),
tooltip: 'Delete variable',
),
],
),
],
),
),
);
}
Widget _buildOutputVariableSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Output Variable',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: _outputVariable.nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Base Unit', style: TextStyle(fontSize: 12)),
DropdownButtonFormField<String?>(
value: _getBaseUnit(_outputVariable.unit),
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('None', style: TextStyle(fontSize: 14)),
),
..._getAllBaseUnits().map((baseUnit) {
return DropdownMenuItem<String?>(
value: baseUnit,
child: Text(baseUnit, style: const TextStyle(fontSize: 14)),
);
}).toList(),
],
onChanged: (baseUnit) {
setState(() {
_outputVariable.unit = baseUnit;
});
},
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Derived Unit', style: TextStyle(fontSize: 12)),
DropdownButtonFormField<String?>(
value: _outputVariable.unit,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('None', style: TextStyle(fontSize: 14)),
),
..._getDerivedUnits(_outputVariable.unit).map((unit) {
final unitSpec = widget.corpus.getUnit(unit);
return DropdownMenuItem<String?>(
value: unit,
child: Text('${unitSpec.symbol} - ${unit}',
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
],
onChanged: (unit) {
setState(() {
_outputVariable.unit = unit;
});
},
),
],
),
),
],
),
],
),
),
);
}
Widget _buildD4rtCodeSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'D4RT Code',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
child: Row(
children: [
Icon(Icons.code, size: 16, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
Text(
'Dart Syntax',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
TextFormField(
controller: _d4rtCodeController,
decoration: const InputDecoration(
hintText: 'Enter D4RT/Dart code here',
border: InputBorder.none,
contentPadding: EdgeInsets.all(12),
),
maxLines: 10,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 14,
),
),
],
),
),
],
),
),
);
}
// Helper methods for unit management
String? _getBaseUnit(String? unit) {
if (unit == null) return null;
try {
return widget.corpus.getUnit(unit).baseUnit;
} catch (e) {
return null;
}
}
List<String> _getAllBaseUnits() {
final baseUnits = <String>{};
for (final unit in widget.corpus.allUnits()) {
baseUnits.add(unit.baseUnit);
}
return baseUnits.toList()..sort();
}
List<String> _getDerivedUnits(String? baseUnit) {
if (baseUnit == null) return [];
return widget.corpus.unitsOfSameMagnitude(baseUnit)..sort();
}
}
// Data classes to track variable state
class _InputVariableRowData {
final TextEditingController nameController;
String? unit;
List<dynamic>? values;
_InputVariableRowData({
required this.nameController,
this.unit,
this.values,
});
}
class _OutputVariableRowData {
final TextEditingController nameController;
String? unit;
_OutputVariableRowData({
required this.nameController,
this.unit,
});
}

View file

@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; // For Clipboard
import 'package:d4rt_formulas/formula_models.dart';
import '../corpus.dart';
import 'formula_screen.dart';
import 'formula_editor.dart';
import 'package:share_plus/share_plus.dart';
class FormulaList extends StatefulWidget {
@ -72,6 +73,18 @@ class _FormulaListState extends State<FormulaList> {
}
}
void _editFormula(Formula formula) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FormulaEditor(
formula: formula,
corpus: widget.corpus,
),
),
);
}
void _copyFormula(Formula formula) async {
try {
// Get the formula and its dependencies
@ -144,8 +157,16 @@ class _FormulaListState extends State<FormulaList> {
subtitle: formula.tags.isNotEmpty
? Text('Tags: ${formula.tags.join(', ')}')
: null,
trailing: PopupMenuButton(
icon: Icon(Icons.share),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _editFormula(formula),
tooltip: 'Edit Formula',
),
PopupMenuButton(
icon: const Icon(Icons.share),
onSelected: (value) {
if (value == 'share') {
_shareFormula(formula);
@ -176,6 +197,8 @@ class _FormulaListState extends State<FormulaList> {
),
],
),
],
),
onTap: () {
Navigator.push(
context,

View file

@ -8,6 +8,7 @@ import '../formula_evaluator.dart';
import '../corpus.dart';
import '../error_handler.dart';
import 'unit_dropdown.dart';
import 'formula_editor.dart';
class FormulaScreen extends StatefulWidget {
final Formula formula;
@ -171,7 +172,26 @@ class _FormulaScreenState extends State<FormulaScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.formula.name)),
appBar: AppBar(
title: Text(widget.formula.name),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FormulaEditor(
formula: widget.formula,
corpus: widget.corpus,
),
),
);
},
tooltip: 'Edit Formula',
),
],
),
body: Form(
key: _formKey,
child: Padding(

View file

@ -62,6 +62,7 @@ Future<Corpus> createDefaultCorpus() async{
"assets/formulas/medical_and_bio.d4rt",
"assets/formulas/conversions_and_constants.d4rt",
"assets/formulas/misc_math.d4rt",
"assets/formulas/networking.d4rt",
];
for (final formRes in formulaResources) {