Version 1.2 is ready. #25

Merged
That-One-Nerd merged 14 commits from canary into main 2024-03-21 12:37:35 -04:00
31 changed files with 727 additions and 179 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.vs/ .vs/
.github/
Base/obj/ Base/obj/
Base/bin/ Base/bin/

View File

@ -0,0 +1,8 @@
using Graphing.Graphables;
namespace Graphing.Abstract;
public interface IDerivable
{
public Graphable Derive();
}

View File

@ -0,0 +1,8 @@
using Graphing.Graphables;
namespace Graphing.Abstract;
public interface IIntegrable
{
public Graphable Integrate();
}

View File

@ -5,25 +5,25 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<RootNamespace>Graphing</RootNamespace> <RootNamespace>Graphing</RootNamespace>
<AssemblyName>ThatOneNerd.Graphing</AssemblyName> <AssemblyName>ThatOneNerd.Graphing</AssemblyName>
<ProduceReferenceAssembly>True</ProduceReferenceAssembly> <ProduceReferenceAssembly>True</ProduceReferenceAssembly>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ThatOneNerd.Graphing</PackageId> <PackageId>ThatOneNerd.Graphing</PackageId>
<Title>ThatOneNerd.Graphing</Title> <Title>ThatOneNerd.Graphing</Title>
<Version>1.1.0</Version> <Version>1.2.0</Version>
<Authors>That_One_Nerd</Authors> <Authors>That_One_Nerd</Authors>
<Description>A fairly adept graphing calculator made in Windows Forms.</Description> <Description>A fairly adept graphing calculator made in Windows Forms.</Description>
<Copyright>MIT</Copyright> <Copyright>MIT</Copyright>
<RepositoryUrl>https://github.com/That-One-Nerd/Graphing</RepositoryUrl> <RepositoryUrl>https://github.com/That-One-Nerd/Graphing</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>graphing;graph;plot;math;calculus;visual;desmos</PackageTags> <PackageTags>graphing;graph;plot;math;calculus;visual;desmos;slope field;slopefield;equation;visualizer</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>True</IncludeSymbols> <IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageReleaseNotes>View the GitHub release for the changelog: <PackageReleaseNotes>View the GitHub release for the changelog:
https://github.com/That-One-Nerd/Graphing/releases/tag/1.1.0</PackageReleaseNotes> https://github.com/That-One-Nerd/Graphing/releases/tag/1.2.0</PackageReleaseNotes>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -20,4 +20,9 @@
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Forms\ViewCacheForm.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,6 @@
namespace Graphing; using System.Drawing;
namespace Graphing;
public record struct Float2 public record struct Float2
{ {

View File

@ -1,4 +1,6 @@
namespace Graphing.Forms.Controls using System.Drawing;
namespace Graphing.Forms.Controls
{ {
partial class PieChart partial class PieChart
{ {
@ -33,7 +35,7 @@
// PieChart // PieChart
// //
AutoScaleDimensions = new SizeF(13F, 32F); AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
Name = "PieChart"; Name = "PieChart";
Size = new Size(500, 500); Size = new Size(500, 500);
ResumeLayout(false); ResumeLayout(false);

View File

@ -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; namespace Graphing.Forms.Controls;
@ -6,12 +10,18 @@ public partial class PieChart : UserControl
{ {
public List<(Color, double)> Values { get; set; } public List<(Color, double)> Values { get; set; }
public float DpiFloat { get; private set; }
public PieChart() public PieChart()
{ {
SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.UserPaint, true);
Graphics tempG = CreateGraphics();
DpiFloat = (tempG.DpiX + tempG.DpiY) / 2;
tempG.Dispose();
Values = []; Values = [];
InitializeComponent(); InitializeComponent();
} }
@ -40,20 +50,25 @@ public partial class PieChart : UserControl
current += item.value; current += item.value;
} }
// Draw the outline. // Draw the outline of each slice.
Pen outlinePartsPen = new(Color.FromArgb(unchecked((int)0xFF_202020)), 3); // Only done if there is more than one slice.
current = 0; if (Values.Count > 1)
foreach ((Color, double value) item in Values)
{ {
double start = 360 * current / sum, Pen outlinePartsPen = new(Color.FromArgb(unchecked((int)0xFF_202020)), DpiFloat * 3 / 192);
end = 360 * (current + item.value) / sum; current = 0;
g.DrawPie(outlinePartsPen, rect, (float)start, (float)(end - start)); 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 // 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); g.DrawEllipse(outlinePen, rect);
} }
} }

View File

@ -1,4 +1,7 @@
namespace Graphing.Forms using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms
{ {
partial class GraphColorPickerForm partial class GraphColorPickerForm
{ {
@ -40,7 +43,7 @@
ResultView = new Panel(); ResultView = new Panel();
BottomPanel = new Panel(); BottomPanel = new Panel();
OkButton = new Button(); OkButton = new Button();
CancelButton = new Button(); CancellingButton = new Button();
RgbSliders.SuspendLayout(); RgbSliders.SuspendLayout();
((System.ComponentModel.ISupportInitialize)BlueTrackBar).BeginInit(); ((System.ComponentModel.ISupportInitialize)BlueTrackBar).BeginInit();
((System.ComponentModel.ISupportInitialize)RedTrackBar).BeginInit(); ((System.ComponentModel.ISupportInitialize)RedTrackBar).BeginInit();
@ -169,7 +172,7 @@
// //
BottomPanel.BackColor = SystemColors.Window; BottomPanel.BackColor = SystemColors.Window;
BottomPanel.Controls.Add(OkButton); BottomPanel.Controls.Add(OkButton);
BottomPanel.Controls.Add(CancelButton); BottomPanel.Controls.Add(CancellingButton);
BottomPanel.Dock = DockStyle.Bottom; BottomPanel.Dock = DockStyle.Bottom;
BottomPanel.Location = new Point(0, 517); BottomPanel.Location = new Point(0, 517);
BottomPanel.Margin = new Padding(0); BottomPanel.Margin = new Padding(0);
@ -191,15 +194,15 @@
// //
// CancelButton // CancelButton
// //
CancelButton.Anchor = AnchorStyles.Right; CancellingButton.Anchor = AnchorStyles.Right;
CancelButton.Location = new Point(384, 9); CancellingButton.Location = new Point(384, 9);
CancelButton.Margin = new Padding(0); CancellingButton.Margin = new Padding(0);
CancelButton.Name = "CancelButton"; CancellingButton.Name = "CancelButton";
CancelButton.Size = new Size(150, 46); CancellingButton.Size = new Size(150, 46);
CancelButton.TabIndex = 0; CancellingButton.TabIndex = 0;
CancelButton.Text = "Cancel"; CancellingButton.Text = "Cancel";
CancelButton.UseVisualStyleBackColor = true; CancellingButton.UseVisualStyleBackColor = true;
CancelButton.Click += CancelButton_Click; CancellingButton.Click += CancelButton_Click;
// //
// GraphColorPickerForm // GraphColorPickerForm
// //
@ -234,7 +237,7 @@
private TrackBar BlueTrackBar; private TrackBar BlueTrackBar;
private TrackBar RedTrackBar; private TrackBar RedTrackBar;
private Panel BottomPanel; private Panel BottomPanel;
private Button CancelButton; private Button CancellingButton;
private Button OkButton; private Button OkButton;
private TextBox RedValueBox; private TextBox RedValueBox;
private TextBox BlueValueBox; private TextBox BlueValueBox;

View File

@ -1,4 +1,8 @@
namespace Graphing.Forms; using System;
using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms;
public partial class GraphColorPickerForm : Form public partial class GraphColorPickerForm : Form
{ {
@ -37,7 +41,7 @@ public partial class GraphColorPickerForm : Form
MessageLabel.Text = $"Pick a color for {able.Name}."; MessageLabel.Text = $"Pick a color for {able.Name}.";
// Add preset buttons. // Add preset buttons.
const int size = 48; int size = (int)(graph.DpiFloat * 48 / 192);
int position = 0; int position = 0;
foreach (uint cId in Graphable.DefaultColors) foreach (uint cId in Graphable.DefaultColors)
{ {

View File

@ -1,4 +1,7 @@
namespace Graphing.Forms using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms
{ {
partial class GraphForm partial class GraphForm
{ {
@ -41,6 +44,7 @@
MenuEquationsIntegral = new ToolStripMenuItem(); MenuEquationsIntegral = new ToolStripMenuItem();
MenuMisc = new ToolStripMenuItem(); MenuMisc = new ToolStripMenuItem();
MenuMiscCaches = new ToolStripMenuItem(); MenuMiscCaches = new ToolStripMenuItem();
MiscMenuPreload = new ToolStripMenuItem();
GraphMenu.SuspendLayout(); GraphMenu.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
@ -129,7 +133,7 @@
// //
// MenuMisc // MenuMisc
// //
MenuMisc.DropDownItems.AddRange(new ToolStripItem[] { MenuMiscCaches }); MenuMisc.DropDownItems.AddRange(new ToolStripItem[] { MenuMiscCaches, MiscMenuPreload });
MenuMisc.Name = "MenuMisc"; MenuMisc.Name = "MenuMisc";
MenuMisc.Size = new Size(83, 38); MenuMisc.Size = new Size(83, 38);
MenuMisc.Text = "Misc"; MenuMisc.Text = "Misc";
@ -141,6 +145,13 @@
MenuMiscCaches.Text = "View Caches"; MenuMiscCaches.Text = "View Caches";
MenuMiscCaches.Click += MenuMiscCaches_Click; MenuMiscCaches.Click += MenuMiscCaches_Click;
// //
// MiscMenuPreload
//
MiscMenuPreload.Name = "MiscMenuPreload";
MiscMenuPreload.Size = new Size(359, 44);
MiscMenuPreload.Text = "Preload Cache";
MiscMenuPreload.Click += MiscMenuPreload_Click;
//
// GraphForm // GraphForm
// //
AutoScaleDimensions = new SizeF(13F, 32F); AutoScaleDimensions = new SizeF(13F, 32F);
@ -172,5 +183,6 @@
private ToolStripMenuItem MenuEquationsIntegral; private ToolStripMenuItem MenuEquationsIntegral;
private ToolStripMenuItem MenuMisc; private ToolStripMenuItem MenuMisc;
private ToolStripMenuItem MenuMiscCaches; private ToolStripMenuItem MenuMiscCaches;
private ToolStripMenuItem MiscMenuPreload;
} }
} }

View File

@ -1,7 +1,11 @@
using Graphing.Extensions; using Graphing.Abstract;
using Graphing.Graphables; using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Text; using System.Linq;
using System.Windows.Forms;
namespace Graphing.Forms; namespace Graphing.Forms;
@ -10,10 +14,13 @@ public partial class GraphForm : Form
public static readonly Color MainAxisColor = Color.Black; public static readonly Color MainAxisColor = Color.Black;
public static readonly Color SemiAxisColor = Color.FromArgb(unchecked((int)0xFF_999999)); 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 QuarterAxisColor = Color.FromArgb(unchecked((int)0xFF_E0E0E0));
public static readonly Color UnitsTextColor = Color.Black;
public Float2 ScreenCenter { get; private set; } public Float2 ScreenCenter { get; private set; }
public Float2 Dpi { get; private set; } public Float2 Dpi { get; private set; }
public float DpiFloat { get; private set; }
public double ZoomLevel public double ZoomLevel
{ {
get => _zoomLevel; get => _zoomLevel;
@ -21,7 +28,7 @@ public partial class GraphForm : Form
{ {
double oldZoom = ZoomLevel; double oldZoom = ZoomLevel;
_zoomLevel = Math.Clamp(value, 1e-2, 1e3); _zoomLevel = Math.Clamp(value, 1e-5, 1e3);
int totalSegments = 0; int totalSegments = 0;
foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count(); foreach (Graphable able in ables) totalSegments += able.GetItemsToRender(this).Count();
@ -57,6 +64,9 @@ public partial class GraphForm : Form
Graphics tempG = CreateGraphics(); Graphics tempG = CreateGraphics();
Dpi = new(tempG.DpiX, tempG.DpiY); Dpi = new(tempG.DpiX, tempG.DpiY);
tempG.Dispose(); tempG.Dispose();
DpiFloat = (float)((Dpi.x + Dpi.y) / 2);
ables = []; ables = [];
ZoomLevel = 1; ZoomLevel = 1;
initialWindowPos = Location; initialWindowPos = Location;
@ -102,7 +112,7 @@ public partial class GraphForm : Form
// Draw horizontal/vertical quarter-axis. // Draw horizontal/vertical quarter-axis.
Brush quarterBrush = new SolidBrush(QuarterAxisColor); 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) 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. // Draw horizontal/vertical semi-axis.
Brush semiBrush = new SolidBrush(SemiAxisColor); 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) 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); 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). // Draw the main axis (on top of the semi axis).
Int2 startCenterY = GraphSpaceToScreenSpace(new Float2(0, MinVisibleGraph.y)), 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, startCenterX, endCenterX);
g.DrawLine(mainLinePen, startCenterY, endCenterY); 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) protected override void OnPaint(PaintEventArgs e)
{ {
@ -156,13 +204,47 @@ public partial class GraphForm : Form
g.FillRectangle(background, e.ClipRectangle); g.FillRectangle(background, e.ClipRectangle);
PaintGrid(g); PaintGrid(g);
PaintUnits(g);
Point clientMousePos = PointToClient(Cursor.Position);
Float2 graphMousePos = ScreenSpaceToGraphSpace(new(clientMousePos.X,
clientMousePos.Y));
// Draw the actual graphs. // Draw the actual graphs.
Pen[] graphPens = new Pen[ables.Count];
for (int i = 0; i < ables.Count; i++) for (int i = 0; i < ables.Count; i++)
{ {
IEnumerable<IGraphPart> lines = ables[i].GetItemsToRender(this); IEnumerable<IGraphPart> lines = ables[i].GetItemsToRender(this);
Brush graphBrush = new SolidBrush(ables[i].Color); 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); base.OnPaint(e);
@ -183,11 +265,28 @@ public partial class GraphForm : Form
private bool mouseDrag = false; private bool mouseDrag = false;
private Int2 initialMouseLocation; private Int2 initialMouseLocation;
private Float2 initialScreenCenter; private Float2 initialScreenCenter;
private bool ableDrag = false;
protected override void OnMouseDown(MouseEventArgs e) protected override void OnMouseDown(MouseEventArgs e)
{ {
mouseDrag = true; if (!mouseDrag)
initialMouseLocation = new Int2(Cursor.Position.X, Cursor.Position.Y); {
initialScreenCenter = ScreenCenter; 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) 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); Float2 graphDiff = new(pixelDiff.x * ZoomLevel / Dpi.x, pixelDiff.y * ZoomLevel / Dpi.y);
ScreenCenter = new(initialScreenCenter.x + graphDiff.x, ScreenCenter = new(initialScreenCenter.x + graphDiff.x,
initialScreenCenter.y + graphDiff.y); initialScreenCenter.y + graphDiff.y);
Invalidate(false);
} }
mouseDrag = false; mouseDrag = false;
ableDrag = false;
Invalidate(false);
} }
protected override void OnMouseMove(MouseEventArgs e) protected override void OnMouseMove(MouseEventArgs e)
{ {
@ -213,6 +313,7 @@ public partial class GraphForm : Form
initialScreenCenter.y + graphDiff.y); initialScreenCenter.y + graphDiff.y);
Invalidate(false); Invalidate(false);
} }
else if (ableDrag) Invalidate(false);
} }
protected override void OnMouseWheel(MouseEventArgs e) protected override void OnMouseWheel(MouseEventArgs e)
{ {
@ -226,7 +327,6 @@ public partial class GraphForm : Form
ZoomLevel = 1; ZoomLevel = 1;
Invalidate(false); Invalidate(false);
} }
private void GraphColorPickerButton_Click(Graphable able) private void GraphColorPickerButton_Click(Graphable able)
{ {
GraphColorPickerForm picker = new(this, able) GraphColorPickerForm picker = new(this, able)
@ -235,6 +335,13 @@ public partial class GraphForm : Form
}; };
picker.Location = new Point(Location.X + ClientRectangle.Width + 10, picker.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2); 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(); picker.ShowDialog();
RegenerateMenuItems(); RegenerateMenuItems();
} }
@ -255,22 +362,24 @@ public partial class GraphForm : Form
colorItem.Click += (o, e) => GraphColorPickerButton_Click(able); colorItem.Click += (o, e) => GraphColorPickerButton_Click(able);
MenuColors.DropDownItems.Add(colorItem); MenuColors.DropDownItems.Add(colorItem);
if (able is Equation equ) if (able is IDerivable derivable)
{ {
ToolStripMenuItem derivativeItem = new() ToolStripMenuItem derivativeItem = new()
{ {
ForeColor = able.Color, ForeColor = able.Color,
Text = able.Name Text = able.Name
}; };
derivativeItem.Click += (o, e) => EquationComputeDerivative_Click(equ); derivativeItem.Click += (o, e) => Graph(derivable.Derive());
MenuEquationsDerivative.DropDownItems.Add(derivativeItem); MenuEquationsDerivative.DropDownItems.Add(derivativeItem);
}
if (able is IIntegrable integrable)
{
ToolStripMenuItem integralItem = new() ToolStripMenuItem integralItem = new()
{ {
ForeColor = able.Color, ForeColor = able.Color,
Text = able.Name Text = able.Name
}; };
integralItem.Click += (o, e) => EquationComputeIntegral_Click(equ); integralItem.Click += (o, e) => Graph(integrable.Integrate());
MenuEquationsIntegral.DropDownItems.Add(integralItem); MenuEquationsIntegral.DropDownItems.Add(integralItem);
} }
} }
@ -286,19 +395,16 @@ public partial class GraphForm : Form
Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2); Location.Y + (ClientRectangle.Height - picker.ClientRectangle.Height) / 2);
picker.ShowDialog(); picker.ShowDialog();
} }
private void ButtonViewportSetCenter_Click(object? sender, EventArgs e) private void ButtonViewportSetCenter_Click(object? sender, EventArgs e)
{ {
MessageBox.Show("TODO", "Set Center Position", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show("TODO", "Set Center Position", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
private void ButtonViewportReset_Click(object? sender, EventArgs e) private void ButtonViewportReset_Click(object? sender, EventArgs e)
{ {
ScreenCenter = new Float2(0, 0); ScreenCenter = new Float2(0, 0);
ZoomLevel = 1; ZoomLevel = 1;
Invalidate(false); Invalidate(false);
} }
private void ButtonViewportResetWindow_Click(object? sender, EventArgs e) private void ButtonViewportResetWindow_Click(object? sender, EventArgs e)
{ {
Location = initialWindowPos; Location = initialWindowPos;
@ -306,63 +412,6 @@ public partial class GraphForm : Form
WindowState = FormWindowState.Normal; 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) private void MenuMiscCaches_Click(object? sender, EventArgs e)
{ {
ViewCacheForm cacheForm = new(this) ViewCacheForm cacheForm = new(this)
@ -372,6 +421,29 @@ public partial class GraphForm : Form
cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10, cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2); 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(); 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);
}
} }

View File

@ -1,4 +1,7 @@
namespace Graphing.Forms using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms
{ {
partial class SetZoomForm partial class SetZoomForm
{ {

View File

@ -1,4 +1,7 @@
namespace Graphing.Forms; using System;
using System.Windows.Forms;
namespace Graphing.Forms;
public partial class SetZoomForm : Form public partial class SetZoomForm : Form
{ {

View File

@ -1,4 +1,7 @@
namespace Graphing.Forms using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms
{ {
partial class ViewCacheForm partial class ViewCacheForm
{ {

View File

@ -1,4 +1,8 @@
using Graphing.Extensions; using Graphing.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace Graphing.Forms; namespace Graphing.Forms;
@ -32,6 +36,10 @@ public partial class ViewCacheForm : Form
CachePie.Values.Add((able.Color, thisBytes)); CachePie.Values.Add((able.Color, thisBytes));
totalBytes += 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) if (index < labelCache.Count)
{ {
Label reuseLabel = labelCache[index]; Label reuseLabel = labelCache[index];
@ -45,9 +53,9 @@ public partial class ViewCacheForm : Form
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right, Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right,
AutoEllipsis = true, AutoEllipsis = true,
ForeColor = able.Color, ForeColor = able.Color,
Location = new Point(0, labelCache.Count * 46), Location = new Point(0, labelCache.Count * buttonHeight),
Parent = SpecificCachePanel, Parent = SpecificCachePanel,
Size = new Size(SpecificCachePanel.Width - 98, 46), Size = new Size(SpecificCachePanel.Width - buttonSpaced, buttonHeight),
Text = $"{able.Name}: {thisBytes.FormatAsBytes()}", Text = $"{able.Name}: {thisBytes.FormatAsBytes()}",
TextAlign = ContentAlignment.MiddleLeft, TextAlign = ContentAlignment.MiddleLeft,
}; };
@ -59,9 +67,9 @@ public partial class ViewCacheForm : Form
Button newButton = new() Button newButton = new()
{ {
Anchor = AnchorStyles.Top | AnchorStyles.Right, 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, Parent = SpecificCachePanel,
Size = new Size(92, 46), Size = new Size(buttonWidth, buttonHeight),
Text = "Clear" Text = "Clear"
}; };
newButton.Click += (o, e) => EraseSpecificGraphable_Click(able); newButton.Click += (o, e) => EraseSpecificGraphable_Click(able);

View File

@ -1,5 +1,6 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts; using System.Collections.Generic;
using System.Drawing;
namespace Graphing; namespace Graphing;
@ -8,12 +9,12 @@ public abstract class Graphable
private static int defaultColorsUsed; private static int defaultColorsUsed;
public static readonly uint[] DefaultColors = public static readonly uint[] DefaultColors =
[ [
0xEF_B34D47, // Red 0xFF_B34D47, // Red
0xEF_4769B3, // Blue 0xFF_4769B3, // Blue
0xEF_50B347, // Green 0xFF_50B347, // Green
0xEF_7047B3, // Purple 0xFF_7047B3, // Purple
0xEF_B38B47, // Orange 0xFF_B38B47, // Orange
0xEF_5B5B5B // Black 0xFF_5B5B5B // Black
]; ];
public Color Color { get; set; } public Color Color { get; set; }
@ -31,6 +32,10 @@ public abstract class Graphable
public abstract Graphable DeepCopy(); public abstract Graphable DeepCopy();
public abstract void EraseCache(); public virtual void EraseCache() { }
public abstract long GetCacheBytes(); 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;
} }

View File

@ -1,5 +1,8 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts; using Graphing.Parts;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -32,7 +35,6 @@ public class ColumnTable : Graphable
tableXY.Add(x, equ(x)); tableXY.Add(x, equ(x));
} }
public override void EraseCache() { }
public override long GetCacheBytes() => 16 * tableXY.Count; public override long GetCacheBytes() => 16 * tableXY.Count;
public override Graphable DeepCopy() => new ColumnTable(width / 0.75, tableXY.ToArray().ToDictionary()); public override Graphable DeepCopy() => new ColumnTable(width / 0.75, tableXY.ToArray().ToDictionary());
@ -48,4 +50,7 @@ public class ColumnTable : Graphable
return items; return items;
} }
// Nothing to preload, everything is already cached.
public override void Preload(Float2 xRange, Float2 yRange, double step) { }
} }

View File

@ -1,9 +1,12 @@
using Graphing.Forms; using Graphing.Abstract;
using Graphing.Forms;
using Graphing.Parts; using Graphing.Parts;
using System;
using System.Collections.Generic;
namespace Graphing.Graphables; namespace Graphing.Graphables;
public class Equation : Graphable public class Equation : Graphable, IIntegrable, IDerivable
{ {
private static int equationNum; private static int equationNum;
@ -22,15 +25,17 @@ public class Equation : Graphable
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
const int step = 10; const int step = 10;
double epsilon = Math.Abs(graph.ScreenSpaceToGraphSpace(new Int2(0, 0)).x double epsilon = Math.Abs(graph.ScreenSpaceToGraphSpace(new Int2(0, 0)).x
- graph.ScreenSpaceToGraphSpace(new Int2(step / 2, 0)).x) / 5; - graph.ScreenSpaceToGraphSpace(new Int2(step / 2, 0)).x) / 5;
epsilon *= graph.DpiFloat / 192;
List<IGraphPart> lines = []; List<IGraphPart> lines = [];
double previousX = graph.MinVisibleGraph.x; double previousX = graph.MinVisibleGraph.x;
double previousY = GetFromCache(previousX, epsilon); 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 currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x;
double currentY = GetFromCache(currentX, epsilon); double currentY = GetFromCache(currentX, epsilon);
@ -44,6 +49,13 @@ public class Equation : Graphable
return lines; 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 EquationDelegate GetDelegate() => equ;
public override void EraseCache() => cache.Clear(); 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) protected (double dist, double y, int index) NearestCachedPoint(double x)
{ {
if (cache.Count == 0) return (double.PositiveInfinity, double.NaN, -1); 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 Graphable DeepCopy() => new Equation(equ);
public override long GetCacheBytes() => cache.Count * 16; 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); public delegate double EquationDelegate(double x);

View File

@ -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<IGraphPart> 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<IGraphPart> 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));
}

View File

@ -1,5 +1,7 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts; using Graphing.Parts;
using System;
using System.Collections.Generic;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -74,6 +76,57 @@ public class SlopeField : Graphable
public override void EraseCache() => cache.Clear(); public override void EraseCache() => cache.Clear();
public override long GetCacheBytes() => cache.Count * 48; 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); public delegate double SlopeFieldsDelegate(double x, double y);

View File

@ -1,21 +1,40 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts; using Graphing.Parts;
using System;
using System.Collections.Generic;
namespace Graphing.Graphables; namespace Graphing.Graphables;
public class TangentLine : Graphable 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 Equation parent;
protected readonly EquationDelegate parentEqu; protected readonly EquationDelegate parentEqu;
protected readonly double length; 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<double, Float2> slopeCache;
public TangentLine(double length, double position, Equation parent) public TangentLine(double length, double position, Equation parent)
{ {
Name = $"Tangent Line of {parent.Name}"; Name = $"Tangent Line of {parent.Name}";
slopeCache = [];
parentEqu = parent.GetDelegate(); parentEqu = parent.GetDelegate();
Position = position; Position = position;
this.length = length; this.length = length;
@ -24,28 +43,73 @@ public class TangentLine : Graphable
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
Float2 point = new(Position, parentEqu(Position)); Float2 point = new(Position, currentSlope.y);
return [MakeSlopeLine(point, DerivativeAtPoint(Position)), return [MakeSlopeLine(), new GraphUiCircle(point, 8)];
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); double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY);
dirX /= magnitude * 2 / length; dirX /= magnitude * 2 / length;
dirY /= 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; 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 Graphable DeepCopy() => new TangentLine(length, Position, parent);
public override void EraseCache() { } public override void EraseCache() => slopeCache.Clear();
public override long GetCacheBytes() => 0; 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);
}
} }

