diff --git a/Base/Abstract/IConvertColumnTable.cs b/Base/Abstract/IConvertColumnTable.cs
new file mode 100644
index 0000000..4b87aaf
--- /dev/null
+++ b/Base/Abstract/IConvertColumnTable.cs
@@ -0,0 +1,10 @@
+using Graphing.Graphables;
+
+namespace Graphing.Abstract;
+
+public interface IConvertColumnTable
+{
+ public bool UngraphWhenConvertedToColumnTable { get; }
+
+ public ColumnTable ToColumnTable(double start, double end, int detail);
+}
diff --git a/Base/Abstract/IConvertEquation.cs b/Base/Abstract/IConvertEquation.cs
new file mode 100644
index 0000000..da4f11a
--- /dev/null
+++ b/Base/Abstract/IConvertEquation.cs
@@ -0,0 +1,10 @@
+using Graphing.Graphables;
+
+namespace Graphing.Abstract;
+
+public interface IConvertEquation
+{
+ public bool UngraphWhenConvertedToEquation { get; }
+
+ public Equation ToEquation();
+}
diff --git a/Base/Abstract/IConvertSlopeField.cs b/Base/Abstract/IConvertSlopeField.cs
new file mode 100644
index 0000000..2c3982a
--- /dev/null
+++ b/Base/Abstract/IConvertSlopeField.cs
@@ -0,0 +1,10 @@
+using Graphing.Graphables;
+
+namespace Graphing.Abstract;
+
+public interface IConvertSlopeField
+{
+ public bool UngraphWhenConvertedToSlopeField { get; }
+
+ public SlopeField ToSlopeField(int detail);
+}
diff --git a/Base/Abstract/ITranslatable.cs b/Base/Abstract/ITranslatable.cs
new file mode 100644
index 0000000..0f9e8a5
--- /dev/null
+++ b/Base/Abstract/ITranslatable.cs
@@ -0,0 +1,3 @@
+namespace Graphing.Abstract;
+
+public interface ITranslatable { }
diff --git a/Base/Abstract/ITranslatableX.cs b/Base/Abstract/ITranslatableX.cs
new file mode 100644
index 0000000..ed13456
--- /dev/null
+++ b/Base/Abstract/ITranslatableX.cs
@@ -0,0 +1,6 @@
+namespace Graphing.Abstract;
+
+public interface ITranslatableX : ITranslatable
+{
+ public double OffsetX { get; set; }
+}
diff --git a/Base/Abstract/ITranslatableXY.cs b/Base/Abstract/ITranslatableXY.cs
new file mode 100644
index 0000000..6414127
--- /dev/null
+++ b/Base/Abstract/ITranslatableXY.cs
@@ -0,0 +1,3 @@
+namespace Graphing.Abstract;
+
+public interface ITranslatableXY : ITranslatableX, ITranslatableY { }
diff --git a/Base/Abstract/ITranslatableY.cs b/Base/Abstract/ITranslatableY.cs
new file mode 100644
index 0000000..f7ad103
--- /dev/null
+++ b/Base/Abstract/ITranslatableY.cs
@@ -0,0 +1,6 @@
+namespace Graphing.Abstract;
+
+public interface ITranslatableY : ITranslatable
+{
+ public double OffsetY { get; set; }
+}
diff --git a/Base/Base.csproj b/Base/Base.csproj
index 2301302..54d38db 100644
--- a/Base/Base.csproj
+++ b/Base/Base.csproj
@@ -12,18 +12,18 @@
True
ThatOneNerd.Graphing
ThatOneNerd.Graphing
- 1.2.0
+ 1.3.0
That_One_Nerd
A fairly adept graphing calculator made in Windows Forms.
MIT
https://github.com/That-One-Nerd/Graphing
README.md
- graphing;graph;plot;math;calculus;visual;desmos;slope field;slopefield;equation;visualizer
+ graphing;graph;plot;math;calculus;visual;desmos;slope field;slopefield;equation;visualizer;parametric equation;parametric;difference;tangent
MIT
True
snupkg
View the GitHub release for the changelog:
-https://github.com/That-One-Nerd/Graphing/releases/tag/1.2.0
+https://github.com/That-One-Nerd/Graphing/releases/tag/1.3.0
diff --git a/Base/Base.csproj.user b/Base/Base.csproj.user
index ef577eb..8d9b1c8 100644
--- a/Base/Base.csproj.user
+++ b/Base/Base.csproj.user
@@ -16,6 +16,12 @@
Form
+
+ Form
+
+
+ Form
+
Form
diff --git a/Base/Forms/GraphForm.Designer.cs b/Base/Forms/GraphForm.Designer.cs
index f30584b..ec64887 100644
--- a/Base/Forms/GraphForm.Designer.cs
+++ b/Base/Forms/GraphForm.Designer.cs
@@ -38,33 +38,45 @@ namespace Graphing.Forms
ButtonViewportSetCenter = new ToolStripMenuItem();
ButtonViewportReset = new ToolStripMenuItem();
ButtonViewportResetWindow = new ToolStripMenuItem();
- MenuColors = new ToolStripMenuItem();
- MenuEquations = new ToolStripMenuItem();
- MenuEquationsDerivative = new ToolStripMenuItem();
- MenuEquationsIntegral = new ToolStripMenuItem();
+ MenuElements = new ToolStripMenuItem();
+ MenuElementsColors = new ToolStripMenuItem();
+ MenuElementsRemove = new ToolStripMenuItem();
+ MenuOperations = new ToolStripMenuItem();
+ MenuOperationsDerivative = new ToolStripMenuItem();
+ MenuOperationsIntegral = new ToolStripMenuItem();
+ MenuOperationsTranslate = new ToolStripMenuItem();
+ MenuConvert = new ToolStripMenuItem();
+ MenuConvertEquation = new ToolStripMenuItem();
+ MenuConvertSlopeField = new ToolStripMenuItem();
MenuMisc = new ToolStripMenuItem();
MenuMiscCaches = new ToolStripMenuItem();
MiscMenuPreload = new ToolStripMenuItem();
+ UpdaterPopup = new Panel();
+ UpdaterPopupDownloadButton = new Button();
+ UpdaterPopupCloseButton = new Button();
+ UpdaterPopupMessage = new Label();
+ MenuElementsDetail = new ToolStripMenuItem();
GraphMenu.SuspendLayout();
+ UpdaterPopup.SuspendLayout();
SuspendLayout();
//
// ResetViewportButton
//
ResetViewportButton.Anchor = AnchorStyles.Top | AnchorStyles.Right;
- ResetViewportButton.Font = new Font("Segoe UI Emoji", 13.875F, FontStyle.Regular, GraphicsUnit.Point, 0);
- ResetViewportButton.Location = new Point(1373, 43);
+ ResetViewportButton.Font = new Font("Segoe UI Emoji", 12F, FontStyle.Regular, GraphicsUnit.Point, 0);
+ ResetViewportButton.Location = new Point(1372, 43);
+ ResetViewportButton.Margin = new Padding(4, 2, 4, 2);
ResetViewportButton.Name = "ResetViewportButton";
- ResetViewportButton.Size = new Size(64, 64);
+ ResetViewportButton.Size = new Size(63, 64);
ResetViewportButton.TabIndex = 0;
- ResetViewportButton.Text = "⌂";
- ResetViewportButton.TextAlign = ContentAlignment.TopRight;
+ ResetViewportButton.Text = "🏠";
ResetViewportButton.UseVisualStyleBackColor = true;
ResetViewportButton.Click += ResetViewportButton_Click;
//
// GraphMenu
//
GraphMenu.ImageScalingSize = new Size(32, 32);
- GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuColors, MenuEquations, MenuMisc });
+ GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuElements, MenuOperations, MenuConvert, MenuMisc });
GraphMenu.Location = new Point(0, 0);
GraphMenu.Name = "GraphMenu";
GraphMenu.Size = new Size(1449, 42);
@@ -81,55 +93,93 @@ namespace Graphing.Forms
// ButtonViewportSetZoom
//
ButtonViewportSetZoom.Name = "ButtonViewportSetZoom";
- ButtonViewportSetZoom.Size = new Size(350, 44);
+ ButtonViewportSetZoom.Size = new Size(359, 44);
ButtonViewportSetZoom.Text = "Set Zoom";
ButtonViewportSetZoom.Click += ButtonViewportSetZoom_Click;
//
// ButtonViewportSetCenter
//
ButtonViewportSetCenter.Name = "ButtonViewportSetCenter";
- ButtonViewportSetCenter.Size = new Size(350, 44);
+ ButtonViewportSetCenter.Size = new Size(359, 44);
ButtonViewportSetCenter.Text = "Set Center Position";
ButtonViewportSetCenter.Click += ButtonViewportSetCenter_Click;
//
// ButtonViewportReset
//
ButtonViewportReset.Name = "ButtonViewportReset";
- ButtonViewportReset.Size = new Size(350, 44);
+ ButtonViewportReset.Size = new Size(359, 44);
ButtonViewportReset.Text = "Reset Viewport";
ButtonViewportReset.Click += ButtonViewportReset_Click;
//
// ButtonViewportResetWindow
//
ButtonViewportResetWindow.Name = "ButtonViewportResetWindow";
- ButtonViewportResetWindow.Size = new Size(350, 44);
+ ButtonViewportResetWindow.Size = new Size(359, 44);
ButtonViewportResetWindow.Text = "Reset Window Size";
ButtonViewportResetWindow.Click += ButtonViewportResetWindow_Click;
//
- // MenuColors
+ // MenuElements
//
- MenuColors.Name = "MenuColors";
- MenuColors.Size = new Size(101, 38);
- MenuColors.Text = "Colors";
+ MenuElements.DropDownItems.AddRange(new ToolStripItem[] { MenuElementsColors, MenuElementsDetail, MenuElementsRemove });
+ MenuElements.Name = "MenuElements";
+ MenuElements.Size = new Size(131, 38);
+ MenuElements.Text = "Elements";
//
- // MenuEquations
+ // MenuElementsColors
//
- MenuEquations.DropDownItems.AddRange(new ToolStripItem[] { MenuEquationsDerivative, MenuEquationsIntegral });
- MenuEquations.Name = "MenuEquations";
- MenuEquations.Size = new Size(138, 38);
- MenuEquations.Text = "Equations";
+ MenuElementsColors.Name = "MenuElementsColors";
+ MenuElementsColors.Size = new Size(359, 44);
+ MenuElementsColors.Text = "Colors";
//
- // MenuEquationsDerivative
+ // MenuElementsRemove
//
- MenuEquationsDerivative.Name = "MenuEquationsDerivative";
- MenuEquationsDerivative.Size = new Size(360, 44);
- MenuEquationsDerivative.Text = "Compute Derivative";
+ MenuElementsRemove.Name = "MenuElementsRemove";
+ MenuElementsRemove.Size = new Size(359, 44);
+ MenuElementsRemove.Text = "Remove";
//
- // MenuEquationsIntegral
+ // MenuOperations
//
- MenuEquationsIntegral.Name = "MenuEquationsIntegral";
- MenuEquationsIntegral.Size = new Size(360, 44);
- MenuEquationsIntegral.Text = "Compute Integral";
+ MenuOperations.DropDownItems.AddRange(new ToolStripItem[] { MenuOperationsDerivative, MenuOperationsIntegral, MenuOperationsTranslate });
+ MenuOperations.Name = "MenuOperations";
+ MenuOperations.Size = new Size(151, 38);
+ MenuOperations.Text = "Operations";
+ //
+ // MenuOperationsDerivative
+ //
+ MenuOperationsDerivative.Name = "MenuOperationsDerivative";
+ MenuOperationsDerivative.Size = new Size(360, 44);
+ MenuOperationsDerivative.Text = "Compute Derivative";
+ //
+ // MenuOperationsIntegral
+ //
+ MenuOperationsIntegral.Name = "MenuOperationsIntegral";
+ MenuOperationsIntegral.Size = new Size(360, 44);
+ MenuOperationsIntegral.Text = "Compute Integral";
+ //
+ // MenuOperationsTranslate
+ //
+ MenuOperationsTranslate.Name = "MenuOperationsTranslate";
+ MenuOperationsTranslate.Size = new Size(360, 44);
+ MenuOperationsTranslate.Text = "Translate";
+ //
+ // MenuConvert
+ //
+ MenuConvert.DropDownItems.AddRange(new ToolStripItem[] { MenuConvertEquation, MenuConvertSlopeField });
+ MenuConvert.Name = "MenuConvert";
+ MenuConvert.Size = new Size(118, 38);
+ MenuConvert.Text = "Convert";
+ //
+ // MenuConvertEquation
+ //
+ MenuConvertEquation.Name = "MenuConvertEquation";
+ MenuConvertEquation.Size = new Size(297, 44);
+ MenuConvertEquation.Text = "To Equation";
+ //
+ // MenuConvertSlopeField
+ //
+ MenuConvertSlopeField.Name = "MenuConvertSlopeField";
+ MenuConvertSlopeField.Size = new Size(297, 44);
+ MenuConvertSlopeField.Text = "To Slope Field";
//
// MenuMisc
//
@@ -141,29 +191,87 @@ namespace Graphing.Forms
// MenuMiscCaches
//
MenuMiscCaches.Name = "MenuMiscCaches";
- MenuMiscCaches.Size = new Size(359, 44);
+ MenuMiscCaches.Size = new Size(299, 44);
MenuMiscCaches.Text = "View Caches";
MenuMiscCaches.Click += MenuMiscCaches_Click;
//
// MiscMenuPreload
//
MiscMenuPreload.Name = "MiscMenuPreload";
- MiscMenuPreload.Size = new Size(359, 44);
+ MiscMenuPreload.Size = new Size(299, 44);
MiscMenuPreload.Text = "Preload Cache";
MiscMenuPreload.Click += MiscMenuPreload_Click;
//
+ // UpdaterPopup
+ //
+ UpdaterPopup.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+ UpdaterPopup.BackColor = SystemColors.HighlightText;
+ UpdaterPopup.BorderStyle = BorderStyle.FixedSingle;
+ UpdaterPopup.Controls.Add(UpdaterPopupDownloadButton);
+ UpdaterPopup.Controls.Add(UpdaterPopupCloseButton);
+ UpdaterPopup.Controls.Add(UpdaterPopupMessage);
+ UpdaterPopup.Location = new Point(966, 791);
+ UpdaterPopup.Margin = new Padding(6, 6, 6, 6);
+ UpdaterPopup.Name = "UpdaterPopup";
+ UpdaterPopup.Size = new Size(483, 115);
+ UpdaterPopup.TabIndex = 2;
+ UpdaterPopup.Visible = false;
+ //
+ // UpdaterPopupDownloadButton
+ //
+ UpdaterPopupDownloadButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+ UpdaterPopupDownloadButton.Location = new Point(336, 58);
+ UpdaterPopupDownloadButton.Margin = new Padding(6, 6, 6, 6);
+ UpdaterPopupDownloadButton.Name = "UpdaterPopupDownloadButton";
+ UpdaterPopupDownloadButton.Size = new Size(139, 49);
+ UpdaterPopupDownloadButton.TabIndex = 2;
+ UpdaterPopupDownloadButton.Text = "Visit";
+ UpdaterPopupDownloadButton.UseVisualStyleBackColor = true;
+ //
+ // UpdaterPopupCloseButton
+ //
+ UpdaterPopupCloseButton.Anchor = AnchorStyles.Top | AnchorStyles.Right;
+ UpdaterPopupCloseButton.Location = new Point(435, 2);
+ UpdaterPopupCloseButton.Margin = new Padding(2, 2, 2, 2);
+ UpdaterPopupCloseButton.Name = "UpdaterPopupCloseButton";
+ UpdaterPopupCloseButton.Size = new Size(45, 51);
+ UpdaterPopupCloseButton.TabIndex = 1;
+ UpdaterPopupCloseButton.Text = "X";
+ UpdaterPopupCloseButton.UseVisualStyleBackColor = true;
+ UpdaterPopupCloseButton.Click += UpdaterPopupCloseButton_Click;
+ //
+ // UpdaterPopupMessage
+ //
+ UpdaterPopupMessage.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left;
+ UpdaterPopupMessage.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0);
+ UpdaterPopupMessage.Location = new Point(6, 6);
+ UpdaterPopupMessage.Margin = new Padding(6, 6, 6, 6);
+ UpdaterPopupMessage.Name = "UpdaterPopupMessage";
+ UpdaterPopupMessage.Size = new Size(423, 100);
+ UpdaterPopupMessage.TabIndex = 0;
+ UpdaterPopupMessage.Text = "A update is available!\r\nA.B.C → E.F.G";
+ //
+ // MenuElementsDetail
+ //
+ MenuElementsDetail.Name = "MenuElementsDetail";
+ MenuElementsDetail.Size = new Size(359, 44);
+ MenuElementsDetail.Text = "Detail";
+ //
// GraphForm
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1449, 907);
+ Controls.Add(UpdaterPopup);
Controls.Add(ResetViewportButton);
Controls.Add(GraphMenu);
MainMenuStrip = GraphMenu;
+ Margin = new Padding(4, 2, 4, 2);
Name = "GraphForm";
Text = "GraphFormBase";
GraphMenu.ResumeLayout(false);
GraphMenu.PerformLayout();
+ UpdaterPopup.ResumeLayout(false);
ResumeLayout(false);
PerformLayout();
}
@@ -172,17 +280,28 @@ namespace Graphing.Forms
private Button ResetViewportButton;
private MenuStrip GraphMenu;
- private ToolStripMenuItem MenuColors;
private ToolStripMenuItem MenuViewport;
private ToolStripMenuItem ButtonViewportSetZoom;
private ToolStripMenuItem ButtonViewportSetCenter;
private ToolStripMenuItem ButtonViewportReset;
private ToolStripMenuItem ButtonViewportResetWindow;
- private ToolStripMenuItem MenuEquations;
- private ToolStripMenuItem MenuEquationsDerivative;
- private ToolStripMenuItem MenuEquationsIntegral;
+ private ToolStripMenuItem MenuOperations;
+ private ToolStripMenuItem MenuOperationsDerivative;
+ private ToolStripMenuItem MenuOperationsIntegral;
private ToolStripMenuItem MenuMisc;
private ToolStripMenuItem MenuMiscCaches;
private ToolStripMenuItem MiscMenuPreload;
+ private ToolStripMenuItem MenuConvert;
+ private ToolStripMenuItem MenuConvertEquation;
+ private ToolStripMenuItem MenuElements;
+ private ToolStripMenuItem MenuElementsColors;
+ private ToolStripMenuItem MenuElementsRemove;
+ private ToolStripMenuItem MenuOperationsTranslate;
+ private ToolStripMenuItem MenuConvertSlopeField;
+ private Panel UpdaterPopup;
+ private Label UpdaterPopupMessage;
+ private Button UpdaterPopupCloseButton;
+ private Button UpdaterPopupDownloadButton;
+ private ToolStripMenuItem MenuElementsDetail;
}
}
\ No newline at end of file
diff --git a/Base/Forms/GraphForm.cs b/Base/Forms/GraphForm.cs
index 152d852..b4630ab 100644
--- a/Base/Forms/GraphForm.cs
+++ b/Base/Forms/GraphForm.cs
@@ -1,46 +1,71 @@
using Graphing.Abstract;
-using Graphing.Parts;
+using Graphing.Graphables;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Windows.Forms;
namespace Graphing.Forms;
public partial class GraphForm : Form
{
+ public static readonly Color BackgroundColor = Color.White;
public static readonly Color MainAxisColor = Color.Black;
- public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999));
- public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0));
+ public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999)); // Grayish
+ public static readonly Color QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0)); // Lighter grayish
public static readonly Color UnitsTextColor = Color.Black;
+ public static readonly Color ZoomBoxColor = Color.Black;
- public Float2 ScreenCenter { get; private set; }
+ public static readonly Color MajorUpdateColor = Color.FromArgb(unchecked((int)0xFF_F74434)); // Red
+ public static readonly Color MinorUpdateColor = Color.FromArgb(unchecked((int)0xFF_FCA103)); // Orange
+
+ public Float2 ScreenCenter { get; set; }
public Float2 Dpi { get; private set; }
public float DpiFloat { get; private set; }
- public double ZoomLevel
+ public Float2 ZoomLevel
{
get => _zoomLevel;
set
{
- double oldZoom = ZoomLevel;
-
- _zoomLevel = Math.Clamp(value, 1e-5, 1e3);
-
- int totalSegments = 0;
- foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count();
-
- if (totalSegments > 10_000)
- {
- _zoomLevel = oldZoom;
- return; // Too many segments, stop.
- }
+ _zoomLevel = new(Math.Clamp(value.x, 1e-5, 1e3),
+ Math.Clamp(value.y, 1e-5, 1e3));
+ OnZoomLevelChanged(this, new());
+ Invalidate(false);
}
}
- private double _zoomLevel;
+ private Float2 _zoomLevel;
+
+ public bool ViewportLocked
+ {
+ get => _viewportLocked;
+ set
+ {
+ if (value)
+ {
+ FormBorderStyle = FormBorderStyle.FixedSingle;
+ ResetViewportButton.Text = "🔒";
+ }
+ else
+ {
+ FormBorderStyle = FormBorderStyle.Sizable;
+ ResetViewportButton.Text = "🏠";
+ }
+ MaximizeBox = !value;
+ ResetViewportButton.Enabled = !value;
+
+ _viewportLocked = value;
+ }
+ }
+ private bool _viewportLocked;
private readonly Point initialWindowPos;
private readonly Size initialWindowSize;
@@ -52,6 +77,8 @@ public partial class GraphForm : Form
private readonly List ables;
+ public event EventHandler OnZoomLevelChanged = delegate { };
+
public GraphForm(string title)
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
@@ -68,9 +95,11 @@ public partial class GraphForm : Form
DpiFloat = (float)((Dpi.x + Dpi.y) / 2);
ables = [];
- ZoomLevel = 1;
+ ZoomLevel = new(1, 1);
initialWindowPos = Location;
initialWindowSize = Size;
+
+ RunUpdateChecker();
}
public Int2 GraphSpaceToScreenSpace(Float2 graphPoint)
@@ -80,8 +109,8 @@ public partial class GraphForm : Form
graphPoint.x -= ScreenCenter.x;
graphPoint.y -= ScreenCenter.y;
- graphPoint.x *= Dpi.x / ZoomLevel;
- graphPoint.y *= Dpi.y / ZoomLevel;
+ graphPoint.x *= Dpi.x / ZoomLevel.x;
+ graphPoint.y *= Dpi.y / ZoomLevel.y;
graphPoint.x += ClientRectangle.Width / 2.0;
graphPoint.y += ClientRectangle.Height / 2.0;
@@ -95,8 +124,8 @@ public partial class GraphForm : Form
result.x -= ClientRectangle.Width / 2.0;
result.y -= ClientRectangle.Height / 2.0;
- result.x /= Dpi.x / ZoomLevel;
- result.y /= Dpi.y / ZoomLevel;
+ result.x /= Dpi.x / ZoomLevel.x;
+ result.y /= Dpi.y / ZoomLevel.y;
result.x += ScreenCenter.x;
result.y += ScreenCenter.y;
@@ -108,19 +137,20 @@ public partial class GraphForm : Form
protected virtual void PaintGrid(Graphics g)
{
- double axisScale = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel)));
+ double axisScaleX = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.x))),
+ axisScaleY = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.y)));
// Draw horizontal/vertical quarter-axis.
Brush quarterBrush = new SolidBrush(QuarterAxisColor);
Pen quarterPen = new(quarterBrush, DpiFloat * 2 / 192);
- for (double x = Math.Ceiling(MinVisibleGraph.x * 4 / axisScale) * axisScale / 4; x <= Math.Floor(MaxVisibleGraph.x * 4 / axisScale) * axisScale / 4; x += axisScale / 4)
+ for (double x = Math.Ceiling(MinVisibleGraph.x * 4 / axisScaleX) * axisScaleX / 4; x <= Math.Floor(MaxVisibleGraph.x * 4 / axisScaleX) * axisScaleX / 4; x += axisScaleX / 4)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)),
endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y));
g.DrawLine(quarterPen, startPos, endPos);
}
- for (double y = Math.Ceiling(MinVisibleGraph.y * 4 / axisScale) * axisScale / 4; y <= Math.Floor(MaxVisibleGraph.y * 4 / axisScale) * axisScale / 4; y += axisScale / 4)
+ for (double y = Math.Ceiling(MinVisibleGraph.y * 4 / axisScaleY) * axisScaleY / 4; y <= Math.Floor(MaxVisibleGraph.y * 4 / axisScaleY) * axisScaleY / 4; y += axisScaleY / 4)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)),
endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y));
@@ -131,13 +161,13 @@ public partial class GraphForm : Form
Brush semiBrush = new SolidBrush(SemiAxisColor);
Pen semiPen = new(semiBrush, DpiFloat * 2 / 192);
- for (double x = Math.Ceiling(MinVisibleGraph.x / axisScale) * axisScale; x <= Math.Floor(MaxVisibleGraph.x / axisScale) * axisScale; x += axisScale)
+ for (double x = Math.Ceiling(MinVisibleGraph.x / axisScaleX) * axisScaleX; x <= Math.Floor(MaxVisibleGraph.x / axisScaleX) * axisScaleX; x += axisScaleX)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(x, MinVisibleGraph.y)),
endPos = GraphSpaceToScreenSpace(new Float2(x, MaxVisibleGraph.y));
g.DrawLine(semiPen, startPos, endPos);
}
- for (double y = Math.Ceiling(MinVisibleGraph.y / axisScale) * axisScale; y <= Math.Floor(MaxVisibleGraph.y / axisScale) * axisScale; y += axisScale)
+ for (double y = Math.Ceiling(MinVisibleGraph.y / axisScaleY) * axisScaleY; y <= Math.Floor(MaxVisibleGraph.y / axisScaleY) * axisScaleY; y += axisScaleY)
{
Int2 startPos = GraphSpaceToScreenSpace(new Float2(MinVisibleGraph.x, y)),
endPos = GraphSpaceToScreenSpace(new Float2(MaxVisibleGraph.x, y));
@@ -158,14 +188,15 @@ public partial class GraphForm : Form
}
protected virtual void PaintUnits(Graphics g)
{
- double axisScale = Math.Pow(2, Math.Round(Math.Log(ZoomLevel, 2)));
+ double axisScaleX = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.x))),
+ axisScaleY = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel.y)));
Brush textBrush = new SolidBrush(UnitsTextColor);
Font textFont = new(Font.Name, 9, FontStyle.Regular);
// X-axis
int minX = (int)(DpiFloat * 50 / 192),
maxX = ClientRectangle.Height - (int)(DpiFloat * 40 / 192);
- for (double x = Math.Ceiling(MinVisibleGraph.x / axisScale) * axisScale; x <= MaxVisibleGraph.x; x += axisScale)
+ for (double x = Math.Ceiling(MinVisibleGraph.x / axisScaleX) * axisScaleX; x <= MaxVisibleGraph.x; x += axisScaleX)
{
if (x == 0) x = 0; // Fixes -0
@@ -179,7 +210,7 @@ public partial class GraphForm : Form
// Y-axis
int minY = (int)(DpiFloat * 10 / 192);
- for (double y = Math.Ceiling(MinVisibleGraph.y / axisScale) * axisScale; y <= MaxVisibleGraph.y; y += axisScale)
+ for (double y = Math.Ceiling(MinVisibleGraph.y / axisScaleY) * axisScaleY; y <= MaxVisibleGraph.y; y += axisScaleY)
{
if (y == 0) continue;
@@ -200,7 +231,7 @@ public partial class GraphForm : Form
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
- Brush background = new SolidBrush(Color.White);
+ Brush background = new SolidBrush(BackgroundColor);
g.FillRectangle(background, e.ClipRectangle);
PaintGrid(g);
@@ -223,29 +254,31 @@ public partial class GraphForm : Form
// Equation selection detection.
// This system lets you select multiple graphs, and that's cool by me.
- if (ableDrag)
+ if (selectState == SelectionState.GraphSelect)
{
- Font textFont = new(Font.Name, 8, FontStyle.Bold);
for (int i = 0; i < ables.Count; i++)
{
if (ables[i].ShouldSelectGraphable(this, graphMousePos, 2.5))
{
- Float2 selectedPoint = ables[i].GetSelectedPoint(this, graphMousePos);
- GraphUiCircle select = new(selectedPoint, 8);
-
- Int2 textPos = GraphSpaceToScreenSpace(select.center);
- textPos.y -= (int)(DpiFloat * 32 / 192);
-
- string content = $"({selectedPoint.x:0.00}, {selectedPoint.y:0.00})";
-
- SizeF textSize = g.MeasureString(content, textFont);
- g.FillRectangle(background, new Rectangle(textPos.x, textPos.y,
- (int)textSize.Width, (int)textSize.Height));
- g.DrawString(content, textFont, graphPens[i].Brush, new Point(textPos.x, textPos.y));
- select.Render(this, g, graphPens[i]);
+ IEnumerable selectionParts = ables[i].GetSelectionItemsToRender(this, graphMousePos);
+ foreach (IGraphPart selPart in selectionParts) selPart.Render(this, g, graphPens[i]);
}
}
}
+ else if (selectState == SelectionState.ZoomBox)
+ {
+ // Draw the current box selection.
+ Int2 boxPosA = GraphSpaceToScreenSpace(boxSelectA),
+ boxPosB = GraphSpaceToScreenSpace(boxSelectB);
+
+ if (boxPosA.x > boxPosB.x) (boxPosA.x, boxPosB.x) = (boxPosB.x, boxPosA.x);
+ if (boxPosA.y > boxPosB.y) (boxPosA.y, boxPosB.y) = (boxPosB.y, boxPosA.y);
+
+ Pen boxPen = new(ZoomBoxColor, 2 * DpiFloat / 192);
+ g.DrawRectangle(boxPen, new(boxPosA.x, boxPosA.y,
+ boxPosB.x - boxPosA.x,
+ boxPosB.y - boxPosA.y));
+ }
base.OnPaint(e);
}
@@ -255,77 +288,152 @@ public partial class GraphForm : Form
Invalidate(false);
}
- public void Graph(params Graphable[] able)
+ public void Graph(params Graphable[] newAbles)
{
- ables.AddRange(able);
+ ables.AddRange(newAbles);
+ RegenerateMenuItems();
+ Invalidate(false);
+ }
+ public void Ungraph(params Graphable[] ables)
+ {
+ this.ables.RemoveAll(x => ables.Contains(x));
RegenerateMenuItems();
Invalidate(false);
}
- private bool mouseDrag = false;
+ public bool IsGraphPointVisible(Float2 point)
+ {
+ Int2 pixelPos = GraphSpaceToScreenSpace(point);
+ return pixelPos.x >= 0 && pixelPos.x < ClientRectangle.Width &&
+ pixelPos.y >= 0 && pixelPos.y < ClientRectangle.Height;
+ }
+
+ private SelectionState selectState = SelectionState.None;
+ internal bool canBoxSelect;
+ private SetZoomForm? setZoomForm;
+
private Int2 initialMouseLocation;
private Float2 initialScreenCenter;
- private bool ableDrag = false;
+ private Float2 boxSelectA, boxSelectB;
+
protected override void OnMouseDown(MouseEventArgs e)
{
- if (!mouseDrag)
+ if (selectState == SelectionState.None && canBoxSelect)
+ {
+ Point clientMousePos = PointToClient(Cursor.Position);
+ Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
+ clientMousePos.Y));
+
+ boxSelectA = graphMousePos;
+ boxSelectB = graphMousePos;
+
+ selectState = SelectionState.ZoomBox;
+ }
+
+ if (selectState == SelectionState.None)
{
Point clientMousePos = PointToClient(Cursor.Position);
Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
clientMousePos.Y));
foreach (Graphable able in Graphables)
{
- if (able.ShouldSelectGraphable(this, graphMousePos, 1)) ableDrag = true;
+ if (able.ShouldSelectGraphable(this, graphMousePos, 1))
+ selectState = SelectionState.GraphSelect;
}
- if (ableDrag) Invalidate(false);
+ if (selectState == SelectionState.GraphSelect) Invalidate(false);
}
- if (!ableDrag)
+ if (selectState == SelectionState.None && !ViewportLocked)
{
- mouseDrag = true;
+ selectState = SelectionState.ViewportDrag;
initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y);
initialScreenCenter = ScreenCenter;
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
- if (mouseDrag)
+ if (selectState == SelectionState.None) return;
+ else if (selectState == SelectionState.ViewportDrag)
{
Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X,
initialMouseLocation.y - Cursor.Position.Y);
- Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y);
+ Float2 graphDiff = new(pixelDiff.x * ZoomLevel.x / Dpi.x, pixelDiff.y * ZoomLevel.y / Dpi.y);
ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y);
}
- mouseDrag = false;
- ableDrag = false;
+ else if (selectState == SelectionState.ZoomBox)
+ {
+ Point clientMousePos = PointToClient(Cursor.Position);
+ Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
+ clientMousePos.Y));
+ boxSelectB = graphMousePos;
+
+ // Set center.
+ ScreenCenter = new((boxSelectA.x + boxSelectB.x) * 0.5,
+ -(boxSelectA.y + boxSelectB.y) * 0.5);
+
+ // Set zoom. Kind of weird but it works.
+ Float2 minGraph = MinVisibleGraph, maxGraph = MaxVisibleGraph;
+ Float2 oldDist = new(maxGraph.x - minGraph.x,
+ maxGraph.y - minGraph.y);
+ Float2 newDist = new(Math.Abs(boxSelectB.x - boxSelectA.x),
+ Math.Abs(boxSelectB.y - boxSelectA.y));
+ ZoomLevel = new(ZoomLevel.x * newDist.x / oldDist.x,
+ ZoomLevel.y * newDist.y / oldDist.y);
+
+ setZoomForm!.CompleteBoxSelection();
+
+ boxSelectA = new(0, 0);
+ boxSelectB = new(0, 0);
+ }
+ selectState = SelectionState.None;
Invalidate(false);
}
protected override void OnMouseMove(MouseEventArgs e)
{
- if (mouseDrag)
+ if (selectState == SelectionState.None) return;
+ else if (selectState == SelectionState.ViewportDrag)
{
Int2 pixelDiff = new(initialMouseLocation.x - Cursor.Position.X,
initialMouseLocation.y - Cursor.Position.Y);
- Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y);
+ Float2 graphDiff = new(pixelDiff.x * ZoomLevel.x / Dpi.x, pixelDiff.y * ZoomLevel.y / Dpi.y);
ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y);
- Invalidate(false);
}
- else if (ableDrag) Invalidate(false);
+ else if (selectState == SelectionState.ZoomBox)
+ {
+ Point clientMousePos = PointToClient(Cursor.Position);
+ Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
+ clientMousePos.Y));
+ boxSelectB = graphMousePos;
+ }
+ Invalidate(false);
}
protected override void OnMouseWheel(MouseEventArgs e)
{
- ZoomLevel *= 1 - e.Delta * 0.00075; // Zoom factor.
+ if (ViewportLocked) return;
+
+ Point clientMousePos = PointToClient(Cursor.Position);
+ Int2 mousePos = new(clientMousePos.X, clientMousePos.Y);
+ Float2 mouseOver = ScreenSpaceToGraphSpace(mousePos);
+
+ Float2 newZoom = ZoomLevel;
+ newZoom.x *= 1 - e.Delta * 0.00075; // Zoom factor.
+ newZoom.y *= 1 - e.Delta * 0.00075;
+ ZoomLevel = newZoom;
+
+ // Keep the mouse as the zoom hotspot.
+ Float2 newOver = ScreenSpaceToGraphSpace(mousePos);
+ Float2 delta = new(newOver.x - mouseOver.x, newOver.y - mouseOver.y);
+ ScreenCenter = new(ScreenCenter.x - delta.x, ScreenCenter.y + delta.y);
+
Invalidate(false);
}
private void ResetViewportButton_Click(object? sender, EventArgs e)
{
- ScreenCenter = new Float2(0, 0);
- ZoomLevel = 1;
- Invalidate(false);
+ ResetAllViewport();
}
private void GraphColorPickerButton_Click(Graphable able)
{
@@ -346,11 +454,46 @@ public partial class GraphForm : Form
RegenerateMenuItems();
}
+ private readonly Dictionary sfDetailForms = [];
+ private void ChangeSlopeFieldDetail(SlopeField sf)
+ {
+ if (sfDetailForms.TryGetValue(sf, out SlopeFieldDetailForm? preexistingForm))
+ {
+ preexistingForm.Focus();
+ return;
+ }
+
+ SlopeFieldDetailForm detailForm = new(this, sf)
+ {
+ StartPosition = FormStartPosition.Manual
+ };
+ sfDetailForms.Add(sf, detailForm);
+
+ detailForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
+ Location.Y + (ClientRectangle.Height - detailForm.ClientRectangle.Height) / 2);
+
+ if (detailForm.Location.X + detailForm.Width > Screen.FromControl(this).WorkingArea.Width)
+ {
+ detailForm.StartPosition = FormStartPosition.WindowsDefaultLocation;
+ }
+ detailForm.TopMost = true;
+ detailForm.Show();
+
+ detailForm.FormClosed += (o, e) => sfDetailForms.Remove(sf);
+ }
+
private void RegenerateMenuItems()
{
- MenuColors.DropDownItems.Clear();
- MenuEquationsDerivative.DropDownItems.Clear();
- MenuEquationsIntegral.DropDownItems.Clear();
+ MenuElementsColors.DropDownItems.Clear();
+ MenuElementsDetail.DropDownItems.Clear();
+ MenuElementsRemove.DropDownItems.Clear();
+ MenuOperationsDerivative.DropDownItems.Clear();
+ MenuOperationsIntegral.DropDownItems.Clear();
+ MenuConvertEquation.DropDownItems.Clear();
+ MenuConvertSlopeField.DropDownItems.Clear();
+ MenuOperationsTranslate.DropDownItems.Clear();
+ // At some point, we'll have a Convert To Column Table button,
+ // but I'll need to make a form for the ranges when I do that.
foreach (Graphable able in ables)
{
@@ -360,7 +503,26 @@ public partial class GraphForm : Form
Text = able.Name
};
colorItem.Click += (o, e) => GraphColorPickerButton_Click(able);
- MenuColors.DropDownItems.Add(colorItem);
+ MenuElementsColors.DropDownItems.Add(colorItem);
+
+ ToolStripMenuItem removeItem = new()
+ {
+ ForeColor = able.Color,
+ Text = able.Name
+ };
+ removeItem.Click += (o, e) => Ungraph(able);
+ MenuElementsRemove.DropDownItems.Add(removeItem);
+
+ if (able is SlopeField sf)
+ {
+ ToolStripMenuItem sfDetailItem = new()
+ {
+ ForeColor = able.Color,
+ Text = able.Name
+ };
+ sfDetailItem.Click += (o, e) => ChangeSlopeFieldDetail(sf);
+ MenuElementsDetail.DropDownItems.Add(sfDetailItem);
+ }
if (able is IDerivable derivable)
{
@@ -370,7 +532,7 @@ public partial class GraphForm : Form
Text = able.Name
};
derivativeItem.Click += (o, e) => Graph(derivable.Derive());
- MenuEquationsDerivative.DropDownItems.Add(derivativeItem);
+ MenuOperationsDerivative.DropDownItems.Add(derivativeItem);
}
if (able is IIntegrable integrable)
{
@@ -380,20 +542,76 @@ public partial class GraphForm : Form
Text = able.Name
};
integralItem.Click += (o, e) => Graph(integrable.Integrate());
- MenuEquationsIntegral.DropDownItems.Add(integralItem);
+ MenuOperationsIntegral.DropDownItems.Add(integralItem);
+ }
+ if (able is IConvertEquation equConvert)
+ {
+ ToolStripMenuItem equItem = new()
+ {
+ ForeColor = able.Color,
+ Text = able.Name
+ };
+ equItem.Click += (o, e) =>
+ {
+ if (equConvert.UngraphWhenConvertedToEquation) Ungraph(able);
+ Graph(equConvert.ToEquation());
+ };
+ MenuConvertEquation.DropDownItems.Add(equItem);
+ }
+ if (able is IConvertSlopeField sfConvert)
+ {
+ ToolStripMenuItem sfItem = new()
+ {
+ ForeColor = able.Color,
+ Text = able.Name
+ };
+ sfItem.Click += (o, e) =>
+ {
+ if (sfConvert.UngraphWhenConvertedToSlopeField) Ungraph(able);
+ Graph(sfConvert.ToSlopeField(2));
+ };
+ MenuConvertSlopeField.DropDownItems.Add(sfItem);
+ }
+ if (able is ITranslatable translatable)
+ {
+ ToolStripMenuItem transItem = new()
+ {
+ ForeColor = able.Color,
+ Text = able.Name
+ };
+ transItem.Click += (o, e) => ElementsOperationsTranslate_Click(able, translatable);
+ MenuOperationsTranslate.DropDownItems.Add(transItem);
}
}
}
private void ButtonViewportSetZoom_Click(object? sender, EventArgs e)
{
- SetZoomForm picker = new(this)
+ if (setZoomForm is not null)
+ {
+ setZoomForm.Focus();
+ return;
+ }
+
+ SetZoomForm zoomForm = new(this)
{
StartPosition = FormStartPosition.Manual,
};
- picker.Location = new Point(Location.X + ClientRectangle.Width + 10,
- Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2);
- picker.ShowDialog();
+ zoomForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
+ Location.Y + (ClientRectangle.Height - zoomForm.ClientRectangle.Height) / 2);
+
+ if (zoomForm.Location.X + zoomForm.Width > Screen.FromControl(this).WorkingArea.Width)
+ {
+ zoomForm.StartPosition = FormStartPosition.WindowsDefaultLocation;
+ }
+
+ setZoomForm = zoomForm;
+ zoomForm.Show();
+ zoomForm.FormClosing += (o, e) =>
+ {
+ zoomForm.CompleteBoxSelection();
+ setZoomForm = null;
+ };
}
private void ButtonViewportSetCenter_Click(object? sender, EventArgs e)
{
@@ -402,7 +620,7 @@ public partial class GraphForm : Form
private void ButtonViewportReset_Click(object? sender, EventArgs e)
{
ScreenCenter = new Float2(0, 0);
- ZoomLevel = 1;
+ ZoomLevel = new(1, 1);
Invalidate(false);
}
private void ButtonViewportResetWindow_Click(object? sender, EventArgs e)
@@ -412,12 +630,30 @@ public partial class GraphForm : Form
WindowState = FormWindowState.Normal;
}
+ public void ResetAllViewport()
+ {
+ ScreenCenter = new Float2(0, 0);
+ ZoomLevel = new(1, 1);
+ Location = initialWindowPos;
+ Size = initialWindowSize;
+ WindowState = FormWindowState.Normal;
+ Invalidate(false);
+ }
+
+ private ViewCacheForm? cacheForm;
private void MenuMiscCaches_Click(object? sender, EventArgs e)
{
+ if (this.cacheForm is not null)
+ {
+ this.cacheForm.Focus();
+ return;
+ }
+
ViewCacheForm cacheForm = new(this)
{
StartPosition = FormStartPosition.Manual
};
+ this.cacheForm = cacheForm;
cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2);
@@ -429,7 +665,7 @@ public partial class GraphForm : Form
cacheForm.TopMost = true;
cacheForm.Show();
}
- private void MiscMenuPreload_Click(object sender, EventArgs e)
+ private void MiscMenuPreload_Click(object? sender, EventArgs e)
{
Float2 min = MinVisibleGraph, max = MaxVisibleGraph;
Float2 add = new(max.x - min.x, max.y - min.y);
@@ -446,4 +682,95 @@ public partial class GraphForm : Form
foreach (Graphable able in Graphables) able.Preload(xRange, yRange, step);
Invalidate(false);
}
+ private void UpdaterPopupCloseButton_Click(object? sender, EventArgs e)
+ {
+ UpdaterPopup.Dispose();
+ }
+
+ private void ElementsOperationsTranslate_Click(Graphable ableRaw, ITranslatable ableTrans)
+ {
+ TranslateForm shifter = new(this, ableRaw, ableTrans)
+ {
+ StartPosition = FormStartPosition.Manual,
+ };
+ shifter.Location = new Point(Location.X + ClientRectangle.Width + 10,
+ Location.Y + (ClientRectangle.Height - shifter.ClientRectangle.Height) / 2);
+ if (shifter.Location.X + shifter.Width > Screen.FromControl(this).WorkingArea.Width)
+ {
+ shifter.StartPosition = FormStartPosition.WindowsDefaultLocation;
+ }
+ shifter.Show();
+ }
+
+ private async void RunUpdateChecker()
+ {
+ try
+ {
+ HttpClient http = new();
+ HttpRequestMessage request = new(HttpMethod.Get, "https://api.github.com/repos/That-One-Nerd/Graphing/releases");
+ request.Headers.Add("User-Agent", "ThatOneNerd.Graphing-Update-Checker");
+
+ HttpResponseMessage result = await http.SendAsync(request);
+ if (!result.IsSuccessStatusCode)
+ {
+ Console.WriteLine($"Failed to check for updates.");
+ return;
+ }
+
+ JsonArray arr = JsonSerializer.Deserialize(await result.Content.ReadAsStreamAsync())!;
+ JsonObject latest = arr[0]!.AsObject();
+
+ Version curVersion = Version.Parse(Assembly.GetAssembly(typeof(GraphForm))!.FullName!.Split(',')[1].Trim()[8..^2]);
+ Version newVersion = Version.Parse(latest["tag_name"]!.GetValue());
+
+ if (newVersion > curVersion)
+ {
+ string type;
+
+ if (newVersion.Major > curVersion.Major || // x.0.0
+ newVersion.Minor > curVersion.Minor) // 0.x.0
+ {
+ type = "major";
+ UpdaterPopupMessage.ForeColor = MajorUpdateColor;
+ }
+ else // 0.0.x
+ {
+ type = "minor";
+ UpdaterPopupMessage.ForeColor = MinorUpdateColor;
+ }
+
+ UpdaterPopupMessage.Text = $"A {type} update is available!\n{curVersion} → {newVersion}";
+ UpdaterPopup.Visible = true;
+
+ string url = latest["html_url"]!.GetValue();
+ Console.WriteLine($"An update is available! {curVersion} -> {newVersion}\n{url}");
+ UpdaterPopupDownloadButton.Click += (o, e) =>
+ {
+ ProcessStartInfo website = new()
+ {
+ FileName = url,
+ UseShellExecute = true
+ };
+ Process.Start(website);
+ };
+ }
+ else
+ {
+ Console.WriteLine("Up-to-date.");
+ UpdaterPopup.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to check for updates:\n{ex}");
+ }
+ }
+
+ private enum SelectionState
+ {
+ None = 0,
+ ViewportDrag,
+ GraphSelect,
+ ZoomBox,
+ }
}
diff --git a/Base/Forms/SetZoomForm.Designer.cs b/Base/Forms/SetZoomForm.Designer.cs
index 52990db..3d8fdaa 100644
--- a/Base/Forms/SetZoomForm.Designer.cs
+++ b/Base/Forms/SetZoomForm.Designer.cs
@@ -1,7 +1,4 @@
-using System.Drawing;
-using System.Windows.Forms;
-
-namespace Graphing.Forms
+namespace Graphing.Forms
{
partial class SetZoomForm
{
@@ -31,90 +28,157 @@ namespace Graphing.Forms
///
private void InitializeComponent()
{
- MessageLabel = new Label();
- ZoomTrackBar = new TrackBar();
- ValueLabel = new Label();
- ZoomMinValue = new TextBox();
- ZoomMaxValue = new TextBox();
- ((System.ComponentModel.ISupportInitialize)ZoomTrackBar).BeginInit();
+ EnableBoxSelect = new System.Windows.Forms.Button();
+ MatchAspectButton = new System.Windows.Forms.Button();
+ ResetButton = new System.Windows.Forms.Button();
+ NormalizeButton = new System.Windows.Forms.Button();
+ MinBoxX = new System.Windows.Forms.TextBox();
+ TextX = new System.Windows.Forms.Label();
+ MaxBoxX = new System.Windows.Forms.TextBox();
+ MaxBoxY = new System.Windows.Forms.TextBox();
+ TextY = new System.Windows.Forms.Label();
+ MinBoxY = new System.Windows.Forms.TextBox();
+ ViewportLock = new System.Windows.Forms.CheckBox();
SuspendLayout();
//
- // MessageLabel
+ // EnableBoxSelect
//
- MessageLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
- MessageLabel.Location = new Point(52, 20);
- MessageLabel.Name = "MessageLabel";
- MessageLabel.Size = new Size(413, 35);
- MessageLabel.TabIndex = 0;
- MessageLabel.Text = "Set the zoom level for the graph.";
- MessageLabel.TextAlign = ContentAlignment.MiddleCenter;
+ EnableBoxSelect.Location = new System.Drawing.Point(12, 12);
+ EnableBoxSelect.Name = "EnableBoxSelect";
+ EnableBoxSelect.Size = new System.Drawing.Size(187, 46);
+ EnableBoxSelect.TabIndex = 0;
+ EnableBoxSelect.Text = "Box Select";
+ EnableBoxSelect.UseVisualStyleBackColor = true;
+ EnableBoxSelect.Click += EnableBoxSelect_Click;
//
- // ZoomTrackBar
+ // MatchAspectButton
//
- ZoomTrackBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
- ZoomTrackBar.LargeChange = 1000;
- ZoomTrackBar.Location = new Point(12, 127);
- ZoomTrackBar.Maximum = 10000;
- ZoomTrackBar.Name = "ZoomTrackBar";
- ZoomTrackBar.Size = new Size(489, 90);
- ZoomTrackBar.TabIndex = 1;
- ZoomTrackBar.TickStyle = TickStyle.None;
- ZoomTrackBar.Scroll += ZoomTrackBar_Scroll;
+ MatchAspectButton.Location = new System.Drawing.Point(12, 64);
+ MatchAspectButton.Name = "MatchAspectButton";
+ MatchAspectButton.Size = new System.Drawing.Size(187, 46);
+ MatchAspectButton.TabIndex = 1;
+ MatchAspectButton.Text = "Match Aspect";
+ MatchAspectButton.UseVisualStyleBackColor = true;
+ MatchAspectButton.Click += MatchAspectButton_Click;
//
- // ValueLabel
+ // ResetButton
//
- ValueLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
- ValueLabel.Location = new Point(52, 91);
- ValueLabel.Name = "ValueLabel";
- ValueLabel.Size = new Size(413, 33);
- ValueLabel.TabIndex = 2;
- ValueLabel.Text = "1.00x";
- ValueLabel.TextAlign = ContentAlignment.TopCenter;
+ ResetButton.Location = new System.Drawing.Point(12, 168);
+ ResetButton.Name = "ResetButton";
+ ResetButton.Size = new System.Drawing.Size(187, 46);
+ ResetButton.TabIndex = 2;
+ ResetButton.Text = "Reset";
+ ResetButton.UseVisualStyleBackColor = true;
+ ResetButton.Click += ResetButton_Click;
//
- // ZoomMinValue
+ // NormalizeButton
//
- ZoomMinValue.Location = new Point(12, 178);
- ZoomMinValue.Name = "ZoomMinValue";
- ZoomMinValue.Size = new Size(83, 39);
- ZoomMinValue.TabIndex = 3;
- ZoomMinValue.Text = "0.50";
- ZoomMinValue.TextChanged += ZoomMinValue_TextChanged;
+ NormalizeButton.Location = new System.Drawing.Point(12, 116);
+ NormalizeButton.Name = "NormalizeButton";
+ NormalizeButton.Size = new System.Drawing.Size(187, 46);
+ NormalizeButton.TabIndex = 3;
+ NormalizeButton.Text = "Normalize";
+ NormalizeButton.UseVisualStyleBackColor = true;
+ NormalizeButton.Click += NormalizeButton_Click;
//
- // ZoomMaxValue
+ // MinBoxX
//
- ZoomMaxValue.Anchor = AnchorStyles.Top | AnchorStyles.Right;
- ZoomMaxValue.Location = new Point(418, 178);
- ZoomMaxValue.Name = "ZoomMaxValue";
- ZoomMaxValue.Size = new Size(83, 39);
- ZoomMaxValue.TabIndex = 4;
- ZoomMaxValue.Text = "2.00";
- ZoomMaxValue.TextAlign = HorizontalAlignment.Right;
- ZoomMaxValue.TextChanged += ZoomMaxValue_TextChanged;
+ MinBoxX.Location = new System.Drawing.Point(227, 49);
+ MinBoxX.Margin = new System.Windows.Forms.Padding(25, 3, 0, 3);
+ MinBoxX.Name = "MinBoxX";
+ MinBoxX.Size = new System.Drawing.Size(108, 39);
+ MinBoxX.TabIndex = 4;
+ //
+ // TextX
+ //
+ TextX.Location = new System.Drawing.Point(335, 49);
+ TextX.Margin = new System.Windows.Forms.Padding(0);
+ TextX.Name = "TextX";
+ TextX.Size = new System.Drawing.Size(77, 39);
+ TextX.TabIndex = 5;
+ TextX.Text = "≤ x ≤";
+ TextX.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // MaxBoxX
+ //
+ MaxBoxX.Location = new System.Drawing.Point(412, 49);
+ MaxBoxX.Margin = new System.Windows.Forms.Padding(0, 3, 25, 3);
+ MaxBoxX.Name = "MaxBoxX";
+ MaxBoxX.Size = new System.Drawing.Size(108, 39);
+ MaxBoxX.TabIndex = 6;
+ //
+ // MaxBoxY
+ //
+ MaxBoxY.Location = new System.Drawing.Point(412, 94);
+ MaxBoxY.Margin = new System.Windows.Forms.Padding(0, 3, 25, 3);
+ MaxBoxY.Name = "MaxBoxY";
+ MaxBoxY.Size = new System.Drawing.Size(108, 39);
+ MaxBoxY.TabIndex = 9;
+ //
+ // TextY
+ //
+ TextY.Location = new System.Drawing.Point(335, 94);
+ TextY.Margin = new System.Windows.Forms.Padding(0);
+ TextY.Name = "TextY";
+ TextY.Size = new System.Drawing.Size(77, 39);
+ TextY.TabIndex = 8;
+ TextY.Text = "≤ y ≤";
+ TextY.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // MinBoxY
+ //
+ MinBoxY.Location = new System.Drawing.Point(227, 94);
+ MinBoxY.Margin = new System.Windows.Forms.Padding(25, 3, 0, 3);
+ MinBoxY.Name = "MinBoxY";
+ MinBoxY.Size = new System.Drawing.Size(108, 39);
+ MinBoxY.TabIndex = 7;
+ //
+ // ViewportLock
+ //
+ ViewportLock.Location = new System.Drawing.Point(227, 139);
+ ViewportLock.Name = "ViewportLock";
+ ViewportLock.Size = new System.Drawing.Size(293, 39);
+ ViewportLock.TabIndex = 10;
+ ViewportLock.Text = "Lock Viewport";
+ ViewportLock.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ ViewportLock.UseVisualStyleBackColor = true;
+ ViewportLock.CheckedChanged += ViewportLock_CheckedChanged;
//
// SetZoomForm
//
- AutoScaleDimensions = new SizeF(13F, 32F);
- AutoScaleMode = AutoScaleMode.Font;
- ClientSize = new Size(513, 230);
- Controls.Add(ZoomMaxValue);
- Controls.Add(ZoomMinValue);
- Controls.Add(ValueLabel);
- Controls.Add(ZoomTrackBar);
- Controls.Add(MessageLabel);
- FormBorderStyle = FormBorderStyle.FixedToolWindow;
+ AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ ClientSize = new System.Drawing.Size(533, 227);
+ Controls.Add(ViewportLock);
+ Controls.Add(MaxBoxY);
+ Controls.Add(TextY);
+ Controls.Add(MinBoxY);
+ Controls.Add(MaxBoxX);
+ Controls.Add(TextX);
+ Controls.Add(MinBoxX);
+ Controls.Add(NormalizeButton);
+ Controls.Add(ResetButton);
+ Controls.Add(MatchAspectButton);
+ Controls.Add(EnableBoxSelect);
+ FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
Name = "SetZoomForm";
- Text = "Zoom Level";
- ((System.ComponentModel.ISupportInitialize)ZoomTrackBar).EndInit();
+ Text = "Set Viewport Zoom";
ResumeLayout(false);
PerformLayout();
}
#endregion
- private Label MessageLabel;
- private TrackBar ZoomTrackBar;
- private Label ValueLabel;
- private TextBox ZoomMinValue;
- private TextBox ZoomMaxValue;
+ private System.Windows.Forms.Button EnableBoxSelect;
+ private System.Windows.Forms.Button MatchAspectButton;
+ private System.Windows.Forms.Button ResetButton;
+ private System.Windows.Forms.Button NormalizeButton;
+ private System.Windows.Forms.TextBox MinBoxX;
+ private System.Windows.Forms.Label TextX;
+ private System.Windows.Forms.TextBox MaxBoxX;
+ private System.Windows.Forms.TextBox MaxBoxY;
+ private System.Windows.Forms.Label TextY;
+ private System.Windows.Forms.TextBox MinBoxY;
+ private System.Windows.Forms.CheckBox ViewportLock;
}
}
\ No newline at end of file
diff --git a/Base/Forms/SetZoomForm.cs b/Base/Forms/SetZoomForm.cs
index fc21296..939a7fc 100644
--- a/Base/Forms/SetZoomForm.cs
+++ b/Base/Forms/SetZoomForm.cs
@@ -5,118 +5,223 @@ namespace Graphing.Forms;
public partial class SetZoomForm : Form
{
- private double minZoomRange;
- private double maxZoomRange;
+ private readonly GraphForm refForm;
- private double zoomLevel;
+ private bool boxSelectEnabled;
- private readonly GraphForm form;
-
- public SetZoomForm(GraphForm form)
+ public SetZoomForm(GraphForm refForm)
{
InitializeComponent();
+ this.refForm = refForm;
- minZoomRange = 1 / (form.ZoomLevel * 2);
- maxZoomRange = 2 / form.ZoomLevel;
- zoomLevel = 1 / form.ZoomLevel;
+ refForm.Paint += (o, e) => RedeclareValues();
+ RedeclareValues();
- ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum);
-
- this.form = form;
- }
-
- protected override void OnPaint(PaintEventArgs e)
- {
- ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
- ZoomMinValue.Text = minZoomRange.ToString("0.00");
-
- ValueLabel.Text = $"{zoomLevel:0.00}x";
-
- base.OnPaint(e);
-
- form.ZoomLevel = 1 / zoomLevel;
- form.Invalidate(false);
- }
-
- private double FactorToZoom(double factor)
- {
- return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange);
- }
- private double ZoomToFactor(double zoom)
- {
- double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange);
- return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue));
- }
-
- private void ZoomTrackBar_Scroll(object? sender, EventArgs e)
- {
- double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
- zoomLevel = FactorToZoom(factor);
-
- Invalidate(true);
- }
-
- private void ZoomMinValue_TextChanged(object? sender, EventArgs e)
- {
- double original = minZoomRange;
- try
+ MinBoxX.Leave += MinBoxX_Finish;
+ MinBoxX.KeyDown += (o, e) =>
{
- double value;
- if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) ||
- ZoomMinValue.Text.EndsWith('.'))
- {
- return;
- }
- else
- {
- value = double.Parse(ZoomMinValue.Text);
- if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new();
- }
+ if (e.KeyCode == Keys.Enter) MinBoxX_Finish(o, e);
+ };
+ MaxBoxX.Leave += MaxBoxX_Finish;
+ MaxBoxX.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) MaxBoxX_Finish(o, e);
+ };
- minZoomRange = value;
- ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum);
- double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
- double newZoom = FactorToZoom(factor);
+ MinBoxY.Leave += MinBoxY_Finish;
+ MinBoxY.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) MinBoxY_Finish(o, e);
+ };
+ MaxBoxY.Leave += MaxBoxY_Finish;
+ MaxBoxY.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) MaxBoxY_Finish(o, e);
+ };
+ }
- zoomLevel = newZoom;
- if (newZoom != factor) Invalidate(true);
+ private void EnableBoxSelect_Click(object? sender, EventArgs e)
+ {
+ boxSelectEnabled = !boxSelectEnabled;
+ refForm.canBoxSelect = boxSelectEnabled;
+
+ if (boxSelectEnabled)
+ {
+ EnableBoxSelect.Text = $"Cancel ...";
+ refForm.Focus();
}
- catch
+ else
{
- minZoomRange = original;
- ZoomMinValue.Text = minZoomRange.ToString("0.00");
+ EnableBoxSelect.Text = "Box Select";
}
}
-
- private void ZoomMaxValue_TextChanged(object sender, EventArgs e)
+ private void MatchAspectButton_Click(object? sender, EventArgs e)
{
- double original = maxZoomRange;
- try
+ double zoomXFactor = refForm.ZoomLevel.x / refForm.ZoomLevel.y;
+ double actualXFactor = refForm.ClientRectangle.Width / refForm.ClientRectangle.Height;
+
+ double diff = actualXFactor / zoomXFactor;
+ int newWidth = (int)(refForm.Width / diff);
+ refForm.ZoomLevel = new(refForm.ZoomLevel.x * diff, refForm.ZoomLevel.y);
+
+ int maxScreenWidth = Screen.FromControl(refForm).WorkingArea.Width;
+ if (newWidth >= maxScreenWidth)
{
- double value;
- if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) ||
- ZoomMaxValue.Text.EndsWith('.'))
+ refForm.Location = new(0, refForm.Location.Y);
+
+ double xScaleFactor = (double)maxScreenWidth / newWidth;
+ newWidth = maxScreenWidth;
+ refForm.Height = (int)(refForm.Height * xScaleFactor);
+ refForm.ZoomLevel = new(refForm.ZoomLevel.x * xScaleFactor, refForm.ZoomLevel.y * xScaleFactor);
+ }
+
+ refForm.Width = newWidth;
+ }
+ private void NormalizeButton_Click(object? sender, EventArgs e)
+ {
+ double factor = 1 / Math.Min(refForm.ZoomLevel.x, refForm.ZoomLevel.y);
+ refForm.ZoomLevel = new(factor * refForm.ZoomLevel.x, factor * refForm.ZoomLevel.y);
+ }
+ private void ResetButton_Click(object? sender, EventArgs e)
+ {
+ refForm.ResetAllViewport();
+ }
+ private void ViewportLock_CheckedChanged(object? sender, EventArgs e)
+ {
+ refForm.ViewportLocked = ViewportLock.Checked;
+ RedeclareValues();
+ }
+
+ private void MinBoxX_Finish(object? sender, EventArgs e)
+ {
+ if (double.TryParse(MinBoxX.Text, out double minX))
+ {
+ Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
+
+ if (minX > max.x)
{
- return;
- }
- else
- {
- value = double.Parse(ZoomMaxValue.Text);
- if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new();
+ MaxBoxX.Text = MinBoxX.Text;
+ MaxBoxX_Finish(sender, e);
+ minX = max.x;
+
+ // Redefine bounds.
+ min = refForm.MinVisibleGraph;
+ max = refForm.MaxVisibleGraph;
}
- maxZoomRange = value;
- ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum);
- double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
- double newZoom = FactorToZoom(factor);
+ double newCenterX = (minX + max.x) / 2,
+ zoomFactorX = (max.x - minX) / (max.x - min.x);
- zoomLevel = newZoom;
- if (newZoom != factor) Invalidate(true);
+ refForm.ScreenCenter = new(newCenterX, refForm.ScreenCenter.y);
+ refForm.ZoomLevel = new(refForm.ZoomLevel.x * zoomFactorX, refForm.ZoomLevel.y);
}
- catch
+
+ refForm.Invalidate(false);
+ }
+ private void MaxBoxX_Finish(object? sender, EventArgs e)
+ {
+ if (double.TryParse(MaxBoxX.Text, out double maxX))
{
- maxZoomRange = original;
- ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
+ Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
+
+ if (maxX < min.x)
+ {
+ MinBoxX.Text = MaxBoxX.Text;
+ MinBoxX_Finish(sender, e);
+ maxX = min.x;
+
+ // Redefine bounds.
+ min = refForm.MinVisibleGraph;
+ max = refForm.MaxVisibleGraph;
+ }
+
+ double newCenterX = (min.x + maxX) / 2,
+ zoomFactorX = (maxX - min.x) / (max.x - min.x);
+
+ refForm.ScreenCenter = new(newCenterX, refForm.ScreenCenter.y);
+ refForm.ZoomLevel = new(refForm.ZoomLevel.x * zoomFactorX, refForm.ZoomLevel.y);
}
+
+ refForm.Invalidate(false);
+ }
+ private void MinBoxY_Finish(object? sender, EventArgs e)
+ {
+ if (double.TryParse(MinBoxY.Text, out double minY))
+ {
+ Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
+
+ if (minY > max.y)
+ {
+ MaxBoxY.Text = MinBoxY.Text;
+ MaxBoxY_Finish(sender, e);
+ minY = max.y;
+
+ // Redefine bounds.
+ min = refForm.MinVisibleGraph;
+ max = refForm.MaxVisibleGraph;
+ }
+
+ double newCenterY = -(minY + max.y) / 2, // Keeping it positive flips it for some reason ???
+ zoomFactorY = (max.y - minY) / (max.y - min.y);
+
+ refForm.ScreenCenter = new(refForm.ScreenCenter.x, newCenterY);
+ refForm.ZoomLevel = new(refForm.ZoomLevel.x, refForm.ZoomLevel.y * zoomFactorY);
+ }
+
+ refForm.Invalidate(false);
+ }
+ private void MaxBoxY_Finish(object? sender, EventArgs e)
+ {
+ if (double.TryParse(MaxBoxY.Text, out double maxY))
+ {
+ Float2 min = refForm.MinVisibleGraph, max = refForm.MaxVisibleGraph;
+
+ if (maxY < min.y)
+ {
+ MinBoxY.Text = MaxBoxY.Text;
+ MinBoxY_Finish(sender, e);
+ maxY = min.y;
+
+ // Redefine bounds.
+ min = refForm.MinVisibleGraph;
+ max = refForm.MaxVisibleGraph;
+ }
+
+ double newCenterY = -(min.y + maxY) / 2, // Keeping it positive flips it for some reason ???
+ zoomFactorY = (maxY - min.y) / (max.y - min.y);
+
+ refForm.ScreenCenter = new(refForm.ScreenCenter.x, newCenterY);
+ refForm.ZoomLevel = new(refForm.ZoomLevel.x, refForm.ZoomLevel.y * zoomFactorY);
+ }
+
+ refForm.Invalidate(false);
+ }
+
+ public void RedeclareValues()
+ {
+ bool enabled = !refForm.ViewportLocked;
+
+ Float2 minGraph = refForm.MinVisibleGraph,
+ maxGraph = refForm.MaxVisibleGraph;
+
+ MinBoxX.Text = $"{minGraph.x:0.000}";
+ MaxBoxX.Text = $"{maxGraph.x:0.000}";
+ MinBoxY.Text = $"{minGraph.y:0.000}";
+ MaxBoxY.Text = $"{maxGraph.y:0.000}";
+
+ ViewportLock.Checked = !enabled;
+ EnableBoxSelect.Enabled = enabled;
+ MatchAspectButton.Enabled = enabled;
+ NormalizeButton.Enabled = enabled;
+ ResetButton.Enabled = enabled;
+ MinBoxX.Enabled = enabled;
+ MaxBoxX.Enabled = enabled;
+ MinBoxY.Enabled = enabled;
+ MaxBoxY.Enabled = enabled;
+ }
+
+ internal void CompleteBoxSelection()
+ {
+ if (boxSelectEnabled) EnableBoxSelect_Click(null, new());
}
}
diff --git a/Base/Forms/SlopeFieldDetailForm.Designer.cs b/Base/Forms/SlopeFieldDetailForm.Designer.cs
new file mode 100644
index 0000000..1d93ed2
--- /dev/null
+++ b/Base/Forms/SlopeFieldDetailForm.Designer.cs
@@ -0,0 +1,147 @@
+namespace Graphing.Forms
+{
+ partial class SlopeFieldDetailForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ Message = new System.Windows.Forms.Label();
+ TrackSlopeDetail = new System.Windows.Forms.TrackBar();
+ MinDetailBox = new System.Windows.Forms.TextBox();
+ MaxDetailBox = new System.Windows.Forms.TextBox();
+ CurrentDetailBox = new System.Windows.Forms.TextBox();
+ IncrementButton = new System.Windows.Forms.Button();
+ DecrementButton = new System.Windows.Forms.Button();
+ ((System.ComponentModel.ISupportInitialize)TrackSlopeDetail).BeginInit();
+ SuspendLayout();
+ //
+ // Message
+ //
+ Message.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ Message.Location = new System.Drawing.Point(119, 25);
+ Message.Margin = new System.Windows.Forms.Padding(110);
+ Message.Name = "Message";
+ Message.Size = new System.Drawing.Size(516, 109);
+ Message.TabIndex = 1;
+ Message.Text = "Change the Detail of %name%\r\nA higher value means more lines per unit.";
+ Message.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // TrackSlopeDetail
+ //
+ TrackSlopeDetail.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ TrackSlopeDetail.LargeChange = 250;
+ TrackSlopeDetail.Location = new System.Drawing.Point(59, 158);
+ TrackSlopeDetail.Margin = new System.Windows.Forms.Padding(50);
+ TrackSlopeDetail.Maximum = 1000;
+ TrackSlopeDetail.Name = "TrackSlopeDetail";
+ TrackSlopeDetail.Size = new System.Drawing.Size(636, 90);
+ TrackSlopeDetail.SmallChange = 0;
+ TrackSlopeDetail.TabIndex = 0;
+ TrackSlopeDetail.TickFrequency = 0;
+ TrackSlopeDetail.TickStyle = System.Windows.Forms.TickStyle.Both;
+ TrackSlopeDetail.Scroll += TrackSlopeDetail_Scroll;
+ //
+ // MinDetailBox
+ //
+ MinDetailBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ MinDetailBox.Location = new System.Drawing.Point(12, 228);
+ MinDetailBox.Name = "MinDetailBox";
+ MinDetailBox.Size = new System.Drawing.Size(100, 39);
+ MinDetailBox.TabIndex = 2;
+ //
+ // MaxDetailBox
+ //
+ MaxDetailBox.Anchor = System.Windows.Forms.AnchorStyles.Right;
+ MaxDetailBox.Location = new System.Drawing.Point(642, 228);
+ MaxDetailBox.Name = "MaxDetailBox";
+ MaxDetailBox.Size = new System.Drawing.Size(100, 39);
+ MaxDetailBox.TabIndex = 3;
+ MaxDetailBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
+ //
+ // CurrentDetailBox
+ //
+ CurrentDetailBox.Anchor = System.Windows.Forms.AnchorStyles.None;
+ CurrentDetailBox.Location = new System.Drawing.Point(330, 228);
+ CurrentDetailBox.Name = "CurrentDetailBox";
+ CurrentDetailBox.Size = new System.Drawing.Size(100, 39);
+ CurrentDetailBox.TabIndex = 4;
+ CurrentDetailBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ //
+ // IncrementButton
+ //
+ IncrementButton.Anchor = System.Windows.Forms.AnchorStyles.None;
+ IncrementButton.Font = new System.Drawing.Font("Segoe UI", 7.875F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
+ IncrementButton.Location = new System.Drawing.Point(436, 228);
+ IncrementButton.Name = "IncrementButton";
+ IncrementButton.Size = new System.Drawing.Size(40, 40);
+ IncrementButton.TabIndex = 5;
+ IncrementButton.Text = "+";
+ IncrementButton.UseVisualStyleBackColor = true;
+ IncrementButton.Click += IncrementButton_Click;
+ //
+ // DecrementButton
+ //
+ DecrementButton.Anchor = System.Windows.Forms.AnchorStyles.None;
+ DecrementButton.Font = new System.Drawing.Font("Segoe UI", 7.875F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
+ DecrementButton.Location = new System.Drawing.Point(284, 228);
+ DecrementButton.Name = "DecrementButton";
+ DecrementButton.Size = new System.Drawing.Size(40, 40);
+ DecrementButton.TabIndex = 6;
+ DecrementButton.Text = "-";
+ DecrementButton.UseVisualStyleBackColor = true;
+ DecrementButton.Click += DecrementButton_Click;
+ //
+ // SlopeFieldDetailForm
+ //
+ AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ ClientSize = new System.Drawing.Size(754, 282);
+ Controls.Add(DecrementButton);
+ Controls.Add(IncrementButton);
+ Controls.Add(CurrentDetailBox);
+ Controls.Add(MaxDetailBox);
+ Controls.Add(MinDetailBox);
+ Controls.Add(Message);
+ Controls.Add(TrackSlopeDetail);
+ FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
+ Name = "SlopeFieldDetailForm";
+ Text = "Change Slope Field Detail";
+ ((System.ComponentModel.ISupportInitialize)TrackSlopeDetail).EndInit();
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Label Message;
+ private System.Windows.Forms.TrackBar TrackSlopeDetail;
+ private System.Windows.Forms.TextBox MinDetailBox;
+ private System.Windows.Forms.TextBox MaxDetailBox;
+ private System.Windows.Forms.TextBox CurrentDetailBox;
+ private System.Windows.Forms.Button IncrementButton;
+ private System.Windows.Forms.Button DecrementButton;
+ }
+}
\ No newline at end of file
diff --git a/Base/Forms/SlopeFieldDetailForm.cs b/Base/Forms/SlopeFieldDetailForm.cs
new file mode 100644
index 0000000..20575c7
--- /dev/null
+++ b/Base/Forms/SlopeFieldDetailForm.cs
@@ -0,0 +1,130 @@
+using Graphing.Graphables;
+using System;
+using System.Windows.Forms;
+
+namespace Graphing.Forms;
+
+public partial class SlopeFieldDetailForm : Form
+{
+ private readonly GraphForm refForm;
+ private readonly SlopeField slopeField;
+
+ private double minDetail, maxDetail;
+
+ public SlopeFieldDetailForm(GraphForm form, SlopeField sf)
+ {
+ InitializeComponent();
+
+ refForm = form;
+ slopeField = sf;
+
+ refForm.Paint += (o, e) => RedeclareValues();
+ RedeclareValues();
+
+ TrackSlopeDetail.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Right) IncrementButton_Click(o, e);
+ else if (e.KeyCode == Keys.Left) DecrementButton_Click(o, e);
+ };
+
+ MinDetailBox.Leave += MinDetailBox_Finish;
+ MinDetailBox.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) MinDetailBox_Finish(o, e);
+ };
+ MaxDetailBox.Leave += MaxDetailBox_Finish;
+ MaxDetailBox.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) MaxDetailBox_Finish(o, e);
+ };
+ CurrentDetailBox.Leave += CurrentDetailBox_Finish;
+ CurrentDetailBox.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) CurrentDetailBox_Finish(o, e);
+ };
+
+ minDetail = sf.Detail / 2;
+ maxDetail = sf.Detail * 2;
+
+ Message.Text = Message.Text.Replace("%name%", sf.Name);
+ }
+
+ // Exponential interpolations are better than simple lerps here since
+ // we're scaling a multiple rather than an additive.
+ private double Interp(double t)
+ {
+ // This is weird. I don't like the +1s and -1s, I don't think I wrote this right.
+ // But it seems to get the job done.
+ return minDetail + Math.Pow(2, t * Math.Log2(maxDetail - minDetail + 1)) - 1;
+ }
+ private double InverseInterp(double c)
+ {
+ return Math.Log2(c - minDetail + 1) / Math.Log2(maxDetail - minDetail + 1);
+ }
+
+ private void RedeclareValues()
+ {
+ double detail = slopeField.Detail;
+ if (detail < minDetail) minDetail = detail;
+ else if (detail > maxDetail) maxDetail = detail;
+
+ double t = InverseInterp(detail);
+ TrackSlopeDetail.Value = (int)(TrackSlopeDetail.Minimum + t * (TrackSlopeDetail.Maximum - TrackSlopeDetail.Minimum));
+
+ MinDetailBox.Text = $"{minDetail:0.00}";
+ MaxDetailBox.Text = $"{maxDetail:0.00}";
+ CurrentDetailBox.Text = $"{detail:0.00}";
+ }
+
+ private void TrackSlopeDetail_Scroll(object? sender, EventArgs e)
+ {
+ double t = (double)(TrackSlopeDetail.Value - TrackSlopeDetail.Minimum) / (TrackSlopeDetail.Maximum - TrackSlopeDetail.Minimum);
+ double newDetail = Interp(t);
+
+ slopeField.Detail = newDetail;
+ refForm.Invalidate(false);
+ }
+ private void MinDetailBox_Finish(object? sender, EventArgs e)
+ {
+ if (double.TryParse(MinDetailBox.Text, out double newMinDetail))
+ {
+ minDetail = newMinDetail;
+ if (minDetail > slopeField.Detail) slopeField.Detail = newMinDetail;
+ }
+ refForm.Invalidate(false);
+ }
+ private void MaxDetailBox_Finish(object? sender, EventArgs e)
+ {
+ if (double.TryParse(MaxDetailBox.Text, out double newMaxDetail))
+ {
+ maxDetail = newMaxDetail;
+ if (maxDetail < slopeField.Detail) slopeField.Detail = newMaxDetail;
+ }
+ refForm.Invalidate(false);
+ }
+ private void CurrentDetailBox_Finish(object? sender, EventArgs e)
+ {
+ if (double.TryParse(CurrentDetailBox.Text, out double newDetail))
+ {
+ if (newDetail < minDetail) minDetail = newDetail;
+ else if (newDetail > maxDetail) maxDetail = newDetail;
+ slopeField.Detail = newDetail;
+ }
+ refForm.Invalidate(false);
+ }
+
+ private void IncrementButton_Click(object? sender, EventArgs e)
+ {
+ double newDetail = slopeField.Detail * 1.0625f;
+ if (newDetail > maxDetail) maxDetail = newDetail;
+ slopeField.Detail = newDetail;
+ refForm.Invalidate(false);
+ }
+ private void DecrementButton_Click(object? sender, EventArgs e)
+ {
+ double newDetail = slopeField.Detail / 1.0625f;
+ if (newDetail < minDetail) minDetail = newDetail;
+ slopeField.Detail = newDetail;
+ refForm.Invalidate(false);
+ }
+}
diff --git a/Base/Forms/SlopeFieldDetailForm.resx b/Base/Forms/SlopeFieldDetailForm.resx
new file mode 100644
index 0000000..af32865
--- /dev/null
+++ b/Base/Forms/SlopeFieldDetailForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Base/Forms/TranslateForm.Designer.cs b/Base/Forms/TranslateForm.Designer.cs
new file mode 100644
index 0000000..2f9f7c6
--- /dev/null
+++ b/Base/Forms/TranslateForm.Designer.cs
@@ -0,0 +1,204 @@
+namespace Graphing.Forms
+{
+ partial class TranslateForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ TrackX = new System.Windows.Forms.TrackBar();
+ LabelX = new System.Windows.Forms.Label();
+ MinBoxX = new System.Windows.Forms.TextBox();
+ MaxBoxX = new System.Windows.Forms.TextBox();
+ ThisValueX = new System.Windows.Forms.TextBox();
+ ThisValueY = new System.Windows.Forms.TextBox();
+ MaxBoxY = new System.Windows.Forms.TextBox();
+ MinBoxY = new System.Windows.Forms.TextBox();
+ LabelY = new System.Windows.Forms.Label();
+ TrackY = new System.Windows.Forms.TrackBar();
+ TitleLabel = new System.Windows.Forms.Label();
+ ((System.ComponentModel.ISupportInitialize)TrackX).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)TrackY).BeginInit();
+ SuspendLayout();
+ //
+ // TrackX
+ //
+ TrackX.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ TrackX.LargeChange = 250;
+ TrackX.Location = new System.Drawing.Point(15, 193);
+ TrackX.Margin = new System.Windows.Forms.Padding(0);
+ TrackX.Maximum = 1000;
+ TrackX.Name = "TrackX";
+ TrackX.Size = new System.Drawing.Size(644, 90);
+ TrackX.SmallChange = 50;
+ TrackX.TabIndex = 0;
+ TrackX.TabStop = false;
+ TrackX.TickFrequency = 50;
+ TrackX.TickStyle = System.Windows.Forms.TickStyle.Both;
+ TrackX.Value = 1;
+ TrackX.Scroll += TrackX_Scroll;
+ //
+ // LabelX
+ //
+ LabelX.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ LabelX.Location = new System.Drawing.Point(15, 157);
+ LabelX.Name = "LabelX";
+ LabelX.Size = new System.Drawing.Size(644, 36);
+ LabelX.TabIndex = 1;
+ LabelX.Text = "X Offset";
+ LabelX.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // MinBoxX
+ //
+ MinBoxX.Location = new System.Drawing.Point(15, 259);
+ MinBoxX.Name = "MinBoxX";
+ MinBoxX.Size = new System.Drawing.Size(100, 39);
+ MinBoxX.TabIndex = 2;
+ //
+ // MaxBoxX
+ //
+ MaxBoxX.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
+ MaxBoxX.Location = new System.Drawing.Point(556, 259);
+ MaxBoxX.Name = "MaxBoxX";
+ MaxBoxX.Size = new System.Drawing.Size(100, 39);
+ MaxBoxX.TabIndex = 3;
+ MaxBoxX.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
+ //
+ // ThisValueX
+ //
+ ThisValueX.Anchor = System.Windows.Forms.AnchorStyles.Top;
+ ThisValueX.Location = new System.Drawing.Point(289, 259);
+ ThisValueX.Name = "ThisValueX";
+ ThisValueX.Size = new System.Drawing.Size(100, 39);
+ ThisValueX.TabIndex = 4;
+ ThisValueX.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ //
+ // ThisValueY
+ //
+ ThisValueY.Anchor = System.Windows.Forms.AnchorStyles.Top;
+ ThisValueY.Location = new System.Drawing.Point(289, 449);
+ ThisValueY.Name = "ThisValueY";
+ ThisValueY.Size = new System.Drawing.Size(100, 39);
+ ThisValueY.TabIndex = 9;
+ ThisValueY.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ //
+ // MaxBoxY
+ //
+ MaxBoxY.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
+ MaxBoxY.Location = new System.Drawing.Point(556, 449);
+ MaxBoxY.Name = "MaxBoxY";
+ MaxBoxY.Size = new System.Drawing.Size(100, 39);
+ MaxBoxY.TabIndex = 8;
+ MaxBoxY.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
+ //
+ // MinBoxY
+ //
+ MinBoxY.Location = new System.Drawing.Point(15, 449);
+ MinBoxY.Name = "MinBoxY";
+ MinBoxY.Size = new System.Drawing.Size(100, 39);
+ MinBoxY.TabIndex = 7;
+ //
+ // LabelY
+ //
+ LabelY.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ LabelY.Location = new System.Drawing.Point(15, 347);
+ LabelY.Name = "LabelY";
+ LabelY.Size = new System.Drawing.Size(644, 36);
+ LabelY.TabIndex = 6;
+ LabelY.Text = "Y Offset";
+ LabelY.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // TrackY
+ //
+ TrackY.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ TrackY.LargeChange = 250;
+ TrackY.Location = new System.Drawing.Point(15, 383);
+ TrackY.Margin = new System.Windows.Forms.Padding(0);
+ TrackY.Maximum = 1000;
+ TrackY.Name = "TrackY";
+ TrackY.Size = new System.Drawing.Size(644, 90);
+ TrackY.SmallChange = 50;
+ TrackY.TabIndex = 5;
+ TrackY.TabStop = false;
+ TrackY.TickFrequency = 50;
+ TrackY.TickStyle = System.Windows.Forms.TickStyle.Both;
+ TrackY.Value = 1;
+ TrackY.Scroll += TrackY_Scroll;
+ //
+ // TitleLabel
+ //
+ TitleLabel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ TitleLabel.Location = new System.Drawing.Point(12, 39);
+ TitleLabel.Name = "TitleLabel";
+ TitleLabel.Padding = new System.Windows.Forms.Padding(0, 0, 0, 18);
+ TitleLabel.Size = new System.Drawing.Size(644, 89);
+ TitleLabel.TabIndex = 10;
+ TitleLabel.Text = "Change the Location of %name%";
+ TitleLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // TranslateForm
+ //
+ AutoScaleDimensions = new System.Drawing.SizeF(13F, 32F);
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ AutoSize = true;
+ AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ ClientSize = new System.Drawing.Size(674, 531);
+ Controls.Add(TitleLabel);
+ Controls.Add(ThisValueY);
+ Controls.Add(MaxBoxY);
+ Controls.Add(MinBoxY);
+ Controls.Add(LabelY);
+ Controls.Add(TrackY);
+ Controls.Add(ThisValueX);
+ Controls.Add(MaxBoxX);
+ Controls.Add(MinBoxX);
+ Controls.Add(LabelX);
+ Controls.Add(TrackX);
+ FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
+ Name = "TranslateForm";
+ Padding = new System.Windows.Forms.Padding(15);
+ Text = "Herm";
+ TopMost = true;
+ ((System.ComponentModel.ISupportInitialize)TrackX).EndInit();
+ ((System.ComponentModel.ISupportInitialize)TrackY).EndInit();
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TrackBar TrackX;
+ private System.Windows.Forms.Label LabelX;
+ private System.Windows.Forms.TextBox MinBoxX;
+ private System.Windows.Forms.TextBox MaxBoxX;
+ private System.Windows.Forms.TextBox ThisValueX;
+ private System.Windows.Forms.TextBox ThisValueY;
+ private System.Windows.Forms.TextBox MaxBoxY;
+ private System.Windows.Forms.TextBox MinBoxY;
+ private System.Windows.Forms.Label LabelY;
+ private System.Windows.Forms.TrackBar TrackY;
+ private System.Windows.Forms.Label TitleLabel;
+ }
+}
\ No newline at end of file
diff --git a/Base/Forms/TranslateForm.cs b/Base/Forms/TranslateForm.cs
new file mode 100644
index 0000000..d0ac022
--- /dev/null
+++ b/Base/Forms/TranslateForm.cs
@@ -0,0 +1,285 @@
+using Graphing.Abstract;
+using System;
+using System.Windows.Forms;
+
+namespace Graphing.Forms;
+
+public partial class TranslateForm : Form
+{
+ private readonly GraphForm refForm;
+
+ // These variables both represent the same graphable.
+ private readonly ITranslatableX? ableTransX;
+ private readonly ITranslatableY? ableTransY;
+
+ private readonly bool useX;
+ private readonly bool useY;
+
+ private double minX, maxX, curX, minY, maxY, curY;
+
+ public TranslateForm(GraphForm graph, Graphable ableRaw, ITranslatable ableTrans)
+ {
+ InitializeComponent();
+
+ Text = $"Translate {ableRaw.Name}";
+ TitleLabel.Text = $"Adjust Location for {ableRaw.Name}";
+
+ MinBoxX.Leave += (o, e) => UpdateFromMinBoxX();
+ MinBoxX.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) UpdateFromMinBoxX();
+ };
+ MaxBoxX.Leave += (o, e) => UpdateFromMaxBoxX();
+ MaxBoxX.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) UpdateFromMaxBoxX();
+ };
+ ThisValueX.Leave += (o, e) => UpdateFromThisBoxX();
+ ThisValueX.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) UpdateFromThisBoxX();
+ };
+
+ MinBoxY.Leave += (o, e) => UpdateFromMinBoxY();
+ MinBoxY.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) UpdateFromMinBoxY();
+ };
+ MaxBoxY.Leave += (o, e) => UpdateFromMaxBoxY();
+ MaxBoxY.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) UpdateFromMaxBoxY();
+ };
+ ThisValueY.Leave += (o, e) => UpdateFromThisBoxY();
+ ThisValueY.KeyDown += (o, e) =>
+ {
+ if (e.KeyCode == Keys.Enter) UpdateFromThisBoxY();
+ };
+
+ refForm = graph;
+
+ double curX = 0, curY = 0;
+ if (ableTrans is ITranslatableX transX)
+ {
+ useX = true;
+ ableTransX = transX;
+ curX = transX.OffsetX;
+ }
+ else
+ {
+ LabelY.Location = LabelX.Location;
+ TrackY.Location = TrackX.Location;
+ MinBoxY.Location = MinBoxX.Location;
+ MaxBoxY.Location = MaxBoxX.Location;
+ ThisValueY.Location = ThisValueX.Location;
+
+ LabelX.Dispose();
+ TrackX.Dispose();
+ MinBoxX.Dispose();
+ MaxBoxX.Dispose();
+ ThisValueX.Dispose();
+ }
+
+ if (ableTrans is ITranslatableY transY)
+ {
+ useY = true;
+ ableTransY = transY;
+ curY = transY.OffsetY;
+ }
+ else
+ {
+ LabelY.Dispose();
+ TrackY.Dispose();
+ MinBoxY.Dispose();
+ MaxBoxY.Dispose();
+ ThisValueY.Dispose();
+ }
+
+ if (!useX && !useY)
+ {
+ TitleLabel.Text = $"There doesn't seem to be anything you can translate for {ableRaw.Name}.";
+ }
+
+ // TODO: Maybe replace these default limits with what's visible on screen?
+ // Tried it and it got a bit confusing so maybe not.
+ minX = -10;
+ maxX = 10;
+ minY = -10;
+ maxY = 10;
+
+ UpdateFromCurX(curX, false);
+ UpdateFromCurY(curY, false);
+ }
+
+ private void UpdateFromCurX(double newCurX, bool invalidate)
+ {
+ curX = newCurX;
+ if (curX < minX) minX = curX;
+ else if (curX > maxX) maxX = curX;
+
+ int step = (int)(1000 * InverseLerp(minX, maxX, curX));
+ TrackX.Value = step;
+ MinBoxX.Text = $"{minX:0.00}";
+ MaxBoxX.Text = $"{maxX:0.00}";
+ ThisValueX.Text = $"{curX:0.00}";
+
+ if (invalidate) refForm.Invalidate(false);
+ }
+ private void UpdateFromSliderX(bool invalidate)
+ {
+ double t = InverseLerp(0, 1000, TrackX.Value);
+ curX = Lerp(minX, maxX, t);
+
+ ThisValueX.Text = $"{curX:0.00}";
+ ableTransX!.OffsetX = curX;
+
+ if (invalidate) refForm.Invalidate(false);
+ }
+ private void UpdateFromMinBoxX()
+ {
+ if (!double.TryParse(MinBoxX.Text, out double newMin))
+ {
+ MinBoxX.Text = $"{minX:0.00}";
+ return;
+ }
+ minX = newMin;
+ MinBoxX.Text = $"{minX:0.00}";
+
+ if (minX > curX)
+ {
+ curX = minX;
+ ThisValueX.Text = $"{curX:0.00}";
+ ableTransX!.OffsetX = curX;
+ }
+
+ int step = (int)(1000 * InverseLerp(minX, maxX, curX));
+ TrackX.Value = step;
+
+ refForm.Invalidate(false);
+ }
+ private void UpdateFromMaxBoxX()
+ {
+ if (!double.TryParse(MaxBoxX.Text, out double newMax))
+ {
+ MaxBoxX.Text = $"{maxX:0.00}";
+ return;
+ }
+
+ maxX = newMax;
+ MaxBoxX.Text = $"{maxX:0.00}";
+
+ if (maxX < curX)
+ {
+ curX = maxX;
+ ThisValueX.Text = $"{curX:0.00}";
+ ableTransX!.OffsetX = curX;
+ }
+
+ int step = (int)(1000 * InverseLerp(minX, maxX, curX));
+ TrackX.Value = step;
+
+ refForm.Invalidate(false);
+ }
+ private void UpdateFromThisBoxX()
+ {
+ if (!double.TryParse(ThisValueX.Text, out double newCur))
+ {
+ ThisValueX.Text = $"{curX:0.00}";
+ return;
+ }
+ ableTransX!.OffsetX = newCur;
+ UpdateFromCurX(newCur, true);
+ }
+
+ private void UpdateFromCurY(double newCurY, bool invalidate)
+ {
+ curY = newCurY;
+ if (curY < minY) minY = curY;
+ else if (curY > maxY) maxY = curY;
+
+ int step = (int)(1000 * InverseLerp(minY, maxY, curY));
+ TrackY.Value = step;
+ MinBoxY.Text = $"{minY:0.00}";
+ MaxBoxY.Text = $"{maxY:0.00}";
+ ThisValueY.Text = $"{curY:0.00}";
+
+ if (invalidate) refForm.Invalidate(false);
+ }
+ private void UpdateFromSliderY(bool invalidate)
+ {
+ double t = InverseLerp(0, 1000, TrackY.Value);
+ curY = Lerp(minY, maxY, t);
+
+ ThisValueY.Text = $"{curY:0.00}";
+ ableTransY!.OffsetY = curY;
+
+ if (invalidate) refForm.Invalidate(false);
+ }
+ private void UpdateFromMinBoxY()
+ {
+ if (!double.TryParse(MinBoxY.Text, out double newMin))
+ {
+ MinBoxY.Text = $"{minY:0.00}";
+ return;
+ }
+ minY = newMin;
+ MinBoxY.Text = $"{minY:0.00}";
+
+ if (minY > curY)
+ {
+ curY = minY;
+ ThisValueY.Text = $"{curY:0.00}";
+ ableTransY!.OffsetY = curY;
+ }
+
+ int step = (int)(1000 * InverseLerp(minY, maxY, curY));
+ TrackY.Value = step;
+
+ refForm.Invalidate(false);
+ }
+ private void UpdateFromMaxBoxY()
+ {
+ if (!double.TryParse(MaxBoxY.Text, out double newMax))
+ {
+ MaxBoxY.Text = $"{maxY:0.00}";
+ return;
+ }
+
+ maxY = newMax;
+ MaxBoxY.Text = $"{maxY:0.00}";
+
+ if (maxY < curY)
+ {
+ curY = maxY;
+ ThisValueY.Text = $"{curY:0.00}";
+ ableTransY!.OffsetY = curY;
+ }
+
+ int step = (int)(1000 * InverseLerp(minY, maxY, curY));
+ TrackY.Value = step;
+
+ refForm.Invalidate(false);
+ }
+ private void UpdateFromThisBoxY()
+ {
+ if (!double.TryParse(ThisValueY.Text, out double newCur))
+ {
+ ThisValueY.Text = $"{curY:0.00}";
+ return;
+ }
+ ableTransY!.OffsetY = newCur;
+ UpdateFromCurY(newCur, true);
+ }
+
+ private static double Lerp(double a, double b, double t) => a + t * (b - a);
+ private static double InverseLerp(double a, double b, double c) => (c - a) / (b - a);
+
+ private void TrackX_Scroll(object sender, EventArgs e)
+ {
+ UpdateFromSliderX(true);
+ }
+ private void TrackY_Scroll(object sender, EventArgs e)
+ {
+ UpdateFromSliderY(true);
+ }
+}
diff --git a/Base/Forms/TranslateForm.resx b/Base/Forms/TranslateForm.resx
new file mode 100644
index 0000000..af32865
--- /dev/null
+++ b/Base/Forms/TranslateForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Base/Forms/ViewCacheForm.cs b/Base/Forms/ViewCacheForm.cs
index 7f767a9..4fa824b 100644
--- a/Base/Forms/ViewCacheForm.cs
+++ b/Base/Forms/ViewCacheForm.cs
@@ -33,6 +33,7 @@ public partial class ViewCacheForm : Form
foreach (Graphable able in refForm.Graphables)
{
long thisBytes = able.GetCacheBytes();
+ if (thisBytes == 0) continue;
CachePie.Values.Add((able.Color, thisBytes));
totalBytes += thisBytes;
diff --git a/Base/Graphable.cs b/Base/Graphable.cs
index 2844ef7..75f2d8a 100644
--- a/Base/Graphable.cs
+++ b/Base/Graphable.cs
@@ -30,12 +30,12 @@ public abstract class Graphable
public abstract IEnumerable GetItemsToRender(in GraphForm graph);
- public abstract Graphable DeepCopy();
+ public abstract Graphable ShallowCopy();
public virtual void EraseCache() { }
public virtual long GetCacheBytes() => 0;
public virtual void Preload(Float2 xRange, Float2 yRange, double step) { }
public virtual bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor) => false;
- public virtual Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) => default;
+ public virtual IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) => [];
}
diff --git a/Base/Graphables/ColumnTable.cs b/Base/Graphables/ColumnTable.cs
index 0461cc3..da154fe 100644
--- a/Base/Graphables/ColumnTable.cs
+++ b/Base/Graphables/ColumnTable.cs
@@ -2,6 +2,7 @@
using Graphing.Parts;
using System;
using System.Collections.Generic;
+using System.Drawing;
using System.Linq;
namespace Graphing.Graphables;
@@ -23,6 +24,7 @@ public class ColumnTable : Graphable
}
public ColumnTable(double step, Equation equation, double min, double max)
{
+ Color = equation.Color;
Name = $"Column Table for {equation.Name}";
tableXY = [];
@@ -37,7 +39,7 @@ public class ColumnTable : Graphable
public override long GetCacheBytes() => 16 * tableXY.Count;
- public override Graphable DeepCopy() => new ColumnTable(width / 0.75, tableXY.ToArray().ToDictionary());
+ public override Graphable ShallowCopy() => new ColumnTable(width / 0.75, tableXY);
public override IEnumerable GetItemsToRender(in GraphForm graph)
{
@@ -45,12 +47,87 @@ public class ColumnTable : Graphable
foreach (KeyValuePair col in tableXY)
{
items.Add(GraphRectangle.FromSize(new Float2(col.Key, col.Value / 2),
- new Float2(width, col.Value)));
+ new Float2(width, col.Value), 0.625));
}
return items;
}
+ public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
+ {
+ // Get closest value to mouse pos.
+ double closestDist = double.PositiveInfinity, closestX = 0, closestY = 0;
+ foreach (KeyValuePair points in tableXY)
+ {
+ double dist = Math.Abs(points.Key - graphMousePos.x);
+ if (dist < closestDist)
+ {
+ closestDist = dist;
+ closestX = points.Key;
+ closestY = points.Value;
+ }
+ }
+
+ Int2 screenMousePos = graph.GraphSpaceToScreenSpace(graphMousePos);
+ Int2 minBox = graph.GraphSpaceToScreenSpace(new(closestX - width / 2, 0)),
+ maxBox = graph.GraphSpaceToScreenSpace(new(closestX + width / 2, closestY));
+
+ int distX, distY;
+ if (screenMousePos.x < minBox.x) distX = minBox.x - screenMousePos.x; // On left side.
+ else if (screenMousePos.x > maxBox.x) distX = screenMousePos.x - maxBox.x; // On right side.
+ else distX = 0; // Inside.
+
+ if (closestY > 0)
+ {
+ if (screenMousePos.y > minBox.y) distY = screenMousePos.y - minBox.y; // Underneath.
+ else if (screenMousePos.y < maxBox.y) distY = maxBox.y - screenMousePos.y; // Above.
+ else distY = 0; // Inside.
+ }
+ else
+ {
+ if (screenMousePos.y < minBox.y) distY = minBox.y - screenMousePos.y; // Underneath.
+ else if (screenMousePos.y > maxBox.y) distY = screenMousePos.y - maxBox.y; // Above.
+ else distY = 0; // Inside.
+ }
+
+ int totalDist = (int)Math.Sqrt(distX * distX + distY * distY);
+ return totalDist < 50 * factor * graph.DpiFloat / 192;
+ }
+ public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
+ {
+ // Get closest value to mouse pos.
+ double closestDist = double.PositiveInfinity, closestX = 0, closestY = 0;
+ foreach (KeyValuePair points in tableXY)
+ {
+ double dist = Math.Abs(points.Key - graphMousePos.x);
+ if (dist < closestDist)
+ {
+ closestDist = dist;
+ closestX = points.Key;
+ closestY = points.Value;
+ }
+ }
+
+ Float2 textPoint = new(closestX, closestY);
+ Int2 offset;
+ ContentAlignment alignment;
+ if (textPoint.y >= 0)
+ {
+ offset = new(0, -5);
+ alignment = ContentAlignment.BottomCenter;
+ }
+ else
+ {
+ offset = new(0, 5);
+ alignment = ContentAlignment.TopCenter;
+ }
+
+ return
+ [
+ new GraphUiText($"{closestY:0.00}", textPoint, alignment, offsetPix: offset)
+ ];
+ }
+
// Nothing to preload, everything is already cached.
public override void Preload(Float2 xRange, Float2 yRange, double step) { }
}
diff --git a/Base/Graphables/Equation.cs b/Base/Graphables/Equation.cs
index c3dfa4e..e28deee 100644
--- a/Base/Graphables/Equation.cs
+++ b/Base/Graphables/Equation.cs
@@ -3,16 +3,26 @@ using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
+using System.Drawing;
namespace Graphing.Graphables;
-public class Equation : Graphable, IIntegrable, IDerivable
+public class Equation : Graphable, IIntegrable, IDerivable, ITranslatableXY, IConvertSlopeField,
+ IConvertColumnTable
{
private static int equationNum;
+ public bool UngraphWhenConvertedToColumnTable => false;
+ public bool UngraphWhenConvertedToSlopeField => false;
+
+ public double OffsetX { get; set; }
+ public double OffsetY { get; set; }
+
protected readonly EquationDelegate equ;
protected readonly List cache;
+ public event Action OnInvalidate;
+
public Equation(EquationDelegate equ)
{
equationNum++;
@@ -20,6 +30,11 @@ public class Equation : Graphable, IIntegrable, IDerivable
this.equ = equ;
cache = [];
+
+ OffsetX = 0;
+ OffsetY = 0;
+
+ OnInvalidate = delegate { };
}
public override IEnumerable GetItemsToRender(in GraphForm graph)
@@ -46,31 +61,44 @@ public class Equation : Graphable, IIntegrable, IDerivable
previousX = currentX;
previousY = currentY;
}
+ OnInvalidate.Invoke(graph);
return lines;
}
- public Graphable Derive() => new Equation(x =>
+ protected double DerivativeAtPoint(double x)
{
const double step = 1e-3;
- return (equ(x + step) - equ(x)) / step;
- });
+ return (equ(x + step - OffsetX) - equ(x - OffsetX)) / step;
+ }
+
+ public Graphable Derive() => new Equation(DerivativeAtPoint);
public Graphable Integrate() => new IntegralEquation(this);
public EquationDelegate GetDelegate() => equ;
+ public SlopeField ToSlopeField(int detail) => new(detail, (x, y) => DerivativeAtPoint(x))
+ {
+ Color = Color,
+ Name = $"Slope Field of {Name}"
+ };
+ public ColumnTable ToColumnTable(double start, double end, int detail)
+ => new(1.0 / detail, this, start, end);
+
public override void EraseCache() => cache.Clear();
protected double GetFromCache(double x, double epsilon)
{
- (double dist, double nearest, int index) = NearestCachedPoint(x);
- if (dist < epsilon) return nearest;
+ (double dist, double nearest, int index) = NearestCachedPoint(x - OffsetX);
+ if (dist < epsilon) return nearest + OffsetY;
else
{
- double result = equ(x);
- cache.Insert(index + 1, new(x, result));
- return result;
+ double result = equ(x - OffsetX);
+ cache.Insert(index + 1, new(x - OffsetX, result));
+ return result + OffsetY;
}
}
+ public double GetValueAt(double x) => GetFromCache(x, 0);
+
protected (double dist, double y, int index) NearestCachedPoint(double x)
{
if (cache.Count == 0) return (double.PositiveInfinity, double.NaN, -1);
@@ -103,7 +131,7 @@ public class Equation : Graphable, IIntegrable, IDerivable
}
}
- public override Graphable DeepCopy() => new Equation(equ);
+ public override Graphable ShallowCopy() => new Equation(equ);
public override long GetCacheBytes() => cache.Count * 16;
@@ -121,8 +149,15 @@ public class Equation : Graphable, IIntegrable, IDerivable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
- public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) =>
- new(graphMousePos.x, GetFromCache(graphMousePos.x, 1e-3));
+ public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
+ {
+ Float2 point = new(graphMousePos.x, GetFromCache(graphMousePos.x, 1e-3));
+ return
+ [
+ new GraphUiText($"({point.x:0.00}, {point.y:0.00})", point, ContentAlignment.BottomLeft),
+ new GraphUiCircle(point),
+ ];
+ }
public override void Preload(Float2 xRange, Float2 yRange, double step)
{
diff --git a/Base/Graphables/EquationDifference.cs b/Base/Graphables/EquationDifference.cs
new file mode 100644
index 0000000..ebe02c7
--- /dev/null
+++ b/Base/Graphables/EquationDifference.cs
@@ -0,0 +1,96 @@
+using Graphing.Abstract;
+using Graphing.Forms;
+using Graphing.Parts;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+
+namespace Graphing.Graphables;
+
+public class EquationDifference : Graphable, ITranslatableX, IConvertEquation
+{
+ public bool UngraphWhenConvertedToEquation => true;
+
+ public double Position
+ {
+ get => _position;
+ set
+ {
+ _position = value;
+ points = new Float2(equA.GetValueAt(value), equB.GetValueAt(value));
+ }
+ }
+ private double _position;
+
+ public double OffsetX
+ {
+ get => Position;
+ set => Position = value;
+ }
+
+ protected readonly Equation equA, equB;
+ protected Float2 points; // X represents equA.y, Y represents equB.y
+
+ public EquationDifference(double position, Equation equA, Equation equB)
+ {
+ this.equA = equA;
+ this.equB = equB;
+
+ Name = $"Difference between {equA.Name} and {equB.Name}";
+
+ Position = position;
+ }
+
+ public override IEnumerable GetItemsToRender(in GraphForm graph)
+ {
+ Float2 pA = new(Position, points.x),
+ pB = new(Position, points.y);
+ return
+ [
+ new GraphUiCircle(pA),
+ new GraphUiCircle(pB),
+ new GraphLine(pA, pB)
+ ];
+ }
+
+ public double DistanceAtPoint(double x) => equA.GetValueAt(x) - equB.GetValueAt(x);
+
+ public override Graphable ShallowCopy() => new EquationDifference(Position, equA, equB);
+
+ public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
+ {
+ Float2 nearestPoint = new(Position, graphMousePos.y);
+ double upper = double.Max(points.x, points.y),
+ lower = double.Min(points.x, points.y);
+ if (nearestPoint.y > upper) nearestPoint.y = upper;
+ else if (nearestPoint.y < lower) nearestPoint.y = lower;
+
+ Int2 nearestPixelPoint = graph.GraphSpaceToScreenSpace(nearestPoint);
+ Int2 screenMousePos = graph.GraphSpaceToScreenSpace(graphMousePos);
+
+ Int2 diff = new(screenMousePos.x - nearestPixelPoint.x,
+ screenMousePos.y - nearestPixelPoint.y);
+ int dist = (int)Math.Sqrt(diff.x * diff.x + diff.y * diff.y);
+ return dist < 50 * factor * graph.DpiFloat / 192;
+ }
+ public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
+ {
+ Float2 nearestPoint = new(Position, graphMousePos.y);
+ double upper = double.Max(points.x, points.y),
+ lower = double.Min(points.x, points.y);
+ if (nearestPoint.y > upper) nearestPoint.y = upper;
+ else if (nearestPoint.y < lower) nearestPoint.y = lower;
+
+ return
+ [
+ new GraphUiText($"Δ = {points.x - points.y:0.000}", nearestPoint, ContentAlignment.MiddleLeft, offsetPix: new Int2(15, 0)),
+ new GraphUiCircle(nearestPoint)
+ ];
+ }
+
+ public Equation ToEquation() => new(DistanceAtPoint)
+ {
+ Color = Color,
+ Name = Name
+ };
+}
diff --git a/Base/Graphables/IntegralEquation.cs b/Base/Graphables/IntegralEquation.cs
index 51742fd..daf28d5 100644
--- a/Base/Graphables/IntegralEquation.cs
+++ b/Base/Graphables/IntegralEquation.cs
@@ -47,7 +47,7 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
usingAlt = true;
}
- public override Graphable DeepCopy() => new IntegralEquation(this);
+ public override Graphable ShallowCopy() => new IntegralEquation(this);
public override IEnumerable GetItemsToRender(in GraphForm graph)
{
@@ -164,7 +164,7 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
}
else
{
- stepY += baseEquDel!(stepX) * dX;
+ stepY += (baseEquDel!(stepX - baseEqu!.OffsetX) + baseEqu.OffsetY) * dX;
}
}
@@ -178,8 +178,8 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
public Graphable Derive()
{
- if (usingAlt) return altBaseEqu!.DeepCopy();
- else return (Equation)baseEqu!.DeepCopy();
+ if (usingAlt) return altBaseEqu!.ShallowCopy();
+ else return (Equation)baseEqu!.ShallowCopy();
}
public Graphable Integrate() => new IntegralEquation(this);
@@ -234,6 +234,6 @@ public class IntegralEquation : Graphable, IIntegrable, IDerivable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
- public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos) =>
- new(graphMousePos.x, IntegralAtPoint(graphMousePos.x));
+ public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos) =>
+ [new GraphUiCircle(new(graphMousePos.x, IntegralAtPoint(graphMousePos.x)))];
}
diff --git a/Base/Graphables/ParametricEquation.cs b/Base/Graphables/ParametricEquation.cs
new file mode 100644
index 0000000..24f7f78
--- /dev/null
+++ b/Base/Graphables/ParametricEquation.cs
@@ -0,0 +1,134 @@
+using Graphing.Abstract;
+using Graphing.Forms;
+using Graphing.Parts;
+using System;
+using System.Collections.Generic;
+
+namespace Graphing.Graphables;
+
+public class ParametricEquation : Graphable, IDerivable, ITranslatableXY
+{
+ private static int equationNum;
+
+ public double OffsetX { get; set; }
+ public double OffsetY { get; set; }
+
+ public double InitialT { get; set; }
+ public double FinalT { get; set; }
+
+ protected readonly ParametricDelegate equX, equY;
+ protected readonly List<(double t, Float2 point)> cache;
+
+ public ParametricEquation(double initialT, double finalT,
+ ParametricDelegate equX, ParametricDelegate equY)
+ {
+ equationNum++;
+ Name = $"Parametric Equation {equationNum}";
+
+ InitialT = initialT;
+ FinalT = finalT;
+
+ this.equX = equX;
+ this.equY = equY;
+ cache = [];
+ }
+
+ public override Graphable ShallowCopy() => new ParametricEquation(InitialT, FinalT, equX, equY);
+
+ public override IEnumerable GetItemsToRender(in GraphForm graph)
+ {
+ const int step = 10;
+
+ double epsilon = Math.Abs(graph.ScreenSpaceToGraphSpace(new Int2(0, 0)).x
+ - graph.ScreenSpaceToGraphSpace(new Int2(step, 0)).x);
+
+ List lines = [];
+
+ Float2 previousPoint = GetFromCache(InitialT, epsilon);
+ for (double t = InitialT; t <= FinalT; t += epsilon)
+ {
+ Float2 currentPoint = GetFromCache(t, epsilon);
+ if (graph.IsGraphPointVisible(currentPoint) ||
+ graph.IsGraphPointVisible(previousPoint))
+ lines.Add(new GraphLine(previousPoint, currentPoint));
+ previousPoint = currentPoint;
+ }
+
+ return lines;
+ }
+
+ public Graphable Derive() =>
+ new ParametricEquation(InitialT, FinalT, GetDerivativeAtPointX, GetDerivativeAtPointY);
+
+ public ParametricDelegate GetXDelegate() => equX;
+ public ParametricDelegate GetYDelegate() => equY;
+
+ public double GetDerivativeAtPointX(double t)
+ {
+ const double step = 1e-3;
+ return (equX(t + step) - equX(t)) / step;
+ }
+ public double GetDerivativeAtPointY(double t)
+ {
+ const double step = 1e-3;
+ return (equY(t + step) - equY(t)) / step;
+ }
+ public Float2 GetDerivativeAtPoint(double t) =>
+ new(GetDerivativeAtPointX(t), GetDerivativeAtPointY(t));
+
+ public Float2 GetPointAt(double t) => GetFromCache(t, 0);
+
+ public override void EraseCache() => cache.Clear();
+ protected Float2 GetFromCache(double t, double epsilon)
+ {
+ (double dist, Float2 nearest, int index) = NearestCachedPoint(t);
+ if (dist < epsilon) return new(nearest.x + OffsetX, nearest.y + OffsetY);
+ else
+ {
+ Float2 result = new(equX(t), equY(t));
+ cache.Insert(index + 1, (t, result));
+ return new(result.x + OffsetX, result.y + OffsetY);
+ }
+ }
+ public override long GetCacheBytes() => cache.Count * 24;
+
+ protected (double dist, Float2 point, int index) NearestCachedPoint(double t)
+ {
+ if (cache.Count <= 1) return (double.PositiveInfinity, new(double.NaN, double.NaN), -1);
+ else if (cache.Count == 1)
+ {
+ (double resultT, Float2 resultPoint) = cache[0];
+ return (Math.Abs(resultT - t), resultPoint, 0);
+ }
+ else
+ {
+ int boundA = 0, boundB = cache.Count;
+ do
+ {
+ int boundC = (boundA + boundB) / 2;
+
+ (double thisT, Float2 thisPoint) = cache[boundC];
+ if (thisT == t) return (0, thisPoint, boundC);
+ else if (thisT > t)
+ {
+ boundA = boundC;
+ }
+ else // thisT < t
+ {
+ boundB = boundC;
+ }
+
+ } while (boundB - boundA > 1);
+
+ (double resultT, Float2 resultPoint) = cache[boundA];
+ return (Math.Abs(resultT - t), resultPoint, boundA);
+ }
+ }
+
+ public override void Preload(Float2 xRange, Float2 yRange, double step)
+ {
+ for (double t = InitialT; t <= FinalT; t += step) GetFromCache(t, step);
+ }
+}
+
+public delegate double ParametricDelegate(double t);
diff --git a/Base/Graphables/SlopeField.cs b/Base/Graphables/SlopeField.cs
index 066f658..44b877e 100644
--- a/Base/Graphables/SlopeField.cs
+++ b/Base/Graphables/SlopeField.cs
@@ -2,6 +2,7 @@
using Graphing.Parts;
using System;
using System.Collections.Generic;
+using System.Drawing;
namespace Graphing.Graphables;
@@ -9,29 +10,50 @@ public class SlopeField : Graphable
{
private static int slopeFieldNum;
- protected readonly SlopeFieldsDelegate equ;
- protected readonly int detail;
+ public double Detail
+ {
+ get => _detail;
+ set
+ {
+ if (Math.Abs(value - Detail) >= 1e-4)
+ {
+ // When changing detail, we need to regenerate all
+ // the lines. Inefficient, I know. Might be optimized
+ // in a future update.
+ EraseCache();
+ }
+ _detail = value;
+ }
+ }
+ private double _detail;
+ protected readonly SlopeFieldsDelegate equ;
protected readonly List<(Float2, GraphLine)> cache;
- public SlopeField(int detail, SlopeFieldsDelegate equ)
+ public SlopeField(double detail, SlopeFieldsDelegate equ)
{
slopeFieldNum++;
Name = $"Slope Field {slopeFieldNum}";
this.equ = equ;
- this.detail = detail;
+ _detail = detail;
cache = [];
}
public override IEnumerable GetItemsToRender(in GraphForm graph)
{
- double epsilon = 1 / (detail * 2.0);
+ double step = 1 / _detail;
+ double epsilon = step * 0.5;
List lines = [];
- for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1.0 / detail)
+ double minX = Math.Round((graph.MinVisibleGraph.x - 1) / step) * step,
+ maxX = Math.Round((graph.MaxVisibleGraph.x + 1) / step) * step,
+ minY = Math.Round((graph.MinVisibleGraph.y - 1) / step) * step,
+ maxY = Math.Round((graph.MaxVisibleGraph.y + 1) / step) * step;
+
+ for (double x = minX; x < maxX; x += step)
{
- for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1.0 / detail)
+ for (double y = minY; y < maxY; y += step)
{
lines.Add(GetFromCache(epsilon, x, y));
}
@@ -42,7 +64,7 @@ public class SlopeField : Graphable
protected GraphLine MakeSlopeLine(Float2 position, double slope)
{
- double size = detail;
+ double size = _detail;
double dirX = size, dirY = slope * size;
double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY);
@@ -72,17 +94,17 @@ public class SlopeField : Graphable
return result;
}
- public override Graphable DeepCopy() => new SlopeField(detail, equ);
+ public override Graphable ShallowCopy() => new SlopeField(_detail, equ);
public override void EraseCache() => cache.Clear();
public override long GetCacheBytes() => cache.Count * 48;
public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
{
- Float2 nearestPos = new(Math.Round(graphMousePos.x * detail) / detail,
- Math.Round(graphMousePos.y * detail) / detail);
+ Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail,
+ Math.Round(graphMousePos.y * _detail) / _detail);
- double epsilon = 1 / (detail * 2.0);
+ double epsilon = 1 / (_detail * 2.0);
GraphLine line = GetFromCache(epsilon, nearestPos.x, nearestPos.y);
double slope = (line.b.y - line.a.y) / (line.b.x - line.a.x);
@@ -101,12 +123,12 @@ public class SlopeField : Graphable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
- public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos)
+ public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
{
- Float2 nearestPos = new(Math.Round(graphMousePos.x * detail) / detail,
- Math.Round(graphMousePos.y * detail) / detail);
+ Float2 nearestPos = new(Math.Round(graphMousePos.x * _detail) / _detail,
+ Math.Round(graphMousePos.y * _detail) / _detail);
- double epsilon = 1 / (detail * 2.0);
+ double epsilon = 1 / (_detail * 2.0);
GraphLine line = GetFromCache(epsilon, nearestPos.x, nearestPos.y);
double slope = (line.b.y - line.a.y) / (line.b.x - line.a.x);
@@ -114,14 +136,18 @@ public class SlopeField : Graphable
lineY = slope * (lineX - nearestPos.x) + nearestPos.y;
Float2 point = new(lineX, lineY);
- return point;
+ return
+ [
+ new GraphUiText($"M = {slope:0.000}", point, ContentAlignment.BottomLeft),
+ new GraphUiCircle(point)
+ ];
}
public override void Preload(Float2 xRange, Float2 yRange, double step)
{
- for (double x = Math.Ceiling(xRange.x - 1); x < xRange.y + 1; x += 1.0 / detail)
+ for (double x = Math.Ceiling(xRange.x - 1); x < xRange.y + 1; x += 1.0 / _detail)
{
- for (double y = Math.Ceiling(yRange.x - 1); y < yRange.y + 1; y += 1.0 / detail)
+ for (double y = Math.Ceiling(yRange.x - 1); y < yRange.y + 1; y += 1.0 / _detail)
{
GetFromCache(step, x, y);
}
diff --git a/Base/Graphables/TangentLine.cs b/Base/Graphables/TangentLine.cs
index f22eec0..10faab3 100644
--- a/Base/Graphables/TangentLine.cs
+++ b/Base/Graphables/TangentLine.cs
@@ -1,12 +1,16 @@
-using Graphing.Forms;
+using Graphing.Abstract;
+using Graphing.Forms;
using Graphing.Parts;
using System;
using System.Collections.Generic;
+using System.Drawing;
namespace Graphing.Graphables;
-public class TangentLine : Graphable
+public class TangentLine : Graphable, IConvertEquation, ITranslatableX
{
+ public bool UngraphWhenConvertedToEquation => true;
+
public double Position
{
get => _position;
@@ -18,8 +22,13 @@ public class TangentLine : Graphable
}
private double _position; // Private because it has exactly the same functionality as `Position`.
+ public double OffsetX
+ {
+ get => Position;
+ set => Position = value;
+ }
+
protected readonly Equation parent;
- protected readonly EquationDelegate parentEqu;
protected readonly double length;
@@ -35,16 +44,26 @@ public class TangentLine : Graphable
Name = $"Tangent Line of {parent.Name}";
slopeCache = [];
- parentEqu = parent.GetDelegate();
- Position = position;
this.length = length;
this.parent = parent;
+ Position = position;
+
+ parent.OnInvalidate += (graph) =>
+ {
+ // I don't love this but it works.
+ EraseCache();
+ Position = _position; // Done for side effects.
+ };
}
public override IEnumerable GetItemsToRender(in GraphForm graph)
{
Float2 point = new(Position, currentSlope.y);
- return [MakeSlopeLine(), new GraphUiCircle(point, 8)];
+ return
+ [
+ MakeSlopeLine(),
+ new GraphUiCircle(point)
+ ];
}
protected GraphLine MakeSlopeLine()
{
@@ -63,13 +82,13 @@ public class TangentLine : Graphable
const double step = 1e-3;
- double initial = parentEqu(x);
- Float2 result = new((parentEqu(x + step) - initial) / step, initial);
+ double initial = parent.GetValueAt(x);
+ Float2 result = new((parent.GetValueAt(x + step) - initial) / step, initial);
slopeCache.Add(x, result);
return result;
}
- public override Graphable DeepCopy() => new TangentLine(length, Position, parent);
+ public override Graphable ShallowCopy() => new TangentLine(length, Position, parent);
public override void EraseCache() => slopeCache.Clear();
public override long GetCacheBytes() => slopeCache.Count * 24;
@@ -93,7 +112,7 @@ public class TangentLine : Graphable
double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
return totalDist <= allowedDist;
}
- public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos)
+ public override IEnumerable GetSelectionItemsToRender(in GraphForm graph, Float2 graphMousePos)
{
GraphLine line = MakeSlopeLine();
@@ -101,7 +120,15 @@ public class TangentLine : Graphable
Math.Min(line.a.x, line.b.x),
Math.Max(line.a.x, line.b.x)),
lineY = currentSlope.x * (lineX - Position) + currentSlope.y;
- return new Float2(lineX, lineY);
+
+ double slope = currentSlope.x;
+ Float2 point = new(lineX, lineY);
+
+ return
+ [
+ new GraphUiText($"M = {slope:0.000}", point, ContentAlignment.BottomLeft),
+ new GraphUiCircle(new(lineX, lineY))
+ ];
}
public override void Preload(Float2 xRange, Float2 yRange, double step)
@@ -112,4 +139,14 @@ public class TangentLine : Graphable
// that can be changed.
for (double x = xRange.x; x <= xRange.y; x += step) DerivativeAtPoint(x);
}
+
+ public Equation ToEquation()
+ {
+ double slope = currentSlope.x, x1 = Position, y1 = currentSlope.y;
+ return new(x => slope * (x - x1) + y1)
+ {
+ Name = Name,
+ Color = Color
+ };
+ }
}
diff --git a/Base/Parts/GraphRectangle.cs b/Base/Parts/GraphRectangle.cs
index 4881397..1d4cac6 100644
--- a/Base/Parts/GraphRectangle.cs
+++ b/Base/Parts/GraphRectangle.cs
@@ -6,24 +6,28 @@ namespace Graphing.Parts;
public record struct GraphRectangle : IGraphPart
{
public Float2 min, max;
+ public double opacity;
public GraphRectangle()
{
min = new();
max = new();
+ opacity = 1;
}
- public static GraphRectangle FromSize(Float2 center, Float2 size) => new()
+ public static GraphRectangle FromSize(Float2 center, Float2 size, double opacity = 1) => new()
{
min = new(center.x - size.x / 2,
center.y - size.y / 2),
max = new(center.x + size.x / 2,
- center.y + size.y / 2)
+ center.y + size.y / 2),
+ opacity = opacity,
};
- public static GraphRectangle FromRange(Float2 min, Float2 max) => new()
+ public static GraphRectangle FromRange(Float2 min, Float2 max, double opacity = 1) => new()
{
min = min,
- max = max
+ max = max,
+ opacity = opacity,
};
public void Render(in GraphForm form, in Graphics g, in Pen pen)
@@ -41,6 +45,9 @@ public record struct GraphRectangle : IGraphPart
start.y - end.y);
if (size.x == 0 || size.y == 0) return;
+ Color initialColor = pen.Color;
+ pen.Color = Color.FromArgb((int)(opacity * 255), pen.Color);
g.FillRectangle(pen.Brush, new Rectangle(start.x, end.y, size.x, size.y));
+ pen.Color = initialColor;
}
}
diff --git a/Base/Parts/GraphUiCircle.cs b/Base/Parts/GraphUiCircle.cs
index 7f46411..8221a46 100644
--- a/Base/Parts/GraphUiCircle.cs
+++ b/Base/Parts/GraphUiCircle.cs
@@ -13,7 +13,7 @@ public record struct GraphUiCircle : IGraphPart
center = new();
radius = 1;
}
- public GraphUiCircle(Float2 center, int radius)
+ public GraphUiCircle(Float2 center, int radius = 8)
{
this.center = center;
this.radius = radius;
diff --git a/Base/Parts/GraphUiText.cs b/Base/Parts/GraphUiText.cs
new file mode 100644
index 0000000..420e006
--- /dev/null
+++ b/Base/Parts/GraphUiText.cs
@@ -0,0 +1,87 @@
+using Graphing.Forms;
+using System.Drawing;
+
+namespace Graphing.Parts;
+
+public record struct GraphUiText : IGraphPart
+{
+ public string text;
+ public Float2 position;
+ public bool background;
+
+ public ContentAlignment alignment;
+ public Int2 offsetPix;
+
+ private readonly Font font;
+ private readonly Brush? backgroundBrush;
+
+ public GraphUiText(string text, Float2 position, ContentAlignment alignment,
+ bool background = true, Int2? offsetPix = null)
+ {
+ font = new Font("Segoe UI", 8, FontStyle.Bold);
+
+ this.text = text;
+ this.position = position;
+ this.background = background;
+ this.alignment = alignment;
+ this.offsetPix = offsetPix ?? new();
+
+ if (background) backgroundBrush = new SolidBrush(GraphForm.BackgroundColor);
+ }
+
+ public readonly void Render(in GraphForm form, in Graphics g, in Pen p)
+ {
+ Int2 posScreen = form.GraphSpaceToScreenSpace(position);
+ SizeF size = g.MeasureString(text, font);
+
+ // Adjust X position based on alignment.
+ switch (alignment)
+ {
+ case ContentAlignment.TopLeft or
+ ContentAlignment.MiddleLeft or
+ ContentAlignment.BottomLeft: break; // Nothing to offset.
+
+ case ContentAlignment.TopCenter or
+ ContentAlignment.MiddleCenter or
+ ContentAlignment.BottomCenter:
+ posScreen.x -= (int)(size.Width / 2);
+ break;
+
+ case ContentAlignment.TopRight or
+ ContentAlignment.MiddleRight or
+ ContentAlignment.BottomRight:
+ posScreen.x -= (int)size.Width;
+ break;
+ }
+
+ // Adjust Y position based on alignment.
+ switch (alignment)
+ {
+ case ContentAlignment.TopLeft or
+ ContentAlignment.TopCenter or
+ ContentAlignment.TopRight: break; // Nothing to offset.
+
+ case ContentAlignment.MiddleLeft or
+ ContentAlignment.MiddleCenter or
+ ContentAlignment.MiddleRight:
+ posScreen.y -= (int)(size.Height / 2);
+ break;
+
+ case ContentAlignment.BottomLeft or
+ ContentAlignment.BottomCenter or
+ ContentAlignment.BottomRight:
+ posScreen.y -= (int)size.Height;
+ break;
+ }
+
+ posScreen.x += (int)(offsetPix.x * form.DpiFloat / 192);
+ posScreen.y += (int)(offsetPix.y * form.DpiFloat / 192);
+
+ if (background)
+ {
+ g.FillRectangle(backgroundBrush!, new Rectangle(posScreen.x, posScreen.y,
+ (int)size.Width, (int)size.Height));
+ }
+ g.DrawString(text, font, p.Brush, new Point(posScreen.x, posScreen.y));
+ }
+}
diff --git a/Base/Properties/PublishProfiles/FolderProfile.pubxml.user b/Base/Properties/PublishProfiles/FolderProfile.pubxml.user
index 706348e..4da2d5e 100644
--- a/Base/Properties/PublishProfiles/FolderProfile.pubxml.user
+++ b/Base/Properties/PublishProfiles/FolderProfile.pubxml.user
@@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
- True|2024-03-20T12:39:01.6402921Z;True|2024-03-13T10:31:43.4569441-04:00;False|2024-03-13T10:30:01.4347009-04:00;False|2024-03-13T10:27:31.9554551-04:00;
+ True|2024-03-20T12:48:45.8740885Z;True|2024-03-20T08:48:35.6948867-04:00;True|2024-03-20T08:39:01.6402921-04:00;True|2024-03-13T10:31:43.4569441-04:00;False|2024-03-13T10:30:01.4347009-04:00;False|2024-03-13T10:27:31.9554551-04:00;
\ No newline at end of file
diff --git a/README.md b/README.md
index ac7ed2b..dcac875 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,9 @@
This is a graphing calculator I made initially for a Calculus project in a day or so. I've written a basic rendering system in Windows Forms that runs on .NET 8.0.
Currently, it doesn't have a whole lot of features, but I'll be adding more in the future. Here's currently what it can do:
-- Graph an equation (duh).
+- Graph standard equations (duh).
- There are currently some rendering issues with asymptotes which will be focused on at some point.
+- Graph parametric equations.
- Integrate and derive equations.
- Graph a slope field of a `dy/dx =` style equation.
- View a tangent line of an equation.
diff --git a/Testing/Program.cs b/Testing/Program.cs
index 851e7f8..35082d7 100644
--- a/Testing/Program.cs
+++ b/Testing/Program.cs
@@ -13,18 +13,15 @@ internal static class Program
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
-
+
GraphForm graph = new("One Of The Graphing Calculators Of All Time");
- Equation equ = new(Math.Sin);
- SlopeField sf = new(2, (x, y) => Math.Cos(x));
- TangentLine tl = new(2, 2, equ);
- graph.Graph(equ, sf, tl);
-
- // Now, when integrating equations, the result is much less jagged
- // and much faster. Try it out! You can also select points along
- // equations and such as well. Click on an equation to see for
- // yourself!
+ Equation equA = new(Math.Sin),
+ equB = new(Math.Cos);
+ EquationDifference diff = new(2, equA, equB);
+ ParametricEquation equC = new(0, 20, t => 0.0375 * t * Math.Cos(t), t => 0.0625 * t * Math.Sin(t) + 3);
+ TangentLine tanA = new(2, 2, equA);
+ graph.Graph(equA, equB, diff, equC, equB.ToColumnTable(-3, 3, 2), tanA);
Application.Run(graph);
}