First version of a formula widget
This commit is contained in:
parent
2472e0db7c
commit
785fe72449
5 changed files with 423 additions and 8 deletions
|
|
@ -33,7 +33,6 @@ The file is a dart array of formulas. Each formula is a dart set literal
|
|||
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Features
|
||||
|
|
|
|||
366
lib/ai/FormulaWidget.dart
Normal file
366
lib/ai/FormulaWidget.dart
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class VariableSpec {
|
||||
final String name;
|
||||
final String magnitude;
|
||||
static final MAGNITUDELESS = "magnitudeless";
|
||||
|
||||
VariableSpec({required this.name, required this.magnitude});
|
||||
|
||||
@override
|
||||
String toString() => 'var($name: $magnitude)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is VariableSpec &&
|
||||
runtimeType == other.runtimeType &&
|
||||
magnitude == other.magnitude &&
|
||||
name == other.name;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(magnitude, name);
|
||||
}
|
||||
|
||||
class Formula {
|
||||
final String name;
|
||||
final List<VariableSpec> input;
|
||||
final VariableSpec output;
|
||||
final String d4rtCode;
|
||||
|
||||
Formula({
|
||||
required this.name,
|
||||
required this.input,
|
||||
required this.output,
|
||||
required this.d4rtCode,
|
||||
});
|
||||
}
|
||||
|
||||
class FormulaWidget extends StatelessWidget {
|
||||
final Formula formula;
|
||||
final double fontSize;
|
||||
final Color? textColor;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets padding;
|
||||
final bool showMagnitudes;
|
||||
final bool showCode;
|
||||
|
||||
const FormulaWidget({
|
||||
Key? key,
|
||||
required this.formula,
|
||||
this.fontSize = 16.0,
|
||||
this.textColor,
|
||||
this.backgroundColor,
|
||||
this.padding = const EdgeInsets.all(16.0),
|
||||
this.showMagnitudes = true,
|
||||
this.showCode = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: backgroundColor,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Formula name
|
||||
Text(
|
||||
formula.name,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 1.2,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor ?? Theme.of(context).textTheme.headlineSmall?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Formula equation
|
||||
_buildFormulaEquation(context),
|
||||
|
||||
if (showMagnitudes) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildMagnitudesSection(context),
|
||||
],
|
||||
|
||||
if (showCode) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildCodeSection(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormulaEquation(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Output variable
|
||||
_buildVariableChip(formula.output, isOutput: true, context: context),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Equals sign
|
||||
Text(
|
||||
'=',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 1.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Function notation
|
||||
Text(
|
||||
'${formula.name}(',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
|
||||
// Input variables
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (int i = 0; i < formula.input.length; i++) ...[
|
||||
_buildVariableChip(formula.input[i], context: context),
|
||||
if (i < formula.input.length - 1)
|
||||
Text(
|
||||
',',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Text(
|
||||
')',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVariableChip(VariableSpec variable, {bool isOutput = false, required BuildContext context}) {
|
||||
final bool hasMagnitude = variable.magnitude != VariableSpec.MAGNITUDELESS;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutput
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: isOutput
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
variable.name,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.9,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isOutput
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
if (hasMagnitude && showMagnitudes) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'[${variable.magnitude}]',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.8,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: (isOutput
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.secondary).withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMagnitudesSection(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Variables:',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.9,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor ?? Theme.of(context).textTheme.titleSmall?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
...formula.input.map((variable) => _buildVariableInfo(variable, context)),
|
||||
_buildVariableInfo(formula.output, context, isOutput: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVariableInfo(VariableSpec variable, BuildContext context, {bool isOutput = false}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isOutput ? Icons.arrow_forward : Icons.input,
|
||||
size: fontSize * 0.8,
|
||||
color: isOutput
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${variable.name}: ',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.85,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textColor ?? Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
variable.magnitude == VariableSpec.MAGNITUDELESS ? 'dimensionless' : variable.magnitude,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.85,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: (textColor ?? Theme.of(context).textTheme.bodyMedium?.color)?.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeSection(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Implementation:',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.9,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor ?? Theme.of(context).textTheme.titleSmall?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
formula.d4rtCode,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.8,
|
||||
fontFamily: 'monospace',
|
||||
color: textColor ?? Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage and demo
|
||||
class FormulaDisplayDemo extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Create sample formulas
|
||||
final sampleFormulas = [
|
||||
Formula(
|
||||
name: 'calculateArea',
|
||||
input: [
|
||||
VariableSpec(name: 'length', magnitude: 'meters'),
|
||||
VariableSpec(name: 'width', magnitude: 'meters'),
|
||||
],
|
||||
output: VariableSpec(name: 'area', magnitude: 'square_meters'),
|
||||
d4rtCode: 'return length * width;',
|
||||
),
|
||||
Formula(
|
||||
name: 'pythagoras',
|
||||
input: [
|
||||
VariableSpec(name: 'a', magnitude: 'meters'),
|
||||
VariableSpec(name: 'b', magnitude: 'meters'),
|
||||
],
|
||||
output: VariableSpec(name: 'c', magnitude: 'meters'),
|
||||
d4rtCode: 'return sqrt(a * a + b * b);',
|
||||
),
|
||||
Formula(
|
||||
name: 'normalize',
|
||||
input: [
|
||||
VariableSpec(name: 'value', magnitude: VariableSpec.MAGNITUDELESS),
|
||||
VariableSpec(name: 'max', magnitude: VariableSpec.MAGNITUDELESS),
|
||||
],
|
||||
output: VariableSpec(name: 'normalized', magnitude: VariableSpec.MAGNITUDELESS),
|
||||
d4rtCode: 'return value / max;',
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Formula Display Demo'),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final formula in sampleFormulas) ...[
|
||||
FormulaWidget(
|
||||
formula: formula,
|
||||
showCode: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -48,9 +48,7 @@ class FormulaEvaluator {
|
|||
try {
|
||||
// Build the complete d4rt source code with variable declarations
|
||||
final completeSource = _buildCompleteSource(formula, inputValues);
|
||||
//print( "$formula");
|
||||
print( "$completeSource" );
|
||||
|
||||
|
||||
// Execute the code using d4rt (no args needed since variables are in source)
|
||||
final result = _interpreter.execute(source: completeSource);
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,19 @@ class Formula {
|
|||
return Formula.fromSet(setLiteral);
|
||||
}
|
||||
|
||||
static List<Formula> fromArrayStringLiteral( String arrayStringLiteral ){
|
||||
var d4rt = D4rt();
|
||||
final buffer = StringBuffer();
|
||||
buffer.write( "main(){ return $arrayStringLiteral; }");
|
||||
final code = buffer.toString();
|
||||
|
||||
final List<Object?> list = d4rt.execute(source: code);
|
||||
|
||||
final formulas = list.map( (set) => Formula.fromSet(set as Map) );
|
||||
|
||||
return formulas.toList(growable: false) as List<Formula>;
|
||||
}
|
||||
|
||||
factory Formula.fromSet(Map<Object?, Object?> theSet) {
|
||||
|
||||
Object safeGet(Map<Object?, Object?> map, String key){
|
||||
|
|
@ -92,10 +105,6 @@ class Formula {
|
|||
return safeGet(map,key) as List<Object?>;
|
||||
}
|
||||
|
||||
Map<String, Object?> mapValue(Map<Object?, Object?> map, String key){
|
||||
return safeGet(map,key) as Map<String, Object?>;
|
||||
}
|
||||
|
||||
VariableSpec parseVar(Map<Object?, Object?> varSpec) {
|
||||
String name = stringValue(varSpec, "name");
|
||||
String magnitude = stringValue(varSpec, "magnitude");
|
||||
|
|
|
|||
|
|
@ -58,5 +58,48 @@ void main() {
|
|||
|
||||
});
|
||||
|
||||
|
||||
test( 'd4rt parses formula from list literal', (){
|
||||
final literal = """
|
||||
[
|
||||
{
|
||||
"name": "Newton's second law",
|
||||
"input": [
|
||||
{ "name": 'm', "magnitude": 'mass'},
|
||||
{ "name": 'a', "magnitude": 'acceleration'}
|
||||
],
|
||||
"output": { "name": 'F', "magnitude": 'force'},
|
||||
"d4rtCode": '''
|
||||
F = a * m;
|
||||
'''
|
||||
},
|
||||
{
|
||||
"name": "Newton's second law, again",
|
||||
"input": [
|
||||
{ "name": 'mass', "magnitude": 'mass'},
|
||||
{ "name": 'acc', "magnitude": 'acceleration'}
|
||||
],
|
||||
"output": { "name": 'force', "magnitude": 'force'},
|
||||
"d4rtCode": '''
|
||||
force = mass * acc;
|
||||
'''
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
final formulas = Formula.fromArrayStringLiteral(literal);
|
||||
final evaluator = FormulaEvaluator();
|
||||
|
||||
final formula = formulas[0];
|
||||
|
||||
final result = evaluator.evaluate(formula, {
|
||||
'm': 10.0, // 10 kg
|
||||
'a': 9.8, // 9.8 m/s²
|
||||
});
|
||||
|
||||
expect(result, 98.0); // F = m * a = 10 * 9.8 = 98 N
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue