Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ env:
src/SharpEquations.Avalonia/SharpEquations.Avalonia.csproj
TEST_PROJECT: tests/SharpEquations.Tests/SharpEquations.Tests.csproj
EDITOR_TEST_PROJECT: tests/SharpEquations.Avalonia.Tests/SharpEquations.Avalonia.Tests.csproj
MATH_TOOLS_TEST_PROJECT: tests/SharpEquations.MathTools.Tests/SharpEquations.MathTools.Tests.csproj
FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json

# Stages: Resolve Version -> Test (Linux, Windows, macOS) -> Pack NuGet
Expand Down Expand Up @@ -172,6 +173,14 @@ jobs:
echo "::endgroup::"
done

# the math tools without their views (samples/SharpEquations.MathTools.*, net10.0 only)
- name: Run math tools tests
shell: bash
run: |
dotnet run --project "$MATH_TOOLS_TEST_PROJECT" -c Release -f net10.0 --no-build -- \
--report-trx --report-trx-filename "${{ matrix.rid }}.mathtools.net10.0.trx" \
--results-directory "$GITHUB_WORKSPACE/TestResults"

- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
Expand Down
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ generated parser, on netstandard2.0): keep it so, and keep containers and other
| `src/SharpEquations.Avalonia/` | The controls for Avalonia: `MathEditBox` (the editor), `MathView`; embeds Libertinus Math |
| `samples/SharpEquations.Avalonia.Demo/` | A window with the editor (`DemoView`, shared with the web demo) |
| `samples/SharpEquations.Avalonia.Browser.Demo/` | The same demo on WebAssembly (needs the `wasm-tools` workload, so not in the solution; `.github/workflows/demo.yml` publishes it to GitHub Pages) |
| `samples/SharpEquations.Avalonia.MathTools.Demo/` | A second app, math tools around the editor (`MathToolsView`, a page each: the scientific keypad, the library of formulas, the unit converter, the solver of triangles; the calculator computes with AngouriMath, through `MathReader`, which reads what the normalized presentation MathML means) |
| `samples/SharpEquations.Avalonia.MathTools.Demo/` | A second app, math tools around the editor: its views (`MathToolsView`, a page each: the scientific keypad, the library of formulas, the unit converter, the solver of triangles) and the desktop window |
| `samples/SharpEquations.MathTools.Core/` | The math tools without a user interface: `ICasEngine` (`AngouriMathEngine`, through `MathReader`, which reads what the normalized presentation MathML means), the formulas, units and triangles, and the services the views ask the platform for (`ISettingsStore`: a file on the desktop, the local storage in the browser) |
| `samples/SharpEquations.MathTools.Graphing/` | The graphs of the math tools without a user interface: the region shown (`Viewport`) and the marks of the axes |
| `samples/SharpEquations.Avalonia.Browser.MathTools.Demo/` | The math tools on WebAssembly (as the web demo, not in the solution build; `demo.yml` publishes them under `math/`) |
| `benchmarks/SharpEquations.Benchmarks/` | BenchmarkDotNet benchmarks of reading, writing and converting the corpora |
| `tests/SharpEquations.Tests/` | TUnit tests, in folders by subject (PublicApi, MathML, Omml, Mtef, UnicodeMath, LaTeX, Property, Regression) |
| `tests/SharpEquations.Avalonia.Tests/` | TUnit tests of the editor (Editing, Rendering, and the controls in a headless Avalonia session with Skia) |
| `tests/SharpEquations.Avalonia.Tests/` | TUnit tests of the editor (Editing, Rendering, and the controls in a headless Avalonia session with Skia), and of the views of the math tools |
| `tests/SharpEquations.MathTools.Tests/` | TUnit tests of `SharpEquations.MathTools.Core` and `.Graphing` (net10.0) |
| `tests/data/` | Test data and its provenance (`tests/data/README.md`) |

