diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9d7cf80..5b38618 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
@@ -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
diff --git a/AGENTS.md b/AGENTS.md
index cbdd5ab..a57ac32 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
diff --git a/README.md b/README.md
index 71c3c0d..e60d8e2 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/SharpEquations.slnx b/SharpEquations.slnx
index 221091a..850f3ce 100644
--- a/SharpEquations.slnx
+++ b/SharpEquations.slnx
@@ -9,6 +9,8 @@
+
+
@@ -32,6 +34,7 @@
+
diff --git a/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/BrowserSettingsStore.cs b/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/BrowserSettingsStore.cs
new file mode 100644
index 0000000..364fc42
--- /dev/null
+++ b/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/BrowserSettingsStore.cs
@@ -0,0 +1,32 @@
+namespace SharpEquations.Avalonia.MathTools
+{
+ using System.Runtime.InteropServices.JavaScript;
+ using System.Runtime.Versioning;
+ using SharpEquations.MathTools;
+
+ /// Settings kept in the local storage of the page, under keys that start with mathtools..
+ [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);
+ }
+}
diff --git a/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/Program.cs b/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/Program.cs
index eed1006..ad4638a 100644
--- a/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/Program.cs
+++ b/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/Program.cs
@@ -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();
}
}
diff --git a/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/SharpEquations.Avalonia.Browser.MathTools.Demo.csproj b/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/SharpEquations.Avalonia.Browser.MathTools.Demo.csproj
index 71816c7..8019bc4 100644
--- a/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/SharpEquations.Avalonia.Browser.MathTools.Demo.csproj
+++ b/samples/SharpEquations.Avalonia.Browser.MathTools.Demo/SharpEquations.Avalonia.Browser.MathTools.Demo.csproj
@@ -17,7 +17,8 @@
-
+
@@ -25,11 +26,12 @@
-
+
+
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/AlgebraPanel.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/AlgebraPanel.cs
index ccb63c2..6539a61 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/AlgebraPanel.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/AlgebraPanel.cs
@@ -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;
///
- /// 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 ():
+ /// 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.
///
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 (_, _) =>
@@ -83,40 +85,40 @@ public AlgebraPanel(MathEditBox editor)
public ComboBox Letter { get; }
/// The value of the equation of the editor, as it was last computed.
- public AlgebraResult? Answer { get; private set; }
+ public CasResult? Answer { get; private set; }
/// The results of the actions, the last first.
- public IReadOnlyList Results => history;
+ public IReadOnlyList Results => history;
- private readonly List history = new List();
+ private readonly List history = new List();
/// Computes the value of the equation of the editor now, as typing does after a pause.
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());
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);
}
/// Does an action to the equation of the editor, as its button does, and adds the result to the list.
- public async Task RunAsync(string action)
+ public async Task 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!);
@@ -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 letters, out string? error)
{
- expression = null;
+ letters = Array.Empty();
error = null;
- var equation = editor.Equation;
+ equation = editor.Equation;
if (equation == null)
{
error = "The value of what is typed shows here.";
@@ -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)
@@ -156,27 +158,26 @@ 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 letters)
{
- var letters = expression == null ? Array.Empty() : Algebra.Variables(expression);
var selected = Letter.SelectedItem as string;
if (Letter.ItemsSource is IReadOnlyList old && old.SequenceEqual(letters))
return;
@@ -184,7 +185,7 @@ private void UpdateLetters(Entity? expression)
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;
@@ -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 }
@@ -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(),
};
}
}
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/CalculatorPage.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/CalculatorPage.cs
index 8faeb4d..7cd6a38 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/CalculatorPage.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/CalculatorPage.cs
@@ -8,6 +8,7 @@ namespace SharpEquations.Avalonia.MathTools
using global::Avalonia.Layout;
using global::Avalonia.Media;
using SharpEquations;
+ using SharpEquations.MathTools;
///
/// The scientific keypad: the keys type into the equation editor, which the keyboard types into too; what the
@@ -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
{
@@ -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
{
@@ -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),
},
};
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs
index 1820ded..6f63cbc 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs
@@ -9,6 +9,7 @@ namespace SharpEquations.Avalonia.MathTools
using global::Avalonia.Layout;
using global::Avalonia.Media;
using SharpEquations;
+ using SharpEquations.MathTools;
///
/// The library of formulas: each one drawn by a , filtered by subject and by name, and opened in
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/MainWindow.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/MainWindow.cs
index 558b530..ec1bdbc 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/MainWindow.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/MainWindow.cs
@@ -1,16 +1,17 @@
namespace SharpEquations.Avalonia.MathTools
{
using global::Avalonia.Controls;
+ using SharpEquations.MathTools;
- /// The window of the desktop app: the view the WebAssembly page shows too.
+ /// The window of the desktop app: the view the WebAssembly page shows too, with the settings kept where the store says (none by default).
public sealed class MainWindow : Window
{
- public MainWindow()
+ public MainWindow(ISettingsStore? settings = null)
{
Title = "SharpEquations Math Tools";
Width = 1180;
Height = 860;
- Content = new MathToolsView();
+ Content = new MathToolsView(settings);
}
}
}
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs
index ba76d3e..4362586 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs
@@ -1,25 +1,37 @@
namespace SharpEquations.Avalonia.MathTools
{
using System;
+ using System.Globalization;
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Layout;
using global::Avalonia.Media;
using global::Avalonia.Styling;
using SharpEquations;
+ using SharpEquations.MathTools;
///
/// The math tools, a page each: the scientific keypad around the equation editor, the library of formulas, the
- /// unit converter and the solver of triangles. The desktop window and the WebAssembly page show the same view.
+ /// unit converter and the solver of triangles. The desktop window and the WebAssembly page show the same view, each
+ /// with the services of its platform: where the settings are kept (the page shown, the equation of the calculator,
+ /// the theme), and the computer algebra system.
///
public sealed class MathToolsView : UserControl
{
private const string RepositoryUrl = "https://github.com/simonebortolin/SharpEquations";
- public MathToolsView()
+ private const string TabKey = "tab";
+ private const string EquationKey = "equation";
+ private const string ThemeKey = "theme";
+
+ private readonly ISettingsStore settings;
+
+ public MathToolsView(ISettingsStore? settings = null, ICasEngine? engine = null)
{
+ this.settings = settings ?? new MemorySettingsStore();
+ Engine = engine ?? new AngouriMathEngine();
Ui.AddBrushes(Resources);
- Calculator = new CalculatorPage();
+ Calculator = new CalculatorPage(Engine);
Converter = new UnitsPage();
TriangleSolver = new TrianglePage();
Tabs = new TabControl { Margin = new Thickness(20, 12, 20, 0) };
@@ -37,11 +49,18 @@ public MathToolsView()
Tab("Triangles", TriangleSolver),
};
+ Restore();
+ Tabs.SelectionChanged += (_, _) => this.settings.Set(TabKey, Tabs.SelectedIndex.ToString(CultureInfo.InvariantCulture));
+ Calculator.Editor.EquationChanged += (_, _) => this.settings.Set(EquationKey, Calculator.Editor.Equation?.ToString(EquationFormat.UnicodeMath));
+
var root = new DockPanel { Children = { Ui.Docked(Header(), Dock.Top), Tabs } };
root.Bind(Panel.BackgroundProperty, root.GetResourceObservable("PageBrush"));
Content = root;
}
+ /// The computer algebra system of the calculator.
+ public ICasEngine Engine { get; }
+
/// The page of the scientific keypad.
public CalculatorPage Calculator { get; }
@@ -57,6 +76,17 @@ public MathToolsView()
/// The tabs of the pages.
public TabControl Tabs { get; }
+ // the page, the equation and the theme of the last session
+ private void Restore()
+ {
+ if (int.TryParse(settings.Get(TabKey), NumberStyles.None, CultureInfo.InvariantCulture, out var tab) && tab < Tabs.ItemCount)
+ Tabs.SelectedIndex = tab;
+ if (settings.Get(EquationKey) is { } text && Equation.TryParse(text, EquationFormat.UnicodeMath, new EquationOptions { Display = true }, out var equation))
+ Calculator.Editor.Equation = equation;
+ if (Application.Current is { } app && settings.Get(ThemeKey) is { } theme)
+ app.RequestedThemeVariant = theme == "dark" ? ThemeVariant.Dark : ThemeVariant.Light;
+ }
+
private static TabItem Tab(string header, Control page) => new TabItem
{
Header = new TextBlock { Text = header, FontSize = 15, FontWeight = FontWeight.SemiBold },
@@ -99,7 +129,11 @@ private Border Header()
theme.Click += (_, _) =>
{
if (Application.Current is { } app)
- app.RequestedThemeVariant = app.ActualThemeVariant == ThemeVariant.Dark ? ThemeVariant.Light : ThemeVariant.Dark;
+ {
+ var dark = app.ActualThemeVariant != ThemeVariant.Dark;
+ app.RequestedThemeVariant = dark ? ThemeVariant.Dark : ThemeVariant.Light;
+ settings.Set(ThemeKey, dark ? "dark" : "light");
+ }
};
var github = HeaderButton("GitHub ↗", RepositoryUrl);
github.Click += async (_, _) =>
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/Program.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/Program.cs
index 85b9c18..6ab154a 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/Program.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/Program.cs
@@ -3,6 +3,7 @@ namespace SharpEquations.Avalonia.MathTools
using System;
using global::Avalonia;
using global::Avalonia.Controls.ApplicationLifetimes;
+ using SharpEquations.MathTools;
public static class Program
{
@@ -19,7 +20,7 @@ public sealed class App : Application
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
- desktop.MainWindow = new MainWindow();
+ desktop.MainWindow = new MainWindow(new FileSettingsStore(FileSettingsStore.DefaultPath));
base.OnFrameworkInitializationCompleted();
}
}
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/SharpEquations.Avalonia.MathTools.Demo.csproj b/samples/SharpEquations.Avalonia.MathTools.Demo/SharpEquations.Avalonia.MathTools.Demo.csproj
index a54a8bb..032dc72 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/SharpEquations.Avalonia.MathTools.Demo.csproj
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/SharpEquations.Avalonia.MathTools.Demo.csproj
@@ -1,6 +1,7 @@
-
+
WinExe
net10.0
@@ -12,11 +13,12 @@
-
+
+
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/TrianglePage.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/TrianglePage.cs
index 33df727..0044faf 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/TrianglePage.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/TrianglePage.cs
@@ -9,6 +9,7 @@ namespace SharpEquations.Avalonia.MathTools
using global::Avalonia.Layout;
using global::Avalonia.Media;
using SharpEquations;
+ using SharpEquations.MathTools;
///
/// The solver of triangles: three of the sides and angles give the others, the area, the perimeter and the radii of
@@ -107,11 +108,6 @@ public void Fill(IReadOnlyList values)
Solve();
}
- /// The values of a triangle as UnicodeMath: a=3, b=4, c=5 and α=36.8699°, β=53.1301°, γ=90°.
- public static (string Sides, string Angles) Describe(Triangle triangle) => (
- string.Join(", ", Enumerable.Range(0, 3).Select(i => Triangles.SideNames[i] + "=" + Units.Format(triangle.Side(i)))),
- string.Join(", ", Enumerable.Range(0, 3).Select(i => Triangles.AngleNames[i] + "=" + Units.Format(triangle.Angle(i)) + "°")));
-
private void Solve()
{
if (updating)
@@ -160,7 +156,7 @@ private void Show(TriangleSolution solution)
private static Grid Result(Triangle triangle, string? title)
{
- var (sides, angles) = Describe(triangle);
+ var (sides, angles) = Triangles.Describe(triangle);
var values = new StackPanel { Spacing = 10, VerticalAlignment = VerticalAlignment.Center };
if (title != null)
values.Children.Add(Ui.Small(title));
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs
index 9b683c9..2dfed11 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/UnitsPage.cs
@@ -7,6 +7,7 @@ namespace SharpEquations.Avalonia.MathTools
using global::Avalonia.Layout;
using global::Avalonia.Media;
using SharpEquations;
+ using SharpEquations.MathTools;
///
/// The unit converter: a value in a unit of a quantity, in another unit and in all of them, the results written in
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/Algebra.cs b/samples/SharpEquations.MathTools.Core/Cas/AngouriMathEngine.cs
similarity index 54%
rename from samples/SharpEquations.Avalonia.MathTools.Demo/Algebra.cs
rename to samples/SharpEquations.MathTools.Core/Cas/AngouriMathEngine.cs
index 814a8c9..24efe02 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/Algebra.cs
+++ b/samples/SharpEquations.MathTools.Core/Cas/AngouriMathEngine.cs
@@ -1,4 +1,4 @@
-namespace SharpEquations.Avalonia.MathTools
+namespace SharpEquations.MathTools
{
using System;
using System.Collections.Generic;
@@ -7,72 +7,63 @@ namespace SharpEquations.Avalonia.MathTools
using AngouriMath;
using SharpEquations;
- /// A result of : what was done, and the result as an equation (null if it cannot be drawn) and as text.
- public sealed record AlgebraResult(string Action, Equation? Math, string Text);
-
///
- /// What the calculator computes with AngouriMath: the value of an expression (exact, and as a decimal number),
- /// the solutions of an equation, simplification, expansion, factoring, derivatives and integrals. The results come
- /// back through LaTeX, which AngouriMath writes and SharpEquations reads.
+ /// The computer algebra of the math tools, with AngouriMath: the equation is read by , and
+ /// the results come back through LaTeX, which AngouriMath writes and SharpEquations reads.
///
- public static class Algebra
+ public sealed class AngouriMathEngine : ICasEngine
{
- public const string Evaluate = "Evaluate";
- public const string Solve = "Solve";
- public const string Simplify = "Simplify";
- public const string Expand = "Expand";
- public const string Factor = "Factor";
- public const string Differentiate = "Differentiate";
- public const string Integrate = "Integrate";
+ public string Name => "AngouriMath";
+
+ public IReadOnlyList Variables(Equation equation) => Variables(MathReader.Read(equation));
- /// The actions, in the order of the buttons.
- public static readonly IReadOnlyList Actions = new[] { Evaluate, Solve, Simplify, Expand, Factor, Differentiate, Integrate };
+ public CasResult Run(CasAction action, Equation equation, string? variable = null) => Run(action, MathReader.Read(equation), variable);
/// The letters of an expression, x first when there is one.
public static IReadOnlyList Variables(Entity expression) =>
expression.Vars.Select(v => v.Name).Distinct().OrderBy(v => v == "x" ? 0 : 1).ThenBy(v => v, StringComparer.Ordinal).ToList();
- /// Does an action to an expression read by ; is the letter to solve for, or to differentiate and integrate by.
- public static AlgebraResult Run(string action, Entity expression, string? variable)
+ /// Does an action to an expression of AngouriMath.
+ public static CasResult Run(CasAction action, Entity expression, string? variable)
{
var letters = Variables(expression);
var x = MathS.Var(variable ?? (letters.Count > 0 ? letters[0] : "x"));
switch (action)
{
- case Evaluate:
+ case CasAction.Evaluate:
return Value(expression);
- case Solve:
+ case CasAction.Solve:
var equation = expression is Entity.Equalsf ? expression : expression.Equalizes(0);
var solutions = equation.Solve(x).Simplify();
- return Result(Solve, Latex(x) + @"\in " + Latex(solutions), x.ToString() + " ∈ " + solutions.ToString());
- case Simplify:
- return Result(Simplify, expression.Simplify());
- case Expand:
- return Result(Expand, expression.Expand());
- case Factor:
- return Result(Factor, expression.Factorize());
- case Differentiate:
- return Result(Differentiate, expression.Differentiate(x).Simplify());
- case Integrate:
- return Result(Integrate, expression.Integrate(x).Simplify());
+ return Result(action, Latex(x) + @"\in " + Latex(solutions), x.ToString() + " ∈ " + solutions.ToString());
+ case CasAction.Simplify:
+ return Result(action, expression.Simplify());
+ case CasAction.Expand:
+ return Result(action, expression.Expand());
+ case CasAction.Factor:
+ return Result(action, expression.Factorize());
+ case CasAction.Differentiate:
+ return Result(action, expression.Differentiate(x).Simplify());
+ case CasAction.Integrate:
+ return Result(action, expression.Integrate(x).Simplify());
default:
- throw new ArgumentOutOfRangeException(nameof(action), action, "Not an action of the calculator.");
+ throw new ArgumentOutOfRangeException(nameof(action), action, "Not an action of the engine.");
}
}
// the exact value, and the decimal one when it differs: 1/3 ≈ 0.333333, √2 ≈ 1.41421
- private static AlgebraResult Value(Entity expression)
+ private static CasResult Value(Entity expression)
{
if (expression is Entity.Equalsf || expression.Vars.Any())
- return Result(Evaluate, expression.Simplify());
+ return Result(CasAction.Evaluate, expression.Simplify());
var exact = expression.Simplify();
var approximate = Decimal(exact.EvalNumerical());
var exactLatex = Latex(exact);
var exactText = exact.ToString();
if (approximate == null || approximate == exactText)
- return Result(Evaluate, exactLatex, exactText);
- return Result(Evaluate, exactLatex + @"\approx " + approximate, exactText + " ≈ " + approximate);
+ return Result(CasAction.Evaluate, exactLatex, exactText);
+ return Result(CasAction.Evaluate, exactLatex + @"\approx " + approximate, exactText + " ≈ " + approximate);
}
/// A number of AngouriMath as the calculator shows it: ten significant digits, a+bi for a complex one; null for what is not a number.
@@ -97,16 +88,16 @@ private static AlgebraResult Value(Entity expression)
private static string Format(double value) =>
double.IsInfinity(value) ? (value > 0 ? "∞" : "-∞") : value.ToString("G10", CultureInfo.InvariantCulture);
- private static AlgebraResult Result(string action, Entity value) => Result(action, Latex(value), value.ToString());
+ private static CasResult Result(CasAction action, Entity value) => Result(action, Latex(value), value.ToString());
- private static AlgebraResult Result(string action, string latex, string text)
+ private static CasResult Result(CasAction action, string latex, string text)
{
var options = new EquationOptions { Display = true };
var math = Equation.TryParse(latex, EquationFormat.LaTeX, options, out var parsed)
&& !parsed!.ToMathML().Descendants().Any(e => e.Name.LocalName == "merror")
? parsed
: null;
- return new AlgebraResult(action, math, text);
+ return new CasResult(action, math, text);
}
// the LaTeX of AngouriMath, with its constants as the LaTeX reader knows them
diff --git a/samples/SharpEquations.MathTools.Core/Cas/ICasEngine.cs b/samples/SharpEquations.MathTools.Core/Cas/ICasEngine.cs
new file mode 100644
index 0000000..0f1f11f
--- /dev/null
+++ b/samples/SharpEquations.MathTools.Core/Cas/ICasEngine.cs
@@ -0,0 +1,52 @@
+namespace SharpEquations.MathTools
+{
+ using System;
+ using System.Collections.Generic;
+ using SharpEquations;
+
+ /// What a computer algebra system does to an equation of the editor.
+ public enum CasAction
+ {
+ /// The value: exact, and as a decimal number when it differs.
+ Evaluate,
+
+ /// The solutions of an equation, or where an expression is 0.
+ Solve,
+
+ Simplify,
+
+ /// Products and powers multiplied out.
+ Expand,
+
+ /// The expression as a product.
+ Factor,
+
+ /// The derivative by a letter.
+ Differentiate,
+
+ /// An antiderivative by a letter.
+ Integrate,
+ }
+
+ /// A result of an : what was done, the result as an equation (null if it cannot be drawn) and as text.
+ public sealed record CasResult(CasAction Action, Equation? Math, string Text);
+
+ ///
+ /// A computer algebra system behind the math tools: it reads what an equation of the editor means and computes with
+ /// it. is the one of every platform; others can stand beside it (to compare results).
+ ///
+ public interface ICasEngine
+ {
+ /// The name of the engine, as the app shows it.
+ string Name { get; }
+
+ /// The letters of an equation, x first when there is one.
+ /// The equation has something the engine cannot read, as the message says.
+ IReadOnlyList Variables(Equation equation);
+
+ /// Does an action to an equation; is the letter to solve for, or to differentiate and integrate by (the first of if null).
+ /// The equation has something the engine cannot read, as the message says.
+ /// It can take long, and throw exceptions of other kinds for what the engine cannot do.
+ CasResult Run(CasAction action, Equation equation, string? variable = null);
+ }
+}
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/MathReader.cs b/samples/SharpEquations.MathTools.Core/Cas/MathReader.cs
similarity index 99%
rename from samples/SharpEquations.Avalonia.MathTools.Demo/MathReader.cs
rename to samples/SharpEquations.MathTools.Core/Cas/MathReader.cs
index a006aba..ffb82c6 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/MathReader.cs
+++ b/samples/SharpEquations.MathTools.Core/Cas/MathReader.cs
@@ -1,4 +1,4 @@
-namespace SharpEquations.Avalonia.MathTools
+namespace SharpEquations.MathTools
{
using System;
using System.Collections.Generic;
diff --git a/samples/SharpEquations.MathTools.Core/Services/FileSettingsStore.cs b/samples/SharpEquations.MathTools.Core/Services/FileSettingsStore.cs
new file mode 100644
index 0000000..4033097
--- /dev/null
+++ b/samples/SharpEquations.MathTools.Core/Services/FileSettingsStore.cs
@@ -0,0 +1,55 @@
+namespace SharpEquations.MathTools
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Text.Json;
+
+ /// Settings kept in a JSON file, written again at each change (the store of the desktop app).
+ public sealed class FileSettingsStore : ISettingsStore
+ {
+ private readonly string path;
+ private readonly Dictionary values;
+
+ public FileSettingsStore(string path)
+ {
+ this.path = path;
+ values = Load(path);
+ }
+
+ /// The file of the settings of the desktop app, in the application data of the user.
+ public static string DefaultPath { get; } =
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SharpEquations", "MathTools", "settings.json");
+
+ public string? Get(string key) => values.TryGetValue(key, out var value) ? value : null;
+
+ public void Set(string key, string? value)
+ {
+ if (value == null ? !values.Remove(key) : values.TryGetValue(key, out var old) && old == value)
+ return;
+ if (value != null)
+ values[key] = value;
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ File.WriteAllText(path, JsonSerializer.Serialize(values));
+ }
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException)
+ {
+ // settings that cannot be written are kept for the session
+ }
+ }
+
+ private static Dictionary Load(string path)
+ {
+ try
+ {
+ return File.Exists(path) ? JsonSerializer.Deserialize>(File.ReadAllText(path)) ?? new() : new();
+ }
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
+ {
+ return new Dictionary();
+ }
+ }
+ }
+}
diff --git a/samples/SharpEquations.MathTools.Core/Services/ISettingsStore.cs b/samples/SharpEquations.MathTools.Core/Services/ISettingsStore.cs
new file mode 100644
index 0000000..b595b1d
--- /dev/null
+++ b/samples/SharpEquations.MathTools.Core/Services/ISettingsStore.cs
@@ -0,0 +1,33 @@
+namespace SharpEquations.MathTools
+{
+ using System.Collections.Generic;
+
+ ///
+ /// Where the app keeps its settings between sessions, as text by key: a file on the desktop, the local storage of
+ /// the page in a browser, nothing in the tests.
+ ///
+ public interface ISettingsStore
+ {
+ /// The value of a key, or null if it has none.
+ string? Get(string key);
+
+ /// Sets the value of a key; null removes it.
+ void Set(string key, string? value);
+ }
+
+ /// Settings kept in memory only, for the session.
+ public sealed class MemorySettingsStore : ISettingsStore
+ {
+ private readonly Dictionary values = new Dictionary();
+
+ public string? Get(string key) => values.TryGetValue(key, out var value) ? value : null;
+
+ public void Set(string key, string? value)
+ {
+ if (value == null)
+ values.Remove(key);
+ else
+ values[key] = value;
+ }
+ }
+}
diff --git a/samples/SharpEquations.MathTools.Core/SharpEquations.MathTools.Core.csproj b/samples/SharpEquations.MathTools.Core/SharpEquations.MathTools.Core.csproj
new file mode 100644
index 0000000..5559665
--- /dev/null
+++ b/samples/SharpEquations.MathTools.Core/SharpEquations.MathTools.Core.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+ net10.0
+ false
+ enable
+ SharpEquations.MathTools
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs b/samples/SharpEquations.MathTools.Core/Tools/Formulas.cs
similarity index 99%
rename from samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs
rename to samples/SharpEquations.MathTools.Core/Tools/Formulas.cs
index 3d59723..df06ca0 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs
+++ b/samples/SharpEquations.MathTools.Core/Tools/Formulas.cs
@@ -1,4 +1,4 @@
-namespace SharpEquations.Avalonia.MathTools
+namespace SharpEquations.MathTools
{
using System;
using System.Collections.Generic;
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/Triangles.cs b/samples/SharpEquations.MathTools.Core/Tools/Triangles.cs
similarity index 94%
rename from samples/SharpEquations.Avalonia.MathTools.Demo/Triangles.cs
rename to samples/SharpEquations.MathTools.Core/Tools/Triangles.cs
index 3bf6d60..af7adc1 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/Triangles.cs
+++ b/samples/SharpEquations.MathTools.Core/Tools/Triangles.cs
@@ -1,4 +1,4 @@
-namespace SharpEquations.Avalonia.MathTools
+namespace SharpEquations.MathTools
{
using System;
using System.Collections.Generic;
@@ -44,6 +44,11 @@ public static class Triangles
public static double Degrees(double radians) => radians * 180 / Math.PI;
+ /// The values of a triangle as UnicodeMath: a=3, b=4, c=5 and α=36.8699°, β=53.1301°, γ=90°.
+ public static (string Sides, string Angles) Describe(Triangle triangle) => (
+ string.Join(", ", Enumerable.Range(0, 3).Select(i => Triangles.SideNames[i] + "=" + Units.Format(triangle.Side(i)))),
+ string.Join(", ", Enumerable.Range(0, 3).Select(i => Triangles.AngleNames[i] + "=" + Units.Format(triangle.Angle(i)) + "°")));
+
/// Solves the triangle of the values given (sides, and angles in degrees; null for the unknown ones).
public static TriangleSolution Solve(double? a, double? b, double? c, double? alpha, double? beta, double? gamma)
{
diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/Units.cs b/samples/SharpEquations.MathTools.Core/Tools/Units.cs
similarity index 99%
rename from samples/SharpEquations.Avalonia.MathTools.Demo/Units.cs
rename to samples/SharpEquations.MathTools.Core/Tools/Units.cs
index 4c609a3..0d117d5 100644
--- a/samples/SharpEquations.Avalonia.MathTools.Demo/Units.cs
+++ b/samples/SharpEquations.MathTools.Core/Tools/Units.cs
@@ -1,4 +1,4 @@
-namespace SharpEquations.Avalonia.MathTools
+namespace SharpEquations.MathTools
{
using System;
using System.Collections.Generic;
diff --git a/samples/SharpEquations.MathTools.Graphing/SharpEquations.MathTools.Graphing.csproj b/samples/SharpEquations.MathTools.Graphing/SharpEquations.MathTools.Graphing.csproj
new file mode 100644
index 0000000..0de858e
--- /dev/null
+++ b/samples/SharpEquations.MathTools.Graphing/SharpEquations.MathTools.Graphing.csproj
@@ -0,0 +1,11 @@
+
+
+
+
+ net10.0
+ false
+ enable
+ SharpEquations.MathTools.Graphing
+
+
+
diff --git a/samples/SharpEquations.MathTools.Graphing/Ticks.cs b/samples/SharpEquations.MathTools.Graphing/Ticks.cs
new file mode 100644
index 0000000..f44f33b
--- /dev/null
+++ b/samples/SharpEquations.MathTools.Graphing/Ticks.cs
@@ -0,0 +1,36 @@
+namespace SharpEquations.MathTools.Graphing
+{
+ using System;
+ using System.Collections.Generic;
+
+ /// The marks of an axis: round values (1, 2 or 5 times a power of ten) about count of them in a range.
+ public static class Ticks
+ {
+ /// The step between the marks: 1, 2 or 5 times a power of ten, closest to a range divided in .
+ public static double Step(double min, double max, int count = 8)
+ {
+ if (!(max > min) || count < 1)
+ throw new ArgumentOutOfRangeException(nameof(max), "The range is empty, or there is no mark to put.");
+ var rough = (max - min) / count;
+ var power = Math.Pow(10, Math.Floor(Math.Log10(rough)));
+ var fraction = rough / power;
+ return power * fraction switch
+ {
+ < 1.5 => 1,
+ < 3.5 => 2,
+ < 7.5 => 5,
+ _ => 10,
+ };
+ }
+
+ /// The multiples of between and , with 0 exactly 0.
+ public static IReadOnlyList Values(double min, double max, int count = 8)
+ {
+ var step = Step(min, max, count);
+ var values = new List();
+ for (var i = Math.Ceiling(min / step); i * step <= max; i++)
+ values.Add(i == 0 ? 0 : i * step);
+ return values;
+ }
+ }
+}
diff --git a/samples/SharpEquations.MathTools.Graphing/Viewport.cs b/samples/SharpEquations.MathTools.Graphing/Viewport.cs
new file mode 100644
index 0000000..8efb1a3
--- /dev/null
+++ b/samples/SharpEquations.MathTools.Graphing/Viewport.cs
@@ -0,0 +1,49 @@
+namespace SharpEquations.MathTools.Graphing
+{
+ using System;
+
+ ///
+ /// The region of the plane a graph shows, from (, ) to
+ /// (, ), and how it moves: panned by a fraction of its size, zoomed around a point.
+ ///
+ public sealed record Viewport(double XMin, double XMax, double YMin, double YMax)
+ {
+ /// The region of a new graph: −10 to 10 on both axes.
+ public static Viewport Default { get; } = new Viewport(-10, 10, -10, 10);
+
+ public double Width => XMax - XMin;
+
+ public double Height => YMax - YMin;
+
+ /// Whether the region has a size on both axes, and finite bounds.
+ public bool IsValid => Width > 0 && Height > 0 && double.IsFinite(Width) && double.IsFinite(Height);
+
+ /// The region moved by a fraction of its width and height (positive: right and up).
+ public Viewport Pan(double dx, double dy) => this with
+ {
+ XMin = XMin + dx * Width,
+ XMax = XMax + dx * Width,
+ YMin = YMin + dy * Height,
+ YMax = YMax + dy * Height,
+ };
+
+ /// The region scaled by around (, ), which stays where it is: below 1 zooms in.
+ public Viewport Zoom(double factor, double x, double y)
+ {
+ if (!(factor > 0) || double.IsInfinity(factor))
+ throw new ArgumentOutOfRangeException(nameof(factor), factor, "The factor of a zoom is a positive number.");
+ return new Viewport(x + (XMin - x) * factor, x + (XMax - x) * factor, y + (YMin - y) * factor, y + (YMax - y) * factor);
+ }
+
+ /// The region scaled around its center.
+ public Viewport Zoom(double factor) => Zoom(factor, (XMin + XMax) / 2, (YMin + YMax) / 2);
+
+ /// A point of the plane on a surface of × pixels, y down.
+ public (double X, double Y) ToScreen(double x, double y, double width, double height) =>
+ ((x - XMin) / Width * width, (YMax - y) / Height * height);
+
+ /// The point of the plane at a pixel of a surface of × pixels.
+ public (double X, double Y) FromScreen(double px, double py, double width, double height) =>
+ (XMin + px / width * Width, YMax - py / height * Height);
+ }
+}
diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs b/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs
index e0c489d..4f40c04 100644
--- a/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs
+++ b/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs
@@ -10,6 +10,7 @@ namespace SharpEquations.Avalonia.Tests.MathTools
using global::Avalonia.VisualTree;
using SharpEquations;
using SharpEquations.Avalonia.MathTools;
+ using SharpEquations.MathTools;
/// The window of the math tools opens, and its pages work (a picture of each with SHARPEQUATIONS_SCREENSHOTS).
[NotInParallel("headless")]
@@ -46,6 +47,30 @@ public async Task KeypadTypesIntoTheEditor()
await Assert.That(unicodeMath).IsNotNull();
}
+ /// The page and the equation of the last session come back from the settings, and the new ones go there.
+ [Test]
+ public async Task SettingsAreKept()
+ {
+ var settings = new MemorySettingsStore();
+ settings.Set("tab", "3");
+ settings.Set("equation", "x^2-1");
+ var (tab, equation, savedTab, savedEquation) = await Headless.Run(() =>
+ {
+ var window = new MainWindow(settings);
+ window.Show();
+ var view = window.GetVisualDescendants().OfType().Single();
+ var restored = (view.Tabs.SelectedIndex, view.Calculator.Editor.Equation);
+ view.Tabs.SelectedIndex = 1;
+ view.Calculator.Editor.Equation = Equation.Parse("a+b", EquationFormat.UnicodeMath);
+ window.Close();
+ return (restored.SelectedIndex, restored.Equation, settings.Get("tab"), settings.Get("equation"));
+ });
+ await Assert.That(tab).IsEqualTo(3);
+ await Assert.That(equation).IsEqualTo(Equation.Parse("x^2-1", EquationFormat.UnicodeMath, new EquationOptions { Display = true }));
+ await Assert.That(savedTab).IsEqualTo("1");
+ await Assert.That(Equation.Parse(savedEquation!, EquationFormat.UnicodeMath)).IsEqualTo(Equation.Parse("a+b", EquationFormat.UnicodeMath));
+ }
+
[Test]
public async Task FormulaOpensInTheCalculator()
{
@@ -145,10 +170,10 @@ public async Task CalculatorComputes()
var first = page.Solver.Answer;
page.Editor.Equation = Equation.Parse("x^2-5x+6=0", EquationFormat.UnicodeMath, display);
await page.Solver.RefreshAnswer();
- var second = await page.Solver.RunAsync(Algebra.Solve);
+ var second = await page.Solver.RunAsync(CasAction.Solve);
page.Editor.Equation = Equation.Parse("x^3 sin x", EquationFormat.UnicodeMath, display);
await page.Solver.RefreshAnswer();
- var third = await page.Solver.RunAsync(Algebra.Differentiate);
+ var third = await page.Solver.RunAsync(CasAction.Differentiate);
window.Height = 1500;
Save(window, "math-tools-compute");
page.Editor.Equation = Equation.Parse("\"text\"", EquationFormat.UnicodeMath, display);
diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/AlgebraTests.cs b/tests/SharpEquations.MathTools.Tests/AngouriMathEngineTests.cs
similarity index 69%
rename from tests/SharpEquations.Avalonia.Tests/MathTools/AlgebraTests.cs
rename to tests/SharpEquations.MathTools.Tests/AngouriMathEngineTests.cs
index f0f4d1e..cbd0910 100644
--- a/tests/SharpEquations.Avalonia.Tests/MathTools/AlgebraTests.cs
+++ b/tests/SharpEquations.MathTools.Tests/AngouriMathEngineTests.cs
@@ -1,15 +1,14 @@
-#if NET10_0_OR_GREATER
-namespace SharpEquations.Avalonia.Tests.MathTools
+namespace SharpEquations.MathTools.Tests
{
using System;
using System.Linq;
using System.Threading.Tasks;
using AngouriMath;
using SharpEquations;
- using SharpEquations.Avalonia.MathTools;
+ using SharpEquations.MathTools;
- /// What an equation means, read from its presentation MathML, and what the calculator computes from it with AngouriMath.
- public class AlgebraTests
+ /// What an equation means, read from its presentation MathML, and what the engine of the calculator computes from it with AngouriMath.
+ public class AngouriMathEngineTests
{
private static Entity Read(string unicodeMath) => MathReader.Read(Equation.Parse(unicodeMath, EquationFormat.UnicodeMath));
@@ -48,7 +47,7 @@ public async Task Evaluates(string unicodeMath, string expected)
var value = Read(unicodeMath);
if (value.Vars.Any())
value = value.Simplify();
- await Assert.That(Algebra.Decimal(value.EvalNumerical())).IsEqualTo(expected);
+ await Assert.That(AngouriMathEngine.Decimal(value.EvalNumerical())).IsEqualTo(expected);
}
[Test]
@@ -61,25 +60,25 @@ public async Task Solves(string unicodeMath, string variable, string[] real)
var expression = Read(unicodeMath);
var equation = expression is Entity.Equalsf ? expression : expression.Equalizes(0);
var solutions = ((Entity.Set.FiniteSet)equation.Solve(variable).Simplify())
- .Select(s => Algebra.Decimal(s.EvalNumerical())).ToList();
+ .Select(s => AngouriMathEngine.Decimal(s.EvalNumerical())).ToList();
foreach (var solution in real)
await Assert.That(solutions).Contains(solution);
}
/// The result of each action, drawn: it reads back from the LaTeX of AngouriMath without errors.
[Test]
- [Arguments("Evaluate", "1/3+1/6", "1/2 ≈ 0.5")]
- [Arguments("Evaluate", "√8", "2 * sqrt(2) ≈ 2.828427125")]
- [Arguments("Solve", "x^2-5x+6=0", null)]
- [Arguments("Simplify", "(x^2-1)/(x-1)", null)]
- [Arguments("Expand", "(x+1)^3", null)]
- [Arguments("Factor", "x^2-1", null)]
- [Arguments("Differentiate", "x^3 sin x", null)]
- [Arguments("Integrate", "x e^x", null)]
- [Arguments("Evaluate", "d/dx x^3", null)]
- public async Task ResultIsDrawn(string action, string unicodeMath, string? text)
+ [Arguments(CasAction.Evaluate, "1/3+1/6", "1/2 ≈ 0.5")]
+ [Arguments(CasAction.Evaluate, "√8", "2 * sqrt(2) ≈ 2.828427125")]
+ [Arguments(CasAction.Solve, "x^2-5x+6=0", null)]
+ [Arguments(CasAction.Simplify, "(x^2-1)/(x-1)", null)]
+ [Arguments(CasAction.Expand, "(x+1)^3", null)]
+ [Arguments(CasAction.Factor, "x^2-1", null)]
+ [Arguments(CasAction.Differentiate, "x^3 sin x", null)]
+ [Arguments(CasAction.Integrate, "x e^x", null)]
+ [Arguments(CasAction.Evaluate, "d/dx x^3", null)]
+ public async Task ResultIsDrawn(CasAction action, string unicodeMath, string? text)
{
- var result = Algebra.Run(action, Read(unicodeMath), null);
+ var result = new AngouriMathEngine().Run(action, Equation.Parse(unicodeMath, EquationFormat.UnicodeMath));
await Assert.That(result.Math).IsNotNull().Because(result.Text);
if (text != null)
await Assert.That(result.Text).IsEqualTo(text);
@@ -88,9 +87,10 @@ public async Task ResultIsDrawn(string action, string unicodeMath, string? text)
[Test]
public async Task SolvesForTheLetterAsked()
{
- var expression = Read("a x+b=0");
- await Assert.That(Algebra.Variables(expression)).IsEquivalentTo(new[] { "x", "a", "b" });
- var result = Algebra.Run(Algebra.Solve, expression, "x");
+ var engine = new AngouriMathEngine();
+ var equation = Equation.Parse("a x+b=0", EquationFormat.UnicodeMath);
+ await Assert.That(engine.Variables(equation)).IsEquivalentTo(new[] { "x", "a", "b" });
+ var result = engine.Run(CasAction.Solve, equation, "x");
await Assert.That(result.Text).Contains("b").And.Contains("a");
}
@@ -106,4 +106,3 @@ public async Task SaysWhatItCannotRead(string unicodeMath)
}
}
}
-#endif
diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs b/tests/SharpEquations.MathTools.Tests/FormulasTests.cs
similarity index 94%
rename from tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs
rename to tests/SharpEquations.MathTools.Tests/FormulasTests.cs
index f725fcb..6be978e 100644
--- a/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs
+++ b/tests/SharpEquations.MathTools.Tests/FormulasTests.cs
@@ -1,10 +1,9 @@
-#if NET10_0_OR_GREATER
-namespace SharpEquations.Avalonia.Tests.MathTools
+namespace SharpEquations.MathTools.Tests
{
using System.Linq;
using System.Threading.Tasks;
using SharpEquations;
- using SharpEquations.Avalonia.MathTools;
+ using SharpEquations.MathTools;
/// The formulas of the library parse without errors, and their UnicodeMath and LaTeX read back.
public class FormulasTests
@@ -38,4 +37,3 @@ public async Task FindBySubjectAndName()
}
}
}
-#endif
diff --git a/tests/SharpEquations.MathTools.Tests/GraphingTests.cs b/tests/SharpEquations.MathTools.Tests/GraphingTests.cs
new file mode 100644
index 0000000..ee0aa06
--- /dev/null
+++ b/tests/SharpEquations.MathTools.Tests/GraphingTests.cs
@@ -0,0 +1,48 @@
+namespace SharpEquations.MathTools.Tests
+{
+ using System.Threading.Tasks;
+ using SharpEquations.MathTools.Graphing;
+
+ /// The region of the plane a graph shows, and the marks of its axes.
+ public class GraphingTests
+ {
+ [Test]
+ [Arguments(-10, 10, 8, 2)]
+ [Arguments(0, 1, 8, 0.1)]
+ [Arguments(0, 1, 4, 0.2)]
+ [Arguments(-3, 3, 8, 1)]
+ [Arguments(0, 1000, 5, 200)]
+ [Arguments(0, 0.003, 6, 0.0005)]
+ public async Task StepIsRound(double min, double max, int count, double step) =>
+ await Assert.That(Ticks.Step(min, max, count)).IsEqualTo(step).Within(1e-12);
+
+ [Test]
+ public async Task ValuesHaveZero()
+ {
+ var values = Ticks.Values(-1, 1, 10);
+ await Assert.That(values.Count).IsEqualTo(11);
+ await Assert.That(values[5]).IsEqualTo(0);
+ await Assert.That(values[0]).IsEqualTo(-1).Within(1e-12);
+ }
+
+ [Test]
+ public async Task ZoomKeepsThePoint()
+ {
+ var zoomed = Viewport.Default.Zoom(0.5, 4, -2);
+ await Assert.That(zoomed).IsEqualTo(new Viewport(-3, 7, -6, 4));
+ var (x, y) = zoomed.FromScreen(350, 200, 500, 500);
+ await Assert.That(x).IsEqualTo(4);
+ await Assert.That(y).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task ScreenRoundTrips()
+ {
+ var viewport = new Viewport(-2, 6, -1, 3).Pan(0.25, -0.5);
+ await Assert.That(viewport).IsEqualTo(new Viewport(0, 8, -3, 1));
+ var (px, py) = viewport.ToScreen(2, 0, 800, 400);
+ await Assert.That(px).IsEqualTo(200);
+ await Assert.That(py).IsEqualTo(100);
+ }
+ }
+}
diff --git a/tests/SharpEquations.MathTools.Tests/SettingsTests.cs b/tests/SharpEquations.MathTools.Tests/SettingsTests.cs
new file mode 100644
index 0000000..43c0f5b
--- /dev/null
+++ b/tests/SharpEquations.MathTools.Tests/SettingsTests.cs
@@ -0,0 +1,49 @@
+namespace SharpEquations.MathTools.Tests
+{
+ using System.IO;
+ using System.Threading.Tasks;
+ using SharpEquations.MathTools;
+
+ /// The settings of the desktop app, kept in a file from one session to the next.
+ public class SettingsTests
+ {
+ [Test]
+ public async Task FileKeepsTheSettings()
+ {
+ var directory = Directory.CreateTempSubdirectory();
+ try
+ {
+ var path = Path.Combine(directory.FullName, "sub", "settings.json");
+ var store = new FileSettingsStore(path);
+ store.Set("tab", "2");
+ store.Set("equation", "x^2-1");
+ store.Set("equation", null);
+ var again = new FileSettingsStore(path);
+ await Assert.That(again.Get("tab")).IsEqualTo("2");
+ await Assert.That(again.Get("equation")).IsNull();
+ }
+ finally
+ {
+ directory.Delete(true);
+ }
+ }
+
+ [Test]
+ public async Task UnreadableFileIsNoSettings()
+ {
+ var path = Path.GetTempFileName();
+ try
+ {
+ File.WriteAllText(path, "{ not json");
+ var store = new FileSettingsStore(path);
+ await Assert.That(store.Get("tab")).IsNull();
+ store.Set("tab", "1");
+ await Assert.That(new FileSettingsStore(path).Get("tab")).IsEqualTo("1");
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+ }
+}
diff --git a/tests/SharpEquations.MathTools.Tests/SharpEquations.MathTools.Tests.csproj b/tests/SharpEquations.MathTools.Tests/SharpEquations.MathTools.Tests.csproj
new file mode 100644
index 0000000..cc0ee8e
--- /dev/null
+++ b/tests/SharpEquations.MathTools.Tests/SharpEquations.MathTools.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+ net10.0
+ Exe
+ false
+ SharpEquations.MathTools.Tests
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/TrianglesTests.cs b/tests/SharpEquations.MathTools.Tests/TrianglesTests.cs
similarity index 95%
rename from tests/SharpEquations.Avalonia.Tests/MathTools/TrianglesTests.cs
rename to tests/SharpEquations.MathTools.Tests/TrianglesTests.cs
index 6fe287d..b7dbf4b 100644
--- a/tests/SharpEquations.Avalonia.Tests/MathTools/TrianglesTests.cs
+++ b/tests/SharpEquations.MathTools.Tests/TrianglesTests.cs
@@ -1,12 +1,11 @@
-#if NET10_0_OR_GREATER
-namespace SharpEquations.Avalonia.Tests.MathTools
+namespace SharpEquations.MathTools.Tests
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SharpEquations;
- using SharpEquations.Avalonia.MathTools;
+ using SharpEquations.MathTools;
/// The cases of the solver of triangles, and the laws it writes as UnicodeMath.
public class TrianglesTests
@@ -136,7 +135,7 @@ public async Task StepsAndValuesParse()
texts.AddRange(solution.Steps);
foreach (var t in solution.Triangles)
{
- var (sides, angles) = TrianglePage.Describe(t);
+ var (sides, angles) = Triangles.Describe(t);
texts.Add(sides);
texts.Add(angles);
}
@@ -147,8 +146,7 @@ public async Task StepsAndValuesParse()
await Assert.That(equation.ToMathML().Descendants().Any(e => e.Name.LocalName == "merror")).IsFalse().Because(text);
await Assert.That(Equation.Parse(equation.ToString(EquationFormat.UnicodeMath), EquationFormat.UnicodeMath)).IsEqualTo(equation).Because(text);
}
- await Assert.That(TrianglePage.Describe(Triangles.Solve(3, 4, 5, null, null, null).Triangles[0])).IsEqualTo(("a=3, b=4, c=5", "α=36.8699°, β=53.1301°, γ=90°"));
+ await Assert.That(Triangles.Describe(Triangles.Solve(3, 4, 5, null, null, null).Triangles[0])).IsEqualTo(("a=3, b=4, c=5", "α=36.8699°, β=53.1301°, γ=90°"));
}
}
}
-#endif
diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/UnitsTests.cs b/tests/SharpEquations.MathTools.Tests/UnitsTests.cs
similarity index 97%
rename from tests/SharpEquations.Avalonia.Tests/MathTools/UnitsTests.cs
rename to tests/SharpEquations.MathTools.Tests/UnitsTests.cs
index 222fb02..cb31dd1 100644
--- a/tests/SharpEquations.Avalonia.Tests/MathTools/UnitsTests.cs
+++ b/tests/SharpEquations.MathTools.Tests/UnitsTests.cs
@@ -1,11 +1,10 @@
-#if NET10_0_OR_GREATER
-namespace SharpEquations.Avalonia.Tests.MathTools
+namespace SharpEquations.MathTools.Tests
{
using System;
using System.Linq;
using System.Threading.Tasks;
using SharpEquations;
- using SharpEquations.Avalonia.MathTools;
+ using SharpEquations.MathTools;
/// The conversions of the unit converter, and the values written as UnicodeMath.
public class UnitsTests
@@ -92,4 +91,3 @@ public async Task EverySymbolIsUniqueAndParses()
}
}
}
-#endif