View File

@ -1,8 +1,9 @@
using Graphing.Forms; using Graphing.Forms;
using System.Drawing;
namespace Graphing; namespace Graphing;
public interface IGraphPart 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);
} }

View File

@ -1,4 +1,6 @@
namespace Graphing; using System.Drawing;
namespace Graphing;
public record struct Int2 public record struct Int2
{ {

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using System.Drawing;
namespace Graphing.Parts; namespace Graphing.Parts;
@ -18,15 +19,13 @@ public record struct GraphLine : IGraphPart
this.b = b; 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) || if (!double.IsFinite(a.x) || !double.IsFinite(a.y) ||
!double.IsFinite(b.x) || !double.IsFinite(b.y)) return; !double.IsFinite(b.x) || !double.IsFinite(b.y)) return;
Int2 start = form.GraphSpaceToScreenSpace(a), Int2 start = form.GraphSpaceToScreenSpace(a),
end = form.GraphSpaceToScreenSpace(b); end = form.GraphSpaceToScreenSpace(b);
Pen pen = new(brush, 3);
g.DrawLine(pen, start, end); g.DrawLine(pen, start, end);
} }
} }

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using System.Drawing;
namespace Graphing.Parts; namespace Graphing.Parts;
@ -25,7 +26,7 @@ public record struct GraphRectangle : IGraphPart
max = max 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) || if (!double.IsFinite(max.x) || !double.IsFinite(max.y) ||
!double.IsFinite(min.x) || !double.IsFinite(min.y)) return; !double.IsFinite(min.x) || !double.IsFinite(min.y)) return;
@ -40,6 +41,6 @@ public record struct GraphRectangle : IGraphPart
start.y - end.y); start.y - end.y);
if (size.x == 0 || size.y == 0) return; 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));
} }
} }

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using System.Drawing;
namespace Graphing.Parts; namespace Graphing.Parts;
@ -18,14 +19,16 @@ public record struct GraphUiCircle : IGraphPart
this.radius = radius; 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) || if (!double.IsFinite(center.x) || !double.IsFinite(center.y) ||
!double.IsFinite(radius) || radius == 0) return; !double.IsFinite(radius) || radius == 0) return;
int rad = (int)(form.DpiFloat * radius / 192);
Int2 centerPix = form.GraphSpaceToScreenSpace(center); Int2 centerPix = form.GraphSpaceToScreenSpace(center);
g.FillEllipse(brush, new Rectangle(new Point(centerPix.x - radius, g.FillEllipse(pen.Brush, new Rectangle(new Point(centerPix.x - rad,
centerPix.y - radius), centerPix.y - rad),
new Size(radius * 2, radius * 2))); new Size(rad * 2, rad * 2)));
} }
} }

