Various things. Added a tangent line system, allows for different graph parts, and more caching stuff.

This commit is contained in:
That_One_Nerd 2024-02-29 10:44:28 -05:00
parent 21c498f445
commit e31e6bfdb6
11 changed files with 294 additions and 179 deletions

View File

@ -1,5 +1,7 @@
using Graphing.Extensions;
using Graphing.Graphables;
using Graphing.Parts;
using System.Drawing.Drawing2D;
using System.Text;
namespace Graphing.Forms;
@ -145,6 +147,7 @@ public partial class GraphForm : Form
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
Brush background = new SolidBrush(Color.White);
g.FillRectangle(background, e.ClipRectangle);
@ -154,19 +157,9 @@ public partial class GraphForm : Form
// Draw the actual graphs.
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);
Pen penBrush = new(graphBrush, 3);
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);
}
foreach (IGraphPart gp in lines) gp.Render(this, g, graphBrush);
}
base.OnPaint(e);
@ -375,24 +368,17 @@ public partial class GraphForm : Form
long total = 0;
foreach (Graphable able in ables)
{
if (able is Equation equ)
{
long size = equ.GetCacheBytes();
long size = able.GetCacheBytes();
message.AppendLine($"{able.Name}: {size.FormatAsBytes()}");
total += size;
}
}
message.AppendLine($"\nTotal: {total.FormatAsBytes()}\n\nClick \"No\" to erase caches.");
message.AppendLine($"\nTotal: {total.FormatAsBytes()}\n\nErase cache?");
DialogResult result = MessageBox.Show(message.ToString(), "Graph Caches", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
if (result == DialogResult.No)
if (result == DialogResult.Yes)
{
foreach (Graphable able in ables)
{
if (able is Equation equ) equ.EraseCache();
}
foreach (Graphable able in ables) able.EraseCache();
}
}
}

View File

@ -1,16 +1,5 @@
using System;
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;
namespace Graphing.Forms
{
public partial class SetZoomForm : Form
{
private double minZoomRange;
@ -128,4 +117,3 @@ namespace Graphing.Forms
}
}
}
}

View File

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

View File

@ -1,4 +1,5 @@
using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables;
@ -19,73 +20,73 @@ public class Equation : Graphable
cache = [];
}
public override IEnumerable<Line2d> GetItemsToRender(in GraphForm graph)
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;
List<Line2d> lines = [];
List<IGraphPart> lines = [];
bool addedToDictionary = false;
double previousX = graph.MinVisibleGraph.x;
double previousY = GetFromCache(previousX, epsilon, ref addedToDictionary);
double previousY = GetFromCache(previousX, epsilon);
for (int i = 1; i < graph.ClientRectangle.Width; i += step)
{
double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x;
double currentY = GetFromCache(currentX, epsilon, ref addedToDictionary);
double currentY = GetFromCache(currentX, epsilon);
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;
previousY = currentY;
}
if (addedToDictionary) cache.Sort((a, b) => a.y.CompareTo(b.y));
// if (addedToDictionary) cache.Sort((a, b) => a.y.CompareTo(b.y)); todo: not required until binary search
return lines;
}
public EquationDelegate GetDelegate() => equ;
public void EraseCache() => cache.Clear();
private double GetFromCache(double x, double epsilon, ref bool addedToDictionary)
public override void EraseCache() => cache.Clear();
private double GetFromCache(double x, double epsilon)
{
(double dist, double nearest) = NearestCachedPoint(x);
(double dist, double nearest, int index) = NearestCachedPoint(x);
if (dist < epsilon) return nearest;
else
{
addedToDictionary = true;
double result = equ(x);
cache.Add(new(x, result));
// TODO: Rather than sorting the whole list when we add a single number,
// we could just insert it.
// we could just insert it in its proper place.
return result;
}
}
private (double dist, double y) NearestCachedPoint(double x)
private (double dist, double y, int index) NearestCachedPoint(double x)
{
// TODO: Replace with a binary search system.
double closestDist = double.PositiveInfinity;
double closest = 0;
int closestIndex = -1;
foreach (Float2 p in cache)
for (int i = 0; i < cache.Count; i++)
{
double dist = Math.Abs(x - p.x);
double dist = Math.Abs(x - cache[i].x);
if (dist < closestDist)
{
closestDist = dist;
closest = p.y;
closest = cache[i].y;
closestIndex = i;
}
}
return (closestDist, closest);
return (closestDist, closest, closestIndex);
}
public long GetCacheBytes() => cache.Count * 16;
public override long GetCacheBytes() => cache.Count * 16;
}
public delegate double EquationDelegate(double x);

