Version 1.2 is ready. #25
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.vs/
|
||||
.github/
|
||||
|
||||
Base/obj/
|
||||
Base/bin/
|
||||
|
||||
8
Base/Abstract/IDerivable.cs
Normal file
8
Base/Abstract/IDerivable.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Graphing.Graphables;
|
||||
|
||||
namespace Graphing.Abstract;
|
||||
|
||||
public interface IDerivable
|
||||
{
|
||||
public Graphable Derive();
|
||||
}
|
||||
8
Base/Abstract/IIntegrable.cs
Normal file
8
Base/Abstract/IIntegrable.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Graphing.Graphables;
|
||||
|
||||
namespace Graphing.Abstract;
|
||||
|
||||
public interface IIntegrable
|
||||
{
|
||||
public Graphable Integrate();
|
||||
}
|
||||
@ -5,25 +5,25 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<RootNamespace>Graphing</RootNamespace>
|
||||
<AssemblyName>ThatOneNerd.Graphing</AssemblyName>
|
||||
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageId>ThatOneNerd.Graphing</PackageId>
|
||||
<Title>ThatOneNerd.Graphing</Title>
|
||||
<Version>1.1.0</Version>
|
||||
<Version>1.2.0</Version>
|
||||
<Authors>That_One_Nerd</Authors>
|
||||
<Description>A fairly adept graphing calculator made in Windows Forms.</Description>
|
||||
<Copyright>MIT</Copyright>
|
||||
<RepositoryUrl>https://github.com/That-One-Nerd/Graphing</RepositoryUrl>
|
||||
<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>
|
||||
<IncludeSymbols>True</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<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 Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@ -20,4 +20,9 @@
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Forms\ViewCacheForm.resx">
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,4 +1,6 @@
|
||||
namespace Graphing;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Graphing;
|
||||
|
||||
public record struct Float2
|
||||
{
|
||||
|
||||
6
Base/Forms/Controls/PieChart.Designer.cs
generated
6
Base/Forms/Controls/PieChart.Designer.cs
generated
@ -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);
|
||||
|
||||
@ -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);
|
||||
// Draw the outline of each slice.
|
||||
// Only done if there is more than one slice.
|
||||
if (Values.Count > 1)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
29
Base/Forms/GraphColorPickerForm.Designer.cs
generated
29
Base/Forms/GraphColorPickerForm.Designer.cs
generated
@ -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;
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
16
Base/Forms/GraphForm.Designer.cs
generated
16
Base/Forms/GraphForm.Designer.cs
generated
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<IGraphPart> 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,12 +265,29 @@ 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (mouseDrag)
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
5
Base/Forms/SetZoomForm.Designer.cs
generated
5
Base/Forms/SetZoomForm.Designer.cs
generated
@ -1,4 +1,7 @@
|
||||
namespace Graphing.Forms
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Graphing.Forms
|
||||
{
|
||||
partial class SetZoomForm
|
||||
{
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
namespace Graphing.Forms;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Graphing.Forms;
|
||||
|
||||
public partial class SetZoomForm : Form
|
||||
{
|
||||
|
||||
5
Base/Forms/ViewCacheForm.Designer.cs
generated
5
Base/Forms/ViewCacheForm.Designer.cs
generated
@ -1,4 +1,7 @@
|
||||
namespace Graphing.Forms
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Graphing.Forms
|
||||
{
|
||||
partial class ViewCacheForm
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) { }
|
||||
}
|
||||
|
||||
@ -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<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 = [];
|
||||
|
||||
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);
|
||||
|
||||
239
Base/Graphables/IntegralEquation.cs
Normal file
239
Base/Graphables/IntegralEquation.cs
Normal 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));
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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<double, Float2> 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<IGraphPart> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
namespace Graphing;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Graphing;
|
||||
|
||||
public record struct Int2
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<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 />
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@ -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"
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<RootNamespace>Graphing.Testing</RootNamespace>
|
||||
<AssemblyName>ThatOneNerd.Graphing.Testing</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user