diff --git a/.gitignore b/.gitignore
index 9b828e4..9b96ea0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.vs/
+.github/
Base/obj/
Base/bin/
diff --git a/Base/Abstract/IDerivable.cs b/Base/Abstract/IDerivable.cs
new file mode 100644
index 0000000..56571c4
--- /dev/null
+++ b/Base/Abstract/IDerivable.cs
@@ -0,0 +1,8 @@
+using Graphing.Graphables;
+
+namespace Graphing.Abstract;
+
+public interface IDerivable
+{
+ public Graphable Derive();
+}
diff --git a/Base/Abstract/IIntegrable.cs b/Base/Abstract/IIntegrable.cs
new file mode 100644
index 0000000..3e3dd52
--- /dev/null
+++ b/Base/Abstract/IIntegrable.cs
@@ -0,0 +1,8 @@
+using Graphing.Graphables;
+
+namespace Graphing.Abstract;
+
+public interface IIntegrable
+{
+ public Graphable Integrate();
+}
diff --git a/Base/Base.csproj b/Base/Base.csproj
index a26f79f..2301302 100644
--- a/Base/Base.csproj
+++ b/Base/Base.csproj
@@ -5,25 +5,25 @@
net8.0-windows
enable
true
- enable
+ disable
Graphing
ThatOneNerd.Graphing
True
True
ThatOneNerd.Graphing
ThatOneNerd.Graphing
- 1.1.0
+ 1.2.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
+ graphing;graph;plot;math;calculus;visual;desmos;slope field;slopefield;equation;visualizer
MIT
True
snupkg
View the GitHub release for the changelog:
-https://github.com/That-One-Nerd/Graphing/releases/tag/1.1.0
+https://github.com/That-One-Nerd/Graphing/releases/tag/1.2.0
diff --git a/Base/Base.csproj.user b/Base/Base.csproj.user
index 4bb9e32..ef577eb 100644
--- a/Base/Base.csproj.user
+++ b/Base/Base.csproj.user
@@ -20,4 +20,9 @@
Form
+
+
+ Designer
+
+
\ No newline at end of file
diff --git a/Base/Float2.cs b/Base/Float2.cs
index 07005bc..cabb225 100644
--- a/Base/Float2.cs
+++ b/Base/Float2.cs
@@ -1,4 +1,6 @@
-namespace Graphing;
+using System.Drawing;
+
+namespace Graphing;
public record struct Float2
{
diff --git a/Base/Forms/Controls/PieChart.Designer.cs b/Base/Forms/Controls/PieChart.Designer.cs
index 18b52e0..ccf2086 100644
--- a/Base/Forms/Controls/PieChart.Designer.cs
+++ b/Base/Forms/Controls/PieChart.Designer.cs
@@ -1,4 +1,6 @@
-namespace Graphing.Forms.Controls
+using System.Drawing;
+
+namespace Graphing.Forms.Controls
{
partial class PieChart
{
@@ -33,7 +35,7 @@
// PieChart
//
AutoScaleDimensions = new SizeF(13F, 32F);
- AutoScaleMode = AutoScaleMode.Font;
+ AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
Name = "PieChart";
Size = new Size(500, 500);
ResumeLayout(false);
diff --git a/Base/Forms/Controls/PieChart.cs b/Base/Forms/Controls/PieChart.cs
index 19d0e71..3245320 100644
--- a/Base/Forms/Controls/PieChart.cs
+++ b/Base/Forms/Controls/PieChart.cs
@@ -1,4 +1,8 @@
-using System.Drawing.Drawing2D;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Windows.Forms;
namespace Graphing.Forms.Controls;
@@ -6,12 +10,18 @@ public partial class PieChart : UserControl
{
public List<(Color, double)> Values { get; set; }
+ public float DpiFloat { get; private set; }
+
public PieChart()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);
+ Graphics tempG = CreateGraphics();
+ DpiFloat = (tempG.DpiX + tempG.DpiY) / 2;
+ tempG.Dispose();
+
Values = [];
InitializeComponent();
}
@@ -40,20 +50,25 @@ public partial class PieChart : UserControl
current += item.value;
}
- // Draw the outline.
- Pen outlinePartsPen = new(Color.FromArgb(unchecked((int)0xFF_202020)), 3);
- current = 0;
- foreach ((Color, double value) item in Values)
+ // Draw the outline of each slice.
+ // Only done if there is more than one slice.
+ if (Values.Count > 1)
{
- double start = 360 * current / sum,
- end = 360 * (current + item.value) / sum;
- g.DrawPie(outlinePartsPen, rect, (float)start, (float)(end - start));
+ Pen outlinePartsPen = new(Color.FromArgb(unchecked((int)0xFF_202020)), DpiFloat * 3 / 192);
+ current = 0;
+ foreach ((Color, double value) item in Values)
+ {
+ double start = 360 * current / sum,
+ end = 360 * (current + item.value) / sum;
+ if (item.value > 0)
+ g.DrawPie(outlinePartsPen, rect, (float)start, (float)(end - start));
- current += item.value;
+ current += item.value;
+ }
}
// Outline
- Pen outlinePen = new(Color.FromArgb(unchecked((int)0xFF_202020)), 5);
+ Pen outlinePen = new(Color.FromArgb(unchecked((int)0xFF_202020)), DpiFloat * 5 / 192);
g.DrawEllipse(outlinePen, rect);
}
}
diff --git a/Base/Forms/GraphColorPickerForm.Designer.cs b/Base/Forms/GraphColorPickerForm.Designer.cs
index f50ae6c..4bca829 100644
--- a/Base/Forms/GraphColorPickerForm.Designer.cs
+++ b/Base/Forms/GraphColorPickerForm.Designer.cs
@@ -1,4 +1,7 @@
-namespace Graphing.Forms
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Graphing.Forms
{
partial class GraphColorPickerForm
{
@@ -40,7 +43,7 @@
ResultView = new Panel();
BottomPanel = new Panel();
OkButton = new Button();
- CancelButton = new Button();
+ CancellingButton = new Button();
RgbSliders.SuspendLayout();
((System.ComponentModel.ISupportInitialize)BlueTrackBar).BeginInit();
((System.ComponentModel.ISupportInitialize)RedTrackBar).BeginInit();
@@ -169,7 +172,7 @@
//
BottomPanel.BackColor = SystemColors.Window;
BottomPanel.Controls.Add(OkButton);
- BottomPanel.Controls.Add(CancelButton);
+ BottomPanel.Controls.Add(CancellingButton);
BottomPanel.Dock = DockStyle.Bottom;
BottomPanel.Location = new Point(0, 517);
BottomPanel.Margin = new Padding(0);
@@ -191,15 +194,15 @@
//
// CancelButton
//
- CancelButton.Anchor = AnchorStyles.Right;
- CancelButton.Location = new Point(384, 9);
- CancelButton.Margin = new Padding(0);
- CancelButton.Name = "CancelButton";
- CancelButton.Size = new Size(150, 46);
- CancelButton.TabIndex = 0;
- CancelButton.Text = "Cancel";
- CancelButton.UseVisualStyleBackColor = true;
- CancelButton.Click += CancelButton_Click;
+ CancellingButton.Anchor = AnchorStyles.Right;
+ CancellingButton.Location = new Point(384, 9);
+ CancellingButton.Margin = new Padding(0);
+ CancellingButton.Name = "CancelButton";
+ CancellingButton.Size = new Size(150, 46);
+ CancellingButton.TabIndex = 0;
+ CancellingButton.Text = "Cancel";
+ CancellingButton.UseVisualStyleBackColor = true;
+ CancellingButton.Click += CancelButton_Click;
//
// GraphColorPickerForm
//
@@ -234,7 +237,7 @@
private TrackBar BlueTrackBar;
private TrackBar RedTrackBar;
private Panel BottomPanel;
- private Button CancelButton;
+ private Button CancellingButton;
private Button OkButton;
private TextBox RedValueBox;
private TextBox BlueValueBox;
diff --git a/Base/Forms/GraphColorPickerForm.cs b/Base/Forms/GraphColorPickerForm.cs
index b1ec674..42afe02 100644
--- a/Base/Forms/GraphColorPickerForm.cs
+++ b/Base/Forms/GraphColorPickerForm.cs
@@ -1,4 +1,8 @@
-namespace Graphing.Forms;
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Graphing.Forms;
public partial class GraphColorPickerForm : Form
{
@@ -37,7 +41,7 @@ public partial class GraphColorPickerForm : Form
MessageLabel.Text = $"Pick a color for {able.Name}.";
// Add preset buttons.
- const int size = 48;
+ int size = (int)(graph.DpiFloat * 48 / 192);
int position = 0;
foreach (uint cId in Graphable.DefaultColors)
{
diff --git a/Base/Forms/GraphForm.Designer.cs b/Base/Forms/GraphForm.Designer.cs
index 15c0cfe..f30584b 100644
--- a/Base/Forms/GraphForm.Designer.cs
+++ b/Base/Forms/GraphForm.Designer.cs
@@ -1,4 +1,7 @@
-namespace Graphing.Forms
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Graphing.Forms
{
partial class GraphForm
{
@@ -41,6 +44,7 @@
MenuEquationsIntegral = new ToolStripMenuItem();
MenuMisc = new ToolStripMenuItem();
MenuMiscCaches = new ToolStripMenuItem();
+ MiscMenuPreload = new ToolStripMenuItem();
GraphMenu.SuspendLayout();
SuspendLayout();
//
@@ -129,7 +133,7 @@
//
// MenuMisc
//
- MenuMisc.DropDownItems.AddRange(new ToolStripItem[] { MenuMiscCaches });
+ MenuMisc.DropDownItems.AddRange(new ToolStripItem[] { MenuMiscCaches, MiscMenuPreload });
MenuMisc.Name = "MenuMisc";
MenuMisc.Size = new Size(83, 38);
MenuMisc.Text = "Misc";
@@ -141,6 +145,13 @@
MenuMiscCaches.Text = "View Caches";
MenuMiscCaches.Click += MenuMiscCaches_Click;
//
+ // MiscMenuPreload
+ //
+ MiscMenuPreload.Name = "MiscMenuPreload";
+ MiscMenuPreload.Size = new Size(359, 44);
+ MiscMenuPreload.Text = "Preload Cache";
+ MiscMenuPreload.Click += MiscMenuPreload_Click;
+ //
// GraphForm
//
AutoScaleDimensions = new SizeF(13F, 32F);
@@ -172,5 +183,6 @@
private ToolStripMenuItem MenuEquationsIntegral;
private ToolStripMenuItem MenuMisc;
private ToolStripMenuItem MenuMiscCaches;
+ private ToolStripMenuItem MiscMenuPreload;
}
}
\ No newline at end of file
diff --git a/Base/Forms/GraphForm.cs b/Base/Forms/GraphForm.cs
index 5991244..152d852 100644
--- a/Base/Forms/GraphForm.cs
+++ b/Base/Forms/GraphForm.cs
@@ -1,7 +1,11 @@
-using Graphing.Extensions;
-using Graphing.Graphables;
+using Graphing.Abstract;
+using Graphing.Parts;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
using System.Drawing.Drawing2D;
-using System.Text;
+using System.Linq;
+using System.Windows.Forms;
namespace Graphing.Forms;
@@ -10,10 +14,13 @@ public partial class GraphForm : Form
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 UnitsTextColor = Color.Black;
public Float2 ScreenCenter { get; private set; }
public Float2 Dpi { get; private set; }
+ public float DpiFloat { get; private set; }
+
public double ZoomLevel
{
get => _zoomLevel;
@@ -21,7 +28,7 @@ public partial class GraphForm : Form
{
double oldZoom = ZoomLevel;
- _zoomLevel = Math.Clamp(value, 1e-2, 1e3);
+ _zoomLevel = Math.Clamp(value, 1e-5, 1e3);
int totalSegments = 0;
foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count();
@@ -57,6 +64,9 @@ public partial class GraphForm : Form
Graphics tempG = CreateGraphics();
Dpi = new(tempG.DpiX, tempG.DpiY);
tempG.Dispose();
+
+ DpiFloat = (float)((Dpi.x + Dpi.y) / 2);
+
ables = [];
ZoomLevel = 1;
initialWindowPos = Location;
@@ -102,7 +112,7 @@ public partial class GraphForm : Form
// Draw horizontal/vertical quarter-axis.
Brush quarterBrush = new SolidBrush(QuarterAxisColor);
- Pen quarterPen = new(quarterBrush, 2);
+ 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)
{
@@ -119,7 +129,7 @@ public partial class GraphForm : Form
// Draw horizontal/vertical semi-axis.
Brush semiBrush = new SolidBrush(SemiAxisColor);
- Pen semiPen = new(semiBrush, 2);
+ 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)
{
@@ -135,7 +145,7 @@ public partial class GraphForm : Form
}
Brush mainLineBrush = new SolidBrush(MainAxisColor);
- Pen mainLinePen = new(mainLineBrush, 3);
+ Pen mainLinePen = new(mainLineBrush, DpiFloat * 3 / 192);
// Draw the main axis (on top of the semi axis).
Int2 startCenterY = GraphSpaceToScreenSpace(new Float2(0, MinVisibleGraph.y)),
@@ -146,6 +156,44 @@ public partial class GraphForm : Form
g.DrawLine(mainLinePen, startCenterX, endCenterX);
g.DrawLine(mainLinePen, startCenterY, endCenterY);
}
+ protected virtual void PaintUnits(Graphics g)
+ {
+ double axisScale = Math.Pow(2, Math.Round(Math.Log(ZoomLevel, 2)));
+ 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)
+ {
+ if (x == 0) x = 0; // Fixes -0
+
+ Int2 screenPos = GraphSpaceToScreenSpace(new Float2(x, 0));
+
+ if (screenPos.y < minX) screenPos.y = minX;
+ else if (screenPos.y > maxX) screenPos.y = maxX;
+
+ g.DrawString($"{x}", textFont, textBrush, screenPos.x, screenPos.y);
+ }
+
+ // Y-axis
+ int minY = (int)(DpiFloat * 10 / 192);
+ for (double y = Math.Ceiling(MinVisibleGraph.y / axisScale) * axisScale; y <= MaxVisibleGraph.y; y += axisScale)
+ {
+ if (y == 0) continue;
+
+ Int2 screenPos = GraphSpaceToScreenSpace(new Float2(0, y));
+
+ string result = y.ToString();
+ int maxY = ClientRectangle.Width - (int)(DpiFloat * (textFont.Height * result.Length * 0.40 + 15) / 192);
+
+ if (screenPos.x < minY) screenPos.x = minY;
+ else if (screenPos.x > maxY) screenPos.x = maxY;
+
+ g.DrawString($"{y}", textFont, textBrush, screenPos.x, screenPos.y);
+ }
+ }
protected override void OnPaint(PaintEventArgs e)
{
@@ -156,13 +204,47 @@ public partial class GraphForm : Form
g.FillRectangle(background, e.ClipRectangle);
PaintGrid(g);
+ PaintUnits(g);
+
+ Point clientMousePos = PointToClient(Cursor.Position);
+ Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
+ clientMousePos.Y));
// Draw the actual graphs.
+ Pen[] graphPens = new Pen[ables.Count];
for (int i = 0; i < ables.Count; i++)
{
IEnumerable lines = ables[i].GetItemsToRender(this);
Brush graphBrush = new SolidBrush(ables[i].Color);
- foreach (IGraphPart gp in lines) gp.Render(this, g, graphBrush);
+ Pen graphPen = new(graphBrush, DpiFloat * 3 / 192);
+ graphPens[i] = graphPen;
+ foreach (IGraphPart gp in lines) gp.Render(this, g, graphPen);
+ }
+
+ // Equation selection detection.
+ // This system lets you select multiple graphs, and that's cool by me.
+ if (ableDrag)
+ {
+ 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]);
+ }
+ }
}
base.OnPaint(e);
@@ -183,11 +265,28 @@ public partial class GraphForm : Form
private bool mouseDrag = false;
private Int2 initialMouseLocation;
private Float2 initialScreenCenter;
+
+ private bool ableDrag = false;
protected override void OnMouseDown(MouseEventArgs e)
{
- mouseDrag = true;
- initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y);
- initialScreenCenter = ScreenCenter;
+ if (!mouseDrag)
+ {
+ 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 (ableDrag) Invalidate(false);
+ }
+
+ if (!ableDrag)
+ {
+ mouseDrag = true;
+ initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y);
+ initialScreenCenter = ScreenCenter;
+ }
}
protected override void OnMouseUp(MouseEventArgs e)
{
@@ -198,9 +297,10 @@ public partial class GraphForm : Form
Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y);
ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y);
- Invalidate(false);
}
mouseDrag = false;
+ ableDrag = false;
+ Invalidate(false);
}
protected override void OnMouseMove(MouseEventArgs e)
{
@@ -213,6 +313,7 @@ public partial class GraphForm : Form
initialScreenCenter.y + graphDiff.y);
Invalidate(false);
}
+ else if (ableDrag) Invalidate(false);
}
protected override void OnMouseWheel(MouseEventArgs e)
{
@@ -226,7 +327,6 @@ public partial class GraphForm : Form
ZoomLevel = 1;
Invalidate(false);
}
-
private void GraphColorPickerButton_Click(Graphable able)
{
GraphColorPickerForm picker = new(this, able)
@@ -235,6 +335,13 @@ public partial class GraphForm : Form
};
picker.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2);
+
+ if (picker.Location.X + picker.Width > Screen.FromControl(this).WorkingArea.Width)
+ {
+ picker.StartPosition = FormStartPosition.WindowsDefaultLocation;
+ }
+
+ picker.TopMost = true;
picker.ShowDialog();
RegenerateMenuItems();
}
@@ -255,22 +362,24 @@ public partial class GraphForm : Form
colorItem.Click += (o, e) => GraphColorPickerButton_Click(able);
MenuColors.DropDownItems.Add(colorItem);
- if (able is Equation equ)
+ if (able is IDerivable derivable)
{
ToolStripMenuItem derivativeItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
- derivativeItem.Click += (o, e) => EquationComputeDerivative_Click(equ);
+ derivativeItem.Click += (o, e) => Graph(derivable.Derive());
MenuEquationsDerivative.DropDownItems.Add(derivativeItem);
-
+ }
+ if (able is IIntegrable integrable)
+ {
ToolStripMenuItem integralItem = new()
{
ForeColor = able.Color,
Text = able.Name
};
- integralItem.Click += (o, e) => EquationComputeIntegral_Click(equ);
+ integralItem.Click += (o, e) => Graph(integrable.Integrate());
MenuEquationsIntegral.DropDownItems.Add(integralItem);
}
}
@@ -286,19 +395,16 @@ public partial class GraphForm : Form
Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2);
picker.ShowDialog();
}
-
private void ButtonViewportSetCenter_Click(object? sender, EventArgs e)
{
MessageBox.Show("TODO", "Set Center Position", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
-
private void ButtonViewportReset_Click(object? sender, EventArgs e)
{
ScreenCenter = new Float2(0, 0);
ZoomLevel = 1;
Invalidate(false);
}
-
private void ButtonViewportResetWindow_Click(object? sender, EventArgs e)
{
Location = initialWindowPos;
@@ -306,63 +412,6 @@ public partial class GraphForm : Form
WindowState = FormWindowState.Normal;
}
- private void EquationComputeDerivative_Click(Equation equation)
- {
- EquationDelegate equ = equation.GetDelegate();
- string oldName = equation.Name, newName;
- if (oldName.StartsWith("Derivative of ")) newName = "Second Derivative of " + oldName[14..];
- else if (oldName.StartsWith("Second Derivative of ")) newName = "Third Derivative of " + oldName[21..];
- else newName = "Derivative of " + oldName;
- // TODO: anti-integrate (maybe).
-
- Graph(new Equation(DerivativeAtPoint(equ))
- {
- Name = newName
- });
-
- static EquationDelegate DerivativeAtPoint(EquationDelegate e)
- {
- const double step = 1e-3;
- return x => (e(x + step) - e(x)) / step;
- }
- }
-
- private void EquationComputeIntegral_Click(Equation equation)
- {
- EquationDelegate equ = equation.GetDelegate();
- string oldName = equation.Name, newName;
- if (oldName.StartsWith("Integral of ")) newName = "Second Integral of " + oldName[12..];
- else if (oldName.StartsWith("Second Integral of ")) newName = "Third Integral of " + oldName[19..];
- else newName = "Integral of " + oldName;
- // TODO: anti-derive (maybe)
-
- Graph(new Equation(x => Integrate(equ, 0, x))
- {
- Name = newName
- });
-
- static double Integrate(EquationDelegate e, double lower, double upper)
- {
- // TODO: a better rendering method could make this much faster.
- const double step = 1e-2;
-
- double factor = 1;
- if (upper < lower)
- {
- factor = -1;
- (lower, upper) = (upper, lower);
- }
-
- double sum = 0;
- for (double x = lower; x <= upper; x += step)
- {
- sum += e(x) * step;
- }
-
- return sum * factor;
- }
- }
-
private void MenuMiscCaches_Click(object? sender, EventArgs e)
{
ViewCacheForm cacheForm = new(this)
@@ -372,6 +421,29 @@ public partial class GraphForm : Form
cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2);
+
+ if (cacheForm.Location.X + cacheForm.Width > Screen.FromControl(this).WorkingArea.Width)
+ {
+ cacheForm.StartPosition = FormStartPosition.WindowsDefaultLocation;
+ }
+ cacheForm.TopMost = true;
cacheForm.Show();
}
+ private void MiscMenuPreload_Click(object sender, EventArgs e)
+ {
+ Float2 min = MinVisibleGraph, max = MaxVisibleGraph;
+ Float2 add = new(max.x - min.x, max.y - min.y);
+ add.x *= 0.75; // Expansion
+ add.y *= 0.75; // Screen + 75%
+
+ Float2 xRange = new(min.x - add.x, max.x + add.x),
+ yRange = new(min.y - add.y, max.y + add.y);
+
+ double step = ScreenSpaceToGraphSpace(new Int2(1, 0)).x
+ - ScreenSpaceToGraphSpace(new Int2(0, 0)).x;
+ step /= 10;
+
+ foreach (Graphable able in Graphables) able.Preload(xRange, yRange, step);
+ Invalidate(false);
+ }
}
diff --git a/Base/Forms/SetZoomForm.Designer.cs b/Base/Forms/SetZoomForm.Designer.cs
index e659456..52990db 100644
--- a/Base/Forms/SetZoomForm.Designer.cs
+++ b/Base/Forms/SetZoomForm.Designer.cs
@@ -1,4 +1,7 @@
-namespace Graphing.Forms
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Graphing.Forms
{
partial class SetZoomForm
{
diff --git a/Base/Forms/SetZoomForm.cs b/Base/Forms/SetZoomForm.cs
index 1d30ee4..fc21296 100644
--- a/Base/Forms/SetZoomForm.cs
+++ b/Base/Forms/SetZoomForm.cs
@@ -1,4 +1,7 @@
-namespace Graphing.Forms;
+using System;
+using System.Windows.Forms;
+
+namespace Graphing.Forms;
public partial class SetZoomForm : Form
{
diff --git a/Base/Forms/ViewCacheForm.Designer.cs b/Base/Forms/ViewCacheForm.Designer.cs
index c847e6b..e2d9498 100644
--- a/Base/Forms/ViewCacheForm.Designer.cs
+++ b/Base/Forms/ViewCacheForm.Designer.cs
@@ -1,4 +1,7 @@
-namespace Graphing.Forms
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Graphing.Forms
{
partial class ViewCacheForm
{
diff --git a/Base/Forms/ViewCacheForm.cs b/Base/Forms/ViewCacheForm.cs
index 55cd1e4..7f767a9 100644
--- a/Base/Forms/ViewCacheForm.cs
+++ b/Base/Forms/ViewCacheForm.cs
@@ -1,4 +1,8 @@
using Graphing.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Windows.Forms;
namespace Graphing.Forms;
@@ -32,6 +36,10 @@ public partial class ViewCacheForm : Form
CachePie.Values.Add((able.Color, thisBytes));
totalBytes += thisBytes;
+ int buttonHeight = (int)(refForm.DpiFloat * 46 / 192),
+ buttonWidth = (int)(refForm.DpiFloat * 92 / 192),
+ buttonSpaced = (int)(refForm.DpiFloat * 98 / 192);
+
if (index < labelCache.Count)
{
Label reuseLabel = labelCache[index];
@@ -45,9 +53,9 @@ public partial class ViewCacheForm : Form
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right,
AutoEllipsis = true,
ForeColor = able.Color,
- Location = new Point(0, labelCache.Count * 46),
+ Location = new Point(0, labelCache.Count * buttonHeight),
Parent = SpecificCachePanel,
- Size = new Size(SpecificCachePanel.Width - 98, 46),
+ Size = new Size(SpecificCachePanel.Width - buttonSpaced, buttonHeight),
Text = $"{able.Name}: {thisBytes.FormatAsBytes()}",
TextAlign = ContentAlignment.MiddleLeft,
};
@@ -59,9 +67,9 @@ public partial class ViewCacheForm : Form
Button newButton = new()
{
Anchor = AnchorStyles.Top | AnchorStyles.Right,
- Location = new Point(SpecificCachePanel.Width - 92, buttonCache.Count * 46),
+ Location = new Point(SpecificCachePanel.Width - buttonWidth, buttonCache.Count * buttonHeight),
Parent = SpecificCachePanel,
- Size = new Size(92, 46),
+ Size = new Size(buttonWidth, buttonHeight),
Text = "Clear"
};
newButton.Click += (o, e) => EraseSpecificGraphable_Click(able);
diff --git a/Base/Graphable.cs b/Base/Graphable.cs
index 5f42e15..2844ef7 100644
--- a/Base/Graphable.cs
+++ b/Base/Graphable.cs
@@ -1,5 +1,6 @@
using Graphing.Forms;
-using Graphing.Parts;
+using System.Collections.Generic;
+using System.Drawing;
namespace Graphing;
@@ -8,12 +9,12 @@ public abstract class Graphable
private static int defaultColorsUsed;
public static readonly uint[] DefaultColors =
[
- 0xEF_B34D47, // Red
- 0xEF_4769B3, // Blue
- 0xEF_50B347, // Green
- 0xEF_7047B3, // Purple
- 0xEF_B38B47, // Orange
- 0xEF_5B5B5B // Black
+ 0xFF_B34D47, // Red
+ 0xFF_4769B3, // Blue
+ 0xFF_50B347, // Green
+ 0xFF_7047B3, // Purple
+ 0xFF_B38B47, // Orange
+ 0xFF_5B5B5B // Black
];
public Color Color { get; set; }
@@ -31,6 +32,10 @@ public abstract class Graphable
public abstract Graphable DeepCopy();
- public abstract void EraseCache();
- public abstract long GetCacheBytes();
+ 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;
}
diff --git a/Base/Graphables/ColumnTable.cs b/Base/Graphables/ColumnTable.cs
index 876e070..0461cc3 100644
--- a/Base/Graphables/ColumnTable.cs
+++ b/Base/Graphables/ColumnTable.cs
@@ -1,5 +1,8 @@
using Graphing.Forms;
using Graphing.Parts;
+using System;
+using System.Collections.Generic;
+using System.Linq;
namespace Graphing.Graphables;
@@ -32,7 +35,6 @@ public class ColumnTable : Graphable
tableXY.Add(x, equ(x));
}
- public override void EraseCache() { }
public override long GetCacheBytes() => 16 * tableXY.Count;
public override Graphable DeepCopy() => new ColumnTable(width / 0.75, tableXY.ToArray().ToDictionary());
@@ -48,4 +50,7 @@ public class ColumnTable : Graphable
return items;
}
+
+ // 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 fb265e8..c3dfa4e 100644
--- a/Base/Graphables/Equation.cs
+++ b/Base/Graphables/Equation.cs
@@ -1,9 +1,12 @@
-using Graphing.Forms;
+using Graphing.Abstract;
+using Graphing.Forms;
using Graphing.Parts;
+using System;
+using System.Collections.Generic;
namespace Graphing.Graphables;
-public class Equation : Graphable
+public class Equation : Graphable, IIntegrable, IDerivable
{
private static int equationNum;
@@ -22,15 +25,17 @@ public class Equation : Graphable
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 / 2, 0)).x) / 5;
+ epsilon *= graph.DpiFloat / 192;
List lines = [];
double previousX = graph.MinVisibleGraph.x;
double previousY = GetFromCache(previousX, epsilon);
- for (int i = 1; i < graph.ClientRectangle.Width; i += step)
+ for (int i = 0; i < graph.ClientRectangle.Width + step; i += step)
{
double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x;
double currentY = GetFromCache(currentX, epsilon);
@@ -44,6 +49,13 @@ public class Equation : Graphable
return lines;
}
+ public Graphable Derive() => new Equation(x =>
+ {
+ const double step = 1e-3;
+ return (equ(x + step) - equ(x)) / step;
+ });
+ public Graphable Integrate() => new IntegralEquation(this);
+
public EquationDelegate GetDelegate() => equ;
public override void EraseCache() => cache.Clear();
@@ -59,8 +71,6 @@ public class Equation : Graphable
}
}
- // Pretty sure this works. Certainly works pretty well with "hard-to-compute"
- // equations.
protected (double dist, double y, int index) NearestCachedPoint(double x)
{
if (cache.Count == 0) return (double.PositiveInfinity, double.NaN, -1);
@@ -96,6 +106,28 @@ public class Equation : Graphable
public override Graphable DeepCopy() => new Equation(equ);
public override long GetCacheBytes() => cache.Count * 16;
+
+ public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
+ {
+ Int2 screenMousePos = graph.GraphSpaceToScreenSpace(graphMousePos);
+
+ (_, _, int index) = NearestCachedPoint(graphMousePos.x);
+ Int2 screenCachePos = graph.GraphSpaceToScreenSpace(cache[index]);
+
+ double allowedDist = factor * graph.DpiFloat * 80 / 192;
+
+ Int2 dist = new(screenCachePos.x - screenMousePos.x,
+ screenCachePos.y - screenMousePos.y);
+ 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 void Preload(Float2 xRange, Float2 yRange, double step)
+ {
+ for (double x = xRange.x; x <= xRange.y; x += step) GetFromCache(x, step);
+ }
}
public delegate double EquationDelegate(double x);
diff --git a/Base/Graphables/IntegralEquation.cs b/Base/Graphables/IntegralEquation.cs
new file mode 100644
index 0000000..51742fd
--- /dev/null
+++ b/Base/Graphables/IntegralEquation.cs
@@ -0,0 +1,239 @@
+using Graphing.Abstract;
+using Graphing.Forms;
+using Graphing.Parts;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Design;
+
+namespace Graphing.Graphables;
+
+public class IntegralEquation : Graphable, IIntegrable, IDerivable
+{
+ protected readonly Equation? baseEqu;
+ protected readonly EquationDelegate? baseEquDel;
+
+ protected readonly IntegralEquation? altBaseEqu;
+
+ protected readonly bool usingAlt;
+
+ public IntegralEquation(Equation baseEquation)
+ {
+ string oldName = baseEquation.Name, newName;
+ if (oldName.StartsWith("Integral of ")) newName = "Second Integral of " + oldName[12..];
+ else if (oldName.StartsWith("Second Integral of ")) newName = "Third Integral of " + oldName[19..];
+ else newName = "Integral of " + oldName;
+
+ Name = newName;
+
+ baseEqu = baseEquation;
+ baseEquDel = baseEquation.GetDelegate();
+
+ altBaseEqu = null;
+ usingAlt = false;
+ }
+ public IntegralEquation(IntegralEquation baseEquation)
+ {
+ string oldName = baseEquation.Name, newName;
+ if (oldName.StartsWith("Integral of ")) newName = "Second Integral of " + oldName[12..];
+ else if (oldName.StartsWith("Second Integral of ")) newName = "Third Integral of " + oldName[19..];
+ else newName = "Integral of " + oldName;
+
+ Name = newName;
+
+ baseEqu = null;
+ baseEquDel = null;
+
+ altBaseEqu = baseEquation;
+ usingAlt = true;
+ }
+
+ public override Graphable DeepCopy() => new IntegralEquation(this);
+
+ 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 / 2, 0)).x) / 5;
+ epsilon *= graph.DpiFloat / 192;
+ List lines = [];
+
+ Int2 originLocation = graph.GraphSpaceToScreenSpace(new Float2(0, 0));
+ if (originLocation.x < 0)
+ {
+ // Origin is off the left side of the screen.
+ // Get to the left side from the origin.
+ double start = graph.MinVisibleGraph.x, end = graph.MaxVisibleGraph.x;
+ SetInternalStepper(start, epsilon);
+
+ // Now we can start.
+ double previousX = stepX;
+ double previousY = stepY;
+ for (double x = start; x <= end; x += epsilon)
+ {
+ MoveInternalStepper(epsilon);
+ lines.Add(new GraphLine(new Float2(previousX, previousY),
+ new Float2(stepX, stepY)));
+ previousX = stepX;
+ previousY = stepY;
+ }
+ }
+ else if (originLocation.x > graph.ClientRectangle.Width)
+ {
+ // Origin is off the right side of the screen.
+ // Get to the right side of the origin.
+ double start = graph.MaxVisibleGraph.x, end = graph.MinVisibleGraph.x;
+ SetInternalStepper(start, epsilon);
+
+ // Now we can start.
+ double previousX = stepX;
+ double previousY = stepY;
+ for (double x = start; x >= end; x -= epsilon)
+ {
+ MoveInternalStepper(-epsilon);
+ lines.Add(new GraphLine(new Float2(previousX, previousY),
+ new Float2(stepX, stepY)));
+ previousX = stepX;
+ previousY = stepY;
+ }
+ }
+ else
+ {
+ // Origin is on-screen.
+ // We need to do two cycles.
+
+ // Start with right.
+ double start = 0, end = graph.MaxVisibleGraph.x;
+ SetInternalStepper(start, epsilon);
+
+ double previousX = stepX;
+ double previousY = stepY;
+ for (double x = start; x <= end; x += epsilon)
+ {
+ MoveInternalStepper(epsilon);
+ lines.Add(new GraphLine(new Float2(previousX, previousY),
+ new Float2(stepX, stepY)));
+ previousX = stepX;
+ previousY = stepY;
+ }
+
+ // Now do left.
+ start = 0;
+ end = graph.MinVisibleGraph.x;
+ SetInternalStepper(start, epsilon);
+
+ previousX = stepX;
+ previousY = stepY;
+
+ for (double x = start; x >= end; x -= epsilon)
+ {
+ MoveInternalStepper(-epsilon);
+ lines.Add(new GraphLine(new Float2(previousX, previousY),
+ new Float2(stepX, stepY)));
+ previousX = stepX;
+ previousY = stepY;
+ }
+ }
+
+ return lines;
+ }
+
+ private double stepX = 0;
+ private double stepY = 0;
+ private void SetInternalStepper(double x, double dX)
+ {
+ stepX = 0;
+ stepY = 0;
+ if (usingAlt) altBaseEqu!.SetInternalStepper(0, dX);
+
+ if (x > 0)
+ {
+ while (stepX < x) MoveInternalStepper(dX);
+ }
+ else if (x < 0)
+ {
+ while (x < stepX) MoveInternalStepper(-dX);
+ }
+ }
+ private void MoveInternalStepper(double dX)
+ {
+ stepX += dX;
+ if (usingAlt)
+ {
+ altBaseEqu!.MoveInternalStepper(dX);
+ stepY += altBaseEqu!.stepY * dX;
+ }
+ else
+ {
+ stepY += baseEquDel!(stepX) * dX;
+ }
+ }
+
+ // Try to avoid using this, as it converts the integral into a
+ // far less efficient format (uses the `IntegralAtPoint` method).
+ public Equation AsEquation() => new(IntegralAtPoint)
+ {
+ Name = Name,
+ Color = Color
+ };
+
+ public Graphable Derive()
+ {
+ if (usingAlt) return altBaseEqu!.DeepCopy();
+ else return (Equation)baseEqu!.DeepCopy();
+ }
+ public Graphable Integrate() => new IntegralEquation(this);
+
+ // Standard integral method.
+ // Inefficient for successive calls.
+ public double IntegralAtPoint(double x)
+ {
+ if (x > 0)
+ {
+ double start = Math.Min(0, x), end = Math.Max(0, x);
+ const double step = 1e-3;
+ double sum = 0;
+
+ SetInternalStepper(start, step);
+ for (double t = start; t <= end; t += step)
+ {
+ MoveInternalStepper(step);
+ sum += stepY * step;
+ }
+
+ return sum;
+ }
+ else if (x < 0)
+ {
+ double start = Math.Max(0, x), end = Math.Min(0, x);
+ const double step = 1e-3;
+ double sum = 0;
+
+ SetInternalStepper(start, step);
+ for (double t = start; t >= end; t -= step)
+ {
+ MoveInternalStepper(-step);
+ sum -= stepY * step;
+ }
+
+ return sum;
+ }
+ else return 0;
+ }
+
+ public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
+ {
+ Int2 screenMousePos = graph.GraphSpaceToScreenSpace(graphMousePos);
+
+ Int2 screenPos = graph.GraphSpaceToScreenSpace(new Float2(graphMousePos.x,
+ IntegralAtPoint(graphMousePos.x)));
+
+ double allowedDist = factor * graph.DpiFloat * 80 / 192;
+
+ Int2 dist = new(screenPos.x - screenMousePos.x,
+ screenPos.y - screenMousePos.y);
+ 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));
+}
diff --git a/Base/Graphables/SlopeField.cs b/Base/Graphables/SlopeField.cs
index 9fd63dc..066f658 100644
--- a/Base/Graphables/SlopeField.cs
+++ b/Base/Graphables/SlopeField.cs
@@ -1,5 +1,7 @@
using Graphing.Forms;
using Graphing.Parts;
+using System;
+using System.Collections.Generic;
namespace Graphing.Graphables;
@@ -74,6 +76,57 @@ public class SlopeField : Graphable
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);
+
+ 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);
+
+ if (graphMousePos.x < Math.Min(line.a.x, line.b.x) ||
+ graphMousePos.x > Math.Max(line.a.x, line.b.x)) return false;
+
+ double allowedDist = factor * graph.DpiFloat * 10 / 192;
+
+ double lineX = graphMousePos.x,
+ lineY = slope * (lineX - nearestPos.x) + nearestPos.y;
+
+ Int2 pointScreen = graph.GraphSpaceToScreenSpace(new Float2(lineX, lineY));
+ Int2 mouseScreen = graph.GraphSpaceToScreenSpace(graphMousePos);
+ Int2 dist = new(pointScreen.x - mouseScreen.x,
+ pointScreen.y - mouseScreen.y);
+ double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
+ return totalDist <= allowedDist;
+ }
+ public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos)
+ {
+ Float2 nearestPos = new(Math.Round(graphMousePos.x * detail) / detail,
+ Math.Round(graphMousePos.y * detail) / detail);
+
+ 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);
+
+ double lineX = graphMousePos.x,
+ lineY = slope * (lineX - nearestPos.x) + nearestPos.y;
+ Float2 point = new(lineX, lineY);
+
+ return 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 y = Math.Ceiling(yRange.x - 1); y < yRange.y + 1; y += 1.0 / detail)
+ {
+ GetFromCache(step, x, y);
+ }
+ }
+ }
}
public delegate double SlopeFieldsDelegate(double x, double y);
diff --git a/Base/Graphables/TangentLine.cs b/Base/Graphables/TangentLine.cs
index 33ceb7f..f22eec0 100644
--- a/Base/Graphables/TangentLine.cs
+++ b/Base/Graphables/TangentLine.cs
@@ -1,21 +1,40 @@
using Graphing.Forms;
using Graphing.Parts;
+using System;
+using System.Collections.Generic;
namespace Graphing.Graphables;
public class TangentLine : Graphable
{
- public double Position { get; set; }
+ public double Position
+ {
+ get => _position;
+ set
+ {
+ currentSlope = DerivativeAtPoint(value);
+ _position = value;
+ }
+ }
+ private double _position; // Private because it has exactly the same functionality as `Position`.
protected readonly Equation parent;
protected readonly EquationDelegate parentEqu;
protected readonly double length;
+ // X is slope, Y is height.
+ protected Float2 currentSlope;
+
+ // No binary search for this, I want it to be exact.
+ // Value: X is slope, Y is height.
+ protected Dictionary slopeCache;
+
public TangentLine(double length, double position, Equation parent)
{
Name = $"Tangent Line of {parent.Name}";
+ slopeCache = [];
parentEqu = parent.GetDelegate();
Position = position;
this.length = length;
@@ -24,28 +43,73 @@ public class TangentLine : Graphable
public override IEnumerable GetItemsToRender(in GraphForm graph)
{
- Float2 point = new(Position, parentEqu(Position));
- return [MakeSlopeLine(point, DerivativeAtPoint(Position)),
- new GraphUiCircle(point, 8)];
+ Float2 point = new(Position, currentSlope.y);
+ return [MakeSlopeLine(), new GraphUiCircle(point, 8)];
}
- protected GraphLine MakeSlopeLine(Float2 position, double slope)
+ protected GraphLine MakeSlopeLine()
{
- double dirX = length, dirY = slope * length;
+ double dirX = length, dirY = currentSlope.x * length;
double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY);
dirX /= magnitude * 2 / length;
dirY /= magnitude * 2 / length;
- return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY));
+ return new(new(Position + dirX, currentSlope.y + dirY), new(Position - dirX, currentSlope.y - dirY));
}
- protected double DerivativeAtPoint(double x)
+ protected Float2 DerivativeAtPoint(double x)
{
+ // If value is already computed, return it.
+ if (slopeCache.TryGetValue(x, out Float2 val)) return val;
+
const double step = 1e-3;
- return (parentEqu(x + step) - parentEqu(x)) / step;
+
+ double initial = parentEqu(x);
+ Float2 result = new((parentEqu(x + step) - initial) / step, initial);
+ slopeCache.Add(x, result);
+ return result;
}
public override Graphable DeepCopy() => new TangentLine(length, Position, parent);
- public override void EraseCache() { }
- public override long GetCacheBytes() => 0;
+ public override void EraseCache() => slopeCache.Clear();
+ public override long GetCacheBytes() => slopeCache.Count * 24;
+
+ public override bool ShouldSelectGraphable(in GraphForm graph, Float2 graphMousePos, double factor)
+ {
+ GraphLine line = MakeSlopeLine();
+
+ if (graphMousePos.x < Math.Min(line.a.x - 0.25, line.b.x - 0.25) ||
+ graphMousePos.x > Math.Max(line.a.x + 0.25, line.b.x + 0.25)) return false;
+
+ double allowedDist = factor * graph.DpiFloat * 80 / 192;
+
+ double lineX = graphMousePos.x,
+ lineY = currentSlope.x * (lineX - Position) + currentSlope.y;
+
+ Int2 pointScreen = graph.GraphSpaceToScreenSpace(new Float2(lineX, lineY));
+ Int2 mouseScreen = graph.GraphSpaceToScreenSpace(graphMousePos);
+ Int2 dist = new(pointScreen.x - mouseScreen.x,
+ pointScreen.y - mouseScreen.y);
+ double totalDist = Math.Sqrt(dist.x * dist.x + dist.y * dist.y);
+ return totalDist <= allowedDist;
+ }
+ public override Float2 GetSelectedPoint(in GraphForm graph, Float2 graphMousePos)
+ {
+ GraphLine line = MakeSlopeLine();
+
+ double lineX = Math.Clamp(graphMousePos.x,
+ 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);
+ }
+
+ public override void Preload(Float2 xRange, Float2 yRange, double step)
+ {
+ // Despite the tangent line barely using any data, when preloaded it
+ // will always take as much memory as an equation. Seems like a bit much,
+ // but may be used when the tangent line is moved. Not sure there's much
+ // that can be changed.
+ for (double x = xRange.x; x <= xRange.y; x += step) DerivativeAtPoint(x);
+ }
}
diff --git a/Base/IGraphPart.cs b/Base/IGraphPart.cs
index 0e3c592..fb4ad53 100644
--- a/Base/IGraphPart.cs
+++ b/Base/IGraphPart.cs
@@ -1,8 +1,9 @@
using Graphing.Forms;
+using System.Drawing;
namespace Graphing;
public interface IGraphPart
{
- public void Render(in GraphForm form, in Graphics g, in Brush brush);
+ public void Render(in GraphForm form, in Graphics g, in Pen pen);
}
diff --git a/Base/Int2.cs b/Base/Int2.cs
index 6377498..92dfd18 100644
--- a/Base/Int2.cs
+++ b/Base/Int2.cs
@@ -1,4 +1,6 @@
-namespace Graphing;
+using System.Drawing;
+
+namespace Graphing;
public record struct Int2
{
diff --git a/Base/Parts/GraphLine.cs b/Base/Parts/GraphLine.cs
index fff70f1..3b7527b 100644
--- a/Base/Parts/GraphLine.cs
+++ b/Base/Parts/GraphLine.cs
@@ -1,4 +1,5 @@
using Graphing.Forms;
+using System.Drawing;
namespace Graphing.Parts;
@@ -18,15 +19,13 @@ public record struct GraphLine : IGraphPart
this.b = b;
}
- public readonly void Render(in GraphForm form, in Graphics g, in Brush brush)
+ public readonly void Render(in GraphForm form, in Graphics g, in Pen pen)
{
if (!double.IsFinite(a.x) || !double.IsFinite(a.y) ||
!double.IsFinite(b.x) || !double.IsFinite(b.y)) return;
Int2 start = form.GraphSpaceToScreenSpace(a),
end = form.GraphSpaceToScreenSpace(b);
-
- Pen pen = new(brush, 3);
g.DrawLine(pen, start, end);
}
}
diff --git a/Base/Parts/GraphRectangle.cs b/Base/Parts/GraphRectangle.cs
index a874a42..4881397 100644
--- a/Base/Parts/GraphRectangle.cs
+++ b/Base/Parts/GraphRectangle.cs
@@ -1,4 +1,5 @@
using Graphing.Forms;
+using System.Drawing;
namespace Graphing.Parts;
@@ -25,7 +26,7 @@ public record struct GraphRectangle : IGraphPart
max = max
};
- public void Render(in GraphForm form, in Graphics g, in Brush brush)
+ public void Render(in GraphForm form, in Graphics g, in Pen pen)
{
if (!double.IsFinite(max.x) || !double.IsFinite(max.y) ||
!double.IsFinite(min.x) || !double.IsFinite(min.y)) return;
@@ -40,6 +41,6 @@ public record struct GraphRectangle : IGraphPart
start.y - end.y);
if (size.x == 0 || size.y == 0) return;
- g.FillRectangle(brush, new Rectangle(start.x, end.y, size.x, size.y));
+ g.FillRectangle(pen.Brush, new Rectangle(start.x, end.y, size.x, size.y));
}
}
diff --git a/Base/Parts/GraphUiCircle.cs b/Base/Parts/GraphUiCircle.cs
index 28bb010..7f46411 100644
--- a/Base/Parts/GraphUiCircle.cs
+++ b/Base/Parts/GraphUiCircle.cs
@@ -1,4 +1,5 @@
using Graphing.Forms;
+using System.Drawing;
namespace Graphing.Parts;
@@ -18,14 +19,16 @@ public record struct GraphUiCircle : IGraphPart
this.radius = radius;
}
- public readonly void Render(in GraphForm form, in Graphics g, in Brush brush)
+ public readonly void Render(in GraphForm form, in Graphics g, in Pen pen)
{
if (!double.IsFinite(center.x) || !double.IsFinite(center.y) ||
!double.IsFinite(radius) || radius == 0) return;
+ int rad = (int)(form.DpiFloat * radius / 192);
+
Int2 centerPix = form.GraphSpaceToScreenSpace(center);
- g.FillEllipse(brush, new Rectangle(new Point(centerPix.x - radius,
- centerPix.y - radius),
- new Size(radius * 2, radius * 2)));
+ g.FillEllipse(pen.Brush, new Rectangle(new Point(centerPix.x - rad,
+ centerPix.y - rad),
+ new Size(rad * 2, rad * 2)));
}
}
diff --git a/Base/Properties/PublishProfiles/FolderProfile.pubxml.user b/Base/Properties/PublishProfiles/FolderProfile.pubxml.user
index 083b367..706348e 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-13T14:31:43.4569441Z;False|2024-03-13T10:30:01.4347009-04:00;False|2024-03-13T10:27:31.9554551-04:00;
+ 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;
\ No newline at end of file
diff --git a/README.md b/README.md
index 7266efe..ac7ed2b 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,12 @@ This is a graphing calculator I made initially for a Calculus project in a day o
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).
- There are currently some rendering issues with asymptotes which will be focused on at some point.
+- Integrate and derive equations.
- Graph a slope field of a `dy/dx =` style equation.
+- View a tangent line of an equation.
+- Display a vertical bar graph.
+
+However, you can develop your own features as well.
The system does not and likely will not (at least for a while) support text-to-equation parsing. You must import this project as a library and add graphs that way.
@@ -70,7 +75,7 @@ An equation requires a delegate such as the one you see. Alternatively, you can
graph.Graph(new Equation(x => Math.Pow(2, x))
{
Color = Color.Green,
- Name = "2^x"
+ Name = "Exponential Base 2"
});
```
diff --git a/Testing/Program.cs b/Testing/Program.cs
index 105bc57..851e7f8 100644
--- a/Testing/Program.cs
+++ b/Testing/Program.cs
@@ -1,5 +1,7 @@
using Graphing.Forms;
using Graphing.Graphables;
+using System;
+using System.Windows.Forms;
namespace Graphing.Testing;
@@ -10,31 +12,19 @@ internal static class Program
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
- Application.SetHighDpiMode(HighDpiMode.SystemAware);
+ Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
GraphForm graph = new("One Of The Graphing Calculators Of All Time");
- Equation equ1 = new(x =>
- {
- // Demonstrate the caching abilities of the software.
- // This extra waiting is done every time the form requires a
- // calculation done. At the start, it'll be laggy, but as you
- // move around and zoom in, more pieces are cached, and when
- // you reset, the viewport will be a lot less laggy.
+ 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);
- // Remove this loop to make the equation fast again. I didn't
- // slow the engine down much more with this improvement, so any
- // speed decrease you might notice is likely this function.
- for (int i = 0; i < 1_000_000; i++) ;
- return -x * x + 2;
- });
- Equation equ2 = new(x => x);
- Equation equ3 = new(x => -Math.Sqrt(x));
- SlopeField sf = new(2, (x, y) => (x * x - y * y) / x);
- graph.Graph(equ1, equ2, equ3, sf);
-
- // You can also now view and reset caches in the UI by going to
- // Misc > View Caches.
+ // 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!
Application.Run(graph);
}
diff --git a/Testing/Testing.csproj b/Testing/Testing.csproj
index fe3e2f2..6d21d69 100644
--- a/Testing/Testing.csproj
+++ b/Testing/Testing.csproj
@@ -4,7 +4,7 @@
net8.0-windows
enable
true
- enable
+ disable
Graphing.Testing
ThatOneNerd.Graphing.Testing