Merge pull request #1 from That-One-Nerd/canary

Version 1.1 is out.
This commit is contained in:
That_One_Nerd 2024-03-13 10:36:23 -04:00 committed by GitHub
commit a93b5855d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1124 additions and 199 deletions

View File

@ -12,9 +12,9 @@
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ThatOneNerd.Graphing</PackageId> <PackageId>ThatOneNerd.Graphing</PackageId>
<Title>ThatOneNerd.Graphing</Title> <Title>ThatOneNerd.Graphing</Title>
<Version>1.0.0</Version> <Version>1.1.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>
@ -22,6 +22,8 @@
<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:
https://github.com/That-One-Nerd/Graphing/releases/tag/1.1.0</PackageReleaseNotes>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>C:\Users\kyley\Desktop\Coding\C#\Graphing\Base\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Forms\Controls\PieChart.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Update="Forms\GraphColorPickerForm.cs"> <Compile Update="Forms\GraphColorPickerForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@ -10,5 +16,8 @@
<Compile Update="Forms\SetZoomForm.cs"> <Compile Update="Forms\SetZoomForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Update="Forms\ViewCacheForm.cs">
<SubType>Form</SubType>
</Compile>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,32 @@
namespace Graphing.Extensions;
public static class FormattingExtensions
{
private static readonly string[] sizeUnits =
[
" bytes",
" KB",
" MB",
" GB",
" TB",
" PB",
];
public static string FormatAsBytes(this long bytes)
{
double val = bytes;
int unitIndex = 0;
while (val > 1024)
{
unitIndex++;
val /= 1024;
}
string result;
if (unitIndex == 0) result = val.ToString("0");
else result = val.ToString("0.00");
return result + sizeUnits[unitIndex];
}
}

44
Base/Forms/Controls/PieChart.Designer.cs generated Normal file
View File

@ -0,0 +1,44 @@
namespace Graphing.Forms.Controls
{
partial class PieChart
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
SuspendLayout();
//
// PieChart
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
Name = "PieChart";
Size = new Size(500, 500);
ResumeLayout(false);
}
#endregion
}
}

View File

@ -0,0 +1,59 @@
using System.Drawing.Drawing2D;
namespace Graphing.Forms.Controls;
public partial class PieChart : UserControl
{
public List<(Color, double)> Values { get; set; }
public PieChart()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);
Values = [];
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
int size = Math.Min(Width, Height);
Rectangle rect = new(5, 5, size - 10, size - 10);
double sum = 0;
foreach ((Color, double v) item in Values)
sum += item.v;
// Draw them.
double current = 0;
foreach ((Color color, double value) item in Values)
{
double start = 360 * current / sum,
end = 360 * (current + item.value) / sum;
Brush filler = new SolidBrush(item.color);
g.FillPie(filler, rect, (float)start, (float)(end - start));
current += item.value;
}
// Draw the outline.
Pen outlinePartsPen = new(Color.FromArgb(unchecked((int)0xFF_202020)), 3);
current = 0;
foreach ((Color, double value) item in Values)
{
double start = 360 * current / sum,
end = 360 * (current + item.value) / sum;
g.DrawPie(outlinePartsPen, rect, (float)start, (float)(end - start));
current += item.value;
}
// Outline
Pen outlinePen = new(Color.FromArgb(unchecked((int)0xFF_202020)), 5);
g.DrawEllipse(outlinePen, rect);
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -39,6 +39,8 @@
MenuEquations = new ToolStripMenuItem(); MenuEquations = new ToolStripMenuItem();
MenuEquationsDerivative = new ToolStripMenuItem(); MenuEquationsDerivative = new ToolStripMenuItem();
MenuEquationsIntegral = new ToolStripMenuItem(); MenuEquationsIntegral = new ToolStripMenuItem();
MenuMisc = new ToolStripMenuItem();
MenuMiscCaches = new ToolStripMenuItem();
GraphMenu.SuspendLayout(); GraphMenu.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
@ -58,7 +60,7 @@
// GraphMenu // GraphMenu
// //
GraphMenu.ImageScalingSize = new Size(32, 32); GraphMenu.ImageScalingSize = new Size(32, 32);
GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuColors, MenuEquations }); GraphMenu.Items.AddRange(new ToolStripItem[] { MenuViewport, MenuColors, MenuEquations, MenuMisc });
GraphMenu.Location = new Point(0, 0); GraphMenu.Location = new Point(0, 0);
GraphMenu.Name = "GraphMenu"; GraphMenu.Name = "GraphMenu";
GraphMenu.Size = new Size(1449, 42); GraphMenu.Size = new Size(1449, 42);
@ -125,6 +127,20 @@
MenuEquationsIntegral.Size = new Size(360, 44); MenuEquationsIntegral.Size = new Size(360, 44);
MenuEquationsIntegral.Text = "Compute Integral"; MenuEquationsIntegral.Text = "Compute Integral";
// //
// MenuMisc
//
MenuMisc.DropDownItems.AddRange(new ToolStripItem[] { MenuMiscCaches });
MenuMisc.Name = "MenuMisc";
MenuMisc.Size = new Size(83, 38);
MenuMisc.Text = "Misc";
//
// MenuMiscCaches
//
MenuMiscCaches.Name = "MenuMiscCaches";
MenuMiscCaches.Size = new Size(359, 44);
MenuMiscCaches.Text = "View Caches";
MenuMiscCaches.Click += MenuMiscCaches_Click;
//
// GraphForm // GraphForm
// //
AutoScaleDimensions = new SizeF(13F, 32F); AutoScaleDimensions = new SizeF(13F, 32F);
@ -154,5 +170,7 @@
private ToolStripMenuItem MenuEquations; private ToolStripMenuItem MenuEquations;
private ToolStripMenuItem MenuEquationsDerivative; private ToolStripMenuItem MenuEquationsDerivative;
private ToolStripMenuItem MenuEquationsIntegral; private ToolStripMenuItem MenuEquationsIntegral;
private ToolStripMenuItem MenuMisc;
private ToolStripMenuItem MenuMiscCaches;
} }
} }