View File

@ -1,4 +1,6 @@
using Graphing.Forms;
using Graphing.Parts;
using static System.Windows.Forms.LinkLabel;
namespace Graphing.Graphables;
@ -9,6 +11,8 @@ public class SlopeField : Graphable
private readonly SlopeFieldsDelegate equ;
private readonly double detail;
private readonly List<(Float2, GraphLine)> cache;
public SlopeField(int detail, SlopeFieldsDelegate equ)
{
slopeFieldNum++;
@ -16,25 +20,26 @@ public class SlopeField : Graphable
this.equ = equ;
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);
List<IGraphPart> lines = [];
for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1 / detail)
{
for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1 / detail)
{
double slope = equ(x, y);
lines.Add(MakeSlopeLine(new Float2(x, y), slope));
lines.Add(GetFromCache(epsilon, x, y));
}
}
return lines;
}
private Line2d MakeSlopeLine(Float2 position, double slope)
private GraphLine MakeSlopeLine(Float2 position, double slope)
{
double size = detail;
@ -46,6 +51,28 @@ public class SlopeField : Graphable
return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY));
}
private 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 void EraseCache() => cache.Clear();
public override long GetCacheBytes() => cache.Count * 48;
}
public delegate double SlopeFieldsDelegate(double x, double y);

View File

@ -0,0 +1,47 @@
using Graphing.Forms;
using Graphing.Parts;
namespace Graphing.Graphables;
public class TangentLine : Graphable
{
public double Position { get; set; }
private readonly EquationDelegate parentEqu;
private 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;
}
public override IEnumerable<IGraphPart> GetItemsToRender(in GraphForm graph)
{
Float2 point = new(Position, parentEqu(Position));
return [MakeSlopeLine(point, DerivativeAtPoint(Position)),
new GraphCircle(point, 8)];
}
private 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));
}
private double DerivativeAtPoint(double x)
{
const double step = 1e-3;
return (parentEqu(x + step) - parentEqu(x)) / step;
}
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;
}
}

31
Base/Parts/GraphCircle.cs Normal file
View File

@ -0,0 +1,31 @@
using Graphing.Forms;
namespace Graphing.Parts;
public record struct GraphCircle : IGraphPart
{
public Float2 center;
public int radius;
public GraphCircle()
{
center = new();
radius = 1;
}
public GraphCircle(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.IsNormal(center.x) || !double.IsNormal(center.y) ||
!double.IsNormal(radius)) 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)));
}
}

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.IsNormal(a.x) || !double.IsNormal(a.y) ||
!double.IsNormal(b.x) || !double.IsNormal(b.y)) return;
Int2 start = form.GraphSpaceToScreenSpace(a),
end = form.GraphSpaceToScreenSpace(b);
Pen pen = new(brush, 3);
g.DrawLine(pen, start, end);
}
}

View File

@ -10,12 +10,21 @@ internal static class Program
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Application.SetHighDpiMode(HighDpiMode.SystemAware);
GraphForm graph = new("One Of The Graphing Calculators Of All Time");
graph.Graph(new Equation(x => Math.Pow(2, x)));
graph.Graph(new Equation(Math.Log2));
Equation equ = new(x => x * x);
TangentLine tangent = new(5, 2, equ);
graph.Graph(equ);
graph.Graph(tangent);
Application.Run(graph);
}
private static double PopulationGraph(double max, double k, double A, double t)
{
return max / (1 + A * Math.Exp(-k * t));
}
}