View File

@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
--> -->
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<History>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;</History> <History>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;</History>
<LastFailureDetails /> <LastFailureDetails />
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -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: Currently, it doesn't have a whole lot of features, but I'll be adding more in the future. Here's currently what it can do:
- Graph an equation (duh). - Graph an equation (duh).
- There are currently some rendering issues with asymptotes which will be focused on at some point. - 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. - 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. 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)) graph.Graph(new Equation(x => Math.Pow(2, x))
{ {
Color = Color.Green, Color = Color.Green,
Name = "2^x" Name = "Exponential Base 2"
}); });
``` ```

View File

@ -1,5 +1,7 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Graphables; using Graphing.Graphables;
using System;
using System.Windows.Forms;
namespace Graphing.Testing; namespace Graphing.Testing;
@ -10,31 +12,19 @@ internal static class Program
{ {
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
GraphForm graph = new("One Of The Graphing Calculators Of All Time"); GraphForm graph = new("One Of The Graphing Calculators Of All Time");
Equation equ1 = new(x => Equation equ = new(Math.Sin);
{ SlopeField sf = new(2, (x, y) => Math.Cos(x));
// Demonstrate the caching abilities of the software. TangentLine tl = new(2, 2, equ);
// This extra waiting is done every time the form requires a graph.Graph(equ, sf, tl);
// 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.
// Remove this loop to make the equation fast again. I didn't // Now, when integrating equations, the result is much less jagged
// slow the engine down much more with this improvement, so any // and much faster. Try it out! You can also select points along
// speed decrease you might notice is likely this function. // equations and such as well. Click on an equation to see for
for (int i = 0; i < 1_000_000; i++) ; // yourself!
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.
Application.Run(graph); Application.Run(graph);
} }

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<RootNamespace>Graphing.Testing</RootNamespace> <RootNamespace>Graphing.Testing</RootNamespace>
<AssemblyName>ThatOneNerd.Graphing.Testing</AssemblyName> <AssemblyName>ThatOneNerd.Graphing.Testing</AssemblyName>
</PropertyGroup> </PropertyGroup>