View File

@ -1,9 +1,16 @@
using Graphing.Graphables; using Graphing.Extensions;
using Graphing.Graphables;
using System.Drawing.Drawing2D;
using System.Text;
namespace Graphing.Forms; namespace Graphing.Forms;
public partial class GraphForm : Form 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 Float2 ScreenCenter { get; private set; } public Float2 ScreenCenter { get; private set; }
public Float2 Dpi { get; private set; } public Float2 Dpi { get; private set; }
@ -94,7 +101,7 @@ public partial class GraphForm : Form
double axisScale = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel))); double axisScale = Math.Pow(2, Math.Round(Math.Log2(ZoomLevel)));
// Draw horizontal/vertical quarter-axis. // Draw horizontal/vertical quarter-axis.
Brush quarterBrush = new SolidBrush(Color.FromArgb(unchecked((int)0xFF_E0E0E0))); Brush quarterBrush = new SolidBrush(QuarterAxisColor);
Pen quarterPen = new(quarterBrush, 2); Pen quarterPen = new(quarterBrush, 2);
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)
@ -111,7 +118,7 @@ public partial class GraphForm : Form
} }
// Draw horizontal/vertical semi-axis. // Draw horizontal/vertical semi-axis.
Brush semiBrush = new SolidBrush(Color.FromArgb(unchecked((int)0xFF_999999))); Brush semiBrush = new SolidBrush(SemiAxisColor);
Pen semiPen = new(semiBrush, 2); Pen semiPen = new(semiBrush, 2);
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)
@ -127,7 +134,7 @@ public partial class GraphForm : Form
g.DrawLine(semiPen, startPos, endPos); g.DrawLine(semiPen, startPos, endPos);
} }
Brush mainLineBrush = new SolidBrush(Color.Black); Brush mainLineBrush = new SolidBrush(MainAxisColor);
Pen mainLinePen = new(mainLineBrush, 3); Pen mainLinePen = new(mainLineBrush, 3);
// Draw the main axis (on top of the semi axis). // Draw the main axis (on top of the semi axis).
@ -143,6 +150,7 @@ public partial class GraphForm : Form
protected override void OnPaint(PaintEventArgs e) protected override void OnPaint(PaintEventArgs e)
{ {
Graphics g = e.Graphics; Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
Brush background = new SolidBrush(Color.White); Brush background = new SolidBrush(Color.White);
g.FillRectangle(background, e.ClipRectangle); g.FillRectangle(background, e.ClipRectangle);
@ -152,19 +160,9 @@ public partial class GraphForm : Form
// Draw the actual graphs. // Draw the actual graphs.
for (int i = 0; i < ables.Count; i++) for (int i = 0; i < ables.Count; i++)
{ {
IEnumerable<Line2d> 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);
Pen penBrush = new(graphBrush, 3); foreach (IGraphPart gp in lines) gp.Render(this, g, graphBrush);
foreach (Line2d l in lines)
{
if (!double.IsNormal(l.a.x) || !double.IsNormal(l.a.y) ||
!double.IsNormal(l.b.x) || !double.IsNormal(l.b.y)) continue;
Int2 start = GraphSpaceToScreenSpace(l.a),
end = GraphSpaceToScreenSpace(l.b);
g.DrawLine(penBrush, start, end);
}
} }
base.OnPaint(e); base.OnPaint(e);
@ -175,9 +173,9 @@ public partial class GraphForm : Form
Invalidate(false); Invalidate(false);
} }
public void Graph(Graphable able) public void Graph(params Graphable[] able)
{ {
ables.Add(able); ables.AddRange(able);
RegenerateMenuItems(); RegenerateMenuItems();
Invalidate(false); Invalidate(false);
} }
@ -257,14 +255,14 @@ 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) if (able is Equation equ)
{ {
ToolStripMenuItem derivativeItem = new() ToolStripMenuItem derivativeItem = new()
{ {
ForeColor = able.Color, ForeColor = able.Color,
Text = able.Name Text = able.Name
}; };
derivativeItem.Click += (o, e) => EquationComputeDerivative_Click((able as Equation)!); derivativeItem.Click += (o, e) => EquationComputeDerivative_Click(equ);
MenuEquationsDerivative.DropDownItems.Add(derivativeItem); MenuEquationsDerivative.DropDownItems.Add(derivativeItem);
ToolStripMenuItem integralItem = new() ToolStripMenuItem integralItem = new()
@ -272,7 +270,7 @@ public partial class GraphForm : Form
ForeColor = able.Color, ForeColor = able.Color,
Text = able.Name Text = able.Name
}; };
integralItem.Click += (o, e) => EquationComputeIntegral_Click((able as Equation)!); integralItem.Click += (o, e) => EquationComputeIntegral_Click(equ);
MenuEquationsIntegral.DropDownItems.Add(integralItem); MenuEquationsIntegral.DropDownItems.Add(integralItem);
} }
} }
@ -346,7 +344,7 @@ public partial class GraphForm : Form
static double Integrate(EquationDelegate e, double lower, double upper) static double Integrate(EquationDelegate e, double lower, double upper)
{ {
// TODO: a better rendering method could make this much faster. // TODO: a better rendering method could make this much faster.
const double step = 1e-1; const double step = 1e-2;
double factor = 1; double factor = 1;
if (upper < lower) if (upper < lower)
@ -364,4 +362,16 @@ public partial class GraphForm : Form
return sum * factor; return sum * factor;
} }
} }
private void MenuMiscCaches_Click(object? sender, EventArgs e)
{
ViewCacheForm cacheForm = new(this)
{
StartPosition = FormStartPosition.Manual
};
cacheForm.Location = new Point(Location.X + ClientRectangle.Width + 10,
Location.Y + (ClientRectangle.Height - cacheForm.ClientRectangle.Height) / 2);
cacheForm.Show();
}
} }