## Code Style
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ UnicodeMath and LaTeX, and `samples/SharpEquations.Avalonia.Browser.Demo` the sa
second app around the editor, a set of math tools (a scientific keypad that types into the editor and computes what is
typed with [AngouriMath](https://github.com/asc-community/AngouriMath), a library of formulas, a unit converter, a
solver of triangles), with
`samples/SharpEquations.Avalonia.Browser.MathTools.Demo` in the browser (published under `math/` of the same site).
`samples/SharpEquations.Avalonia.Browser.MathTools.Demo` in the browser (published under `math/` of the same site); what
they compute is in `samples/SharpEquations.MathTools.Core` and `samples/SharpEquations.MathTools.Graphing`, which have
no user interface.

## Architecture

Expand Down
3 changes: 3 additions & 0 deletions SharpEquations.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
</Project>
<Project Path="samples/SharpEquations.Avalonia.Demo/SharpEquations.Avalonia.Demo.csproj" />
<Project Path="samples/SharpEquations.Avalonia.MathTools.Demo/SharpEquations.Avalonia.MathTools.Demo.csproj" />
<Project Path="samples/SharpEquations.MathTools.Core/SharpEquations.MathTools.Core.csproj" />
<Project Path="samples/SharpEquations.MathTools.Graphing/SharpEquations.MathTools.Graphing.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path="AGENTS.md" />
Expand All @@ -32,6 +34,7 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpEquations.Avalonia.Tests/SharpEquations.Avalonia.Tests.csproj" />
<Project Path="tests/SharpEquations.MathTools.Tests/SharpEquations.MathTools.Tests.csproj" />
<Project Path="tests/SharpEquations.Tests/SharpEquations.Tests.csproj" />
</Folder>
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace SharpEquations.Avalonia.MathTools
{
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using SharpEquations.MathTools;

/// <summary>Settings kept in the local storage of the page, under keys that start with <c>mathtools.</c>.</summary>
[SupportedOSPlatform("browser")]
public sealed partial class BrowserSettingsStore : ISettingsStore
{
private const string Prefix = "mathtools.";

public string? Get(string key) => GetItem(Prefix + key);

public void Set(string key, string? value)
{
if (value == null)
RemoveItem(Prefix + key);
else
SetItem(Prefix + key, value);
}

[JSImport("globalThis.localStorage.getItem")]
private static partial string? GetItem(string key);

[JSImport("globalThis.localStorage.setItem")]
private static partial void SetItem(string key, string value);

[JSImport("globalThis.localStorage.removeItem")]
private static partial void RemoveItem(string key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed class App : Application
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is ISingleViewApplicationLifetime view)
view.MainView = new MathToolsView();
view.MainView = new MathToolsView(new BrowserSettingsStore());
base.OnFrameworkInitializationCompleted();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@
</PropertyGroup>

<ItemGroup>
<!-- every file of the desktop app but its window and its entry point -->
<!-- the views of the desktop app (every file but its window and its entry point); the engine and the tools are
in SharpEquations.MathTools.Core, the graphs in SharpEquations.MathTools.Graphing -->
<Compile Include="..\SharpEquations.Avalonia.MathTools.Demo\*.cs" Exclude="..\SharpEquations.Avalonia.MathTools.Demo\Program.cs;..\SharpEquations.Avalonia.MathTools.Demo\MainWindow.cs" LinkBase="MathTools" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia.Browser" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="AngouriMath" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\SharpEquations.Avalonia\SharpEquations.Avalonia.csproj" />
<ProjectReference Include="..\SharpEquations.MathTools.Core\SharpEquations.MathTools.Core.csproj" />
<ProjectReference Include="..\SharpEquations.MathTools.Graphing\SharpEquations.MathTools.Graphing.csproj" />
</ItemGroup>

</Project>
83 changes: 42 additions & 41 deletions samples/SharpEquations.Avalonia.MathTools.Demo/AlgebraPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,45 @@ namespace SharpEquations.Avalonia.MathTools
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AngouriMath;
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Layout;
using global::Avalonia.Threading;
using SharpEquations;
using SharpEquations.MathTools;

/// <summary>
/// What the calculator computes from the equation of the editor, with AngouriMath: its value as it is typed, and on a
/// click its solutions, its simplified, expanded or factored form, its derivative and its integral, each result
/// drawn and ready to go back to the editor.
/// What the calculator computes from the equation of the editor, with a computer algebra system (<see cref="ICasEngine"/>):
/// its value as it is typed, and on a click its solutions, its simplified, expanded or factored form, its derivative
/// and its integral, each result drawn and ready to go back to the editor.
/// </summary>
public sealed class AlgebraPanel : UserControl
{
private static readonly TimeSpan Delay = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);

private static readonly (string Action, string Face, string Tip)[] Buttons =
private static readonly (CasAction Action, string Face, string Tip)[] Buttons =
{
(Algebra.Solve, "Solve", "The solutions of the equation, or where the expression is 0"),
(Algebra.Simplify, "Simplify", "The expression in a simpler form"),
(Algebra.Expand, "Expand", "Products and powers multiplied out"),
(Algebra.Factor, "Factor", "The expression as a product"),
(Algebra.Differentiate, "d/dx", "The derivative by the letter chosen"),
(Algebra.Integrate, "∫ dx", "An antiderivative by the letter chosen"),
(CasAction.Solve, "Solve", "The solutions of the equation, or where the expression is 0"),
(CasAction.Simplify, "Simplify", "The expression in a simpler form"),
(CasAction.Expand, "Expand", "Products and powers multiplied out"),
(CasAction.Factor, "Factor", "The expression as a product"),
(CasAction.Differentiate, "d/dx", "The derivative by the letter chosen"),
(CasAction.Integrate, "∫ dx", "An antiderivative by the letter chosen"),
};

private readonly MathEditBox editor;
private readonly ICasEngine engine;
private readonly MathView answer = new MathView { FontSize = 26, HorizontalAlignment = HorizontalAlignment.Left };
private readonly TextBlock answerNote = Ui.Muted("", 12.5);
private readonly StackPanel results = new StackPanel { Spacing = 12 };
private readonly DispatcherTimer timer;
private int version;

public AlgebraPanel(MathEditBox editor)
public AlgebraPanel(MathEditBox editor, ICasEngine engine)
{
this.editor = editor;
this.engine = engine;
Letter = new ComboBox { MinWidth = 70 };
timer = new DispatcherTimer { Interval = Delay };
timer.Tick += async (_, _) =>
Expand Down Expand Up @@ -83,40 +85,40 @@ public AlgebraPanel(MathEditBox editor)
public ComboBox Letter { get; }

/// <summary>The value of the equation of the editor, as it was last computed.</summary>
public AlgebraResult? Answer { get; private set; }
public CasResult? Answer { get; private set; }

/// <summary>The results of the actions, the last first.</summary>
public IReadOnlyList<AlgebraResult> Results => history;
public IReadOnlyList<CasResult> Results => history;

private readonly List<AlgebraResult> history = new List<AlgebraResult>();
private readonly List<CasResult> history = new List<CasResult>();

/// <summary>Computes the value of the equation of the editor now, as typing does after a pause.</summary>
public async Task RefreshAnswer()
{
timer.Stop();
var current = ++version;
if (!TryRead(out var expression, out var error))
if (!TryRead(out var equation, out var letters, out var error))
{
UpdateLetters(null);
UpdateLetters(Array.Empty<string>());
ShowAnswer(null, error);
return;
}
UpdateLetters(expression);
var (result, message) = await Compute(Algebra.Evaluate, expression!, null).ConfigureAwait(true);
UpdateLetters(letters);
var (result, message) = await Compute(CasAction.Evaluate, equation!, null).ConfigureAwait(true);
if (current == version)
ShowAnswer(result, message);
}

/// <summary>Does an action to the equation of the editor, as its button does, and adds the result to the list.</summary>
public async Task<AlgebraResult?> RunAsync(string action)
public async Task<CasResult?> RunAsync(CasAction action)
{
if (!TryRead(out var expression, out var error))
if (!TryRead(out var equation, out _, out var error))
{
AddMessage(error!);
return null;
}
var letter = Letter.SelectedItem as string;
var (result, message) = await Compute(action, expression!, letter).ConfigureAwait(true);
var (result, message) = await Compute(action, equation!, letter).ConfigureAwait(true);
if (result == null)
{
AddMessage(message!);
Expand All @@ -129,11 +131,11 @@ public async Task RefreshAnswer()
return result;
}

private bool TryRead(out Entity? expression, out string? error)
private bool TryRead(out Equation? equation, out IReadOnlyList<string> letters, out string? error)
{
expression = null;
letters = Array.Empty<string>();
error = null;
var equation = editor.Equation;
equation = editor.Equation;
if (equation == null)
{
error = "The value of what is typed shows here.";
Expand All @@ -146,7 +148,7 @@ private bool TryRead(out Entity? expression, out string? error)
}
try
{
expression = MathReader.Read(equation);
letters = engine.Variables(equation);
return true;
}
catch (NotSupportedException e)
Expand All @@ -156,35 +158,34 @@ private bool TryRead(out Entity? expression, out string? error)
}
}

// on another thread, since AngouriMath can take long (it cannot be stopped: a result too late is dropped)
private static async Task<(AlgebraResult? Result, string? Message)> Compute(string action, Entity expression, string? letter)
// on another thread, since the engine can take long (it cannot be stopped: a result too late is dropped)
private async Task<(CasResult? Result, string? Message)> Compute(CasAction action, Equation equation, string? letter)
{
var work = Task.Run(() => Algebra.Run(action, expression, letter));
var work = Task.Run(() => engine.Run(action, equation, letter));
if (await Task.WhenAny(work, Task.Delay(Timeout)).ConfigureAwait(true) != work)
return (null, "It takes too long: AngouriMath found no result in " + Timeout.TotalSeconds + " seconds.");
return (null, "It takes too long: " + engine.Name + " found no result in " + Timeout.TotalSeconds + " seconds.");
try
{
return (await work.ConfigureAwait(true), null);
}
#pragma warning disable CA1031 // what AngouriMath cannot do, it says with exceptions of many kinds
#pragma warning disable CA1031 // what the engine cannot do, it says with exceptions of many kinds
catch (Exception e)
#pragma warning restore CA1031
{
return (null, "AngouriMath cannot do it: " + e.Message);
return (null, engine.Name + " cannot do it: " + e.Message);
}
}

private void UpdateLetters(Entity? expression)
private void UpdateLetters(IReadOnlyList<string> letters)
{
var letters = expression == null ? Array.Empty<string>() : Algebra.Variables(expression);
var selected = Letter.SelectedItem as string;
if (Letter.ItemsSource is IReadOnlyList<string> old && old.SequenceEqual(letters))
return;
Letter.ItemsSource = letters;
Letter.SelectedItem = selected != null && letters.Contains(selected) ? selected : letters.Count > 0 ? letters[0] : null;
}

private void ShowAnswer(AlgebraResult? result, string? message)
private void ShowAnswer(CasResult? result, string? message)
{
Answer = result;
answer.Equation = result?.Math;
Expand All @@ -200,7 +201,7 @@ private void AddMessage(string message)
results.Children.RemoveAt(results.Children.Count - 1);
}

private Border ResultCard(AlgebraResult result, string label)
private Border ResultCard(CasResult result, string label)
{
Control math = result.Math != null
? new MathView { FontSize = 22, Equation = result.Math, HorizontalAlignment = HorizontalAlignment.Left }
Expand All @@ -222,12 +223,12 @@ private Border ResultCard(AlgebraResult result, string label)
return card;
}

private static string Label(string action, string letter) => action switch
private static string Label(CasAction action, string letter) => action switch
{
Algebra.Solve => "Solve for " + letter,
Algebra.Differentiate => "Derivative by " + letter,
Algebra.Integrate => "Antiderivative by " + letter,
_ => action,
CasAction.Solve => "Solve for " + letter,
CasAction.Differentiate => "Derivative by " + letter,
CasAction.Integrate => "Antiderivative by " + letter,
_ => action.ToString(),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace SharpEquations.Avalonia.MathTools
using global::Avalonia.Layout;
using global::Avalonia.Media;
using SharpEquations;
using SharpEquations.MathTools;

/// <summary>
/// The scientific keypad: the keys type into the equation editor, which the keyboard types into too; what the
Expand All @@ -18,7 +19,7 @@ public sealed class CalculatorPage : UserControl
private readonly TextBox unicodeMath = Code();
private readonly TextBox latex = Code();

public CalculatorPage()
public CalculatorPage(ICasEngine engine)
{
Editor = new MathEditBox
{
Expand Down Expand Up @@ -54,7 +55,7 @@ public CalculatorPage()
foreach (var key in CalculatorKeys.Editing)
editing.Children.Add(Key(key, "KeyBrush"));

Solver = new AlgebraPanel(Editor);
Solver = new AlgebraPanel(Editor, engine);

var formats = new Grid
{
Expand All @@ -72,7 +73,7 @@ public CalculatorPage()
Spacing = 16,
Children = { frame, keypad, editing },
}),
Ui.Card("Compute", "With AngouriMath: the value of what is typed, and its solutions, simpler forms, derivative and antiderivative", Solver),
Ui.Card("Compute", "With " + engine.Name + ": the value of what is typed, and its solutions, simpler forms, derivative and antiderivative", Solver),
Ui.Card("As text", "The equation in the linear formats, as it is typed", formats),
},
};
Expand Down
Loading
Loading