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