View File

@ -1,131 +1,119 @@
using System; namespace Graphing.Forms;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Graphing.Forms public partial class SetZoomForm : Form
{ {
public partial class SetZoomForm : Form private double minZoomRange;
private double maxZoomRange;
private double zoomLevel;
private readonly GraphForm form;
public SetZoomForm(GraphForm form)
{ {
private double minZoomRange; InitializeComponent();
private double maxZoomRange;
private double zoomLevel; minZoomRange = 1 / (form.ZoomLevel * 2);
maxZoomRange = 2 / form.ZoomLevel;
zoomLevel = 1 / form.ZoomLevel;
private readonly GraphForm form; ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum);
public SetZoomForm(GraphForm form) this.form = form;
}
protected override void OnPaint(PaintEventArgs e)
{
ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
ZoomMinValue.Text = minZoomRange.ToString("0.00");
ValueLabel.Text = $"{zoomLevel:0.00}x";
base.OnPaint(e);
form.ZoomLevel = 1 / zoomLevel;
form.Invalidate(false);
}
private double FactorToZoom(double factor)
{
return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange);
}
private double ZoomToFactor(double zoom)
{
double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange);
return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue));
}
private void ZoomTrackBar_Scroll(object? sender, EventArgs e)
{
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
zoomLevel = FactorToZoom(factor);
Invalidate(true);
}
private void ZoomMinValue_TextChanged(object? sender, EventArgs e)
{
double original = minZoomRange;
try
{ {
InitializeComponent(); double value;
if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) ||
ZoomMinValue.Text.EndsWith('.'))
{
return;
}
else
{
value = double.Parse(ZoomMinValue.Text);
if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new();
}
minZoomRange = 1 / (form.ZoomLevel * 2); minZoomRange = value;
maxZoomRange = 2 / form.ZoomLevel; ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum);
zoomLevel = 1 / form.ZoomLevel;
ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum);
this.form = form;
}
protected override void OnPaint(PaintEventArgs e)
{
ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
ZoomMinValue.Text = minZoomRange.ToString("0.00");
ValueLabel.Text = $"{zoomLevel:0.00}x";
base.OnPaint(e);
form.ZoomLevel = 1 / zoomLevel;
form.Invalidate(false);
}
private double FactorToZoom(double factor)
{
return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange);
}
private double ZoomToFactor(double zoom)
{
double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange);
return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue));
}
private void ZoomTrackBar_Scroll(object? sender, EventArgs e)
{
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
zoomLevel = FactorToZoom(factor); double newZoom = FactorToZoom(factor);
Invalidate(true); zoomLevel = newZoom;
if (newZoom != factor) Invalidate(true);
} }
catch
private void ZoomMinValue_TextChanged(object? sender, EventArgs e)
{ {
double original = minZoomRange; minZoomRange = original;
try ZoomMinValue.Text = minZoomRange.ToString("0.00");
{
double value;
if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) ||
ZoomMinValue.Text.EndsWith('.'))
{
return;
}
else
{
value = double.Parse(ZoomMinValue.Text);
if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new();
}
minZoomRange = value;
ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum);
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
double newZoom = FactorToZoom(factor);
zoomLevel = newZoom;
if (newZoom != factor) Invalidate(true);
}
catch
{
minZoomRange = original;
ZoomMinValue.Text = minZoomRange.ToString("0.00");
}
} }
}
private void ZoomMaxValue_TextChanged(object sender, EventArgs e) private void ZoomMaxValue_TextChanged(object sender, EventArgs e)
{
double original = maxZoomRange;
try
{ {
double original = maxZoomRange; double value;
try if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) ||
ZoomMaxValue.Text.EndsWith('.'))
{ {
double value; return;
if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) ||
ZoomMaxValue.Text.EndsWith('.'))
{
return;
}
else
{
value = double.Parse(ZoomMaxValue.Text);
if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new();
}
maxZoomRange = value;
ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum);
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
double newZoom = FactorToZoom(factor);
zoomLevel = newZoom;
if (newZoom != factor) Invalidate(true);
} }
catch else
{ {
maxZoomRange = original; value = double.Parse(ZoomMaxValue.Text);
ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new();
} }
maxZoomRange = value;
ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum);
double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum);
double newZoom = FactorToZoom(factor);
zoomLevel = newZoom;
if (newZoom != factor) Invalidate(true);
}
catch
{
maxZoomRange = original;
ZoomMaxValue.Text = maxZoomRange.ToString("0.00");
} }
} }
} }

