diff --git a/.github/workflows/graphviewer-review.yml b/.github/workflows/graphviewer-review.yml
new file mode 100644
index 0000000..ad0795c
--- /dev/null
+++ b/.github/workflows/graphviewer-review.yml
@@ -0,0 +1,102 @@
+name: GraphViewer package review
+
+on:
+ push:
+ branches: [master, experimental/math-expression-migration]
+ paths: [GraphViewer/**, GraphViewer.Review/**, Directory.Build.props, .github/workflows/graphviewer-review.yml]
+ pull_request:
+ paths: [GraphViewer/**, GraphViewer.Review/**, Directory.Build.props, .github/workflows/graphviewer-review.yml]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ review:
+ runs-on: windows-2025
+ timeout-minutes: 20
+ env:
+ LocalNuGetPath: ${{ github.workspace }}/.package-review/graphviewer-feed
+ NUGET_PACKAGES: ${{ github.workspace }}/.package-review/graphviewer-packages
+ GRAPHVIEWER_REVIEW_OUTPUT: ${{ github.workspace }}/.package-review/graphviewer-images
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ defaults:
+ run:
+ shell: pwsh
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ path: consumer
+ - uses: actions/checkout@v4
+ with:
+ repository: LTRData/Library
+ ref: 0e853ea1abdc3d4484695db1e026ebf88535ae42
+ path: library
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 10.0.x
+ 9.0.x
+ - uses: microsoft/setup-msbuild@v3
+ - name: Configure the shared package feed
+ run: |
+ New-Item -ItemType Directory -Force $env:LocalNuGetPath | Out-Null
+ @'
+
+
+
+
+
+
+
+
+
+
+
+
+ '@ | Set-Content NuGet.Config
+ - name: Build Library packages
+ run: |
+ foreach ($name in @('LTRData.Extensions', 'LTRData.MathExpression', 'LTRData.FunctionPlotting')) {
+ $project = "library/$name/$name.csproj"
+ dotnet restore $project --configfile NuGet.Config
+ if ($LASTEXITCODE) { throw "Restore failed: $project" }
+ dotnet build $project -c Release --no-restore
+ if ($LASTEXITCODE) { throw "Package build failed: $project" }
+ }
+ - name: Restore GraphViewer packages
+ run: |
+ dotnet restore consumer/GraphViewer/GraphViewer.vbproj --configfile NuGet.Config
+ if ($LASTEXITCODE) { throw 'Consumer restore failed' }
+ - name: Build modern Windows targets
+ run: |
+ foreach ($framework in @('net8.0-windows', 'net9.0-windows', 'net10.0-windows')) {
+ dotnet build consumer/GraphViewer/GraphViewer.vbproj -c Release -f $framework --no-restore
+ if ($LASTEXITCODE) { throw "Build failed: $framework" }
+ }
+ - name: Build legacy targets using Windows resource tools
+ run: |
+ # Windows 2025 has VS 2022 MSBuild. Select its supported SDK for these
+ # two legacy targets; modern targets and the review still use .NET 10.
+ '{"sdk":{"version":"9.0.100","rollForward":"latestFeature"}}' | Set-Content consumer/global.json
+ Push-Location consumer
+ try {
+ foreach ($framework in @('net35', 'net40')) {
+ msbuild GraphViewer/GraphViewer.vbproj /p:Configuration=Release /p:TargetFramework=$framework /m
+ if ($LASTEXITCODE) { throw "Build failed: $framework" }
+ }
+ } finally {
+ Remove-Item global.json
+ Pop-Location
+ }
+ - name: Run native drawing and form scenarios
+ run: |
+ dotnet restore consumer/GraphViewer.Review/GraphViewer.Review.csproj --configfile NuGet.Config
+ if ($LASTEXITCODE) { throw 'Review restore failed' }
+ dotnet run --project consumer/GraphViewer.Review/GraphViewer.Review.csproj -c Release --no-restore
+ if ($LASTEXITCODE) { throw 'Review failed' }
+ - uses: actions/upload-artifact@v4
+ with:
+ name: graphviewer-review-images
+ path: ${{ env.GRAPHVIEWER_REVIEW_OUTPUT }}
+ if-no-files-found: error
diff --git a/GraphViewer.Review/GraphViewer.Review.csproj b/GraphViewer.Review/GraphViewer.Review.csproj
new file mode 100644
index 0000000..4ae4a00
--- /dev/null
+++ b/GraphViewer.Review/GraphViewer.Review.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net10.0-windows
+ true
+ true
+ enable
+ enable
+ false
+
+
+
+
+
diff --git a/GraphViewer.Review/Program.cs b/GraphViewer.Review/Program.cs
new file mode 100644
index 0000000..1b5034b
--- /dev/null
+++ b/GraphViewer.Review/Program.cs
@@ -0,0 +1,111 @@
+using System.Drawing.Imaging;
+using GraphViewer;
+using LTRData.FunctionPlotting;
+
+internal static class Program
+{
+ [STAThread]
+ private static int Main()
+ {
+ CheckGeometryAndDrawing();
+ CheckFormComposition();
+ Console.WriteLine("PASS: Windows drawing, five image formats, print margins, diagnostics, overlays, visibility and resize.");
+ return 0;
+ }
+
+ private static void CheckGeometryAndDrawing()
+ {
+ var definitions = new[] { PlotDefinition.Parse("sin(x)"), PlotDefinition.Parse("x^2") };
+ var frame = PlotSnapshot.Build(definitions, new NumericRange(-3, 3), new NumericRange(-2, 4), new CanvasSize(399, 239));
+ Require(frame.Layers.Count == 2, "both overlays are retained");
+ Require(frame.Layers.All(p => p.Curve.Polylines.Count > 0 && p.Derivative.Polylines.Count > 0 && p.Integral.Polylines.Count > 0), "all three curves are available");
+ var gap = PlotSnapshot.Build([PlotDefinition.Parse("1/x")], new NumericRange(-1, 1), new NumericRange(-10, 10), new CanvasSize(400, 240));
+ Require(gap.Layers[0].Curve.Polylines.Count == 2, "a non-finite sample splits the curve");
+ foreach (var source in new[] { "y+x", "sin(1,2)", "(x+" })
+ {
+ try { PlotDefinition.Parse(source); throw new Exception("Invalid expression accepted: " + source); }
+ catch (FormatException) { }
+ }
+
+ using var bitmap = new Bitmap(460, 300);
+ using (var graphics = Graphics.FromImage(bitmap))
+ {
+ graphics.Clear(Color.Magenta);
+ var originalTransform = graphics.Transform.Elements;
+ var originalClip = graphics.ClipBounds;
+ PlotDrawing.Render(graphics, frame, new Rectangle(30, 20, 400, 240), true, true, true);
+ Require(graphics.Transform.Elements.SequenceEqual(originalTransform), "drawing restores the caller's transform");
+ Require(graphics.ClipBounds == originalClip, "drawing restores the caller's clipping region");
+ }
+ Require(bitmap.GetPixel(0, 0).ToArgb() == Color.Magenta.ToArgb(), "print margins are untouched");
+ Require(bitmap.GetPixel(31, 21).ToArgb() == Color.White.ToArgb(), "print background is positioned at the margin");
+ var colored = 0;
+ for (var y = 20; y < 260; y++)
+ for (var x = 30; x < 430; x++)
+ if (bitmap.GetPixel(x, y).ToArgb() != Color.White.ToArgb()) colored++;
+ Require(colored > 200, "native rendering draws visible curves");
+ foreach (var format in new[] { ImageFormat.Bmp, ImageFormat.Gif, ImageFormat.Jpeg, ImageFormat.Png, ImageFormat.Tiff })
+ {
+ using var stream = new MemoryStream();
+ bitmap.Save(stream, format);
+ stream.Position = 0;
+ using var decoded = Image.FromStream(stream);
+ Require(decoded.Width == 460 && decoded.Height == 300, "encoded image dimensions: " + format);
+ }
+ var output = Environment.GetEnvironmentVariable("GRAPHVIEWER_REVIEW_OUTPUT");
+ if (!string.IsNullOrEmpty(output))
+ {
+ Directory.CreateDirectory(output);
+ bitmap.Save(Path.Combine(output, "graphviewer-print.png"), ImageFormat.Png);
+ }
+ }
+
+ private static void CheckFormComposition()
+ {
+ Application.EnableVisualStyles();
+ using var form = new GraphView { StartPosition = FormStartPosition.Manual, Location = new Point(-20000, -20000) };
+ form.Show();
+ Application.DoEvents();
+ var formula = (ComboBox)form.Controls.Find("cmbExpression", true).Single();
+ var picture = (PictureBox)form.Controls.Find("pbSurface", true).Single();
+ var menu = (MenuStrip)form.Controls.Find("MenuStrip", true).Single();
+ var items = AllItems(menu.Items).ToDictionary(p => p.Name!);
+ var clear = (ToolStripMenuItem)items["ClearSurfaceBeforeDrawingToolStripMenuItem"];
+ clear.Checked = true;
+ formula.Text = "sin(x)";
+ form.RedrawGraphs();
+ Require(form.CurrentPlot.Definitions.Count == 1, "replace mode creates one plot");
+ clear.Checked = false;
+ formula.Text = "x^2";
+ form.RedrawGraphs();
+ Require(form.CurrentPlot.Definitions.Count == 2, "overlay mode retains both formulas");
+ var derivative = (ToolStripMenuItem)items["DrawCalculatedderivativeGraphToolStripMenuItem"];
+ derivative.Checked = !derivative.Checked;
+ Require(form.CurrentPlot.Definitions.Count == 2, "visibility toggles do not append plots");
+ form.Size = new Size(900, 640);
+ Application.DoEvents();
+ Require(form.CurrentPlot.Definitions.Count == 2, "resize preserves overlays");
+ Require(form.CurrentPlot.Viewport.Canvas.Width == picture.Width - 1, "resize rebuilds geometry for the canvas");
+ using (var image = new Bitmap(picture.Width, picture.Height))
+ picture.DrawToBitmap(image, picture.ClientRectangle);
+ Require(form.CurrentPlot.Definitions.Count == 2, "painting does not append plots");
+ items["ClearDrawingSurfaceToolStripMenuItem"].PerformClick();
+ Require(form.CurrentPlot.Definitions.Count == 1 && form.CurrentPlot.Definitions[0].Source == "x^2", "clear retains the current formula");
+ form.Close();
+ }
+
+ private static IEnumerable AllItems(ToolStripItemCollection items)
+ {
+ foreach (ToolStripItem item in items)
+ {
+ yield return item;
+ if (item is ToolStripDropDownItem dropDown)
+ foreach (var child in AllItems(dropDown.DropDownItems)) yield return child;
+ }
+ }
+
+ private static void Require(bool condition, string message)
+ {
+ if (!condition) throw new Exception(message);
+ }
+}
diff --git a/GraphViewer/GraphView.Designer.vb b/GraphViewer/GraphView.Designer.vb
index dc1d675..2467bf5 100644
--- a/GraphViewer/GraphView.Designer.vb
+++ b/GraphViewer/GraphView.Designer.vb
@@ -313,7 +313,7 @@ Partial Class GraphView
Me.ClearSurfaceBeforeDrawingToolStripMenuItem.CheckState = System.Windows.Forms.CheckState.Checked
Me.ClearSurfaceBeforeDrawingToolStripMenuItem.Name = "ClearSurfaceBeforeDrawingToolStripMenuItem"
Me.ClearSurfaceBeforeDrawingToolStripMenuItem.Size = New System.Drawing.Size(279, 22)
- Me.ClearSurfaceBeforeDrawingToolStripMenuItem.Text = "&Clear surface before drawing new graph"
+ Me.ClearSurfaceBeforeDrawingToolStripMenuItem.Text = "&Replace previous graphs on redraw"
'
'DrawCalculatedderivativeGraphToolStripMenuItem
'
@@ -322,14 +322,14 @@ Partial Class GraphView
Me.DrawCalculatedderivativeGraphToolStripMenuItem.CheckState = System.Windows.Forms.CheckState.Checked
Me.DrawCalculatedderivativeGraphToolStripMenuItem.Name = "DrawCalculatedderivativeGraphToolStripMenuItem"
Me.DrawCalculatedderivativeGraphToolStripMenuItem.Size = New System.Drawing.Size(279, 22)
- Me.DrawCalculatedderivativeGraphToolStripMenuItem.Text = "Draw calculated &derivative graph"
+ Me.DrawCalculatedderivativeGraphToolStripMenuItem.Text = "Draw numerical &derivative graph"
'
'DrawCalculatedantiderivativeGraphToolStripMenuItem
'
Me.DrawCalculatedantiderivativeGraphToolStripMenuItem.CheckOnClick = True
Me.DrawCalculatedantiderivativeGraphToolStripMenuItem.Name = "DrawCalculatedantiderivativeGraphToolStripMenuItem"
Me.DrawCalculatedantiderivativeGraphToolStripMenuItem.Size = New System.Drawing.Size(279, 22)
- Me.DrawCalculatedantiderivativeGraphToolStripMenuItem.Text = "Draw calculated &antiderivative graph"
+ Me.DrawCalculatedantiderivativeGraphToolStripMenuItem.Text = "Draw numerical &integral graph"
'
'AboutToolStripMenuItem
'
diff --git a/GraphViewer/GraphView.vb b/GraphViewer/GraphView.vb
index 0d82c4c..4ac8fab 100644
--- a/GraphViewer/GraphView.vb
+++ b/GraphViewer/GraphView.vb
@@ -2,321 +2,194 @@ Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Globalization
Imports System.Windows.Forms
-Imports LTRLib.MathGraph
-Imports LTRData.MathExpression
+Imports LTRData.FunctionPlotting
#Disable Warning IDE1006 ' Naming Styles
Public Class GraphView
+ Private snapshot As PlotSnapshot
- Public Shared ReadOnly fntTimes As New Font("Times", 12)
-
- Public Shared ReadOnly sftRightAligned As New StringFormat With {
- .LineAlignment = StringAlignment.Center,
- .Alignment = StringAlignment.Far}
-
- Public Shared ReadOnly sftLeftAligned As New StringFormat With {
- .LineAlignment = StringAlignment.Center,
- .Alignment = StringAlignment.Near}
-
- Public Shared ReadOnly sftTopAligned As New StringFormat With {
- .LineAlignment = StringAlignment.Near,
- .Alignment = StringAlignment.Center}
-
- Public Shared ReadOnly sftBottomAligned As New StringFormat With {
- .LineAlignment = StringAlignment.Far,
- .Alignment = StringAlignment.Center}
-
- ' This is the object that provides formula evaluation functions
- Private ReadOnly ScriptControl As New ScriptControl(New MathExpressionParser(CultureInfo.InvariantCulture))
-
- ' This is the drawing surface instance for the form
- Private ReadOnly ScreenSurface As New Surface
-
- Private Property Ymax As Double
+ Public ReadOnly Property CurrentPlot As PlotSnapshot
Get
- Return Double.Parse(tbYmax.Text)
+ Return snapshot
End Get
- Set(value As Double)
- tbYmax.Text = value.ToString()
- End Set
End Property
- Private Property Ymin As Double
- Get
- Return Double.Parse(tbYmin.Text)
- End Get
- Set(value As Double)
- tbYmin.Text = value.ToString()
- End Set
- End Property
-
- Private Property Xmax As Double
- Get
- Return Double.Parse(tbXmax.Text)
- End Get
- Set(value As Double)
- tbXmax.Text = value.ToString()
- End Set
- End Property
-
- Private Property Xmin As Double
- Get
- Return Double.Parse(tbXmin.Text)
- End Get
- Set(value As Double)
- tbXmin.Text = value.ToString()
- End Set
- End Property
-
- Private Sub NumericTextBox_KeyPress(sender As Object, e As KeyPressEventArgs) Handles tbXmin.KeyPress, tbXmax.KeyPress, tbYmin.KeyPress, tbYmax.KeyPress
- If _
- (Not Char.IsDigit(e.KeyChar)) AndAlso
- e.KeyChar <> Microsoft.VisualBasic.ControlChars.Back AndAlso
- e.KeyChar <> "." AndAlso
- Not (e.KeyChar = "-" AndAlso
- DirectCast(sender, TextBox).SelectionStart = 0 AndAlso
- Not DirectCast(sender, TextBox).Text.StartsWith("-"c)) Then
-
- e.Handled = True
-
- End If
- End Sub
-
Private Sub cmbExpression_KeyPress(sender As Object, e As KeyPressEventArgs) Handles cmbExpression.KeyPress
If e.KeyChar = Microsoft.VisualBasic.ControlChars.Cr Then
e.Handled = True
-
cmbExpression.DroppedDown = False
-
- If cmbExpression.Text <> "" AndAlso Not cmbExpression.Items.Contains(cmbExpression.Text) Then
- cmbExpression.Items.Insert(0, cmbExpression.Text)
- End If
-
RedrawGraphs()
End If
End Sub
Public Sub RedrawGraphs()
- If Not Visible Then
- Exit Sub
- End If
-
- If ClearSurfaceBeforeDrawingToolStripMenuItem.Checked Then
- ScreenSurface.Clear()
- End If
+ CommitGraph(ClearSurfaceBeforeDrawingToolStripMenuItem.Checked)
+ End Sub
+ Private Sub CommitGraph(clearPrevious As Boolean)
+ If Not Visible OrElse pbSurface.Width < 2 OrElse pbSurface.Height < 2 Then Return
Try
- ScriptControl.Expression = cmbExpression.Text
- ScreenSurface.Refresh(ScriptControl)
- Catch Ex As Exception
- MessageBox.Show(Ex.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
+ Dim plot = PlotDefinition.Parse(cmbExpression.Text)
+ Dim xRange = ReadRange(tbXmin, tbXmax, "X")
+ Dim yRange = ReadRange(tbYmin, tbYmax, "Y")
+ Dim plots As New List(Of PlotDefinition)
+ If Not clearPrevious AndAlso snapshot IsNot Nothing Then plots.AddRange(snapshot.Definitions)
+ plots.Add(plot)
+ ' Commit only after the whole new frame succeeds. Invalid input keeps the previous graph.
+ Dim candidate = PlotSnapshot.Build(plots, xRange, yRange, CanvasFor(pbSurface.ClientSize))
+ snapshot = candidate
+ If Not cmbExpression.Items.Contains(plot.Source) Then cmbExpression.Items.Insert(0, plot.Source)
+ pbSurface.Invalidate()
+ Catch ex As Exception
+ MessageBox.Show(ex.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
End Try
-
- pbSurface.Refresh()
- End Sub
-
- Private Sub TextBox_Leave(sender As Object, e As EventArgs) Handles tbXmin.Leave, tbXmax.Leave, tbYmin.Leave, tbYmax.Leave
- With CType(sender, TextBox)
- .SelectionStart = 0
- .SelectionLength = .Text.Length
- End With
End Sub
- Private Sub ComboBox_Leave(sender As Object, e As EventArgs) Handles cmbExpression.Leave
- With CType(sender, ComboBox)
- .SelectionStart = 0
- .SelectionLength = .Text.Length
- End With
- End Sub
-
- Private Sub pbSurface_Layout(sender As Object, e As LayoutEventArgs) Handles pbSurface.Layout
- With ScreenSurface
- .Area.Size = CType(sender, PictureBox).ClientRectangle.Size
-
- .Clear()
- End With
+ Private Shared Function ReadRange(minimum As TextBox, maximum As TextBox, name As String) As NumericRange
+ Dim minValue, maxValue As Double
+ If Not TryReadNumber(minimum.Text, minValue) OrElse Not TryReadNumber(maximum.Text, maxValue) Then
+ Throw New FormatException($"The {name} range requires finite numbers.")
+ End If
+ If maxValue <= minValue OrElse Double.IsInfinity(maxValue - minValue) Then
+ Throw New FormatException($"The {name} maximum must exceed its minimum, with a finite difference.")
+ End If
+ Return New NumericRange(minValue, maxValue)
+ End Function
- RedrawGraphs()
- End Sub
+ Private Shared Function TryReadNumber(text As String, ByRef value As Double) As Boolean
+ ' Range controls accept local decimals and invariant scientific notation.
+ Return (Double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, value) OrElse
+ Double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, value)) AndAlso
+ Not Double.IsNaN(value) AndAlso Not Double.IsInfinity(value)
+ End Function
- Private Sub tbXmin_TextChanged(sender As Object, e As EventArgs) Handles tbXmin.TextChanged
- Try
- ScreenSurface.Xmin = Xmin
- Catch
- ScreenSurface.Xmin = 0
- End Try
- End Sub
+ Private Shared Function CanvasFor(size As Size) As CanvasSize
+ Return New CanvasSize(Math.Max(1, size.Width - 1), Math.Max(1, size.Height - 1))
+ End Function
- Private Sub tbXmax_TextChanged(sender As Object, e As EventArgs) Handles tbXmax.TextChanged
- Try
- ScreenSurface.Xmax = Xmax
- Catch
- ScreenSurface.Xmax = 0
- End Try
+ Private Sub TextBox_Leave(sender As Object, e As EventArgs) Handles tbXmin.Leave, tbXmax.Leave, tbYmin.Leave, tbYmax.Leave
+ DirectCast(sender, TextBox).SelectAll()
End Sub
- Private Sub tbYmin_TextChanged(sender As Object, e As EventArgs) Handles tbYmin.TextChanged
- Try
- ScreenSurface.Ymin = Ymin
- Catch
- ScreenSurface.Ymin = 0
- End Try
+ Private Sub ComboBox_Leave(sender As Object, e As EventArgs) Handles cmbExpression.Leave
+ DirectCast(sender, ComboBox).SelectAll()
End Sub
- Private Sub tbYmax_TextChanged(sender As Object, e As EventArgs) Handles tbYmax.TextChanged
+ Private Sub pbSurface_Layout(sender As Object, e As LayoutEventArgs) Handles pbSurface.Layout
+ If snapshot Is Nothing OrElse pbSurface.Width < 2 OrElse pbSurface.Height < 2 Then Return
Try
- ScreenSurface.Ymax = Ymax
- Catch
- ScreenSurface.Ymax = 0
+ snapshot = PlotSnapshot.Build(snapshot.Definitions, snapshot.Viewport.XRange,
+ snapshot.Viewport.YRange, CanvasFor(pbSurface.ClientSize))
+ pbSurface.Invalidate()
+ Catch ex As Exception
+ Debug.WriteLine(ex)
End Try
End Sub
- Private Sub ExitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ExitToolStripMenuItem.Click
- Close()
- End Sub
-
- Protected Overrides Sub OnFormClosed(e As FormClosedEventArgs)
- My.Settings.DrawDerivative = DrawCalculatedderivativeGraphToolStripMenuItem.Checked
- My.Settings.DrawAntiderivative = DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked
- My.Settings.ClearBeforeRedraw = ClearSurfaceBeforeDrawingToolStripMenuItem.Checked
- End Sub
-
Protected Overrides Sub OnLoad(e As EventArgs)
- PrintDocument.DefaultPageSettings.Landscape = True
-
+ MyBase.OnLoad(e)
+ If PrintDocument.PrinterSettings.IsValid Then PrintDocument.DefaultPageSettings.Landscape = True
DrawCalculatedderivativeGraphToolStripMenuItem.Checked = My.Settings.DrawDerivative
DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked = My.Settings.DrawAntiderivative
ClearSurfaceBeforeDrawingToolStripMenuItem.Checked = My.Settings.ClearBeforeRedraw
+ MenuStrip.ShowItemToolTips = True
+ DrawCalculatedderivativeGraphToolStripMenuItem.ToolTipText = "Numerical derivative; differences never cross gaps."
+ DrawCalculatedantiderivativeGraphToolStripMenuItem.ToolTipText = "Numerical integral, starting at zero at the left edge of each finite segment."
End Sub
Protected Overrides Sub OnShown(e As EventArgs)
- If cmbExpression.Text <> "" Then
- cmbExpression.Items.Add(cmbExpression.Text)
- End If
-
+ MyBase.OnShown(e)
RedrawGraphs()
End Sub
+ Protected Overrides Sub OnFormClosed(e As FormClosedEventArgs)
+ My.Settings.DrawDerivative = DrawCalculatedderivativeGraphToolStripMenuItem.Checked
+ My.Settings.DrawAntiderivative = DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked
+ My.Settings.ClearBeforeRedraw = ClearSurfaceBeforeDrawingToolStripMenuItem.Checked
+ My.Settings.Save()
+ MyBase.OnFormClosed(e)
+ End Sub
+
Private Sub ClearDrawingSurfaceToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ClearDrawingSurfaceToolStripMenuItem.Click
- ScreenSurface.Clear()
- RedrawGraphs()
+ CommitGraph(True)
End Sub
- Private Sub RedrawGraphToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles RedrawGraphToolStripMenuItem.Click, DrawCalculatedderivativeGraphToolStripMenuItem.CheckedChanged, DrawCalculatedantiderivativeGraphToolStripMenuItem.CheckedChanged
+ Private Sub RedrawGraphToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles RedrawGraphToolStripMenuItem.Click
RedrawGraphs()
End Sub
- Private Sub SaveAsPictureToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles SaveAsPictureToolStripMenuItem.Click
- Dim SelectedImageFormat As ImageFormat
-
- Try
- With SaveFileDialog
- If .ShowDialog(Me) = DialogResult.Cancel Then
- Exit Sub
- End If
-
- Select Case SaveFileDialog.FilterIndex
- Case 2
- SelectedImageFormat = ImageFormat.Gif
- Case 3
- SelectedImageFormat = ImageFormat.Jpeg
- Case 4
- SelectedImageFormat = ImageFormat.Png
- Case 5
- SelectedImageFormat = ImageFormat.Tiff
- Case Else
- SelectedImageFormat = ImageFormat.Bmp
- End Select
- End With
-
- Dim Bitmap As New Bitmap(pbSurface.Width, pbSurface.Height)
- pbSurface.DrawToBitmap(Bitmap, pbSurface.ClientRectangle)
- Bitmap.Save(SaveFileDialog.FileName, SelectedImageFormat)
- Catch Ex As Exception
- MessageBox.Show(Ex.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
- End Try
+ Private Sub CurveVisibilityChanged(sender As Object, e As EventArgs) Handles DrawCalculatedderivativeGraphToolStripMenuItem.CheckedChanged, DrawCalculatedantiderivativeGraphToolStripMenuItem.CheckedChanged
+ pbSurface.Invalidate()
End Sub
Private Sub pbSurface_Paint(sender As Object, e As PaintEventArgs) Handles pbSurface.Paint
- Try
+ If snapshot Is Nothing Then
e.Graphics.Clear(Color.DarkBlue)
+ Return
+ End If
+ PlotDrawing.Render(e.Graphics, snapshot, pbSurface.ClientRectangle,
+ DrawCalculatedderivativeGraphToolStripMenuItem.Checked,
+ DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked, False)
+ End Sub
- With ScreenSurface
- .DrawAxis(e.Graphics, Pens.DarkRed)
-
- If DrawCalculatedderivativeGraphToolStripMenuItem.Checked Then
- .DrawDerivative(e.Graphics, Pens.DarkGreen)
- End If
- If DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked Then
- .DrawIntegral(e.Graphics, Pens.Blue)
- End If
- .DrawGraph(e.Graphics, Pens.Yellow)
- End With
- Catch
+ Private Sub SaveAsPictureToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles SaveAsPictureToolStripMenuItem.Click
+ If snapshot Is Nothing Then Return
+ Try
+ If SaveFileDialog.ShowDialog(Me) <> DialogResult.OK Then Return
+ Dim formats = {ImageFormat.Bmp, ImageFormat.Gif, ImageFormat.Jpeg, ImageFormat.Png, ImageFormat.Tiff}
+ Using bitmap As New Bitmap(pbSurface.Width, pbSurface.Height)
+ Using drawing = Graphics.FromImage(bitmap)
+ PlotDrawing.Render(drawing, snapshot, pbSurface.ClientRectangle,
+ DrawCalculatedderivativeGraphToolStripMenuItem.Checked,
+ DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked, False)
+ End Using
+ bitmap.Save(SaveFileDialog.FileName, formats(Math.Max(0, Math.Min(4, SaveFileDialog.FilterIndex - 1))))
+ End Using
+ Catch ex As Exception
+ MessageBox.Show(ex.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
End Try
End Sub
Private Sub PrintDocument_PrintPage(sender As Object, e As Drawing.Printing.PrintPageEventArgs) Handles PrintDocument.PrintPage
- With New Surface
- .Xmin = ScreenSurface.Xmin
- .Xmax = ScreenSurface.Xmax
- .Ymin = ScreenSurface.Ymin
- .Ymax = ScreenSurface.Ymax
-
- .Area = e.MarginBounds
-
- ScriptControl.Expression = cmbExpression.Text
- .Refresh(ScriptControl)
-
- .DrawAxis(e.Graphics, Pens.Orange)
-
- If DrawCalculatedderivativeGraphToolStripMenuItem.Checked Then
- .DrawDerivative(e.Graphics, Pens.LightGray)
- End If
- If DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked Then
- .DrawIntegral(e.Graphics, Pens.DarkGray)
- End If
- .DrawGraph(e.Graphics, Pens.DarkRed)
- End With
-
- With e.Graphics
- .DrawString("y = " & cmbExpression.Text, fntTimes, Brushes.DarkRed, New Rectangle(e.MarginBounds.Left, e.PageBounds.Top, e.MarginBounds.Width, e.MarginBounds.Top), sftLeftAligned)
-
- .DrawString(tbXmin.Text, fntTimes, Brushes.Black, New RectangleF(e.PageBounds.Left, e.MarginBounds.Top, e.MarginBounds.Left, CSng(Ymax * 2 * e.MarginBounds.Height / (Ymax - Ymin))), sftRightAligned)
- .DrawString(tbXmax.Text, fntTimes, Brushes.Black, New RectangleF(e.MarginBounds.Right, e.MarginBounds.Top, e.PageBounds.Right - e.MarginBounds.Right, CSng(Ymax * 2 * e.MarginBounds.Height / (Ymax - Ymin))), sftLeftAligned)
-
- .DrawString(tbYmin.Text, fntTimes, Brushes.Black, New RectangleF(e.MarginBounds.Left, e.MarginBounds.Bottom, CSng(Xmin * -2 * e.MarginBounds.Width / (Xmax - Xmin)), e.PageBounds.Bottom - e.MarginBounds.Bottom), sftTopAligned)
- .DrawString(tbYmax.Text, fntTimes, Brushes.Black, New RectangleF(e.MarginBounds.Left, e.PageBounds.Top, CSng(Xmin * -2 * e.MarginBounds.Width / (Xmax - Xmin)), e.MarginBounds.Top), sftBottomAligned)
- End With
+ If snapshot Is Nothing Then Return
+ Dim printed = PlotSnapshot.Build(snapshot.Definitions, snapshot.Viewport.XRange,
+ snapshot.Viewport.YRange, CanvasFor(e.MarginBounds.Size))
+ PlotDrawing.Render(e.Graphics, printed, e.MarginBounds,
+ DrawCalculatedderivativeGraphToolStripMenuItem.Checked,
+ DrawCalculatedantiderivativeGraphToolStripMenuItem.Checked, True)
+ Using font As New Font("Times New Roman", 12), alignment As New StringFormat With {.Alignment = StringAlignment.Near}
+ Dim formulas = String.Join("; ", printed.Definitions.Select(Function(p) "y = " & p.Source).ToArray())
+ Dim header As New RectangleF(e.MarginBounds.Left, e.PageBounds.Top, e.MarginBounds.Width, e.MarginBounds.Top)
+ e.Graphics.DrawString(formulas, font, Brushes.DarkRed, header, alignment)
+ Dim ranges = $"X: {printed.Viewport.XRange.Minimum} to {printed.Viewport.XRange.Maximum} Y: {printed.Viewport.YRange.Minimum} to {printed.Viewport.YRange.Maximum}"
+ e.Graphics.DrawString(ranges, font, Brushes.Black, e.MarginBounds.Left, e.MarginBounds.Bottom + 4)
+ End Using
End Sub
Private Sub PrintToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles PrintToolStripMenuItem.Click
- If PrintDialog.ShowDialog() = DialogResult.OK Then
- PrintDocument.Print()
- End If
+ If snapshot IsNot Nothing AndAlso PrintDialog.ShowDialog(Me) = DialogResult.OK Then PrintDocument.Print()
End Sub
Private Sub PrintPreviewToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles PrintPreviewToolStripMenuItem.Click
- PrintPreviewDialog.ShowDialog(Me)
+ If snapshot IsNot Nothing Then PrintPreviewDialog.ShowDialog(Me)
End Sub
Private Sub PrintPreviewDialog_Load(sender As Object, e As EventArgs) Handles PrintPreviewDialog.Load
- With PrintPreviewDialog
- .Left = Left + 10
- .Top = Top + 40
- .Width = ScreenSurface.Area.Width - 20
- .Height = ScreenSurface.Area.Height
- End With
+ PrintPreviewDialog.SetBounds(Left + 10, Top + 40, Math.Max(300, Width - 20), Math.Max(300, Height))
End Sub
Private Sub PrintSetupToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles PrintSetupToolStripMenuItem.Click
PageSetupDialog.ShowDialog(Me)
End Sub
+ Private Sub ExitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ExitToolStripMenuItem.Click
+ Close()
+ End Sub
+
Private Sub AboutToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click
- Using AboutBox As New AboutBox
- AboutBox.ShowDialog(Me)
+ Using about As New AboutBox
+ about.ShowDialog(Me)
End Using
End Sub
-End Class
\ No newline at end of file
+End Class
diff --git a/GraphViewer/GraphViewer.vbproj b/GraphViewer/GraphViewer.vbproj
index bf8bf98..829c875 100644
--- a/GraphViewer/GraphViewer.vbproj
+++ b/GraphViewer/GraphViewer.vbproj
@@ -24,7 +24,8 @@
-
+
+
diff --git a/GraphViewer/PlotDrawing.vb b/GraphViewer/PlotDrawing.vb
new file mode 100644
index 0000000..5f8b5bf
--- /dev/null
+++ b/GraphViewer/PlotDrawing.vb
@@ -0,0 +1,54 @@
+Imports System.Drawing
+Imports System.Drawing.Drawing2D
+Imports LTRData.FunctionPlotting
+
+' System.Drawing stays at the Windows application boundary.
+Public Module PlotDrawing
+ Public Sub Render(graphics As Graphics, snapshot As PlotSnapshot, bounds As Rectangle,
+ drawDerivative As Boolean, drawIntegral As Boolean, forPrint As Boolean)
+#If NET6_0_OR_GREATER Then
+ ArgumentNullException.ThrowIfNull(graphics)
+ ArgumentNullException.ThrowIfNull(snapshot)
+#Else
+ If graphics Is Nothing Then Throw New ArgumentNullException(NameOf(graphics))
+ If snapshot Is Nothing Then Throw New ArgumentNullException(NameOf(snapshot))
+#End If
+ If bounds.Width <= 0 OrElse bounds.Height <= 0 Then Return
+ Dim state = graphics.Save()
+ Try
+ graphics.SetClip(bounds, CombineMode.Intersect)
+ Using background As New SolidBrush(If(forPrint, Color.White, Color.DarkBlue))
+ graphics.FillRectangle(background, bounds)
+ End Using
+ graphics.TranslateTransform(bounds.Left, bounds.Top)
+ graphics.SmoothingMode = SmoothingMode.AntiAlias
+ Dim viewport = snapshot.Viewport
+ Dim axisPen = If(forPrint, Pens.Orange, Pens.DarkRed)
+ If viewport.YRange.Contains(0) Then
+ Dim origin = CartesianTransform.ToCanvas(viewport.XRange.Minimum, 0, viewport)
+ graphics.DrawLine(axisPen, 0.0F, CSng(origin.Y), CSng(viewport.Canvas.Width), CSng(origin.Y))
+ End If
+ If viewport.XRange.Contains(0) Then
+ Dim origin = CartesianTransform.ToCanvas(0, viewport.YRange.Minimum, viewport)
+ graphics.DrawLine(axisPen, CSng(origin.X), 0.0F, CSng(origin.X), CSng(viewport.Canvas.Height))
+ End If
+ For Each layer In snapshot.Layers
+ If drawDerivative Then DrawCurve(graphics, layer.Derivative, If(forPrint, Pens.LightGray, Pens.DarkGreen))
+ If drawIntegral Then DrawCurve(graphics, layer.Integral, If(forPrint, Pens.DarkGray, Pens.Blue))
+ DrawCurve(graphics, layer.Curve, If(forPrint, Pens.DarkRed, Pens.Yellow))
+ Next
+ Finally
+ graphics.Restore(state)
+ End Try
+ End Sub
+
+ Private Sub DrawCurve(graphics As Graphics, geometry As CurveGeometry, pen As Pen)
+ For Each line In geometry.Polylines
+ Dim points(line.Points.Count - 1) As PointF
+ For index = 0 To points.Length - 1
+ points(index) = New PointF(CSng(line.Points(index).X), CSng(line.Points(index).Y))
+ Next
+ If points.Length >= 2 Then graphics.DrawLines(pen, points)
+ Next
+ End Sub
+End Module
diff --git a/GraphViewer/PlotSnapshot.vb b/GraphViewer/PlotSnapshot.vb
new file mode 100644
index 0000000..8006e05
--- /dev/null
+++ b/GraphViewer/PlotSnapshot.vb
@@ -0,0 +1,84 @@
+Imports System.Collections.ObjectModel
+Imports LTRData.FunctionPlotting
+Imports LTRData.MathExpression
+
+' Application-owned plot definitions contain no native drawing resources.
+Public NotInheritable Class PlotDefinition
+ Private Sub New(source As String, formula As UnaryMathFunction)
+ Me.Source = source
+ Me.Formula = formula
+ End Sub
+
+ Public ReadOnly Property Source As String
+ Public ReadOnly Property Formula As UnaryMathFunction
+
+ Public Shared Function Parse(source As String) As PlotDefinition
+ Dim parsed = MathParser.Default.Parse(source)
+ If Not parsed.Success Then
+ Throw New FormatException(FormatDiagnostics(parsed.Diagnostics))
+ End If
+ Dim binding = MathBinder.Bind(parsed.Root, MathSymbolCatalog.Standard)
+ If Not binding.Success Then
+ Throw New FormatException(FormatDiagnostics(binding.Diagnostics))
+ End If
+ If binding.Expression.Variables.Any(Function(v) Not String.Equals(v.Name, "x", StringComparison.OrdinalIgnoreCase)) Then
+ Throw New FormatException("A graph may use x as its only variable. Previous-y recurrence is not supported.")
+ End If
+ Return New PlotDefinition(source, binding.Expression.BindUnary("x"))
+ End Function
+
+ Private Shared Function FormatDiagnostics(diagnostics As IEnumerable(Of MathDiagnostic)) As String
+ Return String.Join(Environment.NewLine, diagnostics.Select(Function(d) d.ToString()).ToArray())
+ End Function
+End Class
+
+Public NotInheritable Class PlotLayer
+ Friend Sub New(curve As CurveGeometry, derivative As CurveGeometry, integral As CurveGeometry)
+ Me.Curve = curve
+ Me.Derivative = derivative
+ Me.Integral = integral
+ End Sub
+
+ Public ReadOnly Property Curve As CurveGeometry
+ Public ReadOnly Property Derivative As CurveGeometry
+ Public ReadOnly Property Integral As CurveGeometry
+End Class
+
+' A complete, immutable frame. All overlays share one coordinate system.
+Public NotInheritable Class PlotSnapshot
+ Private Sub New(definitions As PlotDefinition(), viewport As PlotViewport, layers As PlotLayer())
+ Me.Definitions = Array.AsReadOnly(definitions)
+ Me.Viewport = viewport
+ Me.Layers = Array.AsReadOnly(layers)
+ End Sub
+
+ Public ReadOnly Property Definitions As ReadOnlyCollection(Of PlotDefinition)
+ Public ReadOnly Property Viewport As PlotViewport
+ Public ReadOnly Property Layers As ReadOnlyCollection(Of PlotLayer)
+
+ Public Shared Function Build(definitions As IEnumerable(Of PlotDefinition), xRange As NumericRange,
+ yRange As NumericRange, canvas As CanvasSize) As PlotSnapshot
+#If NET6_0_OR_GREATER Then
+ ArgumentNullException.ThrowIfNull(definitions)
+#Else
+ If definitions Is Nothing Then Throw New ArgumentNullException(NameOf(definitions))
+#End If
+ If Double.IsInfinity(xRange.Length) OrElse Double.IsInfinity(yRange.Length) Then
+ Throw New ArgumentException("The difference between each range's endpoints must be finite.")
+ End If
+ Dim viewport = New PlotViewport(xRange, yRange, canvas)
+ Dim plots = definitions.ToArray()
+ Dim layers(plots.Length - 1) As PlotLayer
+ Dim sampleCount = CInt(Math.Min(32769, Math.Max(3, Math.Ceiling(canvas.Width) * 2 + 1)))
+ For index = 0 To plots.Length - 1
+ Dim plot = plots(index)
+ If plot Is Nothing Then Throw New ArgumentException("Plot definitions cannot contain Nothing.", NameOf(definitions))
+ Dim samples = FunctionSampler.Sample(AddressOf plot.Formula.Evaluate, xRange, sampleCount)
+ layers(index) = New PlotLayer(
+ CurveGeometryBuilder.Build(samples, viewport),
+ CurveGeometryBuilder.Build(SampleCalculus.Differentiate(samples), viewport),
+ CurveGeometryBuilder.Build(SampleCalculus.IntegrateFiniteRuns(samples), viewport))
+ Next
+ Return New PlotSnapshot(plots, viewport, layers)
+ End Function
+End Class
diff --git a/WindowsTools.slnx b/WindowsTools.slnx
index 1e103ff..07385c1 100644
--- a/WindowsTools.slnx
+++ b/WindowsTools.slnx
@@ -5,6 +5,7 @@
-
+
+
diff --git a/docs/graphviewer-migration.md b/docs/graphviewer-migration.md
new file mode 100644
index 0000000..b64a813
--- /dev/null
+++ b/docs/graphviewer-migration.md
@@ -0,0 +1,79 @@
+# GraphViewer modern expression and plotting API review
+
+GraphViewer now consumes `LTRData.MathExpression` 1.1.0 and
+`LTRData.FunctionPlotting` 1.2.0 through NuGet. The existing net35/net40
+and net8.0-windows/net9.0-windows/net10.0-windows targets remain.
+
+The old `ScriptControl`/`Surface` implementation and the `LTRLib.Windows` package
+reference are replaced by bound unary functions and portable plot geometry. A
+small application-owned System.Drawing renderer serves painting, printing and
+BMP/GIF/JPEG/PNG/TIFF export. No SkiaSharp/native deployment package is needed.
+
+## Behavior to review
+
+- With replacement enabled, redraw captures one formula. With it disabled, redraw
+ appends an overlay. Repaint, resize and derivative/integral toggles preserve the
+ formula collection without adding duplicates. Changing ranges reprojects every
+ retained formula into the new common viewport.
+- Invalid formula/range input preserves the last successful graph. Range controls
+ accept local decimals and invariant scientific notation, with finite increasing
+ limits. Formula syntax itself remains invariant and uses the modern language.
+- Numerical derivatives use three-point differences with one-sided endpoints.
+ Integrals use signed trapezoidal areas, starting at zero at the leftmost sample
+ of each finite run. Neither calculation crosses a non-finite sample. Integrals
+ do not wrap at viewport edges. Tooltips describe these rules.
+- Printing and export use the committed plots, including overlays. Editing the
+ formula box without redrawing does not change the printed graph. Print geometry
+ is rebuilt for the page and offset into the margins.
+- Previous-`y` recurrence and shift/bitwise syntax are rejected. Conventional
+ power precedence applies, including `-2^2` = -4 and `2^3^2` = 512.
+
+Sampling is twice the canvas width plus one, bounded to 3–32769 points. These are
+numerical approximations, so changing the sample density can change derivative
+and integral values. A discontinuity between finite samples can still be missed;
+there is no improper integration or complete asymptote detection.
+
+## Build through the local feed
+
+Set `LocalNuGetPath` to the shared package output directory and include it in this
+repository's local NuGet.Config. Build these Library projects in Release first:
+
+```powershell
+dotnet build LTRData.Extensions/LTRData.Extensions.csproj -c Release
+dotnet build LTRData.MathExpression/LTRData.MathExpression.csproj -c Release
+dotnet build LTRData.FunctionPlotting/LTRData.FunctionPlotting.csproj -c Release
+```
+
+Then, from WindowsTools:
+
+```powershell
+dotnet restore GraphViewer/GraphViewer.vbproj --force-evaluate
+dotnet build GraphViewer/GraphViewer.vbproj -c Release -f net10.0-windows --no-restore
+dotnet run --project GraphViewer.Review/GraphViewer.Review.csproj -c Release
+```
+
+The .NET Framework targets contain bitmap/icon resources that need the full
+Windows MSBuild resource toolchain. From a Visual Studio developer shell, after
+the restore above:
+
+```powershell
+msbuild GraphViewer/GraphViewer.vbproj /p:Configuration=Release /p:TargetFramework=net35
+msbuild GraphViewer/GraphViewer.vbproj /p:Configuration=Release /p:TargetFramework=net40
+```
+
+The focused `GraphViewer package review` workflow builds the producer packages,
+compiles all application targets and runs the review executable on Windows. The
+review checks actual GDI+ rendering, all five encoders, margin placement and
+graphics-state restoration, plus form overlays, toggles and resize. Its optional
+`GRAPHVIEWER_REVIEW_OUTPUT` directory receives a print-style PNG, also uploaded as
+a CI artifact. Library's
+[local package workflow](https://github.com/LTRData/Library/blob/experimental/math-expression-redesign/docs/local-package-workflow.md)
+explains source mapping and fresh caches to ensure the locally built packages are
+used. There are no project references across repositories.
+
+The application owner has built and tested this migration successfully. Actual
+printers, DPI scaling, saved preferences and curve appearance at usual ranges
+remain useful manual review cases. Compilation of net35/net40 does not establish
+execution on an old Windows installation. FreeBSD SkiaSharp work remains deferred;
+XML serialization generation is addressed in ltrwebdb. Neither is a dependency
+of this application.