97
Base/Forms/ViewCacheForm.Designer.cs generated Normal file
View File

@ -0,0 +1,97 @@
namespace Graphing.Forms
{
partial class ViewCacheForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ViewCacheForm));
CachePie = new Controls.PieChart();
TotalCacheText = new Label();
EraseAllCacheButton = new Button();
SpecificCachePanel = new Panel();
SuspendLayout();
//
// CachePie
//
CachePie.Location = new Point(50, 50);
CachePie.Name = "CachePie";
CachePie.Size = new Size(450, 450);
CachePie.TabIndex = 0;
//
// TotalCacheText
//
TotalCacheText.Font = new Font("Segoe UI Semibold", 10.125F, FontStyle.Bold, GraphicsUnit.Point, 0);
TotalCacheText.Location = new Point(62, 540);
TotalCacheText.Name = "TotalCacheText";
TotalCacheText.Size = new Size(425, 45);
TotalCacheText.TabIndex = 1;
TotalCacheText.Text = "Total Cache: Something";
TotalCacheText.TextAlign = ContentAlignment.TopCenter;
//
// EraseAllCacheButton
//
EraseAllCacheButton.Location = new Point(200, 580);
EraseAllCacheButton.Name = "EraseAllCacheButton";
EraseAllCacheButton.Size = new Size(150, 46);
EraseAllCacheButton.TabIndex = 2;
EraseAllCacheButton.Text = "Erase All";
EraseAllCacheButton.UseVisualStyleBackColor = true;
EraseAllCacheButton.Click += EraseAllCacheButton_Click;
//
// SpecificCachePanel
//
SpecificCachePanel.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
SpecificCachePanel.AutoScroll = true;
SpecificCachePanel.Location = new Point(520, 12);
SpecificCachePanel.Name = "SpecificCachePanel";
SpecificCachePanel.Size = new Size(542, 657);
SpecificCachePanel.TabIndex = 3;
//
// ViewCacheForm
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1074, 679);
Controls.Add(SpecificCachePanel);
Controls.Add(EraseAllCacheButton);
Controls.Add(TotalCacheText);
Controls.Add(CachePie);
FormBorderStyle = FormBorderStyle.SizableToolWindow;
MinimumSize = new Size(885, 750);
Name = "ViewCacheForm";
Text = "Graph Caches";
ResumeLayout(false);
}
#endregion
private Controls.PieChart CachePie;
private Label TotalCacheText;
private Button EraseAllCacheButton;
private Panel SpecificCachePanel;
}
}

View File

@ -0,0 +1,89 @@
using Graphing.Extensions;
namespace Graphing.Forms;
public partial class ViewCacheForm : Form
{
private readonly GraphForm refForm;
private readonly List<Label> labelCache;
private readonly List<Button> buttonCache;
public ViewCacheForm(GraphForm thisForm)
{
InitializeComponent();
refForm = thisForm;
refForm.Paint += (o, e) => UpdatePieChart();
labelCache = [];
buttonCache = [];
UpdatePieChart();
}
private void UpdatePieChart()
{
CachePie.Values.Clear();
long totalBytes = 0;
int index = 0;
foreach (Graphable able in refForm.Graphables)
{
long thisBytes = able.GetCacheBytes();
CachePie.Values.Add((able.Color, thisBytes));
totalBytes += thisBytes;
if (index < labelCache.Count)
{
Label reuseLabel = labelCache[index];
reuseLabel.ForeColor = able.Color;
reuseLabel.Text = $"{able.Name}: {thisBytes.FormatAsBytes()}";
}
else
{
Label newText = new()
{
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right,
AutoEllipsis = true,
ForeColor = able.Color,
Location = new Point(0, labelCache.Count * 46),
Parent = SpecificCachePanel,
Size = new Size(SpecificCachePanel.Width - 98, 46),
Text = $"{able.Name}: {thisBytes.FormatAsBytes()}",
TextAlign = ContentAlignment.MiddleLeft,
};
labelCache.Add(newText);
}
if (index >= buttonCache.Count)
{
Button newButton = new()
{
Anchor = AnchorStyles.Top | AnchorStyles.Right,
Location = new Point(SpecificCachePanel.Width - 92, buttonCache.Count * 46),
Parent = SpecificCachePanel,
Size = new Size(92, 46),
Text = "Clear"
};
newButton.Click += (o, e) => EraseSpecificGraphable_Click(able);
buttonCache.Add(newButton);
}
index++;
}
TotalCacheText.Text = $"Total Cache: {totalBytes.FormatAsBytes()}";
Invalidate(true);
}
private void EraseAllCacheButton_Click(object? sender, EventArgs e)
{
foreach (Graphable able in refForm.Graphables) able.EraseCache();
refForm.Invalidate(false);
}
private void EraseSpecificGraphable_Click(Graphable able)
{
able.EraseCache();
refForm.Invalidate(false);
}
}

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CachePie.Values" mimetype="application/x-microsoft.net.object.binary.base64">
<value>
AAEAAAD/////AQAAAAAAAAAEAQAAAM0CU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTGlzdGAxW1tT
eXN0ZW0uVmFsdWVUdXBsZWAyW1tTeXN0ZW0uRHJhd2luZy5Db2xvciwgU3lzdGVtLkRyYXdpbmcsIFZl
cnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iMDNmNWY3ZjExZDUw
YTNhXSxbU3lzdGVtLkRvdWJsZSwgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0
cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0sIG1zY29ybGliLCBWZXJzaW9uPTQu
MC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dAwAA
AAZfaXRlbXMFX3NpemUIX3ZlcnNpb24DAADdAVN5c3RlbS5WYWx1ZVR1cGxlYDJbW1N5c3RlbS5EcmF3
aW5nLkNvbG9yLCBTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWws
IFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2FdLFtTeXN0ZW0uRG91YmxlLCBtc2NvcmxpYiwg
VmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkz
NGUwODldXVtdCAgJAgAAAAAAAAAAAAAABwIAAAAAAQAAAAAAAAAD2wFTeXN0ZW0uVmFsdWVUdXBsZWAy
W1tTeXN0ZW0uRHJhd2luZy5Db2xvciwgU3lzdGVtLkRyYXdpbmcsIFZlcnNpb249NC4wLjAuMCwgQ3Vs
dHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iMDNmNWY3ZjExZDUwYTNhXSxbU3lzdGVtLkRvdWJs
ZSwgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tl
bj1iNzdhNWM1NjE5MzRlMDg5XV0L
</value>
</data>
</root>

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts;
namespace Graphing; namespace Graphing;
@ -26,5 +27,10 @@ public abstract class Graphable
Name = "Unnamed Graphable."; Name = "Unnamed Graphable.";
} }
public abstract IEnumerable<Line2d> GetItemsToRender(in GraphForm graph); public abstract IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph);
public abstract Graphable DeepCopy();
public abstract void EraseCache();
public abstract long GetCacheBytes();
} }

View File

@ -0,0 +1,51 @@
using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables;
public class ColumnTable : Graphable
{
private static int tableNum;
protected readonly Dictionary<double, double> tableXY;
protected readonly double width;
public ColumnTable(double width, Dictionary<double, double> tableXY)
{
tableNum++;
Name = $"Column Table {tableNum}";
this.tableXY = tableXY;
this.width = width;
}
public ColumnTable(double step, Equation equation, double min, double max)
{
Name = $"Column Table for {equation.Name}";
tableXY = [];
EquationDelegate equ = equation.GetDelegate();
width = 0.75 * step;
double minRounded = Math.Round(min / step) * step,
maxRounded = Math.Round(max / step) * step;
for (double x = minRounded; x <= maxRounded; x += step)
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());
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
List<IGraphPart> items = [];
foreach (KeyValuePair<double, double> col in tableXY)
{
items.Add(GraphRectangle.FromSize(new Float2(col.Key, col.Value / 2),
new Float2(width, col.Value)));
}
return items;
}
}

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -6,7 +7,8 @@ public class Equation : Graphable
{ {
private static int equationNum; private static int equationNum;
private readonly EquationDelegate equ; protected readonly EquationDelegate equ;
protected readonly List<Float2> cache;
public Equation(EquationDelegate equ) public Equation(EquationDelegate equ)
{ {
@ -14,20 +16,27 @@ public class Equation : Graphable
Name = $"Equation {equationNum}"; Name = $"Equation {equationNum}";
this.equ = equ; this.equ = equ;
cache = [];
} }
public override IEnumerable<Line2d> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
List<Line2d> lines = []; const int step = 10;
double epsilon = Math.Abs(graph.ScreenSpaceToGraphSpace(new Int2(0, 0)).x
- graph.ScreenSpaceToGraphSpace(new Int2(step / 2, 0)).x) / 5;
List<IGraphPart> lines = [];
double previousX = graph.MinVisibleGraph.x; double previousX = graph.MinVisibleGraph.x;
double previousY = equ(previousX); double previousY = GetFromCache(previousX, epsilon);
for (int i = 1; i < graph.ClientRectangle.Width; i += 10)
for (int i = 1; i < graph.ClientRectangle.Width; i += step)
{ {
double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x; double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x;
double currentY = equ(currentX); double currentY = GetFromCache(currentX, epsilon);
if (Math.Abs(currentY - previousY) <= 10) if (Math.Abs(currentY - previousY) <= 10)
{ {
lines.Add(new Line2d(new Float2(previousX, previousY), new Float2(currentX, currentY))); lines.Add(new GraphLine(new Float2(previousX, previousY), new Float2(currentX, currentY)));
} }
previousX = currentX; previousX = currentX;
previousY = currentY; previousY = currentY;
@ -36,6 +45,57 @@ public class Equation : Graphable
} }
public EquationDelegate GetDelegate() => equ; public EquationDelegate GetDelegate() => equ;
public override void EraseCache() => cache.Clear();
protected double GetFromCache(double x, double epsilon)
{
(double dist, double nearest, int index) = NearestCachedPoint(x);
if (dist < epsilon) return nearest;
else
{
double result = equ(x);
cache.Insert(index + 1, new(x, result));
return result;
}
}
// 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);
else if (cache.Count == 1)
{
Float2 single = cache[0];
return (Math.Abs(single.x - x), single.y, 0);
}
else
{
int boundA = 0, boundB = cache.Count;
do
{
int boundC = (boundA + boundB) / 2;
Float2 pointC = cache[boundC];
if (pointC.x == x) return (0, pointC.y, boundC);
else if (pointC.x > x)
{
boundA = boundC;
}
else // pointC.x < x
{
boundB = boundC;
}
} while (boundB - boundA > 1);
return (Math.Abs(cache[boundA].x - x), cache[boundA].y, boundA);
}
}
public override Graphable DeepCopy() => new Equation(equ);
public override long GetCacheBytes() => cache.Count * 16;
} }
public delegate double EquationDelegate(double x); public delegate double EquationDelegate(double x);

View File

@ -1,4 +1,5 @@
using Graphing.Forms; using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables; namespace Graphing.Graphables;
@ -6,8 +7,10 @@ public class SlopeField : Graphable
{ {
private static int slopeFieldNum; private static int slopeFieldNum;
private readonly SlopeFieldsDelegate equ; protected readonly SlopeFieldsDelegate equ;
private readonly double detail; protected readonly int detail;
protected readonly List<(Float2, GraphLine)> cache;
public SlopeField(int detail, SlopeFieldsDelegate equ) public SlopeField(int detail, SlopeFieldsDelegate equ)
{ {
@ -16,25 +19,26 @@ public class SlopeField : Graphable
this.equ = equ; this.equ = equ;
this.detail = detail; this.detail = detail;
cache = [];
} }
public override IEnumerable<Line2d> GetItemsToRender(in GraphForm graph) public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{ {
List<Line2d> lines = []; double epsilon = 1 / (detail * 2.0);
List<IGraphPart> lines = [];
for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1 / detail) for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1.0 / detail)
{ {
for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1 / detail) for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1.0 / detail)
{ {
double slope = equ(x, y); lines.Add(GetFromCache(epsilon, x, y));
lines.Add(MakeSlopeLine(new Float2(x, y), slope));
} }
} }
return lines; return lines;
} }
private Line2d MakeSlopeLine(Float2 position, double slope) protected GraphLine MakeSlopeLine(Float2 position, double slope)
{ {
double size = detail; double size = detail;
@ -46,6 +50,30 @@ public class SlopeField : Graphable
return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY)); return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY));
} }
protected GraphLine GetFromCache(double epsilon, double x, double y)
{
// Probably no binary search here, though maybe it could be done
// in terms of just one axis.
foreach ((Float2 p, GraphLine l) in cache)
{
double diffX = Math.Abs(p.x - x),
diffY = Math.Abs(p.y - y);
if (diffX < epsilon && diffY < epsilon) return l;
}
// Create a new value.
double slope = equ(x, y);
GraphLine result = MakeSlopeLine(new Float2(x, y), slope);
cache.Add((new Float2(x, y), result));
return result;
}
public override Graphable DeepCopy() => new SlopeField(detail, equ);
public override void EraseCache() => cache.Clear();
public override long GetCacheBytes() => cache.Count * 48;
} }
public delegate double SlopeFieldsDelegate(double x, double y); public delegate double SlopeFieldsDelegate(double x, double y);

View File

@ -0,0 +1,51 @@
using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables;
public class TangentLine : Graphable
{
public double Position { get; set; }
protected readonly Equation parent;
protected readonly EquationDelegate parentEqu;
protected readonly double length;
public TangentLine(double length, double position, Equation parent)
{
Name = $"Tangent Line of {parent.Name}";
parentEqu = parent.GetDelegate();
Position = position;
this.length = length;
this.parent = parent;
}
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
Float2 point = new(Position, parentEqu(Position));
return [MakeSlopeLine(point, DerivativeAtPoint(Position)),
new GraphUiCircle(point, 8)];
}
protected GraphLine MakeSlopeLine(Float2 position, double slope)
{
double dirX = length, dirY = slope * 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));
}
protected double DerivativeAtPoint(double x)
{
const double step = 1e-3;
return (parentEqu(x + step) - parentEqu(x)) / step;
}
public override Graphable DeepCopy() => new TangentLine(length, Position, parent);
public override void EraseCache() { }
public override long GetCacheBytes() => 0;
}

8
Base/IGraphPart.cs Normal file
View File

@ -0,0 +1,8 @@
using Graphing.Forms;
namespace Graphing;
public interface IGraphPart
{
public void Render(in GraphForm form, in Graphics g, in Brush brush);
}

View File

@ -1,18 +0,0 @@
namespace Graphing;
public record struct Line2d
{
public Float2 a;
public Float2 b;
public Line2d()
{
a = new();
b = new();
}
public Line2d(Float2 a, Float2 b)
{
this.a = a;
this.b = b;
}
}

32
Base/Parts/GraphLine.cs Normal file
View File

@ -0,0 +1,32 @@
using Graphing.Forms;
namespace Graphing.Parts;
public record struct GraphLine : IGraphPart
{
public Float2 a;
public Float2 b;
public GraphLine()
{
a = new();
b = new();
}
public GraphLine(Float2 a, Float2 b)
{
this.a = a;
this.b = b;
}
public readonly void Render(in GraphForm form, in Graphics g, in Brush brush)
{
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);
}
}

View File

@ -0,0 +1,45 @@
using Graphing.Forms;
namespace Graphing.Parts;
public record struct GraphRectangle : IGraphPart
{
public Float2 min, max;
public GraphRectangle()
{
min = new();
max = new();
}
public static GraphRectangle FromSize(Float2 center, Float2 size) => new()
{
min = new(center.x - size.x / 2,
center.y - size.y / 2),
max = new(center.x + size.x / 2,
center.y + size.y / 2)
};
public static GraphRectangle FromRange(Float2 min, Float2 max) => new()
{
min = min,
max = max
};
public void Render(in GraphForm form, in Graphics g, in Brush brush)
{
if (!double.IsFinite(max.x) || !double.IsFinite(max.y) ||
!double.IsFinite(min.x) || !double.IsFinite(min.y)) return;
if (min.x > max.x) (min.x, max.x) = (max.x, min.x);
if (min.y > max.y) (min.y, max.y) = (max.y, min.y);
Int2 start = form.GraphSpaceToScreenSpace(min),
end = form.GraphSpaceToScreenSpace(max);
Int2 size = new(end.x - start.x + 1,
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));
}
}

View File

@ -0,0 +1,31 @@
using Graphing.Forms;
namespace Graphing.Parts;
public record struct GraphUiCircle : IGraphPart
{
public Float2 center;
public int radius;
public GraphUiCircle()
{
center = new();
radius = 1;
}
public GraphUiCircle(Float2 center, int radius)
{
this.center = center;
this.radius = radius;
}
public readonly void Render(in GraphForm form, in Graphics g, in Brush brush)
{
if (!double.IsFinite(center.x) || !double.IsFinite(center.y) ||
!double.IsFinite(radius) || radius == 0) return;
Int2 centerPix = form.GraphSpaceToScreenSpace(center);
g.FillEllipse(brush, new Rectangle(new Point(centerPix.x - radius,
centerPix.y - radius),
new Size(radius * 2, radius * 2)));
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0-windows\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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>
<LastFailureDetails />
</PropertyGroup>
</Project>

View File

@ -1,27 +0,0 @@
namespace Graphing;
public record struct Range2d
{
public double minX;
public double minY;
public double maxX;
public double maxY;
public Range2d()
{
minX = 0;
minY = 0;
maxX = 0;
maxY = 0;
}
public Range2d(double minX, double minY, double maxX, double maxY)
{
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
public readonly bool Contains(Float2 p) =>
p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY;
}

View File

@ -10,10 +10,31 @@ internal static class Program
{ {
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); Application.SetHighDpiMode(HighDpiMode.SystemAware);
GraphForm graph = new("One Of The Graphing Calculators Of All Time"); GraphForm graph = new("One Of The Graphing Calculators Of All Time");
graph.Graph(new Equation(Math.Cos));
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.
// 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.
Application.Run(graph); Application.Run(graph);
} }

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
@ -9,6 +8,14 @@
<RootNamespace>Graphing.Testing</RootNamespace> <RootNamespace>Graphing.Testing</RootNamespace>
<AssemblyName>ThatOneNerd.Graphing.Testing</AssemblyName> <AssemblyName>ThatOneNerd.Graphing.Testing</AssemblyName>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputType>Exe</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputType>WinExe</OutputType>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" /> <ProjectReference Include="..\Base\Base.csproj" />